Repository: freedomofpress/securedrop

Issue:
[xenial] Ensure TestInfra test completeness on Trusty, Xenial
By design, the TestInfra config test suite runs slightly different checks for Trusty and Xenial. Care should be taken to preserve functionality of the config tests against both releases. Please note omissions or test failures in this ticket; we can then scope new issues as appropriate.

Specifically, config tests should pass for the following scenarios:

- Clean Trusty install (done)
- Clean Xenial install (likely done)
- Trusty -> Xenial upgrade (not done)

Part of #3204.



Discussion:

Resolving patch:
diff --git a/securedrop/journalist_app/__init__.py b/securedrop/journalist_app/__init__.py
--- a/securedrop/journalist_app/__init__.py
+++ b/securedrop/journalist_app/__init__.py
@@ -40,7 +40,7 @@ def create_app(config):
                 template_folder=config.JOURNALIST_TEMPLATES_DIR,
                 static_folder=path.join(config.SECUREDROP_ROOT, 'static'))
 
-    app.config.from_object(config.JournalistInterfaceFlaskConfig)
+    app.config.from_object(config.JournalistInterfaceFlaskConfig)  # type: ignore
     app.sdconfig = config
     app.session_interface = JournalistInterfaceSessionInterface()
 
diff --git a/securedrop/source_app/__init__.py b/securedrop/source_app/__init__.py
--- a/securedrop/source_app/__init__.py
+++ b/securedrop/source_app/__init__.py
@@ -37,7 +37,7 @@ def create_app(config):
                 template_folder=config.SOURCE_TEMPLATES_DIR,
                 static_folder=path.join(config.SECUREDROP_ROOT, 'static'))
     app.request_class = RequestThatSecuresFileUploads
-    app.config.from_object(config.SourceInterfaceFlaskConfig)
+    app.config.from_object(config.SourceInterfaceFlaskConfig)  # type: ignore
     app.sdconfig = config
 
     # The default CSRF token expiration is 1 hour. Since large uploads can


Test patch:
diff --git a/molecule/builder-trusty/tests/test_build_dependencies.py b/molecule/builder-trusty/tests/test_build_dependencies.py
--- a/molecule/builder-trusty/tests/test_build_dependencies.py
+++ b/molecule/builder-trusty/tests/test_build_dependencies.py
@@ -8,43 +8,43 @@
 ]
 
 
-def test_pip_wheel_installed(Command):
+def test_pip_wheel_installed(host):
     """
     Ensure `wheel` is installed via pip, for packaging Python
     dependencies into a Debian package.
     """
-    c = Command("pip list installed")
+    c = host.run("pip list installed")
     assert "wheel" in c.stdout
     assert c.rc == 0
 
 
-def test_sass_gem_installed(Command):
+def test_sass_gem_installed(host):
     """
     Ensure the `sass` Ruby gem is installed, for compiling SASS to CSS.
     """
-    c = Command("gem list")
+    c = host.run("gem list")
     assert "sass (3.4.23)" in c.stdout
     assert c.rc == 0
 
 
-def test_pip_dependencies_installed(Command):
+def test_pip_dependencies_installed(host):
     """
     Ensure the development pip dependencies are installed
     """
-    c = Command("pip list installed")
+    c = host.run("pip list installed")
     assert "Flask-Babel" in c.stdout
     assert c.rc == 0
 
 
 @pytest.mark.xfail(reason="This check conflicts with the concept of pegging"
                           "dependencies")
-def test_build_all_packages_updated(Command):
+def test_build_all_packages_updated(host):
     """
     Ensure a dist-upgrade has already been run, by checking that no
     packages are eligible for upgrade currently. This will ensure that
     all upgrades, security and otherwise, have been applied to the VM
     used to build packages.
     """
-    c = Command('aptitude --simulate -y dist-upgrade')
+    c = host.run('aptitude --simulate -y dist-upgrade')
     assert c.rc == 0
     assert "No packages will be installed, upgraded, or removed." in c.stdout
diff --git a/molecule/builder-trusty/tests/test_legacy_paths.py b/molecule/builder-trusty/tests/test_legacy_paths.py
--- a/molecule/builder-trusty/tests/test_legacy_paths.py
+++ b/molecule/builder-trusty/tests/test_legacy_paths.py
@@ -9,7 +9,7 @@
     '/tmp/build-securedrop-ossec-agent',
     '/tmp/build-securedrop-ossec-server',
 ])
-def test_build_ossec_apt_dependencies(File, build_path):
+def test_build_ossec_apt_dependencies(host, build_path):
     """
     Ensure that unwanted build paths are absent. Most of these were created
     as unwanted side-effects during CI-related changes to the build scripts.
@@ -17,4 +17,4 @@ def test_build_ossec_apt_dependencies(File, build_path):
     All paths are rightly considered "legacy" and should never be present on
     the build host. This test is strictly for guarding against regressions.
     """
-    assert not File(build_path).exists
+    assert not host.file(build_path).exists
diff --git a/molecule/builder-trusty/tests/test_securedrop_deb_package.py b/molecule/builder-trusty/tests/test_securedrop_deb_package.py
--- a/molecule/builder-trusty/tests/test_securedrop_deb_package.py
+++ b/molecule/builder-trusty/tests/test_securedrop_deb_package.py
@@ -72,18 +72,18 @@ def get_deb_tags():
 
 
 @pytest.mark.parametrize("deb", deb_packages)
-def test_build_deb_packages(File, deb):
+def test_build_deb_packages(host, deb):
     """
     Sanity check the built Debian packages for Control field
     values and general package structure.
     """
-    deb_package = File(deb.format(
+    deb_package = host.file(deb.format(
         securedrop_test_vars.securedrop_version))
     assert deb_package.is_file
 
 
 @pytest.mark.parametrize("deb", deb_packages)
-def test_deb_packages_appear_installable(File, Command, Sudo, deb):
+def test_deb_packages_appear_installable(host, deb):
     """
     Confirms that a dry-run of installation reports no errors.
     Simple check for valid Debian package structure, but not thorough.
@@ -94,16 +94,16 @@ def test_deb_packages_appear_installable(File, Command, Sudo, deb):
     Testing application behavior is left to the functional tests.
     """
 
-    deb_package = File(deb.format(
+    deb_package = host.file(deb.format(
         securedrop_test_vars.securedrop_version))
 
     deb_basename = os.path.basename(deb_package.path)
     package_name = extract_package_name_from_filepath(deb_package.path)
     assert deb_basename.startswith(package_name)
 
-    # Sudo is required to call `dpkg --install`, even as dry-run.
-    with Sudo():
-        c = Command("dpkg --install --dry-run {}".format(deb_package.path))
+    # sudo is required to call `dpkg --install`, even as dry-run.
+    with host.sudo():
+        c = host.run("dpkg --install --dry-run {}".format(deb_package.path))
         assert "Selecting previously unselected package {}".format(
             package_name) in c.stdout
         regex = "Preparing to unpack [./]+{} ...".format(
@@ -113,18 +113,18 @@ def test_deb_packages_appear_installable(File, Command, Sudo, deb):
 
 
 @pytest.mark.parametrize("deb", deb_packages)
-def test_deb_package_control_fields(File, Command, deb):
+def test_deb_package_control_fields(host, deb):
     """
     Ensure Debian Control fields are populated as expected in the package.
     These checks are rather superficial, and don't actually confirm that the
     .deb files are not broken. At a later date, consider integration tests
     that actually use these built files during an Ansible provisioning run.
     """
-    deb_package = File(deb.format(
+    deb_package = host.file(deb.format(
         securedrop_test_vars.securedrop_version))
     package_name = extract_package_name_from_filepath(deb_package.path)
     # The `--field` option will display all fields if none are specified.
-    c = Command("dpkg-deb --field {}".format(deb_package.path))
+    c = host.run("dpkg-deb --field {}".format(deb_package.path))
 
     assert "Maintainer: SecureDrop Team <securedrop@freedom.press>" in c.stdout
     # The securedrop-config package is architecture indepedent
@@ -138,11 +138,11 @@ def test_deb_package_control_fields(File, Command, deb):
 
 
 @pytest.mark.parametrize("deb", deb_packages)
-def test_deb_package_control_fields_homepage(File, Command, deb):
-    deb_package = File(deb.format(
+def test_deb_package_control_fields_homepage(host, deb):
+    deb_package = host.file(deb.format(
         securedrop_test_vars.securedrop_version))
     # The `--field` option will display all fields if none are specified.
-    c = Command("dpkg-deb --field {}".format(deb_package.path))
+    c = host.run("dpkg-deb --field {}".format(deb_package.path))
     # The OSSEC source packages will have a different homepage;
     # all other packages should set securedrop.org as homepage.
     if os.path.basename(deb_package.path).startswith('ossec-'):
@@ -152,7 +152,7 @@ def test_deb_package_control_fields_homepage(File, Command, deb):
 
 
 @pytest.mark.parametrize("deb", deb_packages)
-def test_deb_package_contains_no_config_file(File, Command, deb):
+def test_deb_package_contains_no_config_file(host, deb):
     """
     Ensures the `securedrop-app-code` package does not ship a `config.py`
     file. Doing so would clobber the site-specific changes made via Ansible.
@@ -160,53 +160,53 @@ def test_deb_package_contains_no_config_file(File, Command, deb):
     Somewhat lazily checking all deb packages, rather than just the app-code
     package, but it accomplishes the same in a DRY manner.
     """
-    deb_package = File(deb.format(
+    deb_package = host.file(deb.format(
         securedrop_test_vars.securedrop_version))
-    c = Command("dpkg-deb --contents {}".format(deb_package.path))
+    c = host.run("dpkg-deb --contents {}".format(deb_package.path))
     assert not re.search("^.*/config\.py$", c.stdout, re.M)
 
 
 @pytest.mark.parametrize("deb", deb_packages)
-def test_deb_package_contains_pot_file(File, Command, deb):
+def test_deb_package_contains_pot_file(host, deb):
     """
     Ensures the `securedrop-app-code` package has the
     messages.pot file
     """
-    deb_package = File(deb.format(
+    deb_package = host.file(deb.format(
         securedrop_test_vars.securedrop_version))
-    c = Command("dpkg-deb --contents {}".format(deb_package.path))
+    c = host.run("dpkg-deb --contents {}".format(deb_package.path))
     # Only relevant for the securedrop-app-code package:
     if "securedrop-app-code" in deb_package.path:
         assert re.search("^.*messages.pot$", c.stdout, re.M)
 
 
 @pytest.mark.parametrize("deb", deb_packages)
-def test_deb_package_contains_mo_file(File, Command, deb):
+def test_deb_package_contains_mo_file(host, deb):
     """
     Ensures the `securedrop-app-code` package has at least one
     compiled mo file.
     """
-    deb_package = File(deb.format(
+    deb_package = host.file(deb.format(
         securedrop_test_vars.securedrop_version))
-    c = Command("dpkg-deb --contents {}".format(deb_package.path))
+    c = host.run("dpkg-deb --contents {}".format(deb_package.path))
     # Only relevant for the securedrop-app-code package:
     if "securedrop-app-code" in deb_package.path:
         assert re.search("^.*messages\.mo$", c.stdout, re.M)
 
 
 @pytest.mark.parametrize("deb", deb_packages)
-def test_deb_package_contains_no_generated_assets(File, Command, deb):
+def test_deb_package_contains_no_generated_assets(host, deb):
     """
     Ensures the `securedrop-app-code` package does not ship a minified
     static assets, which are built automatically via Flask-Assets, and may be
     present in the source directory used to build from.
     """
-    deb_package = File(deb.format(
+    deb_package = host.file(deb.format(
         securedrop_test_vars.securedrop_version))
 
     # Only relevant for the securedrop-app-code package:
     if "securedrop-app-code" in deb_package.path:
-        c = Command("dpkg-deb --contents {}".format(deb_package.path))
+        c = host.run("dpkg-deb --contents {}".format(deb_package.path))
         # static/gen/ directory should exist
         assert re.search("^.*\./var/www/securedrop"
                          "/static/gen/$", c.stdout, re.M)
@@ -229,17 +229,17 @@ def test_deb_package_contains_no_generated_assets(File, Command, deb):
 
 
 @pytest.mark.parametrize("deb", deb_packages)
-def test_deb_package_contains_css(File, Command, deb):
+def test_deb_package_contains_css(host, deb):
     """
     Ensures the `securedrop-app-code` package contains files that
     are generated during the `sass` build process.
     """
-    deb_package = File(deb.format(
+    deb_package = host.file(deb.format(
         securedrop_test_vars.securedrop_version))
 
     # Only relevant for the securedrop-app-code package:
     if "securedrop-app-code" in deb_package.path:
-        c = Command("dpkg-deb --contents {}".format(deb_package.path))
+        c = host.run("dpkg-deb --contents {}".format(deb_package.path))
 
         for css_type in ['journalist', 'source']:
             assert re.search("^.*\./var/www/securedrop/static/"
@@ -247,13 +247,13 @@ def test_deb_package_contains_css(File, Command, deb):
 
 
 @pytest.mark.parametrize("deb, tag", deb_tags)
-def test_deb_package_lintian(File, Command, deb, tag):
+def test_deb_package_lintian(host, deb, tag):
     """
     Ensures lintian likes our  Debian packages.
     """
-    deb_package = File(deb.format(
+    deb_package = host.file(deb.format(
         securedrop_test_vars.securedrop_version))
-    c = Command("""lintian --tags {} --no-tag-display-limit {}""".format(
+    c = host.run("lintian --tags {} --no-tag-display-limit {}".format(
         tag, deb_package.path))
     assert len(c.stdout) == 0
 
diff --git a/molecule/testinfra/staging/app-code/test_haveged.py b/molecule/testinfra/staging/app-code/test_haveged.py
--- a/molecule/testinfra/staging/app-code/test_haveged.py
+++ b/molecule/testinfra/staging/app-code/test_haveged.py
@@ -1,11 +1,11 @@
 testinfra_hosts = ["app-staging"]
 
 
-def test_haveged_config(File):
+def test_haveged_config(host):
     """
     Ensure haveged's low entrop watermark is sufficiently high.
     """
-    f = File('/etc/default/haveged')
+    f = host.file('/etc/default/haveged')
     assert f.is_file
     assert f.user == 'root'
     assert f.group == 'root'
@@ -13,25 +13,25 @@ def test_haveged_config(File):
     assert f.contains('^DAEMON_ARGS="-w 2400"$')
 
 
-def test_haveged_no_duplicate_lines(Command):
+def test_haveged_no_duplicate_lines(host):
     """
     Regression test to check for duplicate entries. Earlier playbooks
     for configuring the SD instances needlessly appended the `DAEMON_ARGS`
     line everytime the playbook was run. Fortunately the duplicate lines don't
     break the service, but it's still poor form.
     """
-    c = Command("uniq --repeated /etc/default/haveged")
+    c = host.run("uniq --repeated /etc/default/haveged")
     assert c.rc == 0
     assert c.stdout == ""
 
 
-def test_haveged_is_running(Service, Sudo):
+def test_haveged_is_running(host):
     """
     Ensure haveged service is running, to provide additional entropy.
     """
-    # Sudo is necessary to read /proc when running under grsecurity,
+    # sudo is necessary to read /proc when running under grsecurity,
     # which the App hosts do. Not technically necessary under development.
-    with Sudo():
-        s = Service("haveged")
+    with host.sudo():
+        s = host.service("haveged")
         assert s.is_running
         assert s.is_enabled
diff --git a/molecule/testinfra/staging/app-code/test_redis_worker.py b/molecule/testinfra/staging/app-code/test_redis_worker.py
--- a/molecule/testinfra/staging/app-code/test_redis_worker.py
+++ b/molecule/testinfra/staging/app-code/test_redis_worker.py
@@ -18,19 +18,19 @@
   "user={}".format(securedrop_test_vars.securedrop_user),
   'environment=HOME="/tmp/python-gnupg"',
 ])
-def test_redis_worker_configuration(File, config_line):
+def test_redis_worker_configuration(host, config_line):
     """
     Ensure SecureDrop Redis worker config for supervisor service
     management is configured correctly.
     """
-    f = File('/etc/supervisor/conf.d/securedrop_worker.conf')
+    f = host.file('/etc/supervisor/conf.d/securedrop_worker.conf')
     # Config lines may have special characters such as [] which will
     # throw off the regex matching, so let's escape those chars.
     regex = re.escape(config_line)
     assert f.contains('^{}$'.format(regex))
 
 
-def test_redis_worker_config_file(File):
+def test_redis_worker_config_file(host):
     """
     Ensure SecureDrop Redis worker config for supervisor service
     management has proper ownership and mode.
@@ -38,7 +38,7 @@ def test_redis_worker_config_file(File):
     Using separate test so that the parametrization doesn't rerun
     the file mode checks, which would be useless.
     """
-    f = File('/etc/supervisor/conf.d/securedrop_worker.conf')
+    f = host.file('/etc/supervisor/conf.d/securedrop_worker.conf')
     assert f.is_file
     assert oct(f.mode) == '0644'
     assert f.user == "root"
diff --git a/molecule/testinfra/staging/app-code/test_securedrop_app_code.py b/molecule/testinfra/staging/app-code/test_securedrop_app_code.py
--- a/molecule/testinfra/staging/app-code/test_securedrop_app_code.py
+++ b/molecule/testinfra/staging/app-code/test_securedrop_app_code.py
@@ -5,13 +5,13 @@
 securedrop_test_vars = pytest.securedrop_test_vars
 
 
-def test_apache_default_docroot_is_absent(File):
+def test_apache_default_docroot_is_absent(host):
     """
     Ensure that the default docroot for Apache, containing static HTML
     under Debian, has been removed. Leaving it in place can be a privacy
     leak, as it displays version information by default.
     """
-    assert not File('/var/www/html').exists
+    assert not host.file('/var/www/html').exists
 
 
 @pytest.mark.parametrize('package', [
@@ -25,38 +25,38 @@ def test_apache_default_docroot_is_absent(File):
   'sqlite3',
   'supervisor',
 ])
-def test_securedrop_application_apt_dependencies(Package, package):
+def test_securedrop_application_apt_dependencies(host, package):
     """
     Ensure apt dependencies required to install `securedrop-app-code`
     are present. These should be pulled in automatically via apt,
     due to specification in Depends in package control file.
     """
-    assert Package(package).is_installed
+    assert host.package(package).is_installed
 
 
-def test_securedrop_application_test_locale(File, Sudo):
+def test_securedrop_application_test_locale(host):
     """
     Ensure SecureDrop DEFAULT_LOCALE is present.
     """
-    securedrop_config = File("{}/config.py".format(
+    securedrop_config = host.file("{}/config.py".format(
         securedrop_test_vars.securedrop_code))
-    with Sudo():
+    with host.sudo():
         assert securedrop_config.is_file
         assert securedrop_config.contains("^DEFAULT_LOCALE")
         assert securedrop_config.content.count("DEFAULT_LOCALE") == 1
 
 
-def test_securedrop_application_test_journalist_key(File, Sudo):
+def test_securedrop_application_test_journalist_key(host):
     """
     Ensure the SecureDrop Application GPG public key file is present.
     This is a test-only pubkey provided in the repository strictly for testing.
     """
-    pubkey_file = File("{}/test_journalist_key.pub".format(
+    pubkey_file = host.file("{}/test_journalist_key.pub".format(
         securedrop_test_vars.securedrop_data))
-    # Sudo is only necessary when testing against app hosts, since the
+    # sudo is only necessary when testing against app hosts, since the
     # permissions are tighter. Let's elevate privileges so we're sure
     # we can read the correct file attributes and test them.
-    with Sudo():
+    with host.sudo():
         assert pubkey_file.is_file
         assert pubkey_file.user == "root"
         assert pubkey_file.group == "root"
@@ -64,9 +64,9 @@ def test_securedrop_application_test_journalist_key(File, Sudo):
 
     # Let's make sure the corresponding fingerprint is specified
     # in the SecureDrop app configuration.
-    securedrop_config = File("{}/config.py".format(
+    securedrop_config = host.file("{}/config.py".format(
         securedrop_test_vars.securedrop_code))
-    with Sudo():
+    with host.sudo():
         assert securedrop_config.is_file
         assert securedrop_config.user == \
             securedrop_test_vars.securedrop_user
@@ -77,15 +77,15 @@ def test_securedrop_application_test_journalist_key(File, Sudo):
             "^JOURNALIST_KEY = '65A1B5FF195B56353CC63DFFCC40EF1228271441'$")
 
 
-def test_securedrop_application_sqlite_db(File, Sudo):
+def test_securedrop_application_sqlite_db(host):
     """
     Ensure sqlite database exists for application. The database file should be
     created by Ansible on first run.
     """
-    # Sudo is necessary under the App hosts, which have restrictive file
+    # sudo is necessary under the App hosts, which have restrictive file
     # permissions on the doc root. Not technically necessary under dev host.
-    with Sudo():
-        f = File("{}/db.sqlite".format(securedrop_test_vars.securedrop_data))
+    with host.sudo():
+        f = host.file("{}/db.sqlite".format(securedrop_test_vars.securedrop_data))
         assert f.is_file
         assert f.user == securedrop_test_vars.securedrop_user
         assert f.group == securedrop_test_vars.securedrop_user
diff --git a/molecule/testinfra/staging/app-code/test_xvfb.py b/molecule/testinfra/staging/app-code/test_xvfb.py
--- a/molecule/testinfra/staging/app-code/test_xvfb.py
+++ b/molecule/testinfra/staging/app-code/test_xvfb.py
@@ -1,40 +1,35 @@
 testinfra_hosts = ["app-staging"]
 
 
-def test_xvfb_is_installed(Package):
+def test_xvfb_is_installed(host):
     """
     Ensure apt requirements for Xvfb are present.
     """
-    assert Package("xvfb").is_installed
+    assert host.package("xvfb").is_installed
 
 
-def test_firefox_is_installed(Package, Command):
+def test_firefox_is_installed(host):
     """
     The app test suite requires a very specific version of Firefox, for
     compatibility with Selenium. Make sure to check the explicit
     version of Firefox, not just that any version of Firefox is installed.
     """
-    p = Package("firefox")
+    p = host.package("firefox")
     assert p.is_installed
 
-    c = Command("firefox --version")
+    c = host.run("firefox --version")
     # Reminder: the rstrip is only necessary for local-context actions,
     # but it's a fine practice in all contexts.
     assert c.stdout.rstrip() == "Mozilla Firefox 46.0.1"
 
 
-def test_xvfb_service_config_trusty(host):
+def _xvfb_service_config_trusty(host):
     """
     Ensure xvfb service configuration file is present.
-    Using Sudo context manager because the expected mode is 700.
+    Using sudo context manager because the expected mode is 700.
     Not sure it's really necessary to have this script by 700; 755
     sounds sufficient.
     """
-    # We're checking the upstart/sysv-style init script, which is only
-    # relevant for Trusty.
-    if host.system_info.codename == "xenial":
-        return True
-
     with host.sudo():
         f = host.file("/etc/init.d/xvfb")
     assert f.is_file
@@ -79,20 +74,49 @@ def test_xvfb_service_config_trusty(host):
         assert f.content.rstrip() == xvfb_init_content
 
 
-def test_xvfb_service_enabled_trusty(host):
+def _xvfb_service_config_xenial(host):
+    """
+    Validate the service config for xvfb under Xenial, using
+    systemd unit files.
+    """
+    f = host.file("/etc/systemd/system/xvfb.service")
+    assert f.exists
+    xvfb_systemd_config = """
+[Unit]
+Description=X Virtual Frame Buffer Service
+After=network.target
+
+[Service]
+ExecStart=/usr/bin/Xvfb :1 -screen 0 1024x768x24 -ac +extension GLX +render -noreset
+
+[Install]
+WantedBy=multi-user.target
+""".lstrip().rstrip()
+    assert f.content.rstrip() == xvfb_systemd_config
+
+
+def test_xvfb_service_config(host):
+    """
+    Validate the service config for Xvfb.
+
+    Calls separate functions per platform, to accommodate for init
+    script system divergence on e.g. Trusty & Xenial.
+    """
+    if host.system_info.codename == "trusty":
+        _xvfb_service_config_trusty(host)
+    else:
+        _xvfb_service_config_xenial(host)
+
+
+def _xvfb_service_enabled_trusty(host):
     """
     Ensure xvfb is configured to start on boot via update-rc.d.
     The `-n` option to update-rc.d is dry-run.
 
-    Using Sudo context manager because the service file is mode 700.
+    Using sudo context manager because the service file is mode 700.
     Not sure it's really necessary to have this script by 700; 755
     sounds sufficient.
     """
-    # We're checking the upstart/sysv-style init script, which is only
-    # relevant for Trusty.
-    if host.system_info.codename == "xenial":
-        return True
-
     with host.sudo():
         c = host.command('update-rc.d -n xvfb defaults')
     assert c.rc == 0
@@ -100,12 +124,33 @@ def test_xvfb_service_enabled_trusty(host):
     assert wanted_text in c.stdout
 
 
-def test_xvfb_display_config(File):
+def _xvfb_service_enabled_xenial(host):
+    """
+    Ensure xvfb is configured to start on boot, under Xenial.
+    """
+    s = host.service("xvfb")
+    assert s.is_enabled
+
+
+def test_xvfb_service_enabled(host):
+    """
+    Ensure the xvfb service is configured to start on boot.
+
+    Calls separate functions per platform, to accommodate upstart vs
+    sysv style init scripts.
+    """
+    if host.system_info.codename == "trusty":
+        _xvfb_service_enabled_trusty(host)
+    else:
+        _xvfb_service_enabled_xenial(host)
+
+
+def test_xvfb_display_config(host):
     """
     Ensure DISPLAY environment variable is set on boot, for running
     headless tests via Xvfb.
     """
-    f = File('/etc/profile.d/xvfb_display.sh')
+    f = host.file('/etc/profile.d/xvfb_display.sh')
     assert f.is_file
     assert oct(f.mode) == "0444"
     assert f.user == "root"
@@ -113,20 +158,36 @@ def test_xvfb_display_config(File):
     assert f.contains("export DISPLAY=:1\n")
 
 
-def test_xvfb_service_running_trusty(host):
+def test_xvfb_service_running(host):
     """
     Ensure that xvfb service is running.
 
+    Calls separate functions per platform, to accommodate for upstart/sysv
+    style init scripts.
+    """
+    if host.system_info.codename == "trusty":
+        _xvfb_service_running_trusty(host)
+    else:
+        _xvfb_service_running_xenial(host)
+
+
+def _xvfb_service_running_xenial(host):
+    """
+    Ensure xvfb is running under Xenial.
+    """
+    s = host.service("xvfb")
+    assert s.is_running
+
+
+def _xvfb_service_running_trusty(host):
+    """
+    Ensure xvfb is running under Trusty.
+
     We can't use the Service module because it expects a "status"
     subcommand for the init script, and our custom version doesn't have
     one. So let's make sure the process is running.
     """
-    # We're checking the upstart/sysv-style init script, which is only
-    # relevant for Trusty.
-    if host.system_info.codename == "xenial":
-        return True
-
-    # Sudo isn't necessary to read out of /proc on development, but is
+    # sudo isn't necessary to read out of /proc on development, but is
     # required when running under Grsecurity, which app-staging does.
     # So let's escalate privileges to ensure we can determine service state.
     with host.sudo():
diff --git a/molecule/testinfra/staging/app/apache/test_apache_journalist_interface.py b/molecule/testinfra/staging/app/apache/test_apache_journalist_interface.py
--- a/molecule/testinfra/staging/app/apache/test_apache_journalist_interface.py
+++ b/molecule/testinfra/staging/app/apache/test_apache_journalist_interface.py
@@ -23,11 +23,11 @@
 # Test is not DRY; haven't figured out how to parametrize on
 # multiple inputs, so explicitly redeclaring test logic.
 @pytest.mark.parametrize("header", wanted_apache_headers)
-def test_apache_headers_journalist_interface(File, header):
+def test_apache_headers_journalist_interface(host, header):
     """
     Test for expected headers in Document Interface vhost config.
     """
-    f = File("/etc/apache2/sites-available/journalist.conf")
+    f = host.file("/etc/apache2/sites-available/journalist.conf")
     assert f.is_file
     assert f.user == "root"
     assert f.group == "root"
@@ -104,7 +104,7 @@ def test_apache_headers_journalist_interface(File, header):
   'ErrorLog /var/log/apache2/journalist-error.log',
   'CustomLog /var/log/apache2/journalist-access.log combined',
 ])
-def test_apache_config_journalist_interface(File, apache_opt):
+def test_apache_config_journalist_interface(host, apache_opt):
     """
     Ensure the necessary Apache settings for serving the application
     are in place. Some values will change according to the host,
@@ -113,7 +113,7 @@ def test_apache_config_journalist_interface(File, apache_opt):
 
     These checks apply only to the Document Interface, used by Journalists.
     """
-    f = File("/etc/apache2/sites-available/journalist.conf")
+    f = host.file("/etc/apache2/sites-available/journalist.conf")
     assert f.is_file
     assert f.user == "root"
     assert f.group == "root"
@@ -122,16 +122,16 @@ def test_apache_config_journalist_interface(File, apache_opt):
     assert re.search(regex, f.content, re.M)
 
 
-def test_apache_journalist_interface_vhost(File):
+def test_apache_journalist_interface_vhost(host):
     """
     Ensure the document root is configured with correct access restrictions
     for serving Journalist Interface application code.
     """
-    f = File("/etc/apache2/sites-available/journalist.conf")
+    f = host.file("/etc/apache2/sites-available/journalist.conf")
     assert common_apache2_directory_declarations in f.content
 
 
-def test_apache_logging_journalist_interface(File, Command, Sudo):
+def test_apache_logging_journalist_interface(host):
     """
     Check that logging is configured correctly for the Journalist Interface.
     The actions of Journalists are logged by the system, so that an Admin can
@@ -140,15 +140,15 @@ def test_apache_logging_journalist_interface(File, Command, Sudo):
     Logs were broken for some period of time, logging only "combined" to
     the logfile, rather than the combined LogFormat intended.
     """
-    # Sudo is necessary because /var/log/apache2 is mode 0750.
-    with Sudo():
-        f = File("/var/log/apache2/journalist-access.log")
+    # sudo is necessary because /var/log/apache2 is mode 0750.
+    with host.sudo():
+        f = host.file("/var/log/apache2/journalist-access.log")
         assert f.is_file
         if f.size == 0:
             # If the file is empty, the Journalist Interface hasn't been used
             # yet, so make a quick GET request local to the host so we can
             # validate the log entry.
-            Command.check_output("curl http://127.0.0.1:8080")
+            host.check_output("curl http://127.0.0.1:8080")
 
         assert f.size > 0  # Make sure something was logged.
         # LogFormat declaration was missing, so track regressions that log
diff --git a/molecule/testinfra/staging/app/apache/test_apache_service.py b/molecule/testinfra/staging/app/apache/test_apache_service.py
--- a/molecule/testinfra/staging/app/apache/test_apache_service.py
+++ b/molecule/testinfra/staging/app/apache/test_apache_service.py
@@ -9,12 +9,12 @@
     "source",
     "journalist",
 ])
-def test_apache_enabled_sites(Command, Sudo, apache_site):
+def test_apache_enabled_sites(host, apache_site):
     """
     Ensure the Source and Journalist interfaces are enabled.
     """
-    with Sudo():
-        c = Command("/usr/sbin/a2query -s {}".format(apache_site))
+    with host.sudo():
+        c = host.run("/usr/sbin/a2query -s {}".format(apache_site))
         assert "{} (enabled".format(apache_site) in c.stdout
         assert c.rc == 0
 
@@ -22,32 +22,32 @@ def test_apache_enabled_sites(Command, Sudo, apache_site):
 @pytest.mark.parametrize("apache_site", [
     "000-default",
 ])
-def test_apache_disabled_sites(Command, apache_site):
+def test_apache_disabled_sites(host, apache_site):
     """
     Ensure the default HTML document root is disabled.
     """
-    c = Command("a2query -s {}".format(apache_site))
+    c = host.run("a2query -s {}".format(apache_site))
     assert "No site matches {} (disabled".format(apache_site) in c.stderr
     assert c.rc == 32
 
 
-def test_apache_service(Service, Sudo):
+def test_apache_service(host):
     """
     Ensure Apache service is running.
     """
-    # Sudo is necessary to run `service apache2 status`, otherwise
+    # sudo is necessary to run `service apache2 status`, otherwise
     # the service is falsely reported as not running.
-    with Sudo():
-        s = Service("apache2")
+    with host.sudo():
+        s = host.service("apache2")
         assert s.is_running
         assert s.is_enabled
 
 
-def test_apache_user(User):
+def test_apache_user(host):
     """
     Ensure user account for running application code is configured correctly.
     """
-    u = User("www-data")
+    u = host.user("www-data")
     assert u.exists
     assert u.home == "/var/www"
     assert u.shell == "/usr/sbin/nologin"
@@ -57,14 +57,14 @@ def test_apache_user(User):
     "80",
     "8080",
 ])
-def test_apache_listening(Socket, Sudo, port):
+def test_apache_listening(host, port):
     """
     Ensure Apache is listening on proper ports and interfaces.
     In staging, expect the service to be bound to 0.0.0.0,
     but in prod, it should be restricted to 127.0.0.1.
     """
-    # Sudo is necessary to read from /proc/net/tcp.
-    with Sudo():
-        s = Socket("tcp://{}:{}".format(
+    # sudo is necessary to read from /proc/net/tcp.
+    with host.sudo():
+        s = host.socket("tcp://{}:{}".format(
             securedrop_test_vars.apache_listening_address, port))
         assert s.is_listening
diff --git a/molecule/testinfra/staging/app/apache/test_apache_source_interface.py b/molecule/testinfra/staging/app/apache/test_apache_source_interface.py
--- a/molecule/testinfra/staging/app/apache/test_apache_source_interface.py
+++ b/molecule/testinfra/staging/app/apache/test_apache_source_interface.py
@@ -6,11 +6,11 @@
 
 
 @pytest.mark.parametrize("header", securedrop_test_vars.wanted_apache_headers)
-def test_apache_headers_source_interface(File, header):
+def test_apache_headers_source_interface(host, header):
     """
     Test for expected headers in Source Interface vhost config.
     """
-    f = File("/etc/apache2/sites-available/source.conf")
+    f = host.file("/etc/apache2/sites-available/source.conf")
     assert f.is_file
     assert f.user == "root"
     assert f.group == "root"
@@ -46,7 +46,7 @@ def test_apache_headers_source_interface(File, header):
     'ErrorDocument 500 /notfound',
     "ErrorLog {}".format(securedrop_test_vars.apache_source_log),
 ])
-def test_apache_config_source_interface(File, apache_opt):
+def test_apache_config_source_interface(host, apache_opt):
     """
     Ensure the necessary Apache settings for serving the application
     are in place. Some values will change according to the host,
@@ -55,7 +55,7 @@ def test_apache_config_source_interface(File, apache_opt):
 
     These checks apply only to the Source Interface, used by Sources.
     """
-    f = File("/etc/apache2/sites-available/source.conf")
+    f = host.file("/etc/apache2/sites-available/source.conf")
     assert f.is_file
     assert f.user == "root"
     assert f.group == "root"
diff --git a/molecule/testinfra/staging/app/apache/test_apache_system_config.py b/molecule/testinfra/staging/app/apache/test_apache_system_config.py
--- a/molecule/testinfra/staging/app/apache/test_apache_system_config.py
+++ b/molecule/testinfra/staging/app/apache/test_apache_system_config.py
@@ -9,31 +9,31 @@
     "libapache2-mod-wsgi",
     "libapache2-mod-xsendfile",
 ])
-def test_apache_apt_packages(Package, package):
+def test_apache_apt_packages(host, package):
     """
     Ensure required Apache packages are installed.
     """
-    assert Package(package).is_installed
+    assert host.package(package).is_installed
 
 
-def test_apache_apt_packages_trusty(Package, SystemInfo):
+def test_apache_apt_packages_trusty(host):
     """
     Ensure required Apache packages are installed. Only checks Trusty-specific
     packages; other tests handle more general apt dependencies for Apache.
     """
     # Skip if testing against Xenial
-    if SystemInfo.release != "trusty":
+    if host.system_info.codename == "xenial":
         return True
-    assert Package("apache2-mpm-worker").is_installed
+    assert host.package("apache2-mpm-worker").is_installed
 
 
-def test_apache_security_config_deprecated(File):
+def test_apache_security_config_deprecated(host):
     """
     Ensure that /etc/apache2/security is absent, since it was setting
     redundant options already presentin /etc/apache2/apache2.conf.
     See #643 for discussion.
     """
-    assert not File("/etc/apache2/security").exists
+    assert not host.file("/etc/apache2/security").exists
 
 
 @pytest.mark.parametrize("apache_opt", [
@@ -58,14 +58,14 @@ def test_apache_security_config_deprecated(File):
     'ServerSignature Off',
     'TraceEnable Off',
 ])
-def test_apache_config_settings(File, apache_opt):
+def test_apache_config_settings(host, apache_opt):
     """
     Check required Apache config settings for general server.
     These checks do not target individual interfaces, e.g.
     Source versus Document Interface, and instead apply to
     Apache more generally.
     """
-    f = File("/etc/apache2/apache2.conf")
+    f = host.file("/etc/apache2/apache2.conf")
     assert f.is_file
     assert f.user == "root"
     assert f.group == "root"
@@ -77,7 +77,7 @@ def test_apache_config_settings(File, apache_opt):
     "80",
     "8080",
 ])
-def test_apache_ports_config(File, SystemInfo, port):
+def test_apache_ports_config(host, port):
     """
     Ensure Apache ports config items, which specify how the
     Source and Document Interfaces are configured to be served
@@ -85,7 +85,7 @@ def test_apache_ports_config(File, SystemInfo, port):
     to permit port forwarding for local testing, but in production,
     they're restricted to localhost, for use over Tor.
     """
-    f = File("/etc/apache2/ports.conf")
+    f = host.file("/etc/apache2/ports.conf")
     assert f.is_file
     assert f.user == "root"
     assert f.group == "root"
@@ -115,14 +115,14 @@ def test_apache_ports_config(File, SystemInfo, port):
   'wsgi',
   'xsendfile',
 ])
-def test_apache_modules_present(Command, Sudo, apache_module):
+def test_apache_modules_present(host, apache_module):
     """
     Ensure presence of required Apache modules. Application will not work
     correctly if these are missing. A separate test will check for
     disabled modules.
     """
-    with Sudo():
-        c = Command("/usr/sbin/a2query -m {}".format(apache_module))
+    with host.sudo():
+        c = host.run("/usr/sbin/a2query -m {}".format(apache_module))
         assert "{} (enabled".format(apache_module) in c.stdout
         assert c.rc == 0
 
@@ -134,14 +134,14 @@ def test_apache_modules_present(Command, Sudo, apache_module):
   'env',
   'status',
 ])
-def test_apache_modules_absent(Command, Sudo, apache_module):
+def test_apache_modules_absent(host, apache_module):
     """
     Ensure absence of unwanted Apache modules. Application does not require
     these modules, so they should be disabled to reduce attack surface.
     A separate test will check for disabled modules.
     """
-    with Sudo():
-        c = Command("/usr/sbin/a2query -m {}".format(apache_module))
+    with host.sudo():
+        c = host.run("/usr/sbin/a2query -m {}".format(apache_module))
         assert "No module matches {} (disabled".format(apache_module) in \
             c.stderr
         assert c.rc == 32
@@ -149,7 +149,7 @@ def test_apache_modules_absent(Command, Sudo, apache_module):
 
 @pytest.mark.parametrize("logfile",
                          securedrop_test_vars.allowed_apache_logfiles)
-def test_apache_logfiles_present(File, Command, Sudo, logfile):
+def test_apache_logfiles_present(host, logfile):
     """"
     Ensure that whitelisted Apache log files for the Source and Journalist
     Interfaces are present. In staging, we permit a "source-error" log,
@@ -158,13 +158,13 @@ def test_apache_logfiles_present(File, Command, Sudo, logfile):
     Apache log directory.
     """
     # We need elevated privileges to read files inside /var/log/apache2
-    with Sudo():
-        f = File(logfile)
+    with host.sudo():
+        f = host.file(logfile)
         assert f.is_file
         assert f.user == "root"
 
 
-def test_apache_logfiles_no_extras(Command, Sudo):
+def test_apache_logfiles_no_extras(host):
     """
     Ensure that no unwanted Apache logfiles are present. Complements the
     `test_apache_logfiles_present` config test. Here, we confirm that the
@@ -172,7 +172,7 @@ def test_apache_logfiles_no_extras(Command, Sudo):
     on the Application Server, whether staging or prod.
     """
     # We need elevated privileges to read files inside /var/log/apache2
-    with Sudo():
-        c = Command("find /var/log/apache2 -mindepth 1 | wc -l")
+    with host.sudo():
+        c = host.run("find /var/log/apache2 -mindepth 1 | wc -l")
         assert int(c.stdout) == \
             len(securedrop_test_vars.allowed_apache_logfiles)
diff --git a/molecule/testinfra/staging/app/test_app_network.py b/molecule/testinfra/staging/app/test_app_network.py
--- a/molecule/testinfra/staging/app/test_app_network.py
+++ b/molecule/testinfra/staging/app/test_app_network.py
@@ -9,16 +9,16 @@
 securedrop_test_vars = pytest.securedrop_test_vars
 
 
-def test_app_iptables_rules(SystemInfo, Command, Sudo):
+def test_app_iptables_rules(host):
 
     # Build a dict of variables to pass to jinja for iptables comparison
     kwargs = dict(
         mon_ip=os.environ.get('MON_IP', securedrop_test_vars.mon_ip),
-        default_interface=Command.check_output("ip r | head -n 1 | "
-                                               "awk '{ print $5 }'"),
-        tor_user_id=Command.check_output("id -u debian-tor"),
-        securedrop_user_id=Command.check_output("id -u www-data"),
-        ssh_group_gid=Command.check_output("getent group ssh | cut -d: -f3"),
+        default_interface=host.check_output("ip r | head -n 1 | "
+                                            "awk '{ print $5 }'"),
+        tor_user_id=host.check_output("id -u debian-tor"),
+        securedrop_user_id=host.check_output("id -u www-data"),
+        ssh_group_gid=host.check_output("getent group ssh | cut -d: -f3"),
         dns_server=securedrop_test_vars.dns_server)
 
     # Build iptables scrape cmd, purge comments + counters
@@ -32,9 +32,9 @@ def test_app_iptables_rules(SystemInfo, Command, Sudo):
     jinja_iptables = Template(io.open(iptables_file, 'r').read())
     iptables_expected = jinja_iptables.render(**kwargs)
 
-    with Sudo():
+    with host.sudo():
         # Actually run the iptables scrape command
-        iptables = Command.check_output(iptables)
+        iptables = host.check_output(iptables)
         # print diff comparison (only shows up in pytests if test fails or
         # verbosity turned way up)
         for iptablesdiff in difflib.context_diff(iptables_expected.split('\n'),
diff --git a/molecule/testinfra/staging/app/test_apparmor.py b/molecule/testinfra/staging/app/test_apparmor.py
--- a/molecule/testinfra/staging/app/test_apparmor.py
+++ b/molecule/testinfra/staging/app/test_apparmor.py
@@ -6,37 +6,37 @@
 
 
 @pytest.mark.parametrize('pkg', ['apparmor', 'apparmor-utils'])
-def test_apparmor_pkg(Package, pkg):
+def test_apparmor_pkg(host, pkg):
     """ Apparmor package dependencies """
-    assert Package(pkg).is_installed
+    assert host.package(pkg).is_installed
 
 
-def test_apparmor_enabled(Command, Sudo):
+def test_apparmor_enabled(host):
     """ Check that apparmor is enabled """
-    with Sudo():
-        assert Command("aa-status --enabled").rc == 0
+    with host.sudo():
+        assert host.run("aa-status --enabled").rc == 0
 
 
 apache2_capabilities = [
-        'dac_override',
-        'kill',
-        'net_bind_service',
-        'sys_ptrace'
-        ]
+    'dac_override',
+    'kill',
+    'net_bind_service',
+    'sys_ptrace'
+]
 
 
 @pytest.mark.parametrize('cap', apache2_capabilities)
-def test_apparmor_apache_capabilities(Command, cap):
+def test_apparmor_apache_capabilities(host, cap):
     """ check for exact list of expected app-armor capabilities for apache2 """
-    c = Command("perl -nE \'/^\s+capability\s+(\w+),$/ && say $1\' "
-                "/etc/apparmor.d/usr.sbin.apache2")
+    c = host.run("perl -nE \'/^\s+capability\s+(\w+),$/ && say $1\' "
+                 "/etc/apparmor.d/usr.sbin.apache2")
     assert cap in c.stdout
 
 
-def test_apparmor_apache_exact_capabilities(Command):
+def test_apparmor_apache_exact_capabilities(host):
     """ ensure no extra capabilities are defined for apache2 """
-    c = Command.check_output("grep -ic capability "
-                             "/etc/apparmor.d/usr.sbin.apache2")
+    c = host.check_output("grep -ic capability "
+                          "/etc/apparmor.d/usr.sbin.apache2")
     assert str(len(apache2_capabilities)) == c
 
 
@@ -44,74 +44,72 @@ def test_apparmor_apache_exact_capabilities(Command):
 
 
 @pytest.mark.parametrize('cap', tor_capabilities)
-def test_apparmor_tor_capabilities(Command, cap):
+def test_apparmor_tor_capabilities(host, cap):
     """ check for exact list of expected app-armor capabilities for tor """
-    c = Command("perl -nE \'/^\s+capability\s+(\w+),$/ && "
-                "say $1\' /etc/apparmor.d/usr.sbin.tor")
+    c = host.run("perl -nE \'/^\s+capability\s+(\w+),$/ && "
+                 "say $1\' /etc/apparmor.d/usr.sbin.tor")
     assert cap in c.stdout
 
 
-def test_apparmor_tor_exact_capabilities(Command):
+def test_apparmor_tor_exact_capabilities(host):
     """ ensure no extra capabilities are defined for tor """
-    c = Command.check_output("grep -ic capability "
-                             "/etc/apparmor.d/usr.sbin.tor")
+    c = host.check_output("grep -ic capability "
+                          "/etc/apparmor.d/usr.sbin.tor")
     assert str(len(tor_capabilities)) == c
 
 
-enforced_profiles = [
-        'ntpd',
-        'apache2',
-        'tcpdump',
-        'tor']
-
-
-@pytest.mark.parametrize('profile', enforced_profiles)
-def test_apparmor_ensure_not_disabled(File, Sudo, profile):
-    """ Explicitly check that enforced profiles are NOT in
-        /etc/apparmor.d/disable
-        Polling aa-status only checks the last config that was loaded,
-        this ensures it wont be disabled on reboot.
+@pytest.mark.parametrize('profile', [
+    'ntpd',
+    'apache2',
+    'tcpdump',
+    'tor',
+])
+def test_apparmor_ensure_not_disabled(host, profile):
     """
-    f = File("/etc/apparmor.d/disabled/usr.sbin.{}".format(profile))
-    with Sudo():
+    Explicitly check that enforced profiles are NOT in /etc/apparmor.d/disable
+    Polling aa-status only checks the last config that was loaded,
+    this ensures it wont be disabled on reboot.
+    """
+    f = host.file("/etc/apparmor.d/disabled/usr.sbin.{}".format(profile))
+    with host.sudo():
         assert not f.exists
 
 
 @pytest.mark.parametrize('complain_pkg', sdvars.apparmor_complain)
-def test_app_apparmor_complain(Command, Sudo, complain_pkg):
+def test_app_apparmor_complain(host, complain_pkg):
     """ Ensure app-armor profiles are in complain mode for staging """
-    with Sudo():
+    with host.sudo():
         awk = ("awk '/[0-9]+ profiles.*complain."
                "/{flag=1;next}/^[0-9]+.*/{flag=0}flag'")
-        c = Command.check_output("aa-status | {}".format(awk))
+        c = host.check_output("aa-status | {}".format(awk))
         assert complain_pkg in c
 
 
-def test_app_apparmor_complain_count(Command, Sudo):
+def test_app_apparmor_complain_count(host):
     """ Ensure right number of app-armor profiles are in complain mode """
-    with Sudo():
-        c = Command.check_output("aa-status --complaining")
+    with host.sudo():
+        c = host.check_output("aa-status --complaining")
         assert c == str(len(sdvars.apparmor_complain))
 
 
 @pytest.mark.parametrize('aa_enforced', sdvars.apparmor_enforce)
-def test_apparmor_enforced(Command, Sudo, aa_enforced):
+def test_apparmor_enforced(host, aa_enforced):
     awk = ("awk '/[0-9]+ profiles.*enforce./"
            "{flag=1;next}/^[0-9]+.*/{flag=0}flag'")
-    with Sudo():
-        c = Command.check_output("aa-status | {}".format(awk))
+    with host.sudo():
+        c = host.check_output("aa-status | {}".format(awk))
         assert aa_enforced in c
 
 
-def test_apparmor_total_profiles(Command, Sudo):
+def test_apparmor_total_profiles(host):
     """ Ensure number of total profiles is sum of enforced and
         complaining profiles """
-    with Sudo():
+    with host.sudo():
         total_expected = str((len(sdvars.apparmor_enforce)
                              + len(sdvars.apparmor_complain)))
         # Trusty has ~10, Xenial about ~20 profiles, so let's expect
         # *at least* the sum.
-        assert Command.check_output("aa-status --profiled") >= total_expected
+        assert host.check_output("aa-status --profiled") >= total_expected
 
 
 def test_aastatus_unconfined(host):
@@ -132,8 +130,8 @@ def test_aastatus_unconfined(host):
         assert unconfined_chk in aa_status_output
 
 
-def test_aa_no_denies_in_syslog(host, File, Sudo):
+def test_aa_no_denies_in_syslog(host):
     """ Ensure that there are no apparmor denials in syslog """
-    with Sudo():
-        f = File("/var/log/syslog")
+    with host.sudo():
+        f = host.file("/var/log/syslog")
         assert 'apparmor="DENIED"' not in f.content_string
diff --git a/molecule/testinfra/staging/app/test_appenv.py b/molecule/testinfra/staging/app/test_appenv.py
--- a/molecule/testinfra/staging/app/test_appenv.py
+++ b/molecule/testinfra/staging/app/test_appenv.py
@@ -5,16 +5,16 @@
 
 
 @pytest.mark.parametrize('exp_pip_pkg', sdvars.pip_deps)
-def test_app_pip_deps(PipPackage, exp_pip_pkg):
+def test_app_pip_deps(host, exp_pip_pkg):
     """ Ensure pip dependencies are installed """
-    pip = PipPackage.get_packages()
+    pip = host.pip_package.get_packages()
     assert pip[exp_pip_pkg['name']]['version'] == exp_pip_pkg['version']
 
 
-def test_app_wsgi(File, Sudo):
+def test_app_wsgi(host):
     """ ensure logging is enabled for source interface in staging """
-    f = File("/var/www/source.wsgi")
-    with Sudo():
+    f = host.file("/var/www/source.wsgi")
+    with host.sudo():
         assert f.is_file
         assert oct(f.mode) == "0640"
         assert f.user == 'www-data'
@@ -23,58 +23,57 @@ def test_app_wsgi(File, Sudo):
         assert f.contains("^logging\.basicConfig(stream=sys\.stderr)$")
 
 
-def test_pidfile(File):
+def test_pidfile(host):
     """ ensure there are no pid files """
-    assert not File('/tmp/journalist.pid').exists
-    assert not File('/tmp/source.pid').exists
+    assert not host.file('/tmp/journalist.pid').exists
+    assert not host.file('/tmp/source.pid').exists
 
 
 @pytest.mark.parametrize('app_dir', sdvars.app_directories)
-def test_app_directories(File, Sudo, app_dir):
+def test_app_directories(host, app_dir):
     """ ensure securedrop app directories exist with correct permissions """
-    f = File(app_dir)
-    with Sudo():
+    f = host.file(app_dir)
+    with host.sudo():
         assert f.is_directory
         assert f.user == sdvars.securedrop_user
         assert f.group == sdvars.securedrop_user
         assert oct(f.mode) == "0700"
 
 
-def test_app_code_pkg(Package):
+def test_app_code_pkg(host):
     """ ensure securedrop-app-code package is installed """
-    assert Package("securedrop-app-code").is_installed
+    assert host.package("securedrop-app-code").is_installed
 
 
-def test_gpg_key_in_keyring(Command, Sudo):
+def test_gpg_key_in_keyring(host):
     """ ensure test gpg key is present in app keyring """
-    with Sudo(sdvars.securedrop_user):
-        c = Command("gpg --homedir /var/lib/securedrop/keys "
-                    "--list-keys 28271441")
+    with host.sudo(sdvars.securedrop_user):
+        c = host.run("gpg --homedir /var/lib/securedrop/keys "
+                     "--list-keys 28271441")
         assert "pub   4096R/28271441 2013-10-12" in c.stdout
 
 
-def test_ensure_logo(File, Sudo):
+def test_ensure_logo(host):
     """ ensure default logo header file exists """
-    f = File("{}/static/i/logo.png".format(sdvars.securedrop_code))
-    with Sudo():
+    f = host.file("{}/static/i/logo.png".format(sdvars.securedrop_code))
+    with host.sudo():
         assert oct(f.mode) == "0644"
         assert f.user == sdvars.securedrop_user
         assert f.group == sdvars.securedrop_user
 
 
-def test_securedrop_tmp_clean_cron(Command, Sudo):
+def test_securedrop_tmp_clean_cron(host):
     """ Ensure securedrop tmp clean cron job in place """
-    with Sudo():
-        cronlist = Command("crontab -l").stdout
-        cronjob = "@daily {}/manage.py clean-tmp".format(
-            sdvars.securedrop_code)
+    with host.sudo():
+        cronlist = host.run("crontab -l").stdout
+        cronjob = "@daily {}/manage.py clean-tmp".format(sdvars.securedrop_code)
         assert cronjob in cronlist
 
 
-def test_app_workerlog_dir(File, Sudo):
+def test_app_workerlog_dir(host):
     """ ensure directory for worker logs is present """
-    f = File('/var/log/securedrop_worker')
-    with Sudo():
+    f = host.file('/var/log/securedrop_worker')
+    with host.sudo():
         assert f.is_directory
         assert f.user == "root"
         assert f.group == "root"
diff --git a/molecule/testinfra/staging/app/test_ossec_agent.py b/molecule/testinfra/staging/app/test_ossec_agent.py
--- a/molecule/testinfra/staging/app/test_ossec_agent.py
+++ b/molecule/testinfra/staging/app/test_ossec_agent.py
@@ -6,9 +6,9 @@
 testinfra_hosts = ["app", "app-staging"]
 
 
-def test_hosts_files(File, SystemInfo):
+def test_hosts_files(host):
     """ Ensure host files mapping are in place """
-    f = File('/etc/hosts')
+    f = host.file('/etc/hosts')
 
     mon_ip = os.environ.get('MON_IP', sdvars.mon_ip)
     mon_host = sdvars.monitor_hostname
@@ -19,27 +19,27 @@ def test_hosts_files(File, SystemInfo):
                                                                     mon_host))
 
 
-def test_hosts_duplicate(Command):
+def test_hosts_duplicate(host):
     """ Regression test for duplicate entries """
-    assert Command.check_output("uniq --repeated /etc/hosts") == ""
+    assert host.check_output("uniq --repeated /etc/hosts") == ""
 
 
-def test_ossec_agent_installed(Package):
+def test_ossec_agent_installed(host):
     """ Check that ossec-agent package is present """
-    assert Package("securedrop-ossec-agent").is_installed
+    assert host.package("securedrop-ossec-agent").is_installed
 
 
 # Permissions don't match between Ansible and OSSEC deb packages postinst.
 @pytest.mark.xfail
-def test_ossec_keyfile_present(File, Command, Sudo, SystemInfo):
+def test_ossec_keyfile_present(host):
     """ ensure client keyfile for ossec-agent is present """
     pattern = "^1024 {} {} [0-9a-f]{{64}}$".format(
                                     sdvars.app_hostname,
                                     os.environ.get('APP_IP', sdvars.app_ip))
     regex = re.compile(pattern)
 
-    with Sudo():
-        f = File("/var/ossec/etc/client.keys")
+    with host.sudo():
+        f = host.file("/var/ossec/etc/client.keys")
         assert f.exists
         assert oct(f.mode) == "0644"
         assert f.user == "root"
diff --git a/molecule/testinfra/staging/app/test_tor_config.py b/molecule/testinfra/staging/app/test_tor_config.py
--- a/molecule/testinfra/staging/app/test_tor_config.py
+++ b/molecule/testinfra/staging/app/test_tor_config.py
@@ -16,7 +16,7 @@ def test_tor_packages(Package, package):
     assert Package(package).is_installed
 
 
-def test_tor_service_running_trusty(host):
+def _tor_service_running_trusty(host):
     """
     Ensure tor is running and enabled. Tor is required for SSH access,
     so it must be enabled to start on boot. Checks upstart/sysv-style
@@ -27,9 +27,6 @@ def test_tor_service_running_trusty(host):
     # `initctl` command. The tor service is handled via a SysV-style init
     # script, so let's just shell out and verify the running and enabled
     # states explicitly.
-    if host.system_info.codename == "xenial":
-        return True
-
     with host.sudo():
         assert host.check_output("service tor status") == \
                " * tor is running"
@@ -45,31 +42,37 @@ def test_tor_service_running_trusty(host):
         assert t.linked_to == "/etc/init.d/tor"
 
 
-def test_tor_service_running_xenial(host):
+def _tor_service_running_xenial(host):
     """
     Ensure tor is running and enabled. Tor is required for SSH access,
     so it must be enabled to start on boot. Checks systemd-style services,
     used by Xenial.
     """
-    # TestInfra tries determine the service manager intelligently, and
-    # inappropriately assumes Upstart on Trusty, due to presence of the
-    # `initctl` command. The tor service is handled via a SysV-style init
-    # script, so let's just shell out and verify the running and enabled
-    # states explicitly.
-    if host.system_info.codename == "trusty":
-        return True
-
     s = host.service("tor")
     assert s.is_running
     assert s.is_enabled
 
 
+def test_tor_service_running(host):
+    """
+    Ensure tor is running and enabled. Tor is required for SSH access,
+    so it must be enabled to start on boot.
+
+    Calls a separate function depending on platform, to handle nuances
+    of upstart vs sysv init systems.
+    """
+    if host.system_info.codename == "trusty":
+        _tor_service_running_trusty(host)
+    else:
+        _tor_service_running_xenial(host)
+
+
 @pytest.mark.parametrize('torrc_option', [
     'SocksPort 0',
     'SafeLogging 1',
     'RunAsDaemon 1',
 ])
-def test_tor_torrc_options(File, torrc_option):
+def test_tor_torrc_options(host, torrc_option):
     """
     Check for required options in the system Tor config file.
     These options should be present regardless of machine role,
@@ -77,21 +80,21 @@ def test_tor_torrc_options(File, torrc_option):
 
     Separate tests will check for specific hidden services.
     """
-    f = File("/etc/tor/torrc")
+    f = host.file("/etc/tor/torrc")
     assert f.is_file
     assert f.user == "debian-tor"
     assert oct(f.mode) == "0644"
     assert f.contains("^{}$".format(torrc_option))
 
 
-def test_tor_torrc_sandbox(File):
+def test_tor_torrc_sandbox(host):
     """
     Check that the `Sandbox 1` declaration is not present in the torrc.
     The torrc manpage states this option is experimental, and although we
     use it already on Tails workstations, further testing is required
     before we push it out to servers. See issues #944 and #1969.
     """
-    f = File("/etc/tor/torrc")
+    f = host.file("/etc/tor/torrc")
     # Only `Sandbox 1` will enable, but make sure there are zero occurrances
     # of "Sandbox", otherwise we may have a regression somewhere.
     assert not f.contains("^.*Sandbox.*$")
diff --git a/molecule/testinfra/staging/app/test_tor_hidden_services.py b/molecule/testinfra/staging/app/test_tor_hidden_services.py
--- a/molecule/testinfra/staging/app/test_tor_hidden_services.py
+++ b/molecule/testinfra/staging/app/test_tor_hidden_services.py
@@ -7,12 +7,12 @@
 
 
 @pytest.mark.parametrize('tor_service', sdvars.tor_services)
-def test_tor_service_directories(File, Sudo, tor_service):
+def test_tor_service_directories(host, tor_service):
     """
     Check mode and ownership on Tor service directories.
     """
-    with Sudo():
-        f = File("/var/lib/tor/services/{}".format(tor_service['name']))
+    with host.sudo():
+        f = host.file("/var/lib/tor/services/{}".format(tor_service['name']))
         assert f.is_directory
         assert oct(f.mode) == "0700"
         assert f.user == "debian-tor"
@@ -20,19 +20,18 @@ def test_tor_service_directories(File, Sudo, tor_service):
 
 
 @pytest.mark.parametrize('tor_service', sdvars.tor_services)
-def test_tor_service_hostnames(File, Sudo, tor_service):
+def test_tor_service_hostnames(host, tor_service):
     """
     Check contents of tor service hostname file. For normal Hidden Services,
     the file should contain only hostname (.onion URL). For Authenticated
     Hidden Services, it should also contain the HidServAuth cookie.
     """
-
     # Declare regex only for THS; we'll build regex for ATHS only if
     # necessary, since we won't have the required values otherwise.
     ths_hostname_regex = "[a-z0-9]{16}\.onion"
 
-    with Sudo():
-        f = File("/var/lib/tor/services/{}/hostname".format(
+    with host.sudo():
+        f = host.file("/var/lib/tor/services/{}/hostname".format(
             tor_service['name']))
         assert f.is_file
         assert oct(f.mode) == "0600"
diff --git a/molecule/testinfra/staging/common/test_cron_apt.py b/molecule/testinfra/staging/common/test_cron_apt.py
--- a/molecule/testinfra/staging/common/test_cron_apt.py
+++ b/molecule/testinfra/staging/common/test_cron_apt.py
@@ -9,7 +9,7 @@
     'cron-apt',
     'ntp'
 ])
-def test_cron_apt_dependencies(Package, dependency):
+def test_cron_apt_dependencies(host, dependency):
     """
     Ensure critical packages are installed. If any of these are missing,
     the system will fail to receive automatic updates.
@@ -20,14 +20,14 @@ def test_cron_apt_dependencies(Package, dependency):
     problematic. With better procedures in place regarding apt repo
     maintenance, we can ensure the field is populated going forward.
     """
-    assert Package(dependency).is_installed
+    assert host.package(dependency).is_installed
 
 
-def test_cron_apt_config(File):
+def test_cron_apt_config(host):
     """
     Ensure custom cron-apt config file is present.
     """
-    f = File('/etc/cron-apt/config')
+    f = host.file('/etc/cron-apt/config')
     assert f.is_file
     assert f.user == "root"
     assert oct(f.mode) == "0644"
@@ -59,12 +59,12 @@ def test_cron_apt_repo_list(host, repo):
     assert f.contains(repo_regex)
 
 
-def test_cron_apt_repo_config_update(File):
+def test_cron_apt_repo_config_update(host):
     """
     Ensure cron-apt updates repos from the security.list config.
     """
 
-    f = File('/etc/cron-apt/action.d/0-update')
+    f = host.file('/etc/cron-apt/action.d/0-update')
     assert f.is_file
     assert f.user == "root"
     assert oct(f.mode) == "0644"
@@ -74,12 +74,12 @@ def test_cron_apt_repo_config_update(File):
     assert f.contains('^{}$'.format(repo_config))
 
 
-def test_cron_apt_delete_vanilla_kernels(File):
+def test_cron_apt_delete_vanilla_kernels(host):
     """
     Ensure cron-apt removes generic linux image packages when installed.
     """
 
-    f = File('/etc/cron-apt/action.d/9-remove')
+    f = host.file('/etc/cron-apt/action.d/9-remove')
     assert f.is_file
     assert f.user == "root"
     assert oct(f.mode) == "0644"
@@ -89,11 +89,11 @@ def test_cron_apt_delete_vanilla_kernels(File):
     assert f.contains('^{}$'.format(command))
 
 
-def test_cron_apt_repo_config_upgrade(File):
+def test_cron_apt_repo_config_upgrade(host):
     """
     Ensure cron-apt upgrades packages from the security.list config.
     """
-    f = File('/etc/cron-apt/action.d/5-security')
+    f = host.file('/etc/cron-apt/action.d/5-security')
     assert f.is_file
     assert f.user == "root"
     assert oct(f.mode) == "0644"
@@ -105,11 +105,11 @@ def test_cron_apt_repo_config_upgrade(File):
     assert f.contains(re.escape(repo_config))
 
 
-def test_cron_apt_config_deprecated(File):
+def test_cron_apt_config_deprecated(host):
     """
     Ensure default cron-apt file to download all updates does not exist.
     """
-    f = File('/etc/cron-apt/action.d/3-download')
+    f = host.file('/etc/cron-apt/action.d/3-download')
     assert not f.exists
 
 
@@ -121,13 +121,13 @@ def test_cron_apt_config_deprecated(File):
     {'job': '0 5 * * * root    /sbin/reboot',
      'state': 'absent'},
 ])
-def test_cron_apt_cron_jobs(File, cron_job):
+def test_cron_apt_cron_jobs(host, cron_job):
     """
     Check for correct cron job for upgrading all packages and rebooting.
     We'll also check for absence of previous versions of the cron job,
     to make sure those have been cleaned up via the playbooks.
     """
-    f = File('/etc/cron.d/cron-apt')
+    f = host.file('/etc/cron.d/cron-apt')
     assert f.is_file
     assert f.user == "root"
     assert oct(f.mode) == "0644"
@@ -139,7 +139,7 @@ def test_cron_apt_cron_jobs(File, cron_job):
         assert not f.contains(regex_job)
 
 
-def test_cron_apt_all_packages_updated(Command):
+def test_cron_apt_all_packages_updated(host):
     """
     Ensure a safe-upgrade has already been run, by checking that no
     packages are eligible for upgrade currently.
@@ -148,7 +148,7 @@ def test_cron_apt_all_packages_updated(Command):
     for use with Selenium. Therefore apt will report it's possible to upgrade
     Firefox, which we'll need to mark as "OK" in terms of the tests.
     """
-    c = Command('aptitude --simulate -y safe-upgrade')
+    c = host.run('aptitude --simulate -y safe-upgrade')
     assert c.rc == 0
     # Staging hosts will have locally built deb packages, marked as held.
     # Staging and development will have a version-locked Firefox pinned for
diff --git a/molecule/testinfra/staging/common/test_fpf_apt_repo.py b/molecule/testinfra/staging/common/test_fpf_apt_repo.py
--- a/molecule/testinfra/staging/common/test_fpf_apt_repo.py
+++ b/molecule/testinfra/staging/common/test_fpf_apt_repo.py
@@ -32,7 +32,7 @@ def test_fpf_apt_repo_present(host):
     assert f.contains(repo_regex)
 
 
-def test_fpf_apt_repo_fingerprint(Command):
+def test_fpf_apt_repo_fingerprint(host):
     """
     Ensure the FPF apt repo has the correct fingerprint on the associated
     signing pubkey. The key changed in October 2016, so test for the
@@ -40,7 +40,7 @@ def test_fpf_apt_repo_fingerprint(Command):
     `securedrop-keyring` package.
     """
 
-    c = Command('apt-key finger')
+    c = host.run('apt-key finger')
 
     fpf_gpg_pub_key_info = """/etc/apt/trusted.gpg.d/securedrop-keyring.gpg
 ---------------------------------------------
@@ -59,12 +59,12 @@ def test_fpf_apt_repo_fingerprint(Command):
     'uid                  Freedom of the Press Foundation Master Signing Key',
     'B89A 29DB 2128 160B 8E4B  1B4C BADD E0C7 FC9F 6818',
 ])
-def test_fpf_apt_repo_old_pubkeys_absent(Command, old_pubkey):
+def test_fpf_apt_repo_old_pubkeys_absent(host, old_pubkey):
     """
     Ensure that expired (or about-to-expire) public keys for the FPF
     apt repo are NOT present. Updates to the securedrop-keyring package
     should enforce clobbering of old pubkeys, and this check will confirm
     absence.
     """
-    c = Command('apt-key finger')
+    c = host.run('apt-key finger')
     assert old_pubkey not in c.stdout
diff --git a/molecule/testinfra/staging/common/test_grsecurity.py b/molecule/testinfra/staging/common/test_grsecurity.py
--- a/molecule/testinfra/staging/common/test_grsecurity.py
+++ b/molecule/testinfra/staging/common/test_grsecurity.py
@@ -5,12 +5,12 @@
 KERNEL_VERSION = pytest.securedrop_test_vars.grsec_version
 
 
-def test_ssh_motd_disabled(File):
+def test_ssh_motd_disabled(host):
     """
     Ensure the SSH MOTD (Message of the Day) is disabled.
     Grsecurity balks at Ubuntu's default MOTD.
     """
-    f = File("/etc/pam.d/sshd")
+    f = host.file("/etc/pam.d/sshd")
     assert f.is_file
     assert not f.contains("pam\.motd")
 
@@ -21,13 +21,13 @@ def test_ssh_motd_disabled(File):
     'paxctl',
     'securedrop-grsec',
 ])
-def test_grsecurity_apt_packages(Package, package):
+def test_grsecurity_apt_packages(host, package):
     """
     Ensure the grsecurity-related apt packages are present on the system.
     Includes the FPF-maintained metapackage, as well as paxctl, for managing
     PaX flags on binaries.
     """
-    assert Package(package).is_installed
+    assert host.package(package).is_installed
 
 
 @pytest.mark.parametrize("package", [
@@ -38,7 +38,7 @@ def test_grsecurity_apt_packages(Package, package):
     '^linux-image-.*generic$',
     '^linux-headers-.*',
 ])
-def test_generic_kernels_absent(Command, package):
+def test_generic_kernels_absent(host, package):
     """
     Ensure the default Ubuntu-provided kernel packages are absent.
     In the past, conflicting version numbers have caused machines
@@ -49,28 +49,28 @@ def test_generic_kernels_absent(Command, package):
     # Can't use the TestInfra Package module to check state=absent,
     # so let's check by shelling out to `dpkg -l`. Dpkg will automatically
     # honor simple regex in package names.
-    c = Command("dpkg -l {}".format(package))
+    c = host.run("dpkg -l {}".format(package))
     assert c.rc == 1
     error_text = "dpkg-query: no packages found matching {}".format(package)
     assert c.stderr == error_text
 
 
-def test_grsecurity_lock_file(File):
+def test_grsecurity_lock_file(host):
     """
     Ensure system is rerunning a grsecurity kernel by testing for the
     `grsec_lock` file, which is automatically created by grsecurity.
     """
-    f = File("/proc/sys/kernel/grsecurity/grsec_lock")
+    f = host.file("/proc/sys/kernel/grsecurity/grsec_lock")
     assert oct(f.mode) == "0600"
     assert f.user == "root"
     assert f.size == 0
 
 
-def test_grsecurity_kernel_is_running(Command):
+def test_grsecurity_kernel_is_running(host):
     """
     Make sure the currently running kernel is specific grsec kernel.
     """
-    c = Command('uname -r')
+    c = host.run('uname -r')
     assert c.stdout.endswith('-grsec')
     assert c.stdout == '{}-grsec'.format(KERNEL_VERSION)
 
@@ -80,13 +80,13 @@ def test_grsecurity_kernel_is_running(Command):
   ('kernel.grsecurity.rwxmap_logging', 0),
   ('vm.heap_stack_gap', 1048576),
 ])
-def test_grsecurity_sysctl_options(Sysctl, Sudo, sysctl_opt):
+def test_grsecurity_sysctl_options(host, sysctl_opt):
     """
     Check that the grsecurity-related sysctl options are set correctly.
     In production the RWX logging is disabled, to reduce log noise.
     """
-    with Sudo():
-        assert Sysctl(sysctl_opt[0]) == sysctl_opt[1]
+    with host.sudo():
+        assert host.sysctl(sysctl_opt[0]) == sysctl_opt[1]
 
 
 @pytest.mark.parametrize('paxtest_check', [
@@ -108,48 +108,47 @@ def test_grsecurity_sysctl_options(Sysctl, Sudo, sysctl_opt):
   "Return to function (memcpy)",
   "Return to function (memcpy, PIE)",
 ])
-def test_grsecurity_paxtest(Command, Sudo, paxtest_check):
+def test_grsecurity_paxtest(host, paxtest_check):
     """
     Check that paxtest does not report anything vulnerable
     Requires the package paxtest to be installed.
     The paxtest package is currently being installed in the app-test role.
     """
-    if Command.exists("/usr/bin/paxtest"):
-        with Sudo():
-            c = Command("paxtest blackhat")
+    if host.exists("/usr/bin/paxtest"):
+        with host.sudo():
+            c = host.run("paxtest blackhat")
             assert c.rc == 0
             assert "Vulnerable" not in c.stdout
             regex = "^{}\s*:\sKilled$".format(re.escape(paxtest_check))
             assert re.search(regex, c.stdout)
 
 
-def test_grub_pc_marked_manual(Command):
+def test_grub_pc_marked_manual(host):
     """
     Ensure the `grub-pc` packaged is marked as manually installed.
     This is necessary for VirtualBox with Vagrant.
     """
-    c = Command('apt-mark showmanual grub-pc')
+    c = host.run('apt-mark showmanual grub-pc')
     assert c.rc == 0
     assert c.stdout == "grub-pc"
 
 
-def test_apt_autoremove(Command):
+def test_apt_autoremove(host):
     """
     Ensure old packages have been autoremoved.
     """
-    c = Command('apt-get --dry-run autoremove')
+    c = host.run('apt-get --dry-run autoremove')
     assert c.rc == 0
     assert "The following packages will be REMOVED" not in c.stdout
 
 
-@pytest.mark.xfail(strict=True,
-                   reason="PaX flags unset at install time, see issue #3916")
+@pytest.mark.xfail(reason="PaX flags unset at install time, see issue #3916")
 @pytest.mark.parametrize("binary", [
     "/usr/sbin/grub-probe",
     "/usr/sbin/grub-mkdevicemap",
     "/usr/bin/grub-script-check",
 ])
-def test_pax_flags(Command, File, binary):
+def test_pax_flags(host, binary):
     """
     Ensure PaX flags are set correctly on critical Grub binaries.
     These flags are maintained as part of a post-install kernel hook
@@ -157,11 +156,11 @@ def test_pax_flags(Command, File, binary):
     the machine may fail to boot into a new kernel.
     """
 
-    f = File("/etc/kernel/postinst.d/paxctl-grub")
+    f = host.file("/etc/kernel/postinst.d/paxctl-grub")
     assert f.is_file
     assert f.contains("^paxctl -zCE {}".format(binary))
 
-    c = Command("paxctl -v {}".format(binary))
+    c = host.run("paxctl -v {}".format(binary))
     assert c.rc == 0
 
     assert "- PaX flags: --------E--- [{}]".format(binary) in c.stdout
diff --git a/molecule/testinfra/staging/common/test_ip6tables.py b/molecule/testinfra/staging/common/test_ip6tables.py
--- a/molecule/testinfra/staging/common/test_ip6tables.py
+++ b/molecule/testinfra/staging/common/test_ip6tables.py
@@ -1,4 +1,4 @@
-def test_ip6tables_drop_everything(Command, Sudo):
+def test_ip6tables_drop_everything(host):
     """
     Ensure that all IPv6 packets are dropped by default.
     The IPv4 rules are more complicated, and tested separately.
@@ -9,6 +9,6 @@ def test_ip6tables_drop_everything(Command, Sudo):
 -P OUTPUT DROP
 """.lstrip().rstrip()
 
-    with Sudo():
-        c = Command.check_output("ip6tables -S")
+    with host.sudo():
+        c = host.check_output("ip6tables -S")
         assert c == desired_ip6tables_output
diff --git a/molecule/testinfra/staging/common/test_system_hardening.py b/molecule/testinfra/staging/common/test_system_hardening.py
--- a/molecule/testinfra/staging/common/test_system_hardening.py
+++ b/molecule/testinfra/staging/common/test_system_hardening.py
@@ -21,21 +21,21 @@
   ('net.ipv6.conf.default.disable_ipv6', 1),
   ('net.ipv6.conf.lo.disable_ipv6', 1),
 ])
-def test_sysctl_options(Sysctl, Sudo, sysctl_opt):
+def test_sysctl_options(host, sysctl_opt):
     """
     Ensure sysctl flags are set correctly. Most of these checks
     are disabling IPv6 and hardening IPv4, which is appropriate
     due to the heavy use of Tor.
     """
-    with Sudo():
-        assert Sysctl(sysctl_opt[0]) == sysctl_opt[1]
+    with host.sudo():
+        assert host.sysctl(sysctl_opt[0]) == sysctl_opt[1]
 
 
-def test_dns_setting(File):
+def test_dns_setting(host):
     """
     Ensure DNS service is hard-coded in resolv.conf config.
     """
-    f = File('/etc/resolvconf/resolv.conf.d/base')
+    f = host.file('/etc/resolvconf/resolv.conf.d/base')
     assert f.is_file
     assert f.user == "root"
     assert f.group == "root"
@@ -47,36 +47,44 @@ def test_dns_setting(File):
     'bluetooth',
     'iwlwifi',
 ])
-def test_blacklisted_kernel_modules(Command, File, Sudo, kernel_module):
+def test_blacklisted_kernel_modules(host, kernel_module):
     """
     Test that unwanted kernel modules are blacklisted on the system.
     Mostly these checks are defense-in-depth approaches to ensuring
     that wireless interfaces will not work.
     """
-    with Sudo():
-        assert kernel_module not in Command("lsmod").stdout
+    with host.sudo():
+        c = host.run("lsmod")
+        assert kernel_module not in c.stdout
 
-    f = File("/etc/modprobe.d/blacklist.conf")
+    f = host.file("/etc/modprobe.d/blacklist.conf")
     assert f.contains("^blacklist {}$".format(kernel_module))
 
 
-def test_swap_disabled(Command):
+def test_swap_disabled(host):
     """
     Ensure swap space is disabled. Prohibit writing memory to swapfiles
     to reduce the threat of forensic analysis leaking any sensitive info.
     """
-    hostname = Command.check_output('hostname')
+    hostname = host.check_output('hostname')
 
     # Mon doesn't have swap disabled yet
-    if not hostname.startswith('mon'):
-        c = Command.check_output('swapon --summary')
-        # A leading slash will indicate full path to a swapfile.
-        assert not re.search("^/", c, re.M)
+    if hostname.startswith('mon'):
+        return True
+
+    c = host.check_output('swapon --summary')
+    # A leading slash will indicate full path to a swapfile.
+    assert not re.search("^/", c, re.M)
+
+    if host.system_info.codename == "trusty":
         # Expect that ONLY the headers will be present in the output.
         rgx = re.compile("Filename\s*Type\s*Size\s*Used\s*Priority")
+    else:
         # On Xenial, swapon 2.27.1 shows blank output, with no headers, so
         # check for empty output as confirmation of no swap.
-        assert any((re.search(rgx, c), c == ""))
+        rgx = re.compile("^$")
+
+    assert re.search(rgx, c)
 
 
 def test_twofactor_disabled_on_tty(host):
@@ -109,16 +117,19 @@ def test_sshd_config(host, sshd_opts):
     assert line in sshd_config_file
 
 
-@pytest.mark.parametrize('filenames', [
+@pytest.mark.parametrize('logfile', [
     '/var/log/auth.log',
     '/var/log/syslog',
 ])
-def test_pam_(host, filenames, Command, Sudo):
+def test_no_ecrypt_messages_in_logs(host, logfile):
     """
     Ensure pam_ecryptfs is removed from /etc/pam.d/common-auth : not only is
     no longer needed, it causes error messages (see issue #3963)
     """
     error_message = "pam_ecryptfs.so: cannot open shared object file"
-    with Sudo():
-        log_file = host.file(filenames).content_string
-        assert error_message not in log_file
+    with host.sudo():
+        f = host.file(logfile)
+        # Not using `f.contains(<pattern>)` because that'd cause the sought
+        # string to make it into syslog as a side-effect of the testinfra
+        # invocation, causing subsequent test runs to report failure.
+        assert error_message not in f.content_string
diff --git a/molecule/testinfra/staging/common/test_user_config.py b/molecule/testinfra/staging/common/test_user_config.py
--- a/molecule/testinfra/staging/common/test_user_config.py
+++ b/molecule/testinfra/staging/common/test_user_config.py
@@ -1,12 +1,12 @@
 import re
 
 
-def test_sudoers_config(File, Sudo):
+def test_sudoers_config(host):
     """
     Check sudoers config for passwordless sudo via group membership,
     as well as environment-related hardening.
     """
-    f = File("/etc/sudoers")
+    f = host.file("/etc/sudoers")
     assert f.is_file
     assert f.user == "root"
     assert f.group == "root"
@@ -14,7 +14,7 @@ def test_sudoers_config(File, Sudo):
 
     # Restrictive file mode requires sudo for reading, so let's
     # read once and store the content in a var.
-    with Sudo():
+    with host.sudo():
         sudoers_config = f.content
 
     # Using re.search rather than `f.contains` since the basic grep
@@ -30,7 +30,7 @@ def test_sudoers_config(File, Sudo):
     assert re.search('Defaults:%sudo\s+!requiretty', sudoers_config, re.M)
 
 
-def test_sudoers_tmux_env(File):
+def test_sudoers_tmux_env(host):
     """
     Ensure SecureDrop-specific bashrc additions are present.
     This checks for automatic tmux start on interactive shells.
@@ -38,7 +38,7 @@ def test_sudoers_tmux_env(File):
     the corresponding settings there.
     """
 
-    f = File('/etc/profile.d/securedrop_additions.sh')
+    f = host.file('/etc/profile.d/securedrop_additions.sh')
     non_interactive_str = re.escape('[[ $- != *i* ]] && return')
     tmux_check = re.escape('test -z "$TMUX" && (tmux attach ||'
                            ' tmux new-session)')
@@ -50,7 +50,7 @@ def test_sudoers_tmux_env(File):
     assert f.contains(tmux_check)
 
 
-def test_tmux_installed(Package):
+def test_tmux_installed(host):
     """
     Ensure the `tmux` package is present, since it's required for the user env.
     When running an interactive SSH session over Tor, tmux should be started
@@ -58,10 +58,10 @@ def test_tmux_installed(Package):
     unexpectedly, as sometimes happens over Tor. The Admin will be able to
     reconnect to the running tmux session and review command output.
     """
-    assert Package("tmux").is_installed
+    assert host.package("tmux").is_installed
 
 
-def test_sudoers_tmux_env_deprecated(File):
+def test_sudoers_tmux_env_deprecated(host):
     """
     Previous version of the Ansible config set the tmux config
     in per-user ~/.bashrc, which was redundant. The config has
@@ -72,5 +72,5 @@ def test_sudoers_tmux_env_deprecated(File):
 
     admin_user = "vagrant"
 
-    f = File("/home/{}/.bashrc".format(admin_user))
+    f = host.file("/home/{}/.bashrc".format(admin_user))
     assert not f.contains("^. \/etc\/bashrc\.securedrop_additions$")
diff --git a/molecule/testinfra/staging/mon/test_mon_network.py b/molecule/testinfra/staging/mon/test_mon_network.py
--- a/molecule/testinfra/staging/mon/test_mon_network.py
+++ b/molecule/testinfra/staging/mon/test_mon_network.py
@@ -9,16 +9,16 @@
 securedrop_test_vars = pytest.securedrop_test_vars
 
 
-def test_mon_iptables_rules(SystemInfo, Command, Sudo):
+def test_mon_iptables_rules(host):
 
     # Build a dict of variables to pass to jinja for iptables comparison
     kwargs = dict(
         app_ip=os.environ.get('APP_IP', securedrop_test_vars.app_ip),
-        default_interface=Command.check_output(
+        default_interface=host.check_output(
             "ip r | head -n 1 | awk '{ print $5 }'"),
-        tor_user_id=Command.check_output("id -u debian-tor"),
-        ssh_group_gid=Command.check_output("getent group ssh | cut -d: -f3"),
-        postfix_user_id=Command.check_output("id -u postfix"),
+        tor_user_id=host.check_output("id -u debian-tor"),
+        ssh_group_gid=host.check_output("getent group ssh | cut -d: -f3"),
+        postfix_user_id=host.check_output("id -u postfix"),
         dns_server=securedrop_test_vars.dns_server)
 
     # Build iptables scrape cmd, purge comments + counters
@@ -32,9 +32,9 @@ def test_mon_iptables_rules(SystemInfo, Command, Sudo):
     jinja_iptables = Template(io.open(iptables_file, 'r').read())
     iptables_expected = jinja_iptables.render(**kwargs)
 
-    with Sudo():
+    with host.sudo():
         # Actually run the iptables scrape command
-        iptables = Command.check_output(iptables)
+        iptables = host.check_output(iptables)
         # print diff comparison (only shows up in pytests if test fails or
         # verbosity turned way up)
         for iptablesdiff in difflib.context_diff(iptables_expected.split('\n'),
diff --git a/molecule/testinfra/staging/mon/test_ossec_ruleset.py b/molecule/testinfra/staging/mon/test_ossec_ruleset.py
--- a/molecule/testinfra/staging/mon/test_ossec_ruleset.py
+++ b/molecule/testinfra/staging/mon/test_ossec_ruleset.py
@@ -9,19 +9,19 @@
 
 @pytest.mark.parametrize('log_event',
                          sdvars.log_events_without_ossec_alerts)
-def test_ossec_false_positives_suppressed(Command, Sudo, log_event):
-    with Sudo():
-        c = Command('echo "{}" | /var/ossec/bin/ossec-logtest'.format(
-                log_event["alert"]))
+def test_ossec_false_positives_suppressed(host, log_event):
+    with host.sudo():
+        c = host.run('echo "{}" | /var/ossec/bin/ossec-logtest'.format(
+                     log_event["alert"]))
         assert "Alert to be generated" not in c.stderr
 
 
 @pytest.mark.parametrize('log_event',
                          sdvars.log_events_with_ossec_alerts)
-def test_ossec_expected_alerts_are_present(Command, Sudo, log_event):
-    with Sudo():
-        c = Command('echo "{}" | /var/ossec/bin/ossec-logtest'.format(
-                log_event["alert"]))
+def test_ossec_expected_alerts_are_present(host, log_event):
+    with host.sudo():
+        c = host.run('echo "{}" | /var/ossec/bin/ossec-logtest'.format(
+                     log_event["alert"]))
         assert "Alert to be generated" in c.stderr
         alert_level = alert_level_regex.findall(c.stderr)[0]
         assert alert_level == log_event["level"]
diff --git a/molecule/testinfra/staging/mon/test_ossec_server.py b/molecule/testinfra/staging/mon/test_ossec_server.py
--- a/molecule/testinfra/staging/mon/test_ossec_server.py
+++ b/molecule/testinfra/staging/mon/test_ossec_server.py
@@ -6,7 +6,7 @@
 securedrop_test_vars = pytest.securedrop_test_vars
 
 
-def test_ossec_connectivity(Command, Sudo):
+def test_ossec_connectivity(host):
     """
     Ensure ossec-server machine has active connection to the ossec-agent.
     The ossec service will report all available agents, and we can inspect
@@ -15,8 +15,8 @@ def test_ossec_connectivity(Command, Sudo):
     desired_output = "{}-{} is available.".format(
         securedrop_test_vars.app_hostname,
         os.environ.get('APP_IP', securedrop_test_vars.app_ip))
-    with Sudo():
-        c = Command.check_output("/var/ossec/bin/list_agents -a")
+    with host.sudo():
+        c = host.check_output("/var/ossec/bin/list_agents -a")
         assert c == desired_output
 
 
@@ -26,7 +26,7 @@ def test_ossec_connectivity(Command, Sudo):
     '/var/ossec/etc/sslmanager.key',
     '/var/ossec/etc/sslmanager.cert',
 ])
-def test_ossec_keyfiles(File, Sudo, keyfile):
+def test_ossec_keyfiles(host, keyfile):
     """
     Ensure that the OSSEC transport key pair exists. These keys are used
     to protect the connection between the ossec-server and ossec-agent.
@@ -34,8 +34,8 @@ def test_ossec_keyfiles(File, Sudo, keyfile):
     All this check does in confirm they're present, it doesn't perform any
     matching checks to validate the configuration.
     """
-    with Sudo():
-        f = File(keyfile)
+    with host.sudo():
+        f = host.file(keyfile)
         assert f.is_file
         # The postinst scripts in the OSSEC deb packages set 440 on the
         # keyfiles; the Ansible config should be updated to do the same.
@@ -46,30 +46,30 @@ def test_ossec_keyfiles(File, Sudo, keyfile):
 
 # Permissions don't match between Ansible and OSSEC deb packages postinst.
 @pytest.mark.xfail
-def test_procmail_log(File, Sudo):
+def test_procmail_log(host):
     """
     Ensure procmail log file exist with proper ownership.
     Only the ossec user should have read/write permissions.
     """
-    with Sudo():
-        f = File("/var/log/procmail.log")
+    with host.sudo():
+        f = host.file("/var/log/procmail.log")
         assert f.is_file
         assert f.user == "ossec"
         assert f.group == "root"
         assert oct(f.mode) == "0660"
 
 
-def test_ossec_authd(Command, Sudo):
+def test_ossec_authd(host):
     """ Ensure that authd is not running """
-    with Sudo():
-        c = Command("pgrep ossec-authd")
+    with host.sudo():
+        c = host.run("pgrep ossec-authd")
         assert c.stdout == ""
         assert c.rc != 0
 
 
-def test_hosts_files(File, SystemInfo):
+def test_hosts_files(host):
     """ Ensure host files mapping are in place """
-    f = File('/etc/hosts')
+    f = host.file('/etc/hosts')
 
     app_ip = os.environ.get('APP_IP', securedrop_test_vars.app_ip)
     app_host = securedrop_test_vars.app_hostname
@@ -78,7 +78,7 @@ def test_hosts_files(File, SystemInfo):
     assert f.contains('^{}\s*{}$'.format(app_ip, app_host))
 
 
-def test_ossec_log_contains_no_malformed_events(File, Sudo):
+def test_ossec_log_contains_no_malformed_events(host):
     """
     Ensure the OSSEC log reports no errors for incorrectly formatted
     messages. These events indicate that the OSSEC server failed to decrypt
@@ -88,11 +88,11 @@ def test_ossec_log_contains_no_malformed_events(File, Sudo):
     Documentation regarding this error message can be found at:
     http://ossec-docs.readthedocs.io/en/latest/faq/unexpected.html#id4
     """
-    with Sudo():
-        f = File("/var/ossec/logs/ossec.log")
+    with host.sudo():
+        f = host.file("/var/ossec/logs/ossec.log")
         assert not f.contains("ERROR: Incorrectly formated message from")
 
 
-def test_regression_hosts(Command):
+def test_regression_hosts(host):
     """ Regression test to check for duplicate entries. """
-    assert Command.check_output("uniq --repeated /etc/hosts") == ""
+    assert host.check_output("uniq --repeated /etc/hosts") == ""
diff --git a/molecule/testinfra/staging/mon/test_postfix.py b/molecule/testinfra/staging/mon/test_postfix.py
--- a/molecule/testinfra/staging/mon/test_postfix.py
+++ b/molecule/testinfra/staging/mon/test_postfix.py
@@ -13,20 +13,20 @@
     '/^User-Agent:/  IGNORE',
     '/^Received:/    IGNORE',
 ])
-def test_postfix_headers(File, header):
+def test_postfix_headers(host, header):
     """
     Ensure postfix header filters are set correctly. Common mail headers
     are stripped by default to avoid leaking metadata about the instance.
     Message body is always encrypted prior to sending.
     """
-    f = File("/etc/postfix/header_checks")
+    f = host.file("/etc/postfix/header_checks")
     assert f.is_file
     assert oct(f.mode) == "0644"
     regex = '^{}$'.format(re.escape(header))
     assert re.search(regex, f.content, re.M)
 
 
-def test_postfix_generic_maps(File):
+def test_postfix_generic_maps(host):
     """
     Regression test to check that generic Postfix maps are not configured
     by default. As of #1565 Admins can opt-in to overriding the FROM address
@@ -34,11 +34,11 @@ def test_postfix_generic_maps(File):
     `ossec@ossec.server` behavior, to avoid breaking email for previously
     existing instances.
     """
-    assert not File("/etc/postfix/generic").exists
-    assert not File("/etc/postfix/main.cf").contains("^smtp_generic_maps")
+    assert not host.file("/etc/postfix/generic").exists
+    assert not host.file("/etc/postfix/main.cf").contains("^smtp_generic_maps")
 
 
-def test_postfix_service(Service, Socket, Sudo):
+def test_postfix_service(host):
     """
     Check Postfix service. Postfix is used to deliver OSSEC alerts via
     encrypted email. On staging hosts, Postfix is disabled, due to lack
@@ -46,10 +46,10 @@ def test_postfix_service(Service, Socket, Sudo):
     """
     # Elevated privileges are required to read Postfix service info,
     # specifically `/var/spool/postfix/pid/master.pid`.
-    with Sudo():
-        postfix = Service("postfix")
+    with host.sudo():
+        postfix = host.service("postfix")
         assert postfix.is_running == securedrop_test_vars.postfix_enabled
         assert postfix.is_enabled == securedrop_test_vars.postfix_enabled
 
-        socket = Socket("tcp://127.0.0.1:25")
+        socket = host.socket("tcp://127.0.0.1:25")
         assert socket.is_listening == securedrop_test_vars.postfix_enabled