]> git-server-git.apps.pok.os.sepia.ceph.com Git - teuthology.git/commitdiff
Move tests to tests/ 2219/head
authorZack Cerza <zack@cerza.org>
Thu, 18 Jun 2026 00:30:23 +0000 (18:30 -0600)
committerZack Cerza <zack@cerza.org>
Thu, 18 Jun 2026 17:23:14 +0000 (11:23 -0600)
This has more or less become the standard. It is also the best way for us to
avoid shipping tests in the wheel, because of setuptools' behavior when using
pyproject.toml. For more information, see:
https://github.com/pypa/setuptools/issues/3260

Signed-off-by: Zack Cerza <zack@cerza.org>
212 files changed:
.github/workflows/ci.yml
pyproject.toml
scripts/test/script.py [deleted file]
scripts/test/test_dispatcher_.py [deleted file]
scripts/test/test_exporter_.py [deleted file]
scripts/test/test_lock.py [deleted file]
scripts/test/test_ls.py [deleted file]
scripts/test/test_prune_logs.py [deleted file]
scripts/test/test_report.py [deleted file]
scripts/test/test_results.py [deleted file]
scripts/test/test_run.py [deleted file]
scripts/test/test_schedule.py [deleted file]
scripts/test/test_suite.py [deleted file]
scripts/test/test_supervisor_.py [deleted file]
scripts/test/test_updatekeys.py [deleted file]
tests/__init__.py [new file with mode: 0644]
tests/dispatcher/test_dispatcher.py [new file with mode: 0644]
tests/dispatcher/test_reimage_error_mark_machine_down.py [new file with mode: 0644]
tests/dispatcher/test_supervisor.py [new file with mode: 0644]
tests/fake_archive.py [new file with mode: 0644]
tests/fake_fs.py [new file with mode: 0644]
tests/integration/__init__.py [new file with mode: 0644]
tests/integration/test_suite.py [new file with mode: 0644]
tests/lock/__init__.py [new file with mode: 0644]
tests/lock/test_lock.py [new file with mode: 0644]
tests/openstack/__init__.py [new file with mode: 0644]
tests/openstack/archive-on-error.yaml [new file with mode: 0644]
tests/openstack/noop.yaml [new file with mode: 0644]
tests/openstack/openstack-integration.py [new file with mode: 0644]
tests/openstack/resources_hint.yaml [new file with mode: 0644]
tests/openstack/resources_hint_no_cinder.yaml [new file with mode: 0644]
tests/openstack/stop_worker.yaml [new file with mode: 0644]
tests/openstack/suites/noop/+ [new file with mode: 0644]
tests/openstack/suites/noop/noop.yaml [new file with mode: 0644]
tests/openstack/suites/nuke/+ [new file with mode: 0644]
tests/openstack/test_config.py [new file with mode: 0644]
tests/openstack/test_openstack.py [new file with mode: 0644]
tests/openstack/user-data-test1.txt [new file with mode: 0644]
tests/orchestra/__init__.py [new file with mode: 0644]
tests/orchestra/files/daemon-systemdstate-pid-ps-ef.output [new file with mode: 0644]
tests/orchestra/integration/__init__.py [new file with mode: 0644]
tests/orchestra/integration/test_integration.py [new file with mode: 0644]
tests/orchestra/test_cluster.py [new file with mode: 0644]
tests/orchestra/test_connection.py [new file with mode: 0644]
tests/orchestra/test_console.py [new file with mode: 0644]
tests/orchestra/test_opsys.py [new file with mode: 0644]
tests/orchestra/test_remote.py [new file with mode: 0644]
tests/orchestra/test_run.py [new file with mode: 0644]
tests/orchestra/test_systemd.py [new file with mode: 0644]
tests/orchestra/util.py [new file with mode: 0644]
tests/provision/cloud/test_base.py [new file with mode: 0644]
tests/provision/cloud/test_cloud_init.py [new file with mode: 0644]
tests/provision/cloud/test_cloud_util.py [new file with mode: 0644]
tests/provision/cloud/test_openstack.py [new file with mode: 0644]
tests/provision/cloud/test_openstack_userdata_conf.yaml [new file with mode: 0644]
tests/provision/test_downburst.py [new file with mode: 0644]
tests/provision/test_fog.py [new file with mode: 0644]
tests/provision/test_init_provision.py [new file with mode: 0644]
tests/provision/test_maas.py [new file with mode: 0644]
tests/provision/test_pelagos.py [new file with mode: 0644]
tests/scripts/script.py [new file with mode: 0644]
tests/scripts/test_dispatcher_.py [new file with mode: 0644]
tests/scripts/test_exporter_.py [new file with mode: 0644]
tests/scripts/test_lock.py [new file with mode: 0644]
tests/scripts/test_ls.py [new file with mode: 0644]
tests/scripts/test_prune_logs.py [new file with mode: 0644]
tests/scripts/test_report.py [new file with mode: 0644]
tests/scripts/test_results.py [new file with mode: 0644]
tests/scripts/test_run.py [new file with mode: 0644]
tests/scripts/test_schedule.py [new file with mode: 0644]
tests/scripts/test_suite.py [new file with mode: 0644]
tests/scripts/test_supervisor_.py [new file with mode: 0644]
tests/scripts/test_updatekeys.py [new file with mode: 0644]
tests/suite/conftest.py [new file with mode: 0644]
tests/suite/suites/noop/noop.yaml [new file with mode: 0644]
tests/suite/test_build_matrix.py [new file with mode: 0644]
tests/suite/test_init.py [new file with mode: 0644]
tests/suite/test_matrix.py [new file with mode: 0644]
tests/suite/test_merge.py [new file with mode: 0644]
tests/suite/test_placeholder.py [new file with mode: 0644]
tests/suite/test_run_.py [new file with mode: 0644]
tests/suite/test_util.py [new file with mode: 0644]
tests/task/__init__.py [new file with mode: 0644]
tests/task/test_ansible.py [new file with mode: 0644]
tests/task/test_ceph_ansible.py [new file with mode: 0644]
tests/task/test_console_log.py [new file with mode: 0644]
tests/task/test_install.py [new file with mode: 0644]
tests/task/test_internal.py [new file with mode: 0644]
tests/task/test_kernel.py [new file with mode: 0644]
tests/task/test_pcp.py [new file with mode: 0644]
tests/task/test_selinux.py [new file with mode: 0644]
tests/test_config.py [new file with mode: 0644]
tests/test_contextutil.py [new file with mode: 0644]
tests/test_describe_tests.py [new file with mode: 0644]
tests/test_email_sleep_before_teardown.py [new file with mode: 0644]
tests/test_exit.py [new file with mode: 0644]
tests/test_get_distro.py [new file with mode: 0644]
tests/test_get_distro_version.py [new file with mode: 0644]
tests/test_get_multi_machine_types.py [new file with mode: 0644]
tests/test_imports.py [new file with mode: 0644]
tests/test_job_status.py [new file with mode: 0644]
tests/test_kill.py [new file with mode: 0644]
tests/test_ls.py [new file with mode: 0644]
tests/test_misc.py [new file with mode: 0644]
tests/test_packaging.py [new file with mode: 0644]
tests/test_parallel.py [new file with mode: 0644]
tests/test_repo_utils.py [new file with mode: 0644]
tests/test_report.py [new file with mode: 0644]
tests/test_results.py [new file with mode: 0644]
tests/test_run.py [new file with mode: 0644]
tests/test_safepath.py [new file with mode: 0644]
tests/test_schedule.py [new file with mode: 0644]
tests/test_scrape.py [new file with mode: 0644]
tests/test_timer.py [new file with mode: 0644]
tests/test_vps_os_vers_parameter_checking.py [new file with mode: 0644]
tests/util/files/test_unit_test.xml [new file with mode: 0644]
tests/util/files/test_valgrind.xml [new file with mode: 0644]
tests/util/test_scanner.py [new file with mode: 0644]
tests/util/test_time.py [new file with mode: 0644]
teuthology/dispatcher/test/test_dispatcher.py [deleted file]
teuthology/dispatcher/test/test_reimage_error_mark_machine_down.py [deleted file]
teuthology/dispatcher/test/test_supervisor.py [deleted file]
teuthology/lock/test/__init__.py [deleted file]
teuthology/lock/test/test_lock.py [deleted file]
teuthology/openstack/test/__init__.py [deleted file]
teuthology/openstack/test/archive-on-error.yaml [deleted file]
teuthology/openstack/test/noop.yaml [deleted file]
teuthology/openstack/test/openstack-integration.py [deleted file]
teuthology/openstack/test/resources_hint.yaml [deleted file]
teuthology/openstack/test/resources_hint_no_cinder.yaml [deleted file]
teuthology/openstack/test/stop_worker.yaml [deleted file]
teuthology/openstack/test/suites/noop/+ [deleted file]
teuthology/openstack/test/suites/noop/noop.yaml [deleted file]
teuthology/openstack/test/suites/nuke/+ [deleted file]
teuthology/openstack/test/test_config.py [deleted file]
teuthology/openstack/test/test_openstack.py [deleted file]
teuthology/openstack/test/user-data-test1.txt [deleted file]
teuthology/orchestra/test/__init__.py [deleted file]
teuthology/orchestra/test/files/daemon-systemdstate-pid-ps-ef.output [deleted file]
teuthology/orchestra/test/integration/__init__.py [deleted file]
teuthology/orchestra/test/integration/test_integration.py [deleted file]
teuthology/orchestra/test/test_cluster.py [deleted file]
teuthology/orchestra/test/test_connection.py [deleted file]
teuthology/orchestra/test/test_console.py [deleted file]
teuthology/orchestra/test/test_opsys.py [deleted file]
teuthology/orchestra/test/test_remote.py [deleted file]
teuthology/orchestra/test/test_run.py [deleted file]
teuthology/orchestra/test/test_systemd.py [deleted file]
teuthology/orchestra/test/util.py [deleted file]
teuthology/provision/cloud/test/test_base.py [deleted file]
teuthology/provision/cloud/test/test_cloud_init.py [deleted file]
teuthology/provision/cloud/test/test_cloud_util.py [deleted file]
teuthology/provision/cloud/test/test_openstack.py [deleted file]
teuthology/provision/cloud/test/test_openstack_userdata_conf.yaml [deleted file]
teuthology/provision/test/test_downburst.py [deleted file]
teuthology/provision/test/test_fog.py [deleted file]
teuthology/provision/test/test_init_provision.py [deleted file]
teuthology/provision/test/test_maas.py [deleted file]
teuthology/provision/test/test_pelagos.py [deleted file]
teuthology/suite/test/conftest.py [deleted file]
teuthology/suite/test/suites/noop/noop.yaml [deleted file]
teuthology/suite/test/test_build_matrix.py [deleted file]
teuthology/suite/test/test_init.py [deleted file]
teuthology/suite/test/test_matrix.py [deleted file]
teuthology/suite/test/test_merge.py [deleted file]
teuthology/suite/test/test_placeholder.py [deleted file]
teuthology/suite/test/test_run_.py [deleted file]
teuthology/suite/test/test_util.py [deleted file]
teuthology/task/tests/__init__.py
teuthology/test/__init__.py [deleted file]
teuthology/test/fake_archive.py [deleted file]
teuthology/test/fake_fs.py [deleted file]
teuthology/test/integration/__init__.py [deleted file]
teuthology/test/integration/test_suite.py [deleted file]
teuthology/test/task/__init__.py [deleted file]
teuthology/test/task/test_ansible.py [deleted file]
teuthology/test/task/test_ceph_ansible.py [deleted file]
teuthology/test/task/test_console_log.py [deleted file]
teuthology/test/task/test_install.py [deleted file]
teuthology/test/task/test_internal.py [deleted file]
teuthology/test/task/test_kernel.py [deleted file]
teuthology/test/task/test_pcp.py [deleted file]
teuthology/test/task/test_selinux.py [deleted file]
teuthology/test/test_config.py [deleted file]
teuthology/test/test_contextutil.py [deleted file]
teuthology/test/test_describe_tests.py [deleted file]
teuthology/test/test_email_sleep_before_teardown.py [deleted file]
teuthology/test/test_exit.py [deleted file]
teuthology/test/test_get_distro.py [deleted file]
teuthology/test/test_get_distro_version.py [deleted file]
teuthology/test/test_get_multi_machine_types.py [deleted file]
teuthology/test/test_imports.py [deleted file]
teuthology/test/test_job_status.py [deleted file]
teuthology/test/test_kill.py [deleted file]
teuthology/test/test_ls.py [deleted file]
teuthology/test/test_misc.py [deleted file]
teuthology/test/test_packaging.py [deleted file]
teuthology/test/test_parallel.py [deleted file]
teuthology/test/test_repo_utils.py [deleted file]
teuthology/test/test_report.py [deleted file]
teuthology/test/test_results.py [deleted file]
teuthology/test/test_run.py [deleted file]
teuthology/test/test_safepath.py [deleted file]
teuthology/test/test_schedule.py [deleted file]
teuthology/test/test_scrape.py [deleted file]
teuthology/test/test_timer.py [deleted file]
teuthology/test/test_vps_os_vers_parameter_checking.py [deleted file]
teuthology/util/test/files/test_unit_test.xml [deleted file]
teuthology/util/test/files/test_valgrind.xml [deleted file]
teuthology/util/test/test_scanner.py [deleted file]
teuthology/util/test/test_time.py [deleted file]
tox.ini

index 2f3e0f4a75d359c0574700b1ce514de2977fd89c..b61a2242cf5ac2ac2873b784b2ca400707aae621 100644 (file)
@@ -34,6 +34,6 @@ jobs:
       - name: Lint
         run: uv tool run ruff check
       - name: Run tests
-        run: uv run --all-extras pytest teuthology scripts
+        run: uv run --all-extras pytest
       - name: Run docs build
         run: uv run tox -e docs
index 6320ca3eec8303b01c67d2f295fb8ef7bbb1136d..163bede98450e7ba97fe305945c0c14a1ebd981b 100644 (file)
@@ -127,13 +127,10 @@ cache-keys = [{ file = "pyproject.toml" }, { git = { commit = true, tags = true
 
 [tool.setuptools.packages.find]
 exclude = [
-    "*test*",
+    "tests*",
     "docs*",
 ]
 
-[tool.setuptools.exclude-package-data]
-"*" = ["test*"]
-
 [tool.setuptools_scm]
 version_scheme = "python-simplified-semver"
 
@@ -141,7 +138,7 @@ version_scheme = "python-simplified-semver"
 lint.select = ["F", "E9"]
 
 [tool.pytest]
-testpaths = ["teuthology", "scripts"]
+testpaths = ["tests"]
 norecursedirs = [".git", "build", "virtualenv", "teuthology.egg-info", ".tox", "*/integration", "teuthology/task/tests"]
 log_cli = true
 log_level = "NOTSET"
diff --git a/scripts/test/script.py b/scripts/test/script.py
deleted file mode 100644 (file)
index a178c8a..0000000
+++ /dev/null
@@ -1,53 +0,0 @@
-import sys
-import pytest
-from importlib import import_module
-
-
-class Script(object):
-    script_name = "teuthology"
-    script_module = None  # Override in subclasses, e.g., 'scripts.run'
-
-    @pytest.fixture(scope="class")
-    def module_name(self) -> str:
-        # e.g., 'teuthology-dispatcher' -> 'scripts.dispatcher'
-        return self.script_name.replace("teuthology-", "").replace("teuthology", "run")
-
-    @pytest.fixture(scope="class")
-    def module(self, module_name):
-        return import_module(self.script_module or f"scripts.{module_name}")
-
-    def test_help(self, capsys: pytest.CaptureFixture[str], module):
-        # docopt
-        if module.__doc__ and "usage" in module.__doc__.lower():
-            return
-        if hasattr(module, "doc") and module.doc and "usage" in module.doc.lower():
-            return
-        # argparse
-        if hasattr(module, "parse_args"):
-            with pytest.raises(SystemExit):
-                module.parse_args([])
-            captured = capsys.readouterr()
-            assert "usage: " in captured.err
-            return
-        # If neither, fail
-        raise AssertionError(
-            f"{self.script_name} has neither a docstring/doc variable with usage info "
-            f"nor a parse_args function"
-        )
-
-    def test_invalid(self, module):
-        original_argv = sys.argv
-        try:
-            sys.argv = [self.script_name, "--invalid-option"]
-            with pytest.raises(SystemExit):
-                if hasattr(module, "parse_args"):
-                    module.parse_args(sys.argv[1:])
-                elif hasattr(module, "main"):
-                    # For docopt-based scripts, main() will call docopt which exits on error
-                    module.main()
-                else:
-                    raise NotImplementedError(
-                        f"Don't know how to test {self.script_name}"
-                    )
-        finally:
-            sys.argv = original_argv
diff --git a/scripts/test/test_dispatcher_.py b/scripts/test/test_dispatcher_.py
deleted file mode 100644 (file)
index 3dc1659..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-from script import Script
-
-
-class TestDispatcher(Script):
-    script_name = 'teuthology-dispatcher'
-    script_module = 'scripts.dispatcher'
diff --git a/scripts/test/test_exporter_.py b/scripts/test/test_exporter_.py
deleted file mode 100644 (file)
index 0d3c171..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-from script import Script
-
-
-class TestExporter(Script):
-    script_name = 'teuthology-exporter'
-    script_module = 'scripts.exporter'
diff --git a/scripts/test/test_lock.py b/scripts/test/test_lock.py
deleted file mode 100644 (file)
index b1687c1..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-from script import Script
-
-
-class TestLock(Script):
-    script_name = 'teuthology-lock'
-    script_module = 'scripts.lock'
diff --git a/scripts/test/test_ls.py b/scripts/test/test_ls.py
deleted file mode 100644 (file)
index 2965789..0000000
+++ /dev/null
@@ -1,16 +0,0 @@
-import docopt
-
-from script import Script
-from scripts import ls
-
-doc = ls.__doc__
-
-
-class TestLs(Script):
-    script_name = 'teuthology-ls'
-    script_module = 'scripts.ls'
-
-    def test_args(self):
-        args = docopt.docopt(doc, ["--verbose", "some/archive/dir"])
-        assert args["--verbose"]
-        assert args["<archive_dir>"] == "some/archive/dir"
diff --git a/scripts/test/test_prune_logs.py b/scripts/test/test_prune_logs.py
deleted file mode 100644 (file)
index 1688017..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-from script import Script
-
-
-class TestPruneLogs(Script):
-    script_name = 'teuthology-prune-logs'
-    script_module = 'scripts.prune_logs'
diff --git a/scripts/test/test_report.py b/scripts/test/test_report.py
deleted file mode 100644 (file)
index 40b750a..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-from script import Script
-
-
-class TestReport(Script):
-    script_name = 'teuthology-report'
-    script_module = 'scripts.report'
diff --git a/scripts/test/test_results.py b/scripts/test/test_results.py
deleted file mode 100644 (file)
index 9f201b8..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-from script import Script
-
-
-class TestResults(Script):
-    script_name = 'teuthology-results'
-    script_module = 'scripts.results'
diff --git a/scripts/test/test_run.py b/scripts/test/test_run.py
deleted file mode 100644 (file)
index de33e6e..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-import docopt
-
-from script import Script
-from scripts import run
-
-doc = run.__doc__
-
-
-class TestRun(Script):
-    script_name = 'teuthology'
-    script_module = 'scripts.run'
-
-    def test_all_args(self):
-        args = docopt.docopt(doc, [
-            "--verbose",
-            "--archive", "some/archive/dir",
-            "--description", "the_description",
-            "--owner", "the_owner",
-            "--lock",
-            "--machine-type", "machine_type",
-            "--os-type", "os_type",
-            "--os-version", "os_version",
-            "--block",
-            "--name", "the_name",
-            "--suite-path", "some/suite/dir",
-            "path/to/config.yml",
-        ])
-        assert args["--verbose"]
-        assert args["--archive"] == "some/archive/dir"
-        assert args["--description"] == "the_description"
-        assert args["--owner"] == "the_owner"
-        assert args["--lock"]
-        assert args["--machine-type"] == "machine_type"
-        assert args["--os-type"] == "os_type"
-        assert args["--os-version"] == "os_version"
-        assert args["--block"]
-        assert args["--name"] == "the_name"
-        assert args["--suite-path"] == "some/suite/dir"
-        assert args["<config>"] == ["path/to/config.yml"]
-
-    def test_multiple_configs(self):
-        args = docopt.docopt(doc, [
-            "config1.yml",
-            "config2.yml",
-        ])
-        assert args["<config>"] == ["config1.yml", "config2.yml"]
diff --git a/scripts/test/test_schedule.py b/scripts/test/test_schedule.py
deleted file mode 100644 (file)
index d69c13a..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-from script import Script
-
-
-class TestSchedule(Script):
-    script_name = 'teuthology-schedule'
-    script_module = 'scripts.schedule'
diff --git a/scripts/test/test_suite.py b/scripts/test/test_suite.py
deleted file mode 100644 (file)
index f890c80..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-from script import Script
-
-
-class TestSuite(Script):
-    script_name = 'teuthology-suite'
-    script_module = 'scripts.suite'
diff --git a/scripts/test/test_supervisor_.py b/scripts/test/test_supervisor_.py
deleted file mode 100644 (file)
index dad7882..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-from script import Script
-
-
-class TestSupervisor(Script):
-    script_name = 'teuthology-supervisor'
-    script_module = 'scripts.supervisor'
diff --git a/scripts/test/test_updatekeys.py b/scripts/test/test_updatekeys.py
deleted file mode 100644 (file)
index 5a38829..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-from script import Script
-import docopt
-from pytest import raises
-from pytest import skip
-from scripts import updatekeys
-
-
-class TestUpdatekeys(Script):
-    script_name = 'teuthology-updatekeys'
-    script_module = 'scripts.updatekeys'
-
-    def test_invalid(self):
-        skip("teuthology.lock needs to be partially refactored to allow" +
-             "teuthology-updatekeys to return nonzero in all erorr cases")
-
-    def test_all_and_targets(self):
-        with raises(docopt.DocoptExit):
-            docopt.docopt(updatekeys.doc, ['-a', '-t', 'foo'])
-
-    def test_no_args(self):
-        with raises(docopt.DocoptExit):
-            docopt.docopt(updatekeys.doc, [])
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644 (file)
index 0000000..1eb9e81
--- /dev/null
@@ -0,0 +1,9 @@
+import os
+import pytest
+import sys
+
+skipif_teuthology_process = pytest.mark.skipif(
+    os.path.basename(sys.argv[0]) == "teuthology",
+    reason="Skipped because this test cannot pass when run in a teuthology " \
+        "process (as opposed to py.test)"
+)
\ No newline at end of file
diff --git a/tests/dispatcher/test_dispatcher.py b/tests/dispatcher/test_dispatcher.py
new file mode 100644 (file)
index 0000000..cf35aff
--- /dev/null
@@ -0,0 +1,169 @@
+import datetime
+import pytest
+
+from unittest.mock import patch, Mock, MagicMock
+
+from teuthology import dispatcher
+from teuthology.config import FakeNamespace
+from teuthology.contextutil import MaxWhileTries
+from teuthology.util.time import TIMESTAMP_FMT
+
+
+class TestDispatcher(object):
+    @pytest.fixture(autouse=True)
+    def setup_method(self, tmp_path):
+        self.ctx = FakeNamespace()
+        self.ctx.verbose = True
+        self.ctx.archive_dir = str(tmp_path / "archive/dir")
+        self.ctx.log_dir = str(tmp_path / "log/dir")
+        self.ctx.tube = 'tube'
+
+    @patch("teuthology.repo_utils.ls_remote")
+    @patch("os.path.isdir")
+    @patch("teuthology.repo_utils.fetch_teuthology")
+    @patch("teuthology.dispatcher.teuth_config")
+    @patch("teuthology.repo_utils.fetch_qa_suite")
+    def test_prep_job(self, m_fetch_qa_suite, m_teuth_config,
+            m_fetch_teuthology, m_isdir, m_ls_remote):
+        config = dict(
+            name="the_name",
+            job_id="1",
+            suite_sha1="suite_hash",
+        )
+        m_fetch_teuthology.return_value = '/teuth/path'
+        m_fetch_qa_suite.return_value = '/suite/path'
+        m_ls_remote.return_value = 'teuth_hash'
+        m_isdir.return_value = True
+        m_teuth_config.teuthology_path = None
+        got_config, teuth_bin_path = dispatcher.prep_job(config)
+        assert got_config['teuthology_branch'] == 'main'
+        m_fetch_teuthology.assert_called_once_with(branch='main', commit='teuth_hash')
+        assert teuth_bin_path == '/teuth/path/.venv/bin'
+        m_fetch_qa_suite.assert_called_once_with('main', 'suite_hash')
+        assert got_config['suite_path'] == '/suite/path'
+
+    def build_fake_jobs(self, m_connection, m_job, job_bodies):
+        """
+        Given patched copies of:
+            beanstalkc.Connection
+            beanstalkc.Job
+        And a list of basic job bodies, return a list of mocked Job objects
+        """
+        # Make sure instantiating m_job returns a new object each time
+        jobs = []
+        job_id = 0
+        for job_body in job_bodies:
+            job_id += 1
+            job = MagicMock(conn=m_connection, jid=job_id, body=job_body)
+            job.jid = job_id
+            job.body = job_body
+            jobs.append(job)
+        return jobs
+
+    @patch("teuthology.dispatcher.find_dispatcher_processes")
+    @patch("teuthology.repo_utils.ls_remote")
+    @patch("teuthology.dispatcher.report.try_push_job_info")
+    @patch("teuthology.dispatcher.supervisor.run_job")
+    @patch("beanstalkc.Job", autospec=True)
+    @patch("teuthology.repo_utils.fetch_qa_suite")
+    @patch("teuthology.repo_utils.fetch_teuthology")
+    @patch("teuthology.dispatcher.beanstalk.watch_tube")
+    @patch("teuthology.dispatcher.beanstalk.connect")
+    @patch("os.path.isdir", return_value=True)
+    @patch("teuthology.dispatcher.setup_log_file")
+    def test_main_loop(
+        self, m_setup_log_file, m_isdir, m_connect, m_watch_tube,
+        m_fetch_teuthology, m_fetch_qa_suite, m_job, m_run_job,
+        m_try_push_job_info, m_ls_remote, m_find_dispatcher_processes,
+                       ):
+        m_find_dispatcher_processes.return_value = {}
+        m_connection = Mock()
+        jobs = self.build_fake_jobs(
+            m_connection,
+            m_job,
+            [
+                'name: name\nfoo: bar',
+                'name: name\nstop_worker: true',
+            ],
+        )
+        m_connection.reserve.side_effect = jobs
+        m_connect.return_value = m_connection
+        dispatcher.main(self.ctx)
+        # There should be one reserve call per item in the jobs list
+        expected_reserve_calls = [
+            dict(timeout=60) for i in range(len(jobs))
+        ]
+        got_reserve_calls = [
+            call[1] for call in m_connection.reserve.call_args_list
+        ]
+        assert got_reserve_calls == expected_reserve_calls
+        for job in jobs:
+            job.bury.assert_called_once_with()
+            job.delete.assert_called_once_with()
+
+    @patch("teuthology.dispatcher.find_dispatcher_processes")
+    @patch("teuthology.repo_utils.ls_remote")
+    @patch("teuthology.dispatcher.report.try_push_job_info")
+    @patch("teuthology.dispatcher.supervisor.run_job")
+    @patch("beanstalkc.Job", autospec=True)
+    @patch("teuthology.repo_utils.fetch_qa_suite")
+    @patch("teuthology.repo_utils.fetch_teuthology")
+    @patch("teuthology.dispatcher.beanstalk.watch_tube")
+    @patch("teuthology.dispatcher.beanstalk.connect")
+    @patch("os.path.isdir", return_value=True)
+    @patch("teuthology.dispatcher.setup_log_file")
+    def test_main_loop_13925(
+        self, m_setup_log_file, m_isdir, m_connect, m_watch_tube,
+        m_fetch_teuthology, m_fetch_qa_suite, m_job, m_run_job,
+        m_try_push_job_info, m_ls_remote, m_find_dispatcher_processes,
+                       ):
+        m_find_dispatcher_processes.return_value = {}
+        m_connection = Mock()
+        jobs = self.build_fake_jobs(
+            m_connection,
+            m_job,
+            [
+                'name: name',
+                'name: name\nstop_worker: true',
+            ],
+        )
+        m_connection.reserve.side_effect = jobs
+        m_connect.return_value = m_connection
+        m_fetch_qa_suite.side_effect = [
+            MaxWhileTries(),
+            MaxWhileTries(),
+        ]
+        dispatcher.main(self.ctx)
+        assert len(m_run_job.call_args_list) == 0
+        assert len(m_try_push_job_info.call_args_list) == len(jobs)
+        for i in range(len(jobs)):
+            push_call = m_try_push_job_info.call_args_list[i]
+            assert push_call[0][1]['status'] == 'dead'
+
+    @pytest.mark.parametrize(
+        ["timestamp", "expire", "skip"],
+        [
+            [datetime.timedelta(days=-1), None, False],
+            [datetime.timedelta(days=-30), None, True],
+            [None, datetime.timedelta(days=1), False],
+            [None, datetime.timedelta(days=-1), True],
+            [datetime.timedelta(days=-1), datetime.timedelta(days=1), False],
+            [datetime.timedelta(days=1), datetime.timedelta(days=-1), True],
+        ]
+    )
+    @patch("teuthology.dispatcher.report.try_push_job_info")
+    def test_check_job_expiration(self, _, timestamp, expire, skip):
+        now = datetime.datetime.now(datetime.timezone.utc)
+        job_config = dict(
+            job_id="1",
+            name="job_name",
+        )
+        if timestamp:
+            job_config["timestamp"] = (now + timestamp).strftime(TIMESTAMP_FMT)
+        if expire:
+            job_config["expire"] = (now + expire).strftime(TIMESTAMP_FMT)
+        if skip:
+            with pytest.raises(dispatcher.SkipJob):
+                dispatcher.check_job_expiration(job_config)
+        else:
+            dispatcher.check_job_expiration(job_config)
diff --git a/tests/dispatcher/test_reimage_error_mark_machine_down.py b/tests/dispatcher/test_reimage_error_mark_machine_down.py
new file mode 100644 (file)
index 0000000..63ec8eb
--- /dev/null
@@ -0,0 +1,104 @@
+from teuthology.dispatcher import supervisor
+from unittest.mock import patch
+
+class TestCheckReImageFailureMarkDown(object):
+    def setup_method(self):
+        self.the_function = supervisor.check_for_reimage_failures_and_mark_down
+
+    def create_n_out_of_10_reimage_failed_jobs(self, n):
+        ret_list = []
+        for i in range(n):
+            obj1 = {
+              "failure_reason":"Error reimaging machines: Manually raised error"
+              }
+            ret_list.append(obj1)
+        for j in range(10-n):
+            obj2 = {"failure_reason":"Error something else: dummy"}
+            ret_list.append(obj2)
+        return ret_list
+
+    @patch('teuthology.dispatcher.supervisor.shortname')
+    @patch('teuthology.lock.ops.update_lock')
+    @patch('teuthology.dispatcher.supervisor.requests')
+    @patch('teuthology.dispatcher.supervisor.urljoin')
+    @patch('teuthology.dispatcher.supervisor.teuth_config')
+    def test_one_machine_ten_reimage_failed_jobs(
+        self,
+        m_t_config,
+        m_urljoin,
+        m_requests,
+        mark_down,
+        shortname
+        ):
+        targets = {'fakeos@rmachine061.front.sepia.ceph.com': 'ssh-ed25519'}
+        m_requests.get.return_value.json.return_value = \
+            self.create_n_out_of_10_reimage_failed_jobs(10)
+        shortname.return_value = 'rmachine061'
+        self.the_function(targets)
+        assert mark_down.called
+
+    @patch('teuthology.dispatcher.supervisor.shortname')
+    @patch('teuthology.lock.ops.update_lock')
+    @patch('teuthology.dispatcher.supervisor.requests')
+    @patch('teuthology.dispatcher.supervisor.urljoin')
+    @patch('teuthology.dispatcher.supervisor.teuth_config')
+    def test_one_machine_seven_reimage_failed_jobs(
+        self,
+        m_t_config,
+        m_urljoin,
+        m_requests,
+        mark_down,
+        shortname,
+        ):
+        targets = {'fakeos@rmachine061.front.sepia.ceph.com': 'ssh-ed25519'}
+        m_requests.get.return_value.json.return_value = \
+            self.create_n_out_of_10_reimage_failed_jobs(7)
+        shortname.return_value = 'rmachine061'
+        self.the_function(targets)
+        assert mark_down.called is False
+
+    @patch('teuthology.dispatcher.supervisor.shortname')
+    @patch('teuthology.lock.ops.update_lock')
+    @patch('teuthology.dispatcher.supervisor.requests')
+    @patch('teuthology.dispatcher.supervisor.urljoin')
+    @patch('teuthology.dispatcher.supervisor.teuth_config')
+    def test_two_machine_all_reimage_failed_jobs(
+        self,
+        m_t_config,
+        m_urljoin,
+        m_requests,
+        mark_down,
+        shortname,
+        ):
+        targets = {'fakeos@rmachine061.front.sepia.ceph.com': 'ssh-ed25519',
+                   'fakeos@rmachine179.back.sepia.ceph.com': 'ssh-ed45333'}
+        m_requests.get.return_value.json.side_effect = \
+            [self.create_n_out_of_10_reimage_failed_jobs(10),
+            self.create_n_out_of_10_reimage_failed_jobs(10)]
+        shortname.return_value.side_effect = ['rmachine061', 'rmachine179']
+        self.the_function(targets)
+        assert mark_down.call_count == 2
+
+    @patch('teuthology.dispatcher.supervisor.shortname')
+    @patch('teuthology.lock.ops.update_lock')
+    @patch('teuthology.dispatcher.supervisor.requests')
+    @patch('teuthology.dispatcher.supervisor.urljoin')
+    @patch('teuthology.dispatcher.supervisor.teuth_config')
+    def test_two_machine_one_healthy_one_reimage_failure(
+        self,
+        m_t_config,
+        m_urljoin,
+        m_requests,
+        mark_down,
+        shortname,
+        ):
+        targets = {'fakeos@rmachine061.front.sepia.ceph.com': 'ssh-ed25519',
+                   'fakeos@rmachine179.back.sepia.ceph.com': 'ssh-ed45333'}
+        m_requests.get.return_value.json.side_effect = \
+            [self.create_n_out_of_10_reimage_failed_jobs(0),
+            self.create_n_out_of_10_reimage_failed_jobs(10)]
+        shortname.return_value.side_effect = ['rmachine061', 'rmachine179']
+        self.the_function(targets)
+        assert mark_down.call_count == 1
+        assert mark_down.call_args_list[0][0][0].startswith('rmachine179')
+
diff --git a/tests/dispatcher/test_supervisor.py b/tests/dispatcher/test_supervisor.py
new file mode 100644 (file)
index 0000000..0a524a5
--- /dev/null
@@ -0,0 +1,121 @@
+from subprocess import DEVNULL
+from unittest.mock import patch, Mock, MagicMock
+
+from teuthology.dispatcher import supervisor
+
+
+class TestSuperviser(object):
+    @patch("teuthology.dispatcher.supervisor.run_with_watchdog")
+    @patch("teuthology.dispatcher.supervisor.teuth_config")
+    @patch("subprocess.Popen")
+    @patch("os.environ")
+    @patch("os.mkdir")
+    @patch("yaml.safe_dump")
+    @patch("tempfile.NamedTemporaryFile")
+    def test_run_job_with_watchdog(self, m_tempfile, m_safe_dump, m_mkdir,
+                                   m_environ, m_popen, m_t_config,
+                                   m_run_watchdog):
+        config = {
+            "suite_path": "suite/path",
+            "config": {"foo": "bar"},
+            "verbose": True,
+            "owner": "the_owner",
+            "archive_path": "archive/path",
+            "name": "the_name",
+            "description": "the_description",
+            "job_id": "1",
+        }
+        m_tmp = MagicMock()
+        temp_file = Mock()
+        temp_file.name = "the_name"
+        m_tmp.__enter__.return_value = temp_file
+        m_tempfile.return_value = m_tmp
+        m_p = Mock()
+        m_p.returncode = 0
+        m_popen.return_value = m_p
+        m_t_config.results_server = True
+        config_path = "archive/path/initial.config.yaml"
+        supervisor.run_job(config, config_path, "teuth/bin/path", "archive/dir", verbose=False)
+        m_run_watchdog.assert_called_with(m_p, config)
+        expected_args = [
+            'teuth/bin/path/teuthology',
+            '-v',
+            '--owner', 'the_owner',
+            '--archive', 'archive/path',
+            '--name', 'the_name',
+            '--description',
+            'the_description',
+            '--',
+            config_path,
+        ]
+        m_popen.assert_called_with(args=expected_args, stderr=DEVNULL, stdout=DEVNULL)
+
+    @patch("time.sleep")
+    @patch("teuthology.dispatcher.supervisor.teuth_config")
+    @patch("subprocess.Popen")
+    @patch("os.environ")
+    @patch("os.mkdir")
+    @patch("yaml.safe_dump")
+    @patch("tempfile.NamedTemporaryFile")
+    def test_run_job_no_watchdog(self, m_tempfile, m_safe_dump, m_mkdir,
+                                 m_environ, m_popen, m_t_config,
+                                 m_sleep):
+        config = {
+            "suite_path": "suite/path",
+            "config": {"foo": "bar"},
+            "verbose": True,
+            "owner": "the_owner",
+            "archive_path": "archive/path",
+            "name": "the_name",
+            "description": "the_description",
+            "job_id": "1",
+        }
+        m_tmp = MagicMock()
+        temp_file = Mock()
+        temp_file.name = "the_name"
+        m_tmp.__enter__.return_value = temp_file
+        m_tempfile.return_value = m_tmp
+        env = dict(PYTHONPATH="python/path")
+        m_environ.copy.return_value = env
+        m_p = Mock()
+        m_p.returncode = 1
+        m_popen.return_value = m_p
+        m_t_config.results_server = False
+        config_path = "archive/path/initial.config.yaml"
+        supervisor.run_job(config, config_path, "teuth/bin/path", "archive/dir", verbose=False)
+
+    @patch("teuthology.dispatcher.supervisor.report.try_push_job_info")
+    @patch("time.sleep")
+    def test_run_with_watchdog_no_reporting(self, m_sleep, m_try_push):
+        config = {
+            "name": "the_name",
+            "job_id": "1",
+            "archive_path": "archive/path",
+            "teuthology_branch": "main",
+            "owner": "the_owner",
+        }
+        process = Mock()
+        process.poll.return_value = "not None"
+        supervisor.run_with_watchdog(process, config)
+        m_try_push.assert_called_with(
+            dict(name=config["name"], job_id=config["job_id"]),
+            dict(status='dead')
+        )
+
+    @patch("subprocess.Popen")
+    @patch("time.sleep")
+    @patch("teuthology.dispatcher.supervisor.report.try_push_job_info")
+    def test_run_with_watchdog_with_reporting(self, m_tpji, m_sleep, m_popen):
+        config = {
+            "name": "the_name",
+            "job_id": "1",
+            "archive_path": "archive/path",
+            "teuthology_branch": "jewel",
+            "owner": "the_owner",
+        }
+        process = Mock()
+        process.poll.return_value = "not None"
+        m_proc = Mock()
+        m_proc.poll.return_value = "not None"
+        m_popen.return_value = m_proc
+        supervisor.run_with_watchdog(process, config)
diff --git a/tests/fake_archive.py b/tests/fake_archive.py
new file mode 100644 (file)
index 0000000..76a944f
--- /dev/null
@@ -0,0 +1,107 @@
+import os
+import shutil
+import yaml
+import random
+
+
+class FakeArchive(object):
+    def __init__(self, archive_base="./test_archive"):
+        self.archive_base = archive_base
+
+    def get_random_metadata(self, run_name, job_id=None, hung=False):
+        """
+        Generate a random info dict for a fake job. If 'hung' is not True, also
+        generate a summary dict.
+
+        :param run_name:   Run name e.g. 'test_foo'
+        :param job_id: Job ID e.g. '12345'
+        :param hung:   Simulate a hung job e.g. don't return a summary.yaml
+        :return:       A dict with keys 'job_id', 'info' and possibly
+                       'summary', with corresponding values
+        """
+        rand = random.Random()
+
+        description = 'description for job with id %s' % job_id
+        owner = 'job@owner'
+        duration = rand.randint(1, 36000)
+        pid = rand.randint(1000, 99999)
+        job_id = rand.randint(1, 99999)
+
+        info = {
+            'description': description,
+            'job_id': job_id,
+            'run_name': run_name,
+            'owner': owner,
+            'pid': pid,
+        }
+
+        metadata = {
+            'info': info,
+            'job_id': job_id,
+        }
+
+        if not hung:
+            success = True if rand.randint(0, 1) != 1 else False
+
+            summary = {
+                'description': description,
+                'duration': duration,
+                'owner': owner,
+                'success': success,
+            }
+
+            if not success:
+                summary['failure_reason'] = 'Failure reason!'
+            metadata['summary'] = summary
+
+        return metadata
+
+    def setup(self):
+        if os.path.exists(self.archive_base):
+            shutil.rmtree(self.archive_base)
+        os.mkdir(self.archive_base)
+
+    def teardown(self):
+        shutil.rmtree(self.archive_base)
+
+    def populate_archive(self, run_name, jobs):
+        run_archive_dir = os.path.join(self.archive_base, run_name)
+        os.mkdir(run_archive_dir)
+        for job in jobs:
+            archive_dir = os.path.join(run_archive_dir, str(job['job_id']))
+            os.mkdir(archive_dir)
+
+            with open(os.path.join(archive_dir, 'info.yaml'), 'w') as yfile:
+                yaml.safe_dump(job['info'], yfile)
+
+            if 'summary' in job:
+                summary_path = os.path.join(archive_dir, 'summary.yaml')
+                with open(summary_path, 'w') as yfile:
+                    yaml.safe_dump(job['summary'], yfile)
+
+    def create_fake_run(self, run_name, job_count, yaml_path, num_hung=0):
+        """
+        Creates a fake run using run_name. Uses the YAML specified for each
+        job's config.yaml
+
+        Returns a list of job_ids
+        """
+        assert os.path.exists(yaml_path)
+        assert job_count > 0
+        jobs = []
+        made_hung = 0
+        for i in range(job_count):
+            if made_hung < num_hung:
+                jobs.append(self.get_random_metadata(run_name, hung=True))
+                made_hung += 1
+            else:
+                jobs.append(self.get_random_metadata(run_name, hung=False))
+            #job_config = yaml.safe_load(yaml_path)
+        self.populate_archive(run_name, jobs)
+        for job in jobs:
+            job_id = job['job_id']
+            job_yaml_path = os.path.join(self.archive_base, run_name,
+                                         str(job_id), 'config.yaml')
+            shutil.copyfile(yaml_path, job_yaml_path)
+        return jobs
+
diff --git a/tests/fake_fs.py b/tests/fake_fs.py
new file mode 100644 (file)
index 0000000..c5cb6e4
--- /dev/null
@@ -0,0 +1,90 @@
+from io import BytesIO
+from contextlib import closing
+
+
+try:
+     FileNotFoundError, NotADirectoryError
+except NameError:
+     FileNotFoundError = NotADirectoryError = OSError
+
+
+def make_fake_fstools(fake_filesystem):
+    """
+    Build fake versions of os.listdir(), os.isfile(), etc. for use in
+    unit tests
+
+    An example fake_filesystem value:
+        >>> fake_fs = {\
+            'a_directory': {\
+                'another_directory': {\
+                    'empty_file': None,\
+                    'another_empty_file': None,\
+                },\
+                'random_file': None,\
+                'yet_another_directory': {\
+                    'empty_directory': {},\
+                },\
+                'file_with_contents': 'data',\
+            },\
+        }
+        >>> fake_listdir, fake_isfile, _, _ = \
+                make_fake_fstools(fake_fs)
+        >>> fake_listdir('a_directory/yet_another_directory')
+        ['empty_directory']
+        >>> fake_isfile('a_directory/yet_another_directory')
+        False
+
+    :param fake_filesystem: A dict representing a filesystem
+    """
+    assert isinstance(fake_filesystem, dict)
+
+    def fake_listdir(path, fsdict=False):
+        if fsdict is False:
+            fsdict = fake_filesystem
+
+        remainder = path.strip('/') + '/'
+        subdict = fsdict
+        while '/' in remainder:
+            next_dir, remainder = remainder.split('/', 1)
+            if next_dir not in subdict:
+                raise FileNotFoundError(
+                    '[Errno 2] No such file or directory: %s' % next_dir)
+            subdict = subdict.get(next_dir)
+            if not isinstance(subdict, dict):
+                raise NotADirectoryError('[Errno 20] Not a directory: %s' % next_dir)
+            if subdict and not remainder:
+                return list(subdict)
+        return []
+
+    def fake_isfile(path, fsdict=False):
+        if fsdict is False:
+            fsdict = fake_filesystem
+
+        components = path.strip('/').split('/')
+        subdict = fsdict
+        for component in components:
+            if component not in subdict:
+                raise FileNotFoundError('[Errno 2] No such file or directory: %s' % component)
+            subdict = subdict.get(component)
+        return subdict is None or isinstance(subdict, str)
+
+    def fake_isdir(path, fsdict=False):
+        return not fake_isfile(path)
+
+    def fake_exists(path, fsdict=False):
+        return fake_isfile(path, fsdict) or fake_isdir(path, fsdict)
+
+    def fake_open(path, mode=None, buffering=None):
+        components = path.strip('/').split('/')
+        subdict = fake_filesystem
+        for component in components:
+            if component not in subdict:
+                raise FileNotFoundError('[Errno 2] No such file or directory: %s' % component)
+            subdict = subdict.get(component)
+        if isinstance(subdict, dict):
+            raise IOError('[Errno 21] Is a directory: %s' % path)
+        elif subdict is None:
+            return closing(BytesIO(b''))
+        return closing(BytesIO(subdict.encode()))
+
+    return fake_exists, fake_listdir, fake_isfile, fake_isdir, fake_open
diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/tests/integration/test_suite.py b/tests/integration/test_suite.py
new file mode 100644 (file)
index 0000000..a618c4c
--- /dev/null
@@ -0,0 +1,86 @@
+import os
+import requests
+from pytest import raises, skip
+
+from teuthology.config import config
+from teuthology import suite
+
+
+class TestSuiteOnline(object):
+    def setup_method(self):
+        if 'TEST_ONLINE' not in os.environ:
+            skip("To run these sets, set the environment variable TEST_ONLINE")
+
+    def test_ceph_hash_simple(self):
+        resp = requests.get(
+            'https://api.github.com/repos/ceph/ceph/git/refs/heads/main')
+        ref_hash = resp.json()['object']['sha']
+        assert suite.get_hash('ceph') == ref_hash
+
+    def test_kernel_hash_saya(self):
+        # We don't currently have these packages.
+        assert suite.get_hash('kernel', 'main', 'default', 'saya') is None
+
+    def test_all_main_branches(self):
+        # Don't attempt to send email
+        config.results_email = None
+        job_config = suite.create_initial_config('suite', 'main',
+                                                 'main', 'main', 'testing',
+                                                 'default', 'centos', 'plana')
+        assert ((job_config.branch, job_config.teuthology_branch,
+                 job_config.suite_branch) == ('main', 'main', 'main'))
+
+    def test_config_bogus_kernel_branch(self):
+        # Don't attempt to send email
+        config.results_email = None
+        with raises(suite.ScheduleFailError):
+            suite.create_initial_config('s', None, 'main', 't',
+                                        'bogus_kernel_branch', 'f', 'd', 'm')
+
+    def test_config_bogus_flavor(self):
+        # Don't attempt to send email
+        config.results_email = None
+        with raises(suite.ScheduleFailError):
+            suite.create_initial_config('s', None, 'main', 't', 'k',
+                                        'bogus_flavor', 'd', 'm')
+
+    def test_config_bogus_ceph_branch(self):
+        # Don't attempt to send email
+        config.results_email = None
+        with raises(suite.ScheduleFailError):
+            suite.create_initial_config('s', None, 'bogus_ceph_branch', 't',
+                                        'k', 'f', 'd', 'm')
+
+    def test_config_bogus_suite_branch(self):
+        # Don't attempt to send email
+        config.results_email = None
+        with raises(suite.ScheduleFailError):
+            suite.create_initial_config('s', 'bogus_suite_branch', 'main',
+                                        't', 'k', 'f', 'd', 'm')
+
+    def test_config_bogus_teuthology_branch(self):
+        # Don't attempt to send email
+        config.results_email = None
+        with raises(suite.ScheduleFailError):
+            suite.create_initial_config('s', None, 'main',
+                                        'bogus_teuth_branch', 'k', 'f', 'd',
+                                        'm')
+
+    def test_config_substitution(self):
+        # Don't attempt to send email
+        config.results_email = None
+        job_config = suite.create_initial_config('MY_SUITE', 'main',
+                                                 'main', 'main', 'testing',
+                                                 'default', 'centos', 'plana')
+        assert job_config['suite'] == 'MY_SUITE'
+
+    def test_config_kernel_section(self):
+        # Don't attempt to send email
+        config.results_email = None
+        job_config = suite.create_initial_config('MY_SUITE', 'main',
+                                                 'main', 'main', 'testing',
+                                                 'default', 'centos', 'plana')
+        assert job_config['kernel']['kdb'] is True
+
+
+# maybe use notario for the above?
diff --git a/tests/lock/__init__.py b/tests/lock/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/tests/lock/test_lock.py b/tests/lock/test_lock.py
new file mode 100644 (file)
index 0000000..5f1679a
--- /dev/null
@@ -0,0 +1,7 @@
+import teuthology.lock.util
+
+class TestLock(object):
+
+    def test_locked_since_seconds(self):
+        node = { "locked_since": "2013-02-07 19:33:55.000000" }
+        assert teuthology.lock.util.locked_since_seconds(node) > 3600
diff --git a/tests/openstack/__init__.py b/tests/openstack/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/tests/openstack/archive-on-error.yaml b/tests/openstack/archive-on-error.yaml
new file mode 100644 (file)
index 0000000..f9f5247
--- /dev/null
@@ -0,0 +1 @@
+archive-on-error: true
diff --git a/tests/openstack/noop.yaml b/tests/openstack/noop.yaml
new file mode 100644 (file)
index 0000000..6aae7ec
--- /dev/null
@@ -0,0 +1,12 @@
+stop_worker: true
+machine_type: openstack
+os_type: ubuntu
+os_version: "14.04"
+roles:
+- - mon.a
+  - osd.0
+tasks:
+- exec:
+    mon.a:
+      - echo "Well done !"
+
diff --git a/tests/openstack/openstack-integration.py b/tests/openstack/openstack-integration.py
new file mode 100644 (file)
index 0000000..5d1a266
--- /dev/null
@@ -0,0 +1,243 @@
+#
+# Copyright (c) 2015, 2016 Red Hat, Inc.
+#
+# Author: Loic Dachary <loic@dachary.org>
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+import argparse
+import logging
+import json
+import os
+import subprocess
+import tempfile
+import shutil
+
+import teuthology.lock
+import teuthology.lock.cli
+import teuthology.lock.query
+import teuthology.lock.util
+import teuthology.misc
+import teuthology.schedule
+import teuthology.suite
+import teuthology.openstack
+import scripts.schedule
+import scripts.lock
+import scripts.suite
+from teuthology.config import config as teuth_config
+from teuthology.config import set_config_attr
+
+
+class Integration(object):
+
+    @classmethod
+    def setup_class(self):
+        teuthology.log.setLevel(logging.DEBUG)
+        set_config_attr(argparse.Namespace())
+        self.teardown_class()
+
+    @classmethod
+    def teardown_class(self):
+        os.system("sudo /etc/init.d/beanstalkd restart")
+        # if this fails it will not show the error but some weird
+        # INTERNALERROR> IndexError: list index out of range
+        # move that to def tearDown for debug and when it works move it
+        # back in tearDownClass so it is not called on every test
+        ownedby = "ownedby='" + teuth_config.openstack['ip']
+        all_instances = teuthology.openstack.OpenStack().run(
+            "server list -f json --long")
+        for instance in json.loads(all_instances):
+            if ownedby in instance['Properties']:
+                teuthology.openstack.OpenStack().run(
+                    "server delete --wait " + instance['ID'])
+
+    def setup_worker(self):
+        self.logs = self.d + "/log"
+        os.mkdir(self.logs, 0o755)
+        self.archive = self.d + "/archive"
+        os.mkdir(self.archive, 0o755)
+        self.worker_cmd = ("teuthology-worker --tube openstack " +
+                           "-l " + self.logs + " "
+                           "--archive-dir " + self.archive + " ")
+        logging.info(self.worker_cmd)
+        self.worker = subprocess.Popen(self.worker_cmd,
+                                       stdout=subprocess.PIPE,
+                                       stderr=subprocess.PIPE,
+                                       shell=True)
+
+    def wait_worker(self):
+        if not self.worker:
+            return
+
+        (stdoutdata, stderrdata) = self.worker.communicate()
+        stdoutdata = stdoutdata.decode('utf-8')
+        stderrdata = stderrdata.decode('utf-8')
+        logging.info(self.worker_cmd + ":" +
+                     " stdout " + stdoutdata +
+                     " stderr " + stderrdata + " end ")
+        assert self.worker.returncode == 0
+        self.worker = None
+
+    def get_teuthology_log(self):
+        # the archive is removed before each test, there must
+        # be only one run and one job
+        run = os.listdir(self.archive)[0]
+        job = os.listdir(os.path.join(self.archive, run))[0]
+        path = os.path.join(self.archive, run, job, 'teuthology.log')
+        return open(path, 'r').read()
+
+class TestSuite(Integration):
+
+    def setup_method(self):
+        self.d = tempfile.mkdtemp()
+        self.setup_worker()
+        logging.info("TestSuite: done worker")
+
+    def teardown(self):
+        self.wait_worker()
+        shutil.rmtree(self.d)
+
+    def test_suite_noop(self):
+        cwd = os.getcwd()
+        os.mkdir(self.d + '/upload', 0o755)
+        upload = 'localhost:' + self.d + '/upload'
+        args = ['--suite', 'noop',
+                '--suite-dir', cwd + '/teuthology/openstack/test',
+                '--machine-type', 'openstack',
+                '--archive-upload', upload,
+                '--verbose']
+        logging.info("TestSuite:test_suite_noop")
+        scripts.suite.main(args)
+        self.wait_worker()
+        log = self.get_teuthology_log()
+        assert "teuthology.run:pass" in log
+        assert "Well done" in log
+        upload_key = teuth_config.archive_upload_key
+        if upload_key:
+            ssh = "RSYNC_RSH='ssh -i " + upload_key + "'"
+        else:
+            ssh = ''
+        assert 'teuthology.log' in teuthology.misc.sh(ssh + " rsync -av " + upload)
+
+class TestSchedule(Integration):
+
+    def setup_method(self):
+        self.d = tempfile.mkdtemp()
+        self.setup_worker()
+
+    def teardown(self):
+        self.wait_worker()
+        shutil.rmtree(self.d)
+
+    def test_schedule_stop_worker(self):
+        job = 'teuthology/openstack/test/stop_worker.yaml'
+        args = ['--name', 'fake',
+                '--verbose',
+                '--owner', 'test@test.com',
+                '--worker', 'openstack',
+                job]
+        scripts.schedule.main(args)
+        self.wait_worker()
+
+    def test_schedule_noop(self):
+        job = 'teuthology/openstack/test/noop.yaml'
+        args = ['--name', 'fake',
+                '--verbose',
+                '--owner', 'test@test.com',
+                '--worker', 'openstack',
+                job]
+        scripts.schedule.main(args)
+        self.wait_worker()
+        log = self.get_teuthology_log()
+        assert "teuthology.run:pass" in log
+        assert "Well done" in log
+
+    def test_schedule_resources_hint(self):
+        """It is tricky to test resources hint in a provider agnostic way. The
+        best way seems to ask for at least 1GB of RAM and 10GB
+        disk. Some providers do not offer a 1GB RAM flavor (OVH for
+        instance) and the 2GB RAM will be chosen instead. It however
+        seems unlikely that a 4GB RAM will be chosen because it would
+        mean such a provider has nothing under that limit and it's a
+        little too high.
+
+        Since the default when installing is to ask for 7000 MB, we
+        can reasonably assume that the hint has been taken into
+        account if the instance has less than 4GB RAM.
+        """
+        try:
+            teuthology.openstack.OpenStack().run("volume list")
+            job = 'teuthology/openstack/test/resources_hint.yaml'
+            has_cinder = True
+        except subprocess.CalledProcessError:
+            job = 'teuthology/openstack/test/resources_hint_no_cinder.yaml'
+            has_cinder = False
+        args = ['--name', 'fake',
+                '--verbose',
+                '--owner', 'test@test.com',
+                '--worker', 'openstack',
+                job]
+        scripts.schedule.main(args)
+        self.wait_worker()
+        log = self.get_teuthology_log()
+        assert "teuthology.run:pass" in log
+        assert "RAM size ok" in log
+        if has_cinder:
+            assert "Disk size ok" in log
+
+class TestLock(Integration):
+
+    def setup_method(self):
+        self.options = ['--verbose',
+                        '--machine-type', 'openstack' ]
+
+    def test_main(self):
+        args = scripts.lock.parse_args(self.options + ['--lock'])
+        assert teuthology.lock.cli.main(args) == 0
+
+    def test_lock_unlock(self):
+        default_archs = teuthology.openstack.OpenStack().get_available_archs()
+        if 'TEST_IMAGES' in os.environ:
+            images = os.environ['TEST_IMAGES'].split()
+        else:
+            images = teuthology.openstack.OpenStack.image2url.keys()
+        for image in images:
+            (os_type, os_version, arch) = image.split('-')
+            if arch not in default_archs:
+                logging.info("skipping " + image + " because arch " +
+                             " is not supported (" + str(default_archs) + ")")
+                continue
+            args = scripts.lock.parse_args(self.options +
+                                           ['--lock-many', '1',
+                                            '--os-type', os_type,
+                                            '--os-version', os_version,
+                                            '--arch', arch])
+            assert teuthology.lock.cli.main(args) == 0
+            locks = teuthology.lock.query.list_locks(locked=True)
+            assert len(locks) == 1
+            args = scripts.lock.parse_args(self.options +
+                                           ['--unlock', locks[0]['name']])
+            assert teuthology.lock.cli.main(args) == 0
+
+    def test_list(self, capsys):
+        args = scripts.lock.parse_args(self.options + ['--list', '--all'])
+        teuthology.lock.cli.main(args)
+        out, err = capsys.readouterr()
+        assert 'machine_type' in out
+        assert 'openstack' in out
diff --git a/tests/openstack/resources_hint.yaml b/tests/openstack/resources_hint.yaml
new file mode 100644 (file)
index 0000000..b8f5959
--- /dev/null
@@ -0,0 +1,25 @@
+stop_worker: true
+machine_type: openstack
+openstack:
+  - machine:
+     disk: 10 # GB
+     ram: 10000 # MB
+     cpus: 1
+    volumes:
+     count: 1
+     size: 2 # GB
+os_type: ubuntu
+os_version: "14.04"
+roles:
+- - mon.a
+  - osd.0
+tasks:
+- exec:
+    mon.a:
+      - test $(sed -n -e 's/MemTotal.* \([0-9][0-9]*\).*/\1/p' < /proc/meminfo) -ge 10000000 && echo "RAM" "size" "ok"
+      - cat /proc/meminfo
+# wait for the attached volume to show up
+      - for delay in 1 2 4 8 16 32 64 128 256 512 ; do if test -e /sys/block/vdb/size ; then break ; else sleep $delay ; fi ; done
+# 4000000 because 512 bytes sectors
+      - test $(cat /sys/block/vdb/size) -gt 4000000 && echo "Disk" "size" "ok"
+      - cat /sys/block/vdb/size
diff --git a/tests/openstack/resources_hint_no_cinder.yaml b/tests/openstack/resources_hint_no_cinder.yaml
new file mode 100644 (file)
index 0000000..c603804
--- /dev/null
@@ -0,0 +1,20 @@
+stop_worker: true
+machine_type: openstack
+openstack:
+  - machine:
+     disk: 10 # GB
+     ram: 10000 # MB
+     cpus: 1
+    volumes:
+     count: 0
+     size: 2 # GB
+os_type: ubuntu
+os_version: "14.04"
+roles:
+- - mon.a
+  - osd.0
+tasks:
+- exec:
+    mon.a:
+      - cat /proc/meminfo
+      - test $(sed -n -e 's/MemTotal.* \([0-9][0-9]*\).*/\1/p' < /proc/meminfo) -ge 10000000 && echo "RAM" "size" "ok"
diff --git a/tests/openstack/stop_worker.yaml b/tests/openstack/stop_worker.yaml
new file mode 100644 (file)
index 0000000..45133bb
--- /dev/null
@@ -0,0 +1 @@
+stop_worker: true
diff --git a/tests/openstack/suites/noop/+ b/tests/openstack/suites/noop/+
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/tests/openstack/suites/noop/noop.yaml b/tests/openstack/suites/noop/noop.yaml
new file mode 100644 (file)
index 0000000..49497c2
--- /dev/null
@@ -0,0 +1,9 @@
+stop_worker: true
+roles:
+- - mon.a
+  - osd.0
+tasks:
+- exec:
+    mon.a:
+      - echo "Well done !"
+
diff --git a/tests/openstack/suites/nuke/+ b/tests/openstack/suites/nuke/+
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/tests/openstack/test_config.py b/tests/openstack/test_config.py
new file mode 100644 (file)
index 0000000..530aea9
--- /dev/null
@@ -0,0 +1,35 @@
+from teuthology.config import config
+
+
+class TestOpenStack(object):
+
+    def setup_method(self):
+        self.openstack_config = config['openstack']
+
+    def test_config_clone(self):
+        assert 'clone' in self.openstack_config
+
+    def test_config_user_data(self):
+        os_type = 'rhel'
+        os_version = '7.0'
+        template_path = self.openstack_config['user-data'].format(
+            os_type=os_type,
+            os_version=os_version)
+        assert os_type in template_path
+        assert os_version in template_path
+
+    def test_config_ip(self):
+        assert 'ip' in self.openstack_config
+
+    def test_config_machine(self):
+        assert 'machine' in self.openstack_config
+        machine_config = self.openstack_config['machine']
+        assert 'disk' in machine_config
+        assert 'ram' in machine_config
+        assert 'cpus' in machine_config
+
+    def test_config_volumes(self):
+        assert 'volumes' in self.openstack_config
+        volumes_config = self.openstack_config['volumes']
+        assert 'count' in volumes_config
+        assert 'size' in volumes_config
diff --git a/tests/openstack/test_openstack.py b/tests/openstack/test_openstack.py
new file mode 100644 (file)
index 0000000..1f34d82
--- /dev/null
@@ -0,0 +1,1695 @@
+#
+# Copyright (c) 2015,2016 Red Hat, Inc.
+#
+# Author: Loic Dachary <loic@dachary.org>
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+import argparse
+import logging
+import os
+import pytest
+import subprocess
+import tempfile
+import time
+from mock import patch
+
+import teuthology
+from teuthology import misc
+from teuthology.config import set_config_attr
+from teuthology.openstack import TeuthologyOpenStack, OpenStack, OpenStackInstance
+from teuthology.openstack import NoFlavorException
+import scripts.openstack
+
+
+class TestOpenStackBase(object):
+
+    def setup_method(self):
+        OpenStack.token = None
+        OpenStack.token_expires = None
+        self.environ = {}
+        for k in os.environ.keys():
+            if k.startswith('OS_'):
+                self.environ[k] = os.environ[k]
+
+    def teardown_method(self):
+        OpenStack.token = None
+        OpenStack.token_expires = None
+        for k in os.environ.keys():
+            if k.startswith('OS_'):
+                if k in self.environ:
+                    os.environ[k] = self.environ[k]
+                else:
+                    del os.environ[k]
+
+class TestOpenStackInstance(TestOpenStackBase):
+
+    teuthology_instance = """
+{
+  "OS-EXT-STS:task_state": null,
+  "addresses": "Ext-Net=167.114.233.32",
+  "image": "Ubuntu 14.04 (0d315a8d-75e3-418a-80e4-48e62d599627)",
+  "OS-EXT-STS:vm_state": "active",
+  "OS-SRV-USG:launched_at": "2015-08-17T12:22:13.000000",
+  "flavor": "vps-ssd-1 (164fcc7e-7771-414f-a607-b388cb7b7aa0)",
+  "id": "f3ca32d7-212b-458b-a0d4-57d1085af953",
+  "security_groups": [
+    {
+      "name": "default"
+    }
+  ],
+  "user_id": "3a075820e5d24fda96cd340b87fd94e9",
+  "OS-DCF:diskConfig": "AUTO",
+  "accessIPv4": "",
+  "accessIPv6": "",
+  "progress": 0,
+  "OS-EXT-STS:power_state": 1,
+  "project_id": "62cf1be03cec403c8ed8e64df55732ea",
+  "config_drive": "",
+  "status": "ACTIVE",
+  "updated": "2015-11-03T13:48:53Z",
+  "hostId": "bcdf964b6f724e573c07156ff85b4db1707f6f0969f571cf33e0468d",
+  "OS-SRV-USG:terminated_at": null,
+  "key_name": "loic",
+  "properties": "",
+  "OS-EXT-AZ:availability_zone": "nova",
+  "name": "mrdarkdragon",
+  "created": "2015-08-17T12:21:31Z",
+  "os-extended-volumes:volumes_attached": [{"id": "627e2631-fbb3-48cd-b801-d29cd2a76f74"}, {"id": "09837649-0881-4ee2-a560-adabefc28764"}, {"id": "44e5175b-6044-40be-885a-c9ddfb6f75bb"}]
+}
+    """
+
+    teuthology_instance_no_addresses = """
+{
+  "OS-EXT-STS:task_state": null,
+  "addresses": "",
+  "image": "Ubuntu 14.04 (0d315a8d-75e3-418a-80e4-48e62d599627)",
+  "OS-EXT-STS:vm_state": "active",
+  "OS-SRV-USG:launched_at": "2015-08-17T12:22:13.000000",
+  "flavor": "vps-ssd-1 (164fcc7e-7771-414f-a607-b388cb7b7aa0)",
+  "id": "f3ca32d7-212b-458b-a0d4-57d1085af953",
+  "security_groups": [
+    {
+      "name": "default"
+    }
+  ],
+  "user_id": "3a075820e5d24fda96cd340b87fd94e9",
+  "OS-DCF:diskConfig": "AUTO",
+  "accessIPv4": "",
+  "accessIPv6": "",
+  "progress": 0,
+  "OS-EXT-STS:power_state": 1,
+  "project_id": "62cf1be03cec403c8ed8e64df55732ea",
+  "config_drive": "",
+  "status": "ACTIVE",
+  "updated": "2015-11-03T13:48:53Z",
+  "hostId": "bcdf964b6f724e573c07156ff85b4db1707f6f0969f571cf33e0468d",
+  "OS-SRV-USG:terminated_at": null,
+  "key_name": "loic",
+  "properties": "",
+  "OS-EXT-AZ:availability_zone": "nova",
+  "name": "mrdarkdragon",
+  "created": "2015-08-17T12:21:31Z",
+  "os-extended-volumes:volumes_attached": []
+}
+    """
+
+    @classmethod
+    def setup_class(self):
+        if 'OS_AUTH_URL' not in os.environ:
+            pytest.skip('no OS_AUTH_URL environment variable')
+
+    def test_init(self):
+        with patch.multiple(
+                misc,
+                sh=lambda cmd: self.teuthology_instance,
+        ):
+            o = OpenStackInstance('NAME')
+            assert o['id'] == 'f3ca32d7-212b-458b-a0d4-57d1085af953'
+        o = OpenStackInstance('NAME', {"id": "OTHER"})
+        assert o['id'] == "OTHER"
+
+    def test_get_created(self):
+        with patch.multiple(
+                misc,
+                sh=lambda cmd: self.teuthology_instance,
+        ):
+            o = OpenStackInstance('NAME')
+            assert o.get_created() > 0
+
+    def test_exists(self):
+        with patch.multiple(
+                misc,
+                sh=lambda cmd: self.teuthology_instance,
+        ):
+            o = OpenStackInstance('NAME')
+            assert o.exists()
+        def sh_raises(cmd):
+            raise subprocess.CalledProcessError('FAIL', 'BAD')
+        with patch.multiple(
+                misc,
+                sh=sh_raises,
+        ):
+            o = OpenStackInstance('NAME')
+            assert not o.exists()
+
+    def test_volumes(self):
+        with patch.multiple(
+                misc,
+                sh=lambda cmd: self.teuthology_instance,
+        ):
+            o = OpenStackInstance('NAME')
+            assert len(o.get_volumes()) == 3
+
+    def test_get_addresses(self):
+        answers = [
+            self.teuthology_instance_no_addresses,
+            self.teuthology_instance,
+        ]
+        def sh(self):
+            return answers.pop(0)
+        with patch.multiple(
+                misc,
+                sh=sh,
+        ):
+            o = OpenStackInstance('NAME')
+            assert o.get_addresses() == 'Ext-Net=167.114.233.32'
+
+    def test_get_ip_neutron(self):
+        instance_id = '8e1fd70a-3065-46f8-9c30-84dc028c1834'
+        ip = '10.10.10.4'
+        def sh(cmd):
+            if 'neutron subnet-list' in cmd:
+                return """
+[
+  {
+    "ip_version": 6,
+    "id": "c45b9661-b2ba-4817-9e3a-f8f63bf32989"
+  },
+  {
+    "ip_version": 4,
+    "id": "e03a3dbc-afc8-4b52-952e-7bf755397b50"
+  }
+]
+                """
+            elif 'neutron port-list' in cmd:
+                return ("""
+[
+  {
+    "device_id": "915504ad-368b-4cce-be7c-4f8a83902e28",
+    "fixed_ips": "{\\"subnet_id\\": \\"e03a3dbc-afc8-4b52-952e-7bf755397b50\\", \\"ip_address\\": \\"10.10.10.1\\"}\\n{\\"subnet_id\\": \\"c45b9661-b2ba-4817-9e3a-f8f63bf32989\\", \\"ip_address\\": \\"2607:f298:6050:9afc::1\\"}"
+  },
+  {
+    "device_id": "{instance_id}",
+    "fixed_ips": "{\\"subnet_id\\": \\"e03a3dbc-afc8-4b52-952e-7bf755397b50\\", \\"ip_address\\": \\"{ip}\\"}\\n{\\"subnet_id\\": \\"c45b9661-b2ba-4817-9e3a-f8f63bf32989\\", \\"ip_address\\": \\"2607:f298:6050:9afc:f816:3eff:fe07:76c1\\"}"
+  },
+  {
+    "device_id": "17e4a968-4caa-4cee-8e4b-f950683a02bd",
+    "fixed_ips": "{\\"subnet_id\\": \\"e03a3dbc-afc8-4b52-952e-7bf755397b50\\", \\"ip_address\\": \\"10.10.10.5\\"}\\n{\\"subnet_id\\": \\"c45b9661-b2ba-4817-9e3a-f8f63bf32989\\", \\"ip_address\\": \\"2607:f298:6050:9afc:f816:3eff:fe9c:37f0\\"}"
+  }
+]
+                """.replace('{instance_id}', instance_id).
+                        replace('{ip}', ip))
+            else:
+                raise Exception("unexpected " + cmd)
+        with patch.multiple(
+                misc,
+                sh=sh,
+        ):
+            assert ip == OpenStackInstance(
+                instance_id,
+                { 'id': instance_id },
+            ).get_ip_neutron()
+
+class TestOpenStack(TestOpenStackBase):
+
+    flavors = """[
+  {
+    "Name": "eg-120-ssd",
+    "RAM": 120000,
+    "Ephemeral": 0,
+    "VCPUs": 32,
+    "Is Public": true,
+    "Disk": 800,
+    "ID": "008f75de-c467-4d15-8f70-79c8fbe19538"
+  },
+  {
+    "Name": "hg-60",
+    "RAM": 60000,
+    "Ephemeral": 0,
+    "VCPUs": 16,
+    "Is Public": true,
+    "Disk": 1600,
+    "ID": "0297d7ac-fe6f-4ff1-b6e7-0b8b0908c94f"
+  },
+  {
+    "Name": "win-sp-120-ssd-flex",
+    "RAM": 120000,
+    "Ephemeral": 0,
+    "VCPUs": 8,
+    "Is Public": true,
+    "Disk": 50,
+    "ID": "039e31f2-6541-46c8-85cf-7f47fab7ad78"
+  },
+  {
+    "Name": "win-sp-60",
+    "RAM": 60000,
+    "Ephemeral": 0,
+    "VCPUs": 4,
+    "Is Public": true,
+    "Disk": 400,
+    "ID": "0417a0e6-f68a-4b8b-a642-ca5ecb9652f7"
+  },
+  {
+    "Name": "hg-120-ssd",
+    "RAM": 120000,
+    "Ephemeral": 0,
+    "VCPUs": 32,
+    "Is Public": true,
+    "Disk": 800,
+    "ID": "042aefc6-b713-4a7e-ada5-3ff81daa1960"
+  },
+  {
+    "Name": "win-sp-60-flex",
+    "RAM": 60000,
+    "Ephemeral": 0,
+    "VCPUs": 4,
+    "Is Public": true,
+    "Disk": 50,
+    "ID": "0609290c-ad2a-40f0-8c66-c755dd38fe3f"
+  },
+  {
+    "Name": "win-eg-120",
+    "RAM": 120000,
+    "Ephemeral": 0,
+    "VCPUs": 32,
+    "Is Public": true,
+    "Disk": 800,
+    "ID": "0651080f-5d07-44b1-a759-7ea4594b669e"
+  },
+  {
+    "Name": "win-sp-240",
+    "RAM": 240000,
+    "Ephemeral": 0,
+    "VCPUs": 16,
+    "Is Public": true,
+    "Disk": 1600,
+    "ID": "07885848-8831-486d-8525-91484c09cc7e"
+  },
+  {
+    "Name": "win-hg-60-ssd",
+    "RAM": 60000,
+    "Ephemeral": 0,
+    "VCPUs": 16,
+    "Is Public": true,
+    "Disk": 800,
+    "ID": "079aa0a2-5e48-4e58-8205-719bc962736e"
+  },
+  {
+    "Name": "eg-120",
+    "RAM": 120000,
+    "Ephemeral": 0,
+    "VCPUs": 32,
+    "Is Public": true,
+    "Disk": 1600,
+    "ID": "090f8b8c-673c-4ab8-9a07-6e54a8776e7b"
+  },
+  {
+    "Name": "win-hg-15-ssd-flex",
+    "RAM": 15000,
+    "Ephemeral": 0,
+    "VCPUs": 4,
+    "Is Public": true,
+    "Disk": 50,
+    "ID": "10e10c58-d29f-4ff6-a1fd-085c35a3bd9b"
+  },
+  {
+    "Name": "eg-15-ssd",
+    "RAM": 15000,
+    "Ephemeral": 0,
+    "VCPUs": 4,
+    "Is Public": true,
+    "Disk": 200,
+    "ID": "1340a920-0f2f-4c1b-8d74-e2502258da73"
+  },
+  {
+    "Name": "win-eg-30-ssd-flex",
+    "RAM": 30000,
+    "Ephemeral": 0,
+    "VCPUs": 8,
+    "Is Public": true,
+    "Disk": 50,
+    "ID": "13e54752-fbd0-47a6-aa93-e5a67dfbc743"
+  },
+  {
+    "Name": "eg-120-ssd-flex",
+    "RAM": 120000,
+    "Ephemeral": 0,
+    "VCPUs": 32,
+    "Is Public": true,
+    "Disk": 50,
+    "ID": "15c07a54-2dfb-41d9-aa73-6989fd8cafc2"
+  },
+  {
+    "Name": "win-eg-120-ssd-flex",
+    "RAM": 120000,
+    "Ephemeral": 0,
+    "VCPUs": 32,
+    "Is Public": true,
+    "Disk": 50,
+    "ID": "15e0dfcc-10f4-4e70-8ac1-30bc323273e2"
+  },
+  {
+    "Name": "vps-ssd-1",
+    "RAM": 2000,
+    "Ephemeral": 0,
+    "VCPUs": 1,
+    "Is Public": true,
+    "Disk": 10,
+    "ID": "164fcc7e-7771-414f-a607-b388cb7b7aa0"
+  },
+  {
+    "Name": "win-sp-120-flex",
+    "RAM": 120000,
+    "Ephemeral": 0,
+    "VCPUs": 8,
+    "Is Public": true,
+    "Disk": 50,
+    "ID": "169415e1-0979-4527-94fb-638c885bbd8c"
+  },
+  {
+    "Name": "win-hg-60-flex",
+    "RAM": 60000,
+    "Ephemeral": 0,
+    "VCPUs": 16,
+    "Is Public": true,
+    "Disk": 50,
+    "ID": "16f13d5b-be27-4b8b-88da-959d3904d3ba"
+  },
+  {
+    "Name": "win-sp-30-ssd",
+    "RAM": 30000,
+    "Ephemeral": 0,
+    "VCPUs": 2,
+    "Is Public": true,
+    "Disk": 100,
+    "ID": "1788102b-ab80-4a0c-b819-541deaca7515"
+  },
+  {
+    "Name": "win-sp-240-flex",
+    "RAM": 240000,
+    "Ephemeral": 0,
+    "VCPUs": 16,
+    "Is Public": true,
+    "Disk": 50,
+    "ID": "17bcfa14-135f-442f-9397-a4dc25265560"
+  },
+  {
+    "Name": "win-eg-60-ssd-flex",
+    "RAM": 60000,
+    "Ephemeral": 0,
+    "VCPUs": 16,
+    "Is Public": true,
+    "Disk": 50,
+    "ID": "194ca9ba-04af-4d86-ba37-d7da883a7eab"
+  },
+  {
+    "Name": "win-eg-60-flex",
+    "RAM": 60000,
+    "Ephemeral": 0,
+    "VCPUs": 16,
+    "Is Public": true,
+    "Disk": 50,
+    "ID": "19ff8837-4751-4f6c-a82b-290bc53c83c1"
+  },
+  {
+    "Name": "win-eg-30-flex",
+    "RAM": 30000,
+    "Ephemeral": 0,
+    "VCPUs": 8,
+    "Is Public": true,
+    "Disk": 50,
+    "ID": "1aaef5e5-4df9-4462-80d3-701683ab9ff0"
+  },
+  {
+    "Name": "eg-15",
+    "RAM": 15000,
+    "Ephemeral": 0,
+    "VCPUs": 4,
+    "Is Public": true,
+    "Disk": 400,
+    "ID": "1cd85b81-5e4d-477a-a127-eb496b1d75de"
+  },
+  {
+    "Name": "hg-120",
+    "RAM": 120000,
+    "Ephemeral": 0,
+    "VCPUs": 32,
+    "Is Public": true,
+    "Disk": 1600,
+    "ID": "1f1efedf-ec91-4a42-acd7-f5cf64b02d3c"
+  },
+  {
+    "Name": "hg-15-ssd-flex",
+    "RAM": 15000,
+    "Ephemeral": 0,
+    "VCPUs": 4,
+    "Is Public": true,
+    "Disk": 50,
+    "ID": "20347a07-a289-4c07-a645-93cb5e8e2d30"
+  },
+  {
+    "Name": "win-eg-7-ssd",
+    "RAM": 7000,
+    "Ephemeral": 0,
+    "VCPUs": 2,
+    "Is Public": true,
+    "Disk": 100,
+    "ID": "20689394-bd77-4f4d-900e-52cc8a86aeb4"
+  },
+  {
+    "Name": "win-sp-60-ssd-flex",
+    "RAM": 60000,
+    "Ephemeral": 0,
+    "VCPUs": 4,
+    "Is Public": true,
+    "Disk": 50,
+    "ID": "21104d99-ba7b-47a0-9133-7e884710089b"
+  },
+  {
+    "Name": "win-sp-120-ssd",
+    "RAM": 120000,
+    "Ephemeral": 0,
+    "VCPUs": 8,
+    "Is Public": true,
+    "Disk": 400,
+    "ID": "23c21ecc-9ee8-4ad3-bd9f-aa17a3faf84e"
+  },
+  {
+    "Name": "win-hg-15-ssd",
+    "RAM": 15000,
+    "Ephemeral": 0,
+    "VCPUs": 4,
+    "Is Public": true,
+    "Disk": 200,
+    "ID": "24e293ed-bc54-4f26-8fb7-7b9457d08e66"
+  },
+  {
+    "Name": "eg-15-ssd-flex",
+    "RAM": 15000,
+    "Ephemeral": 0,
+    "VCPUs": 4,
+    "Is Public": true,
+    "Disk": 50,
+    "ID": "25f3534a-89e5-489d-aa8b-63f62e76875b"
+  },
+  {
+    "Name": "win-eg-60",
+    "RAM": 60000,
+    "Ephemeral": 0,
+    "VCPUs": 16,
+    "Is Public": true,
+    "Disk": 800,
+    "ID": "291173f1-ea1d-410b-8045-667361a4addb"
+  },
+  {
+    "Name": "sp-30-ssd-flex",
+    "RAM": 30000,
+    "Ephemeral": 0,
+    "VCPUs": 2,
+    "Is Public": true,
+    "Disk": 50,
+    "ID": "2b646463-2efa-428b-94ed-4059923c3636"
+  },
+  {
+    "Name": "win-eg-120-flex",
+    "RAM": 120000,
+    "Ephemeral": 0,
+    "VCPUs": 32,
+    "Is Public": true,
+    "Disk": 50,
+    "ID": "2c74df82-29d2-4b1a-a32c-d5633e7359b4"
+  },
+  {
+    "Name": "win-eg-15-ssd",
+    "RAM": 15000,
+    "Ephemeral": 0,
+    "VCPUs": 4,
+    "Is Public": true,
+    "Disk": 200,
+    "ID": "2fe4344f-d701-4bc4-8dcd-6d0b5d83fa13"
+  },
+  {
+    "Name": "sp-30-flex",
+    "RAM": 30000,
+    "Ephemeral": 0,
+    "VCPUs": 2,
+    "Is Public": true,
+    "Disk": 50,
+    "ID": "31487b30-eeb6-472f-a9b6-38ace6587ebc"
+  },
+  {
+    "Name": "win-sp-240-ssd",
+    "RAM": 240000,
+    "Ephemeral": 0,
+    "VCPUs": 16,
+    "Is Public": true,
+    "Disk": 800,
+    "ID": "325b602f-ecc4-4444-90bd-5a2cf4e0da53"
+  },
+  {
+    "Name": "win-hg-7",
+    "RAM": 7000,
+    "Ephemeral": 0,
+    "VCPUs": 2,
+    "Is Public": true,
+    "Disk": 200,
+    "ID": "377ded36-491f-4ad7-9eb4-876798b2aea9"
+  },
+  {
+    "Name": "sp-30-ssd",
+    "RAM": 30000,
+    "Ephemeral": 0,
+    "VCPUs": 2,
+    "Is Public": true,
+    "Disk": 100,
+    "ID": "382f2831-4dba-40c4-bb7a-6fadff71c4db"
+  },
+  {
+    "Name": "hg-30",
+    "RAM": 30000,
+    "Ephemeral": 0,
+    "VCPUs": 8,
+    "Is Public": true,
+    "Disk": 800,
+    "ID": "3c1d6170-0097-4b5c-a3b3-adff1b7a86e0"
+  },
+  {
+    "Name": "hg-60-flex",
+    "RAM": 60000,
+    "Ephemeral": 0,
+    "VCPUs": 16,
+    "Is Public": true,
+    "Disk": 50,
+    "ID": "3c669730-b5cd-4e44-8bd2-bc8d9f984ab2"
+  },
+  {
+    "Name": "sp-240-ssd-flex",
+    "RAM": 240000,
+    "Ephemeral": 0,
+    "VCPUs": 16,
+    "Is Public": true,
+    "Disk": 50,
+    "ID": "3d66fea3-26f2-4195-97ab-fdea3b836099"
+  },
+  {
+    "Name": "sp-240-flex",
+    "RAM": 240000,
+    "Ephemeral": 0,
+    "VCPUs": 16,
+    "Is Public": true,
+    "Disk": 50,
+    "ID": "40c781f7-d7a7-4b0d-bcca-5304aeabbcd9"
+  },
+  {
+    "Name": "hg-7-flex",
+    "RAM": 7000,
+    "Ephemeral": 0,
+    "VCPUs": 2,
+    "Is Public": true,
+    "Disk": 50,
+    "ID": "42730e52-147d-46b8-9546-18e31e5ac8a9"
+  },
+  {
+    "Name": "eg-30-ssd",
+    "RAM": 30000,
+    "Ephemeral": 0,
+    "VCPUs": 8,
+    "Is Public": true,
+    "Disk": 400,
+    "ID": "463f30e9-7d7a-4693-944f-142067cf553b"
+  },
+  {
+    "Name": "hg-15-flex",
+    "RAM": 15000,
+    "Ephemeral": 0,
+    "VCPUs": 4,
+    "Is Public": true,
+    "Disk": 50,
+    "ID": "534f07c6-91af-44c8-9e62-156360fe8359"
+  },
+  {
+    "Name": "win-sp-30-flex",
+    "RAM": 30000,
+    "Ephemeral": 0,
+    "VCPUs": 2,
+    "Is Public": true,
+    "Disk": 50,
+    "ID": "55533fdf-ad57-4aa7-a2c6-ee31bb94e77b"
+  },
+  {
+    "Name": "win-hg-60-ssd-flex",
+    "RAM": 60000,
+    "Ephemeral": 0,
+    "VCPUs": 16,
+    "Is Public": true,
+    "Disk": 50,
+    "ID": "58b24234-3804-4c4f-9eb6-5406a3a13758"
+  },
+  {
+    "Name": "hg-7-ssd-flex",
+    "RAM": 7000,
+    "Ephemeral": 0,
+    "VCPUs": 2,
+    "Is Public": true,
+    "Disk": 50,
+    "ID": "596c1276-8e53-40a0-b183-cdd9e9b1907d"
+  },
+  {
+    "Name": "win-hg-30-ssd",
+    "RAM": 30000,
+    "Ephemeral": 0,
+    "VCPUs": 8,
+    "Is Public": true,
+    "Disk": 400,
+    "ID": "5c54dc08-28b9-4860-9f24-a2451b2a28ec"
+  },
+  {
+    "Name": "eg-7",
+    "RAM": 7000,
+    "Ephemeral": 0,
+    "VCPUs": 2,
+    "Is Public": true,
+    "Disk": 200,
+    "ID": "5e409dbc-3f4b-46e8-a629-a418c8497922"
+  },
+  {
+    "Name": "hg-30-flex",
+    "RAM": 30000,
+    "Ephemeral": 0,
+    "VCPUs": 8,
+    "Is Public": true,
+    "Disk": 50,
+    "ID": "656423ea-0551-48c6-9e0f-ec6e15952029"
+  },
+  {
+    "Name": "hg-15",
+    "RAM": 15000,
+    "Ephemeral": 0,
+    "VCPUs": 4,
+    "Is Public": true,
+    "Disk": 400,
+    "ID": "675558ea-04fe-47a2-83de-40be9b2eacd4"
+  },
+  {
+    "Name": "eg-60-flex",
+    "RAM": 60000,
+    "Ephemeral": 0,
+    "VCPUs": 16,
+    "Is Public": true,
+    "Disk": 50,
+    "ID": "68a8e4e1-d291-46e8-a724-fbb1c4b9b051"
+  },
+  {
+    "Name": "hg-30-ssd",
+    "RAM": 30000,
+    "Ephemeral": 0,
+    "VCPUs": 8,
+    "Is Public": true,
+    "Disk": 400,
+    "ID": "6ab72807-e0a5-4e9f-bbb9-7cbbf0038b26"
+  },
+  {
+    "Name": "win-hg-30",
+    "RAM": 30000,
+    "Ephemeral": 0,
+    "VCPUs": 8,
+    "Is Public": true,
+    "Disk": 800,
+    "ID": "6e12cae3-0492-483c-aa39-54a0dcaf86dd"
+  },
+  {
+    "Name": "win-hg-7-ssd",
+    "RAM": 7000,
+    "Ephemeral": 0,
+    "VCPUs": 2,
+    "Is Public": true,
+    "Disk": 100,
+    "ID": "6ead771c-e8b9-424c-afa0-671280416422"
+  },
+  {
+    "Name": "win-hg-30-flex",
+    "RAM": 30000,
+    "Ephemeral": 0,
+    "VCPUs": 8,
+    "Is Public": true,
+    "Disk": 50,
+    "ID": "70ded741-8f58-4bb9-8cfd-5e838b66b5f3"
+  },
+  {
+    "Name": "win-sp-30-ssd-flex",
+    "RAM": 30000,
+    "Ephemeral": 0,
+    "VCPUs": 2,
+    "Is Public": true,
+    "Disk": 50,
+    "ID": "7284d104-a260-421d-8cee-6dc905107b25"
+  },
+  {
+    "Name": "win-eg-120-ssd",
+    "RAM": 120000,
+    "Ephemeral": 0,
+    "VCPUs": 32,
+    "Is Public": true,
+    "Disk": 800,
+    "ID": "72c0b262-855d-40bb-a3e9-fd989a1bc466"
+  },
+  {
+    "Name": "win-hg-7-flex",
+    "RAM": 7000,
+    "Ephemeral": 0,
+    "VCPUs": 2,
+    "Is Public": true,
+    "Disk": 50,
+    "ID": "73961591-c5f1-436f-b641-1a506eddaef4"
+  },
+  {
+    "Name": "sp-240-ssd",
+    "RAM": 240000,
+    "Ephemeral": 0,
+    "VCPUs": 16,
+    "Is Public": true,
+    "Disk": 800,
+    "ID": "7568d834-3b16-42ce-a2c1-0654e0781160"
+  },
+  {
+    "Name": "win-eg-60-ssd",
+    "RAM": 60000,
+    "Ephemeral": 0,
+    "VCPUs": 16,
+    "Is Public": true,
+    "Disk": 800,
+    "ID": "75f7fe5c-f87a-41d8-a961-a0169d02c268"
+  },
+  {
+    "Name": "eg-7-ssd-flex",
+    "RAM": 7000,
+    "Ephemeral": 0,
+    "VCPUs": 2,
+    "Is Public": true,
+    "Disk": 50,
+    "ID": "77e1db73-0b36-4e37-8e47-32c2d2437ca9"
+  },
+  {
+    "Name": "eg-60-ssd-flex",
+    "RAM": 60000,
+    "Ephemeral": 0,
+    "VCPUs": 16,
+    "Is Public": true,
+    "Disk": 50,
+    "ID": "78df4e30-98ca-4362-af68-037d958edaf0"
+  },
+  {
+    "Name": "vps-ssd-2",
+    "RAM": 4000,
+    "Ephemeral": 0,
+    "VCPUs": 1,
+    "Is Public": true,
+    "Disk": 20,
+    "ID": "7939cc5c-79b1-45c0-be2d-aa935d92faa1"
+  },
+  {
+    "Name": "sp-60",
+    "RAM": 60000,
+    "Ephemeral": 0,
+    "VCPUs": 4,
+    "Is Public": true,
+    "Disk": 400,
+    "ID": "80d8510a-79cc-4307-8db7-d1965c9e8ddb"
+  },
+  {
+    "Name": "win-hg-120-ssd-flex",
+    "RAM": 120000,
+    "Ephemeral": 0,
+    "VCPUs": 32,
+    "Is Public": true,
+    "Disk": 50,
+    "ID": "835e734a-46b6-4cb2-be68-e8678fd71059"
+  },
+  {
+    "Name": "win-eg-7",
+    "RAM": 7000,
+    "Ephemeral": 0,
+    "VCPUs": 2,
+    "Is Public": true,
+    "Disk": 200,
+    "ID": "84869b00-b43a-4523-babd-d47d206694e9"
+  },
+  {
+    "Name": "win-eg-7-ssd-flex",
+    "RAM": 7000,
+    "Ephemeral": 0,
+    "VCPUs": 2,
+    "Is Public": true,
+    "Disk": 50,
+    "ID": "852308f8-b8bf-44a4-af41-cbc27437b275"
+  },
+  {
+    "Name": "win-sp-30",
+    "RAM": 30000,
+    "Ephemeral": 0,
+    "VCPUs": 2,
+    "Is Public": true,
+    "Disk": 200,
+    "ID": "8be9dc29-3eca-499b-ae2d-e3c99699131a"
+  },
+  {
+    "Name": "win-hg-7-ssd-flex",
+    "RAM": 7000,
+    "Ephemeral": 0,
+    "VCPUs": 2,
+    "Is Public": true,
+    "Disk": 50,
+    "ID": "8d704cfd-05b2-4d4a-add2-e2868bcc081f"
+  },
+  {
+    "Name": "eg-30",
+    "RAM": 30000,
+    "Ephemeral": 0,
+    "VCPUs": 8,
+    "Is Public": true,
+    "Disk": 800,
+    "ID": "901f77c2-73f6-4fae-b28a-18b829b55a17"
+  },
+  {
+    "Name": "sp-60-ssd-flex",
+    "RAM": 60000,
+    "Ephemeral": 0,
+    "VCPUs": 4,
+    "Is Public": true,
+    "Disk": 50,
+    "ID": "944b92fb-9a0c-406d-bb9f-a1d93cda9f01"
+  },
+  {
+    "Name": "eg-30-flex",
+    "RAM": 30000,
+    "Ephemeral": 0,
+    "VCPUs": 8,
+    "Is Public": true,
+    "Disk": 50,
+    "ID": "965472c7-eb54-4d4d-bd6e-01ebb694a631"
+  },
+  {
+    "Name": "sp-120-ssd",
+    "RAM": 120000,
+    "Ephemeral": 0,
+    "VCPUs": 8,
+    "Is Public": true,
+    "Disk": 400,
+    "ID": "97824a8c-e683-49a8-a70a-ead64240395c"
+  },
+  {
+    "Name": "hg-60-ssd",
+    "RAM": 60000,
+    "Ephemeral": 0,
+    "VCPUs": 16,
+    "Is Public": true,
+    "Disk": 800,
+    "ID": "9831d7f1-3e79-483d-8958-88e3952c7ea2"
+  },
+  {
+    "Name": "eg-60",
+    "RAM": 60000,
+    "Ephemeral": 0,
+    "VCPUs": 16,
+    "Is Public": true,
+    "Disk": 1600,
+    "ID": "9e1f13d0-4fcc-4abc-a9e6-9c76d662c92d"
+  },
+  {
+    "Name": "win-eg-30-ssd",
+    "RAM": 30000,
+    "Ephemeral": 0,
+    "VCPUs": 8,
+    "Is Public": true,
+    "Disk": 400,
+    "ID": "9e6b85fa-6f37-45ce-a3d6-11ab40a28fad"
+  },
+  {
+    "Name": "hg-120-flex",
+    "RAM": 120000,
+    "Ephemeral": 0,
+    "VCPUs": 32,
+    "Is Public": true,
+    "Disk": 50,
+    "ID": "9ed787cc-a0db-400b-8cc1-49b6384a1000"
+  },
+  {
+    "Name": "sp-120-flex",
+    "RAM": 120000,
+    "Ephemeral": 0,
+    "VCPUs": 8,
+    "Is Public": true,
+    "Disk": 50,
+    "ID": "9f3cfdf7-b850-47cc-92be-33aefbfd2b05"
+  },
+  {
+    "Name": "hg-60-ssd-flex",
+    "RAM": 60000,
+    "Ephemeral": 0,
+    "VCPUs": 16,
+    "Is Public": true,
+    "Disk": 50,
+    "ID": "a37bdf17-e1b1-41cc-a67f-ed665a120446"
+  },
+  {
+    "Name": "win-hg-120-ssd",
+    "RAM": 120000,
+    "Ephemeral": 0,
+    "VCPUs": 32,
+    "Is Public": true,
+    "Disk": 800,
+    "ID": "aa753e73-dadb-4528-9c4a-24e36fc41bf4"
+  },
+  {
+    "Name": "win-sp-240-ssd-flex",
+    "RAM": 240000,
+    "Ephemeral": 0,
+    "VCPUs": 16,
+    "Is Public": true,
+    "Disk": 50,
+    "ID": "abc007b8-cc44-4b6b-9606-fd647b03e101"
+  },
+  {
+    "Name": "sp-120",
+    "RAM": 120000,
+    "Ephemeral": 0,
+    "VCPUs": 8,
+    "Is Public": true,
+    "Disk": 800,
+    "ID": "ac74cb45-d895-47dd-b9cf-c17778033d83"
+  },
+  {
+    "Name": "win-hg-15",
+    "RAM": 15000,
+    "Ephemeral": 0,
+    "VCPUs": 4,
+    "Is Public": true,
+    "Disk": 400,
+    "ID": "ae900175-72bd-4fbc-8ab2-4673b468aa5b"
+  },
+  {
+    "Name": "win-eg-15-ssd-flex",
+    "RAM": 15000,
+    "Ephemeral": 0,
+    "VCPUs": 4,
+    "Is Public": true,
+    "Disk": 50,
+    "ID": "aeb37dbf-d7c9-4fd7-93f1-f3818e488ede"
+  },
+  {
+    "Name": "hg-7-ssd",
+    "RAM": 7000,
+    "Ephemeral": 0,
+    "VCPUs": 2,
+    "Is Public": true,
+    "Disk": 100,
+    "ID": "b1dc776c-b6e3-4a96-b230-850f570db3d5"
+  },
+  {
+    "Name": "sp-60-ssd",
+    "RAM": 60000,
+    "Ephemeral": 0,
+    "VCPUs": 4,
+    "Is Public": true,
+    "Disk": 200,
+    "ID": "b24df495-10f3-466e-95ab-26f0f6839a2f"
+  },
+  {
+    "Name": "win-hg-120",
+    "RAM": 120000,
+    "Ephemeral": 0,
+    "VCPUs": 32,
+    "Is Public": true,
+    "Disk": 1600,
+    "ID": "b798e44e-bf71-488c-9335-f20bf5976547"
+  },
+  {
+    "Name": "eg-7-ssd",
+    "RAM": 7000,
+    "Ephemeral": 0,
+    "VCPUs": 2,
+    "Is Public": true,
+    "Disk": 100,
+    "ID": "b94e6623-913d-4147-b2a3-34ccf6fe7a5e"
+  },
+  {
+    "Name": "eg-15-flex",
+    "RAM": 15000,
+    "Ephemeral": 0,
+    "VCPUs": 4,
+    "Is Public": true,
+    "Disk": 50,
+    "ID": "bb5fdda8-34ec-40c8-a4e3-308b9e2c9ee2"
+  },
+  {
+    "Name": "win-eg-7-flex",
+    "RAM": 7000,
+    "Ephemeral": 0,
+    "VCPUs": 2,
+    "Is Public": true,
+    "Disk": 50,
+    "ID": "c65384f6-4665-461a-a292-2f3f5a016244"
+  },
+  {
+    "Name": "eg-60-ssd",
+    "RAM": 60000,
+    "Ephemeral": 0,
+    "VCPUs": 16,
+    "Is Public": true,
+    "Disk": 800,
+    "ID": "c678f1a8-6542-4f9d-89af-ffc98715d674"
+  },
+  {
+    "Name": "hg-30-ssd-flex",
+    "RAM": 30000,
+    "Ephemeral": 0,
+    "VCPUs": 8,
+    "Is Public": true,
+    "Disk": 50,
+    "ID": "d147a094-b653-41e7-9250-8d4da3044334"
+  },
+  {
+    "Name": "sp-30",
+    "RAM": 30000,
+    "Ephemeral": 0,
+    "VCPUs": 2,
+    "Is Public": true,
+    "Disk": 200,
+    "ID": "d1acf88d-6f55-4c5c-a914-4ecbdbd50d6b"
+  },
+  {
+    "Name": "sp-120-ssd-flex",
+    "RAM": 120000,
+    "Ephemeral": 0,
+    "VCPUs": 8,
+    "Is Public": true,
+    "Disk": 50,
+    "ID": "d2d33e8e-58b1-4661-8141-826c47f82166"
+  },
+  {
+    "Name": "hg-120-ssd-flex",
+    "RAM": 120000,
+    "Ephemeral": 0,
+    "VCPUs": 32,
+    "Is Public": true,
+    "Disk": 50,
+    "ID": "d7322c37-9881-4a57-9b40-2499fe2e8f42"
+  },
+  {
+    "Name": "win-hg-15-flex",
+    "RAM": 15000,
+    "Ephemeral": 0,
+    "VCPUs": 4,
+    "Is Public": true,
+    "Disk": 50,
+    "ID": "daf597ea-fbbc-4c71-a35e-5b41d33ccc6c"
+  },
+  {
+    "Name": "win-hg-30-ssd-flex",
+    "RAM": 30000,
+    "Ephemeral": 0,
+    "VCPUs": 8,
+    "Is Public": true,
+    "Disk": 50,
+    "ID": "dcfd834c-3932-47a3-8b4b-cdfeecdfde2c"
+  },
+  {
+    "Name": "win-hg-60",
+    "RAM": 60000,
+    "Ephemeral": 0,
+    "VCPUs": 16,
+    "Is Public": true,
+    "Disk": 1600,
+    "ID": "def75cbd-a4b1-4f82-9152-90c65df9587b"
+  },
+  {
+    "Name": "eg-30-ssd-flex",
+    "RAM": 30000,
+    "Ephemeral": 0,
+    "VCPUs": 8,
+    "Is Public": true,
+    "Disk": 50,
+    "ID": "e04c7ad6-a5de-45f5-93c9-f3343bdfe8d1"
+  },
+  {
+    "Name": "vps-ssd-3",
+    "RAM": 8000,
+    "Ephemeral": 0,
+    "VCPUs": 2,
+    "Is Public": true,
+    "Disk": 40,
+    "ID": "e43d7458-6b82-4a78-a712-3a4dc6748cf4"
+  },
+  {
+    "Name": "win-eg-15-flex",
+    "RAM": 15000,
+    "Ephemeral": 0,
+    "VCPUs": 4,
+    "Is Public": true,
+    "Disk": 50,
+    "ID": "e8bd3402-7310-4a0f-8b99-d9212359c957"
+  },
+  {
+    "Name": "win-eg-30",
+    "RAM": 30000,
+    "Ephemeral": 0,
+    "VCPUs": 8,
+    "Is Public": true,
+    "Disk": 800,
+    "ID": "ebf7a997-e2f8-42f4-84f7-33a3d53d1af9"
+  },
+  {
+    "Name": "eg-120-flex",
+    "RAM": 120000,
+    "Ephemeral": 0,
+    "VCPUs": 32,
+    "Is Public": true,
+    "Disk": 50,
+    "ID": "ec852ed3-1e42-4c59-abc3-12bcd26abec8"
+  },
+  {
+    "Name": "sp-240",
+    "RAM": 240000,
+    "Ephemeral": 0,
+    "VCPUs": 16,
+    "Is Public": true,
+    "Disk": 1600,
+    "ID": "ed286e2c-769f-4c47-ac52-b8de7a4891f6"
+  },
+  {
+    "Name": "win-sp-60-ssd",
+    "RAM": 60000,
+    "Ephemeral": 0,
+    "VCPUs": 4,
+    "Is Public": true,
+    "Disk": 200,
+    "ID": "ed835a73-d9a0-43ee-bd89-999c51d8426d"
+  },
+  {
+    "Name": "win-eg-15",
+    "RAM": 15000,
+    "Ephemeral": 0,
+    "VCPUs": 4,
+    "Is Public": true,
+    "Disk": 400,
+    "ID": "f06056c1-a2d4-40e7-a7d8-e5bfabada72e"
+  },
+  {
+    "Name": "win-sp-120",
+    "RAM": 120000,
+    "Ephemeral": 0,
+    "VCPUs": 8,
+    "Is Public": true,
+    "Disk": 800,
+    "ID": "f247dc56-395b-49de-9a62-93ccc4fff4ed"
+  },
+  {
+    "Name": "eg-7-flex",
+    "RAM": 7000,
+    "Ephemeral": 0,
+    "VCPUs": 2,
+    "Is Public": true,
+    "Disk": 50,
+    "ID": "f476f959-ffa6-46f2-94d8-72293570604d"
+  },
+  {
+    "Name": "sp-60-flex",
+    "RAM": 60000,
+    "Ephemeral": 0,
+    "VCPUs": 4,
+    "Is Public": true,
+    "Disk": 50,
+    "ID": "f52db47a-315f-49d4-bc5c-67dd118e7ac0"
+  },
+  {
+    "Name": "win-hg-120-flex",
+    "RAM": 120000,
+    "Ephemeral": 0,
+    "VCPUs": 32,
+    "Is Public": true,
+    "Disk": 50,
+    "ID": "f6cb8144-5d98-4057-b44f-46da342fb571"
+  },
+  {
+    "Name": "hg-7",
+    "RAM": 7000,
+    "Ephemeral": 0,
+    "VCPUs": 2,
+    "Is Public": true,
+    "Disk": 200,
+    "ID": "fa3cc551-0358-4170-be64-56ea432b064c"
+  },
+  {
+    "Name": "hg-15-ssd",
+    "RAM": 15000,
+    "Ephemeral": 0,
+    "VCPUs": 4,
+    "Is Public": true,
+    "Disk": 200,
+    "ID": "ff48c2cf-c17f-4682-aaf6-31d66786f808"
+  }
+    ]"""
+
+    @classmethod
+    def setup_class(self):
+        if 'OS_AUTH_URL' not in os.environ:
+            pytest.skip('no OS_AUTH_URL environment variable')
+
+    @patch('teuthology.misc.sh')
+    def test_sorted_flavors(self, m_sh):
+        o = OpenStack()
+        select = '^(vps|hg)-.*ssd'
+        m_sh.return_value = TestOpenStack.flavors
+        flavors = o.get_sorted_flavors('arch', select)
+        assert [u'vps-ssd-1',
+                u'vps-ssd-2',
+                u'hg-7-ssd-flex',
+                u'hg-7-ssd',
+                u'vps-ssd-3',
+                u'hg-15-ssd-flex',
+                u'hg-15-ssd',
+                u'hg-30-ssd-flex',
+                u'hg-30-ssd',
+                u'hg-60-ssd-flex',
+                u'hg-60-ssd',
+                u'hg-120-ssd-flex',
+                u'hg-120-ssd',
+        ] == [ f['Name'] for f in flavors ]
+        m_sh.assert_called_with("openstack --quiet flavor list -f json")
+
+    def test_flavor(self):
+        def get_sorted_flavors(self, arch, select):
+            return [
+                {
+                    'Name': 'too_small',
+                    'RAM': 2048,
+                    'Disk': 50,
+                    'VCPUs': 1,
+                },
+            ]
+        with patch.multiple(
+                OpenStack,
+                get_sorted_flavors=get_sorted_flavors,
+        ):
+            with pytest.raises(NoFlavorException):
+                hint = { 'ram': 1000, 'disk': 40, 'cpus': 2 }
+                OpenStack().flavor(hint, 'arch')
+
+        flavor = 'good-flavor'
+        def get_sorted_flavors(self, arch, select):
+            return [
+                {
+                    'Name': flavor,
+                    'RAM': 2048,
+                    'Disk': 50,
+                    'VCPUs': 2,
+                },
+            ]
+        with patch.multiple(
+                OpenStack,
+                get_sorted_flavors=get_sorted_flavors,
+        ):
+            hint = { 'ram': 1000, 'disk': 40, 'cpus': 2 }
+            assert flavor == OpenStack().flavor(hint, 'arch')
+
+    def test_flavor_range(self):
+        flavors = [
+                {
+                    'Name': 'too_small',
+                    'RAM': 2048,
+                    'Disk': 50,
+                    'VCPUs': 1,
+                },
+        ]
+        def get_sorted_flavors(self, arch, select):
+            return flavors
+
+        min = { 'ram': 1000, 'disk': 40, 'cpus': 2 }
+        good = { 'ram': 4000, 'disk': 40, 'cpus': 2 }
+
+        #
+        # there are no flavors in the required range
+        #
+        with patch.multiple(
+                OpenStack,
+                get_sorted_flavors=get_sorted_flavors,
+        ):
+            with pytest.raises(NoFlavorException):
+                OpenStack().flavor_range(min, good, 'arch')
+
+        #
+        # there is one flavor in the required range
+        #
+        flavors.append({
+                    'Name': 'min',
+                    'RAM': 2048,
+                    'Disk': 40,
+                    'VCPUs': 2,
+        })
+
+        with patch.multiple(
+                OpenStack,
+                get_sorted_flavors=get_sorted_flavors,
+        ):
+
+            assert 'min' == OpenStack().flavor_range(min, good, 'arch')
+
+        #
+        # out of the two flavors in the required range, get the bigger one
+        #
+        flavors.append({
+                    'Name': 'good',
+                    'RAM': 3000,
+                    'Disk': 40,
+                    'VCPUs': 2,
+        })
+
+        with patch.multiple(
+                OpenStack,
+                get_sorted_flavors=get_sorted_flavors,
+        ):
+
+            assert 'good' == OpenStack().flavor_range(min, good, 'arch')
+
+        #
+        # there is one flavor bigger or equal to good, get this one
+        #
+        flavors.append({
+                    'Name': 'best',
+                    'RAM': 4000,
+                    'Disk': 40,
+                    'VCPUs': 2,
+        })
+
+        with patch.multiple(
+                OpenStack,
+                get_sorted_flavors=get_sorted_flavors,
+        ):
+
+            assert 'best' == OpenStack().flavor_range(min, good, 'arch')
+
+        #
+        # there are two flavors bigger or equal to good, get the smallest one
+        #
+        flavors.append({
+                    'Name': 'too_big',
+                    'RAM': 30000,
+                    'Disk': 400,
+                    'VCPUs': 20,
+        })
+
+        with patch.multiple(
+                OpenStack,
+                get_sorted_flavors=get_sorted_flavors,
+        ):
+
+            assert 'best' == OpenStack().flavor_range(min, good, 'arch')
+
+
+    def test_interpret_hints(self):
+        defaults = {
+            'machine': {
+                'ram': 0,
+                'disk': 0,
+                'cpus': 0,
+            },
+            'volumes': {
+                'count': 0,
+                'size': 0,
+            },
+        }
+        expected_disk = 10 # first hint larger than the second
+        expected_ram = 20 # second hint larger than the first
+        expected_cpus = 0 # not set, hence zero by default
+        expected_count = 30 # second hint larger than the first
+        expected_size = 40 # does not exist in the first hint
+        hints = [
+            {
+                'machine': {
+                    'ram': 2,
+                    'disk': expected_disk,
+                },
+                'volumes': {
+                    'count': 9,
+                    'size': expected_size,
+                },
+            },
+            {
+                'machine': {
+                    'ram': expected_ram,
+                    'disk': 3,
+                },
+                'volumes': {
+                    'count': expected_count,
+                },
+            },
+        ]
+        hint = OpenStack().interpret_hints(defaults, hints)
+        assert hint == {
+            'machine': {
+                'ram': expected_ram,
+                'disk': expected_disk,
+                'cpus': expected_cpus,
+            },
+            'volumes': {
+                'count': expected_count,
+                'size': expected_size,
+            }
+        }
+        assert defaults == OpenStack().interpret_hints(defaults, None)
+
+    def test_get_provider(self):
+        auth = os.environ.get('OS_AUTH_URL', None)
+        os.environ['OS_AUTH_URL'] = 'cloud.ovh.net'
+        assert OpenStack().get_provider() == 'ovh'
+        if auth != None:
+            os.environ['OS_AUTH_URL'] = auth
+        else:
+            del os.environ['OS_AUTH_URL']
+
+    def test_get_os_url(self):
+        o = OpenStack()
+        #
+        # Only for OVH
+        #
+        o.provider = 'something'
+        assert "" == o.get_os_url("server ")
+        o.provider = 'ovh'
+        assert "" == o.get_os_url("unknown ")
+        type2cmd = {
+            'compute': ('server', 'flavor'),
+            'network': ('ip', 'security', 'network'),
+            'image': ('image',),
+            'volume': ('volume',),
+        }
+        os.environ['OS_REGION_NAME'] = 'REGION'
+        os.environ['OS_TENANT_ID'] = 'TENANT'
+        for (type, cmds) in type2cmd.items():
+            for cmd in cmds:
+                assert ("//" + type) in o.get_os_url(cmd + " ")
+        for type in type2cmd.keys():
+            assert ("//" + type) in o.get_os_url("whatever ", type=type)
+
+    @patch('teuthology.misc.sh')
+    def test_cache_token(self, m_sh):
+        token = 'TOKEN VALUE'
+        m_sh.return_value = token
+        OpenStack.token = None
+        o = OpenStack()
+        #
+        # Only for OVH
+        #
+        o.provider = 'something'
+        assert False == o.cache_token()
+        o.provider = 'ovh'
+        #
+        # Set the environment with the token
+        #
+        assert 'OS_TOKEN_VALUE' not in os.environ
+        assert 'OS_TOKEN_EXPIRES' not in os.environ
+        assert True == o.cache_token()
+        m_sh.assert_called_with('openstack -q token issue -c id -f value')
+        assert token == os.environ['OS_TOKEN_VALUE']
+        assert token == OpenStack.token
+        assert time.time() < int(os.environ['OS_TOKEN_EXPIRES'])
+        assert time.time() < OpenStack.token_expires
+        #
+        # Reset after it expires
+        #
+        token_expires = int(time.time()) - 2000
+        OpenStack.token_expires = token_expires
+        assert True == o.cache_token()
+        assert time.time() < int(os.environ['OS_TOKEN_EXPIRES'])
+        assert time.time() < OpenStack.token_expires
+
+    @patch('teuthology.misc.sh')
+    def test_cache_token_from_environment(self, m_sh):
+        OpenStack.token = None
+        o = OpenStack()
+        o.provider = 'ovh'
+        token = 'TOKEN VALUE'
+        os.environ['OS_TOKEN_VALUE'] = token
+        token_expires = int(time.time()) + OpenStack.token_cache_duration
+        os.environ['OS_TOKEN_EXPIRES'] = str(token_expires)
+        assert True == o.cache_token()
+        assert token == OpenStack.token
+        assert token_expires == OpenStack.token_expires
+        m_sh.assert_not_called()
+        
+    @patch('teuthology.misc.sh')
+    def test_cache_token_expired_environment(self, m_sh):
+        token = 'TOKEN VALUE'
+        m_sh.return_value = token
+        OpenStack.token = None
+        o = OpenStack()
+        o.provider = 'ovh'
+        os.environ['OS_TOKEN_VALUE'] = token
+        token_expires = int(time.time()) - 2000
+        os.environ['OS_TOKEN_EXPIRES'] = str(token_expires)
+        assert True == o.cache_token()
+        m_sh.assert_called_with('openstack -q token issue -c id -f value')
+        assert token == os.environ['OS_TOKEN_VALUE']
+        assert token == OpenStack.token
+        assert time.time() < int(os.environ['OS_TOKEN_EXPIRES'])
+        assert time.time() < OpenStack.token_expires
+
+class TestTeuthologyOpenStack(TestOpenStackBase):
+
+    @classmethod
+    def setup_class(self):
+        if 'OS_AUTH_URL' not in os.environ:
+            pytest.skip('no OS_AUTH_URL environment variable')
+
+        teuthology.log.setLevel(logging.DEBUG)
+        set_config_attr(argparse.Namespace())
+
+        ip = TeuthologyOpenStack.create_floating_ip()
+        if ip:
+            ip_id = TeuthologyOpenStack.get_floating_ip_id(ip)
+            OpenStack().run("ip floating delete " + ip_id)
+            self.can_create_floating_ips = True
+        else:
+            self.can_create_floating_ips = False
+
+    def setup_method(self):
+        super(TestTeuthologyOpenStack, self).setup_method()
+        self.key_filename = tempfile.mktemp()
+        self.key_name = 'teuthology-test'
+        self.name = 'teuthology-test'
+        self.clobber()
+        misc.sh("""
+openstack keypair create {key_name} > {key_filename}
+chmod 600 {key_filename}
+        """.format(key_filename=self.key_filename,
+                   key_name=self.key_name))
+        self.options = ['--key-name', self.key_name,
+                        '--key-filename', self.key_filename,
+                        '--name', self.name,
+                        '--verbose']
+
+    def teardown_method(self):
+        super(TestTeuthologyOpenStack, self).teardown_method()
+        self.clobber()
+        os.unlink(self.key_filename)
+
+    def clobber(self):
+        misc.sh("""
+openstack server delete {name} --wait || true
+openstack keypair delete {key_name} || true
+        """.format(key_name=self.key_name,
+                   name=self.name))
+
+    def test_create(self, caplog):
+        teuthology_argv = [
+            '--suite', 'upgrade/hammer',
+            '--dry-run',
+            '--ceph', 'main',
+            '--kernel', 'distro',
+            '--flavor', 'gcov',
+            '--distro', 'ubuntu',
+            '--suite-branch', 'hammer',
+            '--email', 'loic@dachary.org',
+            '--num', '10',
+            '--limit', '23',
+            '--subset', '1/2',
+            '--priority', '101',
+            '--timeout', '234',
+            '--filter', 'trasher',
+            '--filter-out', 'erasure-code',
+            '--throttle', '3',
+        ]
+        archive_upload = 'user@archive:/tmp'
+        argv = (self.options +
+                ['--teuthology-git-url', 'TEUTHOLOGY_URL',
+                 '--teuthology-branch', 'TEUTHOLOGY_BRANCH',
+                 '--ceph-workbench-git-url', 'CEPH_WORKBENCH_URL',
+                 '--ceph-workbench-branch', 'CEPH_WORKBENCH_BRANCH',
+                 '--upload',
+                 '--archive-upload', archive_upload] +
+                teuthology_argv)
+        args = scripts.openstack.parse_args(argv)
+        teuthology_argv.extend([
+            '--archive-upload', archive_upload,
+            '--archive-upload-url', args.archive_upload_url,
+        ])
+        teuthology = TeuthologyOpenStack(args, None, argv)
+        teuthology.user_data = 'teuthology/openstack/test/user-data-test1.txt'
+        teuthology.teuthology_suite = 'echo --'
+
+        teuthology.main()
+        assert 0 == teuthology.ssh("lsb_release -a")
+        assert 0 == teuthology.ssh("grep 'substituded variables' /var/log/cloud-init.log")
+        l = caplog.text
+        assert 'Ubuntu 14.04' in l
+        assert "nworkers=" + str(args.simultaneous_jobs) in l
+        assert "username=" + teuthology.username in l
+        assert "upload=--archive-upload user@archive:/tmp" in l
+        assert ("ceph_workbench="
+                " --ceph-workbench-branch CEPH_WORKBENCH_BRANCH"
+                " --ceph-workbench-git-url CEPH_WORKBENCH_URL") in l
+        assert "clone=git clone -b TEUTHOLOGY_BRANCH TEUTHOLOGY_URL" in l
+        assert os.environ['OS_AUTH_URL'] in l
+        assert " ".join(teuthology_argv) in l
+
+        if self.can_create_floating_ips:
+            ip = teuthology.get_floating_ip(self.name)
+        teuthology.teardown()
+        if self.can_create_floating_ips:
+            assert teuthology.get_floating_ip_id(ip) == None
+
+    def test_floating_ip(self):
+        if not self.can_create_floating_ips:
+            pytest.skip('unable to create floating ips')
+
+        expected = TeuthologyOpenStack.create_floating_ip()
+        ip = TeuthologyOpenStack.get_unassociated_floating_ip()
+        assert expected == ip
+        ip_id = TeuthologyOpenStack.get_floating_ip_id(ip)
+        OpenStack().run("ip floating delete " + ip_id)
diff --git a/tests/openstack/user-data-test1.txt b/tests/openstack/user-data-test1.txt
new file mode 100644 (file)
index 0000000..4e3e466
--- /dev/null
@@ -0,0 +1,5 @@
+#cloud-config
+system_info:
+  default_user:
+    name: ubuntu
+final_message: "teuthology is up and running after $UPTIME seconds, substituded variables nworkers=NWORKERS openrc=OPENRC username=TEUTHOLOGY_USERNAME upload=UPLOAD ceph_workbench=CEPH_WORKBENCH clone=CLONE_OPENSTACK"
diff --git a/tests/orchestra/__init__.py b/tests/orchestra/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/tests/orchestra/files/daemon-systemdstate-pid-ps-ef.output b/tests/orchestra/files/daemon-systemdstate-pid-ps-ef.output
new file mode 100644 (file)
index 0000000..ddddf57
--- /dev/null
@@ -0,0 +1,5 @@
+ceph       658     1  0 Jun08 ?        00:07:43 /usr/bin/ceph-mgr -f --cluster ceph --id host1 --setuser ceph --setgroup ceph
+ceph      1634     1  0 Jun08 ?        00:02:17 /usr/bin/ceph-mds -f --cluster ceph --id host1 --setuser ceph --setgroup ceph
+ceph     31555     1  0 Jun08 ?        01:13:50 /usr/bin/ceph-mon -f --cluster ceph --id host1 --setuser ceph --setgroup ceph
+ceph     31765     1  0 Jun08 ?        00:48:42 /usr/bin/radosgw -f --cluster ceph --name client.rgw.host1.rgw0 --setuser ceph --setgroup ceph
+ceph     97427     1  0 Jun17 ?        00:41:39 /usr/bin/ceph-osd -f --cluster ceph --id 0 --setuser ceph --setgroup ceph
\ No newline at end of file
diff --git a/tests/orchestra/integration/__init__.py b/tests/orchestra/integration/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/tests/orchestra/integration/test_integration.py b/tests/orchestra/integration/test_integration.py
new file mode 100644 (file)
index 0000000..5adeb6d
--- /dev/null
@@ -0,0 +1,94 @@
+from teuthology.orchestra import monkey
+monkey.patch_all()
+
+from io import StringIO
+
+import os
+from teuthology.orchestra import connection, remote, run
+from teuthology.orchestra.test.util import assert_raises
+from teuthology.exceptions import CommandCrashedError, ConnectionLostError
+
+from pytest import skip
+
+HOST = None
+
+
+class TestIntegration():
+    def setup_method(self):
+        try:
+            host = os.environ['ORCHESTRA_TEST_HOST']
+        except KeyError:
+            skip('To run integration tests, set environment ' +
+                 'variable ORCHESTRA_TEST_HOST to user@host to use.')
+        global HOST
+        HOST = host
+
+    def test_crash(self):
+        ssh = connection.connect(HOST)
+        e = assert_raises(
+            CommandCrashedError,
+            run.run,
+            client=ssh,
+            args=['sh', '-c', 'kill -ABRT $$'],
+            )
+        assert e.command == "sh -c 'kill -ABRT $$'"
+        assert str(e) == "Command crashed: \"sh -c 'kill -ABRT $$'\""
+
+    def test_lost(self):
+        ssh = connection.connect(HOST)
+        e = assert_raises(
+            ConnectionLostError,
+            run.run,
+            client=ssh,
+            args=['sh', '-c', 'kill -ABRT $PPID'],
+            name=HOST,
+            )
+        assert e.command == "sh -c 'kill -ABRT $PPID'"
+        assert str(e) == \
+            "SSH connection to {host} was lost: ".format(host=HOST) + \
+            "\"sh -c 'kill -ABRT $PPID'\""
+
+    def test_pipe(self):
+        ssh = connection.connect(HOST)
+        r = run.run(
+            client=ssh,
+            args=['cat'],
+            stdin=run.PIPE,
+            stdout=StringIO(),
+            wait=False,
+            )
+        assert r.stdout.getvalue() == ''
+        r.stdin.write('foo\n')
+        r.stdin.write('bar\n')
+        r.stdin.close()
+
+        r.wait()
+        got = r.exitstatus
+        assert got == 0
+        assert r.stdout.getvalue() == 'foo\nbar\n'
+
+    def test_and(self):
+        ssh = connection.connect(HOST)
+        r = run.run(
+            client=ssh,
+            args=['true', run.Raw('&&'), 'echo', 'yup'],
+            stdout=StringIO(),
+            )
+        assert r.stdout.getvalue() == 'yup\n'
+
+    def test_os(self):
+        rem = remote.Remote(HOST)
+        assert rem.os.name
+        assert rem.os.version
+
+    def test_17102(self, caplog):
+        # http://tracker.ceph.com/issues/17102
+        rem = remote.Remote(HOST)
+        interval = 3
+        rem.run(args="echo before; sleep %s; echo after" % interval)
+        for record in caplog.records:
+            if record.msg == 'before':
+                before_time = record.created
+            elif record.msg == 'after':
+                after_time = record.created
+        assert int(round(after_time - before_time)) == interval
diff --git a/tests/orchestra/test_cluster.py b/tests/orchestra/test_cluster.py
new file mode 100644 (file)
index 0000000..27bef8b
--- /dev/null
@@ -0,0 +1,232 @@
+import pytest
+
+from mock import patch, Mock
+
+from teuthology.orchestra import cluster, remote, run
+
+
+class TestCluster(object):
+    def test_init_empty(self):
+        c = cluster.Cluster()
+        assert c.remotes == {}
+
+    def test_init(self):
+        r1 = Mock()
+        r2 = Mock()
+        c = cluster.Cluster(
+            remotes=[
+                (r1, ['foo', 'bar']),
+                (r2, ['baz']),
+                ],
+            )
+        r3 = Mock()
+        c.add(r3, ['xyzzy', 'thud', 'foo'])
+        assert c.remotes == {
+            r1: ['foo', 'bar'],
+            r2: ['baz'],
+            r3: ['xyzzy', 'thud', 'foo'],
+        }
+
+    def test_repr(self):
+        r1 = remote.Remote('r1', ssh=Mock())
+        r2 = remote.Remote('r2', ssh=Mock())
+        c = cluster.Cluster(
+            remotes=[
+                (r1, ['foo', 'bar']),
+                (r2, ['baz']),
+                ],
+            )
+        assert repr(c) == \
+            "Cluster(remotes=[[Remote(name='r1'), ['foo', 'bar']], " \
+            "[Remote(name='r2'), ['baz']]])"
+
+    def test_str(self):
+        r1 = remote.Remote('r1', ssh=Mock())
+        r2 = remote.Remote('r2', ssh=Mock())
+        c = cluster.Cluster(
+            remotes=[
+                (r1, ['foo', 'bar']),
+                (r2, ['baz']),
+                ],
+            )
+        assert str(c) == "r1[foo,bar] r2[baz]"
+
+    def test_run_all(self):
+        r1 = Mock(spec=remote.Remote)
+        r1.configure_mock(name='r1')
+        ret1 = Mock(spec=run.RemoteProcess)
+        r1.run.return_value = ret1
+        r2 = Mock(spec=remote.Remote)
+        r2.configure_mock(name='r2')
+        ret2 = Mock(spec=run.RemoteProcess)
+        r2.run.return_value = ret2
+        c = cluster.Cluster(
+            remotes=[
+                (r1, ['foo', 'bar']),
+                (r2, ['baz']),
+                ],
+            )
+        got = c.run(args=['test'])
+        r1.run.assert_called_once_with(args=['test'], wait=True)
+        r2.run.assert_called_once_with(args=['test'], wait=True)
+        assert len(got) == 2
+        assert got, [ret1 == ret2]
+        # check identity not equality
+        assert got[0] is ret1
+        assert got[1] is ret2
+
+    def test_only_one(self):
+        r1 = Mock()
+        r2 = Mock()
+        r3 = Mock()
+        c = cluster.Cluster(
+            remotes=[
+                (r1, ['foo', 'bar']),
+                (r2, ['bar']),
+                (r3, ['foo']),
+                ],
+            )
+        c_foo = c.only('foo')
+        assert c_foo.remotes == {r1: ['foo', 'bar'], r3: ['foo']}
+
+    def test_only_two(self):
+        r1 = Mock()
+        r2 = Mock()
+        r3 = Mock()
+        c = cluster.Cluster(
+            remotes=[
+                (r1, ['foo', 'bar']),
+                (r2, ['bar']),
+                (r3, ['foo']),
+                ],
+            )
+        c_both = c.only('foo', 'bar')
+        assert c_both.remotes, {r1: ['foo' == 'bar']}
+
+    def test_only_none(self):
+        r1 = Mock()
+        r2 = Mock()
+        r3 = Mock()
+        c = cluster.Cluster(
+            remotes=[
+                (r1, ['foo', 'bar']),
+                (r2, ['bar']),
+                (r3, ['foo']),
+                ],
+            )
+        c_none = c.only('impossible')
+        assert c_none.remotes == {}
+
+    def test_only_match(self):
+        r1 = Mock()
+        r2 = Mock()
+        r3 = Mock()
+        c = cluster.Cluster(
+            remotes=[
+                (r1, ['foo', 'bar']),
+                (r2, ['bar']),
+                (r3, ['foo']),
+                ],
+            )
+        c_foo = c.only('foo', lambda role: role.startswith('b'))
+        assert c_foo.remotes, {r1: ['foo' == 'bar']}
+
+    def test_exclude_one(self):
+        r1 = Mock()
+        r2 = Mock()
+        r3 = Mock()
+        c = cluster.Cluster(
+            remotes=[
+                (r1, ['foo', 'bar']),
+                (r2, ['bar']),
+                (r3, ['foo']),
+                ],
+            )
+        c_foo = c.exclude('foo')
+        assert c_foo.remotes == {r2: ['bar']}
+
+    def test_exclude_two(self):
+        r1 = Mock()
+        r2 = Mock()
+        r3 = Mock()
+        c = cluster.Cluster(
+            remotes=[
+                (r1, ['foo', 'bar']),
+                (r2, ['bar']),
+                (r3, ['foo']),
+                ],
+            )
+        c_both = c.exclude('foo', 'bar')
+        assert c_both.remotes == {r2: ['bar'], r3: ['foo']}
+
+    def test_exclude_none(self):
+        r1 = Mock()
+        r2 = Mock()
+        r3 = Mock()
+        c = cluster.Cluster(
+            remotes=[
+                (r1, ['foo', 'bar']),
+                (r2, ['bar']),
+                (r3, ['foo']),
+                ],
+            )
+        c_none = c.exclude('impossible')
+        assert c_none.remotes == {r1: ['foo', 'bar'], r2: ['bar'], r3: ['foo']}
+
+    def test_exclude_match(self):
+        r1 = Mock()
+        r2 = Mock()
+        r3 = Mock()
+        c = cluster.Cluster(
+            remotes=[
+                (r1, ['foo', 'bar']),
+                (r2, ['bar']),
+                (r3, ['foo']),
+                ],
+            )
+        c_foo = c.exclude('foo', lambda role: role.startswith('b'))
+        assert c_foo.remotes == {r2: ['bar'], r3: ['foo']}
+
+    def test_filter(self):
+        r1 = Mock(_name='r1')
+        r2 = Mock(_name='r2')
+        def func(r):
+            return r._name == "r1"
+        c = cluster.Cluster(remotes=[
+                (r1, ['foo']),
+                (r2, ['bar']),
+            ])
+        assert c.filter(func).remotes == {
+            r1: ['foo']
+        }
+
+
+class TestWriteFile(object):
+    """ Tests for cluster.write_file """
+    def setup_method(self):
+        self.r1 = remote.Remote('r1', ssh=Mock())
+        self.c = cluster.Cluster(
+            remotes=[
+                (self.r1, ['foo', 'bar']),
+            ],
+        )
+
+    @patch("teuthology.orchestra.remote.RemoteShell.write_file")
+    def test_write_file(self, m_write_file):
+        self.c.write_file("filename", "content")
+        m_write_file.assert_called_with("filename", "content")
+
+    @patch("teuthology.orchestra.remote.RemoteShell.write_file")
+    def test_fails_with_invalid_perms(self, m_write_file):
+        with pytest.raises(ValueError):
+            self.c.write_file("filename", "content", sudo=False, perms="invalid")
+
+    @patch("teuthology.orchestra.remote.RemoteShell.write_file")
+    def test_fails_with_invalid_owner(self, m_write_file):
+        with pytest.raises(ValueError):
+            self.c.write_file("filename", "content", sudo=False, owner="invalid")
+
+    @patch("teuthology.orchestra.remote.RemoteShell.write_file")
+    def test_with_sudo(self, m_write_file):
+        self.c.write_file("filename", "content", sudo=True)
+        m_write_file.assert_called_with("filename", "content", sudo=True, owner=None, mode=None)
diff --git a/tests/orchestra/test_connection.py b/tests/orchestra/test_connection.py
new file mode 100644 (file)
index 0000000..8051d57
--- /dev/null
@@ -0,0 +1,120 @@
+from mock import patch, Mock
+
+from teuthology import config
+from teuthology.orchestra import connection
+from tests.orchestra.util import assert_raises
+
+
+class TestConnection(object):
+    def setup_method(self):
+        self.start_patchers()
+
+    def teardown_method(self):
+        self.stop_patchers()
+
+    def start_patchers(self):
+        self.patcher_sleep = patch(
+            'time.sleep',
+        )
+        self.patcher_sleep.start()
+        self.m_ssh = Mock()
+        self.patcher_ssh = patch(
+            'teuthology.orchestra.connection.paramiko.SSHClient',
+            self.m_ssh,
+        )
+        self.patcher_ssh.start()
+
+    def stop_patchers(self):
+        self.patcher_ssh.stop()
+        self.patcher_sleep.stop()
+
+    def clear_config(self):
+        config.config.teuthology_yaml = ''
+        config.config.load()
+        config.config.ssh_key = None
+
+    def test_split_user_just_host(self):
+        got = connection.split_user('somehost.example.com')
+        assert got == (None, 'somehost.example.com')
+
+    def test_split_user_both(self):
+        got = connection.split_user('jdoe@somehost.example.com')
+        assert got == ('jdoe', 'somehost.example.com')
+
+    def test_split_user_empty_user(self):
+        s = '@somehost.example.com'
+        e = assert_raises(AssertionError, connection.split_user, s)
+        assert str(e) == 'Bad input to split_user: {s!r}'.format(s=s)
+
+    def test_connect(self):
+        self.clear_config()
+        config.config.verify_host_keys = True
+        m_ssh_instance = self.m_ssh.return_value = Mock();
+        m_transport = Mock()
+        m_ssh_instance.get_transport.return_value = m_transport
+        got = connection.connect(
+            'jdoe@orchestra.test.newdream.net.invalid',
+            _SSHClient=self.m_ssh,
+        )
+        self.m_ssh.assert_called_once()
+        m_ssh_instance.set_missing_host_key_policy.assert_called_once()
+        m_ssh_instance.load_system_host_keys.assert_called_once_with()
+        m_ssh_instance.connect.assert_called_once_with(
+            hostname='orchestra.test.newdream.net.invalid',
+            username='jdoe',
+            timeout=60,
+        )
+        m_transport.set_keepalive.assert_called_once_with(False)
+        assert got is m_ssh_instance
+
+    def test_connect_no_verify_host_keys(self):
+        self.clear_config()
+        config.config.verify_host_keys = False
+        m_ssh_instance = self.m_ssh.return_value = Mock();
+        m_transport = Mock()
+        m_ssh_instance.get_transport.return_value = m_transport
+        got = connection.connect(
+            'jdoe@orchestra.test.newdream.net.invalid',
+            _SSHClient=self.m_ssh,
+        )
+        self.m_ssh.assert_called_once()
+        m_ssh_instance.set_missing_host_key_policy.assert_called_once()
+        assert not m_ssh_instance.load_system_host_keys.called
+        m_ssh_instance.connect.assert_called_once_with(
+            hostname='orchestra.test.newdream.net.invalid',
+            username='jdoe',
+            timeout=60,
+        )
+        m_transport.set_keepalive.assert_called_once_with(False)
+        assert got is m_ssh_instance
+
+    def test_connect_override_hostkeys(self):
+        self.clear_config()
+        m_ssh_instance = self.m_ssh.return_value = Mock();
+        m_transport = Mock()
+        m_ssh_instance.get_transport.return_value = m_transport
+        m_host_keys = Mock()
+        m_ssh_instance.get_host_keys.return_value = m_host_keys
+        m_create_key = Mock()
+        m_create_key.return_value = "frobnitz"
+        got = connection.connect(
+            'jdoe@orchestra.test.newdream.net.invalid',
+            host_key='ssh-rsa testkey',
+            _SSHClient=self.m_ssh,
+            _create_key=m_create_key,
+            )
+        self.m_ssh.assert_called_once()
+        m_ssh_instance.get_host_keys.assert_called_once()
+        m_host_keys.add.assert_called_once_with(
+            hostname='orchestra.test.newdream.net.invalid',
+            keytype='ssh-rsa',
+            key='frobnitz',
+        )
+        m_create_key.assert_called_once_with('ssh-rsa', 'testkey')
+        m_ssh_instance.connect.assert_called_once_with(
+            hostname='orchestra.test.newdream.net.invalid',
+            username='jdoe',
+            timeout=60,
+        )
+        m_transport.set_keepalive.assert_called_once_with(False)
+        assert got is m_ssh_instance
diff --git a/tests/orchestra/test_console.py b/tests/orchestra/test_console.py
new file mode 100644 (file)
index 0000000..fe0399b
--- /dev/null
@@ -0,0 +1,217 @@
+from mock import patch
+
+from teuthology.config import config as teuth_config
+
+from teuthology.orchestra import console
+
+
+class TestConsole(object):
+    pass
+
+
+class TestPhysicalConsole(TestConsole):
+    klass = console.PhysicalConsole
+    ipmi_cmd_templ = 'ipmitool -H {h}.{d} -I lanplus -U {u} -P {p} {c}'
+    conserver_cmd_templ = 'console -M {m} -p {p} {mode} {h}'
+
+    def setup_method(self):
+        self.hostname = 'host'
+        teuth_config.ipmi_domain = 'ipmi_domain'
+        teuth_config.ipmi_user = 'ipmi_user'
+        teuth_config.ipmi_password = 'ipmi_pass'
+        teuth_config.conserver_master = 'conserver_master'
+        teuth_config.conserver_port = 3109
+        teuth_config.use_conserver = True
+
+    def test_has_ipmi_creds(self):
+        cons = self.klass(self.hostname)
+        assert cons.has_ipmi_credentials is True
+        teuth_config.ipmi_domain = None
+        cons = self.klass(self.hostname)
+        assert cons.has_ipmi_credentials is False
+
+    def test_console_command_conserver(self):
+        cons = self.klass(
+            self.hostname,
+            teuth_config.ipmi_user,
+            teuth_config.ipmi_password,
+            teuth_config.ipmi_domain,
+        )
+        cons.has_conserver = True
+        console_cmd = cons._console_command()
+        assert console_cmd == self.conserver_cmd_templ.format(
+            m=teuth_config.conserver_master,
+            p=teuth_config.conserver_port,
+            mode='-s',
+            h=self.hostname,
+        )
+        console_cmd = cons._console_command(readonly=False)
+        assert console_cmd == self.conserver_cmd_templ.format(
+            m=teuth_config.conserver_master,
+            p=teuth_config.conserver_port,
+            mode='-f',
+            h=self.hostname,
+        )
+
+    def test_console_command_ipmi(self):
+        teuth_config.conserver_master = None
+        cons = self.klass(
+            self.hostname,
+            teuth_config.ipmi_user,
+            teuth_config.ipmi_password,
+            teuth_config.ipmi_domain,
+        )
+        sol_cmd = cons._console_command()
+        assert sol_cmd == self.ipmi_cmd_templ.format(
+            h=self.hostname,
+            d=teuth_config.ipmi_domain,
+            u=teuth_config.ipmi_user,
+            p=teuth_config.ipmi_password,
+            c='sol activate',
+        )
+
+    def test_ipmi_command_ipmi(self):
+        cons = self.klass(
+            self.hostname,
+            teuth_config.ipmi_user,
+            teuth_config.ipmi_password,
+            teuth_config.ipmi_domain,
+        )
+        pc_cmd = cons._ipmi_command('power cycle')
+        assert pc_cmd == self.ipmi_cmd_templ.format(
+            h=self.hostname,
+            d=teuth_config.ipmi_domain,
+            u=teuth_config.ipmi_user,
+            p=teuth_config.ipmi_password,
+            c='power cycle',
+        )
+
+    def test_spawn_log_conserver(self):
+        with patch(
+            'teuthology.orchestra.console.psutil.subprocess.Popen',
+            autospec=True,
+        ) as m_popen:
+            m_popen.return_value.pid = 42
+            m_popen.return_value.returncode = 0
+            m_popen.return_value.wait.return_value = 0
+            cons = self.klass(self.hostname)
+            assert cons.has_conserver is True
+            m_popen.reset_mock()
+            m_popen.return_value.poll.return_value = None
+            cons.spawn_sol_log('/fake/path')
+            assert m_popen.call_count == 1
+            call_args = m_popen.call_args_list[0][0][0]
+            assert any(
+                [teuth_config.conserver_master in arg for arg in call_args]
+            )
+
+    def test_spawn_log_ipmi(self):
+        with patch(
+            'teuthology.orchestra.console.psutil.subprocess.Popen',
+            autospec=True,
+        ) as m_popen:
+            m_popen.return_value.pid = 42
+            m_popen.return_value.returncode = 1
+            m_popen.return_value.wait.return_value = 1
+            cons = self.klass(self.hostname)
+            assert cons.has_conserver is False
+            m_popen.reset_mock()
+            m_popen.return_value.poll.return_value = 1
+            cons.spawn_sol_log('/fake/path')
+            assert m_popen.call_count == 1
+            call_args = m_popen.call_args_list[0][0][0]
+            assert any(
+                ['ipmitool' in arg for arg in call_args]
+            )
+
+    def test_spawn_log_fallback(self):
+        with patch(
+            'teuthology.orchestra.console.psutil.subprocess.Popen',
+            autospec=True,
+        ) as m_popen:
+            m_popen.return_value.pid = 42
+            m_popen.return_value.returncode = 0
+            m_popen.return_value.wait.return_value = 0
+            cons = self.klass(self.hostname)
+            assert cons.has_conserver is True
+            m_popen.reset_mock()
+            m_popen.return_value.poll.return_value = 1
+            cons.spawn_sol_log('/fake/path')
+            assert cons.has_conserver is False
+            assert m_popen.call_count == 2
+            call_args = m_popen.call_args_list[1][0][0]
+            assert any(
+                ['ipmitool' in arg for arg in call_args]
+            )
+
+    def test_get_console_conserver(self):
+        with patch(
+            'teuthology.orchestra.console.psutil.subprocess.Popen',
+            autospec=True,
+        ) as m_popen:
+            m_popen.return_value.pid = 42
+            m_popen.return_value.returncode = 0
+            m_popen.return_value.wait.return_value = 0
+            cons = self.klass(self.hostname)
+        assert cons.has_conserver is True
+        with patch(
+            'teuthology.orchestra.console.pexpect.spawn',
+            autospec=True,
+        ) as m_spawn:
+            cons._get_console()
+            assert m_spawn.call_count == 1
+            assert teuth_config.conserver_master in \
+                m_spawn.call_args_list[0][0][0]
+
+    def test_get_console_ipmitool(self):
+        with patch(
+            'teuthology.orchestra.console.psutil.subprocess.Popen',
+            autospec=True,
+        ) as m_popen:
+            m_popen.return_value.pid = 42
+            m_popen.return_value.returncode = 0
+            m_popen.return_value.wait.return_value = 0
+            cons = self.klass(self.hostname)
+        assert cons.has_conserver is True
+        with patch(
+            'teuthology.orchestra.console.pexpect.spawn',
+            autospec=True,
+        ) as m_spawn:
+            cons.has_conserver = False
+            cons._get_console()
+            assert m_spawn.call_count == 1
+            assert 'ipmitool' in m_spawn.call_args_list[0][0][0]
+
+    def test_get_console_fallback(self):
+        with patch(
+            'teuthology.orchestra.console.psutil.subprocess.Popen',
+            autospec=True,
+        ) as m_popen:
+            m_popen.return_value.pid = 42
+            m_popen.return_value.returncode = 0
+            m_popen.return_value.wait.return_value = 0
+            cons = self.klass(self.hostname)
+        assert cons.has_conserver is True
+        with patch(
+            'teuthology.orchestra.console.pexpect.spawn',
+            autospec=True,
+        ) as m_spawn:
+            cons.has_conserver = True
+            m_spawn.return_value.isalive.return_value = False
+            cons._get_console()
+            assert m_spawn.return_value.isalive.call_count == 1
+            assert m_spawn.call_count == 2
+            assert cons.has_conserver is False
+            assert 'ipmitool' in m_spawn.call_args_list[1][0][0]
+
+    def test_disable_conserver(self):
+        with patch(
+            'teuthology.orchestra.console.psutil.subprocess.Popen',
+            autospec=True,
+        ) as m_popen:
+            m_popen.return_value.pid = 42
+            m_popen.return_value.returncode = 0
+            m_popen.return_value.wait.return_value = 0
+            teuth_config.use_conserver = False
+            cons = self.klass(self.hostname)
+            assert cons.has_conserver is False
diff --git a/tests/orchestra/test_opsys.py b/tests/orchestra/test_opsys.py
new file mode 100644 (file)
index 0000000..b69d08a
--- /dev/null
@@ -0,0 +1,408 @@
+from textwrap import dedent
+from teuthology.orchestra.opsys import OS
+import pytest
+
+
+class TestOS(object):
+    str_centos_9_os_release = dedent("""
+        NAME="CentOS Stream"
+        VERSION="9"
+        ID="centos"
+        ID_LIKE="rhel fedora"
+        VERSION_ID="9"
+        PLATFORM_ID="platform:el9"
+        PRETTY_NAME="CentOS Stream 9"
+        ANSI_COLOR="0;31"
+        LOGO="fedora-logo-icon"
+        CPE_NAME="cpe:/o:centos:centos:9"
+        HOME_URL="https://centos.org/"
+        BUG_REPORT_URL="https://issues.redhat.com/"
+        REDHAT_SUPPORT_PRODUCT="Red Hat Enterprise Linux 9"
+        REDHAT_SUPPORT_PRODUCT_VERSION="CentOS Stream"
+    """)
+
+    str_centos_7_os_release = dedent("""
+        NAME="CentOS Linux"
+        VERSION="7 (Core)"
+        ID="centos"
+        ID_LIKE="rhel fedora"
+        VERSION_ID="7"
+        PRETTY_NAME="CentOS Linux 7 (Core)"
+        ANSI_COLOR="0;31"
+        CPE_NAME="cpe:/o:centos:centos:7"
+        HOME_URL="https://www.centos.org/"
+        BUG_REPORT_URL="https://bugs.centos.org/"
+    """)
+
+    str_centos_7_os_release_newer = dedent("""
+        NAME="CentOS Linux"
+        VERSION="7 (Core)"
+        ID="centos"
+        ID_LIKE="rhel fedora"
+        VERSION_ID="7"
+        PRETTY_NAME="CentOS Linux 7 (Core)"
+        ANSI_COLOR="0;31"
+        CPE_NAME="cpe:/o:centos:centos:7"
+        HOME_URL="https://www.centos.org/"
+        BUG_REPORT_URL="https://bugs.centos.org/"
+
+        CENTOS_MANTISBT_PROJECT="CentOS-7"
+        CENTOS_MANTISBT_PROJECT_VERSION="7"
+        REDHAT_SUPPORT_PRODUCT="centos"
+        REDHAT_SUPPORT_PRODUCT_VERSION="7"
+    """)
+
+    str_debian_7_lsb_release = dedent("""
+        Distributor ID: Debian
+        Description:    Debian GNU/Linux 7.1 (wheezy)
+        Release:        7.1
+        Codename:       wheezy
+    """)
+
+    str_debian_7_os_release = dedent("""
+        PRETTY_NAME="Debian GNU/Linux 7 (wheezy)"
+        NAME="Debian GNU/Linux"
+        VERSION_ID="7"
+        VERSION="7 (wheezy)"
+        ID=debian
+        ANSI_COLOR="1;31"
+        HOME_URL="http://www.debian.org/"
+        SUPPORT_URL="http://www.debian.org/support/"
+        BUG_REPORT_URL="http://bugs.debian.org/"
+    """)
+
+    str_debian_8_os_release = dedent("""
+        PRETTY_NAME="Debian GNU/Linux 8 (jessie)"
+        NAME="Debian GNU/Linux"
+        VERSION_ID="8"
+        VERSION="8 (jessie)"
+        ID=debian
+        HOME_URL="http://www.debian.org/"
+        SUPPORT_URL="http://www.debian.org/support/"
+        BUG_REPORT_URL="https://bugs.debian.org/"
+    """)
+
+    str_debian_9_os_release = dedent("""
+        PRETTY_NAME="Debian GNU/Linux 9 (stretch)"
+        NAME="Debian GNU/Linux"
+        VERSION_ID="9"
+        VERSION="9 (stretch)"
+        ID=debian
+        HOME_URL="https://www.debian.org/"
+        SUPPORT_URL="https://www.debian.org/support"
+        BUG_REPORT_URL="https://bugs.debian.org/"
+    """)
+
+    str_ubuntu_12_04_lsb_release = dedent("""
+        Distributor ID: Ubuntu
+        Description:    Ubuntu 12.04.4 LTS
+        Release:        12.04
+        Codename:       precise
+    """)
+
+    str_ubuntu_12_04_os_release = dedent("""
+        NAME="Ubuntu"
+        VERSION="12.04.4 LTS, Precise Pangolin"
+        ID=ubuntu
+        ID_LIKE=debian
+        PRETTY_NAME="Ubuntu precise (12.04.4 LTS)"
+        VERSION_ID="12.04"
+    """)
+
+    str_ubuntu_14_04_os_release = dedent("""
+        NAME="Ubuntu"
+        VERSION="14.04.4 LTS, Trusty Tahr"
+        ID=ubuntu
+        ID_LIKE=debian
+        PRETTY_NAME="Ubuntu 14.04.4 LTS"
+        VERSION_ID="14.04"
+        HOME_URL="http://www.ubuntu.com/"
+        SUPPORT_URL="http://help.ubuntu.com/"
+        BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"
+    """)
+
+    str_ubuntu_16_04_os_release = dedent("""
+        NAME="Ubuntu"
+        VERSION="16.04 LTS (Xenial Xerus)"
+        ID=ubuntu
+        ID_LIKE=debian
+        PRETTY_NAME="Ubuntu 16.04 LTS"
+        VERSION_ID="16.04"
+        HOME_URL="http://www.ubuntu.com/"
+        SUPPORT_URL="http://help.ubuntu.com/"
+        BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"
+        UBUNTU_CODENAME=xenial
+    """)
+
+    str_ubuntu_18_04_os_release = dedent("""
+        NAME="Ubuntu"
+        VERSION="18.04 LTS (Bionic Beaver)"
+        ID=ubuntu
+        ID_LIKE=debian
+        PRETTY_NAME="Ubuntu 18.04 LTS"
+        VERSION_ID="18.04"
+        HOME_URL="https://www.ubuntu.com/"
+        SUPPORT_URL="https://help.ubuntu.com/"
+        BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
+        PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
+        VERSION_CODENAME=bionic
+        UBUNTU_CODENAME=bionic
+    """)
+
+    str_rhel_6_4_lsb_release = dedent("""
+        LSB Version:    :base-4.0-amd64:base-4.0-noarch:core-4.0-amd64:core-4.0-noarch:graphics-4.0-amd64:graphics-4.0-noarch:printing-4.0-amd64:printing-4.0-noarch
+        Distributor ID: RedHatEnterpriseServer
+        Description:    Red Hat Enterprise Linux Server release 6.4 (Santiago)
+        Release:        6.4
+        Codename:       Santiago
+    """)
+
+    str_rhel_7_lsb_release = dedent("""
+        LSB Version:    :core-4.1-amd64:core-4.1-noarch:cxx-4.1-amd64:cxx-4.1-noarch:desktop-4.1-amd64:desktop-4.1-noarch:languages-4.1-amd64:languages-4.1-noarch:printing-4.1-amd64:printing-4.1-noarch
+        Distributor ID: RedHatEnterpriseServer
+        Description:    Red Hat Enterprise Linux Server release 7.0 (Maipo)
+        Release:        7.0
+        Codename:       Maipo
+    """)
+
+    str_rhel_7_os_release = dedent("""
+        NAME="Red Hat Enterprise Linux Server"
+        VERSION="7.0 (Maipo)"
+        ID="rhel"
+        ID_LIKE="fedora"
+        VERSION_ID="7.0"
+        PRETTY_NAME="Red Hat Enterprise Linux Server 7.0 (Maipo)"
+        ANSI_COLOR="0;31"
+        CPE_NAME="cpe:/o:redhat:enterprise_linux:7.0:GA:server"
+        HOME_URL="https://www.redhat.com/"
+        BUG_REPORT_URL="https://bugzilla.redhat.com/"
+
+        REDHAT_BUGZILLA_PRODUCT="Red Hat Enterprise Linux 7"
+        REDHAT_BUGZILLA_PRODUCT_VERSION=7.0
+        REDHAT_SUPPORT_PRODUCT="Red Hat Enterprise Linux"
+        REDHAT_SUPPORT_PRODUCT_VERSION=7.0
+    """)
+
+    str_fedora_26_os_release = dedent("""
+        NAME=Fedora
+        VERSION="26 (Twenty Six)"
+        ID=fedora
+        VERSION_ID=26
+        PRETTY_NAME="Fedora 26 (Twenty Six)"
+        ANSI_COLOR="0;34"
+        CPE_NAME="cpe:/o:fedoraproject:fedora:26"
+        HOME_URL="https://fedoraproject.org/"
+        BUG_REPORT_URL="https://bugzilla.redhat.com/"
+        REDHAT_BUGZILLA_PRODUCT="Fedora"
+        REDHAT_BUGZILLA_PRODUCT_VERSION=26
+        REDHAT_SUPPORT_PRODUCT="Fedora"
+        REDHAT_SUPPORT_PRODUCT_VERSION=26
+        PRIVACY_POLICY_URL=https://fedoraproject.org/wiki/Legal:PrivacyPolicy
+    """)
+
+    str_opensuse_42_3_os_release = dedent("""
+        NAME="openSUSE Leap"
+        VERSION="42.3"
+        ID=opensuse
+        ID_LIKE="suse"
+        VERSION_ID="42.3"
+        PRETTY_NAME="openSUSE Leap 42.3"
+        ANSI_COLOR="0;32"
+        CPE_NAME="cpe:/o:opensuse:leap:42.3"
+        BUG_REPORT_URL="https://bugs.opensuse.org"
+        HOME_URL="https://www.opensuse.org/"
+    """)
+
+    str_opensuse_15_0_os_release = dedent("""
+        NAME="openSUSE Leap"
+        VERSION="15.0"
+        ID="opensuse-leap"
+        ID_LIKE="suse opensuse"
+        VERSION_ID="15.0"
+        PRETTY_NAME="openSUSE Leap 15.0"
+        ANSI_COLOR="0;32"
+        CPE_NAME="cpe:/o:opensuse:leap:15.0"
+        BUG_REPORT_URL="https://bugs.opensuse.org"
+        HOME_URL="https://www.opensuse.org/"
+    """)
+
+    str_opensuse_15_1_os_release = dedent("""
+        NAME="openSUSE Leap"
+        VERSION="15.1"
+        ID="opensuse-leap"
+        ID_LIKE="suse opensuse"
+        VERSION_ID="15.1"
+        PRETTY_NAME="openSUSE Leap 15.1"
+        ANSI_COLOR="0;32"
+        CPE_NAME="cpe:/o:opensuse:leap:15.1"
+        BUG_REPORT_URL="https://bugs.opensuse.org"
+        HOME_URL="https://www.opensuse.org/"
+    """)
+
+    def test_centos_9_os_release(self):
+        os = OS.from_os_release(self.str_centos_9_os_release)
+        assert os.name == 'centos'
+        assert os.version == '9.stream'
+        assert os.codename == 'stream'
+        assert os.package_type == 'rpm'
+
+    def test_centos_7_os_release(self):
+        os = OS.from_os_release(self.str_centos_7_os_release)
+        assert os.name == 'centos'
+        assert os.version == '7'
+        assert os.codename == 'core'
+        assert os.package_type == 'rpm'
+
+    def test_centos_7_os_release_newer(self):
+        os = OS.from_os_release(self.str_centos_7_os_release_newer)
+        assert os.name == 'centos'
+        assert os.version == '7'
+        assert os.codename == 'core'
+        assert os.package_type == 'rpm'
+
+    def test_debian_7_lsb_release(self):
+        os = OS.from_lsb_release(self.str_debian_7_lsb_release)
+        assert os.name == 'debian'
+        assert os.version == '7.1'
+        assert os.codename == 'wheezy'
+        assert os.package_type == 'deb'
+
+    def test_debian_7_os_release(self):
+        os = OS.from_os_release(self.str_debian_7_os_release)
+        assert os.name == 'debian'
+        assert os.version == '7'
+        assert os.codename == 'wheezy'
+        assert os.package_type == 'deb'
+
+    def test_debian_8_os_release(self):
+        os = OS.from_os_release(self.str_debian_8_os_release)
+        assert os.name == 'debian'
+        assert os.version == '8'
+        assert os.codename == 'jessie'
+        assert os.package_type == 'deb'
+
+    def test_debian_9_os_release(self):
+        os = OS.from_os_release(self.str_debian_9_os_release)
+        assert os.name == 'debian'
+        assert os.version == '9'
+        assert os.codename == 'stretch'
+        assert os.package_type == 'deb'
+
+    def test_ubuntu_12_04_lsb_release(self):
+        os = OS.from_lsb_release(self.str_ubuntu_12_04_lsb_release)
+        assert os.name == 'ubuntu'
+        assert os.version == '12.04'
+        assert os.codename == 'precise'
+        assert os.package_type == 'deb'
+
+    def test_ubuntu_12_04_os_release(self):
+        os = OS.from_os_release(self.str_ubuntu_12_04_os_release)
+        assert os.name == 'ubuntu'
+        assert os.version == '12.04'
+        assert os.codename == 'precise'
+        assert os.package_type == 'deb'
+
+    def test_ubuntu_14_04_os_release(self):
+        os = OS.from_os_release(self.str_ubuntu_14_04_os_release)
+        assert os.name == 'ubuntu'
+        assert os.version == '14.04'
+        assert os.codename == 'trusty'
+        assert os.package_type == 'deb'
+
+    def test_ubuntu_16_04_os_release(self):
+        os = OS.from_os_release(self.str_ubuntu_16_04_os_release)
+        assert os.name == 'ubuntu'
+        assert os.version == '16.04'
+        assert os.codename == 'xenial'
+        assert os.package_type == 'deb'
+
+    def test_ubuntu_18_04_os_release(self):
+        os = OS.from_os_release(self.str_ubuntu_18_04_os_release)
+        assert os.name == 'ubuntu'
+        assert os.version == '18.04'
+        assert os.codename == 'bionic'
+        assert os.package_type == 'deb'
+
+    def test_rhel_6_4_lsb_release(self):
+        os = OS.from_lsb_release(self.str_rhel_6_4_lsb_release)
+        assert os.name == 'rhel'
+        assert os.version == '6.4'
+        assert os.codename == 'santiago'
+        assert os.package_type == 'rpm'
+
+    def test_rhel_7_lsb_release(self):
+        os = OS.from_lsb_release(self.str_rhel_7_lsb_release)
+        assert os.name == 'rhel'
+        assert os.version == '7.0'
+        assert os.codename == 'maipo'
+        assert os.package_type == 'rpm'
+
+    def test_rhel_7_os_release(self):
+        os = OS.from_os_release(self.str_rhel_7_os_release)
+        assert os.name == 'rhel'
+        assert os.version == '7.0'
+        assert os.codename == 'maipo'
+        assert os.package_type == 'rpm'
+
+    def test_fedora_26_os_release(self):
+        os = OS.from_os_release(self.str_fedora_26_os_release)
+        assert os.name == 'fedora'
+        assert os.version == '26'
+        assert os.codename == '26'
+        assert os.package_type == 'rpm'
+
+    def test_opensuse_42_3_os_release(self):
+        os = OS.from_os_release(self.str_opensuse_42_3_os_release)
+        assert os.name == 'opensuse'
+        assert os.version == '42.3'
+        assert os.codename == 'leap'
+        assert os.package_type == 'rpm'
+
+    def test_opensuse_15_0_os_release(self):
+        os = OS.from_os_release(self.str_opensuse_15_0_os_release)
+        assert os.name == 'opensuse'
+        assert os.version == '15.0'
+        assert os.codename == 'leap'
+        assert os.package_type == 'rpm'
+
+    def test_opensuse_15_1_os_release(self):
+        os = OS.from_os_release(self.str_opensuse_15_1_os_release)
+        assert os.name == 'opensuse'
+        assert os.version == '15.1'
+        assert os.codename == 'leap'
+        assert os.package_type == 'rpm'
+
+    def test_version_codename_success(self):
+        assert OS.version_codename('ubuntu', '14.04') == ('14.04', 'trusty')
+        assert OS.version_codename('ubuntu', 'trusty') == ('14.04', 'trusty')
+
+    def test_version_codename_failure(self):
+        with pytest.raises(KeyError) as excinfo:
+            OS.version_codename('ubuntu', 'frog')
+        assert excinfo.type == KeyError
+        assert 'frog' in excinfo.value.args[0]
+
+    def test_repr(self):
+        os = OS(name='NAME', version='0.1.2', codename='code')
+        assert repr(os) == "OS(name='NAME', version='0.1.2', codename='code')"
+
+    def test_to_dict(self):
+        os = OS(name='NAME', version='0.1.2', codename='code')
+        ref_dict = dict(name='NAME', version='0.1.2', codename='code')
+        assert os.to_dict() == ref_dict
+
+    def test_version_no_codename(self):
+        os = OS(name='ubuntu', version='16.04')
+        assert os.codename == 'xenial'
+
+    def test_codename_no_version(self):
+        os = OS(name='ubuntu', codename='trusty')
+        assert os.version == '14.04'
+
+    def test_eq_equal(self):
+        os = OS(name='ubuntu', codename='trusty', version='14.04')
+        assert OS(name='ubuntu', codename='trusty', version='14.04') == os
+
+    def test_eq_not_equal(self):
+        os = OS(name='ubuntu', codename='trusty', version='16.04')
+        assert OS(name='ubuntu', codename='trusty', version='14.04') != os
diff --git a/tests/orchestra/test_remote.py b/tests/orchestra/test_remote.py
new file mode 100644 (file)
index 0000000..1328bc8
--- /dev/null
@@ -0,0 +1,259 @@
+from mock import patch, Mock, MagicMock
+from pytest import raises
+
+from io import BytesIO
+
+from teuthology.orchestra import remote
+from teuthology.orchestra import opsys
+from teuthology.orchestra.run import RemoteProcess
+from teuthology.exceptions import CommandFailedError, UnitTestError
+
+
+class TestRemote(object):
+
+    def setup_method(self):
+        self.start_patchers()
+
+    def teardown_method(self):
+        self.stop_patchers()
+
+    def start_patchers(self):
+        self.m_ssh = MagicMock()
+        self.patcher_ssh = patch(
+            'teuthology.orchestra.connection.paramiko.SSHClient',
+            self.m_ssh,
+        )
+        self.patcher_ssh.start()
+
+    def stop_patchers(self):
+        self.patcher_ssh.stop()
+
+    def test_shortname(self):
+        r = remote.Remote(
+            name='jdoe@xyzzy.example.com',
+            shortname='xyz',
+            ssh=self.m_ssh,
+            )
+        assert r.shortname == 'xyz'
+        assert str(r) == 'jdoe@xyzzy.example.com'
+
+    def test_shortname_default(self):
+        r = remote.Remote(
+            name='jdoe@xyzzy.example.com',
+            ssh=self.m_ssh,
+            )
+        assert r.shortname == 'xyzzy'
+        assert str(r) == 'jdoe@xyzzy.example.com'
+
+    def test_run(self):
+        m_transport = MagicMock()
+        m_transport.getpeername.return_value = ('name', 22)
+        self.m_ssh.get_transport.return_value = m_transport
+        m_run = MagicMock()
+        args = [
+            'something',
+            'more',
+            ]
+        proc = RemoteProcess(
+            client=self.m_ssh,
+            args=args,
+            )
+        m_run.return_value = proc
+        rem = remote.Remote(name='jdoe@xyzzy.example.com', ssh=self.m_ssh)
+        rem._runner = m_run
+        result = rem.run(args=args)
+        m_transport.getpeername.assert_called_once_with()
+        m_run_call_kwargs = m_run.call_args_list[0][1]
+        assert m_run_call_kwargs['args'] == args
+        assert result is proc
+        assert result.remote is rem
+
+    @patch('teuthology.util.scanner.UnitTestScanner.scan_and_write')
+    def test_run_unit_test(self, m_scan_and_write):
+        m_transport = MagicMock()
+        m_transport.getpeername.return_value = ('name', 22)
+        self.m_ssh.get_transport.return_value = m_transport
+        m_run = MagicMock(name="run", side_effect=CommandFailedError('mocked error', 1, 'smithi'))
+        args = [
+            'something',
+            'more',
+        ]
+        rem = remote.Remote(name='jdoe@xyzzy.example.com', ssh=self.m_ssh)
+        rem._runner = m_run
+        m_scan_and_write.return_value = "Error Message"
+        with raises(UnitTestError) as exc:
+            rem.run_unit_test(args=args, xml_path_regex="xml_path", output_yaml="yaml_path")
+        assert str(exc.value) == "Unit test failed on smithi with status 1: 'Error Message'"
+
+    def test_hostname(self):
+        m_transport = MagicMock()
+        m_transport.getpeername.return_value = ('name', 22)
+        self.m_ssh.get_transport.return_value = m_transport
+        m_run = MagicMock()
+        args = [
+            'hostname',
+            '--fqdn',
+            ]
+        stdout = BytesIO(b'test_hostname')
+        stdout.seek(0)
+        proc = RemoteProcess(
+            client=self.m_ssh,
+            args=args,
+            )
+        proc.stdout = stdout
+        proc._stdout_buf = Mock()
+        proc._stdout_buf.channel.recv_exit_status.return_value = 0
+        r = remote.Remote(name='xyzzy.example.com', ssh=self.m_ssh)
+        m_run.return_value = proc
+        r._runner = m_run
+        assert r.hostname == 'test_hostname'
+
+    def test_arch(self):
+        m_transport = MagicMock()
+        m_transport.getpeername.return_value = ('name', 22)
+        self.m_ssh.get_transport.return_value = m_transport
+        m_run = MagicMock()
+        args = [
+            'uname',
+            '-m',
+            ]
+        stdout = BytesIO(b'test_arch')
+        stdout.seek(0)
+        proc = RemoteProcess(
+            client=self.m_ssh,
+            args=args,
+            )
+        proc._stdout_buf = Mock()
+        proc._stdout_buf.channel = Mock()
+        proc._stdout_buf.channel.recv_exit_status.return_value = 0
+        proc._stdout_buf.channel.expects('recv_exit_status').returns(0)
+        proc.stdout = stdout
+        m_run.return_value = proc
+        r = remote.Remote(name='jdoe@xyzzy.example.com', ssh=self.m_ssh)
+        r._runner = m_run
+        assert r.arch == 'test_arch'
+        assert len(m_run.call_args_list) == 1
+        m_run_call_kwargs = m_run.call_args_list[0][1]
+        assert m_run_call_kwargs['client'] == self.m_ssh
+        assert m_run_call_kwargs['name'] == r.shortname
+        assert m_run_call_kwargs['args'] == ' '.join(args)
+
+    def test_host_key(self):
+        m_key = MagicMock()
+        m_key.get_name.return_value = 'key_type'
+        m_key.get_base64.return_value = 'test ssh key'
+        m_transport = MagicMock()
+        m_transport.get_remote_server_key.return_value = m_key
+        self.m_ssh.get_transport.return_value = m_transport
+        r = remote.Remote(name='jdoe@xyzzy.example.com', ssh=self.m_ssh)
+        assert r.host_key == 'key_type test ssh key'
+        self.m_ssh.get_transport.assert_called_once_with()
+        m_transport.get_remote_server_key.assert_called_once_with()
+
+    def test_inventory_info(self):
+        r = remote.Remote('user@host', host_key='host_key')
+        r._arch = 'arch'
+        r._os = opsys.OS(name='os_name', version='1.2.3', codename='code')
+        inv_info = r.inventory_info
+        assert inv_info == dict(
+            name='host',
+            user='user',
+            arch='arch',
+            os_type='os_name',
+            os_version='1.2',
+            ssh_pub_key='host_key',
+            up=True,
+        )
+
+    def test_sftp_open_file(self):
+        m_file_obj = MagicMock()
+        m_stat = Mock()
+        m_stat.st_size = 42
+        m_file_obj.stat.return_value = m_stat
+        m_open = MagicMock()
+        m_open.return_value = m_file_obj
+        m_open.return_value.__enter__.return_value = m_file_obj
+        with patch.object(remote.Remote, '_sftp_open_file', new=m_open):
+            rem = remote.Remote(name='jdoe@xyzzy.example.com', ssh=self.m_ssh)
+            assert rem._sftp_open_file('x') is m_file_obj
+            assert rem._sftp_open_file('x').stat() is m_stat
+            assert rem._sftp_open_file('x').stat().st_size == 42
+            with rem._sftp_open_file('x') as f:
+                assert f == m_file_obj
+
+    def test_sftp_get_size(self):
+        m_file_obj = MagicMock()
+        m_stat = Mock()
+        m_stat.st_size = 42
+        m_file_obj.stat.return_value = m_stat
+        m_open = MagicMock()
+        m_open.return_value = m_file_obj
+        m_open.return_value.__enter__.return_value = m_file_obj
+        with patch.object(remote.Remote, '_sftp_open_file', new=m_open):
+            rem = remote.Remote(name='jdoe@xyzzy.example.com', ssh=self.m_ssh)
+            assert rem._sftp_get_size('/fake/file') == 42
+
+    def test_format_size(self):
+        assert remote.Remote._format_size(1023).strip() == '1023B'
+        assert remote.Remote._format_size(1024).strip() == '1KB'
+        assert remote.Remote._format_size(1024**2).strip() == '1MB'
+        assert remote.Remote._format_size(1024**5).strip() == '1TB'
+        assert remote.Remote._format_size(1021112).strip() == '997KB'
+        assert remote.Remote._format_size(1021112**2).strip() == '971GB'
+
+    def test_is_container(self):
+        m_transport = MagicMock()
+        m_transport.getpeername.return_value = ('name', 22)
+        self.m_ssh.get_transport.return_value = m_transport
+        m_run = MagicMock()
+        args = []
+        proc = RemoteProcess(
+            client=self.m_ssh,
+            args=args,
+        )
+        proc.returncode = 0
+        m_run.return_value = proc
+        rem = remote.Remote(name='jdoe@xyzzy.example.com', ssh=self.m_ssh)
+        rem._runner = m_run
+        assert rem.is_container
+        proc.returncode = 1
+        rem2 = remote.Remote(name='jdoe@xyzzy.example.com', ssh=self.m_ssh)
+        rem2._runner = m_run
+        assert not rem2.is_container
+
+    @patch("teuthology.orchestra.remote.Remote.sh")
+    def test_resolve_ip(self, m_sh):
+        r = remote.Remote(name="jdoe@xyzzy.example.com", ssh=self.m_ssh)
+        m_sh.return_value = "smithi001.front.sepia.ceph.com has address 172.21.15.1\n"
+        ip4 = remote.Remote.resolve_ip(r, 'smithi001')
+        assert ip4 == "172.21.15.1"
+        m_sh.return_value = "\n"
+        try:
+            ip4 = remote.Remote.resolve_ip(r, 'smithi001')
+        except Exception as e:
+            assert 'Cannot get IPv4 address' in str(e)
+        try:
+            ip4 = remote.Remote.resolve_ip(r, 'smithi001', 5)
+        except Exception as e:
+            assert 'Unknown IP version' in str(e)
+        
+        m_sh.return_value = ("google.com has address 142.251.37.14\n"
+                             "google.com has IPv6 address 2a00:1450:4016:80b::200e\n"
+                             "google.com mail is handled by 10 smtp.google.com.\n")
+        ip4 = remote.Remote.resolve_ip(r, 'google.com')
+        assert ip4 == "142.251.37.14"
+        ip6 = remote.Remote.resolve_ip(r, 'google.com', '6')
+        assert ip6 == "2a00:1450:4016:80b::200e"
+
+
+    @patch("teuthology.orchestra.remote.Remote.run")
+    def test_write_file(self, m_run):
+        file = "fakefile"
+        contents = "fakecontents"
+        rem = remote.Remote(name='jdoe@xyzzy.example.com', ssh=self.m_ssh)
+
+        remote.Remote.write_file(rem, file, contents, bs=1, offset=1024)
+        m_run.assert_called_with(args=f"set -ex\ndd of={file} bs=1 seek=1024", stdin=contents, quiet=True)
+
+        remote.Remote.write_file(rem, file, contents, sync=True)
+        m_run.assert_called_with(args=f"set -ex\ndd of={file} conv=sync", stdin=contents, quiet=True)
diff --git a/tests/orchestra/test_run.py b/tests/orchestra/test_run.py
new file mode 100644 (file)
index 0000000..e8051cc
--- /dev/null
@@ -0,0 +1,286 @@
+from io import BytesIO
+
+import paramiko
+import socket
+
+from mock import MagicMock, patch
+from pytest import raises
+
+from teuthology.orchestra import run
+from teuthology.exceptions import (CommandCrashedError, CommandFailedError,
+                                   ConnectionLostError)
+
+def set_buffer_contents(buf, contents):
+    buf.seek(0)
+    if isinstance(contents, bytes):
+        buf.write(contents)
+    elif isinstance(contents, (list, tuple)):
+        buf.writelines(contents)
+    elif isinstance(contents, str):
+        buf.write(contents.encode())
+    else:
+        raise TypeError(
+            "%s is a %s; should be a byte string, list or tuple" % (
+                contents, type(contents)
+            )
+        )
+    buf.seek(0)
+
+
+class TestRun(object):
+    def setup_method(self):
+        self.start_patchers()
+
+    def teardown_method(self):
+        self.stop_patchers()
+
+    def start_patchers(self):
+        self.m_remote_process = MagicMock(wraps=run.RemoteProcess)
+        self.patcher_remote_proc = patch(
+            'teuthology.orchestra.run.RemoteProcess',
+            self.m_remote_process,
+        )
+        self.m_channel = MagicMock(spec=paramiko.Channel)()
+        """
+        self.m_channelfile = MagicMock(wraps=paramiko.ChannelFile)
+        self.m_stdin_buf = self.m_channelfile(self.m_channel())
+        self.m_stdout_buf = self.m_channelfile(self.m_channel())
+        self.m_stderr_buf = self.m_channelfile(self.m_channel())
+        """
+        class M_ChannelFile(BytesIO):
+            channel = MagicMock(spec=paramiko.Channel)()
+
+        self.m_channelfile = M_ChannelFile
+        self.m_stdin_buf = self.m_channelfile()
+        self.m_stdout_buf = self.m_channelfile()
+        self.m_stderr_buf = self.m_channelfile()
+        self.m_ssh = MagicMock()
+        self.m_ssh.exec_command.return_value = (
+            self.m_stdin_buf,
+            self.m_stdout_buf,
+            self.m_stderr_buf,
+        )
+        self.m_transport = MagicMock()
+        self.m_transport.getpeername.return_value = ('name', 22)
+        self.m_ssh.get_transport.return_value = self.m_transport
+        self.patcher_ssh = patch(
+            'teuthology.orchestra.connection.paramiko.SSHClient',
+            self.m_ssh,
+        )
+        self.patcher_ssh.start()
+        # Tests must start this if they wish to use it
+        # self.patcher_remote_proc.start()
+
+    def stop_patchers(self):
+        # If this patcher wasn't started, it's ok
+        try:
+            self.patcher_remote_proc.stop()
+        except RuntimeError:
+            pass
+        self.patcher_ssh.stop()
+
+    def test_exitstatus(self):
+        self.m_stdout_buf.channel.recv_exit_status.return_value = 0
+        proc = run.run(
+            client=self.m_ssh,
+            args=['foo', 'bar baz'],
+        )
+        assert proc.exitstatus == 0
+
+    def test_run_cwd(self):
+        self.m_stdout_buf.channel.recv_exit_status.return_value = 0
+        run.run(
+            client=self.m_ssh,
+            args=['foo_bar_baz'],
+            cwd='/cwd/test',
+        )
+        self.m_ssh.exec_command.assert_called_with('(cd /cwd/test && exec foo_bar_baz)')
+
+    def test_capture_stdout(self):
+        output = 'foo\nbar'
+        set_buffer_contents(self.m_stdout_buf, output)
+        self.m_stdout_buf.channel.recv_exit_status.return_value = 0
+        stdout = BytesIO()
+        proc = run.run(
+            client=self.m_ssh,
+            args=['foo', 'bar baz'],
+            stdout=stdout,
+        )
+        assert proc.stdout is stdout
+        assert proc.stdout.read().decode() == output
+        assert proc.stdout.getvalue().decode() == output
+
+    def test_capture_stderr_newline(self):
+        output = 'foo\nbar\n'
+        set_buffer_contents(self.m_stderr_buf, output)
+        self.m_stderr_buf.channel.recv_exit_status.return_value = 0
+        stderr = BytesIO()
+        proc = run.run(
+            client=self.m_ssh,
+            args=['foo', 'bar baz'],
+            stderr=stderr,
+        )
+        assert proc.stderr is stderr
+        assert proc.stderr.read().decode() == output
+        assert proc.stderr.getvalue().decode() == output
+
+    def test_status_bad(self):
+        self.m_stdout_buf.channel.recv_exit_status.return_value = 42
+        with raises(CommandFailedError) as exc:
+            run.run(
+                client=self.m_ssh,
+                args=['foo'],
+            )
+        assert str(exc.value) == "Command failed on name with status 42: 'foo'"
+
+    def test_status_bad_nocheck(self):
+        self.m_stdout_buf.channel.recv_exit_status.return_value = 42
+        proc = run.run(
+            client=self.m_ssh,
+            args=['foo'],
+            check_status=False,
+        )
+        assert proc.exitstatus == 42
+
+    def test_status_crash(self):
+        self.m_stdout_buf.channel.recv_exit_status.return_value = -1
+        with raises(CommandCrashedError) as exc:
+            run.run(
+                client=self.m_ssh,
+                args=['foo'],
+            )
+        assert str(exc.value) == "Command crashed: 'foo'"
+
+    def test_status_crash_nocheck(self):
+        self.m_stdout_buf.channel.recv_exit_status.return_value = -1
+        proc = run.run(
+            client=self.m_ssh,
+            args=['foo'],
+            check_status=False,
+        )
+        assert proc.exitstatus == -1
+
+    def test_status_lost(self):
+        m_transport = MagicMock()
+        m_transport.getpeername.return_value = ('name', 22)
+        m_transport.is_active.return_value = False
+        self.m_stdout_buf.channel.recv_exit_status.return_value = -1
+        self.m_ssh.get_transport.return_value = m_transport
+        with raises(ConnectionLostError) as exc:
+            run.run(
+                client=self.m_ssh,
+                args=['foo'],
+            )
+        assert str(exc.value) == "SSH connection to name was lost: 'foo'"
+
+    def test_status_lost_socket(self):
+        m_transport = MagicMock()
+        m_transport.getpeername.side_effect = socket.error
+        self.m_ssh.get_transport.return_value = m_transport
+        with raises(ConnectionLostError) as exc:
+            run.run(
+                client=self.m_ssh,
+                args=['foo'],
+            )
+        assert str(exc.value) == "SSH connection was lost: 'foo'"
+
+    def test_status_lost_nocheck(self):
+        m_transport = MagicMock()
+        m_transport.getpeername.return_value = ('name', 22)
+        m_transport.is_active.return_value = False
+        self.m_stdout_buf.channel.recv_exit_status.return_value = -1
+        self.m_ssh.get_transport.return_value = m_transport
+        proc = run.run(
+            client=self.m_ssh,
+            args=['foo'],
+            check_status=False,
+        )
+        assert proc.exitstatus == -1
+
+    def test_status_bad_nowait(self):
+        self.m_stdout_buf.channel.recv_exit_status.return_value = 42
+        proc = run.run(
+            client=self.m_ssh,
+            args=['foo'],
+            wait=False,
+        )
+        with raises(CommandFailedError) as exc:
+            proc.wait()
+        assert proc.returncode == 42
+        assert str(exc.value) == "Command failed on name with status 42: 'foo'"
+
+    def test_stdin_pipe(self):
+        self.m_stdout_buf.channel.recv_exit_status.return_value = 0
+        proc = run.run(
+            client=self.m_ssh,
+            args=['foo'],
+            stdin=run.PIPE,
+            wait=False
+        )
+        assert proc.poll() == 0
+        code = proc.wait()
+        assert code == 0
+        assert proc.exitstatus == 0
+
+    def test_stdout_pipe(self):
+        self.m_stdout_buf.channel.recv_exit_status.return_value = 0
+        lines = [b'one\n', b'two', b'']
+        set_buffer_contents(self.m_stdout_buf, lines)
+        proc = run.run(
+            client=self.m_ssh,
+            args=['foo'],
+            stdout=run.PIPE,
+            wait=False
+        )
+        assert proc.poll() == 0
+        assert proc.stdout.readline() == lines[0]
+        assert proc.stdout.readline() == lines[1]
+        assert proc.stdout.readline() == lines[2]
+        code = proc.wait()
+        assert code == 0
+        assert proc.exitstatus == 0
+
+    def test_stderr_pipe(self):
+        self.m_stdout_buf.channel.recv_exit_status.return_value = 0
+        lines = [b'one\n', b'two', b'']
+        set_buffer_contents(self.m_stderr_buf, lines)
+        proc = run.run(
+            client=self.m_ssh,
+            args=['foo'],
+            stderr=run.PIPE,
+            wait=False
+        )
+        assert proc.poll() == 0
+        assert proc.stderr.readline() == lines[0]
+        assert proc.stderr.readline() == lines[1]
+        assert proc.stderr.readline() == lines[2]
+        code = proc.wait()
+        assert code == 0
+        assert proc.exitstatus == 0
+
+    def test_copy_and_close(self):
+        run.copy_and_close(None, MagicMock())
+        run.copy_and_close('', MagicMock())
+        run.copy_and_close(b'', MagicMock())
+
+
+class TestQuote(object):
+    def test_quote_simple(self):
+        got = run.quote(['a b', ' c', 'd e '])
+        assert got == "'a b' ' c' 'd e '"
+
+    def test_quote_and_quote(self):
+        got = run.quote(['echo', 'this && is embedded', '&&',
+                         'that was standalone'])
+        assert got == "echo 'this && is embedded' '&&' 'that was standalone'"
+
+    def test_quote_and_raw(self):
+        got = run.quote(['true', run.Raw('&&'), 'echo', 'yay'])
+        assert got == "true && echo yay"
+
+
+class TestRaw(object):
+    def test_eq(self):
+        str_ = "I am a raw something or other"
+        raw = run.Raw(str_)
+        assert raw == run.Raw(str_)
diff --git a/tests/orchestra/test_systemd.py b/tests/orchestra/test_systemd.py
new file mode 100644 (file)
index 0000000..c7cb342
--- /dev/null
@@ -0,0 +1,54 @@
+import argparse
+import os
+
+from logging import debug
+from teuthology import misc
+from teuthology.orchestra import cluster
+from teuthology.orchestra.run import quote
+from teuthology.orchestra.daemon.group import DaemonGroup
+import subprocess
+
+
+class FakeRemote(object):
+    pass
+
+
+def test_pid():
+    ctx = argparse.Namespace()
+    ctx.daemons = DaemonGroup(use_systemd=True)
+    remote = FakeRemote()
+
+    ps_ef_output_path = os.path.join(
+        os.path.dirname(__file__),
+        "files/daemon-systemdstate-pid-ps-ef.output"
+    )
+
+    # patching ps -ef command output using a file
+    def sh(args):
+        args[0:2] = ["cat", ps_ef_output_path]
+        debug(args)
+        return subprocess.getoutput(quote(args))
+
+    remote.sh = sh
+    remote.init_system = 'systemd'
+    remote.shortname = 'host1'
+
+    ctx.cluster = cluster.Cluster(
+        remotes=[
+            (remote, ['rgw.0', 'mon.a', 'mgr.a', 'mds.a', 'osd.0'])
+        ],
+    )
+
+    for remote, roles in ctx.cluster.remotes.items():
+        for role in roles:
+            _, rol, id_ = misc.split_role(role)
+            if any(rol.startswith(x) for x in ['mon', 'mgr', 'mds']):
+                ctx.daemons.register_daemon(remote, rol, remote.shortname)
+            else:
+                ctx.daemons.register_daemon(remote, rol, id_)
+
+    for _, daemons in ctx.daemons.daemons.items():
+        for daemon in daemons.values():
+            pid = daemon.pid
+            debug(pid)
+            assert pid
diff --git a/tests/orchestra/util.py b/tests/orchestra/util.py
new file mode 100644 (file)
index 0000000..4aedc2e
--- /dev/null
@@ -0,0 +1,12 @@
+def assert_raises(excClass, callableObj, *args, **kwargs):
+    """
+    Like unittest.TestCase.assertRaises, but returns the exception.
+    """
+    try:
+        callableObj(*args, **kwargs)
+    except excClass as e:
+        return e
+    else:
+        if hasattr(excClass,'__name__'): excName = excClass.__name__
+        else: excName = str(excClass)
+        raise AssertionError("%s not raised" % excName)
diff --git a/tests/provision/cloud/test_base.py b/tests/provision/cloud/test_base.py
new file mode 100644 (file)
index 0000000..67e8380
--- /dev/null
@@ -0,0 +1,90 @@
+from libcloud.compute.providers import get_driver
+from mock import patch
+
+from teuthology.config import config
+from teuthology.provision import cloud
+
+from test_cloud_init import dummy_config, dummy_drivers
+
+
+class TestBase(object):
+    def setup_method(self):
+        config.load()
+        config.libcloud = dummy_config
+        cloud.supported_drivers['dummy'] = dummy_drivers
+
+    def teardown_method(self):
+        del cloud.supported_drivers['dummy']
+
+
+class TestProvider(TestBase):
+    def test_init(self):
+        obj = cloud.get_provider('my_provider')
+        assert obj.name == 'my_provider'
+        assert obj.driver_name == 'dummy'
+        assert obj.conf == dummy_config['providers']['my_provider']
+
+    def test_driver(self):
+        obj = cloud.get_provider('my_provider')
+        assert isinstance(obj.driver, get_driver('dummy'))
+
+
+class TestProvisioner(TestBase):
+    klass = cloud.base.Provisioner
+
+    def get_obj(
+            self, name='node_name', os_type='ubuntu', os_version='ubuntu'):
+        return cloud.get_provisioner(
+            'my_provider',
+            'node_name',
+            'ubuntu',
+            '16.04',
+        )
+
+    def test_init_provider_string(self):
+        obj = self.klass('my_provider', 'ubuntu', '16.04')
+        assert obj.provider.name == 'my_provider'
+
+    def test_create(self):
+        obj = self.get_obj()
+        with patch.object(
+            self.klass,
+            '_create',
+        ) as m_create:
+            for val in [True, False]:
+                m_create.return_value = val
+                res = obj.create()
+                assert res is val
+                m_create.assert_called_once_with()
+                m_create.reset_mock()
+            m_create.side_effect = RuntimeError
+            res = obj.create()
+            assert res is False
+        assert obj.create() is None
+
+    def test_destroy(self):
+        obj = self.get_obj()
+        with patch.object(
+            self.klass,
+            '_destroy',
+        ) as m_destroy:
+            for val in [True, False]:
+                m_destroy.return_value = val
+                res = obj.destroy()
+                assert res is val
+                m_destroy.assert_called_once_with()
+                m_destroy.reset_mock()
+            m_destroy.side_effect = RuntimeError
+            res = obj.destroy()
+            assert res is False
+        assert obj.destroy() is None
+
+    def test_remote(self):
+        obj = self.get_obj()
+        assert obj.remote.shortname == 'node_name'
+
+    def test_repr(self):
+        obj = self.get_obj()
+        assert repr(obj) == \
+            "Provisioner(provider='my_provider', name='node_name', os_type='ubuntu', os_version='16.04')"  # noqa
+
diff --git a/tests/provision/cloud/test_cloud_init.py b/tests/provision/cloud/test_cloud_init.py
new file mode 100644 (file)
index 0000000..1b0df96
--- /dev/null
@@ -0,0 +1,60 @@
+from teuthology.config import config
+from teuthology.provision import cloud
+
+dummy_config = dict(
+    providers=dict(
+        my_provider=dict(
+            driver='dummy',
+            driver_args=dict(
+                creds=0,
+            ),
+            conf_1='1',
+            conf_2='2',
+        )
+    )
+)
+
+
+class DummyProvider(cloud.base.Provider):
+        # For libcloud's dummy driver
+        _driver_posargs = ['creds']
+
+dummy_drivers = dict(
+    provider=DummyProvider,
+    provisioner=cloud.base.Provisioner,
+)
+
+
+class TestInit(object):
+    def setup_method(self):
+        config.load()
+        config.libcloud = dummy_config
+        cloud.supported_drivers['dummy'] = dummy_drivers
+
+    def teardown_method(self):
+        del cloud.supported_drivers['dummy']
+
+    def test_get_types(self):
+        assert list(cloud.get_types()) == ['my_provider']
+
+    def test_get_provider_conf(self):
+        expected = dummy_config['providers']['my_provider']
+        assert cloud.get_provider_conf('my_provider') == expected
+
+    def test_get_provider(self):
+        obj = cloud.get_provider('my_provider')
+        assert obj.name == 'my_provider'
+        assert obj.driver_name == 'dummy'
+
+    def test_get_provisioner(self):
+        obj = cloud.get_provisioner(
+            'my_provider',
+            'node_name',
+            'ubuntu',
+            '16.04',
+            dict(foo='bar'),
+        )
+        assert obj.provider.name == 'my_provider'
+        assert obj.name == 'node_name'
+        assert obj.os_type == 'ubuntu'
+        assert obj.os_version == '16.04'
diff --git a/tests/provision/cloud/test_cloud_util.py b/tests/provision/cloud/test_cloud_util.py
new file mode 100644 (file)
index 0000000..3093d2c
--- /dev/null
@@ -0,0 +1,172 @@
+import datetime
+import dateutil
+import json
+
+from mock import patch, mock_open
+from pytest import mark
+
+from teuthology.provision.cloud import util
+
+
+@mark.parametrize(
+    'path, exists',
+    [
+        ('/fake/path', True),
+        ('/fake/path', False),
+    ]
+)
+def test_get_user_ssh_pubkey(path, exists):
+    with patch('os.path.exists') as m_exists:
+        m_exists.return_value = exists
+        with patch('teuthology.provision.cloud.util.open', mock_open(), create=True) as m_open:
+            util.get_user_ssh_pubkey(path)
+            if exists:
+                m_open.assert_called_once_with(path)
+
+
+@mark.parametrize(
+    'input_, func, expected',
+    [
+        [
+            [
+                dict(sub0=dict(key0=0, key1=0)),
+                dict(sub0=dict(key1=1, key2=2)),
+            ],
+            lambda x, y: x > y,
+            dict(sub0=dict(key0=0, key1=1, key2=2))
+        ],
+        [
+            [
+                dict(),
+                dict(sub0=dict(key1=1, key2=2)),
+            ],
+            lambda x, y: x > y,
+            dict(sub0=dict(key1=1, key2=2))
+        ],
+        [
+            [
+                dict(sub0=dict(key1=1, key2=2)),
+                dict(),
+            ],
+            lambda x, y: x > y,
+            dict(sub0=dict(key1=1, key2=2))
+        ],
+        [
+            [
+                dict(sub0=dict(key0=0, key1=0, key2=0)),
+                dict(sub0=dict(key0=1, key2=3), sub1=dict(key0=0)),
+                dict(sub0=dict(key0=3, key1=2, key2=1)),
+                dict(sub0=dict(key1=3),
+                     sub1=dict(key0=3, key1=0)),
+            ],
+            lambda x, y: x > y,
+            dict(sub0=dict(key0=3, key1=3, key2=3),
+                 sub1=dict(key0=3, key1=0))
+        ],
+    ]
+)
+def test_combine_dicts(input_, func, expected):
+    assert util.combine_dicts(input_, func) == expected
+
+
+def get_datetime(offset_hours=0):
+    delta = datetime.timedelta(hours=offset_hours)
+    return datetime.datetime.now(dateutil.tz.tzutc()) + delta
+
+
+def get_datetime_string(offset_hours=0):
+    obj = get_datetime(offset_hours)
+    return obj.strftime(util.AuthToken.time_format)
+
+
+class TestAuthToken(object):
+    klass = util.AuthToken
+
+    def setup_method(self):
+        default_expires = get_datetime_string(0)
+        self.test_data = dict(
+            value='token_value',
+            endpoint='endpoint',
+            expires=default_expires,
+        )
+        self.patchers = dict()
+        self.patchers['m_open'] = patch(
+            'teuthology.provision.cloud.util.open'
+        )
+        self.patchers['m_exists'] = patch(
+            'os.path.exists'
+        )
+        self.patchers['m_file_lock'] = patch(
+            'teuthology.provision.cloud.util.FileLock'
+        )
+        self.mocks = dict()
+        for name, patcher in self.patchers.items():
+            self.mocks[name] = patcher.start()
+
+    def teardown_method(self):
+        for patcher in self.patchers.values():
+            patcher.stop()
+
+    def get_obj(self, name='name', directory='/fake/directory'):
+        return self.klass(
+            name=name,
+            directory=directory,
+        )
+
+    def test_no_token(self):
+        obj = self.get_obj()
+        self.mocks['m_exists'].return_value = False
+        with obj:
+            assert obj.value is None
+            assert obj.expired is True
+
+    @mark.parametrize(
+        'test_data, expired',
+        [
+            [
+                dict(
+                    value='token_value',
+                    endpoint='endpoint',
+                    expires=get_datetime_string(-1),
+                ),
+                True
+            ],
+            [
+                dict(
+                    value='token_value',
+                    endpoint='endpoint',
+                    expires=get_datetime_string(1),
+                ),
+                False
+            ],
+        ]
+    )
+    def test_token_read(self, test_data, expired):
+        obj = self.get_obj()
+        self.mocks['m_exists'].return_value = True
+        self.mocks['m_open'].return_value.__enter__.return_value.read.return_value = \
+            json.dumps(test_data)
+        with obj:
+            if expired:
+                assert obj.value is None
+                assert obj.expired is True
+            else:
+                assert obj.value == test_data['value']
+
+    def test_token_write(self):
+        obj = self.get_obj()
+        datetime_obj = get_datetime(0)
+        datetime_string = get_datetime_string(0)
+        self.mocks['m_exists'].return_value = False
+        with obj:
+            obj.write('value', datetime_obj, 'endpoint')
+        m_open = self.mocks['m_open']
+        write_calls = m_open.return_value.__enter__.return_value.write\
+            .call_args_list
+        assert len(write_calls) == 1
+        expected = json.dumps(dict(
+            value='value',
+            expires=datetime_string,
+            endpoint='endpoint',
+        ))
+        assert write_calls[0][0][0] == expected
diff --git a/tests/provision/cloud/test_openstack.py b/tests/provision/cloud/test_openstack.py
new file mode 100644 (file)
index 0000000..c152105
--- /dev/null
@@ -0,0 +1,781 @@
+import socket
+import yaml
+import os
+
+from teuthology.util.compat import parse_qs
+
+from copy import deepcopy
+from libcloud.compute.providers import get_driver
+from mock import patch, Mock, DEFAULT
+from pytest import raises, mark
+
+from teuthology.config import config
+from teuthology.exceptions import MaxWhileTries
+from teuthology.provision import cloud
+
+test_config = dict(
+    providers=dict(
+        my_provider=dict(
+            driver='openstack',
+            driver_args=dict(
+                username='user',
+                password='password',
+                ex_force_auth_url='http://127.0.0.1:9999/v2.0/tokens',
+            ),
+        ),
+        image_exclude_provider=dict(
+            driver='openstack',
+            exclude_image=['.*-exclude1', '.*-exclude2'],
+            driver_args=dict(
+                username='user',
+                password='password',
+                ex_force_auth_url='http://127.0.0.1:9999/v2.0/tokens',
+            ),
+        )
+    )
+)
+
+
+@patch('time.sleep')
+def test_retry(m_sleep):
+    orig_exceptions = cloud.openstack.RETRY_EXCEPTIONS
+    new_exceptions = orig_exceptions + (RuntimeError, )
+
+    class test_cls(object):
+        def __init__(self, min_val):
+            self.min_val = min_val
+            self.cur_val = 0
+
+        def func(self):
+            self.cur_val += 1
+            if self.cur_val < self.min_val:
+                raise RuntimeError
+            return self.cur_val
+
+    with patch.object(
+        cloud.openstack,
+        'RETRY_EXCEPTIONS',
+        new=new_exceptions,
+    ):
+        test_obj = test_cls(min_val=5)
+        assert cloud.openstack.retry(test_obj.func) == 5
+        test_obj = test_cls(min_val=1000)
+        with raises(MaxWhileTries):
+            cloud.openstack.retry(test_obj.func)
+
+
+def get_fake_obj(mock_args=None, attributes=None):
+    if mock_args is None:
+        mock_args = dict()
+    if attributes is None:
+        attributes = dict()
+    obj = Mock(**mock_args)
+    for name, value in attributes.items():
+        setattr(obj, name, value)
+    return obj
+
+
+class TestOpenStackBase(object):
+    def setup_method(self):
+        config.load(dict(libcloud=deepcopy(test_config)))
+        self.start_patchers()
+
+    def start_patchers(self):
+        self.patchers = dict()
+        self.patchers['m_list_images'] = patch(
+            'libcloud.compute.drivers.openstack'
+            '.OpenStackNodeDriver.list_images'
+        )
+        self.patchers['m_list_sizes'] = patch(
+            'libcloud.compute.drivers.openstack'
+            '.OpenStackNodeDriver.list_sizes'
+        )
+        self.patchers['m_ex_list_networks'] = patch(
+            'libcloud.compute.drivers.openstack'
+            '.OpenStack_1_1_NodeDriver.ex_list_networks'
+        )
+        self.patchers['m_ex_list_security_groups'] = patch(
+            'libcloud.compute.drivers.openstack'
+            '.OpenStack_1_1_NodeDriver.ex_list_security_groups'
+        )
+        self.patchers['m_get_user_ssh_pubkey'] = patch(
+            'teuthology.provision.cloud.util.get_user_ssh_pubkey'
+        )
+        self.patchers['m_list_nodes'] = patch(
+            'libcloud.compute.drivers.openstack'
+            '.OpenStackNodeDriver.list_nodes'
+        )
+        self.patchers['m_create_node'] = patch(
+            'libcloud.compute.drivers.openstack'
+            '.OpenStack_1_1_NodeDriver.create_node'
+        )
+        self.patchers['m_wait_until_running'] = patch(
+            'libcloud.compute.drivers.openstack'
+            '.OpenStackNodeDriver.wait_until_running'
+        )
+        self.patchers['m_create_volume'] = patch(
+            'libcloud.compute.drivers.openstack'
+            '.OpenStackNodeDriver.create_volume'
+        )
+        self.patchers['m_attach_volume'] = patch(
+            'libcloud.compute.drivers.openstack'
+            '.OpenStackNodeDriver.attach_volume'
+        )
+        self.patchers['m_detach_volume'] = patch(
+            'libcloud.compute.drivers.openstack'
+            '.OpenStackNodeDriver.detach_volume'
+        )
+        self.patchers['m_list_volumes'] = patch(
+            'libcloud.compute.drivers.openstack'
+            '.OpenStackNodeDriver.list_volumes'
+        )
+        self.patchers['m_destroy_volume'] = patch(
+            'libcloud.compute.drivers.openstack'
+            '.OpenStackNodeDriver.destroy_volume'
+        )
+        self.patchers['m_get_service_catalog'] = patch(
+            'libcloud.common.openstack'
+            '.OpenStackBaseConnection.get_service_catalog'
+        )
+        self.patchers['m_auth_token'] = patch(
+            'teuthology.provision.cloud.util.AuthToken'
+        )
+        self.patchers['m_get_endpoint'] = patch(
+            'libcloud.common.openstack'
+            '.OpenStackBaseConnection.get_endpoint',
+        )
+        self.patchers['m_connect'] = patch(
+            'libcloud.common.base'
+            '.Connection.connect',
+        )
+        self.patchers['m_sleep'] = patch(
+            'time.sleep'
+        )
+        self.patchers['m_get'] = patch(
+            'requests.get'
+        )
+        self.mocks = dict()
+        for name, patcher in self.patchers.items():
+            self.mocks[name] = patcher.start()
+        self.mocks['m_get_endpoint'].return_value = 'endpoint'
+
+    def teardown_method(self):
+        for patcher in self.patchers.values():
+            patcher.stop()
+
+
+class TestOpenStackProvider(TestOpenStackBase):
+    klass = cloud.openstack.OpenStackProvider
+
+    def test_init(self):
+        obj = cloud.get_provider('my_provider')
+        assert obj.name == 'my_provider'
+        assert obj.driver_name == 'openstack'
+        assert obj.conf == test_config['providers']['my_provider']
+
+    def test_driver(self):
+        token = self.mocks['m_auth_token'].return_value
+        self.mocks['m_auth_token'].return_value.__enter__.return_value = token
+        token.value = None
+        obj = cloud.get_provider('my_provider')
+        assert isinstance(obj.driver, get_driver('openstack'))
+        assert obj._auth_token.value is None
+
+    def test_images(self):
+        obj = cloud.get_provider('my_provider')
+        self.mocks['m_list_images'].return_value = [
+                get_fake_obj(attributes=dict(name=_))
+                    for _ in ['image0', 'image1']]
+        assert not hasattr(obj, '_images')
+        assert [_.name for _ in obj.images] == ['image0', 'image1']
+        assert hasattr(obj, '_images')
+
+    def test_exclude_image(self):
+        obj = cloud.get_provider('image_exclude_provider')
+        self.mocks['m_list_images'].return_value = [
+                get_fake_obj(attributes=dict(name=_))
+                    for _ in ['image0', 'image1',
+                        'image2-exclude1', 'image3-exclude2']]
+        assert not hasattr(obj, '_images')
+        assert [_.name for _ in obj.images] == ['image0', 'image1']
+        assert hasattr(obj, '_images')
+
+    def test_sizes(self):
+        obj = cloud.get_provider('my_provider')
+        fake_sizes = [get_fake_obj(attributes=dict(name='size%s' % i)) for
+                      i in range(2)]
+        self.mocks['m_list_sizes'].return_value = fake_sizes
+        assert not hasattr(obj, '_sizes')
+        assert [s.name for s in obj.sizes] == ['size0', 'size1']
+        assert hasattr(obj, '_sizes')
+
+    def test_networks(self):
+        obj = cloud.get_provider('my_provider')
+        nets = [get_fake_obj(attributes=dict(name=i)) for i in ['net0', 'net1']]
+        self.mocks['m_ex_list_networks'].return_value = nets
+        assert not hasattr(obj, '_networks')
+        assert [i.name for i in obj.networks] == [i.name for i in nets]
+        assert hasattr(obj, '_networks')
+        self.mocks['m_ex_list_networks'].side_effect = AttributeError
+        obj = cloud.get_provider('my_provider')
+        assert not hasattr(obj, '_networks')
+        assert obj.networks == list()
+        assert hasattr(obj, '_networks')
+
+    def test_security_groups(self):
+        obj = cloud.get_provider('my_provider')
+        self.mocks['m_ex_list_security_groups'].return_value = ['sg0', 'sg1']
+        assert not hasattr(obj, '_security_groups')
+        assert obj.security_groups == ['sg0', 'sg1']
+        assert hasattr(obj, '_security_groups')
+        self.mocks['m_ex_list_security_groups'].side_effect = AttributeError
+        obj = cloud.get_provider('my_provider')
+        assert not hasattr(obj, '_security_groups')
+        assert obj.security_groups == list()
+        assert hasattr(obj, '_security_groups')
+
+
+class TestOpenStackCustomProvisioner(TestOpenStackBase):
+    klass = cloud.openstack.OpenStackProvisioner
+    def get_obj(
+            self, name='node_name', os_type='ubuntu',
+            os_version='16.04', conf=None, test_conf=None):
+
+        if test_conf:
+            yaml_file = os.path.dirname(__file__) + '/' + test_conf
+            print("Reading conf: %s" % yaml_file)
+            with open(yaml_file) as f:
+                teuth_conf=yaml.safe_load(f)
+            print(teuth_conf)
+            config.libcloud = deepcopy(teuth_conf['libcloud'] or test_config)
+        else:
+            config.libcloud = deepcopy(test_config)
+        return cloud.get_provisioner(
+            node_type='my_provider',
+            name=name,
+            os_type=os_type,
+            os_version=os_version,
+            conf=conf,
+        )
+
+    @mark.parametrize(
+        "conf",
+        [
+            dict(
+                path='test_openstack_userdata_conf.yaml',
+                runcmd_head=['uptime', 'date'],
+                ssh_authorized_keys=['user_public_key1', 'user_public_key2'],
+                user_ssh_pubkey='my_ssh_key',
+                os_version='16.04',
+                os_type='ubuntu',
+            ),
+            dict(
+                path='test_openstack_userdata_conf.yaml',
+                runcmd_head=['uptime', 'date'],
+                ssh_authorized_keys=['user_public_key1', 'user_public_key2'],
+                user_ssh_pubkey=None,
+                os_version='16.04',
+                os_type='ubuntu',
+            ),
+            dict(
+                os_version='16.04',
+                os_type='ubuntu',
+                path=None,
+                user_ssh_pubkey=None,
+            ),
+        ]
+    )
+    def test_userdata_conf(self, conf):
+        self.mocks['m_get_user_ssh_pubkey'].return_value = conf['user_ssh_pubkey']
+        obj = self.get_obj(os_version=conf['os_version'],
+                           os_type=conf['os_type'],
+                           test_conf=conf['path'])
+        userdata = yaml.safe_load(obj.userdata)
+        print(">>>> ", obj.conf)
+        print(">>>> ", obj.provider.conf)
+        print(">>>> ", obj.provider)
+        print(obj.userdata)
+        if conf and 'path' in conf and conf['path']:
+            assert userdata['runcmd'][0:len(conf['runcmd_head'])] == conf['runcmd_head']
+            assert userdata['bootcmd'] == [
+                'SuSEfirewall2 stop || true',
+                'service firewalld stop || true',
+            ]
+            assert 'packages' not in userdata
+        else:
+            assert 'bootcmd' not in userdata
+            assert userdata['packages'] == ['git', 'wget', 'python', 'ntp']
+        assert userdata['user'] == obj.user
+        assert userdata['hostname'] == obj.hostname
+        if 'user_ssh_pubkey' in conf and conf['user_ssh_pubkey']:
+            assert userdata['ssh_authorized_keys'][-1] == conf['user_ssh_pubkey']
+            if 'ssh_authorized_keys' in conf:
+                keys = conf['ssh_authorized_keys']
+                assert userdata['ssh_authorized_keys'][0:len(keys)] == keys
+        else:
+            if 'ssh_authorized_keys' in conf:
+                keys = conf['ssh_authorized_keys']
+                assert userdata['ssh_authorized_keys'][0:len(keys)] == keys
+            else:
+                assert 'ssh_authorized_keys' not in userdata
+
+    @mark.parametrize(
+        "conf",
+        [
+            dict(
+                path='test_openstack_userdata_conf.yaml',
+                runcmd_head=['uptime', 'date'],
+            ),
+            dict(
+                path=None,
+            ),
+        ]
+    )
+    def test_userdata_conf_runcmd(self, conf):
+        self.mocks['m_get_user_ssh_pubkey'].return_value = None
+        obj = self.get_obj(test_conf=conf['path'])
+        userdata = yaml.safe_load(obj.userdata)
+        assert userdata['runcmd'][-2:] == [['passwd', '-d', 'ubuntu'], ['touch', '/.teuth_provisioned']]
+
+    @mark.parametrize(
+        "conf",
+        [
+            dict(
+                path='test_openstack_userdata_conf.yaml',
+                packages=None,
+            ),
+            dict(
+                path=None,
+                packages=['git', 'wget', 'python', 'ntp']
+            ),
+        ]
+    )
+    def test_userdata_conf_packages(self, conf):
+        self.mocks['m_get_user_ssh_pubkey'].return_value = None
+        obj = self.get_obj(test_conf=conf['path'])
+        userdata = yaml.safe_load(obj.userdata)
+        assert userdata.get('packages', None) == conf['packages']
+
+class TestOpenStackProvisioner(TestOpenStackBase):
+    klass = cloud.openstack.OpenStackProvisioner
+
+    def get_obj(
+            self, name='node_name', os_type='ubuntu',
+            os_version='16.04', conf=None):
+        return cloud.get_provisioner(
+            node_type='my_provider',
+            name=name,
+            os_type=os_type,
+            os_version=os_version,
+            conf=conf,
+        )
+
+    def test_init(self):
+        with patch.object(
+            self.klass,
+            '_read_conf',
+        ) as m_read_conf:
+            self.get_obj()
+            assert len(m_read_conf.call_args_list) == 1
+
+    @mark.parametrize(
+        'input_conf',
+        [
+            dict(machine=dict(
+                disk=42,
+                ram=9001,
+                cpus=3,
+            )),
+            dict(volumes=dict(
+                count=3,
+                size=100,
+            )),
+            dict(),
+            dict(
+                machine=dict(
+                    disk=1,
+                    ram=2,
+                    cpus=3,
+                ),
+                volumes=dict(
+                    count=4,
+                    size=5,
+                )
+            ),
+            dict(
+                machine=dict(
+                    disk=100,
+                ),
+            ),
+        ]
+    )
+    def test_read_conf(self, input_conf):
+        obj = self.get_obj(conf=input_conf)
+        for topic in ['machine', 'volumes']:
+            combined = cloud.util.combine_dicts(
+                [input_conf, config.openstack],
+                lambda x, y: x > y,
+            )
+            assert obj.conf[topic] == combined[topic]
+
+    @mark.parametrize(
+        'input_conf, expected_machine, expected_vols',
+        [
+            [
+                dict(openstack=[
+                    dict(machine=dict(disk=64, ram=10000, cpus=3)),
+                    dict(volumes=dict(count=1, size=1)),
+                ]),
+                dict(disk=64, ram=10000, cpus=3),
+                dict(count=1, size=1),
+            ],
+            [
+                dict(openstack=[
+                    dict(machine=dict(cpus=3)),
+                    dict(machine=dict(disk=1, ram=9000)),
+                    dict(machine=dict(disk=50, ram=2, cpus=1)),
+                    dict(machine=dict()),
+                    dict(volumes=dict()),
+                    dict(volumes=dict(count=0, size=0)),
+                    dict(volumes=dict(count=1, size=0)),
+                    dict(volumes=dict(size=1)),
+                ]),
+                dict(disk=50, ram=9000, cpus=3),
+                dict(count=1, size=1),
+            ],
+            [
+                dict(openstack=[
+                    dict(volumes=dict(count=3, size=30)),
+                    dict(volumes=dict(size=50)),
+                ]),
+                None,
+                dict(count=3, size=50),
+            ],
+            [
+                dict(openstack=[
+                    dict(machine=dict(disk=100)),
+                    dict(volumes=dict(count=3, size=30)),
+                ]),
+                dict(disk=100, ram=8000, cpus=1),
+                dict(count=3, size=30),
+            ],
+        ]
+    )
+    def test_read_conf_legacy(
+            self, input_conf, expected_machine, expected_vols):
+        obj = self.get_obj(conf=input_conf)
+        if expected_machine is not None:
+            assert obj.conf['machine'] == expected_machine
+        else:
+            assert obj.conf['machine'] == config.openstack['machine']
+        if expected_vols is not None:
+            assert obj.conf['volumes'] == expected_vols
+
+    @mark.parametrize(
+        "os_type, os_version, should_find",
+        [
+            ('centos', '7', True),
+            ('BeOS', '42', False),
+        ]
+    )
+    def test_image(self, os_type, os_version, should_find):
+        image_attrs = [
+            dict(name='ubuntu-14.04'),
+            dict(name='ubuntu-16.04'),
+            dict(name='centos-7.0'),
+        ]
+        fake_images = list()
+        for item in image_attrs:
+            fake_images.append(
+                get_fake_obj(attributes=item)
+            )
+        obj = self.get_obj(os_type=os_type, os_version=os_version)
+        self.mocks['m_list_images'].return_value = fake_images
+        if should_find:
+            assert obj.os_version in obj.image.name
+            assert obj.image in fake_images
+        else:
+            with raises(RuntimeError):
+                obj.image
+
+    @mark.parametrize(
+        "input_attrs, func_or_exc",
+        [
+            (dict(ram=2**16),
+             lambda s: s.ram == 2**16),
+            (dict(disk=9999),
+             lambda s: s.disk == 9999),
+            (dict(cpus=99),
+             lambda s: s.vcpus == 99),
+            (dict(ram=2**16, disk=9999, cpus=99),
+             IndexError),
+        ]
+    )
+    def test_size(self, input_attrs, func_or_exc):
+        size_attrs = [
+            dict(ram=8000, disk=9999, vcpus=99, name='s0'),
+            dict(ram=2**16, disk=20, vcpus=99, name='s1'),
+            dict(ram=2**16, disk=9999, vcpus=1, name='s2'),
+        ]
+        fake_sizes = list()
+        for item in size_attrs:
+            fake_sizes.append(
+                get_fake_obj(attributes=item)
+            )
+        base_spec = dict(machine=dict(
+            ram=1,
+            disk=1,
+            cpus=1,
+        ))
+        spec = deepcopy(base_spec)
+        spec['machine'].update(input_attrs)
+        obj = self.get_obj(conf=spec)
+        self.mocks['m_list_sizes'].return_value = fake_sizes
+        if isinstance(func_or_exc, type):
+            with raises(func_or_exc):
+                obj.size
+        else:
+            assert obj.size in fake_sizes
+            assert func_or_exc(obj.size) is True
+
+    @mark.parametrize(
+        "wanted_groups",
+        [
+            ['group1'],
+            ['group0', 'group2'],
+            [],
+        ]
+    )
+    def test_security_groups(self, wanted_groups):
+        group_names = ['group0', 'group1', 'group2']
+        fake_groups = list()
+        for name in group_names:
+            fake_groups.append(
+                get_fake_obj(attributes=dict(name=name))
+            )
+        self.mocks['m_ex_list_security_groups'].return_value = fake_groups
+        obj = self.get_obj()
+        assert obj.security_groups is None
+        obj = self.get_obj()
+        obj.provider.conf['security_groups'] = wanted_groups
+        assert [g.name for g in obj.security_groups] == wanted_groups
+
+    def test_security_groups_exc(self):
+        fake_groups = [
+            get_fake_obj(attributes=dict(name='sg')) for i in range(2)
+        ]
+        obj = self.get_obj()
+        obj.provider.conf['security_groups'] = ['sg']
+        with raises(RuntimeError):
+            obj.security_groups
+        self.mocks['m_ex_list_security_groups'].return_value = fake_groups
+        obj = self.get_obj()
+        obj.provider.conf['security_groups'] = ['sg']
+        with raises(RuntimeError):
+            obj.security_groups
+
+    @mark.parametrize(
+        "ssh_key",
+        [
+            'my_ssh_key',
+            None,
+        ]
+    )
+    def test_userdata(self, ssh_key):
+        self.mocks['m_get_user_ssh_pubkey'].return_value = ssh_key
+        obj = self.get_obj()
+        userdata = yaml.safe_load(obj.userdata)
+        assert userdata['user'] == obj.user
+        assert userdata['hostname'] == obj.hostname
+        if ssh_key:
+            assert userdata['ssh_authorized_keys'] == [ssh_key]
+        else:
+            assert 'ssh_authorized_keys' not in userdata
+
+    @mark.parametrize(
+        'wanted_name, should_find, exception',
+        [
+            ('node0', True, None),
+            ('node1', True, None),
+            ('node2', False, RuntimeError),
+            ('node3', False, None),
+        ]
+    )
+    def test_node(self, wanted_name, should_find, exception):
+        node_names = ['node0', 'node1', 'node2', 'node2']
+        fake_nodes = list()
+        for name in node_names:
+            fake_nodes.append(
+                get_fake_obj(attributes=dict(name=name))
+            )
+        self.mocks['m_list_nodes'].return_value = fake_nodes
+        obj = self.get_obj(name=wanted_name)
+        if should_find:
+            assert obj.node.name == wanted_name
+        elif exception:
+            with raises(exception) as excinfo:
+                obj.node
+                assert excinfo.value.message
+        else:
+            assert obj.node is None
+
+    @mark.parametrize(
+        'networks, security_groups',
+        [
+            ([], []),
+            (['net0'], []),
+            ([], ['sg0']),
+            (['net0'], ['sg0']),
+        ]
+    )
+    def test_create(self, networks, security_groups):
+        node_name = 'node0'
+        fake_sizes = [
+            get_fake_obj(
+                attributes=dict(ram=2**16, disk=9999, vcpus=99, name='s0')),
+        ]
+        fake_security_groups = [
+            get_fake_obj(attributes=dict(name=name))
+            for name in security_groups
+        ]
+        self.mocks['m_ex_list_networks'].return_value = networks
+        self.mocks['m_ex_list_security_groups'].return_value = \
+            fake_security_groups
+        self.mocks['m_list_sizes'].return_value = fake_sizes
+        fake_images = [
+            get_fake_obj(attributes=dict(name='ubuntu-16.04')),
+        ]
+        self.mocks['m_list_images'].return_value = fake_images
+        self.mocks['m_get_user_ssh_pubkey'].return_value = 'ssh_key'
+        fake_node = get_fake_obj(attributes=dict(name=node_name))
+        fake_ips = ['555.123.4.0']
+        self.mocks['m_create_node'].return_value = fake_node
+        self.mocks['m_wait_until_running'].return_value = \
+            [(fake_node, fake_ips)]
+        obj = self.get_obj(name=node_name)
+        obj._networks = networks
+        obj.provider.conf['security_groups'] = security_groups
+        p_wait_for_ready = patch(
+            'teuthology.provision.cloud.openstack.OpenStackProvisioner'
+            '._wait_for_ready'
+        )
+        with p_wait_for_ready:
+            res = obj.create()
+        assert res is obj.node
+        # Test once again to ensure that if volume creation/attachment fails,
+        # we destroy any remaining volumes and consider the node creation to
+        # have failed as well.
+        del obj._node
+        with p_wait_for_ready:
+            obj.conf['volumes']['count'] = 1
+            obj.provider.driver.create_volume.side_effect = Exception
+            with patch.object(obj, '_destroy_volumes'):
+                assert obj.create() is False
+                obj._destroy_volumes.assert_called_once_with()
+
+    def test_update_dns(self):
+        config.nsupdate_url = 'nsupdate_url'
+        obj = self.get_obj()
+        obj.name = 'x'
+        obj.ips = ['y']
+        obj._update_dns()
+        call_args = self.mocks['m_get'].call_args_list
+        assert len(call_args) == 1
+        url_base, query_string = call_args[0][0][0].split('?')
+        assert url_base == 'nsupdate_url'
+        parsed_query = parse_qs(query_string)
+        assert parsed_query == dict(name=['x'], ip=['y'])
+
+    @mark.parametrize(
+        'nodes',
+        [[], [Mock()], [Mock(), Mock()]]
+    )
+    def test_destroy(self, nodes):
+        with patch(
+            'teuthology.provision.cloud.openstack.'
+            'OpenStackProvisioner._find_nodes'
+        ) as m_find_nodes:
+            m_find_nodes.return_value = nodes
+            obj = self.get_obj()
+            result = obj.destroy()
+            if not all(nodes):
+                assert result is True
+            else:
+                for node in nodes:
+                    node.destroy.assert_called_once_with()
+
+    _volume_matrix = (
+        'count, size, should_succeed',
+        [
+            (1, 10, True),
+            (0, 10, True),
+            (10, 1, True),
+            (1, 10, False),
+            (10, 1, False),
+        ]
+    )
+
+    @mark.parametrize(*_volume_matrix)
+    def test_create_volumes(self, count, size, should_succeed):
+        obj_conf = dict(volumes=dict(count=count, size=size))
+        obj = self.get_obj(conf=obj_conf)
+        node = get_fake_obj()
+        if not should_succeed:
+            obj.provider.driver.create_volume.side_effect = Exception
+        obj._node = node
+        result = obj._create_volumes()
+        assert result is should_succeed
+        if should_succeed:
+            create_calls = obj.provider.driver.create_volume.call_args_list
+            attach_calls = obj.provider.driver.attach_volume.call_args_list
+            assert len(create_calls) == count
+            assert len(attach_calls) == count
+            for i in range(count):
+                vol_size, vol_name = create_calls[i][0]
+                assert vol_size == size
+                assert vol_name == '%s_%s' % (obj.name, i)
+                assert attach_calls[i][0][0] is obj._node
+                assert attach_calls[i][1]['device'] is None
+
+    @mark.parametrize(*_volume_matrix)
+    def test_destroy_volumes(self, count, size, should_succeed):
+        obj_conf = dict(volumes=dict(count=count, size=size))
+        obj = self.get_obj(conf=obj_conf)
+        fake_volumes = list()
+        for i in range(count):
+            vol_name = '%s_%s' % (obj.name, i)
+            fake_volumes.append(
+                get_fake_obj(attributes=dict(name=vol_name))
+            )
+        obj.provider.driver.list_volumes.return_value = fake_volumes
+        obj._destroy_volumes()
+        detach_calls = obj.provider.driver.detach_volume.call_args_list
+        destroy_calls = obj.provider.driver.destroy_volume.call_args_list
+        assert len(detach_calls) == count
+        assert len(destroy_calls) == count
+        assert len(obj.provider.driver.detach_volume.call_args_list) == count
+        assert len(obj.provider.driver.destroy_volume.call_args_list) == count
+        obj.provider.driver.detach_volume.reset_mock()
+        obj.provider.driver.destroy_volume.reset_mock()
+        obj.provider.driver.detach_volume.side_effect = Exception
+        obj.provider.driver.destroy_volume.side_effect = Exception
+        obj._destroy_volumes()
+        assert len(obj.provider.driver.detach_volume.call_args_list) == count
+        assert len(obj.provider.driver.destroy_volume.call_args_list) == count
+
+    def test_destroy_volumes_exc(self):
+        obj = self.get_obj()
+        obj.provider.driver.detach_volume.side_effect = Exception
+
+    def test_wait_for_ready(self):
+        obj = self.get_obj()
+        obj._node = get_fake_obj(attributes=dict(name='node_name'))
+        with patch.multiple(
+            'teuthology.orchestra.remote.Remote',
+            connect=DEFAULT,
+            run=DEFAULT,
+        ) as mocks:
+            obj._wait_for_ready()
+            mocks['connect'].side_effect = socket.error
+            with raises(MaxWhileTries):
+                obj._wait_for_ready()
diff --git a/tests/provision/cloud/test_openstack_userdata_conf.yaml b/tests/provision/cloud/test_openstack_userdata_conf.yaml
new file mode 100644 (file)
index 0000000..f3e87a8
--- /dev/null
@@ -0,0 +1,24 @@
+libcloud:
+  providers:
+    my_provider:
+      allow_networks:
+        - sesci
+      userdata:
+        'ubuntu-16.04':
+            bootcmd:
+              - 'SuSEfirewall2 stop || true'
+              - 'service firewalld stop || true'
+            runcmd:
+              - 'uptime'
+              - 'date'
+              - 'zypper in -y lsb-release make gcc gcc-c++ chrony || true'
+              - 'systemctl enable chronyd.service || true'
+              - 'systemctl start chronyd.service || true'
+            ssh_authorized_keys:
+              - user_public_key1
+              - user_public_key2
+      driver: openstack
+      driver_args:
+          username: user
+          password: password
+          ex_force_auth_url: 'http://127.0.0.1:9999/v2.0/tokens'
diff --git a/tests/provision/test_downburst.py b/tests/provision/test_downburst.py
new file mode 100644 (file)
index 0000000..4ba7d9a
--- /dev/null
@@ -0,0 +1,105 @@
+from mock import Mock, MagicMock, patch
+
+from teuthology import provision
+
+
+class TestDownburst(object):
+    def setup_method(self):
+        self.ctx = Mock()
+        self.ctx.os_type = 'rhel'
+        self.ctx.os_version = '7.0'
+        self.ctx.config = dict()
+        self.name = 'vpm999'
+        self.status = dict(
+            vm_host=dict(name='host999'),
+            is_vm=True,
+            machine_type='mtype',
+            locked_by='user@a',
+            description="desc",
+        )
+
+    def test_create_if_vm_success(self):
+        name = self.name
+        ctx = self.ctx
+        status = self.status
+
+        dbrst = provision.downburst.Downburst(
+            name, ctx.os_type, ctx.os_version, status)
+        dbrst.executable = '/fake/path'
+        dbrst.build_config = MagicMock(name='build_config')
+        dbrst._run_create = MagicMock(name='_run_create')
+        dbrst._run_create.return_value = (0, '', '')
+        remove_config = MagicMock(name='remove_config')
+        dbrst.remove_config = remove_config
+
+        result = provision.create_if_vm(ctx, name, dbrst)
+        assert result is True
+
+        dbrst._run_create.assert_called_with()
+        dbrst.build_config.assert_called_with()
+        del dbrst
+        remove_config.assert_called_with()
+
+    def test_destroy_if_vm_success(self):
+        name = self.name
+        ctx = self.ctx
+        status = self.status
+
+        dbrst = provision.downburst.Downburst(
+            name, ctx.os_type, ctx.os_version, status)
+        dbrst.destroy = MagicMock(name='destroy')
+        dbrst.destroy.return_value = True
+
+        result = provision.destroy_if_vm(name, user="user@a", _downburst=dbrst)
+        assert result is True
+
+        dbrst.destroy.assert_called_with()
+
+    def test_destroy_if_vm_wrong_owner(self):
+        name = self.name
+        ctx = self.ctx
+        status = self.status
+
+        dbrst = provision.downburst.Downburst(
+            name, ctx.os_type, ctx.os_version, status)
+        dbrst.destroy = MagicMock(name='destroy', side_effect=RuntimeError)
+
+        result = provision.destroy_if_vm(name, user='user@b',
+                                         _downburst=dbrst)
+        assert result is False
+
+    def test_destroy_if_vm_wrong_description(self):
+        name = self.name
+        ctx = self.ctx
+        status = self.status
+
+        dbrst = provision.downburst.Downburst(
+            name, ctx.os_type, ctx.os_version, status)
+        dbrst.destroy = MagicMock(name='destroy')
+        dbrst.destroy = MagicMock(name='destroy', side_effect=RuntimeError)
+
+        result = provision.destroy_if_vm(name, description='desc_b',
+                                         _downburst=dbrst)
+        assert result is False
+
+    @patch('teuthology.provision.downburst.downburst_executable')
+    def test_create_fails_without_executable(self, m_exec):
+        name = self.name
+        ctx = self.ctx
+        status = self.status
+        m_exec.return_value = ''
+        dbrst = provision.downburst.Downburst(
+            name, ctx.os_type, ctx.os_version, status)
+        result = dbrst.create()
+        assert result is False
+
+    @patch('teuthology.provision.downburst.downburst_executable')
+    def test_destroy_fails_without_executable(self, m_exec):
+        name = self.name
+        ctx = self.ctx
+        status = self.status
+        m_exec.return_value = ''
+        dbrst = provision.downburst.Downburst(
+            name, ctx.os_type, ctx.os_version, status)
+        result = dbrst.destroy()
+        assert result is False
diff --git a/tests/provision/test_fog.py b/tests/provision/test_fog.py
new file mode 100644 (file)
index 0000000..3d0baa7
--- /dev/null
@@ -0,0 +1,339 @@
+import datetime
+
+from copy import deepcopy
+from mock import patch, DEFAULT, PropertyMock
+from pytest import raises, mark
+
+from teuthology.config import config
+from teuthology.exceptions import MaxWhileTries, CommandFailedError
+from teuthology.provision import fog
+
+
+test_config = dict(
+    fog=dict(
+        endpoint='http://fog.example.com/fog',
+        api_token='API_TOKEN',
+        user_token='USER_TOKEN',
+        machine_types='type1,type2',
+    ),
+    fog_reimage_timeout=1800,
+)
+
+
+class TestFOG(object):
+    klass = fog.FOG
+
+    def setup_method(self):
+        config.load()
+        config.update(deepcopy(test_config))
+        self.start_patchers()
+
+    def start_patchers(self):
+        self.patchers = dict()
+        self.patchers['m_sleep'] = patch(
+            'time.sleep',
+        )
+        self.patchers['m_requests_Session_send'] = patch(
+            'requests.Session.send',
+        )
+        self.patchers['m_Remote_connect'] = patch(
+            'teuthology.orchestra.remote.Remote.connect'
+        )
+        self.patchers['m_Remote_run'] = patch(
+            'teuthology.orchestra.remote.Remote.run'
+        )
+        self.patchers['m_Remote_console'] = patch(
+            'teuthology.orchestra.remote.Remote.console',
+            new_callable=PropertyMock,
+        )
+        self.patchers['m_Remote_hostname'] = patch(
+            'teuthology.orchestra.remote.Remote.hostname',
+            new_callable=PropertyMock,
+        )
+        self.patchers['m_Remote_machine_type'] = patch(
+            'teuthology.orchestra.remote.Remote.machine_type',
+            new_callable=PropertyMock,
+        )
+        self.mocks = dict()
+        for name, patcher in self.patchers.items():
+            self.mocks[name] = patcher.start()
+
+    def teardown_method(self):
+        for patcher in self.patchers.values():
+            patcher.stop()
+
+    @mark.parametrize('enabled', [True, False])
+    def test_get_types(self, enabled):
+        with patch('teuthology.provision.fog.enabled') as m_enabled:
+            m_enabled.return_value = enabled
+            types = fog.get_types()
+        if enabled:
+            assert types == test_config['fog']['machine_types'].split(',')
+        else:
+            assert types == []
+
+    def test_disabled(self):
+        config.fog['endpoint'] = None
+        obj = self.klass('name.fqdn', 'type', '1.0')
+        with raises(RuntimeError):
+            obj.create()
+
+    def test_init(self):
+        self.mocks['m_Remote_hostname'].return_value = 'name.fqdn'
+        obj = self.klass('name.fqdn', 'type', '1.0')
+        assert obj.name == 'name.fqdn'
+        assert obj.shortname == 'name'
+        assert obj.os_type == 'type'
+        assert obj.os_version == '1.0'
+
+    @mark.parametrize('success', [True, False])
+    def test_create(self, success):
+        self.mocks['m_Remote_hostname'].return_value = 'name.fqdn'
+        self.mocks['m_Remote_machine_type'].return_value = 'type1'
+        obj = self.klass('name.fqdn', 'type', '1.0')
+        host_id = 99
+        task_id = 1234
+        with patch.multiple(
+            'teuthology.provision.fog.FOG',
+            get_host_data=DEFAULT,
+            set_image=DEFAULT,
+            schedule_deploy_task=DEFAULT,
+            wait_for_deploy_task=DEFAULT,
+            cancel_deploy_task=DEFAULT,
+            _wait_for_ready=DEFAULT,
+            _fix_hostname=DEFAULT,
+            _verify_installed_os=DEFAULT,
+        ) as local_mocks:
+            local_mocks['get_host_data'].return_value = dict(id=host_id)
+            local_mocks['schedule_deploy_task'].return_value = task_id
+            if not success:
+                local_mocks['wait_for_deploy_task'].side_effect = RuntimeError
+                with raises(RuntimeError):
+                    obj.create()
+            else:
+                obj.create()
+            local_mocks['get_host_data'].assert_called_once_with()
+            local_mocks['set_image'].assert_called_once_with(host_id)
+            local_mocks['schedule_deploy_task'].assert_called_once_with(host_id)
+            local_mocks['wait_for_deploy_task'].assert_called_once_with(task_id)
+            if success:
+                local_mocks['_wait_for_ready'].assert_called_once_with()
+                local_mocks['_fix_hostname'].assert_called_once_with()
+            else:
+                assert len(local_mocks['cancel_deploy_task'].call_args_list) == 1
+        self.mocks['m_Remote_console'].return_value.power_off.assert_called_once_with()
+        self.mocks['m_Remote_console'].return_value.power_on.assert_called_once_with()
+
+    def test_do_request(self):
+        obj = self.klass('name.fqdn', 'type', '1.0')
+        obj.do_request('test_url', data='DATA', method='GET')
+        assert len(self.mocks['m_requests_Session_send'].call_args_list) == 1
+        req = self.mocks['m_requests_Session_send'].call_args_list[0][0][0]
+        assert req.url == test_config['fog']['endpoint'] + 'test_url'
+        assert req.method == 'GET'
+        assert req.headers['fog-api-token'] == test_config['fog']['api_token']
+        assert req.headers['fog-user-token'] == test_config['fog']['user_token']
+        assert req.body == 'DATA'
+
+    @mark.parametrize(
+        'count',
+        [0, 1, 2],
+    )
+    def test_get_host_data(self, count):
+        host_objs = [dict(id=i) for i in range(count)]
+        resp_obj = dict(count=count, hosts=host_objs)
+        self.mocks['m_requests_Session_send']\
+            .return_value.json.return_value = resp_obj
+        obj = self.klass('name.fqdn', 'type', '1.0')
+        if count != 1:
+            with raises(RuntimeError):
+                result = obj.get_host_data()
+            return
+        result = obj.get_host_data()
+        assert len(self.mocks['m_requests_Session_send'].call_args_list) == 1
+        req = self.mocks['m_requests_Session_send'].call_args_list[0][0][0]
+        assert req.url == test_config['fog']['endpoint'] + '/host'
+        assert req.body == '{"name": "name"}'
+        assert result == host_objs[0]
+
+    @mark.parametrize(
+        'count',
+        [0, 1, 2],
+    )
+    def test_get_image_data(self, count):
+        img_objs = [dict(id=i) for i in range(count)]
+        resp_obj = dict(count=count, images=img_objs)
+        self.mocks['m_requests_Session_send']\
+            .return_value.json.return_value = resp_obj
+        self.mocks['m_Remote_machine_type'].return_value = 'type1'
+        obj = self.klass('name.fqdn', 'windows', 'xp')
+        if count < 1:
+            with raises(RuntimeError):
+                result = obj.get_image_data()
+            return
+        result = obj.get_image_data()
+        assert len(self.mocks['m_requests_Session_send'].call_args_list) == 1
+        req = self.mocks['m_requests_Session_send'].call_args_list[0][0][0]
+        assert req.url == test_config['fog']['endpoint'] + '/image'
+        assert req.body == '{"name": "type1_windows_xp"}'
+        assert result == img_objs[0]
+
+    def test_suggest_image_names(self):
+        data = {'images': [
+            {'name': 'mira_rhel_9.1'},
+            {'name': 'mira_rhel_9.2'},
+        ]}
+        self.mocks['m_requests_Session_send']\
+            .return_value.json.return_value = data
+        self.mocks['m_Remote_machine_type'].return_value = 'mira'
+        # Not sure what this klass() is for here:
+        obj = self.klass('name.fqdn', 'mira', '1.0')
+        result = obj.suggest_image_names()
+        assert len(self.mocks['m_requests_Session_send'].call_args_list) == 1
+        req = self.mocks['m_requests_Session_send'].call_args_list[0][0][0]
+        assert req.url == test_config['fog']['endpoint'] + '/image/search/mira'
+        assert result == ['mira_rhel_9.1', 'mira_rhel_9.2']
+
+    def test_set_image(self):
+        self.mocks['m_Remote_hostname'].return_value = 'name.fqdn'
+        self.mocks['m_Remote_machine_type'].return_value = 'type1'
+        host_id = 999
+        obj = self.klass('name.fqdn', 'type', '1.0')
+        with patch.multiple(
+            'teuthology.provision.fog.FOG',
+            get_image_data=DEFAULT,
+            do_request=DEFAULT,
+        ) as local_mocks:
+            local_mocks['get_image_data'].return_value = dict(id='13')
+            obj.set_image(host_id)
+            local_mocks['do_request'].assert_called_once_with(
+                '/host/999', method='PUT', data='{"imageID": 13}',
+            )
+
+    def test_schedule_deploy_task(self):
+        host_id = 12
+        tasktype_id = 6
+        task_id = 5
+        tasktype_result = dict(tasktypes=[dict(name='deploy', id=tasktype_id)])
+        schedule_result = dict()
+        host_tasks = [dict(
+            createdTime=datetime.datetime.strftime(
+                datetime.datetime.now(datetime.timezone.utc),
+                self.klass.timestamp_format
+            ),
+            id=task_id,
+        )]
+        self.mocks['m_requests_Session_send']\
+            .return_value.json.side_effect = [
+            tasktype_result, schedule_result,
+        ]
+        with patch.multiple(
+            'teuthology.provision.fog.FOG',
+            get_deploy_tasks=DEFAULT,
+        ) as local_mocks:
+            local_mocks['get_deploy_tasks'].return_value = host_tasks
+            obj = self.klass('name.fqdn', 'type', '1.0')
+            result = obj.schedule_deploy_task(host_id)
+            assert len(local_mocks['get_deploy_tasks'].call_args_list) == 2
+        assert len(self.mocks['m_requests_Session_send'].call_args_list) == 3
+        assert result == task_id
+
+    def test_get_deploy_tasks(self):
+        obj = self.klass('name.fqdn', 'type', '1.0')
+        resp_obj = dict(
+            count=2,
+            tasks=[
+                dict(host=dict(name='notme')),
+                dict(host=dict(name='name')),
+            ]
+        )
+        self.mocks['m_requests_Session_send']\
+            .return_value.json.return_value = resp_obj
+        result = obj.get_deploy_tasks()
+        assert result[0]['host']['name'] == 'name'
+
+    @mark.parametrize(
+        'active_ids',
+        [
+            [2, 4, 6, 8],
+            [1],
+            [],
+        ]
+    )
+    def test_deploy_task_active(self, active_ids):
+        our_task_id = 4
+        result_objs = [dict(id=task_id) for task_id in active_ids]
+        obj = self.klass('name.fqdn', 'type', '1.0')
+        with patch.multiple(
+            'teuthology.provision.fog.FOG',
+            get_deploy_tasks=DEFAULT,
+        ) as local_mocks:
+            local_mocks['get_deploy_tasks'].return_value = result_objs
+            result = obj.deploy_task_active(our_task_id)
+            assert result is (our_task_id in active_ids)
+
+    @mark.parametrize(
+        'tries',
+        [3, 121],
+    )
+    def test_wait_for_deploy_task(self, tries):
+        wait_results = [True for i in range(tries)] + [False]
+        obj = self.klass('name.fqdn', 'type', '1.0')
+        with patch.multiple(
+            'teuthology.provision.fog.FOG',
+            deploy_task_active=DEFAULT,
+        ) as local_mocks:
+            local_mocks['deploy_task_active'].side_effect = wait_results
+            if tries >= 60:
+                with raises(MaxWhileTries):
+                    obj.wait_for_deploy_task(9)
+                return
+            obj.wait_for_deploy_task(9)
+            assert len(local_mocks['deploy_task_active'].call_args_list) == \
+                tries + 1
+
+    def test_cancel_deploy_task(self):
+        obj = self.klass('name.fqdn', 'type', '1.0')
+        with patch.multiple(
+            'teuthology.provision.fog.FOG',
+            do_request=DEFAULT,
+        ) as local_mocks:
+            obj.cancel_deploy_task(10)
+            local_mocks['do_request'].assert_called_once_with(
+                '/task/cancel',
+                method='DELETE',
+                data='{"id": 10}',
+            )
+
+    @mark.parametrize(
+        'tries',
+        [1, 101],
+    )
+    def test_wait_for_ready_tries(self, tries):
+        connect_results = [MaxWhileTries for i in range(tries)] + [True]
+        obj = self.klass('name.fqdn', 'type', '1.0')
+        self.mocks['m_Remote_connect'].side_effect = connect_results
+        if tries >= 100:
+            with raises(MaxWhileTries):
+                obj._wait_for_ready()
+            return
+        obj._wait_for_ready()
+        assert len(self.mocks['m_Remote_connect'].call_args_list) == tries + 1
+
+    @mark.parametrize(
+        'sentinel_present',
+        ([False, True]),
+    )
+    def test_wait_for_ready_sentinel(self, sentinel_present):
+        config.fog['sentinel_file'] = '/a_file'
+        obj = self.klass('name.fqdn', 'type', '1.0')
+        if not sentinel_present:
+            self.mocks['m_Remote_run'].side_effect = [
+                CommandFailedError(command='cmd', exitstatus=1)]
+            with raises(CommandFailedError):
+                obj._wait_for_ready()
+        else:
+            obj._wait_for_ready()
+        assert len(self.mocks['m_Remote_run'].call_args_list) == 1
+        assert "'/a_file'" in \
+            self.mocks['m_Remote_run'].call_args_list[0][1]['args']
diff --git a/tests/provision/test_init_provision.py b/tests/provision/test_init_provision.py
new file mode 100644 (file)
index 0000000..e63c5aa
--- /dev/null
@@ -0,0 +1,46 @@
+from copy import deepcopy
+from pytest import raises
+from teuthology.config import config
+
+import teuthology.provision
+
+test_config = dict(
+    pelagos=dict(
+        endpoint='http://pelagos.example:5000/',
+        machine_types='ptype1,ptype2,common_type',
+    ),
+    fog=dict(
+        endpoint='http://fog.example.com/fog',
+        api_token='API_TOKEN',
+        user_token='USER_TOKEN',
+        machine_types='ftype1,ftype2,common_type',
+    )
+)
+
+class TestInitProvision(object):
+
+    def setup_method(self):
+        config.load(deepcopy(test_config))
+
+    def test_get_reimage_types(self):
+        reimage_types = teuthology.provision.get_reimage_types()
+        assert reimage_types == ["ptype1", "ptype2", "common_type",
+                                 "ftype1", "ftype2", "common_type"]
+
+    def test_reimage(self):
+        class context:
+            pass
+        ctx = context()
+        ctx.os_type = 'sle'
+        ctx.os_version = '15.1'
+        with raises(Exception) as e_info:
+            teuthology.provision.reimage(ctx, 'f.q.d.n.org', 'not-defined-type')
+        e_str = str(e_info)
+        print("Caught exception: " +  e_str)
+        assert e_str.find(r"configured\sprovisioners") == -1
+
+        with raises(Exception) as e_info:
+            teuthology.provision.reimage(ctx, 'f.q.d.n.org', 'common_type')
+        e_str = str(e_info)
+        print("Caught exception: " +  e_str)
+        assert e_str.find(r"used\swith\sone\sprovisioner\sonly") == -1
diff --git a/tests/provision/test_maas.py b/tests/provision/test_maas.py
new file mode 100644 (file)
index 0000000..ae47e0c
--- /dev/null
@@ -0,0 +1,311 @@
+from copy import deepcopy
+from mock import patch, DEFAULT, PropertyMock
+from pytest import raises, mark
+from requests.models import Response
+
+from teuthology.config import config
+from teuthology.orchestra.opsys import OS
+from teuthology.provision import maas
+
+
+test_config = dict(
+    maas=dict(
+        api_url="http://maas.example.com:5240/MAAS/api/2.0/",
+        api_key="CONSUMER_KEY:ACCESS_TOKEN:SECRET",
+        machine_types=["typeA", "typeB"],
+        timeout=900,
+        user_data="teuthology/maas/maas-{os_type}-{os_version}-user-data.txt"
+    )
+)
+
+class TestMaas(object):
+    klass = maas.MAAS
+
+    def setup_method(self):
+        config.load()
+        config.update(deepcopy(test_config))
+        self.start_patchers()
+
+    def start_patchers(self):
+        self.patchers = dict()
+        self.patchers["m_Remote_hostname"] = patch(
+            "teuthology.orchestra.remote.Remote.hostname",
+            new_callable=PropertyMock,
+        )
+        self.patchers["m_Remote_machine_os"] = patch(
+            "teuthology.orchestra.remote.Remote.os",
+            new_callable=PropertyMock,
+        )
+        self.patchers["m_Remote_opsys_os"] = patch(
+            "teuthology.orchestra.opsys.OS",
+            new_callable=PropertyMock,
+        )
+
+        self.mocks = dict()
+        for name, patcher in self.patchers.items():
+            self.mocks[name] = patcher.start()
+
+    def teardown_method(self):
+        for patcher in self.patchers.values():
+            patcher.stop()
+
+    def _get_mock_response(self, status_code=200, content=None, headers=None):
+        response = Response()
+        response.status_code = status_code
+        response._content = content
+        if headers is not None:
+            response.headers = headers
+        return response
+
+    def test_api_url_missing(self):
+        config.maas["api_url"] = None
+        with raises(RuntimeError):
+            self.klass("name.fqdn")
+
+    def test_api_key_missing(self):
+        config.maas["api_key"] = None
+        with raises(RuntimeError):
+            self.klass("name.fqdn")
+
+    @mark.parametrize("enabled", [True, False])
+    def test_get_types(self, enabled):
+        with patch("teuthology.provision.maas.enabled") as m_enabled:
+            m_enabled.return_value = enabled
+            types = maas.get_types()
+
+        if enabled:
+            assert types == test_config["maas"]["machine_types"]
+        else:
+            assert types == []
+
+    @mark.parametrize("status_name, osystem, distro_series", [
+        ("ready", "ubuntu", "jammy"),
+        ("deployed", "ubuntu", "jammy"),
+    ])
+    def test_init(self, status_name, osystem, distro_series):
+        self.mocks["m_Remote_hostname"].return_value = "name.fqdn"
+        with patch(
+            "teuthology.provision.maas.MAAS.get_machines_data"
+        ) as maas_machine:
+            maas_machine.return_value = {
+                "status_name": status_name,
+                "system_id": "abc123",
+                "osystem": osystem,
+                "distro_series": distro_series,
+                "architecture": "amd64/generic"
+            }
+            if not (osystem and distro_series):
+                with raises(RuntimeError):
+                    obj = self.klass(
+                        name="name.fqdn",
+                        os_type=osystem,
+                        os_version=distro_series
+                    )
+            else:
+                obj = self.klass(
+                    name="name.fqdn",
+                    os_type=osystem,
+                    os_version=distro_series
+                )
+                assert obj.name == "name.fqdn"
+                assert obj.shortname == "name"
+                assert obj.os_type == osystem
+                assert obj.os_version == distro_series
+                assert obj.system_id == "abc123"
+
+    @mark.parametrize("os_name, os_version, codename", [
+        ("ubuntu", "24.04", "noble"), ("centos", "9.stream", "centos9-stream"),
+    ])
+    def test_get_image_data(self, os_name, os_version, codename):
+        with patch.multiple(
+            "teuthology.provision.maas.MAAS",
+            do_request=DEFAULT,
+            get_machines_data=DEFAULT,
+        ) as local_mocks:
+            local_mocks["do_request"].return_value = self._get_mock_response(
+                content=b'[{"name": "%s/%s", "architecture": "amd64/generic"}]' % (
+                    os_name.encode(), codename.encode()
+                )
+            )
+            local_mocks["get_machines_data"].return_value = {
+                "status_name": "ready",
+                "architecture": "amd64/generic",
+            }
+            obj = self.klass(
+                name="name.fqdn", os_type=os_name, os_version=os_version
+            )
+            assert obj.get_image_data() == {
+                "name": f"{os_name}/{codename}",
+                "architecture": "amd64/generic",
+            }
+
+    def test_lock_machine(self):
+        with patch.multiple(
+            "teuthology.provision.maas.MAAS",
+            do_request=DEFAULT,
+            get_machines_data=DEFAULT,
+        ) as local_mocks:
+            local_mocks["get_machines_data"].return_value = {
+                "system_id": "1234abc",
+                "architecture": "amd64/generic",
+            }
+            local_mocks["do_request"].return_value = self._get_mock_response(
+                content=b'{"locked": "true"}'
+            )
+            assert self.klass(name="name.fqdn").lock_machine() is None
+
+    def test_unlock_machine(self):
+        self.mocks["m_Remote_hostname"].return_value = "name.fqdn"
+        with patch.multiple(
+            "teuthology.provision.maas.MAAS",
+            do_request=DEFAULT,
+            get_machines_data=DEFAULT,
+        ) as local_mocks:
+            local_mocks["get_machines_data"].return_value = {
+                "system_id": "1234abc",
+                "architecture": "amd64/generic",
+            }
+            local_mocks["do_request"].return_value = self._get_mock_response(
+                content=b'{"locked": "true"}'
+            )
+            with raises(RuntimeError):
+                self.klass(name="name.fqdn").unlock_machine()
+
+    @mark.parametrize("type, status_name", [
+        ("uploaded", ""), ("synced", "deploying"), ("synced", "ready"),
+    ])
+    def test_deploy_machine(self, type, status_name):
+        with patch.multiple(
+            "teuthology.provision.maas.MAAS",
+            get_image_data=DEFAULT,
+            _get_user_data=DEFAULT,
+            get_machines_data=DEFAULT,
+            do_request=DEFAULT,
+        ) as local_mocks:
+            local_mocks["get_machines_data"].return_value = {
+                "system_id": "1234abc",
+                "architecture": "amd64/generic",
+            }
+            local_mocks["get_image_data"].return_value = {
+                "name": "ubuntu/noble", "type": type
+            }
+            local_mocks["_get_user_data"].return_value = "init-host-data"
+            local_mocks["do_request"].return_value = self._get_mock_response(
+                content=b'{"status_name": "%s"}' % status_name.encode()
+            )
+            obj = self.klass(name="name.fqdn")
+            if (status_name != "deploying" or type != "synced"):
+                with raises(RuntimeError):
+                    obj.deploy_machine()
+            else:
+                assert obj.deploy_machine() is None
+
+    @mark.parametrize("response", [b'{}', ])
+    def test_release_machine(self, response):
+        with patch.multiple(
+            "teuthology.provision.maas.MAAS",
+            get_machines_data=DEFAULT,
+            do_request=DEFAULT,
+        ) as local_mocks:
+            local_mocks["get_machines_data"].return_value = {
+                "system_id": "1234abc",
+                "architecture": "amd64/generic",
+                "status_name": "ready"
+            }
+            local_mocks["do_request"].return_value = self._get_mock_response(
+                content=b'{"status_name": "disk erasing"}'
+            )
+            assert self.klass(name="name.fqdn").release_machine() is None
+
+    @mark.parametrize("status_name", ["deploying", "ready"])
+    def test_abort_deploy(self, status_name):
+        with patch.multiple(
+            "teuthology.provision.maas.MAAS",
+            get_machines_data=DEFAULT,
+            do_request=DEFAULT,
+        ) as local_mocks:
+            local_mocks["get_machines_data"].return_value = {
+                "system_id": "1234abc",
+                "architecture": "amd64/generic",
+            }
+            if status_name == "deploying":
+                local_mocks["do_request"].return_value = self._get_mock_response(
+                    content=b'{"status_name": "allocated"}'
+                )
+
+            assert self.klass(name="name.fqdn").abort_deploy() is None
+
+    @mark.parametrize("lock_status, machine_status", [
+        (False, "ready"), (False, "allocated"),
+        (True, "deployed"), (False, "deployed"),
+        (False, "releasing"),
+    ])
+    def test_release(self, lock_status, machine_status):
+        with patch.multiple(
+            "teuthology.provision.maas.MAAS",
+            get_machines_data=DEFAULT,
+            unlock_machine=DEFAULT,
+            release_machine=DEFAULT,
+            _wait_for_status=DEFAULT,
+        ) as local_mocks:
+            local_mocks["get_machines_data"].return_value = {
+                "status_name": machine_status,
+                "locked": lock_status,
+                "system_id": "1234abc",
+                "architecture": "amd64/generic",
+            }
+            obj = self.klass(name="name.fqdn")
+            if machine_status in ["ready", "allocated"]:
+                obj.release()
+
+            elif machine_status == "deployed":
+                local_mocks["unlock_machine"].return_value = None
+                local_mocks["release_machine"].return_value = None
+                local_mocks["_wait_for_status"].return_value = None
+                obj.release()
+
+            else:
+                with raises(RuntimeError):
+                    obj.release()
+
+    @mark.parametrize("user_data", [True, False])
+    def test_get_user_data(self, user_data):
+        with patch(
+            "teuthology.provision.maas.MAAS.get_machines_data"
+        ) as maas_machine:
+            maas_machine.return_value = {
+                "system_id": "1234abc",
+                "architecture": "amd64/generic",
+            }
+            obj = self.klass(name="name.fqdn")
+            if user_data:
+                assert user_data is not None
+            else:
+                config.maas["user_data"] = None
+                user_data = obj._get_user_data()
+                assert user_data is None
+    
+    @mark.parametrize(
+        "os_type, os_version, remote_os_type, remote_os_version", [
+        ("centos", "8", "centos", "8"),
+        ("", "", "ubuntu", "22.04")
+    ])
+    def test_verify_installed_os(
+        self, os_type, os_version, remote_os_type, remote_os_version
+    ):
+        self.mocks["m_Remote_machine_os"].return_value = OS(
+            name=remote_os_type, version=remote_os_version
+        )
+        with patch(
+            "teuthology.provision.maas.MAAS.get_machines_data"
+        ) as maas_machine:
+            maas_machine.return_value = {
+                "system_id": "1234abc",
+                "architecture": "amd64/generic"
+            }
+            if os_type and os_version:
+                self.klass(
+                    name="name.fqdn", os_type=os_type, os_version=os_version
+                )._verify_installed_os()
+            else:
+                self.klass(name="name.fqdn")._verify_installed_os()
diff --git a/tests/provision/test_pelagos.py b/tests/provision/test_pelagos.py
new file mode 100644 (file)
index 0000000..43d4741
--- /dev/null
@@ -0,0 +1,46 @@
+from copy import deepcopy
+from pytest import raises
+from teuthology.config import config
+from teuthology.provision import pelagos
+
+import teuthology.provision
+
+
+test_config = dict(
+    pelagos=dict(
+        endpoint='http://pelagos.example:5000/',
+        machine_types='ptype1,ptype2',
+    ),
+)
+
+class TestPelagos(object):
+
+    def setup_method(self):
+        config.load(deepcopy(test_config))
+
+    def teardown_method(self):
+        pass
+
+    def test_get_types(self):
+        #klass = pelagos.Pelagos
+        types = pelagos.get_types()
+        assert types == ["ptype1", "ptype2"]
+
+    def test_disabled(self):
+        config.pelagos['endpoint'] = None
+        enabled = pelagos.enabled()
+        assert enabled == False
+
+    def test_pelagos(self):
+            class context:
+                pass
+
+            ctx = context()
+            ctx.os_type ='sle'
+            ctx.os_version = '15.1'
+            with raises(Exception) as e_info:
+                teuthology.provision.reimage(ctx, 'f.q.d.n.org', 'ptype1')
+            e_str = str(e_info)
+            print("Caught exception: " +  e_str)
+            assert e_str.find(r"Name\sor\sservice\snot\sknown") == -1
+
diff --git a/tests/scripts/script.py b/tests/scripts/script.py
new file mode 100644 (file)
index 0000000..a178c8a
--- /dev/null
@@ -0,0 +1,53 @@
+import sys
+import pytest
+from importlib import import_module
+
+
+class Script(object):
+    script_name = "teuthology"
+    script_module = None  # Override in subclasses, e.g., 'scripts.run'
+
+    @pytest.fixture(scope="class")
+    def module_name(self) -> str:
+        # e.g., 'teuthology-dispatcher' -> 'scripts.dispatcher'
+        return self.script_name.replace("teuthology-", "").replace("teuthology", "run")
+
+    @pytest.fixture(scope="class")
+    def module(self, module_name):
+        return import_module(self.script_module or f"scripts.{module_name}")
+
+    def test_help(self, capsys: pytest.CaptureFixture[str], module):
+        # docopt
+        if module.__doc__ and "usage" in module.__doc__.lower():
+            return
+        if hasattr(module, "doc") and module.doc and "usage" in module.doc.lower():
+            return
+        # argparse
+        if hasattr(module, "parse_args"):
+            with pytest.raises(SystemExit):
+                module.parse_args([])
+            captured = capsys.readouterr()
+            assert "usage: " in captured.err
+            return
+        # If neither, fail
+        raise AssertionError(
+            f"{self.script_name} has neither a docstring/doc variable with usage info "
+            f"nor a parse_args function"
+        )
+
+    def test_invalid(self, module):
+        original_argv = sys.argv
+        try:
+            sys.argv = [self.script_name, "--invalid-option"]
+            with pytest.raises(SystemExit):
+                if hasattr(module, "parse_args"):
+                    module.parse_args(sys.argv[1:])
+                elif hasattr(module, "main"):
+                    # For docopt-based scripts, main() will call docopt which exits on error
+                    module.main()
+                else:
+                    raise NotImplementedError(
+                        f"Don't know how to test {self.script_name}"
+                    )
+        finally:
+            sys.argv = original_argv
diff --git a/tests/scripts/test_dispatcher_.py b/tests/scripts/test_dispatcher_.py
new file mode 100644 (file)
index 0000000..3dc1659
--- /dev/null
@@ -0,0 +1,6 @@
+from script import Script
+
+
+class TestDispatcher(Script):
+    script_name = 'teuthology-dispatcher'
+    script_module = 'scripts.dispatcher'
diff --git a/tests/scripts/test_exporter_.py b/tests/scripts/test_exporter_.py
new file mode 100644 (file)
index 0000000..0d3c171
--- /dev/null
@@ -0,0 +1,6 @@
+from script import Script
+
+
+class TestExporter(Script):
+    script_name = 'teuthology-exporter'
+    script_module = 'scripts.exporter'
diff --git a/tests/scripts/test_lock.py b/tests/scripts/test_lock.py
new file mode 100644 (file)
index 0000000..b1687c1
--- /dev/null
@@ -0,0 +1,6 @@
+from script import Script
+
+
+class TestLock(Script):
+    script_name = 'teuthology-lock'
+    script_module = 'scripts.lock'
diff --git a/tests/scripts/test_ls.py b/tests/scripts/test_ls.py
new file mode 100644 (file)
index 0000000..2965789
--- /dev/null
@@ -0,0 +1,16 @@
+import docopt
+
+from script import Script
+from scripts import ls
+
+doc = ls.__doc__
+
+
+class TestLs(Script):
+    script_name = 'teuthology-ls'
+    script_module = 'scripts.ls'
+
+    def test_args(self):
+        args = docopt.docopt(doc, ["--verbose", "some/archive/dir"])
+        assert args["--verbose"]
+        assert args["<archive_dir>"] == "some/archive/dir"
diff --git a/tests/scripts/test_prune_logs.py b/tests/scripts/test_prune_logs.py
new file mode 100644 (file)
index 0000000..1688017
--- /dev/null
@@ -0,0 +1,6 @@
+from script import Script
+
+
+class TestPruneLogs(Script):
+    script_name = 'teuthology-prune-logs'
+    script_module = 'scripts.prune_logs'
diff --git a/tests/scripts/test_report.py b/tests/scripts/test_report.py
new file mode 100644 (file)
index 0000000..40b750a
--- /dev/null
@@ -0,0 +1,6 @@
+from script import Script
+
+
+class TestReport(Script):
+    script_name = 'teuthology-report'
+    script_module = 'scripts.report'
diff --git a/tests/scripts/test_results.py b/tests/scripts/test_results.py
new file mode 100644 (file)
index 0000000..9f201b8
--- /dev/null
@@ -0,0 +1,6 @@
+from script import Script
+
+
+class TestResults(Script):
+    script_name = 'teuthology-results'
+    script_module = 'scripts.results'
diff --git a/tests/scripts/test_run.py b/tests/scripts/test_run.py
new file mode 100644 (file)
index 0000000..de33e6e
--- /dev/null
@@ -0,0 +1,46 @@
+import docopt
+
+from script import Script
+from scripts import run
+
+doc = run.__doc__
+
+
+class TestRun(Script):
+    script_name = 'teuthology'
+    script_module = 'scripts.run'
+
+    def test_all_args(self):
+        args = docopt.docopt(doc, [
+            "--verbose",
+            "--archive", "some/archive/dir",
+            "--description", "the_description",
+            "--owner", "the_owner",
+            "--lock",
+            "--machine-type", "machine_type",
+            "--os-type", "os_type",
+            "--os-version", "os_version",
+            "--block",
+            "--name", "the_name",
+            "--suite-path", "some/suite/dir",
+            "path/to/config.yml",
+        ])
+        assert args["--verbose"]
+        assert args["--archive"] == "some/archive/dir"
+        assert args["--description"] == "the_description"
+        assert args["--owner"] == "the_owner"
+        assert args["--lock"]
+        assert args["--machine-type"] == "machine_type"
+        assert args["--os-type"] == "os_type"
+        assert args["--os-version"] == "os_version"
+        assert args["--block"]
+        assert args["--name"] == "the_name"
+        assert args["--suite-path"] == "some/suite/dir"
+        assert args["<config>"] == ["path/to/config.yml"]
+
+    def test_multiple_configs(self):
+        args = docopt.docopt(doc, [
+            "config1.yml",
+            "config2.yml",
+        ])
+        assert args["<config>"] == ["config1.yml", "config2.yml"]
diff --git a/tests/scripts/test_schedule.py b/tests/scripts/test_schedule.py
new file mode 100644 (file)
index 0000000..d69c13a
--- /dev/null
@@ -0,0 +1,6 @@
+from script import Script
+
+
+class TestSchedule(Script):
+    script_name = 'teuthology-schedule'
+    script_module = 'scripts.schedule'
diff --git a/tests/scripts/test_suite.py b/tests/scripts/test_suite.py
new file mode 100644 (file)
index 0000000..f890c80
--- /dev/null
@@ -0,0 +1,6 @@
+from script import Script
+
+
+class TestSuite(Script):
+    script_name = 'teuthology-suite'
+    script_module = 'scripts.suite'
diff --git a/tests/scripts/test_supervisor_.py b/tests/scripts/test_supervisor_.py
new file mode 100644 (file)
index 0000000..dad7882
--- /dev/null
@@ -0,0 +1,6 @@
+from script import Script
+
+
+class TestSupervisor(Script):
+    script_name = 'teuthology-supervisor'
+    script_module = 'scripts.supervisor'
diff --git a/tests/scripts/test_updatekeys.py b/tests/scripts/test_updatekeys.py
new file mode 100644 (file)
index 0000000..5a38829
--- /dev/null
@@ -0,0 +1,22 @@
+from script import Script
+import docopt
+from pytest import raises
+from pytest import skip
+from scripts import updatekeys
+
+
+class TestUpdatekeys(Script):
+    script_name = 'teuthology-updatekeys'
+    script_module = 'scripts.updatekeys'
+
+    def test_invalid(self):
+        skip("teuthology.lock needs to be partially refactored to allow" +
+             "teuthology-updatekeys to return nonzero in all erorr cases")
+
+    def test_all_and_targets(self):
+        with raises(docopt.DocoptExit):
+            docopt.docopt(updatekeys.doc, ['-a', '-t', 'foo'])
+
+    def test_no_args(self):
+        with raises(docopt.DocoptExit):
+            docopt.docopt(updatekeys.doc, [])
diff --git a/tests/suite/conftest.py b/tests/suite/conftest.py
new file mode 100644 (file)
index 0000000..4285bdc
--- /dev/null
@@ -0,0 +1,4 @@
+from teuthology.config import config
+
+def pytest_runtest_setup():
+    config.load({})
diff --git a/tests/suite/suites/noop/noop.yaml b/tests/suite/suites/noop/noop.yaml
new file mode 100644 (file)
index 0000000..fb674b1
--- /dev/null
@@ -0,0 +1,7 @@
+roles:
+- - mon.a
+  - osd.0
+tasks:
+- exec:
+    mon.a:
+      - echo "Well done !"
diff --git a/tests/suite/test_build_matrix.py b/tests/suite/test_build_matrix.py
new file mode 100644 (file)
index 0000000..895c91e
--- /dev/null
@@ -0,0 +1,815 @@
+import os
+import random
+
+from mock import patch, MagicMock
+
+from teuthology.suite import build_matrix
+from tests.fake_fs import make_fake_fstools
+
+
+class TestBuildMatrixSimple(object):
+    def test_combine_path(self):
+        result = build_matrix.combine_path("/path/to/left", "right/side")
+        assert result == "/path/to/left/right/side"
+
+    def test_combine_path_no_right(self):
+        result = build_matrix.combine_path("/path/to/left", None)
+        assert result == "/path/to/left"
+
+
+class TestBuildMatrix(object):
+
+    patchpoints = [
+        'os.path.exists',
+        'os.listdir',
+        'os.path.isfile',
+        'os.path.isdir',
+        'builtins.open',
+    ]
+
+    def setup_method(self):
+        self.mocks = dict()
+        self.patchers = dict()
+        for ppoint in self.__class__.patchpoints:
+            self.mocks[ppoint] = MagicMock()
+            self.patchers[ppoint] = patch(ppoint, self.mocks[ppoint])
+
+    def start_patchers(self, fake_fs):
+        fake_fns = make_fake_fstools(fake_fs)
+        # N.B.: relies on fake_fns being in same order as patchpoints
+        for ppoint, fn in zip(self.__class__.patchpoints, fake_fns):
+            self.mocks[ppoint].side_effect = fn
+            self.patchers[ppoint].start()
+
+    def stop_patchers(self):
+        for patcher in self.patchers.values():
+            patcher.stop()
+
+    def teardown_method(self):
+        self.patchers.clear()
+        self.mocks.clear()
+
+    def fragment_occurences(self, jobs, fragment):
+        # What fraction of jobs contain fragment?
+        count = 0
+        for (description, fragment_list) in jobs:
+            for item in fragment_list:
+                if item.endswith(fragment):
+                    count += 1
+        return count / float(len(jobs))
+
+    def test_concatenate_1x2x3(self):
+        fake_fs = {
+            'd0_0': {
+                '+': None,
+                'd1_0': {
+                    'd1_0_0.yaml': None,
+                },
+                'd1_1': {
+                    'd1_1_0.yaml': None,
+                    'd1_1_1.yaml': None,
+                },
+                'd1_2': {
+                    'd1_2_0.yaml': None,
+                    'd1_2_1.yaml': None,
+                    'd1_2_2.yaml': None,
+                },
+            },
+        }
+        self.start_patchers(fake_fs)
+        try:
+            result = build_matrix.build_matrix('d0_0')
+        finally:
+            self.stop_patchers()
+        assert len(result) == 1
+
+    def test_convolve_2x2(self):
+        fake_fs = {
+            'd0_0': {
+                '%': None,
+                'd1_0': {
+                    'd1_0_0.yaml': None,
+                    'd1_0_1.yaml': None,
+                },
+                'd1_1': {
+                    'd1_1_0.yaml': None,
+                    'd1_1_1.yaml': None,
+                },
+            },
+        }
+        self.start_patchers(fake_fs)
+        try:
+            result = build_matrix.build_matrix('d0_0')
+        finally:
+            self.stop_patchers()
+        assert len(result) == 4
+        assert self.fragment_occurences(result, 'd1_1_1.yaml') == 0.5
+
+    def test_convolve_2x2x2(self):
+        fake_fs = {
+            'd0_0': {
+                '%': None,
+                'd1_0': {
+                    'd1_0_0.yaml': None,
+                    'd1_0_1.yaml': None,
+                },
+                'd1_1': {
+                    'd1_1_0.yaml': None,
+                    'd1_1_1.yaml': None,
+                },
+                'd1_2': {
+                    'd1_2_0.yaml': None,
+                    'd1_2_1.yaml': None,
+                },
+            },
+        }
+        self.start_patchers(fake_fs)
+        try:
+            result = build_matrix.build_matrix('d0_0')
+        finally:
+            self.stop_patchers()
+        assert len(result) == 8
+        assert self.fragment_occurences(result, 'd1_2_0.yaml') == 0.5
+
+    def test_convolve_1x2x4(self):
+        fake_fs = {
+            'd0_0': {
+                '%': None,
+                'd1_0': {
+                    'd1_0_0.yaml': None,
+                },
+                'd1_1': {
+                    'd1_1_0.yaml': None,
+                    'd1_1_1.yaml': None,
+                },
+                'd1_2': {
+                    'd1_2_0.yaml': None,
+                    'd1_2_1.yaml': None,
+                    'd1_2_2.yaml': None,
+                    'd1_2_3.yaml': None,
+                },
+            },
+        }
+        self.start_patchers(fake_fs)
+        try:
+            result = build_matrix.build_matrix('d0_0')
+        finally:
+            self.stop_patchers()
+        assert len(result) == 8
+        assert self.fragment_occurences(result, 'd1_2_2.yaml') == 0.25
+
+    def test_convolve_with_concat(self):
+        fake_fs = {
+            'd0_0': {
+                '%': None,
+                'd1_0': {
+                    'd1_0_0.yaml': None,
+                },
+                'd1_1': {
+                    'd1_1_0.yaml': None,
+                    'd1_1_1.yaml': None,
+                },
+                'd1_2': {
+                    '+': None,
+                    'd1_2_0.yaml': None,
+                    'd1_2_1.yaml': None,
+                    'd1_2_2.yaml': None,
+                    'd1_2_3.yaml': None,
+                },
+            },
+        }
+        self.start_patchers(fake_fs)
+        try:
+            result = build_matrix.build_matrix('d0_0')
+        finally:
+            self.stop_patchers()
+        assert len(result) == 2
+        for i in result:
+            assert 'd0_0/d1_2/d1_2_0.yaml' in i[1]
+            assert 'd0_0/d1_2/d1_2_1.yaml' in i[1]
+            assert 'd0_0/d1_2/d1_2_2.yaml' in i[1]
+            assert 'd0_0/d1_2/d1_2_3.yaml' in i[1]
+
+    def test_convolve_nested(self):
+        fake_fs = {
+            'd0_0': {
+                '%': None,
+                'd1_0': {
+                    'd1_0_0.yaml': None,
+                    '%': '2',
+                    'd1_0_1': {
+                        'd1_0_1_0.yaml': None,
+                        'd1_0_1_1.yaml': None,
+                    },
+                    'd1_0_2': {
+                        'd1_0_2_0.yaml': None,
+                        'd1_0_2_1.yaml': None,
+                    },
+                },
+                'd1_2': {
+                    'd1_2_0.yaml': None,
+                    'd1_2_1.yaml': None,
+                    'd1_2_2.yaml': None,
+                    'd1_2_3.yaml': None,
+                },
+            },
+        }
+        self.start_patchers(fake_fs)
+        try:
+            result = build_matrix.build_matrix('d0_0')
+        finally:
+            self.stop_patchers()
+        assert len(result) == 8
+        assert self.fragment_occurences(result, 'd1_0_0.yaml') == 1
+        assert self.fragment_occurences(result, 'd1_0_1_0.yaml') == 0.5
+        assert self.fragment_occurences(result, 'd1_0_1_1.yaml') == 0.5
+        assert self.fragment_occurences(result, 'd1_0_2_0.yaml') == 0.5
+        assert self.fragment_occurences(result, 'd1_0_2_1.yaml') == 0.5
+        assert self.fragment_occurences(result, 'd1_2_0.yaml') == 0.25
+        assert self.fragment_occurences(result, 'd1_2_1.yaml') == 0.25
+        assert self.fragment_occurences(result, 'd1_2_2.yaml') == 0.25
+        assert self.fragment_occurences(result, 'd1_2_3.yaml') == 0.25
+
+
+    def test_random_dollar_sign_2x2x3(self):
+        fake_fs = {
+            'd0_0': {
+                '$': None,
+                'd1_0': {
+                    'd1_0_0.yaml': None,
+                    'd1_0_1.yaml': None,
+                },
+                'd1_1': {
+                    'd1_1_0.yaml': None,
+                    'd1_1_1.yaml': None,
+                },
+                'd1_2': {
+                    'd1_2_0.yaml': None,
+                    'd1_2_1.yaml': None,
+                    'd1_2_2.yaml': None,
+                },
+            },
+        }
+        fake_fs1 = {
+            'd0_0$': {
+                'd1_0': {
+                    'd1_0_0.yaml': None,
+                    'd1_0_1.yaml': None,
+                },
+                'd1_1': {
+                    'd1_1_0.yaml': None,
+                    'd1_1_1.yaml': None,
+                },
+                'd1_2': {
+                    'd1_2_0.yaml': None,
+                    'd1_2_1.yaml': None,
+                    'd1_2_2.yaml': None,
+                },
+            },
+        }
+        self.start_patchers(fake_fs)
+        try:
+            result = build_matrix.build_matrix('d0_0')
+        finally:
+            self.stop_patchers()
+        assert len(result) == 1
+        self.start_patchers(fake_fs1)
+        try:
+            result = build_matrix.build_matrix('d0_0$')
+        finally:
+            self.stop_patchers()
+        assert len(result) == 1
+
+    def test_random_dollar_sign_with_concat(self):
+        fake_fs = {
+            'd0_0': {
+                '$': None,
+                'd1_0': {
+                    'd1_0_0.yaml': None,
+                },
+                'd1_1': {
+                    'd1_1_0.yaml': None,
+                    'd1_1_1.yaml': None,
+                },
+                'd1_2': {
+                    '+': None,
+                    'd1_2_0.yaml': None,
+                    'd1_2_1.yaml': None,
+                    'd1_2_2.yaml': None,
+                    'd1_2_3.yaml': None,
+                },
+            },
+        }
+        fake_fs1 = {
+            'd0_0$': {
+                'd1_0': {
+                    'd1_0_0.yaml': None,
+                },
+                'd1_1': {
+                    'd1_1_0.yaml': None,
+                    'd1_1_1.yaml': None,
+                },
+                'd1_2': {
+                    '+': None,
+                    'd1_2_0.yaml': None,
+                    'd1_2_1.yaml': None,
+                    'd1_2_2.yaml': None,
+                    'd1_2_3.yaml': None,
+                },
+            },
+        }
+        for fs, root in [(fake_fs,'d0_0'), (fake_fs1,'d0_0$')]:
+            self.start_patchers(fs)
+            try:
+                result = build_matrix.build_matrix(root)
+            finally:
+                self.stop_patchers()
+            assert len(result) == 1
+            if result[0][0][1:].startswith('d1_2'):
+                for i in result:
+                    assert os.path.join(root, 'd1_2/d1_2_0.yaml') in i[1]
+                    assert os.path.join(root, 'd1_2/d1_2_1.yaml') in i[1]
+                    assert os.path.join(root, 'd1_2/d1_2_2.yaml') in i[1]
+                    assert os.path.join(root, 'd1_2/d1_2_3.yaml') in i[1]
+
+    def test_random_dollar_sign_with_convolve(self):
+        fake_fs = {
+            'd0_0': {
+                '%': None,
+                'd1_0': {
+                    'd1_0_0.yaml': None,
+                    'd1_0_1.yaml': None,
+                },
+                'd1_1': {
+                    'd1_1_0.yaml': None,
+                    'd1_1_1.yaml': None,
+                },
+                'd1_2': {
+                    '$': None,
+                    'd1_2_0.yaml': None,
+                    'd1_2_1.yaml': None,
+                    'd1_2_2.yaml': None,
+                },
+            },
+        }
+        self.start_patchers(fake_fs)
+        try:
+            result = build_matrix.build_matrix('d0_0')
+        finally:
+            self.stop_patchers()
+        assert len(result) == 4
+        fake_fs1 = {
+            'd0_0': {
+                '%': None,
+                'd1_0': {
+                    'd1_0_0.yaml': None,
+                    'd1_0_1.yaml': None,
+                },
+                'd1_1': {
+                    'd1_1_0.yaml': None,
+                    'd1_1_1.yaml': None,
+                },
+                'd1_2$': {
+                    'd1_2_0.yaml': None,
+                    'd1_2_1.yaml': None,
+                    'd1_2_2.yaml': None,
+                },
+            },
+        }
+        self.start_patchers(fake_fs1)
+        try:
+            result = build_matrix.build_matrix('d0_0')
+        finally:
+            self.stop_patchers()
+        assert len(result) == 4
+
+    def test_emulate_teuthology_noceph(self):
+        fake_fs = {
+            'teuthology': {
+                'no-ceph': {
+                    '%': None,
+                    'clusters': {
+                        'single.yaml': None,
+                    },
+                    'distros': {
+                        'baremetal.yaml': None,
+                        'rhel7.0.yaml': None,
+                        'ubuntu12.04.yaml': None,
+                        'ubuntu14.04.yaml': None,
+                        'vps.yaml': None,
+                        'vps_centos6.5.yaml': None,
+                        'vps_debian7.yaml': None,
+                        'vps_rhel6.4.yaml': None,
+                        'vps_rhel6.5.yaml': None,
+                        'vps_rhel7.0.yaml': None,
+                        'vps_ubuntu14.04.yaml': None,
+                    },
+                    'tasks': {
+                        'teuthology.yaml': None,
+                    },
+                },
+            },
+        }
+        self.start_patchers(fake_fs)
+        try:
+            result = build_matrix.build_matrix('teuthology/no-ceph')
+        finally:
+            self.stop_patchers()
+        assert len(result) == 11
+        assert self.fragment_occurences(result, 'vps.yaml') == 1 / 11.0
+
+    def test_empty_dirs(self):
+        fake_fs = {
+            'teuthology': {
+                'no-ceph': {
+                    '%': None,
+                    'clusters': {
+                        'single.yaml': None,
+                    },
+                    'distros': {
+                        'baremetal.yaml': None,
+                        'rhel7.0.yaml': None,
+                        'ubuntu12.04.yaml': None,
+                        'ubuntu14.04.yaml': None,
+                        'vps.yaml': None,
+                        'vps_centos6.5.yaml': None,
+                        'vps_debian7.yaml': None,
+                        'vps_rhel6.4.yaml': None,
+                        'vps_rhel6.5.yaml': None,
+                        'vps_rhel7.0.yaml': None,
+                        'vps_ubuntu14.04.yaml': None,
+                    },
+                    'tasks': {
+                        'teuthology.yaml': None,
+                    },
+                },
+            },
+        }
+        self.start_patchers(fake_fs)
+        try:
+            result = build_matrix.build_matrix('teuthology/no-ceph')
+        finally:
+            self.stop_patchers()
+
+        fake_fs2 = {
+            'teuthology': {
+                'no-ceph': {
+                    '%': None,
+                    'clusters': {
+                        'single.yaml': None,
+                    },
+                    'distros': {
+                        'empty': {},
+                        'baremetal.yaml': None,
+                        'rhel7.0.yaml': None,
+                        'ubuntu12.04.yaml': None,
+                        'ubuntu14.04.yaml': None,
+                        'vps.yaml': None,
+                        'vps_centos6.5.yaml': None,
+                        'vps_debian7.yaml': None,
+                        'vps_rhel6.4.yaml': None,
+                        'vps_rhel6.5.yaml': None,
+                        'vps_rhel7.0.yaml': None,
+                        'vps_ubuntu14.04.yaml': None,
+                    },
+                    'tasks': {
+                        'teuthology.yaml': None,
+                    },
+                    'empty': {},
+                },
+            },
+        }
+        self.start_patchers(fake_fs2)
+        try:
+            result2 = build_matrix.build_matrix('teuthology/no-ceph')
+        finally:
+            self.stop_patchers()
+        assert len(result) == 11
+        assert len(result2) == len(result)
+
+    def test_hidden(self):
+        fake_fs = {
+            'teuthology': {
+                'no-ceph': {
+                    '%': None,
+                    '.qa': None,
+                    'clusters': {
+                        'single.yaml': None,
+                        '.qa': None,
+                    },
+                    'distros': {
+                        '.qa': None,
+                        'baremetal.yaml': None,
+                        'rhel7.0.yaml': None,
+                        'ubuntu12.04.yaml': None,
+                        'ubuntu14.04.yaml': None,
+                        'vps.yaml': None,
+                        'vps_centos6.5.yaml': None,
+                        'vps_debian7.yaml': None,
+                        'vps_rhel6.4.yaml': None,
+                        'vps_rhel6.5.yaml': None,
+                        'vps_rhel7.0.yaml': None,
+                        'vps_ubuntu14.04.yaml': None,
+                    },
+                    'tasks': {
+                        '.qa': None,
+                        'teuthology.yaml': None,
+                    },
+                    '.foo': {
+                        '.qa': None,
+                        'teuthology.yaml': None,
+                    },
+                },
+            },
+        }
+        self.start_patchers(fake_fs)
+        try:
+            result = build_matrix.build_matrix('teuthology/no-ceph')
+        finally:
+            self.stop_patchers()
+
+        fake_fs2 = {
+            'teuthology': {
+                'no-ceph': {
+                    '%': None,
+                    'clusters': {
+                        'single.yaml': None,
+                    },
+                    'distros': {
+                        'baremetal.yaml': None,
+                        'rhel7.0.yaml': None,
+                        'ubuntu12.04.yaml': None,
+                        'ubuntu14.04.yaml': None,
+                        'vps.yaml': None,
+                        'vps_centos6.5.yaml': None,
+                        'vps_debian7.yaml': None,
+                        'vps_rhel6.4.yaml': None,
+                        'vps_rhel6.5.yaml': None,
+                        'vps_rhel7.0.yaml': None,
+                        'vps_ubuntu14.04.yaml': None,
+                    },
+                    'tasks': {
+                        'teuthology.yaml': None,
+                    },
+                },
+            },
+        }
+        self.start_patchers(fake_fs2)
+        try:
+            result2 = build_matrix.build_matrix('teuthology/no-ceph')
+        finally:
+            self.stop_patchers()
+        assert len(result) == 11
+        assert len(result2) == len(result)
+
+    def test_disable_extension(self):
+        fake_fs = {
+            'teuthology': {
+                'no-ceph': {
+                    '%': None,
+                    'clusters': {
+                        'single.yaml': None,
+                    },
+                    'distros': {
+                        'baremetal.yaml': None,
+                        'rhel7.0.yaml': None,
+                        'ubuntu12.04.yaml': None,
+                        'ubuntu14.04.yaml': None,
+                        'vps.yaml': None,
+                        'vps_centos6.5.yaml': None,
+                        'vps_debian7.yaml': None,
+                        'vps_rhel6.4.yaml': None,
+                        'vps_rhel6.5.yaml': None,
+                        'vps_rhel7.0.yaml': None,
+                        'vps_ubuntu14.04.yaml': None,
+                    },
+                    'tasks': {
+                        'teuthology.yaml': None,
+                    },
+                },
+            },
+        }
+        self.start_patchers(fake_fs)
+        try:
+            result = build_matrix.build_matrix('teuthology/no-ceph')
+        finally:
+            self.stop_patchers()
+
+        fake_fs2 = {
+            'teuthology': {
+                'no-ceph': {
+                    '%': None,
+                    'clusters': {
+                        'single.yaml': None,
+                    },
+                    'distros': {
+                        'baremetal.yaml': None,
+                        'rhel7.0.yaml': None,
+                        'ubuntu12.04.yaml': None,
+                        'ubuntu14.04.yaml': None,
+                        'vps.yaml': None,
+                        'vps_centos6.5.yaml': None,
+                        'vps_debian7.yaml': None,
+                        'vps_rhel6.4.yaml': None,
+                        'vps_rhel6.5.yaml': None,
+                        'vps_rhel7.0.yaml': None,
+                        'vps_ubuntu14.04.yaml': None,
+                        'forcefilevps_ubuntu14.04.yaml.disable': None,
+                        'forcefilevps_ubuntu14.04.yaml.anotherextension': None,
+                    },
+                    'tasks': {
+                        'teuthology.yaml': None,
+                        'forcefilevps_ubuntu14.04notyaml': None,
+                    },
+                    'forcefilevps_ubuntu14.04notyaml': None,
+                    'tasks.disable': {
+                        'teuthology2.yaml': None,
+                        'forcefilevps_ubuntu14.04notyaml': None,
+                    },
+                },
+            },
+        }
+        self.start_patchers(fake_fs2)
+        try:
+            result2 = build_matrix.build_matrix('teuthology/no-ceph')
+        finally:
+            self.stop_patchers()
+        assert len(result) == 11
+        assert len(result2) == len(result)
+
+    def test_sort_order(self):
+        # This test ensures that 'ceph' comes before 'ceph-thrash' when yaml
+        # fragments are sorted.
+        fake_fs = {
+            'thrash': {
+                '%': None,
+                'ceph-thrash': {'default.yaml': None},
+                'ceph': {'base.yaml': None},
+                'clusters': {'mds-1active-1standby.yaml': None},
+                'debug': {'mds_client.yaml': None},
+                'fs': {'btrfs.yaml': None},
+                'msgr-failures': {'none.yaml': None},
+                'overrides': {'allowlist_wrongly_marked_down.yaml': None},
+                'tasks': {'cfuse_workunit_suites_fsstress.yaml': None},
+            },
+        }
+        self.start_patchers(fake_fs)
+        try:
+            result = build_matrix.build_matrix('thrash')
+        finally:
+            self.stop_patchers()
+        assert len(result) == 1
+        assert self.fragment_occurences(result, 'base.yaml') == 1
+        fragments = result[0][1]
+        assert fragments[0] == 'thrash/ceph/base.yaml'
+        assert fragments[1] == 'thrash/ceph-thrash/default.yaml'
+
+class TestSubset(object):
+    patchpoints = [
+        'os.path.exists',
+        'os.listdir',
+        'os.path.isfile',
+        'os.path.isdir',
+        'builtins.open',
+    ]
+
+    def setup_method(self):
+        self.mocks = dict()
+        self.patchers = dict()
+        for ppoint in self.__class__.patchpoints:
+            self.mocks[ppoint] = MagicMock()
+            self.patchers[ppoint] = patch(ppoint, self.mocks[ppoint])
+
+    def start_patchers(self, fake_fs):
+        fake_fns = make_fake_fstools(fake_fs)
+        # N.B.: relies on fake_fns being in same order as patchpoints
+        for ppoint, fn in zip(self.__class__.patchpoints, fake_fns):
+            self.mocks[ppoint].side_effect = fn
+            self.patchers[ppoint].start()
+
+    def stop_patchers(self):
+        for patcher in self.patchers.values():
+            patcher.stop()
+
+    def teardown_method(self):
+        self.patchers.clear()
+        self.mocks.clear()
+
+    MAX_FACETS = 10
+    MAX_FANOUT = 3
+    MAX_DEPTH = 3
+    MAX_SUBSET = 10
+    @staticmethod
+    def generate_fake_fs(max_facets, max_fanout, max_depth):
+        def yamilify(name):
+            return name + ".yaml"
+        def name_generator():
+            x = 0
+            while True:
+                yield(str(x))
+                x += 1
+        def generate_tree(
+                max_facets, max_fanout, max_depth, namegen, top=True):
+            if max_depth == 0:
+                return None
+            if max_facets == 0:
+                return None
+            items = random.choice(range(max_fanout))
+            if items == 0 and top:
+                items = 1
+            if items == 0:
+                return None
+            sub_max_facets = max_facets / items
+            tree = {}
+            for i in range(items):
+                subtree = generate_tree(
+                    sub_max_facets, max_fanout,
+                    max_depth - 1, namegen, top=False)
+                if subtree is not None:
+                    tree['d' + next(namegen)] = subtree
+                else:
+                    tree[yamilify('f' + next(namegen))] = None
+            random.choice([
+                lambda: tree.update({'%': None}),
+                lambda: None])()
+            return tree
+        return {
+            'root':  generate_tree(
+                max_facets, max_fanout, max_depth, name_generator())
+        }
+
+    @staticmethod
+    def generate_subset(maxsub):
+        divisions = random.choice(range(maxsub-1))+1
+        return (random.choice(range(divisions)), divisions)
+
+    @staticmethod
+    def generate_description_list(tree, subset):
+        mat, first, matlimit = build_matrix._get_matrix(
+            'root', subset=subset)
+        return [i[0] for i in build_matrix.generate_combinations(
+            'root', mat, first, matlimit)], mat, first, matlimit
+
+    @staticmethod
+    def verify_facets(tree, description_list, subset, mat, first, matlimit):
+        def flatten(tree):
+            for k,v in tree.items():
+                if v is None and '.yaml' in k:
+                    yield k
+                elif v is not None and '.disable' not in k:
+                    for x in flatten(v):
+                        yield x
+
+        def pptree(tree, tabs=0):
+            ret = ""
+            for k, v in tree.items():
+                if v is None:
+                    ret += ('\t'*tabs) + k.ljust(10) + "\n"
+                else:
+                    ret += ('\t'*tabs) + (k + ':').ljust(10) + "\n"
+                    ret += pptree(v, tabs+1)
+            return ret
+        def deyamlify(name):
+            if name.endswith('.yaml'):
+                return name[:-5]
+            else:
+                return name
+        for facet in (deyamlify(_) for _ in flatten(tree)):
+            found = False
+            for i in description_list:
+                if facet in i:
+                    found = True
+                    break
+            if not found:
+                print("tree\n{tree}\ngenerated list\n{desc}\n\nfrom matrix\n\n{matrix}\nsubset {subset} without facet {fac}".format(
+                    tree=pptree(tree),
+                    desc='\n'.join(description_list),
+                    subset=subset,
+                    matrix=str(mat),
+                    fac=facet))
+                all_desc = build_matrix.generate_combinations(
+                    'root',
+                    mat,
+                    0,
+                    mat.size())
+                for i, desc in zip(range(mat.size()), all_desc):
+                    if i == first:
+                        print('==========')
+                    print("{} {}".format(i, desc))
+                    if i + 1 == matlimit:
+                        print('==========')
+            assert found
+
+    def test_random(self):
+        for i in range(10000):
+            tree = self.generate_fake_fs(
+                self.MAX_FACETS,
+                self.MAX_FANOUT,
+                self.MAX_DEPTH)
+            subset = self.generate_subset(self.MAX_SUBSET)
+            self.start_patchers(tree)
+            try:
+                dlist, mat, first, matlimit = self.generate_description_list(tree, subset)
+            finally:
+                self.stop_patchers()
+            self.verify_facets(tree, dlist, subset, mat, first, matlimit)
diff --git a/tests/suite/test_init.py b/tests/suite/test_init.py
new file mode 100644 (file)
index 0000000..6e91eef
--- /dev/null
@@ -0,0 +1,267 @@
+import os
+
+from copy import deepcopy
+
+from mock import patch, Mock, DEFAULT
+
+from teuthology import suite
+from scripts.suite import main
+from teuthology.config import config
+
+import pytest
+import time
+
+from teuthology.exceptions import ScheduleFailError
+
+def get_fake_time_and_sleep():
+    # Below we set m_time.side_effect, but we also set m_time.return_value.
+    # The reason for this is that we need to store a 'fake time' that
+    # increments when m_sleep() is called; we could use any variable name we
+    # wanted for the return value, but since 'return_value' is already a
+    # standard term in mock, and since setting side_effect causes return_value
+    # to be ignored, it's safe to just reuse the name here.
+    m_time = Mock()
+    m_time.return_value = time.time()
+
+    def m_time_side_effect():
+        # Fake the slow passage of time
+        m_time.return_value += 0.1
+        return m_time.return_value
+    m_time.side_effect = m_time_side_effect
+
+    def f_sleep(seconds):
+        m_time.return_value += seconds
+    m_sleep = Mock(wraps=f_sleep)
+    return m_time, m_sleep
+
+
+def setup_module():
+    global m_time
+    global m_sleep
+    m_time, m_sleep = get_fake_time_and_sleep()
+    global patcher_time_sleep
+    patcher_time_sleep = patch.multiple(
+        'teuthology.suite.time',
+        time=m_time,
+        sleep=m_sleep,
+    )
+    patcher_time_sleep.start()
+
+
+def teardown_module():
+    patcher_time_sleep.stop()
+
+
+@patch.object(suite.ResultsReporter, 'get_jobs')
+def test_wait_success(m_get_jobs, caplog):
+    results = [
+        [{'status': 'queued', 'job_id': '2'}],
+        [],
+    ]
+    final = [
+        {'status': 'pass', 'job_id': '1',
+         'description': 'DESC1', 'log_href': 'http://URL1'},
+        {'status': 'fail', 'job_id': '2',
+         'description': 'DESC2', 'log_href': 'http://URL2'},
+        {'status': 'pass', 'job_id': '3',
+         'description': 'DESC3', 'log_href': 'http://URL3'},
+    ]
+
+    def get_jobs(name, **kwargs):
+        if kwargs['fields'] == ['job_id', 'status']:
+            return in_progress.pop(0)
+        else:
+            return final
+    m_get_jobs.side_effect = get_jobs
+    suite.Run.WAIT_PAUSE = 1
+
+    in_progress = deepcopy(results)
+    assert 0 == suite.wait('name', 1, 'http://UPLOAD_URL')
+    m_get_jobs.assert_any_call('name', fields=['job_id', 'status'])
+    assert 0 == len(in_progress)
+    assert 'fail http://UPLOAD_URL/name/2' in caplog.text
+
+    m_get_jobs.reset_mock()
+    in_progress = deepcopy(results)
+    assert 0 == suite.wait('name', 1, None)
+    m_get_jobs.assert_any_call('name', fields=['job_id', 'status'])
+    assert 0 == len(in_progress)
+    assert 'fail http://URL2' in caplog.text
+
+
+@patch.object(suite.ResultsReporter, 'get_jobs')
+def test_wait_fails(m_get_jobs):
+    results = []
+    results.append([{'status': 'queued', 'job_id': '2'}])
+    results.append([{'status': 'queued', 'job_id': '2'}])
+    results.append([{'status': 'queued', 'job_id': '2'}])
+
+    def get_jobs(name, **kwargs):
+        return results.pop(0)
+    m_get_jobs.side_effect = get_jobs
+    suite.Run.WAIT_PAUSE = 1
+    suite.Run.WAIT_MAX_JOB_TIME = 1
+    with pytest.raises(suite.WaitException):
+        suite.wait('name', 1, None)
+
+
+REPO_SHORTHAND = [
+    ['https://github.com/dude/foo', 'bar',
+     'https://github.com/dude/bar.git'],
+    ['https://github.com/dude/foo/', 'bar',
+     'https://github.com/dude/bar.git'],
+    ['https://github.com/ceph/ceph', 'ceph',
+     'https://github.com/ceph/ceph.git'],
+    ['https://github.com/ceph/ceph/', 'ceph',
+     'https://github.com/ceph/ceph.git'],
+    ['https://github.com/ceph/ceph.git', 'ceph',
+     'https://github.com/ceph/ceph.git'],
+    ['https://github.com/ceph/ceph', 'ceph-ci',
+     'https://github.com/ceph/ceph-ci.git'],
+    ['https://github.com/ceph/ceph-ci', 'ceph',
+     'https://github.com/ceph/ceph.git'],
+    ['git://git.ceph.com/ceph.git', 'ceph',
+     'git://git.ceph.com/ceph.git'],
+    ['git://git.ceph.com/ceph.git', 'ceph-ci',
+     'git://git.ceph.com/ceph-ci.git'],
+    ['git://git.ceph.com/ceph-ci.git', 'ceph',
+     'git://git.ceph.com/ceph.git'],
+    ['https://github.com/ceph/ceph.git', 'ceph/ceph-ci',
+     'https://github.com/ceph/ceph-ci.git'],
+    ['https://github.com/ceph/ceph.git', 'https://github.com/ceph/ceph-ci',
+     'https://github.com/ceph/ceph-ci'],
+    ['https://github.com/ceph/ceph.git', 'https://github.com/ceph/ceph-ci/',
+     'https://github.com/ceph/ceph-ci/'],
+    ['https://github.com/ceph/ceph.git', 'https://github.com/ceph/ceph-ci.git',
+     'https://github.com/ceph/ceph-ci.git'],
+]
+
+
+@pytest.mark.parametrize(['orig', 'shorthand', 'result'], REPO_SHORTHAND)
+def test_expand_short_repo_name(orig, shorthand, result):
+    assert suite.expand_short_repo_name(shorthand, orig) == result
+
+
+class TestSuiteMain(object):
+    def test_main(self):
+        suite_name = 'SUITE'
+        throttle = '3'
+        machine_type = 'burnupi'
+
+        def prepare_and_schedule(obj):
+            assert obj.base_config.suite == suite_name
+            assert obj.args.throttle == throttle
+
+        def fake_str(*args, **kwargs):
+            return 'fake'
+
+        def fake_bool(*args, **kwargs):
+            return True
+
+        def fake_false(*args, **kwargs):
+            return False
+
+        with patch.multiple(
+                'teuthology.suite.run.util',
+                fetch_repos=DEFAULT,
+                package_version_for_hash=fake_str,
+                git_branch_exists=fake_bool,
+                git_ls_remote=fake_str,
+                ):
+            with patch.multiple(
+                'teuthology.suite.run.Run',
+                prepare_and_schedule=prepare_and_schedule,
+            ), patch.multiple(
+                'teuthology.suite.run.os.path',
+                exists=fake_false,
+            ):
+                main([
+                    '--ceph', 'main',
+                    '--suite', suite_name,
+                    '--throttle', throttle,
+                    '--machine-type', machine_type,
+                ])
+
+    @patch('teuthology.suite.util.smtplib.SMTP')
+    def test_machine_type_multi_error(self, m_smtp):
+        config.results_email = "example@example.com"
+        with pytest.raises(ScheduleFailError) as exc:
+            main([
+                    '--ceph', 'main',
+                    '--suite', 'suite_name',
+                    '--throttle', '3',
+                    '--machine-type', 'multi',
+                    '--dry-run',
+            ])
+        assert str(exc.value) == "Scheduling failed: 'multi' is not a valid machine_type. \
+Maybe you want 'gibba,smithi,mira' or similar"
+        m_smtp.assert_not_called()
+
+    @patch('teuthology.suite.util.smtplib.SMTP')
+    def test_machine_type_none_error(self, m_smtp):
+        config.result_email = 'example@example.com'
+        with pytest.raises(ScheduleFailError) as exc:
+            main([
+                    '--ceph', 'main',
+                    '--suite', 'suite_name',
+                    '--throttle', '3',
+                    '--machine-type', 'None',
+                    '--dry-run',
+            ])
+        assert str(exc.value) == "Scheduling failed: Must specify a machine_type"
+        m_smtp.assert_not_called()
+
+    def test_schedule_suite_noverify(self):
+        suite_name = 'noop'
+        suite_dir = os.path.dirname(__file__)
+        throttle = '3'
+        machine_type = 'burnupi'
+
+        with patch.multiple(
+            'teuthology.suite.util',
+            fetch_repos=DEFAULT,
+            teuthology_schedule=DEFAULT,
+            get_arch=lambda x: 'x86_64',
+            get_gitbuilder_hash=DEFAULT,
+            git_ls_remote=lambda *args: '1234',
+            package_version_for_hash=DEFAULT,
+        ) as m:
+            m['package_version_for_hash'].return_value = 'fake-9.5'
+            config.suite_verify_ceph_hash = False
+            main([
+                '--ceph', 'main',
+                '--suite', suite_name,
+                '--suite-dir', suite_dir,
+                '--suite-relpath', '',
+                '--throttle', throttle,
+                '--machine-type', machine_type
+            ])
+            m_sleep.assert_called_with(int(throttle))
+            m['get_gitbuilder_hash'].assert_not_called()
+
+    def test_schedule_suite(self):
+        suite_name = 'noop'
+        suite_dir = os.path.dirname(__file__)
+        throttle = '3'
+        machine_type = 'burnupi'
+
+        with patch.multiple(
+            'teuthology.suite.util',
+            fetch_repos=DEFAULT,
+            teuthology_schedule=DEFAULT,
+            get_arch=lambda x: 'x86_64',
+            get_gitbuilder_hash=DEFAULT,
+            git_ls_remote=lambda *args: '12345',
+            package_version_for_hash=DEFAULT,
+        ) as m:
+            m['package_version_for_hash'].return_value = 'fake-9.5'
+            config.suite_verify_ceph_hash = True
+            main([
+                '--ceph', 'main',
+                '--suite', suite_name,
+                '--suite-dir', suite_dir,
+                '--suite-relpath', '',
+                '--throttle', throttle,
+                '--machine-type', machine_type
+            ])
+            m_sleep.assert_called_with(int(throttle))
diff --git a/tests/suite/test_matrix.py b/tests/suite/test_matrix.py
new file mode 100644 (file)
index 0000000..596bb37
--- /dev/null
@@ -0,0 +1,82 @@
+from teuthology.suite import matrix
+
+
+def verify_matrix_output_diversity(res):
+    """
+    Verifies that the size of the matrix passed matches the number of unique
+    outputs from res.index
+    """
+    sz = res.size()
+    s = frozenset([matrix.generate_lists(res.index(i)) for i in range(sz)])
+    for i in range(res.size()):
+        assert sz == len(s)
+
+
+def mbs(num, l):
+    return matrix.Sum(num*10, [matrix.Base(i + (100*num)) for i in l])
+
+
+class TestMatrix(object):
+    def test_simple(self):
+        verify_matrix_output_diversity(mbs(1, range(6)))
+
+    def test_simple2(self):
+        verify_matrix_output_diversity(mbs(1, range(5)))
+
+    # The test_product* tests differ by the degree by which dimension
+    # sizes share prime factors
+    def test_product_simple(self):
+        verify_matrix_output_diversity(
+            matrix.Product(1, [mbs(1, range(6)), mbs(2, range(2))]))
+
+    def test_product_3_facets_2_prime_factors(self):
+        verify_matrix_output_diversity(matrix.Product(1, [
+                    mbs(1, range(6)),
+                    mbs(2, range(2)),
+                    mbs(3, range(3)),
+                    ]))
+
+    def test_product_3_facets_2_prime_factors_one_larger(self):
+        verify_matrix_output_diversity(matrix.Product(1, [
+                    mbs(1, range(2)),
+                    mbs(2, range(5)),
+                    mbs(4, range(4)),
+                    ]))
+
+    def test_product_4_facets_2_prime_factors(self):
+        verify_matrix_output_diversity(matrix.Sum(1, [
+                    mbs(1, range(6)),
+                    mbs(3, range(3)),
+                    mbs(2, range(2)),
+                    mbs(4, range(9)),
+                    ]))
+
+    def test_product_2_facets_2_prime_factors(self):
+        verify_matrix_output_diversity(matrix.Sum(1, [
+                    mbs(1, range(2)),
+                    mbs(2, range(5)),
+                    ]))
+
+    def test_product_with_sum(self):
+        verify_matrix_output_diversity(matrix.Sum(
+                9,
+                [
+                    mbs(10, range(6)),
+                    matrix.Product(1, [
+                            mbs(1, range(2)),
+                            mbs(2, range(5)),
+                            mbs(4, range(4))]),
+                    matrix.Product(8, [
+                            mbs(7, range(2)),
+                            mbs(6, range(5)),
+                            mbs(5, range(4))])
+                    ]
+                ))
+
+    def test_product_with_pick_random(self):
+        verify_matrix_output_diversity(matrix.PickRandom(1, [
+                    mbs(1, range(6)),
+                    mbs(3, range(3)),
+                    mbs(2, range(2)),
+                    mbs(4, range(9)),
+                    ]))
diff --git a/tests/suite/test_merge.py b/tests/suite/test_merge.py
new file mode 100644 (file)
index 0000000..6ed3890
--- /dev/null
@@ -0,0 +1,231 @@
+import logging
+from textwrap import dedent
+
+from mock import patch, MagicMock
+
+from teuthology.suite import build_matrix
+from teuthology.suite.merge import config_merge
+from tests.fake_fs import make_fake_fstools
+
+log = logging.getLogger(__name__)
+
+class TestMerge:
+    patchpoints = [
+        'os.path.exists',
+        'os.listdir',
+        'os.path.isfile',
+        'os.path.isdir',
+        'builtins.open',
+    ]
+
+    def setup_method(self):
+        self.mocks = dict()
+        self.patchers = dict()
+        for ppoint in self.__class__.patchpoints:
+            self.mocks[ppoint] = MagicMock()
+            self.patchers[ppoint] = patch(ppoint, self.mocks[ppoint])
+
+    def start_patchers(self, fake_fs):
+        fake_fns = make_fake_fstools(fake_fs)
+        # N.B.: relies on fake_fns being in same order as patchpoints
+        for ppoint, fn in zip(self.__class__.patchpoints, fake_fns):
+            self.mocks[ppoint].side_effect = fn
+            self.patchers[ppoint].start()
+
+    def stop_patchers(self):
+        for patcher in self.patchers.values():
+            patcher.stop()
+
+    def teardown_method(self):
+        self.patchers.clear()
+        self.mocks.clear()
+
+    def test_premerge(self):
+        fake_fs = {
+            'd0_0': {
+                '%': None,
+                'd1_0': {
+                  'a.yaml': dedent("""
+                  teuthology:
+                    premerge: reject()
+                  foo: bar
+                  """),
+                },
+                'c.yaml': dedent("""
+                top: pot
+                """),
+            },
+        }
+        self.start_patchers(fake_fs)
+        try:
+            result = build_matrix.build_matrix('d0_0')
+            assert 1 == len(result)
+            configs = list(config_merge(result))
+            assert 1 == len(configs)
+            desc, frags, yaml = configs[0]
+            assert "top" in yaml
+            assert "foo" not in yaml
+        finally:
+            self.stop_patchers()
+
+    def test_postmerge(self):
+        fake_fs = {
+            'd0_0': {
+                '%': None,
+                'd1_0': {
+                  'a.yaml': dedent("""
+                  teuthology:
+                    postmerge:
+                      - reject()
+                  foo: bar
+                  """),
+                  'b.yaml': dedent("""
+                  baz: zab
+                  """),
+                },
+                'c.yaml': dedent("""
+                top: pot
+                """),
+            },
+        }
+        self.start_patchers(fake_fs)
+        try:
+            result = build_matrix.build_matrix('d0_0')
+            assert 2 == len(result)
+            configs = list(config_merge(result))
+            assert 1 == len(configs)
+            desc, frags, yaml = configs[0]
+            assert "top" in yaml
+            assert "baz" in yaml
+            assert "foo" not in yaml
+        finally:
+            self.stop_patchers()
+
+    def test_postmerge_concat(self):
+        fake_fs = {
+            'd0_0': {
+                '%': None,
+                'd1_0': {
+                  'a.yaml': dedent("""
+                  teuthology:
+                    postmerge:
+                      - local a = 1
+                  foo: bar
+                  """),
+                  'b.yaml': dedent("""
+                  teuthology:
+                    postmerge:
+                      - local a = 2
+                  baz: zab
+                  """),
+                },
+                'z.yaml': dedent("""
+                 teuthology:
+                   postmerge:
+                     - if a == 1 then reject() end
+                 top: pot
+                 """),
+            },
+        }
+        self.start_patchers(fake_fs)
+        try:
+            result = build_matrix.build_matrix('d0_0')
+            assert 2 == len(result)
+            configs = list(config_merge(result))
+            assert 1 == len(configs)
+            desc, frags, yaml = configs[0]
+            assert "top" in yaml
+            assert "baz" in yaml
+            assert "foo" not in yaml
+        finally:
+            self.stop_patchers()
+
+
+    def test_yaml_mutation(self):
+        fake_fs = {
+            'd0_0': {
+                '%': None,
+                'c.yaml': dedent("""
+                teuthology:
+                  postmerge:
+                    - |
+                      yaml["test"] = py_dict()
+                top: pot
+                """),
+            },
+        }
+        self.start_patchers(fake_fs)
+        try:
+            result = build_matrix.build_matrix('d0_0')
+            assert 1 == len(result)
+            configs = list(config_merge(result))
+            assert 1 == len(configs)
+            desc, frags, yaml = configs[0]
+            assert "test" in yaml
+            assert {} == yaml["test"]
+        finally:
+            self.stop_patchers()
+
+    def test_sandbox(self):
+        fake_fs = {
+            'd0_0': {
+                '%': None,
+                'c.yaml': dedent("""
+                teuthology:
+                  postmerge:
+                    - |
+                      log.debug(_ENV)
+                      log.debug("_ENV contains:")
+                      for k,v in pairs(_ENV) do
+                        log.debug("_ENV['%s'] = %s", tostring(k), tostring(v))
+                      end
+                      local check = {
+                        "assert",
+                        "error",
+                        "ipairs",
+                        "next",
+                        "pairs",
+                        "tonumber",
+                        "tostring",
+                        "py_attrgetter",
+                        "py_dict",
+                        "py_list",
+                        "py_tuple",
+                        "py_enumerate",
+                        "py_iterex",
+                        "py_itemgetter",
+                        "math",
+                        "reject",
+                        "accept",
+                        "deep_merge",
+                        "log",
+                        "reject",
+                        "yaml_load",
+                      }
+                      for _,v in ipairs(check) do
+                        log.debug("checking %s", tostring(v))
+                        assert(_ENV[v])
+                      end
+                      local block = {
+                        "coroutine",
+                        "debug",
+                        "io",
+                        "os",
+                        "package",
+                      }
+                      for _,v in ipairs(block) do
+                        log.debug("checking %s", tostring(v))
+                        assert(_ENV[v] == nil)
+                      end
+                top: pot
+                """),
+            },
+        }
+        self.start_patchers(fake_fs)
+        try:
+            result = build_matrix.build_matrix('d0_0')
+            assert 1 == len(result)
+            configs = list(config_merge(result))
+            assert 1 == len(configs)
+        finally:
+            self.stop_patchers()
diff --git a/tests/suite/test_placeholder.py b/tests/suite/test_placeholder.py
new file mode 100644 (file)
index 0000000..31b5175
--- /dev/null
@@ -0,0 +1,57 @@
+from teuthology.suite.placeholder import (
+    substitute_placeholders, dict_templ, Placeholder
+)
+
+
+class TestPlaceholder(object):
+    def test_substitute_placeholders(self):
+        suite_hash = 'suite_hash'
+        input_dict = dict(
+            suite='suite',
+            suite_branch='suite_branch',
+            suite_hash=suite_hash,
+            ceph_branch='ceph_branch',
+            ceph_hash='ceph_hash',
+            teuthology_branch='teuthology_branch',
+            teuthology_sha1='teuthology_sha1',
+            machine_type='machine_type',
+            distro='distro',
+            distro_version='distro_version',
+            archive_upload='archive_upload',
+            archive_upload_key='archive_upload_key',
+            suite_repo='https://example.com/ceph/suite.git',
+            suite_relpath='',
+            ceph_repo='https://example.com/ceph/ceph.git',
+            flavor='default',
+            expire='expire',
+        )
+        output_dict = substitute_placeholders(dict_templ, input_dict)
+        assert output_dict['suite'] == 'suite'
+        assert output_dict['suite_sha1'] == suite_hash
+        assert isinstance(dict_templ['suite'], Placeholder)
+        assert isinstance(
+            dict_templ['overrides']['admin_socket']['branch'],
+            Placeholder)
+
+    def test_null_placeholders_dropped(self):
+        input_dict = dict(
+            suite='suite',
+            suite_branch='suite_branch',
+            suite_hash='suite_hash',
+            ceph_branch='ceph_branch',
+            ceph_hash='ceph_hash',
+            teuthology_branch='teuthology_branch',
+            teuthology_sha1='teuthology_sha1',
+            machine_type='machine_type',
+            archive_upload='archive_upload',
+            archive_upload_key='archive_upload_key',
+            distro=None,
+            distro_version=None,
+            suite_repo='https://example.com/ceph/suite.git',
+            suite_relpath='',
+            ceph_repo='https://example.com/ceph/ceph.git',
+            flavor=None,
+            expire='expire',
+        )
+        output_dict = substitute_placeholders(dict_templ, input_dict)
+        assert 'os_type' not in output_dict
diff --git a/tests/suite/test_run_.py b/tests/suite/test_run_.py
new file mode 100644 (file)
index 0000000..d1c8a06
--- /dev/null
@@ -0,0 +1,642 @@
+import logging
+import os
+import pytest
+import requests
+import contextlib
+import yaml
+
+from datetime import datetime, timedelta, timezone
+from mock import patch, call, ANY
+from io import StringIO
+from io import BytesIO
+
+from teuthology.config import config, YamlConfig
+from teuthology.exceptions import ScheduleFailError
+from teuthology.suite import run
+from teuthology.util.time import TIMESTAMP_FMT
+
+log = logging.getLogger(__name__)
+
+class TestRun(object):
+    klass = run.Run
+
+    def setup_method(self):
+        self.args_dict = dict(
+            suite='suite',
+            suite_branch='suite_branch',
+            suite_relpath='',
+            ceph_branch='ceph_branch',
+            ceph_sha1='ceph_sha1',
+            email='address@example.com',
+            teuthology_branch='teuthology_branch',
+            kernel_branch=None,
+            flavor='flavor',
+            distro='ubuntu',
+            machine_type='machine_type',
+            base_yaml_paths=list(),
+        )
+        self.args = YamlConfig.from_dict(self.args_dict)
+
+    @patch('teuthology.suite.run.util.fetch_repos')
+    @patch('teuthology.suite.run.util.git_ls_remote')
+    @patch('teuthology.suite.run.util.git_validate_sha1')
+    def test_email_addr(self, m_git_validate_sha1,
+                        m_git_ls_remote, _):
+        # neuter choose_X_branch
+        m_git_validate_sha1.return_value = self.args_dict['ceph_sha1']
+        self.args_dict['teuthology_branch'] = 'main'
+        self.args_dict['suite_branch'] = 'main'
+        m_git_ls_remote.return_value = 'suite_sha1'
+
+        runobj = self.klass(self.args)
+        assert runobj.base_config.email == self.args_dict['email']
+
+    @patch('teuthology.suite.run.util.fetch_repos')
+    def test_name(self, m_fetch_repos):
+        stamp = datetime.now().strftime(TIMESTAMP_FMT)
+        with patch.object(run.Run, 'create_initial_config',
+                          return_value=run.JobConfig()):
+            name = run.Run(self.args).name
+        assert str(stamp) in name
+
+    @patch('teuthology.suite.run.util.fetch_repos')
+    def test_name_owner(self, m_fetch_repos):
+        self.args.owner = 'USER'
+        with patch.object(run.Run, 'create_initial_config',
+                          return_value=run.JobConfig()):
+            name = run.Run(self.args).name
+        assert name.startswith('USER-')
+
+    @patch('teuthology.suite.run.util.git_branch_exists')
+    @patch('teuthology.suite.run.util.package_version_for_hash')
+    @patch('teuthology.suite.run.util.git_ls_remote')
+    def test_branch_nonexistent(
+        self,
+        m_git_ls_remote,
+        m_package_version_for_hash,
+        m_git_branch_exists,
+    ):
+        config.gitbuilder_host = 'example.com'
+        m_git_ls_remote.side_effect = [
+            # First call will be for the ceph hash
+            None,
+            # Second call will be for the suite hash
+            'suite_hash',
+        ]
+        m_package_version_for_hash.return_value = 'a_version'
+        m_git_branch_exists.return_value = True
+        self.args.ceph_branch = 'ceph_sha1'
+        self.args.ceph_sha1 = None
+        with pytest.raises(ScheduleFailError):
+            self.klass(self.args)
+
+    @pytest.mark.parametrize(
+        ["expire", "delta", "result"],
+        [
+            [None, timedelta(), False],
+            ["1m", timedelta(), True],
+            ["1m", timedelta(minutes=-2), False],
+            ["1m", timedelta(minutes=2), True],
+            ["7d", timedelta(days=-14), False],
+        ]
+    )
+    @patch('teuthology.repo_utils.fetch_repo')
+    @patch('teuthology.suite.run.util.git_branch_exists')
+    @patch('teuthology.suite.run.util.package_version_for_hash')
+    @patch('teuthology.suite.run.util.git_ls_remote')
+    def test_get_expiration(
+        self,
+        m_git_ls_remote,
+        m_package_version_for_hash,
+        m_git_branch_exists,
+        m_fetch_repo,
+        expire,
+        delta,
+        result,
+    ):
+        m_git_ls_remote.side_effect = 'hash'
+        m_package_version_for_hash.return_value = 'a_version'
+        m_git_branch_exists.return_value = True
+        self.args.expire = expire
+        obj = self.klass(self.args)
+        now = datetime.now(timezone.utc)
+        expires_result = obj.get_expiration(_base_time=now + delta)
+        if expire is None:
+            assert expires_result is None
+            assert obj.base_config['expire'] is None
+        else:
+            assert expires_result is not None
+            assert (now < expires_result) is result
+            assert obj.base_config['expire']
+
+    @patch('teuthology.suite.run.util.fetch_repos')
+    @patch('requests.head')
+    @patch('teuthology.suite.run.util.git_branch_exists')
+    @patch('teuthology.suite.run.util.package_version_for_hash')
+    @patch('teuthology.suite.run.util.git_ls_remote')
+    def test_sha1_exists(
+        self,
+        m_git_ls_remote,
+        m_package_version_for_hash,
+        m_git_branch_exists,
+        m_requests_head,
+        m_fetch_repos,
+    ):
+        config.gitbuilder_host = 'example.com'
+        m_package_version_for_hash.return_value = 'ceph_hash'
+        m_git_branch_exists.return_value = True
+        resp = requests.Response()
+        resp.reason = 'OK'
+        resp.status_code = 200
+        m_requests_head.return_value = resp
+        # only one call to git_ls_remote in this case
+        m_git_ls_remote.return_value = "suite_branch"
+        run = self.klass(self.args)
+        assert run.base_config.sha1 == 'ceph_sha1'
+        assert run.base_config.branch == 'ceph_branch'
+
+    @patch('teuthology.suite.run.util.git_ls_remote')
+    @patch('requests.head')
+    @patch('teuthology.suite.util.git_branch_exists')
+    @patch('teuthology.suite.util.package_version_for_hash')
+    def test_sha1_nonexistent(
+        self,
+        m_git_ls_remote,
+        m_package_version_for_hash,
+        m_git_branch_exists,
+        m_requests_head,
+    ):
+        config.gitbuilder_host = 'example.com'
+        m_package_version_for_hash.return_value = 'ceph_hash'
+        m_git_branch_exists.return_value = True
+        resp = requests.Response()
+        resp.reason = 'Not Found'
+        resp.status_code = 404
+        m_requests_head.return_value = resp
+        self.args.ceph_sha1 = 'ceph_hash_dne'
+        with pytest.raises(ScheduleFailError):
+            self.klass(self.args)
+
+    @patch('teuthology.suite.util.smtplib.SMTP')
+    @patch('teuthology.suite.util.git_ls_remote')
+    @patch('teuthology.suite.util.package_version_for_hash')
+    def test_teuthology_branch_nonexistent(
+        self,
+        m_pvfh,
+        m_git_ls_remote,
+        m_smtp,
+    ):
+        m_git_ls_remote.return_value = None
+        config.teuthology_path = None
+        config.results_email = "example@example.com"
+        self.args.dry_run = True
+        self.args.teuthology_branch = 'no_branch'
+        with pytest.raises(ScheduleFailError):
+            self.klass(self.args)
+        m_smtp.assert_not_called()
+
+    @patch('teuthology.suite.run.util.fetch_repos')
+    @patch('teuthology.suite.util.git_ls_remote')
+    @patch('teuthology.suite.run.util.package_version_for_hash')
+    def test_os_type(self, m_pvfh, m_git_ls_remote, m_fetch_repos):
+        m_git_ls_remote.return_value = "sha1"
+        del self.args['distro']
+        run_ = run.Run(self.args)
+        run_.base_args = run_.build_base_args()
+        run_.base_config = run_.build_base_config()
+        configs = [
+            ["desc", [], {"os_type": "debian", "os_version": "8.0"}],
+            ["desc", [], {"os_type": "ubuntu", "os_version": "24.0"}],
+        ]
+        missing, to_schedule = run_.collect_jobs('x86_64', configs, False, False)
+        assert to_schedule[0]['yaml']['os_type'] == "debian"
+        assert to_schedule[0]['yaml']['os_version'] == "8.0"
+        assert to_schedule[1]['yaml']['os_type'] == "ubuntu"
+        assert to_schedule[1]['yaml']['os_version'] == "24.0"
+
+    @patch('teuthology.suite.run.util.fetch_repos')
+    @patch('teuthology.suite.util.git_ls_remote')
+    @patch('teuthology.suite.run.util.package_version_for_hash')
+    def test_sha1(self, m_pvfh, m_git_ls_remote, m_fetch_repos):
+        m_git_ls_remote.return_value = "sha1"
+        del self.args['distro']
+        run_ = run.Run(self.args)
+        run_.base_args = run_.build_base_args()
+        for i in range(5): # mock backtracking
+            run_.config_input['ceph_hash'] = f"boo{i}"
+            run_.config_input['suite_hash'] = f"bar{i}"
+            run_.base_config = run_.build_base_config()
+        configs = [
+            ["desc", [], {"os_type": "debian", "os_version": "8.0", 
+                          "sha1": "old_sha", "suite_sha1": "old_sha",
+                          "overrides": { "workunit": {"sha1": "old_sha"}, "ceph": {"sha1": "old_sha"} }
+                          }],
+        ]
+        missing, to_schedule = run_.collect_jobs('x86_64', configs, False, False)
+        assert to_schedule[0]['yaml']['sha1'] == "boo4"
+        assert to_schedule[0]['yaml']['suite_sha1'] == "bar4"
+        assert to_schedule[0]['yaml']['overrides']['workunit']["sha1"] == "bar4"
+        assert to_schedule[0]['yaml']['overrides']['ceph']["sha1"] == "boo4"
+
+class TestScheduleSuite(object):
+    klass = run.Run
+
+    def setup_method(self):
+        self.args_dict = dict(
+            suite='suite',
+            suite_relpath='',
+            suite_dir='suite_dir',
+            suite_branch='main',
+            suite_repo='ceph',
+            ceph_repo='ceph',
+            ceph_branch='main',
+            ceph_sha1='ceph_sha1',
+            teuthology_branch='main',
+            kernel_branch=None,
+            flavor='flavor',
+            distro='ubuntu',
+            distro_version='14.04',
+            machine_type='machine_type',
+            base_yaml_paths=list(),
+        )
+        self.args = YamlConfig.from_dict(self.args_dict)
+
+    @patch('teuthology.suite.run.Run.schedule_jobs')
+    @patch('teuthology.suite.run.Run.write_rerun_memo')
+    @patch('teuthology.suite.util.get_install_task_flavor')
+    @patch('teuthology.suite.merge.open')
+    @patch('teuthology.suite.run.build_matrix')
+    @patch('teuthology.suite.util.git_ls_remote')
+    @patch('teuthology.suite.util.package_version_for_hash')
+    @patch('teuthology.suite.util.git_validate_sha1')
+    @patch('teuthology.suite.util.get_arch')
+    def test_successful_schedule(
+        self,
+        m_get_arch,
+        m_git_validate_sha1,
+        m_package_version_for_hash,
+        m_git_ls_remote,
+        m_build_matrix,
+        m_open,
+        m_get_install_task_flavor,
+        m_write_rerun_memo,
+        m_schedule_jobs,
+    ):
+        m_get_arch.return_value = 'x86_64'
+        m_git_validate_sha1.return_value = self.args.ceph_sha1
+        m_package_version_for_hash.return_value = 'ceph_version'
+        m_git_ls_remote.return_value = 'suite_hash'
+        build_matrix_desc = 'desc'
+        build_matrix_frags = ['frag1.yml', 'frag2.yml']
+        build_matrix_output = [
+            (build_matrix_desc, build_matrix_frags),
+        ]
+        m_build_matrix.return_value = build_matrix_output
+        frag1_read_output = 'field1: val1'
+        frag2_read_output = 'field2: val2'
+        m_open.side_effect = [
+            StringIO(frag1_read_output),
+            StringIO(frag2_read_output),
+            contextlib.closing(BytesIO())
+        ]
+        m_get_install_task_flavor.return_value = 'default'
+        m_package_version_for_hash.return_value = "v1"
+        # schedule_jobs() is just neutered; check calls below
+
+        self.args.newest = 0
+        self.args.num = 42
+        runobj = self.klass(self.args)
+        runobj.base_args = list()
+        count = runobj.schedule_suite()
+        assert(count == 1)
+        assert runobj.base_config['suite_sha1'] == 'suite_hash'
+        m_package_version_for_hash.assert_has_calls(
+            [call('ceph_sha1', 'default', 'ubuntu', '14.04', 'machine_type')],
+        )
+        y = {
+          'field1': 'val1',
+          'field2': 'val2'
+        }
+        teuthology_keys = [
+          'branch',
+          'machine_type',
+          'name',
+          'os_type',
+          'os_version',
+          'overrides',
+          'priority',
+          'repo',
+          'seed',
+          'sha1',
+          'sleep_before_teardown',
+          'suite',
+          'suite_branch',
+          'suite_relpath',
+          'suite_repo',
+          'suite_sha1',
+          'tasks',
+          'teuthology_branch',
+          'teuthology_sha1',
+          'timestamp',
+          'user',
+          'teuthology',
+          'flavor',
+        ]
+        for t in teuthology_keys:
+            y[t] = ANY
+        expected_job = dict(
+            yaml=y,
+            sha1='ceph_sha1',
+            args=[
+                '--num',
+                '42',
+                '--description',
+                os.path.join(self.args.suite, build_matrix_desc),
+                '--',
+                '-'
+            ],
+            stdin=ANY,
+            desc=os.path.join(self.args.suite, build_matrix_desc),
+        )
+
+        m_schedule_jobs.assert_has_calls(
+            [call([], [expected_job], runobj.name)],
+        )
+        args = m_schedule_jobs.call_args.args
+        log.debug("args =\n%s", args)
+        jobargs  = args[1][0]
+        stdin_yaml = yaml.safe_load(jobargs['stdin'])
+        for k in y:
+            assert y[k] == stdin_yaml[k]
+        for k in teuthology_keys:
+            assert k in stdin_yaml
+        m_write_rerun_memo.assert_called_once_with()
+
+    @patch('teuthology.suite.util.find_git_parents')
+    @patch('teuthology.suite.run.Run.schedule_jobs')
+    @patch('teuthology.suite.util.get_install_task_flavor')
+    @patch('teuthology.suite.run.config_merge')
+    @patch('teuthology.suite.run.build_matrix')
+    @patch('teuthology.suite.util.git_ls_remote')
+    @patch('teuthology.suite.util.package_version_for_hash')
+    @patch('teuthology.suite.util.git_validate_sha1')
+    @patch('teuthology.suite.util.get_arch')
+    def test_newest_failure(
+        self,
+        m_get_arch,
+        m_git_validate_sha1,
+        m_package_version_for_hash,
+        m_git_ls_remote,
+        m_build_matrix,
+        m_config_merge,
+        m_get_install_task_flavor,
+        m_schedule_jobs,
+        m_find_git_parents,
+    ):
+        m_get_arch.return_value = 'x86_64'
+        m_git_validate_sha1.return_value = self.args.ceph_sha1
+        m_package_version_for_hash.return_value = None
+        m_git_ls_remote.return_value = 'suite_hash'
+        build_matrix_desc = 'desc'
+        build_matrix_frags = ['frag.yml']
+        build_matrix_output = [
+            (build_matrix_desc, build_matrix_frags),
+        ]
+        m_build_matrix.return_value = build_matrix_output
+        m_config_merge.return_value = [(a, b, {}) for a, b in build_matrix_output]
+        m_get_install_task_flavor.return_value = 'default'
+
+        m_find_git_parents.side_effect = lambda proj, sha1, count: [f"{sha1}_{i}" for i in range(11)]
+
+        self.args.newest = 10
+        runobj = self.klass(self.args)
+        runobj.base_args = list()
+        with pytest.raises(ScheduleFailError) as exc:
+            runobj.schedule_suite()
+        assert 'Exceeded 10 backtracks' in str(exc.value)
+        m_find_git_parents.assert_has_calls(
+            [call('ceph', 'ceph_sha1', 10)]
+        )
+
+    @patch('teuthology.suite.util.find_git_parents')
+    @patch('teuthology.suite.run.Run.schedule_jobs')
+    @patch('teuthology.suite.run.Run.write_rerun_memo')
+    @patch('teuthology.suite.util.get_install_task_flavor')
+    @patch('teuthology.suite.run.config_merge')
+    @patch('teuthology.suite.run.build_matrix')
+    @patch('teuthology.suite.util.git_ls_remote')
+    @patch('teuthology.suite.util.package_version_for_hash')
+    @patch('teuthology.suite.util.git_validate_sha1')
+    @patch('teuthology.suite.util.get_arch')
+    def test_newest_success_same_branch_same_repo(
+        self,
+        m_get_arch,
+        m_git_validate_sha1,
+        m_package_version_for_hash,
+        m_git_ls_remote,
+        m_build_matrix,
+        m_config_merge,
+        m_get_install_task_flavor,
+        m_write_rerun_memo,
+        m_schedule_jobs,
+        m_find_git_parents,
+    ):
+        """
+        Test that we can successfully schedule a job with newest
+        backtracking when the ceph and suite branches are the same
+        and the ceph_sha1 is not supplied. We should expect that the
+        ceph_hash and suite_hash will be updated to the working sha1
+        """
+        m_get_arch.return_value = 'x86_64'
+        # rig has_packages_for_distro to fail this many times, so
+        # everything will run NUM_FAILS+1 times
+        NUM_FAILS = 5
+        # Here we just assume that even fi ceph_sha1 is not supplied,
+        # in git_valid_sha1, util.git_ls_remote will give us ceph_sha1
+        m_git_validate_sha1.return_value = self.args.ceph_sha1
+        # Here we know that in create_initial_config, we call
+        # git_ls_remote 3 times, choose_ceph_hash, choose_suite_hash,
+        # and choose_teuthology_branch
+        sha1_side_effect = [
+            self.args.ceph_sha1,  # ceph_sha1
+            'suite_sha1',        # suite_sha1
+            'teuthology_sha1',   # teuthology_sha1
+        ]
+        m_git_ls_remote.side_effect = sha1_side_effect
+        build_matrix_desc = 'desc'
+        build_matrix_frags = ['frag.yml']
+        build_matrix_output = [
+            (build_matrix_desc, build_matrix_frags),
+        ]
+        m_build_matrix.return_value = build_matrix_output
+        m_config_merge.return_value = [(a, b, {}) for a, b in build_matrix_output]
+        m_get_install_task_flavor.return_value = 'default'
+
+        # Generate backtracked parent sha1s
+        parent_sha1s = [f"ceph_sha1_{i}" for i in range(NUM_FAILS)]
+        assert len(parent_sha1s)
+        # Last sha1 will be the one that works!
+        working_sha1 = parent_sha1s[-1]
+
+        # NUM_FAILS attempts, then success on the last parent sha1
+        m_package_version_for_hash.side_effect = \
+            [None for i in range(NUM_FAILS)] + ["ceph_version"]
+
+        m_find_git_parents.return_value = parent_sha1s
+
+        self.args.newest = 10
+        runobj = self.klass(self.args)
+        runobj.base_args = list()
+
+        # Call schedule_suite()
+        count = runobj.schedule_suite()
+        # Epect only 1 job to be scheduled
+        assert count == 1
+        # Expect that we called package_version_for_hash NUM_FAILS times + 1 for the working sha1
+        m_package_version_for_hash.assert_has_calls(
+            [call(self.args.ceph_sha1, 'default', 'ubuntu', '14.04', 'machine_type')] +
+            [call(f"ceph_sha1_{i}", 'default', 'ubuntu', '14.04', 'machine_type')
+             for i in range(0, NUM_FAILS)]
+        )
+        # (ceph, base_config.sha1, newest) called once to get grab the backtrace
+        m_find_git_parents.assert_called_once_with('ceph', 'ceph_sha1', 10)
+
+        # Verify that base_config was updated with the working SHA1
+        assert runobj.base_config.sha1 == working_sha1
+
+        # Verify that config_input's ceph_hash and suite_hash was updated
+        assert runobj.config_input['ceph_hash'] == working_sha1
+        assert runobj.config_input['suite_hash'] == working_sha1
+
+        # Verify that config_input's ceph_hash and suite_hash are not the same as the original sha1s
+        assert runobj.config_input['ceph_hash'] != sha1_side_effect[0]  # ceph_sha1
+        assert runobj.config_input['suite_hash'] != sha1_side_effect[1]  # suite_sha1
+
+        # Verify the sha1 in scheduled jobs
+        args = m_schedule_jobs.call_args.args
+        scheduled_jobs = args[1]
+
+        # Check each job has the correct SHA1
+        for job in scheduled_jobs:
+            assert job['sha1'] == working_sha1
+
+            # Parse YAML from stdin to check for sha1 and suite_hash
+            if 'stdin' in job:
+                job_yaml = yaml.safe_load(job['stdin'])
+                assert job_yaml.get('sha1') == working_sha1
+                assert job_yaml.get('suite_sha1') == working_sha1
+
+    @patch('teuthology.suite.util.find_git_parents')
+    @patch('teuthology.suite.run.Run.schedule_jobs')
+    @patch('teuthology.suite.run.Run.write_rerun_memo')
+    @patch('teuthology.suite.util.get_install_task_flavor')
+    @patch('teuthology.suite.run.config_merge')
+    @patch('teuthology.suite.run.build_matrix')
+    @patch('teuthology.suite.util.git_ls_remote')
+    @patch('teuthology.suite.util.package_version_for_hash')
+    @patch('teuthology.suite.util.git_validate_sha1')
+    @patch('teuthology.suite.util.get_arch')
+    def test_newest_success_diff_branch_diff_repo(
+        self,
+        m_get_arch,
+        m_git_validate_sha1,
+        m_package_version_for_hash,
+        m_git_ls_remote,
+        m_build_matrix,
+        m_config_merge,
+        m_get_install_task_flavor,
+        m_write_rerun_memo,
+        m_schedule_jobs,
+        m_find_git_parents,
+    ):
+        """
+        Test that we can successfully schedule a job with newest
+        backtracking when the ceph and suite branches are different
+        and the ceph_sha1 is not supplied. We should expect that the
+        ceph_hash will be updated to the working sha1,
+        but the suite_hash will remain the original suite_sha1.
+        """
+        m_get_arch.return_value = 'x86_64'
+        # Set different branches
+        self.args.ceph_branch = 'ceph_different_branch'
+        self.args.suite_branch = 'suite_different_branch'
+
+        # rig has_packages_for_distro to fail this many times, so
+        # everything will run NUM_FAILS+1 times
+        NUM_FAILS = 5
+        # Here we just assume that even fi ceph_sha1 is not supplied,
+        # in git_valid_sha1, util.git_ls_remote will give us ceph_sha1
+        m_git_validate_sha1.return_value = self.args.ceph_sha1
+        # Here we know that in create_initial_config, we call
+        # git_ls_remote 3 times, choose_ceph_hash, choose_suite_hash,
+        # and choose_teuthology_branch
+        sha1_side_effect = [
+            self.args.ceph_sha1,  # ceph_sha1
+            'suite_sha1',        # suite_sha1
+            'teuthology_sha1',   # teuthology_sha1
+        ]
+        m_git_ls_remote.side_effect = sha1_side_effect
+        build_matrix_desc = 'desc'
+        build_matrix_frags = ['frag.yml']
+        build_matrix_output = [
+            (build_matrix_desc, build_matrix_frags),
+        ]
+        m_build_matrix.return_value = build_matrix_output
+        m_config_merge.return_value = [(a, b, {}) for a, b in build_matrix_output]
+        m_get_install_task_flavor.return_value = 'default'
+
+        # Generate backtracked parent sha1s
+        parent_sha1s = [f"ceph_sha1_{i}" for i in range(NUM_FAILS)]
+        assert len(parent_sha1s)
+        # Last sha1 will be the one that works!
+        working_sha1 = parent_sha1s[-1]
+
+        # NUM_FAILS attempts, then success on the last parent sha1
+        m_package_version_for_hash.side_effect = \
+            [None for i in range(NUM_FAILS)] + ["ceph_version"]
+
+        m_find_git_parents.return_value = parent_sha1s
+
+        self.args.newest = 10
+        runobj = self.klass(self.args)
+        runobj.base_args = list()
+
+        # Call schedule_suite()
+        count = runobj.schedule_suite()
+        # Epect only 1 job to be scheduled
+        assert count == 1
+        # Expect that we called package_version_for_hash NUM_FAILS times + 1 for the working sha1
+        m_package_version_for_hash.assert_has_calls(
+            [call(self.args.ceph_sha1, 'default', 'ubuntu', '14.04', 'machine_type')] +
+            [call(f"ceph_sha1_{i}", 'default', 'ubuntu', '14.04', 'machine_type')
+             for i in range(0, NUM_FAILS)]
+        )
+        # (ceph, base_config.sha1, newest) called once to get grab the backtrace
+        m_find_git_parents.assert_called_once_with('ceph', 'ceph_sha1', 10)
+
+        # Verify that base_config was updated with the working SHA1
+        assert runobj.base_config.sha1 == working_sha1
+
+        # Verify that config_input's ceph_hash was updated,
+        # but suite_hash is still the original suite_sha1
+        assert runobj.config_input['ceph_hash'] == working_sha1
+        assert runobj.config_input['suite_hash'] != working_sha1
+
+        # Verify that config_input's ceph_hash is not the same as the original sha1s
+        # but suite_hash is still the original suite_sha1
+        assert runobj.config_input['ceph_hash'] != sha1_side_effect[0]  # ceph_sha1
+        assert runobj.config_input['suite_hash'] == sha1_side_effect[1]  # suite_sha1
+
+        # Verify the sha1 in scheduled jobs
+        args = m_schedule_jobs.call_args.args
+        scheduled_jobs = args[1]
+
+        # Check each job has the correct SHA1
+        for job in scheduled_jobs:
+            assert job['sha1'] == working_sha1
+
+            # Parse YAML from stdin to check for sha1 and suite_hash
+            if 'stdin' in job:
+                job_yaml = yaml.safe_load(job['stdin'])
+                assert job_yaml.get('sha1') == working_sha1
+                assert job_yaml.get('suite_sha1') == sha1_side_effect[1]
diff --git a/tests/suite/test_util.py b/tests/suite/test_util.py
new file mode 100644 (file)
index 0000000..daa5830
--- /dev/null
@@ -0,0 +1,267 @@
+import os
+import pytest
+import tempfile
+
+from mock import Mock, patch
+
+from teuthology.config import config
+from teuthology.orchestra.opsys import OS
+from teuthology.suite import util
+from teuthology.exceptions import BranchNotFoundError, ScheduleFailError
+
+
+REPO_PROJECTS_AND_URLS = [
+    'ceph',
+    'https://github.com/not_ceph/ceph.git',
+]
+
+
+@pytest.mark.parametrize('project_or_url', REPO_PROJECTS_AND_URLS)
+@patch('subprocess.check_output')
+def test_git_branch_exists(m_check_output, project_or_url):
+    m_check_output.return_value = ''
+    assert False == util.git_branch_exists(
+        project_or_url, 'nobranchnowaycanthappen')
+    m_check_output.return_value = b'HHH branch'
+    assert True == util.git_branch_exists(project_or_url, 'main')
+
+
+@pytest.fixture
+def git_repository(request):
+    d = tempfile.mkdtemp()
+    os.system("""
+    cd {d}
+    git init
+    touch A
+    git config user.email 'you@example.com'
+    git config user.name 'Your Name'
+    git add A
+    git commit -m 'A' A
+    git rev-parse --abbrev-ref main || git checkout -b main
+    """.format(d=d))
+
+    def fin():
+        os.system("rm -fr " + d)
+    request.addfinalizer(fin)
+    return d
+
+
+class TestUtil(object):
+    @patch('teuthology.suite.util.smtplib.SMTP')
+    def test_schedule_fail(self, m_smtp):
+        config.results_email = "example@example.com"
+        with pytest.raises(ScheduleFailError) as exc:
+            util.schedule_fail(message="error msg", dry_run=False)
+        assert str(exc.value) == "Scheduling failed: error msg"
+        m_smtp.assert_called()
+
+    @patch('teuthology.suite.util.smtplib.SMTP')
+    def test_schedule_fail_dryrun(self, m_smtp):
+        config.results_email = "example@example.com"
+        with pytest.raises(ScheduleFailError) as exc:
+            util.schedule_fail(message="error msg", dry_run=True)
+        assert str(exc.value) == "Scheduling failed: error msg"
+        m_smtp.assert_not_called()
+
+    @patch('teuthology.suite.util.fetch_qa_suite')
+    @patch('teuthology.suite.util.smtplib.SMTP')
+    def test_fetch_repo_no_branch(self, m_smtp, m_fetch_qa_suite):
+        m_fetch_qa_suite.side_effect = BranchNotFoundError(
+            "no-branch", "https://github.com/ceph/ceph-ci.git")
+        config.results_email = "example@example.com"
+        with pytest.raises(ScheduleFailError) as exc:
+            util.fetch_repos("no-branch", "test1", dry_run=False)
+        assert str(exc.value) == "Scheduling test1 failed: \
+Branch 'no-branch' not found in repo: https://github.com/ceph/ceph-ci.git!"
+        m_smtp.assert_called()
+
+    @patch('teuthology.suite.util.fetch_qa_suite')
+    @patch('teuthology.suite.util.smtplib.SMTP')
+    def test_fetch_repo_no_branch_dryrun(self, m_smtp, m_fetch_qa_suite):
+        m_fetch_qa_suite.side_effect = BranchNotFoundError(
+            "no-branch", "https://github.com/ceph/ceph-ci.git")
+        config.results_email = "example@example.com"
+        with pytest.raises(ScheduleFailError) as exc:
+            util.fetch_repos("no-branch", "test1", dry_run=True)
+        assert str(exc.value) == "Scheduling test1 failed: \
+Branch 'no-branch' not found in repo: https://github.com/ceph/ceph-ci.git!"
+        m_smtp.assert_not_called()
+
+    @patch('requests.get')
+    def test_get_branch_info(self, m_get):
+        mock_resp = Mock()
+        mock_resp.ok = True
+        mock_resp.json.return_value = "some json"
+        m_get.return_value = mock_resp
+        result = util.get_branch_info("teuthology", "main")
+        m_get.assert_called_with(
+            "https://api.github.com/repos/ceph/teuthology/git/refs/heads/main"
+        )
+        assert result == "some json"
+
+    @patch('teuthology.lock.query')
+    def test_get_arch_fail(self, m_query):
+        m_query.list_locks.return_value = False
+        util.get_arch('magna')
+        m_query.list_locks.assert_called_with(machine_type="magna", count=1, tries=1)
+
+    @patch('teuthology.lock.query')
+    def test_get_arch_success(self, m_query):
+        m_query.list_locks.return_value = [{"arch": "arch"}]
+        result = util.get_arch('magna')
+        m_query.list_locks.assert_called_with(
+            machine_type="magna",
+            count=1, tries=1
+        )
+        assert result == "arch"
+
+    def test_build_git_url_github(self):
+        assert 'project' in util.build_git_url('project')
+        owner = 'OWNER'
+        git_url = util.build_git_url('project', project_owner=owner)
+        assert owner in git_url
+
+    @patch('teuthology.config.TeuthologyConfig.get_ceph_qa_suite_git_url')
+    def test_build_git_url_ceph_qa_suite_custom(
+            self,
+            m_get_ceph_qa_suite_git_url):
+        url = 'http://foo.com/some'
+        m_get_ceph_qa_suite_git_url.return_value = url + '.git'
+        assert url == util.build_git_url('ceph-qa-suite')
+
+    @patch('teuthology.config.TeuthologyConfig.get_ceph_git_url')
+    def test_build_git_url_ceph_custom(self, m_get_ceph_git_url):
+        url = 'http://foo.com/some'
+        m_get_ceph_git_url.return_value = url + '.git'
+        assert url == util.build_git_url('ceph')
+
+    @patch('teuthology.config.TeuthologyConfig.get_ceph_cm_ansible_git_url')
+    def test_build_git_url_ceph_cm_ansible_custom(self, m_get_ceph_cm_ansible_git_url):
+        url = 'http://foo.com/some'
+        m_get_ceph_cm_ansible_git_url.return_value = url + '.git'
+        assert url == util.build_git_url('ceph-cm-ansible')
+
+    @patch('teuthology.config.TeuthologyConfig.get_ceph_git_url')
+    def test_git_ls_remote(self, m_get_ceph_git_url, git_repository):
+        m_get_ceph_git_url.return_value = git_repository
+        assert util.git_ls_remote('ceph', 'nobranch') is None
+        assert util.git_ls_remote('ceph', 'main') is not None
+
+    @patch('teuthology.suite.util.requests.get')
+    def test_find_git_parents(self, m_requests_get):
+        history_resp = Mock(ok=True)
+        history_resp.json.return_value = {'sha1s': ['sha1', 'sha1_p']}
+        m_requests_get.return_value = history_resp
+        parent_sha1s = util.find_git_parents('ceph', 'sha1')
+        assert m_requests_get.call_count == 1
+        assert parent_sha1s == ['sha1_p']
+
+
+class TestFlavor(object):
+
+    def test_get_install_task_flavor_bare(self):
+        config = dict(
+            tasks=[
+                dict(
+                    install=dict(),
+                ),
+            ],
+        )
+        assert util.get_install_task_flavor(config) == 'default'
+
+    def test_get_install_task_flavor_simple(self):
+        config = dict(
+            tasks=[
+                dict(
+                    install=dict(
+                        flavor='notcmalloc',
+                    ),
+                ),
+            ],
+        )
+        assert util.get_install_task_flavor(config) == 'notcmalloc'
+
+    def test_get_install_task_flavor_override_simple(self):
+        config = dict(
+            tasks=[
+                dict(install=dict()),
+            ],
+            overrides=dict(
+                install=dict(
+                    flavor='notcmalloc',
+                ),
+            ),
+        )
+        assert util.get_install_task_flavor(config) == 'notcmalloc'
+
+    def test_get_install_task_flavor_override_project(self):
+        config = dict(
+            tasks=[
+                dict(install=dict()),
+            ],
+            overrides=dict(
+                install=dict(
+                    ceph=dict(
+                        flavor='notcmalloc',
+                    ),
+                ),
+            ),
+        )
+        assert util.get_install_task_flavor(config) == 'notcmalloc'
+
+
+class TestMissingPackages(object):
+    """
+    Tests the functionality that checks to see if a
+    scheduled job will have missing packages in shaman.
+    """
+    @patch("teuthology.packaging.ShamanProject._get_package_version")
+    def test_distro_has_packages(self, m_gpv):
+        m_gpv.return_value = "v1"
+        result = util.package_version_for_hash(
+            "sha1",
+            "basic",
+            "ubuntu",
+            "14.04",
+            "mtype",
+        )
+        assert result
+
+    @patch("teuthology.packaging.ShamanProject._get_package_version")
+    def test_distro_does_not_have_packages(self, m_gpv):
+        m_gpv.return_value = None
+        result = util.package_version_for_hash(
+            "sha1",
+            "basic",
+            "rhel",
+            "7.0",
+            "mtype",
+        )
+        assert not result
+
+
+class TestDistroDefaults(object):
+    def test_distro_defaults_plana(self):
+        expected = ('x86_64', 'ubuntu/22.04',
+                    OS(name='ubuntu', version='22.04', codename='jammy'))
+        assert util.get_distro_defaults('ubuntu', 'plana') == expected
+
+    def test_distro_defaults_debian(self):
+        expected = ('x86_64', 'debian/8.0',
+                    OS(name='debian', version='8.0', codename='jessie'))
+        assert util.get_distro_defaults('debian', 'magna') == expected
+
+    def test_distro_defaults_centos(self):
+        expected = ('x86_64', 'centos/9',
+                    OS(name='centos', version='9.stream', codename='stream'))
+        assert util.get_distro_defaults('centos', 'magna') == expected
+
+    def test_distro_defaults_fedora(self):
+        expected = ('x86_64', 'fedora/25',
+                    OS(name='fedora', version='25', codename='25'))
+        assert util.get_distro_defaults('fedora', 'magna') == expected
+
+    def test_distro_defaults_default(self):
+        expected = ('x86_64', 'centos/9',
+                    OS(name='centos', version='9.stream', codename='stream'))
+        assert util.get_distro_defaults('rhel', 'magna') == expected
diff --git a/tests/task/__init__.py b/tests/task/__init__.py
new file mode 100644 (file)
index 0000000..d000cff
--- /dev/null
@@ -0,0 +1,205 @@
+from mock import patch, DEFAULT
+from pytest import raises
+
+from teuthology.config import FakeNamespace
+from teuthology.orchestra.cluster import Cluster
+from teuthology.orchestra.remote import Remote
+from teuthology.task import Task
+
+
+class TestTask(object):
+    klass = Task
+    task_name = 'task'
+
+    def setup_method(self):
+        self.ctx = FakeNamespace()
+        self.ctx.config = dict()
+        self.task_config = dict()
+
+    def test_overrides(self):
+        self.ctx.config['overrides'] = dict()
+        self.ctx.config['overrides'][self.task_name] = dict(
+            key_1='overridden',
+        )
+        self.task_config.update(dict(
+            key_1='default',
+            key_2='default',
+        ))
+        with patch.multiple(
+            self.klass,
+            begin=DEFAULT,
+            end=DEFAULT,
+        ):
+            with self.klass(self.ctx, self.task_config) as task:
+                assert task.config['key_1'] == 'overridden'
+                assert task.config['key_2'] == 'default'
+
+    def test_hosts_no_filter(self):
+        self.ctx.cluster = Cluster()
+        self.ctx.cluster.add(Remote('user@remote1'), ['role1'])
+        self.ctx.cluster.add(Remote('user@remote2'), ['role2'])
+        with patch.multiple(
+            self.klass,
+            begin=DEFAULT,
+            end=DEFAULT,
+        ):
+            with self.klass(self.ctx, self.task_config) as task:
+                task_hosts = list(task.cluster.remotes)
+                assert len(task_hosts) == 2
+                assert sorted(host.shortname for host in task_hosts) == \
+                    ['remote1', 'remote2']
+
+    def test_hosts_no_results(self):
+        self.ctx.cluster = Cluster()
+        self.ctx.cluster.add(Remote('user@remote1'), ['role1'])
+        self.task_config.update(dict(
+            hosts=['role2'],
+        ))
+        with patch.multiple(
+            self.klass,
+            begin=DEFAULT,
+            end=DEFAULT,
+        ):
+            with raises(RuntimeError):
+                with self.klass(self.ctx, self.task_config):
+                    pass
+
+    def test_hosts_one_role(self):
+        self.ctx.cluster = Cluster()
+        self.ctx.cluster.add(Remote('user@remote1'), ['role1'])
+        self.ctx.cluster.add(Remote('user@remote2'), ['role2'])
+        self.task_config.update(dict(
+            hosts=['role1'],
+        ))
+        with patch.multiple(
+            self.klass,
+            begin=DEFAULT,
+            end=DEFAULT,
+        ):
+            with self.klass(self.ctx, self.task_config) as task:
+                task_hosts = list(task.cluster.remotes)
+                assert len(task_hosts) == 1
+                assert task_hosts[0].shortname == 'remote1'
+
+    def test_hosts_two_roles(self):
+        self.ctx.cluster = Cluster()
+        self.ctx.cluster.add(Remote('user@remote1'), ['role1'])
+        self.ctx.cluster.add(Remote('user@remote2'), ['role2'])
+        self.ctx.cluster.add(Remote('user@remote3'), ['role3'])
+        self.task_config.update(dict(
+            hosts=['role1', 'role3'],
+        ))
+        with patch.multiple(
+            self.klass,
+            begin=DEFAULT,
+            end=DEFAULT,
+        ):
+            with self.klass(self.ctx, self.task_config) as task:
+                task_hosts = list(task.cluster.remotes)
+                assert len(task_hosts) == 2
+                hostnames = [host.shortname for host in task_hosts]
+                assert sorted(hostnames) == ['remote1', 'remote3']
+
+    def test_hosts_two_hostnames(self):
+        self.ctx.cluster = Cluster()
+        self.ctx.cluster.add(Remote('user@remote1.example.com'), ['role1'])
+        self.ctx.cluster.add(Remote('user@remote2.example.com'), ['role2'])
+        self.ctx.cluster.add(Remote('user@remote3.example.com'), ['role3'])
+        self.task_config.update(dict(
+            hosts=['remote1', 'remote2.example.com'],
+        ))
+        with patch.multiple(
+            self.klass,
+            begin=DEFAULT,
+            end=DEFAULT,
+        ):
+            with self.klass(self.ctx, self.task_config) as task:
+                task_hosts = list(task.cluster.remotes)
+                assert len(task_hosts) == 2
+                hostnames = [host.hostname for host in task_hosts]
+                assert sorted(hostnames) == ['remote1.example.com',
+                                             'remote2.example.com']
+
+    def test_hosts_one_role_one_hostname(self):
+        self.ctx.cluster = Cluster()
+        self.ctx.cluster.add(Remote('user@remote1.example.com'), ['role1'])
+        self.ctx.cluster.add(Remote('user@remote2.example.com'), ['role2'])
+        self.ctx.cluster.add(Remote('user@remote3.example.com'), ['role3'])
+        self.task_config.update(dict(
+            hosts=['role1', 'remote2.example.com'],
+        ))
+        with patch.multiple(
+            self.klass,
+            begin=DEFAULT,
+            end=DEFAULT,
+        ):
+            with self.klass(self.ctx, self.task_config) as task:
+                task_hosts = list(task.cluster.remotes)
+                assert len(task_hosts) == 2
+                hostnames = [host.hostname for host in task_hosts]
+                assert sorted(hostnames) == ['remote1.example.com',
+                                             'remote2.example.com']
+
+    def test_setup_called(self):
+        with patch.multiple(
+            self.klass,
+            setup=DEFAULT,
+            begin=DEFAULT,
+            end=DEFAULT,
+            teardown=DEFAULT,
+        ):
+            with self.klass(self.ctx, self.task_config) as task:
+                task.setup.assert_called_once_with()
+
+    def test_begin_called(self):
+        with patch.multiple(
+            self.klass,
+            setup=DEFAULT,
+            begin=DEFAULT,
+            end=DEFAULT,
+            teardown=DEFAULT,
+        ):
+            with self.klass(self.ctx, self.task_config) as task:
+                task.begin.assert_called_once_with()
+
+    def test_end_called(self):
+        self.task_config.update(dict())
+        with patch.multiple(
+            self.klass,
+            begin=DEFAULT,
+            end=DEFAULT,
+        ):
+            with self.klass(self.ctx, self.task_config) as task:
+                pass
+            task.end.assert_called_once_with()
+
+    def test_teardown_called(self):
+        self.task_config.update(dict())
+        with patch.multiple(
+            self.klass,
+            setup=DEFAULT,
+            begin=DEFAULT,
+            end=DEFAULT,
+            teardown=DEFAULT,
+        ):
+            with self.klass(self.ctx, self.task_config) as task:
+                pass
+            task.teardown.assert_called_once_with()
+
+    def test_skip_teardown(self):
+        self.task_config.update(dict(
+            skip_teardown=True,
+        ))
+
+        def fake_teardown(self):
+            assert False
+
+        with patch.multiple(
+            self.klass,
+            setup=DEFAULT,
+            begin=DEFAULT,
+            end=DEFAULT,
+            teardown=fake_teardown,
+        ):
+            with self.klass(self.ctx, self.task_config):
+                pass
diff --git a/tests/task/test_ansible.py b/tests/task/test_ansible.py
new file mode 100644 (file)
index 0000000..7f4839c
--- /dev/null
@@ -0,0 +1,659 @@
+import json
+import os
+import yaml
+
+from unittest.mock import patch, DEFAULT, Mock, mock_open
+from pytest import raises, mark
+from teuthology.util.compat import PY3
+if PY3:
+    from io import StringIO as StringIO
+else:
+    from io import BytesIO as StringIO
+
+from teuthology.config import config, FakeNamespace
+from teuthology.exceptions import CommandFailedError
+from teuthology.orchestra.cluster import Cluster
+from teuthology.orchestra.remote import Remote
+from teuthology.task import ansible
+from teuthology.task.ansible import Ansible, CephLab, FailureAnalyzer
+
+from tests.task import TestTask
+
+
+class TestFailureAnalyzer:
+    klass = FailureAnalyzer
+
+    @mark.parametrize(
+        'line,result',
+        [
+            [
+                "W: --force-yes is deprecated, use one of the options starting with --allow instead.",
+                "",
+            ],
+            [
+                "E: Unable to fetch some archives, maybe run apt-get update or try with --fix-missing?",
+                "",
+            ],
+            [
+                "E: Failed to fetch http://security.ubuntu.com/ubuntu/pool/main/a/apache2/apache2-bin_2.4.41-4ubuntu3.14_amd64.deb  Unable to connect to archive.ubuntu.com:http:",
+                "Unable to connect to archive.ubuntu.com:http:"
+            ],
+            [
+                "E: Failed to fetch http://archive.ubuntu.com/ubuntu/pool/main/libb/libb-hooks-op-check-perl/libb-hooks-op-check-perl_0.22-1build2_amd64.deb  Temporary failure resolving 'archive.ubuntu.com'",
+                "Temporary failure resolving 'archive.ubuntu.com'"
+            ],
+            [
+                "Data could not be sent to remote host \"smithi068.front.sepia.ceph.com\".",
+                "Data could not be sent to remote host \"smithi068.front.sepia.ceph.com\"."
+            ],
+            [
+                "Permissions 0644 for '/root/.ssh/id_rsa' are too open.",
+                "Permissions 0644 for '/root/.ssh/id_rsa' are too open."
+            ],
+        ]
+    )
+    def test_lines(self, line, result):
+        obj = self.klass()
+        assert obj.analyze_line(line) == result
+
+
+class TestAnsibleTask(TestTask):
+    klass = Ansible
+    task_name = 'ansible'
+
+    def setup_method(self):
+        self.ctx = FakeNamespace()
+        self.ctx.cluster = Cluster()
+        self.ctx.cluster.add(Remote('user@remote1'), ['role1'])
+        self.ctx.cluster.add(Remote('user@remote2'), ['role2'])
+        self.ctx.config = dict()
+        self.ctx.summary = dict()
+        self.ctx.archive = ""
+        self.task_config = dict(playbook=[])
+        self.start_patchers()
+
+    def start_patchers(self):
+        self.patchers = dict()
+        self.mocks = dict()
+        self.patchers['mkdtemp'] = patch(
+            'teuthology.task.ansible.mkdtemp', return_value='/tmp/'
+        )
+        m_NTF = Mock()
+        m_file = Mock()
+        m_file.name = 'file_name'
+        m_NTF.return_value = m_file
+        self.patchers['NTF'] = patch(
+            'teuthology.task.ansible.NamedTemporaryFile',
+            m_NTF,
+        )
+        self.patchers['file'] = patch(
+            'teuthology.task.ansible.open', create=True)
+        self.patchers['os_mkdir'] = patch(
+            'teuthology.task.ansible.os.mkdir',
+        )
+        self.patchers['os_remove'] = patch(
+            'teuthology.task.ansible.os.remove',
+        )
+        self.patchers['shutil_rmtree'] = patch(
+            'teuthology.task.ansible.shutil.rmtree',
+        )
+        for name in self.patchers.keys():
+            self.start_patcher(name)
+
+    def start_patcher(self, name):
+        if name not in self.mocks.keys():
+            self.mocks[name] = self.patchers[name].start()
+
+    def teardown_method(self, method):
+        self.stop_patchers()
+
+    def stop_patchers(self):
+        for name in list(self.mocks):
+            self.stop_patcher(name)
+
+    def stop_patcher(self, name):
+        self.patchers[name].stop()
+        del self.mocks[name]
+
+    def test_setup(self):
+        self.task_config.update(dict(
+            playbook=[]
+        ))
+
+        def fake_get_playbook(self):
+            self.playbook_file = 'fake'
+
+        with patch.multiple(
+            self.klass,
+            find_repo=DEFAULT,
+            get_playbook=fake_get_playbook,
+            get_inventory=DEFAULT,
+            generate_inventory=DEFAULT,
+            generate_playbook=Mock(side_effect=Exception),
+        ):
+            task = self.klass(self.ctx, self.task_config)
+            task.setup()
+
+    def test_setup_generate_playbook(self):
+        self.task_config.update(dict(
+            playbook=[]
+        ))
+        with patch.multiple(
+            self.klass,
+            find_repo=DEFAULT,
+            get_playbook=DEFAULT,
+            get_inventory=DEFAULT,
+            generate_inventory=DEFAULT,
+            generate_playbook=DEFAULT,
+        ):
+            task = self.klass(self.ctx, self.task_config)
+            task.setup()
+            task.generate_playbook.assert_called_once_with()
+
+    def test_find_repo_path(self):
+        self.task_config.update(dict(
+            repo='~/my/repo',
+        ))
+        task = self.klass(self.ctx, self.task_config)
+        task.find_repo()
+        assert task.repo_path == os.path.expanduser(self.task_config['repo'])
+
+    @patch('teuthology.repo_utils.fetch_repo')
+    def test_find_repo_path_remote(self, m_fetch_repo):
+        self.task_config.update(dict(
+            repo='git://fake_host/repo.git',
+        ))
+        m_fetch_repo.return_value = '/tmp/repo'
+        task = self.klass(self.ctx, self.task_config)
+        task.find_repo()
+        assert task.repo_path == os.path.expanduser('/tmp/repo')
+
+    @patch('teuthology.repo_utils.fetch_repo')
+    def test_find_repo_http(self, m_fetch_repo):
+        self.task_config.update(dict(
+            repo='http://example.com/my/repo',
+        ))
+        task = self.klass(self.ctx, self.task_config)
+        task.find_repo()
+        m_fetch_repo.assert_called_once_with(self.task_config['repo'],
+                                             'main')
+
+    @patch('teuthology.repo_utils.fetch_repo')
+    def test_find_repo_git(self, m_fetch_repo):
+        self.task_config.update(dict(
+            repo='git@example.com/my/repo',
+        ))
+        task = self.klass(self.ctx, self.task_config)
+        task.find_repo()
+        m_fetch_repo.assert_called_once_with(self.task_config['repo'],
+                                             'main')
+
+    def test_playbook_none(self):
+        del self.task_config['playbook']
+        task = self.klass(self.ctx, self.task_config)
+        with raises(KeyError):
+            task.get_playbook()
+
+    def test_playbook_wrong_type(self):
+        self.task_config.update(dict(
+            playbook=dict(),
+        ))
+        task = self.klass(self.ctx, self.task_config)
+        with raises(TypeError):
+            task.get_playbook()
+
+    def test_playbook_list(self):
+        playbook = [
+            dict(
+                roles=['role1'],
+            ),
+        ]
+        self.task_config.update(dict(
+            playbook=playbook,
+        ))
+        task = self.klass(self.ctx, self.task_config)
+        task.get_playbook()
+        assert task.playbook == playbook
+
+    @patch.object(ansible.requests, 'get')
+    def test_playbook_http(self, m_get):
+        m_get.return_value = Mock()
+        m_get.return_value.text = 'fake playbook text'
+        playbook = "http://example.com/my_playbook.yml"
+        self.task_config.update(dict(
+            playbook=playbook,
+        ))
+        task = self.klass(self.ctx, self.task_config)
+        task.get_playbook()
+        m_get.assert_called_once_with(playbook)
+
+    def test_playbook_file(self):
+        fake_playbook = [dict(fake_playbook=True)]
+        fake_playbook_obj = StringIO(yaml.safe_dump(fake_playbook))
+        self.task_config.update(dict(
+            playbook='~/fake/playbook',
+        ))
+        task = self.klass(self.ctx, self.task_config)
+        self.mocks['file'].return_value = fake_playbook_obj
+        task.get_playbook()
+        assert task.playbook == fake_playbook
+
+    def test_playbook_file_missing(self):
+        self.task_config.update(dict(
+            playbook='~/fake/playbook',
+        ))
+        task = self.klass(self.ctx, self.task_config)
+        self.mocks['file'].side_effect = IOError
+        with raises(IOError):
+            task.get_playbook()
+
+    def test_inventory_none(self):
+        self.task_config.update(dict(
+            playbook=[]
+        ))
+        task = self.klass(self.ctx, self.task_config)
+        with patch.object(ansible.os.path, 'exists') as m_exists:
+            m_exists.return_value = False
+            task.get_inventory()
+        assert task.inventory is None
+
+    def test_inventory_path(self):
+        inventory = '/my/inventory'
+        self.task_config.update(dict(
+            playbook=[],
+            inventory=inventory,
+        ))
+        task = self.klass(self.ctx, self.task_config)
+        task.get_inventory()
+        assert task.inventory == inventory
+        assert task.generated_inventory is False
+
+    def test_inventory_etc(self):
+        self.task_config.update(dict(
+            playbook=[]
+        ))
+        task = self.klass(self.ctx, self.task_config)
+        with patch.object(ansible.os.path, 'exists') as m_exists:
+            m_exists.return_value = True
+            task.get_inventory()
+        assert task.inventory == '/etc/ansible/hosts'
+        assert task.generated_inventory is False
+
+    @mark.parametrize(
+        'group_vars',
+        [
+            dict(),
+            dict(all=dict(var0=0, var1=1)),
+            dict(foo=dict(var0=0), bar=dict(var0=1)),
+        ]
+    )
+    def test_generate_inventory(self, group_vars):
+        self.task_config.update(dict(
+            playbook=[]
+        ))
+        if group_vars:
+            self.task_config.update(dict(group_vars=group_vars))
+        task = self.klass(self.ctx, self.task_config)
+        hosts_file_path = '/my/hosts/inventory'
+        hosts_file_obj = StringIO()
+        hosts_file_obj.name = hosts_file_path
+        inventory_dir = os.path.dirname(hosts_file_path)
+        gv_dir = os.path.join(inventory_dir, 'group_vars')
+        self.mocks['mkdtemp'].return_value = inventory_dir
+        m_file = self.mocks['file']
+        fake_files = [hosts_file_obj]
+        # Create StringIO object for each group_vars file
+        if group_vars:
+            fake_files += [StringIO() for i in sorted(group_vars)]
+        m_file.side_effect = fake_files
+        task.generate_inventory()
+        file_calls = m_file.call_args_list
+        # Verify the inventory file was created
+        assert file_calls[0][0][0] == hosts_file_path
+        # Verify each group_vars file was created
+        for gv_name, call_obj in zip(sorted(group_vars), file_calls[1:]):
+            gv_path = call_obj[0][0]
+            assert gv_path == os.path.join(gv_dir, '%s.yml' % gv_name)
+        # Verify the group_vars dir was created
+        if group_vars:
+            mkdir_call = self.mocks['os_mkdir'].call_args_list
+            assert mkdir_call[0][0][0] == gv_dir
+        assert task.generated_inventory is True
+        assert task.inventory == inventory_dir
+        # Verify the content of the inventory *file*
+        hosts_file_obj.seek(0)
+        assert hosts_file_obj.readlines() == [
+            'remote1\n',
+            'remote2\n',
+        ]
+        # Verify the contents of each group_vars file
+        gv_names = sorted(group_vars)
+        for i in range(len(gv_names)):
+            gv_name = gv_names[i]
+            in_val = group_vars[gv_name]
+            gv_stringio = fake_files[1 + i]
+            gv_stringio.seek(0)
+            out_val = yaml.safe_load(gv_stringio)
+            assert in_val == out_val
+
+    def test_generate_playbook(self):
+        playbook = [
+            dict(
+                roles=['role1', 'role2'],
+            ),
+        ]
+        self.task_config.update(dict(
+            playbook=playbook
+        ))
+        task = self.klass(self.ctx, self.task_config)
+        playbook_file_path = '/my/playbook/file'
+        playbook_file_obj = StringIO()
+        playbook_file_obj.name = playbook_file_path
+        with patch.object(ansible, 'NamedTemporaryFile') as m_NTF:
+            m_NTF.return_value = playbook_file_obj
+            task.find_repo()
+            task.get_playbook()
+            task.generate_playbook()
+            m_NTF.assert_called_once_with(
+                prefix="teuth_ansible_playbook_",
+                dir=task.repo_path,
+                delete=False,
+            )
+        assert task.generated_playbook is True
+        assert task.playbook_file == playbook_file_obj
+        playbook_file_obj.seek(0)
+        playbook_result = yaml.safe_load(playbook_file_obj)
+        assert playbook_result == playbook
+
+    def test_execute_playbook(self):
+        playbook = '/my/playbook'
+        self.task_config.update(dict(
+            playbook=playbook
+        ))
+        fake_playbook = [dict(fake_playbook=True)]
+        fake_playbook_obj = StringIO(yaml.safe_dump(fake_playbook))
+        fake_playbook_obj.name = playbook
+        self.mocks['mkdtemp'].return_value = '/inventory/dir'
+
+        task = self.klass(self.ctx, self.task_config)
+        self.mocks['file'].return_value = fake_playbook_obj
+        task.setup()
+        args = task._build_args()
+        logger = StringIO()
+        with patch.object(ansible.pexpect, 'run') as m_run:
+            m_run.return_value = ('', 0)
+            with patch.object(Remote, 'reconnect') as m_reconnect:
+                m_reconnect.return_value = True
+                task.execute_playbook(_logfile=logger)
+            m_run.assert_called_once_with(
+                ' '.join(args),
+                cwd=task.repo_path,
+                logfile=logger,
+                withexitstatus=True,
+                timeout=None,
+            )
+
+    def test_execute_playbook_fail(self):
+        self.task_config.update(dict(
+            playbook=[],
+        ))
+        self.mocks['mkdtemp'].return_value = '/inventory/dir'
+        task = self.klass(self.ctx, self.task_config)
+        task.setup()
+        with patch.object(ansible.pexpect, 'run') as m_run:
+            with patch('teuthology.task.ansible.open', mock_open()):
+                m_run.return_value = ('', 1)
+                with raises(CommandFailedError):
+                    task.execute_playbook()
+                assert task.ctx.summary.get('status') is None
+
+    def test_build_args_no_tags(self):
+        self.task_config.update(dict(
+            playbook=[],
+        ))
+        task = self.klass(self.ctx, self.task_config)
+        task.setup()
+        args = task._build_args()
+        assert '--tags' not in args
+
+    def test_build_args_tags(self):
+        self.task_config.update(dict(
+            playbook=[],
+            tags="user,pubkeys"
+        ))
+        task = self.klass(self.ctx, self.task_config)
+        task.setup()
+        args = task._build_args()
+        assert args.count('--tags') == 1
+        assert args[args.index('--tags') + 1] == 'user,pubkeys'
+
+    def test_build_args_skip_tags(self):
+        self.task_config.update(dict(
+            playbook=[],
+            skip_tags="user,pubkeys"
+        ))
+        task = self.klass(self.ctx, self.task_config)
+        task.setup()
+        args = task._build_args()
+        assert args.count('--skip-tags') == 1
+        assert args[args.index('--skip-tags') + 1] == 'user,pubkeys'
+
+    def test_build_args_no_vars(self):
+        self.task_config.update(dict(
+            playbook=[],
+        ))
+        task = self.klass(self.ctx, self.task_config)
+        task.setup()
+        args = task._build_args()
+        assert args.count('--extra-vars') == 1
+        vars_str = args[args.index('--extra-vars') + 1].strip("'")
+        extra_vars = json.loads(vars_str)
+        assert list(extra_vars) == ['ansible_ssh_user']
+
+    def test_build_args_vars(self):
+        extra_vars = dict(
+            string1='value1',
+            list1=['item1'],
+            dict1=dict(key='value'),
+        )
+
+        self.task_config.update(dict(
+            playbook=[],
+            vars=extra_vars,
+        ))
+        task = self.klass(self.ctx, self.task_config)
+        task.setup()
+        args = task._build_args()
+        assert args.count('--extra-vars') == 1
+        vars_str = args[args.index('--extra-vars') + 1].strip("'")
+        got_extra_vars = json.loads(vars_str)
+        assert 'ansible_ssh_user' in got_extra_vars
+        assert got_extra_vars['string1'] == extra_vars['string1']
+        assert got_extra_vars['list1'] == extra_vars['list1']
+        assert got_extra_vars['dict1'] == extra_vars['dict1']
+
+    def test_teardown_inventory(self):
+        self.task_config.update(dict(
+            playbook=[],
+        ))
+        task = self.klass(self.ctx, self.task_config)
+        task.generated_inventory = True
+        task.inventory = 'fake'
+        with patch.object(ansible.shutil, 'rmtree') as m_rmtree:
+            task.teardown()
+            m_rmtree.assert_called_once_with('fake')
+
+    def test_teardown_playbook(self):
+        self.task_config.update(dict(
+            playbook=[],
+        ))
+        task = self.klass(self.ctx, self.task_config)
+        task.generated_playbook = True
+        task.playbook_file = Mock()
+        task.playbook_file.name = 'fake'
+        with patch.object(ansible.os, 'remove') as m_remove:
+            task.teardown()
+            m_remove.assert_called_once_with('fake')
+
+    def test_teardown_cleanup_with_vars(self):
+        self.task_config.update(dict(
+            playbook=[],
+            cleanup=True,
+            vars=dict(yum_repos="testing"),
+        ))
+        task = self.klass(self.ctx, self.task_config)
+        task.inventory = "fake"
+        task.generated_playbook = True
+        task.playbook_file = Mock()
+        task.playbook_file.name = 'fake'
+        with patch.object(self.klass, 'execute_playbook') as m_execute:
+            with patch.object(ansible.os, 'remove'):
+                task.teardown()
+            task._build_args()
+            assert m_execute.called
+            assert 'cleanup' in task.config['vars']
+            assert 'yum_repos' in task.config['vars']
+
+    def test_teardown_cleanup_with_no_vars(self):
+        self.task_config.update(dict(
+            playbook=[],
+            cleanup=True,
+        ))
+        task = self.klass(self.ctx, self.task_config)
+        task.inventory = "fake"
+        task.generated_playbook = True
+        task.playbook_file = Mock()
+        task.playbook_file.name = 'fake'
+        with patch.object(self.klass, 'execute_playbook') as m_execute:
+            with patch.object(ansible.os, 'remove'):
+                task.teardown()
+            task._build_args()
+            assert m_execute.called
+            assert 'cleanup' in task.config['vars']
+
+    def test_no_remotes(self):
+        self.task_config.update(dict(
+            playbook=[],
+        ))
+        self.ctx.cluster.remotes = dict()
+        task = self.klass(self.ctx, self.task_config)
+        with patch.object(ansible.pexpect, 'run') as m_run:
+            task.setup()
+            task.begin()
+        assert not m_run.called
+
+
+class TestCephLabTask(TestAnsibleTask):
+    klass = CephLab
+    task_name = 'ansible.cephlab'
+
+    def setup_method(self):
+        super(TestCephLabTask, self).setup_method()
+        self.task_config = dict()
+
+    def start_patchers(self):
+        super(TestCephLabTask, self).start_patchers()
+        self.patchers['fetch_repo'] = patch(
+            'teuthology.repo_utils.fetch_repo',
+        )
+        self.patchers['fetch_repo'].return_value = 'PATH'
+
+        def fake_get_playbook(self):
+            self.playbook_file = Mock()
+            self.playbook_file.name = 'cephlab.yml'
+
+        self.patchers['get_playbook'] = patch(
+            'teuthology.task.ansible.CephLab.get_playbook',
+            new=fake_get_playbook,
+        )
+        for name in self.patchers.keys():
+            self.start_patcher(name)
+
+    @patch('teuthology.repo_utils.fetch_repo')
+    def test_find_repo_http(self, m_fetch_repo):
+        repo = os.path.join(config.ceph_git_base_url,
+                            'ceph-cm-ansible.git')
+        task = self.klass(self.ctx, dict())
+        task.find_repo()
+        m_fetch_repo.assert_called_once_with(repo, 'main')
+
+    def test_playbook_file(self):
+        fake_playbook = [dict(fake_playbook=True)]
+        fake_playbook_obj = StringIO(yaml.safe_dump(fake_playbook))
+        playbook = 'cephlab.yml'
+        fake_playbook_obj.name = playbook
+        task = self.klass(self.ctx, dict())
+        task.repo_path = '/tmp/fake/repo'
+        self.mocks['file'].return_value = fake_playbook_obj
+        task.get_playbook()
+        assert task.playbook_file.name == playbook
+
+    def test_generate_inventory(self):
+        self.task_config.update(dict(
+            playbook=[]
+        ))
+        task = self.klass(self.ctx, self.task_config)
+        hosts_file_path = '/my/hosts/file'
+        hosts_file_obj = StringIO()
+        hosts_file_obj.name = hosts_file_path
+        self.mocks['mkdtemp'].return_value = os.path.dirname(hosts_file_path)
+        self.mocks['file'].return_value = hosts_file_obj
+        task.generate_inventory()
+        assert task.generated_inventory is True
+        assert task.inventory == os.path.dirname(hosts_file_path)
+        hosts_file_obj.seek(0)
+        assert hosts_file_obj.readlines() == [
+            '[testnodes]\n',
+            'remote1\n',
+            'remote2\n',
+        ]
+
+    def test_fail_status_dead(self):
+        self.task_config.update(dict(
+            playbook=[],
+        ))
+        self.mocks['mkdtemp'].return_value = '/inventory/dir'
+        task = self.klass(self.ctx, self.task_config)
+        task.ctx.summary = dict()
+        task.setup()
+        with patch.object(ansible.pexpect, 'run') as m_run:
+            with patch('teuthology.task.ansible.open', mock_open()):
+                m_run.return_value = ('', 1)
+                with raises(CommandFailedError):
+                    task.execute_playbook()
+                assert task.ctx.summary.get('status') == 'dead'
+
+    def test_execute_playbook_fail(self):
+        self.mocks['mkdtemp'].return_value = '/inventory/dir'
+        task = self.klass(self.ctx, self.task_config)
+        task.setup()
+        with patch.object(ansible.pexpect, 'run') as m_run:
+            with patch('teuthology.task.ansible.open', mock_open()):
+                m_run.return_value = ('', 1)
+                with raises(CommandFailedError):
+                    task.execute_playbook()
+                assert task.ctx.summary.get('status') == 'dead'
+
+    @mark.skip("Unsupported")
+    def test_generate_playbook(self):
+        pass
+
+    @mark.skip("Unsupported")
+    def test_playbook_http(self):
+        pass
+
+    @mark.skip("Unsupported")
+    def test_playbook_none(self):
+        pass
+
+    @mark.skip("Unsupported")
+    def test_playbook_wrong_type(self):
+        pass
+
+    @mark.skip("Unsupported")
+    def test_playbook_list(self):
+        pass
+
+    @mark.skip("Test needs to be reimplemented for this class")
+    def test_playbook_file_missing(self):
+        pass
diff --git a/tests/task/test_ceph_ansible.py b/tests/task/test_ceph_ansible.py
new file mode 100644 (file)
index 0000000..239eee7
--- /dev/null
@@ -0,0 +1,177 @@
+from mock import patch, MagicMock
+from pytest import skip
+from teuthology.util.compat import PY3
+if PY3:
+    from io import StringIO as StringIO
+else:
+    from io import BytesIO as StringIO
+
+from teuthology.config import FakeNamespace
+from teuthology.orchestra.cluster import Cluster
+from teuthology.orchestra.remote import Remote
+from teuthology.task import ceph_ansible
+from teuthology.task.ceph_ansible import CephAnsible
+
+from tests.task import TestTask
+
+SKIP_IRRELEVANT = "Not relevant to this subclass"
+
+
+class TestCephAnsibleTask(TestTask):
+    klass = CephAnsible
+    task_name = 'ceph_ansible'
+
+    def setup_method(self):
+        self.ctx = FakeNamespace()
+        self.ctx.cluster = Cluster()
+        self.ctx.cluster.add(Remote('user@remote1'), ['mon.0'])
+        self.ctx.cluster.add(Remote('user@remote2'), ['mds.0'])
+        self.ctx.cluster.add(Remote('user@remote3'), ['osd.0'])
+        self.ctx.summary = dict()
+        self.ctx.config = dict()
+        self.ctx.archive = '../'
+        self.task_config = dict()
+        self.start_patchers()
+
+    def start_patchers(self):
+        m_fetch_repo = MagicMock()
+        m_fetch_repo.return_value = 'PATH'
+
+        def fake_get_scratch_devices(remote):
+            return ['/dev/%s' % remote.shortname]
+
+        self.patcher_get_scratch_devices = patch(
+            'teuthology.task.ceph_ansible.get_scratch_devices',
+            fake_get_scratch_devices,
+        )
+        self.patcher_get_scratch_devices.start()
+
+        self.patcher_teardown = patch(
+            'teuthology.task.ceph_ansible.CephAnsible.teardown',
+        )
+        self.patcher_teardown.start()
+
+        def fake_set_iface_and_cidr(self):
+            self._interface = 'eth0'
+            self._cidr = '172.21.0.0/20'
+
+        self.patcher_remote = patch.multiple(
+            Remote,
+            _set_iface_and_cidr=fake_set_iface_and_cidr,
+        )
+        self.patcher_remote.start()
+
+    def stop_patchers(self):
+        self.patcher_get_scratch_devices.stop()
+        self.patcher_remote.stop()
+        self.patcher_teardown.stop()
+
+    def test_playbook_none(self):
+        skip(SKIP_IRRELEVANT)
+
+    def test_inventory_none(self):
+        skip(SKIP_IRRELEVANT)
+
+    def test_inventory_path(self):
+        skip(SKIP_IRRELEVANT)
+
+    def test_inventory_etc(self):
+        skip(SKIP_IRRELEVANT)
+
+    def test_generate_hosts_file(self):
+        self.task_config.update(dict(
+            playbook=[],
+            vars=dict(
+                osd_auto_discovery=True,
+                monitor_interface='eth0',
+                radosgw_interface='eth0',
+                public_network='172.21.0.0/20',
+            ),
+        ))
+        task = self.klass(self.ctx, self.task_config)
+        hosts_file_path = '/my/hosts/file'
+        hosts_file_obj = StringIO()
+        hosts_file_obj.name = hosts_file_path
+        with patch.object(ceph_ansible, 'NamedTemporaryFile') as m_NTF:
+            m_NTF.return_value = hosts_file_obj
+            task.generate_hosts_file()
+            m_NTF.assert_called_once_with(prefix="teuth_ansible_hosts_",
+                                          mode='w+',
+                                          delete=False)
+        assert task.generated_inventory is True
+        assert task.inventory == hosts_file_path
+        hosts_file_obj.seek(0)
+        assert hosts_file_obj.read() == '\n'.join([
+            '[mdss]',
+            'remote2',
+            '',
+            '[mons]',
+            'remote1',
+            '',
+            '[osds]',
+            'remote3',
+        ])
+
+    def test_generate_hosts_file_with_devices(self):
+        self.task_config.update(dict(
+            playbook=[],
+            vars=dict(
+                monitor_interface='eth0',
+                radosgw_interface='eth0',
+                public_network='172.21.0.0/20',
+            ),
+        ))
+        task = self.klass(self.ctx, self.task_config)
+        hosts_file_path = '/my/hosts/file'
+        hosts_file_obj = StringIO()
+        hosts_file_obj.name = hosts_file_path
+        with patch.object(ceph_ansible, 'NamedTemporaryFile') as m_NTF:
+            m_NTF.return_value = hosts_file_obj
+            task.generate_hosts_file()
+            m_NTF.assert_called_once_with(prefix="teuth_ansible_hosts_",
+                                          mode='w+',
+                                          delete=False)
+        assert task.generated_inventory is True
+        assert task.inventory == hosts_file_path
+        hosts_file_obj.seek(0)
+        assert hosts_file_obj.read() == '\n'.join([
+            '[mdss]',
+            'remote2 devices=\'[]\'',
+            '',
+            '[mons]',
+            'remote1 devices=\'[]\'',
+            '',
+            '[osds]',
+            'remote3 devices=\'["/dev/remote3"]\'',
+        ])
+
+    def test_generate_hosts_file_with_network(self):
+        self.task_config.update(dict(
+            playbook=[],
+            vars=dict(
+                osd_auto_discovery=True,
+            ),
+        ))
+        task = self.klass(self.ctx, self.task_config)
+        hosts_file_path = '/my/hosts/file'
+        hosts_file_obj = StringIO()
+        hosts_file_obj.name = hosts_file_path
+        with patch.object(ceph_ansible, 'NamedTemporaryFile') as m_NTF:
+            m_NTF.return_value = hosts_file_obj
+            task.generate_hosts_file()
+            m_NTF.assert_called_once_with(prefix="teuth_ansible_hosts_",
+                                          mode='w+',
+                                          delete=False)
+        assert task.generated_inventory is True
+        assert task.inventory == hosts_file_path
+        hosts_file_obj.seek(0)
+        assert hosts_file_obj.read() == '\n'.join([
+            '[mdss]',
+            "remote2 monitor_interface='eth0' public_network='172.21.0.0/20' radosgw_interface='eth0'",
+            '',
+            '[mons]',
+            "remote1 monitor_interface='eth0' public_network='172.21.0.0/20' radosgw_interface='eth0'",
+            '',
+            '[osds]',
+            "remote3 monitor_interface='eth0' public_network='172.21.0.0/20' radosgw_interface='eth0'",
+        ])
diff --git a/tests/task/test_console_log.py b/tests/task/test_console_log.py
new file mode 100644 (file)
index 0000000..42be217
--- /dev/null
@@ -0,0 +1,92 @@
+import os
+
+from mock import patch
+
+from teuthology.config import FakeNamespace
+from teuthology.config import config as teuth_config
+from teuthology.orchestra.cluster import Cluster
+from teuthology.orchestra.remote import Remote
+from teuthology.task.console_log import ConsoleLog
+
+from tests.task import TestTask
+
+
+class TestConsoleLog(TestTask):
+    klass = ConsoleLog
+    task_name = 'console_log'
+
+    def setup_method(self):
+        teuth_config.ipmi_domain = 'ipmi.domain'
+        teuth_config.ipmi_user = 'ipmi_user'
+        teuth_config.ipmi_password = 'ipmi_pass'
+        self.ctx = FakeNamespace()
+        self.ctx.cluster = Cluster()
+        self.ctx.cluster.add(Remote('user@remote1'), ['role1'])
+        self.ctx.cluster.add(Remote('user@remote2'), ['role2'])
+        self.ctx.config = dict()
+        self.ctx.archive = '/fake/path'
+        self.task_config = dict()
+        self.start_patchers()
+
+    def start_patchers(self):
+        self.patchers = dict()
+        self.patchers['makedirs'] = patch(
+            'teuthology.task.console_log.os.makedirs',
+        )
+        self.patchers['is_vm'] = patch(
+            'teuthology.lock.query.is_vm',
+        )
+        self.patchers['is_vm'].return_value = False
+        self.patchers['get_status'] = patch(
+            'teuthology.lock.query.get_status',
+        )
+        self.mocks = dict()
+        for name, patcher in self.patchers.items():
+            self.mocks[name] = patcher.start()
+        self.mocks['is_vm'].return_value = False
+
+    def teardown_method(self):
+        for patcher in self.patchers.values():
+            patcher.stop()
+
+    def test_enabled(self):
+        task = self.klass(self.ctx, self.task_config)
+        assert task.enabled is True
+
+    def test_disabled_noarchive(self):
+        self.ctx.archive = None
+        task = self.klass(self.ctx, self.task_config)
+        assert task.enabled is False
+
+    def test_has_ipmi_credentials(self):
+        for remote in self.ctx.cluster.remotes.keys():
+            remote.console.has_ipmi_credentials = False
+            remote.console.has_conserver = False
+        task = self.klass(self.ctx, self.task_config)
+        assert len(task.cluster.remotes.keys()) == 0
+
+    def test_remotes(self):
+        with self.klass(self.ctx, self.task_config) as task:
+            assert len(task.cluster.remotes) == len(self.ctx.cluster.remotes)
+
+    @patch('teuthology.orchestra.console.PhysicalConsole')
+    def test_begin(self, m_pconsole):
+        with self.klass(self.ctx, self.task_config) as task:
+            assert len(task.processes) == len(self.ctx.cluster.remotes)
+            expected_log_paths = []
+            for remote in task.cluster.remotes.keys():
+                expected_log_paths.append(
+                    os.path.join(self.ctx.archive, 'console_logs', '%s.log' % remote.shortname)
+                )
+            assert len(m_pconsole().spawn_sol_log.call_args_list) == len(task.cluster.remotes)
+            got_log_paths = [c[0][0] for c in m_pconsole().spawn_sol_log.call_args_list]
+            assert got_log_paths == expected_log_paths
+
+    @patch('teuthology.orchestra.console.PhysicalConsole')
+    def test_end(self, m_pconsole):
+        m_proc = m_pconsole().spawn_sol_log.return_value
+        m_proc.poll.return_value = None
+        with self.klass(self.ctx, self.task_config):
+            pass
+        assert len(m_proc.terminate.call_args_list) == len(self.ctx.cluster.remotes)
+        assert len(m_proc.kill.call_args_list) == len(self.ctx.cluster.remotes)
diff --git a/tests/task/test_install.py b/tests/task/test_install.py
new file mode 100644 (file)
index 0000000..b5fc5a8
--- /dev/null
@@ -0,0 +1,446 @@
+import os
+import pytest
+import yaml
+
+from mock import patch, Mock
+from pytest import mark
+
+from teuthology.task import install
+
+
+class TestInstall(object):
+
+    def _get_default_package_list(self, project='ceph', debug=False):
+        path = os.path.join(
+            os.path.dirname(__file__),
+            '..', '..', 'teuthology', 'task', 'install', 'packages.yaml',
+        )
+        pkgs = yaml.safe_load(open(path))[project]
+        if not debug:
+            pkgs['deb'] = [p for p in pkgs['deb']
+                                        if not p.endswith('-dbg')]
+            pkgs['rpm'] = [p for p in pkgs['rpm']
+                                        if not p.endswith('-debuginfo')]
+        return pkgs
+
+    def test_get_package_list_debug(self):
+        default_pkgs = self._get_default_package_list(debug=True)
+        default_pkgs['rpm'].sort()
+        default_pkgs['deb'].sort()
+        config = dict(debuginfo=True)
+        result = install.get_package_list(ctx=None, config=config)
+        result['rpm'].sort()
+        result['deb'].sort()
+        assert result == default_pkgs
+
+    def test_get_package_list_no_debug(self):
+        default_pkgs = self._get_default_package_list(debug=False)
+        default_pkgs['rpm'].sort()
+        default_pkgs['deb'].sort()
+        config = dict(debuginfo=False)
+        result = install.get_package_list(ctx=None, config=config)
+        result['rpm'].sort()
+        result['deb'].sort()
+        assert result == default_pkgs
+
+    def test_get_package_list_custom_rpm(self):
+        default_pkgs = self._get_default_package_list(debug=False)
+        default_pkgs['rpm'].sort()
+        default_pkgs['deb'].sort()
+        rpms = ['rpm1', 'rpm2', 'rpm2-debuginfo']
+        config = dict(packages=dict(rpm=rpms))
+        result = install.get_package_list(ctx=None, config=config)
+        result['rpm'].sort()
+        result['deb'].sort()
+        assert result['rpm'] == ['rpm1', 'rpm2']
+        assert result['deb'] == default_pkgs['deb']
+
+    @patch("teuthology.task.install._get_builder_project")
+    @patch("teuthology.task.install.packaging.get_package_version")
+    def test_get_upgrade_version(self, m_get_package_version,
+                                         m_gitbuilder_project):
+        gb = Mock()
+        gb.version = "11.0.0"
+        gb.project = "ceph"
+        m_gitbuilder_project.return_value = gb
+        m_get_package_version.return_value = "11.0.0"
+        install.get_upgrade_version(Mock(), Mock(), Mock())
+
+    @patch("teuthology.task.install._get_builder_project")
+    @patch("teuthology.task.install.packaging.get_package_version")
+    def test_verify_ceph_version_success(self, m_get_package_version,
+                                         m_gitbuilder_project):
+        gb = Mock()
+        gb.version = "0.89.0"
+        gb.project = "ceph"
+        m_gitbuilder_project.return_value = gb
+        m_get_package_version.return_value = "0.89.0"
+        config = dict()
+        install.verify_package_version(Mock(), config, Mock())
+
+    @patch("teuthology.task.install._get_builder_project")
+    @patch("teuthology.task.install.packaging.get_package_version")
+    def test_verify_ceph_version_failed(self, m_get_package_version,
+                                        m_gitbuilder_project):
+        gb = Mock()
+        gb.version = "0.89.0"
+        gb.project = "ceph"
+        m_gitbuilder_project.return_value = gb
+        m_get_package_version.return_value = "0.89.1"
+        config = dict()
+        with pytest.raises(RuntimeError):
+            install.verify_package_version(Mock(), config, Mock())
+
+    def test_get_flavor_default(self):
+        config = dict()
+        assert install.get_flavor(config) == 'default'
+
+    def test_get_flavor_simple(self):
+        config = dict(
+            flavor='notcmalloc'
+        )
+        assert install.get_flavor(config) == 'notcmalloc'
+
+    def test_get_flavor_valgrind(self):
+        config = dict(
+            valgrind=True
+        )
+        assert install.get_flavor(config) == 'notcmalloc'
+
+    def test_upgrade_is_downgrade(self):
+        assert_ok_vals = [
+            ('9.0.0', '10.0.0'),
+            ('10.2.2-63-g8542898-1trusty', '10.2.2-64-gabcdef1-1trusty'),
+            ('11.0.0-918.g13c13c7', '11.0.0-2165.gabcdef1')
+        ]
+        for t in assert_ok_vals:
+            assert install._upgrade_is_downgrade(t[0], t[1]) == False
+
+    @patch("teuthology.packaging.get_package_version")
+    @patch("teuthology.misc.get_system_type")
+    @patch("teuthology.task.install.verify_package_version")
+    @patch("teuthology.task.install.get_upgrade_version")
+    def test_upgrade_common(self,
+                            m_get_upgrade_version,
+                            m_verify_package_version,
+                            m_get_system_type,
+                            m_get_package_version):
+        expected_system_type = 'deb'
+        def make_remote():
+            remote = Mock()
+            remote.arch = 'x86_64'
+            remote.os = Mock()
+            remote.os.name = 'ubuntu'
+            remote.os.version = '14.04'
+            remote.os.codename = 'trusty'
+            remote.system_type = expected_system_type
+            return remote
+        ctx = Mock()
+        class cluster:
+            remote1 = make_remote()
+            remote2 = make_remote()
+            remotes = {
+                remote1: ['client.0'],
+                remote2: ['mon.a','osd.0'],
+            }
+            def only(self, role):
+                result = Mock()
+                if role in ('client.0',):
+                    result.remotes = { cluster.remote1: None }
+                if role in ('osd.0', 'mon.a'):
+                    result.remotes = { cluster.remote2: None }
+                return result
+        ctx.cluster = cluster()
+        config = {
+            'client.0': {
+                'sha1': 'expectedsha1',
+            },
+        }
+        ctx.config = {
+            'roles': [ ['client.0'], ['mon.a','osd.0'] ],
+            'tasks': [
+                {
+                    'install.upgrade': config,
+                },
+            ],
+        }
+        m_get_upgrade_version.return_value = "11.0.0"
+        m_get_package_version.return_value = "10.2.4"
+        m_get_system_type.return_value = "deb"
+        def upgrade(ctx, node, remote, pkgs, system_type):
+            assert system_type == expected_system_type
+        assert install.upgrade_common(ctx, config, upgrade) == 1
+        expected_config = {
+            'project': 'ceph',
+            'sha1': 'expectedsha1',
+        }
+        m_verify_package_version.assert_called_with(ctx,
+                                                    expected_config,
+                                                    cluster.remote1)
+    def test_upgrade_remote_to_config(self):
+        expected_system_type = 'deb'
+        def make_remote():
+            remote = Mock()
+            remote.arch = 'x86_64'
+            remote.os = Mock()
+            remote.os.name = 'ubuntu'
+            remote.os.version = '14.04'
+            remote.os.codename = 'trusty'
+            remote.system_type = expected_system_type
+            return remote
+        ctx = Mock()
+        class cluster:
+            remote1 = make_remote()
+            remote2 = make_remote()
+            remotes = {
+                remote1: ['client.0'],
+                remote2: ['mon.a','osd.0'],
+            }
+            def only(self, role):
+                result = Mock()
+                if role in ('client.0',):
+                    result.remotes = { cluster.remote1: None }
+                elif role in ('osd.0', 'mon.a'):
+                    result.remotes = { cluster.remote2: None }
+                else:
+                    result.remotes = None
+                return result
+        ctx.cluster = cluster()
+        ctx.config = {
+            'roles': [ ['client.0'], ['mon.a','osd.0'] ],
+        }
+
+        # nothing -> nothing
+        assert install.upgrade_remote_to_config(ctx, {}) == {}
+
+        # select the remote for the osd.0 role
+        # the 'ignored' role does not exist and is ignored
+        # the remote for mon.a is the same as for osd.0 and
+        # is silently ignored (actually it could be the other
+        # way around, depending on how the keys are hashed)
+        config = {
+            'osd.0': {
+                'sha1': 'expectedsha1',
+            },
+            'ignored': None,
+            'mon.a': {
+                'sha1': 'expectedsha1',
+            },
+        }
+        expected_config = {
+            cluster.remote2: {
+                'project': 'ceph',
+                'sha1': 'expectedsha1',
+            },
+        }
+        assert install.upgrade_remote_to_config(ctx, config) == expected_config
+
+        # select all nodes, regardless
+        config = {
+            'all': {
+                'sha1': 'expectedsha1',
+            },
+        }
+        expected_config = {
+            cluster.remote1: {
+                'project': 'ceph',
+                'sha1': 'expectedsha1',
+            },
+            cluster.remote2: {
+                'project': 'ceph',
+                'sha1': 'expectedsha1',
+            },
+        }
+        assert install.upgrade_remote_to_config(ctx, config) == expected_config
+
+        # verify that install overrides are used as default
+        # values for the upgrade task, not as override
+        ctx.config['overrides'] = {
+            'install': {
+                'ceph': {
+                    'sha1': 'overridesha1',
+                    'tag': 'overridetag',
+                    'branch': 'overridebranch',
+                },
+            },
+        }
+        config = {
+            'client.0': {
+                'sha1': 'expectedsha1',
+            },
+            'osd.0': {
+            },
+        }
+        expected_config = {
+            cluster.remote1: {
+                'project': 'ceph',
+                'sha1': 'expectedsha1',
+            },
+            cluster.remote2: {
+                'project': 'ceph',
+                'sha1': 'overridesha1',
+                'tag': 'overridetag',
+                'branch': 'overridebranch',
+            },
+        }
+        assert install.upgrade_remote_to_config(ctx, config) == expected_config
+
+
+    @patch("teuthology.task.install.packaging.get_package_version")
+    @patch("teuthology.task.install.redhat.set_deb_repo")
+    def test_rh_install_deb_pkgs(self, m_set_rh_deb_repo, m_get_pkg_version):
+        ctx = Mock()
+        remote = Mock()
+        version = '1.3.2'
+        rh_ds_yaml = dict()
+        rh_ds_yaml = {
+            'versions': {'deb': {'mapped': {'1.3.2': '0.94.5'}}},
+            'pkgs': {'deb': ['pkg1', 'pkg2']},
+            'extra_system_packages': {'deb': ['es_pkg1', 'es_pkg2']},
+            'extra_packages': {'deb': ['e_pkg1', 'e_pkg2']},
+        }
+        m_get_pkg_version.return_value = "0.94.5"
+        install.redhat.install_deb_pkgs(ctx, remote, version, rh_ds_yaml)
+
+    @patch("teuthology.task.install.packaging.get_package_version")
+    def test_rh_install_pkgs(self, m_get_pkg_version):
+        ctx = Mock()
+        remote = Mock()
+        version = '1.3.2'
+        rh_ds_yaml = dict()
+        rh_ds_yaml = {
+            'versions': {'rpm': {'mapped': {'1.3.2': '0.94.5',
+                                            '1.3.1': '0.94.3'}}},
+            'pkgs': {'rpm': ['pkg1', 'pkg2']},
+            'extra_system_packages': {'rpm': ['es_pkg1', 'es_pkg2']},
+            'extra_packages': {'rpm': ['e_pkg1', 'e_pkg2']},
+        }
+
+        m_get_pkg_version.return_value = "0.94.5"
+        install.redhat.install_pkgs(ctx, remote, version, rh_ds_yaml)
+        version = '1.3.1'
+        with pytest.raises(RuntimeError) as e:
+            install.redhat.install_pkgs(ctx, remote, version, rh_ds_yaml)
+        assert "Version check failed" in str(e)
+
+    @mark.parametrize(
+        'conf, expect',
+        [
+            [
+                {
+                    'tasks': [ { 'install': { 'clean': True, }, }, ],
+                    'overrides': {
+                        'install': {
+                            'ceph': {
+                                 'extra_system_packages': ['alpha'],
+                                 'flavor': 'default',
+                                 'sha1': '0123456789abcdef0123456789abcdef01234567',
+                             },
+                            'extra_system_packages': {
+                                'deb': [],
+                                'rpm': ['xerxes', 'yellow'],
+                            },
+                        },
+                    },
+                },
+                {
+                    'deb': ['alpha'],
+                    'rpm': ['alpha', 'xerxes', 'yellow'],
+                }
+            ],
+            [
+                {
+                    'tasks': [ { 'install': { 'clean': True, }, }, ],
+                    'overrides': {
+                        'install': {
+                            'ceph': {
+                                 'extra_system_packages': {
+                                     'deb': [],
+                                     'rpm': ['xerxes', 'yellow'],
+                                 },
+                                 'flavor': 'default',
+                                 'sha1': '0123456789abcdef0123456789abcdef01234567',
+                             },
+                            'extra_system_packages': ['alpha'],
+                        },
+                    },
+                },
+                {
+                    'deb': ['alpha'],
+                    'rpm': ['xerxes', 'yellow', 'alpha'],
+                }
+            ],
+            [
+                {
+                    'tasks': [ { 'install': { 'clean': True, }, }, ],
+                    'overrides': {
+                        'install': {
+                            'ceph': {
+                                 'flavor': 'default',
+                                 'sha1': '0123456789abcdef0123456789abcdef01234567',
+                             },
+                            'extra_system_packages': {
+                                'deb': [],
+                                'rpm': ['xerxes', 'yellow'],
+                            },
+                        },
+                    },
+                },
+                {
+                    'deb': [],
+                    'rpm': ['xerxes', 'yellow'],
+                }
+            ],
+            [
+                {
+                    'tasks': [ { 'install': { 'clean': True, }, }, ],
+                    'overrides': {
+                        'install': {
+                            'ceph': {
+                                 'flavor': 'default',
+                                 'sha1': '0123456789abcdef0123456789abcdef01234567',
+                             },
+                            'extra_system_packages': ['xerxes', 'yellow'],
+                        },
+                    },
+                },
+                {
+                    'deb': ['xerxes', 'yellow'],
+                    'rpm': ['xerxes', 'yellow'],
+                }
+            ],
+            [
+                {
+                    'tasks': [ { 'install': { 'clean': True, }, }, ],
+                    'overrides': { 'install': {
+                            'ceph': { 'flavor': 'default', 'sha1': '012345', },
+                            'extra_system_packages': { 'rpm': ['xerxes', 'yellow'], },
+                        },
+                    },
+                },
+                { 'deb': [], 'rpm': ['xerxes', 'yellow'], }
+
+            ],
+            [
+                {
+                    'tasks': [ { 'install': { 'clean': True,
+                                             'extra_system_packages': { 'deb': ['alpha'], 'rpm': ['bravo'] },
+                                             }, }, ],
+                    'overrides': { 'install': {
+                            'ceph': { 'flavor': 'default', 'sha1': '012345', },
+                            'extra_system_packages': { 'rpm': ['xerxes', 'yellow'], },
+                        },
+                    },
+                },
+                { 'deb': ['alpha'], 'rpm': ['bravo', 'xerxes', 'yellow'], }
+
+            ],
+
+        ]
+    )
+    def test_install_extra_system_packages(self, conf, expect):
+        install_task = conf.get('tasks')[0].get('install')
+        install_overrides = conf.get('overrides').get('install', {})
+        install._override_extra_system_packages(install_task, 'ceph', install_overrides)
+
+        assert install_task.get('extra_system_packages') == expect
diff --git a/tests/task/test_internal.py b/tests/task/test_internal.py
new file mode 100644 (file)
index 0000000..1340b18
--- /dev/null
@@ -0,0 +1,57 @@
+from teuthology.config import FakeNamespace
+from teuthology.task import internal
+
+
+class TestInternal(object):
+    def setup_method(self):
+        self.ctx = FakeNamespace()
+        self.ctx.config = dict()
+
+    def test_buildpackages_prep(self):
+        #
+        # no buildpackages nor install tasks
+        #
+        self.ctx.config = { 'tasks': [] }
+        assert internal.buildpackages_prep(self.ctx,
+                                           self.ctx.config) == internal.BUILDPACKAGES_NOTHING
+        #
+        # make the buildpackages tasks the first to run
+        #
+        self.ctx.config = {
+            'tasks': [ { 'atask': None },
+                       { 'internal.buildpackages_prep': None },
+                       { 'btask': None },
+                       { 'install': None },
+                       { 'buildpackages': None } ],
+        }
+        assert internal.buildpackages_prep(self.ctx,
+                                           self.ctx.config) == internal.BUILDPACKAGES_FIRST
+        assert self.ctx.config == {
+            'tasks': [ { 'atask': None },
+                       { 'internal.buildpackages_prep': None },
+                       { 'buildpackages': None },
+                       { 'btask': None },
+                       { 'install': None } ],
+        }
+        #
+        # the buildpackages task already the first task to run
+        #
+        assert internal.buildpackages_prep(self.ctx,
+                                           self.ctx.config) == internal.BUILDPACKAGES_OK
+        #
+        # no buildpackages task
+        #
+        self.ctx.config = {
+            'tasks': [ { 'install': None } ],
+        }
+        assert internal.buildpackages_prep(self.ctx,
+                                           self.ctx.config) == internal.BUILDPACKAGES_NOTHING
+        #
+        # no install task: the buildpackages task must be removed
+        #
+        self.ctx.config = {
+            'tasks': [ { 'buildpackages': None } ],
+        }
+        assert internal.buildpackages_prep(self.ctx,
+                                           self.ctx.config) == internal.BUILDPACKAGES_REMOVED
+        assert self.ctx.config == {'tasks': []}
diff --git a/tests/task/test_kernel.py b/tests/task/test_kernel.py
new file mode 100644 (file)
index 0000000..593b204
--- /dev/null
@@ -0,0 +1,243 @@
+from teuthology.config import FakeNamespace
+from teuthology.orchestra.cluster import Cluster
+from teuthology.orchestra.remote import Remote
+from teuthology.task.kernel import (
+    normalize_and_apply_overrides,
+    CONFIG_DEFAULT,
+    TIMEOUT_DEFAULT,
+)
+
+class TestKernelNormalizeAndApplyOverrides(object):
+
+    def setup_method(self):
+        self.ctx = FakeNamespace()
+        self.ctx.cluster = Cluster()
+        self.ctx.cluster.add(Remote('remote1'), ['mon.a', 'client.0'])
+        self.ctx.cluster.add(Remote('remote2'), ['osd.0', 'osd.1', 'osd.2'])
+        self.ctx.cluster.add(Remote('remote3'), ['client.1'])
+
+    def test_default(self):
+        config = {}
+        overrides = {}
+        config, t = normalize_and_apply_overrides(self.ctx, config, overrides)
+        assert config == {
+            'mon.a': CONFIG_DEFAULT,
+            'osd.0': CONFIG_DEFAULT,
+            'osd.1': CONFIG_DEFAULT,
+            'osd.2': CONFIG_DEFAULT,
+            'client.0': CONFIG_DEFAULT,
+            'client.1': CONFIG_DEFAULT,
+        }
+        assert t == TIMEOUT_DEFAULT
+
+    def test_timeout_default(self):
+        config = {
+            'client.0': {'branch': 'testing'},
+        }
+        overrides = {}
+        config, t = normalize_and_apply_overrides(self.ctx, config, overrides)
+        assert config == {
+            'client.0': {'branch': 'testing'},
+        }
+        assert t == TIMEOUT_DEFAULT
+
+    def test_timeout(self):
+        config = {
+            'client.0': {'branch': 'testing'},
+            'timeout': 100,
+        }
+        overrides = {}
+        config, t = normalize_and_apply_overrides(self.ctx, config, overrides)
+        assert config == {
+            'client.0': {'branch': 'testing'},
+        }
+        assert t == 100
+
+    def test_override_timeout(self):
+        config = {
+            'client.0': {'branch': 'testing'},
+            'timeout': 100,
+        }
+        overrides = {
+            'timeout': 200,
+        }
+        config, t = normalize_and_apply_overrides(self.ctx, config, overrides)
+        assert config == {
+            'client.0': {'branch': 'testing'},
+        }
+        assert t == 200
+
+    def test_override_same_version_key(self):
+        config = {
+            'client.0': {'branch': 'testing'},
+        }
+        overrides = {
+            'client.0': {'branch': 'wip-foobar'},
+        }
+        config, t = normalize_and_apply_overrides(self.ctx, config, overrides)
+        assert config == {
+            'client.0': {'branch': 'wip-foobar'},
+        }
+        assert t == TIMEOUT_DEFAULT
+
+    def test_override_different_version_key(self):
+        config = {
+            'client.0': {'branch': 'testing'},
+        }
+        overrides = {
+            'client.0': {'tag': 'v4.1'},
+        }
+        config, t = normalize_and_apply_overrides(self.ctx, config, overrides)
+        assert config == {
+            'client.0': {'tag': 'v4.1'},
+        }
+        assert t == TIMEOUT_DEFAULT
+
+    def test_override_actual(self):
+        config = {
+            'osd.1': {'tag': 'v4.1'},
+            'client.0': {'branch': 'testing'},
+        }
+        overrides = {
+            'osd.1': {'koji': 1234, 'kdb': True},
+        }
+        config, t = normalize_and_apply_overrides(self.ctx, config, overrides)
+        assert config == {
+            'osd.1': {'koji': 1234, 'kdb': True},
+            'client.0': {'branch': 'testing'},
+        }
+        assert t == TIMEOUT_DEFAULT
+
+    def test_override_actual_with_generic(self):
+        config = {
+            'osd.1': {'tag': 'v4.1', 'kdb': False},
+            'client.0': {'branch': 'testing'},
+        }
+        overrides = {
+            'osd': {'koji': 1234},
+        }
+        config, t = normalize_and_apply_overrides(self.ctx, config, overrides)
+        assert config == {
+            'osd.0': {'koji': 1234},
+            'osd.1': {'koji': 1234, 'kdb': False},
+            'osd.2': {'koji': 1234},
+            'client.0': {'branch': 'testing'},
+        }
+        assert t == TIMEOUT_DEFAULT
+
+    def test_override_actual_with_top_level(self):
+        config = {
+            'osd.1': {'tag': 'v4.1'},
+            'client.0': {'branch': 'testing', 'kdb': False},
+        }
+        overrides = {'koji': 1234, 'kdb': True}
+        config, t = normalize_and_apply_overrides(self.ctx, config, overrides)
+        assert config == {
+            'mon.a': {'koji': 1234, 'kdb': True},
+            'osd.0': {'koji': 1234, 'kdb': True},
+            'osd.1': {'koji': 1234, 'kdb': True},
+            'osd.2': {'koji': 1234, 'kdb': True},
+            'client.0': {'koji': 1234, 'kdb': True},
+            'client.1': {'koji': 1234, 'kdb': True},
+        }
+        assert t == TIMEOUT_DEFAULT
+
+    def test_override_generic(self):
+        config = {
+            'osd': {'tag': 'v4.1'},
+            'client': {'branch': 'testing'},
+        }
+        overrides = {
+            'client': {'koji': 1234, 'kdb': True},
+        }
+        config, t = normalize_and_apply_overrides(self.ctx, config, overrides)
+        assert config == {
+            'osd.0': {'tag': 'v4.1'},
+            'osd.1': {'tag': 'v4.1'},
+            'osd.2': {'tag': 'v4.1'},
+            'client.0': {'koji': 1234, 'kdb': True},
+            'client.1': {'koji': 1234, 'kdb': True},
+        }
+        assert t == TIMEOUT_DEFAULT
+
+    def test_override_generic_with_top_level(self):
+        config = {
+            'osd': {'tag': 'v4.1'},
+            'client': {'branch': 'testing', 'kdb': False},
+        }
+        overrides = {
+            'client': {'koji': 1234},
+        }
+        config, t = normalize_and_apply_overrides(self.ctx, config, overrides)
+        assert config == {
+            'osd.0': {'tag': 'v4.1'},
+            'osd.1': {'tag': 'v4.1'},
+            'osd.2': {'tag': 'v4.1'},
+            'client.0': {'koji': 1234, 'kdb': False},
+            'client.1': {'koji': 1234, 'kdb': False},
+        }
+        assert t == TIMEOUT_DEFAULT
+
+    def test_override_generic_with_actual(self):
+        config = {
+            'osd': {'tag': 'v4.1', 'kdb': False},
+            'client': {'branch': 'testing'},
+        }
+        overrides = {
+            'osd.2': {'koji': 1234, 'kdb': True},
+        }
+        config, t = normalize_and_apply_overrides(self.ctx, config, overrides)
+        assert config == {
+            'osd.0': {'tag': 'v4.1', 'kdb': False},
+            'osd.1': {'tag': 'v4.1', 'kdb': False},
+            'osd.2': {'koji': 1234, 'kdb': True},
+            'client.0': {'branch': 'testing'},
+            'client.1': {'branch': 'testing'},
+        }
+        assert t == TIMEOUT_DEFAULT
+
+    def test_override_top_level(self):
+        config = {'branch': 'testing'}
+        overrides = {'koji': 1234, 'kdb': True}
+        config, t = normalize_and_apply_overrides(self.ctx, config, overrides)
+        assert config == {
+            'mon.a': {'koji': 1234, 'kdb': True},
+            'osd.0': {'koji': 1234, 'kdb': True},
+            'osd.1': {'koji': 1234, 'kdb': True},
+            'osd.2': {'koji': 1234, 'kdb': True},
+            'client.0': {'koji': 1234, 'kdb': True},
+            'client.1': {'koji': 1234, 'kdb': True},
+        }
+        assert t == TIMEOUT_DEFAULT
+
+    def test_override_top_level_with_actual(self):
+        config = {'branch': 'testing', 'kdb': False}
+        overrides = {
+            'mon.a': {'koji': 1234},
+        }
+        config, t = normalize_and_apply_overrides(self.ctx, config, overrides)
+        assert config == {
+            'mon.a': {'koji': 1234, 'kdb': False},
+            'osd.0': {'branch': 'testing', 'kdb': False},
+            'osd.1': {'branch': 'testing', 'kdb': False},
+            'osd.2': {'branch': 'testing', 'kdb': False},
+            'client.0': {'branch': 'testing', 'kdb': False},
+            'client.1': {'branch': 'testing', 'kdb': False},
+        }
+        assert t == TIMEOUT_DEFAULT
+
+    def test_override_top_level_with_generic(self):
+        config = {'branch': 'testing', 'kdb': False}
+        overrides = {
+            'client': {'koji': 1234, 'kdb': True},
+        }
+        config, t = normalize_and_apply_overrides(self.ctx, config, overrides)
+        assert config == {
+            'mon.a': {'branch': 'testing', 'kdb': False},
+            'osd.0': {'branch': 'testing', 'kdb': False},
+            'osd.1': {'branch': 'testing', 'kdb': False},
+            'osd.2': {'branch': 'testing', 'kdb': False},
+            'client.0': {'koji': 1234, 'kdb': True},
+            'client.1': {'koji': 1234, 'kdb': True},
+        }
+        assert t == TIMEOUT_DEFAULT
diff --git a/tests/task/test_pcp.py b/tests/task/test_pcp.py
new file mode 100644 (file)
index 0000000..424083a
--- /dev/null
@@ -0,0 +1,379 @@
+import os
+import requests
+
+from teuthology.util.compat import parse_qs, urljoin
+
+from mock import patch, DEFAULT, Mock, mock_open, call
+from pytest import raises
+
+from teuthology.config import config, FakeNamespace
+from teuthology.orchestra.cluster import Cluster
+from teuthology.orchestra.remote import Remote
+from teuthology.orchestra.run import Raw
+from teuthology.task.pcp import (PCPDataSource, PCPArchive, PCPGrapher,
+                                 GrafanaGrapher, GraphiteGrapher, PCP)
+
+from tests.task import TestTask
+
+pcp_host = 'http://pcp.front.sepia.ceph.com:44323/'
+
+
+class TestPCPDataSource(object):
+    klass = PCPDataSource
+
+    def setup_method(self):
+        config.pcp_host = pcp_host
+
+    def test_init(self):
+        hosts = ['host1', 'host2']
+        time_from = 'now-2h'
+        time_until = 'now'
+        obj = self.klass(
+            hosts=hosts,
+            time_from=time_from,
+            time_until=time_until,
+        )
+        assert obj.hosts == hosts
+        assert obj.time_from == time_from
+        assert obj.time_until == time_until
+
+
+class TestPCPArchive(TestPCPDataSource):
+    klass = PCPArchive
+
+    def test_get_archive_input_dir(self):
+        hosts = ['host1', 'host2']
+        time_from = 'now-1d'
+        obj = self.klass(
+            hosts=hosts,
+            time_from=time_from,
+        )
+        assert obj.get_archive_input_dir('host1') == \
+            '/var/log/pcp/pmlogger/host1'
+
+    def test_get_pmlogextract_cmd(self):
+        obj = self.klass(
+            hosts=['host1'],
+            time_from='now-3h',
+            time_until='now-1h',
+        )
+        expected = [
+            'pmlogextract',
+            '-S', 'now-3h',
+            '-T', 'now-1h',
+            Raw('/var/log/pcp/pmlogger/host1/*.0'),
+        ]
+        assert obj.get_pmlogextract_cmd('host1') == expected
+
+    def test_format_time(self):
+        assert self.klass._format_time(1462893484) == \
+            '@ Tue May 10 15:18:04 2016'
+
+    def test_format_time_now(self):
+        assert self.klass._format_time('now-1h') == 'now-1h'
+
+
+class TestPCPGrapher(TestPCPDataSource):
+    klass = PCPGrapher
+
+    def test_init(self):
+        hosts = ['host1', 'host2']
+        time_from = 'now-2h'
+        time_until = 'now'
+        obj = self.klass(
+            hosts=hosts,
+            time_from=time_from,
+            time_until=time_until,
+        )
+        assert obj.hosts == hosts
+        assert obj.time_from == time_from
+        assert obj.time_until == time_until
+        expected_url = urljoin(config.pcp_host, self.klass._endpoint)
+        assert obj.base_url == expected_url
+
+
+class TestGrafanaGrapher(TestPCPGrapher):
+    klass = GrafanaGrapher
+
+    def test_build_graph_url(self):
+        hosts = ['host1']
+        time_from = 'now-3h'
+        time_until = 'now-1h'
+        obj = self.klass(
+            hosts=hosts,
+            time_from=time_from,
+            time_until=time_until,
+        )
+        base_url = urljoin(
+            config.pcp_host,
+            'grafana/index.html#/dashboard/script/index.js',
+        )
+        assert obj.base_url == base_url
+        got_url = obj.build_graph_url()
+        parsed_query = parse_qs(got_url.split('?')[1])
+        assert parsed_query['hosts'] == hosts
+        assert len(parsed_query['time_from']) == 1
+        assert parsed_query['time_from'][0] == time_from
+        assert len(parsed_query['time_to']) == 1
+        assert parsed_query['time_to'][0] == time_until
+
+    def test_format_time(self):
+        assert self.klass._format_time(1462893484) == \
+            '2016-05-10T15:18:04'
+
+    def test_format_time_now(self):
+        assert self.klass._format_time('now-1h') == 'now-1h'
+
+
+class TestGraphiteGrapher(TestPCPGrapher):
+    klass = GraphiteGrapher
+
+    def test_build_graph_urls(self):
+        obj = self.klass(
+            hosts=['host1', 'host2'],
+            time_from='now-3h',
+            time_until='now-1h',
+        )
+        expected_urls = [obj.get_graph_url(m) for m in obj.metrics]
+        obj.build_graph_urls()
+        built_urls = []
+        for metric in obj.graphs.keys():
+            built_urls.append(obj.graphs[metric]['url'])
+        assert len(built_urls) == len(expected_urls)
+        assert sorted(built_urls) == sorted(expected_urls)
+
+    def test_check_dest_dir(self):
+        obj = self.klass(
+            hosts=['host1'],
+            time_from='now-3h',
+        )
+        assert obj.dest_dir is None
+        with raises(RuntimeError):
+            obj._check_dest_dir()
+
+    def test_generate_html_dynamic(self):
+        obj = self.klass(
+            hosts=['host1'],
+            time_from='now-3h',
+        )
+        html = obj.generate_html()
+        assert config.pcp_host in html
+
+    def test_download_graphs(self):
+        dest_dir = '/fake/path'
+        obj = self.klass(
+            hosts=['host1'],
+            time_from='now-3h',
+            dest_dir=dest_dir,
+        )
+        _format = obj.graph_defaults.get('format')
+        with patch('teuthology.task.pcp.requests.get', create=True) as m_get:
+            m_resp = Mock()
+            m_resp.ok = True
+            m_get.return_value = m_resp
+            with patch('teuthology.task.pcp.open', mock_open(), create=True):
+                obj.download_graphs()
+        expected_filenames = []
+        for metric in obj.metrics:
+            expected_filenames.append(
+                "{}.{}".format(
+                    os.path.join(
+                        dest_dir,
+                        obj._sanitize_metric_name(metric),
+                    ),
+                    _format,
+                )
+            )
+        graph_filenames = []
+        for metric in obj.graphs.keys():
+            graph_filenames.append(obj.graphs[metric]['file'])
+        assert sorted(graph_filenames) == sorted(expected_filenames)
+
+    def test_generate_html_static(self):
+        obj = self.klass(
+            hosts=['host1'],
+            time_from='now-3h',
+            dest_dir='/fake/path',
+        )
+        with patch('teuthology.task.pcp.requests.get', create=True) as m_get:
+            m_resp = Mock()
+            m_resp.ok = True
+            m_get.return_value = m_resp
+            with patch('teuthology.task.pcp.open', mock_open(), create=True):
+                obj.download_graphs()
+        html = obj.generate_html(mode='static')
+        assert config.pcp_host not in html
+
+    def test_sanitize_metric_name(self):
+        sanitized_metrics = {
+            'foo.bar': 'foo.bar',
+            'foo.*': 'foo._all_',
+            'foo.bar baz': 'foo.bar_baz',
+            'foo.*.bar baz': 'foo._all_.bar_baz',
+        }
+        for in_, out in sanitized_metrics.items():
+            assert self.klass._sanitize_metric_name(in_) == out
+
+    def test_get_target_globs(self):
+        obj = self.klass(
+            hosts=['host1'],
+            time_from='now-3h',
+        )
+        assert obj.get_target_globs() == ['*host1*']
+        assert obj.get_target_globs('a.metric') == ['*host1*.a.metric']
+        obj.hosts.append('host2')
+        assert obj.get_target_globs() == ['*host1*', '*host2*']
+        assert obj.get_target_globs('a.metric') == \
+            ['*host1*.a.metric', '*host2*.a.metric']
+
+
+class TestPCPTask(TestTask):
+    klass = PCP
+    task_name = 'pcp'
+
+    def setup_method(self):
+        self.ctx = FakeNamespace()
+        self.ctx.cluster = Cluster()
+        self.ctx.cluster.add(Remote('user@remote1'), ['role1'])
+        self.ctx.cluster.add(Remote('user@remote2'), ['role2'])
+        self.ctx.config = dict()
+        self.task_config = dict()
+        config.pcp_host = pcp_host
+
+    def test_init(self):
+        task = self.klass(self.ctx, self.task_config)
+        assert task.stop_time == 'now'
+
+    def test_disabled(self):
+        config.pcp_host = None
+        with self.klass(self.ctx, self.task_config) as task:
+            assert task.enabled is False
+            assert not hasattr(task, 'grafana')
+            assert not hasattr(task, 'graphite')
+            assert not hasattr(task, 'archiver')
+
+    def test_setup(self):
+        with patch.multiple(
+            self.klass,
+            setup_collectors=DEFAULT,
+            begin=DEFAULT,
+            end=DEFAULT,
+        ):
+            with self.klass(self.ctx, self.task_config) as task:
+                task.setup_collectors.assert_called_once_with()
+                assert isinstance(task.start_time, int)
+
+    def test_setup_collectors(self):
+        with patch.multiple(
+            self.klass,
+            begin=DEFAULT,
+            end=DEFAULT,
+        ):
+            with self.klass(self.ctx, self.task_config) as task:
+                assert hasattr(task, 'grafana')
+                assert not hasattr(task, 'graphite')
+                assert not hasattr(task, 'archiver')
+            self.task_config['grafana'] = False
+            with self.klass(self.ctx, self.task_config) as task:
+                assert not hasattr(task, 'grafana')
+
+    @patch('os.makedirs')
+    def test_setup_grafana(self, m_makedirs):
+        with patch.multiple(
+            self.klass,
+            begin=DEFAULT,
+            end=DEFAULT,
+        ):
+            self.ctx.archive = '/fake/path'
+            with self.klass(self.ctx, self.task_config) as task:
+                assert hasattr(task, 'grafana')
+            self.task_config['grafana'] = False
+            with self.klass(self.ctx, self.task_config) as task:
+                assert not hasattr(task, 'grafana')
+
+    @patch('os.makedirs')
+    @patch('teuthology.task.pcp.GraphiteGrapher')
+    def test_setup_graphite(self, m_graphite_grapher, m_makedirs):
+        with patch.multiple(
+            self.klass,
+            begin=DEFAULT,
+            end=DEFAULT,
+        ):
+            with self.klass(self.ctx, self.task_config) as task:
+                assert not hasattr(task, 'graphite')
+            self.task_config['graphite'] = False
+            with self.klass(self.ctx, self.task_config) as task:
+                assert not hasattr(task, 'graphite')
+            self.ctx.archive = '/fake/path'
+            self.task_config['graphite'] = True
+            with self.klass(self.ctx, self.task_config) as task:
+                assert hasattr(task, 'graphite')
+            self.task_config['graphite'] = False
+            with self.klass(self.ctx, self.task_config) as task:
+                assert not hasattr(task, 'graphite')
+
+    @patch('os.makedirs')
+    @patch('teuthology.task.pcp.PCPArchive')
+    def test_setup_archiver(self, m_archive, m_makedirs):
+        with patch.multiple(
+            self.klass,
+            begin=DEFAULT,
+            end=DEFAULT,
+        ):
+            self.task_config['fetch_archives'] = True
+            with self.klass(self.ctx, self.task_config) as task:
+                assert not hasattr(task, 'archiver')
+            self.task_config['fetch_archives'] = False
+            with self.klass(self.ctx, self.task_config) as task:
+                assert not hasattr(task, 'archiver')
+            self.ctx.archive = '/fake/path'
+            self.task_config['fetch_archives'] = True
+            with self.klass(self.ctx, self.task_config) as task:
+                assert hasattr(task, 'archiver')
+            self.task_config['fetch_archives'] = False
+            with self.klass(self.ctx, self.task_config) as task:
+                assert not hasattr(task, 'archiver')
+
+    @patch('os.makedirs')
+    @patch('teuthology.task.pcp.GrafanaGrapher')
+    @patch('teuthology.task.pcp.GraphiteGrapher')
+    def test_begin(self, m_grafana, m_graphite, m_makedirs):
+        with patch.multiple(
+            self.klass,
+            end=DEFAULT,
+        ):
+            with self.klass(self.ctx, self.task_config) as task:
+                task.grafana.build_graph_url.assert_called_once_with()
+            self.task_config['graphite'] = True
+            self.ctx.archive = '/fake/path'
+            with self.klass(self.ctx, self.task_config) as task:
+                task.graphite.write_html.assert_called_once_with()
+
+    @patch('os.makedirs')
+    @patch('teuthology.task.pcp.GrafanaGrapher')
+    @patch('teuthology.task.pcp.GraphiteGrapher')
+    def test_end(self, m_grafana, m_graphite, m_makedirs):
+        self.ctx.archive = '/fake/path'
+        with self.klass(self.ctx, self.task_config) as task:
+            # begin() should have called write_html() once by now, with no args
+            task.graphite.write_html.assert_called_once_with()
+        # end() should have called write_html() a second time by now, with
+        # mode=static
+        second_call = task.graphite.write_html.call_args_list[1]
+        assert second_call[1]['mode'] == 'static'
+        assert isinstance(task.stop_time, int)
+
+    @patch('os.makedirs')
+    @patch('teuthology.task.pcp.GrafanaGrapher')
+    @patch('teuthology.task.pcp.GraphiteGrapher')
+    def test_end_16049(self, m_grafana, m_graphite, m_makedirs):
+        # http://tracker.ceph.com/issues/16049
+        # Jobs were failing if graph downloading failed. We don't want that.
+        self.ctx.archive = '/fake/path'
+        with self.klass(self.ctx, self.task_config) as task:
+            task.graphite.download_graphs.side_effect = \
+                requests.ConnectionError
+        # Even though downloading graphs failed, we should have called
+        # write_html() a second time, again with no args
+        assert task.graphite.write_html.call_args_list == [call(), call()]
+        assert isinstance(task.stop_time, int)
diff --git a/tests/task/test_selinux.py b/tests/task/test_selinux.py
new file mode 100644 (file)
index 0000000..68c8e9c
--- /dev/null
@@ -0,0 +1,35 @@
+from mock import patch, Mock, DEFAULT
+
+from teuthology.config import FakeNamespace
+from teuthology.orchestra.cluster import Cluster
+from teuthology.orchestra.remote import Remote
+from teuthology.task.selinux import SELinux
+
+
+class TestSELinux(object):
+    def setup_method(self):
+        self.ctx = FakeNamespace()
+        self.ctx.config = dict()
+
+    def test_host_exclusion(self):
+        with patch.multiple(
+            Remote,
+            os=DEFAULT,
+            run=DEFAULT,
+        ):
+            self.ctx.cluster = Cluster()
+            remote1 = Remote('remote1')
+            remote1.os = Mock()
+            remote1.os.package_type = 'rpm'
+            remote1._is_vm = False
+            self.ctx.cluster.add(remote1, ['role1'])
+            remote2 = Remote('remote1')
+            remote2.os = Mock()
+            remote2.os.package_type = 'deb'
+            remote2._is_vm = False
+            self.ctx.cluster.add(remote2, ['role2'])
+            task_config = dict()
+            with SELinux(self.ctx, task_config) as task:
+                remotes = list(task.cluster.remotes)
+                assert remotes == [remote1]
+
diff --git a/tests/test_config.py b/tests/test_config.py
new file mode 100644 (file)
index 0000000..2620008
--- /dev/null
@@ -0,0 +1,189 @@
+import pytest
+
+from teuthology import config
+
+
+class TestYamlConfig(object):
+    def setup_method(self):
+        self.test_class = config.YamlConfig
+
+    def test_set_multiple(self):
+        conf_obj = self.test_class()
+        conf_obj.foo = 'foo'
+        conf_obj.bar = 'bar'
+        assert conf_obj.foo == 'foo'
+        assert conf_obj.bar == 'bar'
+        assert conf_obj.to_dict()['foo'] == 'foo'
+
+    def test_from_dict(self):
+        in_dict = dict(foo='bar')
+        conf_obj = self.test_class.from_dict(in_dict)
+        assert conf_obj.foo == 'bar'
+
+    def test_contains(self):
+        in_dict = dict(foo='bar')
+        conf_obj = self.test_class.from_dict(in_dict)
+        conf_obj.bar = "foo"
+        assert "bar" in conf_obj
+        assert "foo" in conf_obj
+        assert "baz" not in conf_obj
+
+    def test_to_dict(self):
+        in_dict = dict(foo='bar')
+        conf_obj = self.test_class.from_dict(in_dict)
+        assert conf_obj.to_dict() == in_dict
+
+    def test_from_str(self):
+        in_str = "foo: bar"
+        conf_obj = self.test_class.from_str(in_str)
+        assert conf_obj.foo == 'bar'
+
+    def test_to_str(self):
+        in_str = "foo: bar"
+        conf_obj = self.test_class.from_str(in_str)
+        assert conf_obj.to_str() == in_str
+
+    def test_update(self):
+        conf_obj = self.test_class(dict())
+        conf_obj.foo = 'foo'
+        conf_obj.bar = 'bar'
+        conf_obj.update(dict(bar='baz'))
+        assert conf_obj.foo == 'foo'
+        assert conf_obj.bar == 'baz'
+
+    def test_delattr(self):
+        conf_obj = self.test_class()
+        conf_obj.foo = 'bar'
+        assert conf_obj.foo == 'bar'
+        del conf_obj.foo
+        assert conf_obj.foo is None
+
+    def test_assignment(self):
+        conf_obj = self.test_class()
+        conf_obj["foo"] = "bar"
+        assert conf_obj["foo"] == "bar"
+        assert conf_obj.foo == "bar"
+
+    def test_used_with_update(self):
+        d = dict()
+        conf_obj = self.test_class.from_dict({"foo": "bar"})
+        d.update(conf_obj)
+        assert d["foo"] == "bar"
+
+    def test_get(self):
+        conf_obj = self.test_class()
+        assert conf_obj.get('foo') is None
+        assert conf_obj.get('foo', 'bar') == 'bar'
+        conf_obj.foo = 'baz'
+        assert conf_obj.get('foo') == 'baz'
+
+
+class TestTeuthologyConfig(TestYamlConfig):
+    def setup_method(self):
+        self.test_class = config.TeuthologyConfig
+
+    def test_get_ceph_git_base_default(self):
+        conf_obj = self.test_class()
+        conf_obj.yaml_path = ''
+        conf_obj.load()
+        assert conf_obj.ceph_git_base_url == "https://github.com/ceph/"
+
+    def test_set_ceph_git_base_via_private(self):
+        conf_obj = self.test_class()
+        conf_obj._conf['ceph_git_base_url'] = \
+            "git://git.ceph.com/"
+        assert conf_obj.ceph_git_base_url == "git://git.ceph.com/"
+
+    def test_get_reserve_machines_default(self):
+        conf_obj = self.test_class()
+        conf_obj.yaml_path = ''
+        conf_obj.load()
+        assert conf_obj.reserve_machines == 5
+
+    def test_set_reserve_machines_via_private(self):
+        conf_obj = self.test_class()
+        conf_obj._conf['reserve_machines'] = 2
+        assert conf_obj.reserve_machines == 2
+
+    def test_set_nonstandard(self):
+        conf_obj = self.test_class()
+        conf_obj.something = 'something else'
+        assert conf_obj.something == 'something else'
+
+
+class TestJobConfig(TestYamlConfig):
+    def setup_method(self):
+        self.test_class = config.JobConfig
+
+
+class TestFakeNamespace(TestYamlConfig):
+    def setup_method(self):
+        self.test_class = config.FakeNamespace
+
+    def test_docopt_dict(self):
+        """
+        Tests if a dict in the format that docopt returns can
+        be parsed correctly.
+        """
+        d = {
+            "--verbose": True,
+            "--an-option": "some_option",
+            "<an_arg>": "the_arg",
+            "something": "some_thing",
+        }
+        conf_obj = self.test_class(d)
+        assert conf_obj.verbose
+        assert conf_obj.an_option == "some_option"
+        assert conf_obj.an_arg == "the_arg"
+        assert conf_obj.something == "some_thing"
+
+    def test_config(self):
+        """
+        Tests that a teuthology_config property is automatically added
+        to the conf_obj
+        """
+        conf_obj = self.test_class(dict(foo="bar"))
+        assert conf_obj["foo"] == "bar"
+        assert conf_obj.foo == "bar"
+        assert conf_obj.teuthology_config.get("fake key") is None
+
+    def test_getattr(self):
+        conf_obj = self.test_class.from_dict({"foo": "bar"})
+        result = getattr(conf_obj, "not_there", "default")
+        assert result == "default"
+        result = getattr(conf_obj, "foo")
+        assert result == "bar"
+
+    def test_none(self):
+        conf_obj = self.test_class.from_dict(dict(null=None))
+        assert conf_obj.null is None
+
+    def test_delattr(self):
+        conf_obj = self.test_class()
+        conf_obj.foo = 'bar'
+        assert conf_obj.foo == 'bar'
+        del conf_obj.foo
+        with pytest.raises(AttributeError):
+            conf_obj.foo
+
+    def test_to_str(self):
+        in_str = "foo: bar"
+        conf_obj = self.test_class.from_str(in_str)
+        assert conf_obj.to_str() == "{'foo': 'bar'}"
+
+    def test_multiple_access(self):
+        """
+        Test that config.config and FakeNamespace.teuthology_config reflect
+        each others' modifications
+        """
+        in_str = "foo: bar"
+        conf_obj = self.test_class.from_str(in_str)
+        assert config.config.get('test_key_1') is None
+        assert conf_obj.teuthology_config.get('test_key_1') is None
+        config.config.test_key_1 = 'test value'
+        assert conf_obj.teuthology_config['test_key_1'] == 'test value'
+
+        assert config.config.get('test_key_2') is None
+        assert conf_obj.teuthology_config.get('test_key_2') is None
+        conf_obj.teuthology_config['test_key_2'] = 'test value'
+        assert config.config['test_key_2'] == 'test value'
diff --git a/tests/test_contextutil.py b/tests/test_contextutil.py
new file mode 100644 (file)
index 0000000..980415e
--- /dev/null
@@ -0,0 +1,97 @@
+from pytest import raises
+from teuthology import contextutil
+from logging import ERROR
+
+
+class TestSafeWhile(object):
+
+    def setup_method(self):
+        contextutil.log.setLevel(ERROR)
+        self.fake_sleep = lambda s: True
+        self.s_while = contextutil.safe_while
+
+    def test_6_5_10_deal(self):
+        with raises(contextutil.MaxWhileTries):
+            with self.s_while(_sleeper=self.fake_sleep) as proceed:
+                while proceed():
+                    pass
+
+    def test_6_0_1_deal(self):
+        with raises(contextutil.MaxWhileTries) as error:
+            with self.s_while(
+                tries=1,
+                _sleeper=self.fake_sleep
+            ) as proceed:
+                while proceed():
+                    pass
+
+        assert 'waiting for 6 seconds' in str(error)
+
+    def test_1_0_10_deal(self):
+        with raises(contextutil.MaxWhileTries) as error:
+            with self.s_while(
+                sleep=1,
+                _sleeper=self.fake_sleep
+            ) as proceed:
+                while proceed():
+                    pass
+
+        assert 'waiting for 10 seconds' in str(error)
+
+    def test_6_1_10_deal(self):
+        with raises(contextutil.MaxWhileTries) as error:
+            with self.s_while(
+                increment=1,
+                _sleeper=self.fake_sleep
+            ) as proceed:
+                while proceed():
+                    pass
+
+        assert 'waiting for 105 seconds' in str(error)
+
+    def test_timeout(self):
+        # series of sleep, increment, timeout params to test
+        params = [(10, 0, 100),
+                  (1, 2, 30),
+                  (10, 0.5, 100),
+                  (2, 0, 5),
+                  (2, 3, 5),
+                  (10, 0, 15),
+                  (20, 10, 60)]
+        for sleep, increment, timeout in params:
+            print("trying ", sleep, increment, timeout)
+            with raises(contextutil.MaxWhileTries) as error:
+                with self.s_while(
+                        sleep=sleep,
+                        increment=increment,
+                        timeout=timeout,
+                        _sleeper=self.fake_sleep
+                ) as proceed:
+                    while proceed():
+                        pass
+
+            assert 'waiting for {timeout}'.format(timeout=timeout) in str(error)
+
+    def test_action(self):
+        with raises(contextutil.MaxWhileTries) as error:
+            with self.s_while(
+                action='doing the thing',
+                _sleeper=self.fake_sleep
+            ) as proceed:
+                while proceed():
+                    pass
+
+        assert "'doing the thing' reached maximum tries" in str(error)
+
+    def test_no_raise(self):
+        with self.s_while(_raise=False, _sleeper=self.fake_sleep) as proceed:
+            while proceed():
+                pass
+
+        assert True
+
+    def test_tries(self):
+        attempts = 0
+        with self.s_while(tries=-1, _sleeper=self.fake_sleep) as proceed:
+            while attempts < 100 and proceed():
+                attempts += 1
diff --git a/tests/test_describe_tests.py b/tests/test_describe_tests.py
new file mode 100644 (file)
index 0000000..2292f28
--- /dev/null
@@ -0,0 +1,316 @@
+# -*- coding: utf-8 -*-
+import pytest
+
+from tests.fake_fs import make_fake_fstools
+from teuthology.describe_tests import (tree_with_info, extract_info,
+                                       get_combinations)
+from teuthology.exceptions import ParseError
+from mock import MagicMock, patch
+
+realistic_fs = {
+    'basic': {
+        '%': None,
+        'base': {
+            'install.yaml':
+            """meta:
+- desc: install ceph
+install:
+"""
+        },
+        'clusters': {
+            'fixed-1.yaml':
+            """meta:
+- desc: single node cluster
+roles:
+- [osd.0, osd.1, osd.2, mon.a, mon.b, mon.c, client.0]
+""",
+            'fixed-2.yaml':
+            """meta:
+- desc: couple node cluster
+roles:
+- [osd.0, osd.1, osd.2, mon.a, mon.b, mon.c]
+- [client.0]
+""",
+            'fixed-3.yaml':
+            """meta:
+- desc: triple node cluster
+roles:
+- [osd.0, osd.1, osd.2, mon.a, mon.b, mon.c]
+- [client.0]
+- [client.1]
+"""
+        },
+        'workloads': {
+            'rbd_api_tests_old_format.yaml':
+            """meta:
+- desc: c/c++ librbd api tests with format 1 images
+  rbd_features: none
+overrides:
+  ceph:
+    conf:
+      client:
+        rbd default format: 1
+tasks:
+- workunit:
+    env:
+      RBD_FEATURES: 0
+    clients:
+      client.0:
+        - rbd/test_librbd.sh
+""",
+            'rbd_api_tests.yaml':
+            """meta:
+- desc: c/c++ librbd api tests with default settings
+  rbd_features: default
+tasks:
+- workunit:
+    clients:
+      client.0:
+        - rbd/test_librbd.sh
+""",
+        },
+    },
+}
+
+
+expected_tree = """├── %
+├── base
+│   â””── install.yaml
+├── clusters
+│   â”œâ”€â”€ fixed-1.yaml
+│   â”œâ”€â”€ fixed-2.yaml
+│   â””── fixed-3.yaml
+└── workloads
+    â”œâ”€â”€ rbd_api_tests.yaml
+    â””── rbd_api_tests_old_format.yaml""".split('\n')
+
+
+expected_facets = [
+    '',
+    '',
+    'base',
+    '',
+    'clusters',
+    'clusters',
+    'clusters',
+    '',
+    'workloads',
+    'workloads',
+]
+
+
+expected_desc = [
+    '',
+    '',
+    'install ceph',
+    '',
+    'single node cluster',
+    'couple node cluster',
+    'triple node cluster',
+    '',
+    'c/c++ librbd api tests with default settings',
+    'c/c++ librbd api tests with format 1 images',
+]
+
+
+expected_rbd_features = [
+    '',
+    '',
+    '',
+    '',
+    '',
+    '',
+    '',
+    '',
+    'default',
+    'none',
+]
+
+
+class TestDescribeTests(object):
+
+    def setup_method(self):
+        self.mocks = dict()
+        self.patchers = dict()
+        exists, listdir, isfile, isdir, open = make_fake_fstools(realistic_fs)
+        for ppoint, fn in {
+             'os.listdir': listdir,
+             'os.path.isdir': isdir,
+             'teuthology.describe_tests.open': open,
+             'builtins.open': open,
+             'os.path.exists': exists,
+             'os.path.isfile': isfile,
+         }.items():
+            mockobj = MagicMock()
+            patcher = patch(ppoint, mockobj)
+            mockobj.side_effect = fn
+            patcher.start()
+            self.mocks[ppoint] = mockobj
+            self.patchers[ppoint] = patcher
+
+    def stop_patchers(self):
+        for patcher in self.patchers.values():
+            patcher.stop()
+
+    def teardown_method(self):
+        self.stop_patchers()
+
+    @staticmethod
+    def assert_expected_combo_headers(headers):
+        assert headers == (['subsuite depth 0'] +
+                           sorted(set(filter(bool, expected_facets))))
+
+    def test_no_filters(self):
+        rows = tree_with_info('basic', [], False, '', [])
+        assert rows == [[x] for x in expected_tree]
+
+    def test_single_filter(self):
+        rows = tree_with_info('basic', ['desc'], False, '', [])
+        assert rows == [list(_) for _ in zip(expected_tree, expected_desc)]
+
+        rows = tree_with_info('basic', ['rbd_features'], False, '', [])
+        assert rows == [list(_) for _ in zip(expected_tree, expected_rbd_features)]
+
+    def test_single_filter_with_facets(self):
+        rows = tree_with_info('basic', ['desc'], True, '', [])
+        assert rows == [list(_) for _ in zip(expected_tree, expected_facets,
+                                     expected_desc)]
+
+        rows = tree_with_info('basic', ['rbd_features'], True, '', [])
+        assert rows == [list(_) for _ in zip(expected_tree, expected_facets,
+                                     expected_rbd_features)]
+
+    def test_no_matching(self):
+        rows = tree_with_info('basic', ['extra'], False, '', [])
+        assert rows == [list(_) for _ in zip(expected_tree, [''] * len(expected_tree))]
+
+        rows = tree_with_info('basic', ['extra'], True, '', [])
+        assert rows == [list(_) for _ in zip(expected_tree, expected_facets,
+                                     [''] * len(expected_tree))]
+
+    def test_multiple_filters(self):
+        rows = tree_with_info('basic', ['desc', 'rbd_features'], False, '', [])
+        assert rows == [list(_) for _ in zip(expected_tree,
+                                     expected_desc,
+                                     expected_rbd_features)]
+
+        rows = tree_with_info('basic', ['rbd_features', 'desc'], False, '', [])
+        assert rows == [list(_) for _ in zip(expected_tree,
+                                     expected_rbd_features,
+                                     expected_desc)]
+
+    def test_multiple_filters_with_facets(self):
+        rows = tree_with_info('basic', ['desc', 'rbd_features'], True, '', [])
+        assert rows == [list(_) for _ in zip(expected_tree,
+                                     expected_facets,
+                                     expected_desc,
+                                     expected_rbd_features)]
+
+        rows = tree_with_info('basic', ['rbd_features', 'desc'], True, '', [])
+        assert rows == [list(_) for _ in zip(expected_tree,
+                                     expected_facets,
+                                     expected_rbd_features,
+                                     expected_desc)]
+
+    def test_combinations_only_facets(self):
+        headers, rows = get_combinations('basic',
+                fields=[], subset=None, limit=1,
+                filter_in=None, filter_out=None, filter_all=None,
+                include_facet=True)
+        self.assert_expected_combo_headers(headers)
+        assert rows == [['basic', 'install', 'fixed-1', 'rbd_api_tests']]
+
+    def test_combinations_desc_features(self):
+        headers, rows = get_combinations('basic',
+                fields=['desc', 'rbd_features'], subset=None, limit=1,
+                filter_in=None, filter_out=None, filter_all=None,
+                include_facet=False)
+        assert headers == ['desc', 'rbd_features']
+        descriptions = '\n'.join([
+            'install ceph',
+            'single node cluster',
+            'c/c++ librbd api tests with default settings',
+        ])
+        assert rows == [[descriptions, 'default']]
+
+    def test_combinations_filter_in(self):
+        headers, rows = get_combinations('basic',
+                fields=[], subset=None, limit=0,
+                filter_in=['old_format'], filter_out=None, filter_all=None,
+                include_facet=True)
+        self.assert_expected_combo_headers(headers)
+        assert rows == [
+            ['basic', 'install', 'fixed-1', 'rbd_api_tests_old_format'],
+            ['basic', 'install', 'fixed-2', 'rbd_api_tests_old_format'],
+            ['basic', 'install', 'fixed-3', 'rbd_api_tests_old_format'],
+        ]
+
+    def test_combinations_filter_out(self):
+        headers, rows = get_combinations('basic',
+                fields=[], subset=None, limit=0,
+                filter_in=None, filter_out=['old_format'], filter_all=None,
+                include_facet=True)
+        self.assert_expected_combo_headers(headers)
+        assert rows == [
+            ['basic', 'install', 'fixed-1', 'rbd_api_tests'],
+            ['basic', 'install', 'fixed-2', 'rbd_api_tests'],
+            ['basic', 'install', 'fixed-3', 'rbd_api_tests'],
+        ]
+
+    def test_combinations_filter_all(self):
+        headers, rows = get_combinations('basic',
+                fields=[], subset=None, limit=0,
+                filter_in=None, filter_out=None,
+                filter_all=['fixed-2', 'old_format'],
+                include_facet=True)
+        self.assert_expected_combo_headers(headers)
+        assert rows == [
+            ['basic', 'install', 'fixed-2', 'rbd_api_tests_old_format']
+        ]
+
+
+@patch('teuthology.describe_tests.open')
+@patch('os.path.isdir')
+def test_extract_info_dir(m_isdir, m_open):
+    simple_fs = {'a': {'b.yaml': 'meta: [{foo: c}]'}}
+    _, _, _, m_isdir.side_effect, m_open.side_effect = \
+        make_fake_fstools(simple_fs)
+    info = extract_info('a', [])
+    assert info == {}
+
+    info = extract_info('a', ['foo', 'bar'])
+    assert info == {'foo': '', 'bar': ''}
+
+    info = extract_info('a/b.yaml', ['foo', 'bar'])
+    assert info == {'foo': 'c', 'bar': ''}
+
+
+@patch('teuthology.describe_tests.open')
+@patch('os.path.isdir')
+def check_parse_error(fs, m_isdir, m_open):
+    _, _, _, m_isdir.side_effect, m_open.side_effect = make_fake_fstools(fs)
+    with pytest.raises(ParseError):
+        a = extract_info('a.yaml', ['a'])
+        raise Exception(str(a))
+
+
+def test_extract_info_too_many_elements():
+    check_parse_error({'a.yaml': 'meta: [{a: b}, {b: c}]'})
+
+
+def test_extract_info_not_a_list():
+    check_parse_error({'a.yaml': 'meta: {a: b}'})
+
+
+def test_extract_info_not_a_dict():
+    check_parse_error({'a.yaml': 'meta: [[a, b]]'})
+
+
+@patch('teuthology.describe_tests.open')
+@patch('os.path.isdir')
+def test_extract_info_empty_file(m_isdir, m_open):
+    simple_fs = {'a.yaml': ''}
+    _, _, _, m_isdir.side_effect, m_open.side_effect = \
+        make_fake_fstools(simple_fs)
+    info = extract_info('a.yaml', [])
+    assert info == {}
diff --git a/tests/test_email_sleep_before_teardown.py b/tests/test_email_sleep_before_teardown.py
new file mode 100644 (file)
index 0000000..60fcd24
--- /dev/null
@@ -0,0 +1,81 @@
+from humanfriendly import format_timespan
+from mock import Mock, patch
+from pytest import mark
+from teuthology.config import config
+from teuthology.run_tasks import build_email_body as email_body
+from textwrap import dedent
+
+class TestSleepBeforeTeardownEmail(object):
+    def setup_method(self):
+        config.results_ui_server = "http://example.com/"
+        config.archive_server = "http://qa-proxy.ceph.com/teuthology/"
+
+    @mark.parametrize(
+        ['status', 'owner', 'suite_name', 'run_name', 'job_id', 'dura'],
+        [
+            [
+                'pass',
+                'noreply@host',
+                'dummy',
+                'run-name',
+                123,
+                3600,
+            ],
+            [
+                'fail',
+                'noname',
+                'yummy',
+                'next-run',
+                1000,
+                99999,
+            ],
+        ]
+    )
+    @patch("teuthology.run_tasks.time.time")
+    def test_sleep_before_teardown_email_body(self, m_time, status, owner,
+                                              suite_name, run_name, job_id, dura):
+        ctx = Mock()
+        archive_path='archive/path'
+        archive_dir='/archive/dir'
+        date_sec=3661
+        date_str='1970-01-01 01:01:01'
+        m_time.return_value=float(date_sec)
+        duration_sec=dura
+        duration_str=format_timespan(duration_sec)
+        ref_body=dedent("""
+            Teuthology job {run}/{job} has fallen asleep at {date} for {duration_str}
+
+            Owner: {owner}
+            Suite Name: {suite}
+            Sleep Date: {date}
+            Sleep Time: {duration_sec} seconds ({duration_str})
+            Job Info: http://example.com/{run}/
+            Job Logs: http://qa-proxy.ceph.com/teuthology/path/{job}/
+            Task Stack: a/b/c
+            Current Status: {status}"""
+            .format(duration_sec=duration_sec, duration_str=duration_str,
+                owner=owner, suite=suite_name, run=run_name,
+                job=job_id, status=status, date=date_str))
+        print(ref_body)
+        ctx.config = dict(
+            archive_path=archive_path,
+            job_id=job_id,
+            suite=suite_name,
+            )
+        if status == 'pass':
+            ctx.summary = dict(
+                success=True,
+            )
+        elif status == 'fail':
+            ctx.summary = dict(
+                success=False,
+            )
+        else:
+            ctx.summary = dict()
+
+        ctx.owner   = owner
+        ctx.name    = run_name
+        ctx.archive_dir = archive_dir
+        tasks = [('a', None), ('b', None), ('c', None)]
+        (subj, body) = email_body(ctx, tasks, dura)
+        assert body == ref_body.lstrip('\n')
diff --git a/tests/test_exit.py b/tests/test_exit.py
new file mode 100644 (file)
index 0000000..e8c22bf
--- /dev/null
@@ -0,0 +1,90 @@
+import os
+import random
+
+from unittest.mock import patch, Mock
+
+from teuthology import exit
+
+
+class TestExiter(object):
+    klass = exit.Exiter
+
+    def setup_method(self):
+        self.pid = os.getpid()
+
+        # Below, we patch os.kill() in such a way that the first time it is
+        # invoked it does actually send the signal. Any subsequent invocation
+        # won't send any signal - this is so we don't kill the process running
+        # our unit tests!
+        self.patcher_kill = patch(
+            'teuthology.exit.os.kill',
+            wraps=os.kill,
+        )
+
+        #Keep a copy of the unpatched kill and call this in place of os.kill
+        #In the Exiter objects, the os.kill calls are patched.
+        #So the call_count should be 1.
+        self.kill_unpatched = os.kill
+        self.m_kill = self.patcher_kill.start()
+
+        def m_kill_unwrap(pid, sig):
+            # Setting return_value of a mocked object disables the wrapping
+            if self.m_kill.call_count > 1:
+                self.m_kill.return_value = None
+
+        self.m_kill.side_effect = m_kill_unwrap
+
+    def teardown_method(self):
+        self.patcher_kill.stop()
+        del self.m_kill
+
+    def test_basic(self):
+        sig = 15
+        obj = self.klass()
+        m_func = Mock()
+        obj.add_handler(sig, m_func)
+        assert len(obj.handlers) == 1
+        self.kill_unpatched(self.pid, sig)
+        assert m_func.call_count == 1
+        assert self.m_kill.call_count == 1
+        for arg_list in self.m_kill.call_args_list:
+            assert arg_list[0] == (self.pid, sig)
+
+    def test_remove_handlers(self):
+        sig = [1, 15]
+        send_sig = random.choice(sig)
+        n = 3
+        obj = self.klass()
+        handlers = list()
+        for i in range(n):
+            m_func = Mock(name="handler %s" % i)
+            handlers.append(obj.add_handler(sig, m_func))
+        assert obj.handlers == handlers
+        for handler in handlers:
+            handler.remove()
+        assert obj.handlers == list()
+        self.kill_unpatched(self.pid, send_sig)
+        assert self.m_kill.call_count == 1
+        for handler in handlers:
+            assert handler.func.call_count == 0
+
+    def test_n_handlers(self, n=10, sig=11):
+        if isinstance(sig, int):
+            send_sig = sig
+        else:
+            send_sig = random.choice(sig)
+        obj = self.klass()
+        handlers = list()
+        for i in range(n):
+            m_func = Mock(name="handler %s" % i)
+            handlers.append(obj.add_handler(sig, m_func))
+        assert obj.handlers == handlers
+        self.kill_unpatched(self.pid, send_sig)
+        for i in range(n):
+            assert handlers[i].func.call_count == 1
+        assert self.m_kill.call_count == 1
+        for arg_list in self.m_kill.call_args_list:
+            assert arg_list[0] == (self.pid, send_sig)
+
+    def test_multiple_signals(self):
+        self.test_n_handlers(n=3, sig=[1, 6, 11, 15])
diff --git a/tests/test_get_distro.py b/tests/test_get_distro.py
new file mode 100644 (file)
index 0000000..b03ba7b
--- /dev/null
@@ -0,0 +1,47 @@
+from teuthology.misc import get_distro
+
+
+class Mock:
+    pass
+
+
+class TestGetDistro(object):
+
+    def setup_method(self):
+        self.fake_ctx = Mock()
+        self.fake_ctx.config = {}
+        # os_type in ctx will always default to None
+        self.fake_ctx.os_type = None
+
+    def test_default_distro(self):
+        distro = get_distro(self.fake_ctx)
+        assert distro == 'ubuntu'
+
+    def test_argument(self):
+        # we don't want fake_ctx to have a config
+        self.fake_ctx = Mock()
+        self.fake_ctx.os_type = 'centos'
+        distro = get_distro(self.fake_ctx)
+        assert distro == 'centos'
+
+    def test_teuth_config(self):
+        self.fake_ctx.config = {'os_type': 'fedora'}
+        distro = get_distro(self.fake_ctx)
+        assert distro == 'fedora'
+
+    def test_argument_takes_precedence(self):
+        self.fake_ctx.config = {'os_type': 'fedora'}
+        self.fake_ctx.os_type = "centos"
+        distro = get_distro(self.fake_ctx)
+        assert distro == 'centos'
+
+    def test_no_config_or_os_type(self):
+        self.fake_ctx = Mock()
+        self.fake_ctx.os_type = None
+        distro = get_distro(self.fake_ctx)
+        assert distro == 'ubuntu'
+
+    def test_config_os_type_is_none(self):
+        self.fake_ctx.config["os_type"] = None
+        distro = get_distro(self.fake_ctx)
+        assert distro == 'ubuntu'
diff --git a/tests/test_get_distro_version.py b/tests/test_get_distro_version.py
new file mode 100644 (file)
index 0000000..8a77e39
--- /dev/null
@@ -0,0 +1,47 @@
+from teuthology.misc import get_distro_version
+
+
+class Mock:
+    pass
+
+
+class TestGetDistroVersion(object):
+
+    def setup_method(self):
+        self.fake_ctx = Mock()
+        self.fake_ctx.config = {}
+        self.fake_ctx_noarg = Mock()
+        self.fake_ctx_noarg.config = {}
+        self.fake_ctx_noarg.os_version = None
+        self.fake_ctx.os_type = None
+        self.fake_ctx_noarg.os_type = None
+
+    def test_default_distro_version(self):
+        # Default distro is ubuntu, default version of ubuntu is 20.04
+        self.fake_ctx.os_version = None
+        distroversion = get_distro_version(self.fake_ctx)
+        assert distroversion == '22.04'
+
+    def test_argument_version(self):
+        self.fake_ctx.os_version = '13.04'
+        distroversion = get_distro_version(self.fake_ctx)
+        assert distroversion == '13.04'
+
+    def test_teuth_config_version(self):
+        #Argument takes precidence.
+        self.fake_ctx.os_version = '13.04'
+        self.fake_ctx.config = {'os_version': '13.10'}
+        distroversion = get_distro_version(self.fake_ctx)
+        assert distroversion == '13.04'
+
+    def test_teuth_config_noarg_version(self):
+        self.fake_ctx_noarg.config = {'os_version': '13.04'}
+        distroversion = get_distro_version(self.fake_ctx_noarg)
+        assert distroversion == '13.04'
+
+    def test_no_teuth_config(self):
+        self.fake_ctx = Mock()
+        self.fake_ctx.os_type = None
+        self.fake_ctx.os_version = '13.04'
+        distroversion = get_distro_version(self.fake_ctx)
+        assert distroversion == '13.04'
diff --git a/tests/test_get_multi_machine_types.py b/tests/test_get_multi_machine_types.py
new file mode 100644 (file)
index 0000000..32a6b02
--- /dev/null
@@ -0,0 +1,27 @@
+from teuthology import misc as teuthology
+
+class Mock: pass
+
+class TestGetMultiMachineTypes(object):
+
+    def test_space(self):
+        give = 'burnupi plana vps'
+        expect = ['burnupi','plana','vps']
+        assert teuthology.get_multi_machine_types(give) == expect
+
+    def test_tab(self):
+        give = 'burnupi        plana   vps'
+        expect = ['burnupi','plana','vps']
+        assert teuthology.get_multi_machine_types(give) == expect
+
+    def test_comma(self):
+        give = 'burnupi,plana,vps'
+        expect = ['burnupi','plana','vps']
+        assert teuthology.get_multi_machine_types(give) == expect
+
+    def test_single(self):
+        give = 'burnupi'
+        expect = ['burnupi']
+        assert teuthology.get_multi_machine_types(give) == expect
+
+
diff --git a/tests/test_imports.py b/tests/test_imports.py
new file mode 100644 (file)
index 0000000..cb25c8d
--- /dev/null
@@ -0,0 +1,31 @@
+import importlib
+import pytest
+import sys
+
+from pathlib import Path
+from typing import List
+
+root = Path("./teuthology")
+
+
+def find_modules() -> List[str]:
+    modules = []
+    for path in root.rglob("*.py"):
+        if path.name.startswith("test_"):
+            continue
+        if "-" in path.name:
+            continue
+        if path.name == "__init__.py":
+            path = path.parent
+
+        path_name = str(path).replace("/", ".")
+        if path_name.endswith(".py"):
+            path_name = path_name[:-3]
+        modules.append(path_name)
+    return sorted(modules)
+
+
+@pytest.mark.parametrize("module", find_modules())
+def test_import_modules(module):
+    importlib.import_module(module)
+    assert module in sys.modules
diff --git a/tests/test_job_status.py b/tests/test_job_status.py
new file mode 100644 (file)
index 0000000..ee1b764
--- /dev/null
@@ -0,0 +1,60 @@
+from teuthology import job_status
+
+
+class TestJobStatus(object):
+    def test_get_only_success_true(self):
+        summary = dict(success=True)
+        status = job_status.get_status(summary)
+        assert status == 'pass'
+
+    def test_get_only_success_false(self):
+        summary = dict(success=False)
+        status = job_status.get_status(summary)
+        assert status == 'fail'
+
+    def test_get_status_pass(self):
+        summary = dict(status='pass')
+        status = job_status.get_status(summary)
+        assert status == 'pass'
+
+    def test_get_status_fail(self):
+        summary = dict(status='fail')
+        status = job_status.get_status(summary)
+        assert status == 'fail'
+
+    def test_get_status_dead(self):
+        summary = dict(status='dead')
+        status = job_status.get_status(summary)
+        assert status == 'dead'
+
+    def test_get_status_none(self):
+        summary = dict()
+        status = job_status.get_status(summary)
+        assert status is None
+
+    def test_set_status_pass(self):
+        summary = dict()
+        job_status.set_status(summary, 'pass')
+        assert summary == dict(status='pass', success=True)
+
+    def test_set_status_dead(self):
+        summary = dict()
+        job_status.set_status(summary, 'dead')
+        assert summary == dict(status='dead', success=False)
+
+    def test_set_then_get_status_dead(self):
+        summary = dict()
+        job_status.set_status(summary, 'dead')
+        status = job_status.get_status(summary)
+        assert status == 'dead'
+
+    def test_set_status_none(self):
+        summary = dict()
+        job_status.set_status(summary, None)
+        assert summary == dict()
+
+    def test_legacy_fail(self):
+        summary = dict(success=True)
+        summary['success'] = False
+        status = job_status.get_status(summary)
+        assert status == 'fail'
diff --git a/tests/test_kill.py b/tests/test_kill.py
new file mode 100644 (file)
index 0000000..21ec997
--- /dev/null
@@ -0,0 +1,46 @@
+from unittest.mock import patch
+
+from teuthology.kill import find_targets
+
+
+class TestFindTargets(object):
+    """ Tests for teuthology.kill.find_targets """
+
+    @patch('teuthology.kill.report.ResultsReporter.get_jobs')
+    def test_missing_run_find_targets(self, m_get_jobs):
+        m_get_jobs.return_value = [] 
+        run_targets = find_targets("run-name")
+        assert run_targets == {}
+    
+    @patch('teuthology.kill.report.ResultsReporter.get_jobs')
+    def test_missing_job_find_targets(self, m_get_jobs):
+        m_get_jobs.return_value = {} 
+        job_targets = find_targets("run-name", "3")
+        assert job_targets == {}
+
+    @patch('teuthology.kill.report.ResultsReporter.get_jobs')
+    def test_missing_run_targets_find_targets(self, m_get_jobs):
+        m_get_jobs.return_value = [{"targets": None, "status": "waiting"}] 
+        run_targets = find_targets("run-name")
+        assert run_targets == {}
+    
+    @patch('teuthology.kill.report.ResultsReporter.get_jobs')
+    def test_missing_job_targets_find_targets(self, m_get_jobs):
+        m_get_jobs.return_value = {"targets": None} 
+        job_targets = find_targets("run-name", "3")
+        assert job_targets == {}
+
+    @patch('teuthology.kill.report.ResultsReporter.get_jobs')
+    def test_run_find_targets(self, m_get_jobs):
+        m_get_jobs.return_value = [{"targets": {"node1": ""}, "status": "running"}]
+        run_targets = find_targets("run-name")
+        assert run_targets == {"node1": ""}
+        m_get_jobs.return_value = [{"targets": {"node1": ""}}]
+        run_targets = find_targets("run-name")
+        assert run_targets == {}
+
+    @patch('teuthology.kill.report.ResultsReporter.get_jobs')
+    def test_job_find_targets(self, m_get_jobs):
+        m_get_jobs.return_value = {"targets": {"node1": ""}}
+        job_targets = find_targets("run-name", "3")
+        assert job_targets == {"node1": ""}
diff --git a/tests/test_ls.py b/tests/test_ls.py
new file mode 100644 (file)
index 0000000..631dcfd
--- /dev/null
@@ -0,0 +1,48 @@
+import pytest
+
+from unittest.mock import patch, Mock
+
+from teuthology import ls
+
+
+class TestLs(object):
+    """ Tests for teuthology.ls """
+
+    @patch('os.path.isdir')
+    @patch('os.listdir')
+    def test_get_jobs(self, m_listdir, m_isdir):
+        m_listdir.return_value = ["1", "a", "3"]
+        m_isdir.return_value = True
+        results = ls.get_jobs("some/archive/dir")
+        assert results == ["1", "3"]
+
+    @patch("yaml.safe_load_all")
+    @patch("teuthology.ls.get_jobs")
+    def test_ls(self, m_get_jobs,  m_safe_load_all):
+        m_get_jobs.return_value = ["1", "2"]
+        m_safe_load_all.return_value = [{"failure_reason": "reasons"}]
+        ls.ls("some/archive/div", True)
+
+    @patch("teuthology.ls.open")
+    @patch("teuthology.ls.get_jobs")
+    def test_ls_ioerror(self, m_get_jobs, m_open):
+        m_get_jobs.return_value = ["1", "2"]
+        m_open.side_effect = IOError()
+        with pytest.raises(IOError):
+            ls.ls("some/archive/dir", True)
+
+    @patch("teuthology.ls.open")
+    @patch("os.popen")
+    @patch("os.path.isdir")
+    @patch("os.path.isfile")
+    def test_print_debug_info(self, m_isfile, m_isdir, m_popen, m_open):
+        m_isfile.return_value = True
+        m_isdir.return_value = True
+        m_popen.return_value = Mock()
+        cmdline = Mock()
+        cmdline.find = Mock(return_value=0)
+        m1 = Mock()
+        m2 = Mock()
+        m2.read = Mock(return_value=cmdline)
+        m_open.side_effect = [m1, m2]
+        ls.print_debug_info("the_job", "job/dir", "some/archive/dir")
diff --git a/tests/test_misc.py b/tests/test_misc.py
new file mode 100644 (file)
index 0000000..ca75336
--- /dev/null
@@ -0,0 +1,387 @@
+import argparse
+import pytest
+import subprocess
+
+from unittest.mock import Mock, patch
+
+from teuthology import misc
+from teuthology.config import config
+from teuthology.orchestra import cluster
+from teuthology.orchestra.remote import Remote
+
+
+class FakeRemote(object):
+    pass
+
+
+def test_sh_normal(caplog):
+    assert misc.sh("/bin/echo ABC") == "ABC\n"
+    assert "truncated" not in caplog.text
+
+
+def test_sh_truncate(caplog):
+    assert misc.sh("/bin/echo -n AB ; /bin/echo C", 2) == "ABC\n"
+    assert "truncated" in caplog.text
+    assert "ABC" not in caplog.text
+
+
+def test_sh_fail(caplog):
+    with pytest.raises(subprocess.CalledProcessError) as excinfo:
+        misc.sh("/bin/echo -n AB ; /bin/echo C ; exit 111", 2)
+    assert excinfo.value.returncode == 111
+    for record in caplog.records:
+        if record.levelname == 'ERROR':
+            assert ('replay full' in record.message or
+                    'ABC\n' == record.message)
+
+def test_sh_progress(caplog):
+    assert misc.sh("echo AB ; sleep 0.1 ; /bin/echo C", 2) == "AB\nC\n"
+    records = caplog.records
+    assert ':sh: ' in records[0].message
+    assert 'AB' == records[1].message
+    assert 'C' == records[2].message
+    assert records[2].created > records[1].created
+
+
+def test_wait_until_osds_up():
+    ctx = argparse.Namespace()
+    ctx.daemons = Mock()
+    ctx.daemons.iter_daemons_of_role.return_value = list()
+    remote = Mock(spec=Remote)
+    remote.sh.return_value = 'IGNORED\n{"osds":[{"state":["up"]}]}'
+    ctx.cluster = cluster.Cluster(
+        remotes=[
+            (remote, ['osd.0', 'client.1'])
+        ],
+    )
+    with patch.multiple(
+            misc,
+            get_testdir=lambda _: "TESTDIR",
+    ):
+        misc.wait_until_osds_up(ctx, ctx.cluster, remote)
+
+
+def test_get_clients_simple():
+    ctx = argparse.Namespace()
+    remote = FakeRemote()
+    ctx.cluster = cluster.Cluster(
+        remotes=[
+            (remote, ['client.0', 'client.1'])
+        ],
+    )
+    g = misc.get_clients(ctx=ctx, roles=['client.1'])
+    got = next(g)
+    assert len(got) == 2
+    assert got[0] == ('1')
+    assert got[1] is remote
+    with pytest.raises(StopIteration):
+        next(g)
+
+
+def test_get_mon_names():
+    expected = [
+        ([['mon.a', 'osd.0', 'mon.c']], 'ceph', ['mon.a', 'mon.c']),
+        ([['ceph.mon.a', 'osd.0', 'ceph.mon.c']], 'ceph', ['ceph.mon.a', 'ceph.mon.c']),
+        ([['mon.a', 'osd.0', 'mon.c'], ['ceph.mon.b']], 'ceph', ['mon.a', 'mon.c', 'ceph.mon.b']),
+        ([['mon.a', 'osd.0', 'mon.c'], ['foo.mon.a']], 'ceph', ['mon.a', 'mon.c']),
+        ([['mon.a', 'osd.0', 'mon.c'], ['foo.mon.a']], 'foo', ['foo.mon.a']),
+    ]
+    for remote_roles, cluster_name, expected_mons in expected:
+        ctx = argparse.Namespace()
+        ctx.cluster = Mock()
+        ctx.cluster.remotes = {i: roles for i, roles in enumerate(remote_roles)}
+        mons = misc.get_mon_names(ctx, cluster_name)
+        assert expected_mons == mons
+
+
+def test_get_first_mon():
+    expected = [
+        ([['mon.a', 'osd.0', 'mon.c']], 'ceph', 'mon.a'),
+        ([['ceph.mon.a', 'osd.0', 'ceph.mon.c']], 'ceph', 'ceph.mon.a'),
+        ([['mon.a', 'osd.0', 'mon.c'], ['ceph.mon.b']], 'ceph', 'ceph.mon.b'),
+        ([['mon.a', 'osd.0', 'mon.c'], ['foo.mon.a']], 'ceph', 'mon.a'),
+        ([['foo.mon.b', 'osd.0', 'mon.c'], ['foo.mon.a']], 'foo', 'foo.mon.a'),
+    ]
+    for remote_roles, cluster_name, expected_mon in expected:
+        ctx = argparse.Namespace()
+        ctx.cluster = Mock()
+        ctx.cluster.remotes = {i: roles for i, roles in enumerate(remote_roles)}
+        mon = misc.get_first_mon(ctx, None, cluster_name)
+        assert expected_mon == mon
+
+
+def test_roles_of_type():
+    expected = [
+        (['client.0', 'osd.0', 'ceph.osd.1'], 'osd', ['0', '1']),
+        (['client.0', 'osd.0', 'ceph.osd.1'], 'client', ['0']),
+        (['foo.client.1', 'bar.client.2.3', 'baz.osd.1'], 'mon', []),
+        (['foo.client.1', 'bar.client.2.3', 'baz.osd.1'], 'client',
+         ['1', '2.3']),
+        ]
+    for roles_for_host, type_, expected_ids in expected:
+        ids = list(misc.roles_of_type(roles_for_host, type_))
+        assert ids == expected_ids
+
+
+def test_cluster_roles_of_type():
+    expected = [
+        (['client.0', 'osd.0', 'ceph.osd.1'], 'osd', 'ceph',
+         ['osd.0', 'ceph.osd.1']),
+        (['client.0', 'osd.0', 'ceph.osd.1'], 'client', 'ceph',
+         ['client.0']),
+        (['foo.client.1', 'bar.client.2.3', 'baz.osd.1'], 'mon', None, []),
+        (['foo.client.1', 'bar.client.2.3', 'baz.osd.1'], 'client', None,
+         ['foo.client.1', 'bar.client.2.3']),
+        (['foo.client.1', 'bar.client.2.3', 'baz.osd.1'], 'client', 'bar',
+         ['bar.client.2.3']),
+        ]
+    for roles_for_host, type_, cluster_, expected_roles in expected:
+        roles = list(misc.cluster_roles_of_type(roles_for_host, type_, cluster_))
+        assert roles == expected_roles
+
+
+def test_all_roles_of_type():
+    expected = [
+        ([['client.0', 'osd.0', 'ceph.osd.1'], ['bar.osd.2']],
+         'osd', ['0', '1', '2']),
+        ([['client.0', 'osd.0', 'ceph.osd.1'], ['bar.osd.2', 'baz.client.1']],
+         'client', ['0', '1']),
+        ([['foo.client.1', 'bar.client.2.3'], ['baz.osd.1']], 'mon', []),
+        ([['foo.client.1', 'bar.client.2.3'], ['baz.osd.1', 'ceph.client.bar']],
+         'client', ['1', '2.3', 'bar']),
+        ]
+    for host_roles, type_, expected_ids in expected:
+        cluster_ = Mock()
+        cluster_.remotes = dict(enumerate(host_roles))
+        ids = list(misc.all_roles_of_type(cluster_, type_))
+        assert ids == expected_ids
+
+
+def test_get_http_log_path():
+    # Fake configuration
+    archive_server = "http://example.com/server_root"
+    config.archive_server = archive_server
+    archive_dir = "/var/www/archives"
+
+    path = misc.get_http_log_path(archive_dir)
+    assert path == "http://example.com/server_root/archives/"
+
+    job_id = '12345'
+    path = misc.get_http_log_path(archive_dir, job_id)
+    assert path == "http://example.com/server_root/archives/12345/"
+
+    # Inktank configuration
+    archive_server = "http://qa-proxy.ceph.com/teuthology/"
+    config.archive_server = archive_server
+    archive_dir = "/var/lib/teuthworker/archive/teuthology-2013-09-12_11:49:50-ceph-deploy-main-testing-basic-vps"
+    job_id = 31087
+    path = misc.get_http_log_path(archive_dir, job_id)
+    assert path == "http://qa-proxy.ceph.com/teuthology/teuthology-2013-09-12_11:49:50-ceph-deploy-main-testing-basic-vps/31087/"
+
+    path = misc.get_http_log_path(archive_dir)
+    assert path == "http://qa-proxy.ceph.com/teuthology/teuthology-2013-09-12_11:49:50-ceph-deploy-main-testing-basic-vps/"
+
+
+def test_is_type():
+    is_client = misc.is_type('client')
+    assert is_client('client.0')
+    assert is_client('ceph.client.0')
+    assert is_client('foo.client.0')
+    assert is_client('foo.client.bar.baz')
+
+    with pytest.raises(ValueError):
+        is_client('')
+        is_client('client')
+    assert not is_client('foo.bar.baz')
+    assert not is_client('ceph.client')
+    assert not is_client('hadoop.main.0')
+
+
+def test_is_type_in_cluster():
+    is_c1_osd = misc.is_type('osd', 'c1')
+    with pytest.raises(ValueError):
+        is_c1_osd('')
+    assert not is_c1_osd('osd.0')
+    assert not is_c1_osd('ceph.osd.0')
+    assert not is_c1_osd('ceph.osd.0')
+    assert not is_c1_osd('c11.osd.0')
+    assert is_c1_osd('c1.osd.0')
+    assert is_c1_osd('c1.osd.999')
+
+
+def test_get_mons():
+    ips = ['1.1.1.1', '2.2.2.2', '3.3.3.3']
+    addrs = ['1.1.1.1:6789', '1.1.1.1:6790', '1.1.1.1:6791']
+
+    mons = misc.get_mons([['mon.a']], ips)
+    assert mons == {'mon.a': addrs[0]}
+
+    mons = misc.get_mons([['cluster-a.mon.foo', 'client.b'], ['osd.0']], ips)
+    assert mons == {'cluster-a.mon.foo': addrs[0]}
+
+    mons = misc.get_mons([['mon.a', 'mon.b', 'ceph.mon.c']], ips)
+    assert mons == {'mon.a': addrs[0],
+                    'mon.b': addrs[1],
+                    'ceph.mon.c': addrs[2]}
+
+    mons = misc.get_mons([['mon.a'], ['mon.b'], ['ceph.mon.c']], ips)
+    assert mons == {'mon.a': addrs[0],
+                    'mon.b': ips[1] + ':6789',
+                    'ceph.mon.c': ips[2] + ':6789'}
+
+
+def test_split_role():
+    expected = {
+        'client.0': ('ceph', 'client', '0'),
+        'foo.client.0': ('foo', 'client', '0'),
+        'bar.baz.x.y.z': ('bar', 'baz', 'x.y.z'),
+        'mds.a-s-b': ('ceph', 'mds', 'a-s-b'),
+    }
+
+    for role, expected_split in expected.items():
+        actual_split = misc.split_role(role)
+        assert actual_split == expected_split
+
+def test_update_key():
+    a = { "sha": "foo", "workunit": { "sha": "foo" }, "tasks": [{"task1": "ceph"}], "overrides": [{"sha": "foo"}] }
+    b = { "sha": "blah", "workunit": { "sha": "bar" }, "tasks": [] }
+
+    misc.update_key("sha", a, b)
+    assert a == { "sha": "blah", "workunit": { "sha": "bar" }, "tasks": [{"task1": "ceph"}], "overrides": [{"sha": "foo"}] } 
+
+class TestHostnames(object):
+    def setup_method(self):
+        config._conf = dict()
+
+    def teardown_method(self):
+        config.load()
+
+    def test_canonicalize_hostname(self):
+        host_base = 'box1'
+        result = misc.canonicalize_hostname(host_base)
+        assert result == 'ubuntu@box1.front.sepia.ceph.com'
+
+    def test_decanonicalize_hostname(self):
+        host = 'ubuntu@box1.front.sepia.ceph.com'
+        result = misc.decanonicalize_hostname(host)
+        assert result == 'box1'
+
+    def test_canonicalize_hostname_nouser(self):
+        host_base = 'box1'
+        result = misc.canonicalize_hostname(host_base, user=None)
+        assert result == 'box1.front.sepia.ceph.com'
+
+    def test_decanonicalize_hostname_nouser(self):
+        host = 'box1.front.sepia.ceph.com'
+        result = misc.decanonicalize_hostname(host)
+        assert result == 'box1'
+
+    def test_canonicalize_hostname_otherlab(self):
+        config.lab_domain = 'example.com'
+        host_base = 'box1'
+        result = misc.canonicalize_hostname(host_base)
+        assert result == 'ubuntu@box1.example.com'
+
+    def test_decanonicalize_hostname_otherlab(self):
+        config.lab_domain = 'example.com'
+        host = 'ubuntu@box1.example.com'
+        result = misc.decanonicalize_hostname(host)
+        assert result == 'box1'
+
+    def test_canonicalize_hostname_nodomain(self):
+        config.lab_domain = ''
+        host = 'box2'
+        result = misc.canonicalize_hostname(host)
+        assert result == 'ubuntu@' + host
+
+    def test_decanonicalize_hostname_nodomain(self):
+        config.lab_domain = ''
+        host = 'ubuntu@box2'
+        result = misc.decanonicalize_hostname(host)
+        assert result == 'box2'
+
+    def test_canonicalize_hostname_full_other_user(self):
+        config.lab_domain = 'example.com'
+        host = 'user1@box1.example.come'
+        result = misc.canonicalize_hostname(host)
+        assert result == 'user1@box1.example.com'
+
+    def test_decanonicalize_hostname_full_other_user(self):
+        config.lab_domain = 'example.com'
+        host = 'user1@box1.example.come'
+        result = misc.decanonicalize_hostname(host)
+        assert result == 'box1'
+
+class TestMergeConfigs(object):
+    """ Tests merge_config and deep_merge in teuthology.misc """
+
+    @patch("os.path.exists")
+    @patch("yaml.safe_load")
+    @patch("teuthology.misc.open")
+    def test_merge_configs(self, m_open, m_safe_load, m_exists):
+        """ Only tests with one yaml file being passed, mainly just to test
+            the loop logic.  The actual merge will be tested in subsequent
+            tests.
+        """
+        expected = {"a": "b", "b": "c"}
+        m_exists.return_value = True
+        m_safe_load.return_value = expected
+        result = misc.merge_configs(["path/to/config1"])
+        assert result == expected
+        m_open.assert_called_once_with("path/to/config1")
+
+    def test_merge_configs_empty(self):
+        assert misc.merge_configs([]) == {}
+
+    def test_deep_merge(self):
+        a = {"a": "b"}
+        b = {"b": "c"}
+        result = misc.deep_merge(a, b)
+        assert result == {"a": "b", "b": "c"}
+
+    def test_overwrite_deep_merge(self):
+        a = {"a": "b"}
+        b = {"a": "overwritten", "b": "c"}
+        result = misc.deep_merge(a, b)
+        assert result == {"a": "overwritten", "b": "c"}
+
+    def test_list_deep_merge(self):
+        a = [1, 2]
+        b = [3, 4]
+        result = misc.deep_merge(a, b)
+        assert result == [1, 2, 3, 4]
+
+    def test_missing_list_deep_merge(self):
+        a = [1, 2]
+        b = "not a list"
+        with pytest.raises(AssertionError):
+            misc.deep_merge(a, b)
+
+    def test_missing_a_deep_merge(self):
+        result = misc.deep_merge(None, [1, 2])
+        assert result == [1, 2]
+
+    def test_missing_b_deep_merge(self):
+        result = misc.deep_merge([1, 2], None)
+        assert result == [1, 2]
+
+    def test_invalid_b_deep_merge(self):
+        with pytest.raises(AssertionError):
+            misc.deep_merge({"a": "b"}, "invalid")
+
+
+class TestIsInDict(object):
+    def test_simple_membership(self):
+        assert misc.is_in_dict('a', 'foo', {'a':'foo', 'b':'bar'})
+
+    def test_dict_membership(self):
+        assert misc.is_in_dict(
+            'a', {'sub1':'key1', 'sub2':'key2'},
+            {'a':{'sub1':'key1', 'sub2':'key2', 'sub3':'key3'}}
+        )
+
+    def test_simple_nonmembership(self):
+        assert not misc.is_in_dict('a', 'foo', {'a':'bar', 'b':'foo'})
+
+    def test_nonmembership_with_presence_at_lower_level(self):
+        assert not misc.is_in_dict('a', 'foo', {'a':{'a': 'foo'}})
diff --git a/tests/test_packaging.py b/tests/test_packaging.py
new file mode 100644 (file)
index 0000000..046105e
--- /dev/null
@@ -0,0 +1,1086 @@
+import pytest
+
+from unittest.mock import patch, Mock, PropertyMock
+
+from teuthology import packaging
+from teuthology.exceptions import VersionNotFoundError
+
+KOJI_TASK_RPMS_MATRIX = [
+    ('tasks/6745/9666745/kernel-4.1.0-0.rc2.git2.1.fc23.x86_64.rpm', 'kernel'),
+    ('tasks/6745/9666745/kernel-modules-4.1.0-0.rc2.git2.1.fc23.x86_64.rpm', 'kernel-modules'),
+    ('tasks/6745/9666745/kernel-tools-4.1.0-0.rc2.git2.1.fc23.x86_64.rpm', 'kernel-tools'),
+    ('tasks/6745/9666745/kernel-tools-libs-devel-4.1.0-0.rc2.git2.1.fc23.x86_64.rpm', 'kernel-tools-libs-devel'),
+    ('tasks/6745/9666745/kernel-headers-4.1.0-0.rc2.git2.1.fc23.x86_64.rpm', 'kernel-headers'),
+    ('tasks/6745/9666745/kernel-tools-debuginfo-4.1.0-0.rc2.git2.1.fc23.x86_64.rpm', 'kernel-tools-debuginfo'),
+    ('tasks/6745/9666745/kernel-debuginfo-common-x86_64-4.1.0-0.rc2.git2.1.fc23.x86_64.rpm', 'kernel-debuginfo-common-x86_64'),
+    ('tasks/6745/9666745/perf-debuginfo-4.1.0-0.rc2.git2.1.fc23.x86_64.rpm', 'perf-debuginfo'),
+    ('tasks/6745/9666745/kernel-modules-extra-4.1.0-0.rc2.git2.1.fc23.x86_64.rpm', 'kernel-modules-extra'),
+    ('tasks/6745/9666745/kernel-tools-libs-4.1.0-0.rc2.git2.1.fc23.x86_64.rpm', 'kernel-tools-libs'),
+    ('tasks/6745/9666745/kernel-core-4.1.0-0.rc2.git2.1.fc23.x86_64.rpm', 'kernel-core'),
+    ('tasks/6745/9666745/kernel-debuginfo-4.1.0-0.rc2.git2.1.fc23.x86_64.rpm', 'kernel-debuginfo'),
+    ('tasks/6745/9666745/python-perf-4.1.0-0.rc2.git2.1.fc23.x86_64.rpm', 'python-perf'),
+    ('tasks/6745/9666745/kernel-devel-4.1.0-0.rc2.git2.1.fc23.x86_64.rpm', 'kernel-devel'),
+    ('tasks/6745/9666745/python-perf-debuginfo-4.1.0-0.rc2.git2.1.fc23.x86_64.rpm', 'python-perf-debuginfo'),
+    ('tasks/6745/9666745/perf-4.1.0-0.rc2.git2.1.fc23.x86_64.rpm', 'perf'),
+]
+
+KOJI_TASK_RPMS = [rpm[0] for rpm in KOJI_TASK_RPMS_MATRIX]
+
+
+class TestPackaging(object):
+
+    def test_get_package_name_deb(self):
+        remote = Mock()
+        remote.os.package_type = "deb"
+        assert packaging.get_package_name('sqlite', remote) == "sqlite3"
+
+    def test_get_package_name_rpm(self):
+        remote = Mock()
+        remote.os.package_type = "rpm"
+        assert packaging.get_package_name('sqlite', remote) is None
+
+    def test_get_package_name_not_found(self):
+        remote = Mock()
+        remote.os.package_type = "rpm"
+        assert packaging.get_package_name('notthere', remote) is None
+
+    def test_get_service_name_deb(self):
+        remote = Mock()
+        remote.os.package_type = "deb"
+        assert packaging.get_service_name('httpd', remote) == 'apache2'
+
+    def test_get_service_name_rpm(self):
+        remote = Mock()
+        remote.os.package_type = "rpm"
+        assert packaging.get_service_name('httpd', remote) == 'httpd'
+
+    def test_get_service_name_not_found(self):
+        remote = Mock()
+        remote.os.package_type = "rpm"
+        assert packaging.get_service_name('notthere', remote) is None
+
+    def test_install_package_deb(self):
+        m_remote = Mock()
+        m_remote.os.package_type = "deb"
+        expected = [
+            'DEBIAN_FRONTEND=noninteractive',
+            'sudo',
+            '-E',
+            'apt-get',
+            '-y',
+            '--force-yes',
+            'install',
+            'apache2'
+        ]
+        packaging.install_package('apache2', m_remote)
+        m_remote.run.assert_called_with(args=expected)
+
+    def test_install_package_rpm(self):
+        m_remote = Mock()
+        m_remote.os.package_type = "rpm"
+        expected = [
+            'sudo',
+            'yum',
+            '-y',
+            'install',
+            'httpd'
+        ]
+        packaging.install_package('httpd', m_remote)
+        m_remote.run.assert_called_with(args=expected)
+
+    def test_remove_package_deb(self):
+        m_remote = Mock()
+        m_remote.os.package_type = "deb"
+        expected = [
+            'DEBIAN_FRONTEND=noninteractive',
+            'sudo',
+            '-E',
+            'apt-get',
+            '-y',
+            'purge',
+            'apache2'
+        ]
+        packaging.remove_package('apache2', m_remote)
+        m_remote.run.assert_called_with(args=expected)
+
+    def test_remove_package_rpm(self):
+        m_remote = Mock()
+        m_remote.os.package_type = "rpm"
+        expected = [
+            'sudo',
+            'yum',
+            '-y',
+            'erase',
+            'httpd'
+        ]
+        packaging.remove_package('httpd', m_remote)
+        m_remote.run.assert_called_with(args=expected)
+
+    def test_get_koji_package_name(self):
+        build_info = dict(version="3.10.0", release="123.20.1")
+        result = packaging.get_koji_package_name("kernel", build_info)
+        assert result == "kernel-3.10.0-123.20.1.x86_64.rpm"
+
+    @patch("teuthology.packaging.config")
+    def test_get_kojiroot_base_url(self, m_config):
+        m_config.kojiroot_url = "http://kojiroot.com"
+        build_info = dict(
+            package_name="kernel",
+            version="3.10.0",
+            release="123.20.1",
+        )
+        result = packaging.get_kojiroot_base_url(build_info)
+        expected = "http://kojiroot.com/kernel/3.10.0/123.20.1/x86_64/"
+        assert result == expected
+
+    @patch("teuthology.packaging.config")
+    def test_get_koji_build_info_success(self, m_config):
+        m_config.kojihub_url = "http://kojihub.com"
+        m_proc = Mock()
+        expected = dict(foo="bar")
+        m_proc.exitstatus = 0
+        m_proc.stdout.getvalue.return_value = str(expected)
+        m_remote = Mock()
+        m_remote.run.return_value = m_proc
+        result = packaging.get_koji_build_info(1, m_remote, dict())
+        assert result == expected
+        args, kwargs = m_remote.run.call_args
+        expected_args = [
+            'python', '-c',
+            'import koji; '
+            'hub = koji.ClientSession("http://kojihub.com"); '
+            'print(hub.getBuild(1))',
+        ]
+        assert expected_args == kwargs['args']
+
+    @patch("teuthology.packaging.config")
+    def test_get_koji_build_info_fail(self, m_config):
+        m_config.kojihub_url = "http://kojihub.com"
+        m_proc = Mock()
+        m_proc.exitstatus = 1
+        m_remote = Mock()
+        m_remote.run.return_value = m_proc
+        m_ctx = Mock()
+        m_ctx.summary = dict()
+        with pytest.raises(RuntimeError):
+            packaging.get_koji_build_info(1, m_remote, m_ctx)
+
+    @patch("teuthology.packaging.config")
+    def test_get_koji_task_result_success(self, m_config):
+        m_config.kojihub_url = "http://kojihub.com"
+        m_proc = Mock()
+        expected = dict(foo="bar")
+        m_proc.exitstatus = 0
+        m_proc.stdout.getvalue.return_value = str(expected)
+        m_remote = Mock()
+        m_remote.run.return_value = m_proc
+        result = packaging.get_koji_task_result(1, m_remote, dict())
+        assert result == expected
+        args, kwargs = m_remote.run.call_args
+        expected_args = [
+            'python', '-c',
+            'import koji; '
+            'hub = koji.ClientSession("http://kojihub.com"); '
+            'print(hub.getTaskResult(1))',
+        ]
+        assert expected_args == kwargs['args']
+
+    @patch("teuthology.packaging.config")
+    def test_get_koji_task_result_fail(self, m_config):
+        m_config.kojihub_url = "http://kojihub.com"
+        m_proc = Mock()
+        m_proc.exitstatus = 1
+        m_remote = Mock()
+        m_remote.run.return_value = m_proc
+        m_ctx = Mock()
+        m_ctx.summary = dict()
+        with pytest.raises(RuntimeError):
+            packaging.get_koji_task_result(1, m_remote, m_ctx)
+
+    @patch("teuthology.packaging.config")
+    def test_get_koji_task_rpm_info_success(self, m_config):
+        m_config.koji_task_url = "http://kojihub.com/work"
+        expected = dict(
+            base_url="http://kojihub.com/work/tasks/6745/9666745/",
+            version="4.1.0-0.rc2.git2.1.fc23.x86_64",
+            rpm_name="kernel-4.1.0-0.rc2.git2.1.fc23.x86_64.rpm",
+            package_name="kernel",
+        )
+        result = packaging.get_koji_task_rpm_info('kernel', KOJI_TASK_RPMS)
+        assert expected == result
+
+    @patch("teuthology.packaging.config")
+    def test_get_koji_task_rpm_info_fail(self, m_config):
+        m_config.koji_task_url = "http://kojihub.com/work"
+        with pytest.raises(RuntimeError):
+            packaging.get_koji_task_rpm_info('ceph', KOJI_TASK_RPMS)
+
+    def test_get_package_version_deb_found(self):
+        remote = Mock()
+        remote.os.package_type = "deb"
+        proc = Mock()
+        proc.exitstatus = 0
+        proc.stdout.getvalue.return_value = "2.2"
+        remote.run.return_value = proc
+        result = packaging.get_package_version(remote, "apache2")
+        assert result == "2.2"
+
+    def test_get_package_version_deb_command(self):
+        remote = Mock()
+        remote.os.package_type = "deb"
+        packaging.get_package_version(remote, "apache2")
+        args, kwargs = remote.run.call_args
+        expected_args = ['dpkg-query', '-W', '-f', '${Version}', 'apache2']
+        assert expected_args == kwargs['args']
+
+    def test_get_package_version_rpm_found(self):
+        remote = Mock()
+        remote.os.package_type = "rpm"
+        proc = Mock()
+        proc.exitstatus = 0
+        proc.stdout.getvalue.return_value = "2.2"
+        remote.run.return_value = proc
+        result = packaging.get_package_version(remote, "httpd")
+        assert result == "2.2"
+
+    def test_get_package_version_rpm_command(self):
+        remote = Mock()
+        remote.os.package_type = "rpm"
+        packaging.get_package_version(remote, "httpd")
+        args, kwargs = remote.run.call_args
+        expected_args = ['rpm', '-q', 'httpd', '--qf', '%{VERSION}-%{RELEASE}']
+        assert expected_args == kwargs['args']
+
+    def test_get_package_version_not_found(self):
+        remote = Mock()
+        remote.os.package_type = "rpm"
+        proc = Mock()
+        proc.exitstatus = 1
+        proc.stdout.getvalue.return_value = "not installed"
+        remote.run.return_value = proc
+        result = packaging.get_package_version(remote, "httpd")
+        assert result is None
+
+    def test_get_package_version_invalid_version(self):
+        # this tests the possibility that the package is not found
+        # but the exitstatus is still 0.  Not entirely sure we'll ever
+        # hit this condition, but I want to test the codepath regardless
+        remote = Mock()
+        remote.os.package_type = "rpm"
+        proc = Mock()
+        proc.exitstatus = 0
+        proc.stdout.getvalue.return_value = "not installed"
+        remote.run.return_value = proc
+        result = packaging.get_package_version(remote, "httpd")
+        assert result is None
+
+    @pytest.mark.parametrize("input, expected", KOJI_TASK_RPMS_MATRIX)
+    def test_get_koji_task_result_package_name(self, input, expected):
+        assert packaging._get_koji_task_result_package_name(input) == expected
+
+    @patch("requests.get")
+    def test_get_response_success(self, m_get):
+        resp = Mock()
+        resp.ok = True
+        m_get.return_value = resp
+        result = packaging._get_response("google.com")
+        assert result == resp
+
+    @patch("requests.get")
+    def test_get_response_failed_wait(self, m_get):
+        resp = Mock()
+        resp.ok = False
+        m_get.return_value = resp
+        packaging._get_response("google.com", wait=True, sleep=1, tries=2)
+        assert m_get.call_count == 2
+
+    @patch("requests.get")
+    def test_get_response_failed_no_wait(self, m_get):
+        resp = Mock()
+        resp.ok = False
+        m_get.return_value = resp
+        packaging._get_response("google.com", sleep=1, tries=2)
+        assert m_get.call_count == 1
+
+    @patch('teuthology.packaging.config')
+    def test_get_builder_project_pulp(self, m_config):
+        m_config.package_source = 'pulp'
+        assert packaging.get_builder_project() is packaging.PulpProject
+
+
+class TestBuilderProject(object):
+    klass = None
+
+    def setup_method(self):
+        if self.klass is None:
+            pytest.skip()
+
+    def _get_remote(self, arch="x86_64", system_type="deb", distro="ubuntu",
+                    codename="focal", version="20.04"):
+        rem = Mock()
+        rem.system_type = system_type
+        rem.os.name = distro
+        rem.os.codename = codename
+        rem.os.version = version
+        rem.arch = arch
+
+        return rem
+
+    def test_init_from_remote_base_url(self, expected=None):
+        assert expected is not None
+        rem = self._get_remote()
+        ctx = dict(foo="bar")
+        gp = self.klass("ceph", {}, ctx=ctx, remote=rem)
+        result = gp.base_url
+        assert result == expected
+
+    def test_init_from_remote_base_url_debian(self, expected=None):
+        assert expected is not None
+        # remote.os.codename returns and empty string on debian
+        rem = self._get_remote(distro="debian", codename='', version="7.1")
+        ctx = dict(foo="bar")
+        gp = self.klass("ceph", {}, ctx=ctx, remote=rem)
+        result = gp.base_url
+        assert result == expected
+
+    def test_init_from_config_base_url(self, expected=None):
+        assert expected is not None
+        config = dict(
+            os_type="ubuntu",
+            os_version="20.04",
+            sha1="sha1",
+        )
+        gp = self.klass("ceph", config)
+        result = gp.base_url
+        print(self.m_get.call_args_list)
+        assert result == expected
+
+    def test_init_from_config_branch_ref(self):
+        config = dict(
+            os_type="ubuntu",
+            os_version="20.04",
+            branch='jewel',
+        )
+        gp = self.klass("ceph", config)
+        result = gp.uri_reference
+        expected = 'ref/jewel'
+        assert result == expected
+
+    def test_init_from_config_tag_ref(self):
+        config = dict(
+            os_type="ubuntu",
+            os_version="20.04",
+            tag='v10.0.1',
+        )
+        gp = self.klass("ceph", config)
+        result = gp.uri_reference
+        expected = 'ref/v10.0.1'
+        assert result == expected
+
+    def test_init_from_config_tag_overrides_branch_ref(self, caplog):
+        config = dict(
+            os_type="ubuntu",
+            os_version="20.04",
+            branch='jewel',
+            tag='v10.0.1',
+        )
+        gp = self.klass("ceph", config)
+        result = gp.uri_reference
+        expected = 'ref/v10.0.1'
+        assert result == expected
+        expected_log = 'More than one of ref, tag, branch, or sha1 supplied; using tag'
+        assert expected_log in caplog.text
+        return gp
+
+    def test_init_from_config_branch_overrides_sha1(self, caplog):
+        config = dict(
+            os_type="ubuntu",
+            os_version="20.04",
+            branch='jewel',
+            sha1='sha1',
+        )
+        gp = self.klass("ceph", config)
+        result = gp.uri_reference
+        expected = 'ref/jewel'
+        assert result == expected
+        expected_log = 'More than one of ref, tag, branch, or sha1 supplied; using branch'
+        assert expected_log in caplog.text
+        return gp
+
+    REFERENCE_MATRIX = [
+        ('the_ref', 'the_tag', 'the_branch', 'the_sha1', dict(ref='the_ref')),
+        (None, 'the_tag', 'the_branch', 'the_sha1', dict(tag='the_tag')),
+        (None, None, 'the_branch', 'the_sha1', dict(branch='the_branch')),
+        (None, None, None, 'the_sha1', dict(sha1='the_sha1')),
+        (None, None, 'the_branch', None, dict(branch='the_branch')),
+    ]
+
+    @pytest.mark.parametrize(
+        "ref, tag, branch, sha1, expected",
+        REFERENCE_MATRIX,
+    )
+    def test_choose_reference(self, ref, tag, branch, sha1, expected):
+        config = dict(
+            os_type='ubuntu',
+            os_version='18.04',
+        )
+        if ref:
+            config['ref'] = ref
+        if tag:
+            config['tag'] = tag
+        if branch:
+            config['branch'] = branch
+        if sha1:
+            config['sha1'] = sha1
+        gp = self.klass("ceph", config)
+        assert gp._choose_reference() == expected
+
+    def test_get_package_version_found(self):
+        rem = self._get_remote()
+        ctx = dict(foo="bar")
+        gp = self.klass("ceph", {}, ctx=ctx, remote=rem)
+        assert gp.version == "0.90.0"
+
+    @patch("teuthology.packaging._get_response")
+    def test_get_package_version_not_found(self, m_get_response):
+        rem = self._get_remote()
+        ctx = dict(foo="bar")
+        resp = Mock()
+        resp.ok = False
+        m_get_response.return_value = resp
+        gp = self.klass("ceph", {}, ctx=ctx, remote=rem)
+        with pytest.raises(VersionNotFoundError):
+            gp.version
+
+    def test_get_package_sha1_fetched_found(self):
+        rem = self._get_remote()
+        ctx = dict(foo="bar")
+        gp = self.klass("ceph", {}, ctx=ctx, remote=rem)
+        assert gp.sha1 == "the_sha1"
+
+    def test_get_package_sha1_fetched_not_found(self):
+        rem = self._get_remote()
+        ctx = dict(foo="bar")
+        gp = self.klass("ceph", {}, ctx=ctx, remote=rem)
+        assert not gp.sha1
+
+    DISTRO_MATRIX = [None] * 12
+
+    @pytest.mark.parametrize(
+        "matrix_index",
+        range(len(DISTRO_MATRIX)),
+    )
+    def test_get_distro_remote(self, matrix_index):
+        (distro, version, codename, expected) = \
+            self.DISTRO_MATRIX[matrix_index]
+        rem = self._get_remote(distro=distro, version=version,
+                               codename=codename)
+        ctx = dict(foo="bar")
+        gp = self.klass("ceph", {}, ctx=ctx, remote=rem)
+        assert gp.distro == expected
+
+    DISTRO_MATRIX_NOVER = [
+        ('rhel', None, None, 'centos8'),
+        ('centos', None, None, 'centos8'),
+        ('fedora', None, None, 'fedora25'),
+        ('ubuntu', None, None, 'focal'),
+        ('debian', None, None, 'jessie'),
+    ]
+
+    @pytest.mark.parametrize(
+        "matrix_index",
+        range(len(DISTRO_MATRIX) + len(DISTRO_MATRIX_NOVER)),
+    )
+    def test_get_distro_config(self, matrix_index):
+        (distro, version, codename, expected) = \
+            (self.DISTRO_MATRIX + self.DISTRO_MATRIX_NOVER)[matrix_index]
+        config = dict(
+            os_type=distro,
+            os_version=version
+        )
+        gp = self.klass("ceph", config)
+        assert gp.distro == expected
+
+    DIST_RELEASE_MATRIX = [
+        ('rhel', '7.0', None, 'el7'),
+        ('centos', '6.5', None, 'el6'),
+        ('centos', '7.0', None, 'el7'),
+        ('centos', '7.1', None, 'el7'),
+        ('centos', '8.1', None, 'el8'),
+        ('fedora', '20', None, 'fc20'),
+        ('debian', '7.0', None, 'debian'),
+        ('debian', '7', None, 'debian'),
+        ('debian', '7.1', None, 'debian'),
+        ('ubuntu', '12.04', None, 'ubuntu'),
+        ('ubuntu', '14.04', None, 'ubuntu'),
+        ('ubuntu', '16.04', None, 'ubuntu'),
+        ('ubuntu', '18.04', None, 'ubuntu'),
+        ('ubuntu', '20.04', None, 'ubuntu'),
+    ]
+
+    @pytest.mark.parametrize(
+        "matrix_index",
+        range(len(DIST_RELEASE_MATRIX)),
+    )
+    def test_get_dist_release(self, matrix_index):
+        (distro, version, codename, expected) = \
+            (self.DIST_RELEASE_MATRIX)[matrix_index]
+        rem = self._get_remote(distro=distro, version=version,
+                               codename=codename)
+        ctx = dict(foo="bar")
+        gp = self.klass("ceph", {}, ctx=ctx, remote=rem)
+        assert gp.dist_release == expected
+
+
+class TestShamanProject(TestBuilderProject):
+    klass = packaging.ShamanProject
+
+    def setup_method(self):
+        self.p_config = patch('teuthology.packaging.config')
+        self.m_config = self.p_config.start()
+        self.m_config.package_source = 'shaman'
+        self.m_config.shaman = {'endpoint': 'https://shaman.ceph.com/api/'}
+        self.p_get_config_value = \
+            patch('teuthology.packaging._get_config_value_for_remote')
+        self.m_get_config_value = self.p_get_config_value.start()
+        self.m_get_config_value.return_value = None
+        self.p_get = patch('requests.get')
+        self.m_get = self.p_get.start()
+
+    def teardown_method(self):
+        self.p_config.stop()
+        self.p_get_config_value.stop()
+        self.p_get.stop()
+
+    def test_init_from_remote_base_url(self):
+        # Here, we really just need to make sure ShamanProject._search()
+        # queries the right URL. So let's make _get_base_url() just pass that
+        # URL through and test that value.
+        def m_get_base_url(obj):
+            obj._search()
+            return self.m_get.call_args_list[0][0][0]
+        with patch(
+            'teuthology.packaging.ShamanProject._get_base_url',
+            new=m_get_base_url,
+        ):
+            super(TestShamanProject, self)\
+                .test_init_from_remote_base_url(
+                    "https://shaman.ceph.com/api/search?status=ready"
+                    "&project=ceph&flavor=default"
+                    "&distros=ubuntu%2F20.04%2Fx86_64&ref=main"
+                )
+
+    def test_init_from_remote_base_url_debian(self):
+        # Here, we really just need to make sure ShamanProject._search()
+        # queries the right URL. So let's make _get_base_url() just pass that
+        # URL through and test that value.
+        def m_get_base_url(obj):
+            obj._search()
+            return self.m_get.call_args_list[0][0][0]
+        with patch(
+            'teuthology.packaging.ShamanProject._get_base_url',
+            new=m_get_base_url,
+        ):
+            super(TestShamanProject, self)\
+                .test_init_from_remote_base_url_debian(
+                    "https://shaman.ceph.com/api/search?status=ready"
+                    "&project=ceph&flavor=default"
+                    "&distros=debian%2F7.1%2Fx86_64&ref=main"
+                )
+
+    def test_init_from_config_base_url(self):
+        # Here, we really just need to make sure ShamanProject._search()
+        # queries the right URL. So let's make _get_base_url() just pass that
+        # URL through and test that value.
+        def m_get_base_url(obj):
+            obj._search()
+            return self.m_get.call_args_list[0][0][0]
+        with patch(
+            'teuthology.packaging.ShamanProject._get_base_url',
+            new=m_get_base_url,
+        ):
+            super(TestShamanProject, self).test_init_from_config_base_url(
+                "https://shaman.ceph.com/api/search?status=ready&project=ceph" \
+                "&flavor=default&distros=ubuntu%2F20.04%2Fx86_64&sha1=sha1"
+            )
+
+    @patch('teuthology.packaging.ShamanProject._get_package_sha1')
+    def test_init_from_config_tag_ref(self, m_get_package_sha1):
+        m_get_package_sha1.return_value = 'the_sha1'
+        super(TestShamanProject, self).test_init_from_config_tag_ref()
+
+    def test_init_from_config_tag_overrides_branch_ref(self, caplog):
+        with patch(
+            'teuthology.packaging.repo_utils.ls_remote',
+        ) as m_ls_remote:
+            m_ls_remote.return_value = 'sha1_from_my_tag'
+            obj = super(TestShamanProject, self)\
+                .test_init_from_config_tag_overrides_branch_ref(caplog)
+            search_uri = obj._search_uri
+        assert 'sha1=sha1_from_my_tag' in search_uri
+        assert 'jewel' not in search_uri
+
+    def test_init_from_config_branch_overrides_sha1(self, caplog):
+        obj = super(TestShamanProject, self)\
+            .test_init_from_config_branch_overrides_sha1(caplog)
+        search_uri = obj._search_uri
+        assert 'jewel' in search_uri
+        assert 'sha1' not in search_uri
+
+    def test_get_package_version_found(self):
+        resp = Mock()
+        resp.ok = True
+        resp.json.return_value = [
+            dict(
+                sha1='the_sha1',
+                extra=dict(package_manager_version='0.90.0'),
+            )
+        ]
+        self.m_get.return_value = resp
+        super(TestShamanProject, self)\
+            .test_get_package_version_found()
+
+    def test_get_package_sha1_fetched_found(self):
+        resp = Mock()
+        resp.ok = True
+        resp.json.return_value = [dict(sha1='the_sha1')]
+        self.m_get.return_value = resp
+        super(TestShamanProject, self)\
+            .test_get_package_sha1_fetched_found()
+
+    def test_get_package_sha1_fetched_not_found(self):
+        resp = Mock()
+        resp.json.return_value = []
+        self.m_get.return_value = resp
+        super(TestShamanProject, self)\
+            .test_get_package_sha1_fetched_not_found()
+
+    SHAMAN_SEARCH_RESPONSE = [
+        {
+            "status": "ready",
+            "sha1": "534fc6d936bd506119f9e0921ff8cf8d47caa323",
+            "extra": {
+                "build_url": "https://jenkins.ceph.com/job/ceph-dev-build/ARCH=x86_64,AVAILABLE_ARCH=x86_64,AVAILABLE_DIST=centos8,DIST=centos8,MACHINE_SIZE=gigantic/48556/",
+                "root_build_cause": "SCMTRIGGER",
+                "version": "17.0.0-8856-g534fc6d9",
+                "node_name": "172.21.2.7+braggi07",
+                "job_name": "ceph-dev-build/ARCH=x86_64,AVAILABLE_ARCH=x86_64,AVAILABLE_DIST=centos8,DIST=centos8,MACHINE_SIZE=gigantic",
+                "package_manager_version": "17.0.0-8856.g534fc6d9"
+            },
+            "url": "https://3.chacra.ceph.com/r/ceph/main/534fc6d936bd506119f9e0921ff8cf8d47caa323/centos/8/flavors/default/",
+            "modified": "2021-11-06 21:40:40.669823",
+            "distro_version": "8",
+            "project": "ceph",
+            "flavor": "default",
+            "ref": "main",
+            "chacra_url": "https://3.chacra.ceph.com/repos/ceph/main/534fc6d936bd506119f9e0921ff8cf8d47caa323/centos/8/flavors/default/",
+            "archs": [
+                "x86_64",
+                "arm64",
+                "source"
+            ],
+            "distro": "centos"
+          }
+    ]
+
+    SHAMAN_BUILDS_RESPONSE = [
+        {
+            "status": "completed",
+            "sha1": "534fc6d936bd506119f9e0921ff8cf8d47caa323",
+            "distro_arch": "arm64",
+            "started": "2021-11-06 20:20:15.121203",
+            "completed": "2021-11-06 22:36:27.115950",
+            "extra": {
+                "node_name": "172.21.4.66+confusa04",
+                "version": "17.0.0-8856-g534fc6d9",
+                "build_user": "",
+                "root_build_cause": "SCMTRIGGER",
+
+                "job_name": "ceph-dev-build/ARCH=arm64,AVAILABLE_ARCH=arm64,AVAILABLE_DIST=centos8,DIST=centos8,MACHINE_SIZE=gigantic"
+            },
+            "modified": "2021-11-06 22:36:27.118043",
+            "distro_version": "8",
+            "project": "ceph",
+            "url": "https://jenkins.ceph.com/job/ceph-dev-build/ARCH=arm64,AVAILABLE_ARCH=arm64,AVAILABLE_DIST=centos8,DIST=centos8,MACHINE_SIZE=gigantic/48556/",
+            "log_url": "https://jenkins.ceph.com/job/ceph-dev-build/ARCH=arm64,AVAILABLE_ARCH=arm64,AVAILABLE_DIST=centos8,DIST=centos8,MACHINE_SIZE=gigantic/48556//consoleFull",
+            "flavor": "default",
+            "ref": "main",
+            "distro": "centos"
+        },
+        {
+            "status": "completed",
+            "sha1": "534fc6d936bd506119f9e0921ff8cf8d47caa323",
+            "distro_arch": "x86_64",
+            "started": "2021-11-06 20:20:06.740692",
+            "completed": "2021-11-06 21:43:51.711970",
+            "extra": {
+                "node_name": "172.21.2.7+braggi07",
+                "version": "17.0.0-8856-g534fc6d9",
+                "build_user": "",
+                "root_build_cause": "SCMTRIGGER",
+                "job_name": "ceph-dev-build/ARCH=x86_64,AVAILABLE_ARCH=x86_64,AVAILABLE_DIST=centos8,DIST=centos8,MACHINE_SIZE=gigantic"
+            },
+            "modified": "2021-11-06 21:43:51.713487",
+            "distro_version": "8",
+            "project": "ceph",
+            "url": "https://jenkins.ceph.com/job/ceph-dev-build/ARCH=x86_64,AVAILABLE_ARCH=x86_64,AVAILABLE_DIST=centos8,DIST=centos8,MACHINE_SIZE=gigantic/48556/",
+            "log_url": "https://jenkins.ceph.com/job/ceph-dev-build/ARCH=x86_64,AVAILABLE_ARCH=x86_64,AVAILABLE_DIST=centos8,DIST=centos8,MACHINE_SIZE=gigantic/48556//consoleFull",
+            "flavor": "default",
+            "ref": "main",
+            "distro": "centos"
+        }
+    ]
+
+    def test_build_complete_success(self):
+        config = dict(
+            os_type="centos",
+            os_version="8",
+            branch='main',
+            arch='x86_64',
+            flavor='default',
+        )
+        builder = self.klass("ceph", config)
+
+        search_resp = Mock()
+        search_resp.ok = True
+        search_resp.json.return_value = self.SHAMAN_SEARCH_RESPONSE
+        self.m_get.return_value = search_resp
+        # cause builder to call requests.get and cache search_resp
+        builder.assert_result()
+
+        build_resp = Mock()
+        build_resp.ok = True
+        self.m_get.return_value = build_resp
+
+        # both archs completed, so x86_64 build is complete
+        builds = build_resp.json.return_value = self.SHAMAN_BUILDS_RESPONSE
+        assert builder.build_complete
+
+        # mark the arm64 build failed, x86_64 should still be complete
+        builds[0]['status'] = "failed"
+        build_resp.json.return_value = builds
+        assert builder.build_complete
+
+        # mark the x86_64 build failed, should show incomplete
+        builds[1]['status'] = "failed"
+        build_resp.json.return_value = builds
+        assert not builder.build_complete
+
+        # mark the arm64 build complete again, x86_64 still incomplete
+        builds[0]['status'] = "completed"
+        build_resp.json.return_value = builds
+        assert not builder.build_complete
+
+    DISTRO_MATRIX = [
+        ('rhel', '7.0', None, 'centos/7'),
+        ('alma', '9.7', None, 'alma/9'),
+        ('rocky', '9.7', None, 'rocky/9'),
+        ('rocky', '10.1', None, 'rocky/10'),
+        ('centos', '6.5', None, 'centos/6'),
+        ('centos', '7.0', None, 'centos/7'),
+        ('centos', '7.1', None, 'centos/7'),
+        ('centos', '8.1', None, 'centos/8'),
+        ('fedora', '20', None, 'fedora/20'),
+        ('ubuntu', '14.04', 'trusty', 'ubuntu/14.04'),
+        ('ubuntu', '14.04', None, 'ubuntu/14.04'),
+        ('debian', '7.0', None, 'debian/7.0'),
+        ('debian', '7', None, 'debian/7'),
+        ('debian', '7.1', None, 'debian/7.1'),
+        ('ubuntu', '12.04', None, 'ubuntu/12.04'),
+        ('ubuntu', '14.04', None, 'ubuntu/14.04'),
+        ('ubuntu', '16.04', None, 'ubuntu/16.04'),
+        ('ubuntu', '18.04', None, 'ubuntu/18.04'),
+        ('ubuntu', '20.04', None, 'ubuntu/20.04'),
+        ('opensuse', '15.6', None, 'opensuse/15.6'),
+        ('sle', '15.5', None, 'sle/15.5'),
+    ]
+    @pytest.mark.parametrize(
+        "matrix_index",
+        range(len(DISTRO_MATRIX)),
+    )
+    def test_get_distro_remote(self, matrix_index):
+        super().test_get_distro_remote(matrix_index)
+
+    DISTRO_MATRIX_NOVER = [
+        ('rhel', None, None, 'centos/8'),
+        ('centos', None, None, 'centos/9'),
+        ('fedora', None, None, 'fedora/25'),
+        ('ubuntu', None, None, 'ubuntu/22.04'),
+        ('alma', None, None, 'alma/9'),
+        ('rocky', None, None, 'rocky/9'),
+    ]
+    @pytest.mark.parametrize(
+        "matrix_index",
+        range(len(DISTRO_MATRIX) + len(DISTRO_MATRIX_NOVER)),
+    )
+    def test_get_distro_config(self, matrix_index):
+        super().test_get_distro_config(matrix_index)
+
+
+class TestPulpProject(TestBuilderProject):
+    klass = packaging.PulpProject
+
+    def setup_method(self):
+        self.p_config = patch('teuthology.packaging.config')
+        self.m_config = self.p_config.start()
+        self.m_config.package_source = 'pulp'
+        self.m_config.pulp = {
+            'endpoint': 'https://pulp.example.com/pulp/api/v3/',
+            'username': 'u',
+            'password': 'p',
+        }
+        self.p_get_config_value = \
+            patch('teuthology.packaging._get_config_value_for_remote')
+        self.m_get_config_value = self.p_get_config_value.start()
+        self.m_get_config_value.return_value = None
+        self.p_get = patch('requests.get')
+        self.m_get = self.p_get.start()
+
+        resp = Mock()
+        resp.ok = True
+        resp.json.return_value = {
+            'results': [{
+                'base_url': 'dist/ceph/main/sha/centos/8/flavors/default/',
+                'pulp_labels': {
+                    'sha1': 'the_sha1',
+                    'version': '0.90.0',
+                },
+            }],
+        }
+        self.m_get.return_value = resp
+
+    def teardown_method(self):
+        self.p_config.stop()
+        self.p_get_config_value.stop()
+        self.p_get.stop()
+
+    def test_init_requires_pulp_credentials(self):
+        self.m_config.pulp = {'username': '', 'password': 'p'}
+        with pytest.raises(ValueError, match='Pulp username and password are required'):
+            packaging.PulpProject('ceph', {}, ctx=dict(foo='bar'),
+                                  remote=self._get_remote())
+
+        self.m_config.pulp = {'username': 'u', 'password': ''}
+        with pytest.raises(ValueError, match='Pulp username and password are required'):
+            packaging.PulpProject('ceph', {}, ctx=dict(foo='bar'),
+                                  remote=self._get_remote())
+
+    def test_init_from_remote_base_url(self):
+        def m_get_base_url(obj):
+            obj._search()
+            return self.m_get.call_args_list[0][0][0]
+        with patch(
+            'teuthology.packaging.PulpProject._get_base_url',
+            new=m_get_base_url,
+        ):
+            super(TestPulpProject, self).test_init_from_remote_base_url(
+                'https://pulp.example.com/pulp/api/v3/distributions/deb/apt',
+            )
+
+    def test_init_from_remote_base_url_debian(self):
+        def m_get_base_url(obj):
+            obj._search()
+            return self.m_get.call_args_list[0][0][0]
+        with patch(
+            'teuthology.packaging.PulpProject._get_base_url',
+            new=m_get_base_url,
+        ):
+            super(TestPulpProject, self).test_init_from_remote_base_url_debian(
+                'https://pulp.example.com/pulp/api/v3/distributions/deb/apt',
+            )
+
+    def test_init_from_config_base_url(self):
+        def m_get_base_url(obj):
+            obj._search()
+            return self.m_get.call_args_list[0][0][0]
+        with patch(
+            'teuthology.packaging.PulpProject._get_base_url',
+            new=m_get_base_url,
+        ):
+            super(TestPulpProject, self).test_init_from_config_base_url(
+                'https://pulp.example.com/pulp/api/v3/distributions/deb/apt',
+            )
+
+    @patch('teuthology.packaging.PulpProject._get_package_sha1')
+    def test_init_from_config_tag_ref(self, m_get_package_sha1):
+        m_get_package_sha1.return_value = 'the_sha1'
+        super(TestPulpProject, self).test_init_from_config_tag_ref()
+
+    def test_init_from_config_tag_overrides_branch_ref(self, caplog):
+        obj = super(TestPulpProject, self)\
+            .test_init_from_config_tag_overrides_branch_ref(caplog)
+        obj._search()
+        labels = self.m_get.call_args[1]['params']['pulp_label_select']
+        assert 'tag=v10.0.1' in labels
+        assert 'branch=jewel' not in labels
+
+    def test_init_from_config_branch_overrides_sha1(self, caplog):
+        obj = super(TestPulpProject, self)\
+            .test_init_from_config_branch_overrides_sha1(caplog)
+        obj._search()
+        labels = self.m_get.call_args[1]['params']['pulp_label_select']
+        assert 'branch=jewel' in labels
+        assert 'sha1=sha1' not in labels
+
+    def test_get_package_version_found(self):
+        resp = Mock()
+        resp.ok = True
+        resp.json.return_value = {
+            'results': [{
+                'base_url': 'a/b/c/d',
+                'pulp_labels': {'version': '0.90.0'},
+            }],
+        }
+        self.m_get.return_value = resp
+        super(TestPulpProject, self).test_get_package_version_found()
+
+    def test_get_package_version_not_found(self):
+        rem = self._get_remote()
+        ctx = dict(foo="bar")
+        resp = Mock()
+        resp.ok = False
+        self.m_get.return_value = resp
+        gp = self.klass("ceph", {}, ctx=ctx, remote=rem)
+        with pytest.raises(VersionNotFoundError):
+            gp.version
+
+    def test_get_package_sha1_fetched_found(self):
+        resp = Mock()
+        resp.ok = True
+        resp.json.return_value = {
+            'results': [{'base_url': 'a/b/c/d', 'pulp_labels': {'sha1': 'the_sha1'}}],
+        }
+        self.m_get.return_value = resp
+        super(TestPulpProject, self).test_get_package_sha1_fetched_found()
+
+    def test_get_package_sha1_fetched_not_found(self):
+        resp = Mock()
+        resp.ok = True
+        resp.json.return_value = {
+            'results': [{'base_url': 'a/b/c/d', 'pulp_labels': {}}],
+        }
+        self.m_get.return_value = resp
+        super(TestPulpProject, self).test_get_package_sha1_fetched_not_found()
+
+    DISTRO_MATRIX = [
+        ('rhel', '7.0', None, 'centos/7'),
+        ('alma', '9.7', None, 'alma/9'),
+        ('rocky', '10.1', None, 'rocky/10'),
+        ('centos', '8.1', None, 'centos/8'),
+        ('fedora', '20', None, 'fedora/20'),
+        ('ubuntu', '20.04', 'focal', 'ubuntu/focal'),
+    ]
+
+    @pytest.mark.parametrize(
+        "matrix_index",
+        range(len(DISTRO_MATRIX)),
+    )
+    def test_get_distro_remote(self, matrix_index):
+        super().test_get_distro_remote(matrix_index)
+
+    DISTRO_MATRIX_NOVER = [
+        ('rhel', None, None, 'centos/8'),
+        ('centos', None, None, 'centos/9'),
+        ('fedora', None, None, 'fedora/25'),
+        ('ubuntu', None, None, 'ubuntu/jammy'),
+        ('alma', None, None, 'alma/9'),
+        ('rocky', None, None, 'rocky/9'),
+    ]
+
+    DISTRO_MATRIX_CONFIG = [
+        ('rhel', '7.0', None, 'centos/7'),
+        ('alma', '9.7', None, 'alma/9'),
+        ('rocky', '10.1', None, 'rocky/10'),
+        ('centos', '8.1', None, 'centos/8'),
+        ('fedora', '20', None, 'fedora/20'),
+        ('ubuntu', '20.04', None, 'ubuntu/focal'),
+    ]
+
+    @pytest.mark.parametrize(
+        "matrix_index",
+        range(len(DISTRO_MATRIX_CONFIG) + len(DISTRO_MATRIX_NOVER)),
+    )
+    def test_get_distro_config(self, matrix_index):
+        (distro, version, _, expected) = (
+            self.DISTRO_MATRIX_CONFIG + self.DISTRO_MATRIX_NOVER
+        )[matrix_index]
+        config = dict(
+            os_type=distro,
+            os_version=version
+        )
+        gp = self.klass("ceph", config)
+        assert gp.distro == expected
+
+    def test_get_rpm_repo_content_uses_pulp_base_url(self):
+        pulp_base = (
+            'https://pulp.example.com/pulp/content/repos/ceph/tentacle/'
+            'abc123/rocky/10/flavors/default'
+        )
+        rem = self._get_remote()
+        with patch(
+            'teuthology.packaging.PulpProject.base_url',
+            new_callable=PropertyMock,
+            return_value=pulp_base,
+        ):
+            gp = self.klass('ceph', {}, ctx=dict(foo='bar'), remote=rem)
+            content = gp._get_rpm_repo_content()
+        assert f'baseurl={pulp_base}/$basearch' in content
+        assert f'baseurl={pulp_base}/noarch' in content
+        assert f'baseurl={pulp_base}/SRPMS' in content
+        assert 'chacra' not in content
+
+    @patch('teuthology.packaging.sudo_write_file')
+    def test_install_rpm_repo_writes_repo_file(self, m_sudo_write_file):
+        pulp_base = (
+            'https://pulp.example.com/pulp/content/repos/ceph/tentacle/'
+            'abc123/rocky/10/flavors/default'
+        )
+        rem = self._get_remote()
+        with patch(
+            'teuthology.packaging.PulpProject.base_url',
+            new_callable=PropertyMock,
+            return_value=pulp_base,
+        ):
+            gp = self.klass('ceph', {}, ctx=dict(foo='bar'), remote=rem)
+            gp._install_rpm_repo()
+        m_sudo_write_file.assert_called_once()
+        assert m_sudo_write_file.call_args[0][1] == '/etc/yum.repos.d/ceph.repo'
+        assert pulp_base in m_sudo_write_file.call_args[0][2]
+
+    def test_get_deb_repo_content_uses_pulp_distribution_url(self):
+        pulp_dist = (
+            'https://pulp.example.com/pulp/content/repos/ceph/tentacle/'
+            'ff309bba/ubuntu/jammy/flavors/default/x86_64/'
+        )
+        rem = self._get_remote(distro='ubuntu', version='22.04', codename='jammy')
+        with patch(
+            'teuthology.packaging.PulpProject.repo_url',
+            new_callable=PropertyMock,
+            return_value=pulp_dist,
+        ):
+            gp = self.klass('ceph', {}, ctx=dict(foo='bar'), remote=rem)
+            content = gp._get_deb_repo_content()
+        assert content == (
+            'deb [trusted=yes] '
+            'https://pulp.example.com/pulp/content/repos/ceph/tentacle/'
+            'ff309bba/ubuntu/jammy/flavors/default/x86_64/   default all\n'
+        )
+
+    @patch('teuthology.packaging.sudo_write_file')
+    def test_install_deb_repo_writes_sources_list(self, m_sudo_write_file):
+        pulp_dist = (
+            'https://pulp.example.com/pulp/content/repos/ceph/tentacle/'
+            'ff309bba/ubuntu/jammy/flavors/default/x86_64/'
+        )
+        rem = self._get_remote(distro='ubuntu', version='22.04', codename='jammy')
+        with patch(
+            'teuthology.packaging.PulpProject.repo_url',
+            new_callable=PropertyMock,
+            return_value=pulp_dist,
+        ):
+            gp = self.klass('ceph', {}, ctx=dict(foo='bar'), remote=rem)
+            gp._install_deb_repo()
+        m_sudo_write_file.assert_called_once()
+        assert m_sudo_write_file.call_args[0][1] == '/etc/apt/sources.list.d/ceph.list'
+        assert '[trusted=yes]' in m_sudo_write_file.call_args[0][2]
+        assert '/x86_64/   default all' in m_sudo_write_file.call_args[0][2]
diff --git a/tests/test_parallel.py b/tests/test_parallel.py
new file mode 100644 (file)
index 0000000..bba1d57
--- /dev/null
@@ -0,0 +1,28 @@
+from teuthology.parallel import parallel
+
+
+def identity(item, input_set=None, remove=False):
+    if input_set is not None:
+        assert item in input_set
+        if remove:
+            input_set.remove(item)
+    return item
+
+
+class TestParallel(object):
+    def test_basic(self):
+        in_set = set(range(10))
+        with parallel() as para:
+            for i in in_set:
+                para.spawn(identity, i, in_set, remove=True)
+                assert para.any_spawned is True
+            assert para.count == len(in_set)
+
+    def test_result(self):
+        in_set = set(range(10))
+        with parallel() as para:
+            for i in in_set:
+                para.spawn(identity, i, in_set)
+            for result in para:
+                in_set.remove(result)
+
diff --git a/tests/test_repo_utils.py b/tests/test_repo_utils.py
new file mode 100644 (file)
index 0000000..969f825
--- /dev/null
@@ -0,0 +1,250 @@
+import logging
+import unittest.mock as mock
+import os
+import os.path
+from pytest import raises, mark
+import re
+import shutil
+import subprocess
+import tempfile
+from packaging.version import parse
+
+from teuthology.exceptions import BranchNotFoundError, CommitNotFoundError
+from teuthology import repo_utils
+from teuthology import parallel
+repo_utils.log.setLevel(logging.WARNING)
+
+
+class TestRepoUtils(object):
+
+    @classmethod
+    def setup_class(cls):
+        cls.temp_path = tempfile.mkdtemp(prefix='test_repo-')
+        cls.dest_path = cls.temp_path + '/empty_dest'
+        cls.src_path = cls.temp_path + '/empty_src'
+
+        if 'TEST_ONLINE' in os.environ:
+            cls.repo_url = 'https://github.com/ceph/empty.git'
+            cls.commit = '71245d8e454a06a38a00bff09d8f19607c72e8bf'
+        else:
+            cls.repo_url = 'file://' + cls.src_path
+            cls.commit = None
+
+        cls.git_version = parse(cls.get_system_git_version())
+
+    @classmethod
+    def teardown_class(cls):
+        shutil.rmtree(cls.temp_path)
+
+    @classmethod
+    def get_system_git_version(cls):
+        # parsing following patterns
+        # 1) git version 2.45.2
+        # 2) git version 2.39.3 (Apple Git-146)
+        git_version = subprocess.check_output(('git', 'version')).decode()
+        m = re.match(r"git version (?P<ver>\d+.\d+.\d+) ?", git_version)
+        return m['ver']
+
+    def setup_method(self, method):
+        # In git 2.28.0, the --initial-branch flag was added.
+        if self.git_version >= parse("2.28.0"):
+            subprocess.check_call(
+                ('git', 'init', '--initial-branch', 'main', self.src_path)
+            )
+        else:
+            subprocess.check_call(('git', 'init', self.src_path))
+            subprocess.check_call(
+                ('git', 'checkout', '-b', 'main'),
+                cwd=self.src_path,
+            )
+        proc = subprocess.Popen(
+            ('git', 'config', 'user.email', 'test@ceph.com'),
+            cwd=self.src_path,
+            stdout=subprocess.PIPE,
+        )
+        assert proc.wait() == 0
+        proc = subprocess.Popen(
+            ('git', 'config', 'user.name', 'Test User'),
+            cwd=self.src_path,
+            stdout=subprocess.PIPE,
+        )
+        assert proc.wait() == 0
+        proc = subprocess.Popen(
+            ('git', 'commit', '--allow-empty', '--allow-empty-message',
+             '--no-edit'),
+            cwd=self.src_path,
+            stdout=subprocess.PIPE,
+        )
+        assert proc.wait() == 0
+        if not self.commit:
+            result = subprocess.check_output(
+                'git rev-parse HEAD',
+                shell=True,
+                cwd=self.src_path,
+            ).split()
+            assert result
+            self.commit = result[0].decode()
+
+    def teardown_method(self, method):
+        shutil.rmtree(self.src_path, ignore_errors=True)
+        shutil.rmtree(self.dest_path, ignore_errors=True)
+
+    def test_clone_repo_existing_branch(self):
+        repo_utils.clone_repo(self.repo_url, self.dest_path, 'main', self.commit)
+        assert os.path.exists(self.dest_path)
+
+    def test_clone_repo_non_existing_branch(self):
+        with raises(BranchNotFoundError):
+            repo_utils.clone_repo(self.repo_url, self.dest_path, 'nobranch', self.commit)
+        assert not os.path.exists(self.dest_path)
+
+    def test_fetch_no_repo(self):
+        fake_dest_path = self.temp_path + '/not_a_repo'
+        assert not os.path.exists(fake_dest_path)
+        with raises(OSError):
+            repo_utils.fetch(fake_dest_path)
+        assert not os.path.exists(fake_dest_path)
+
+    def test_fetch_noop(self):
+        repo_utils.clone_repo(self.repo_url, self.dest_path, 'main', self.commit)
+        repo_utils.fetch(self.dest_path)
+        assert os.path.exists(self.dest_path)
+
+    def test_fetch_branch_no_repo(self):
+        fake_dest_path = self.temp_path + '/not_a_repo'
+        assert not os.path.exists(fake_dest_path)
+        with raises(OSError):
+            repo_utils.fetch_branch(fake_dest_path, 'main')
+        assert not os.path.exists(fake_dest_path)
+
+    def test_fetch_branch_fake_branch(self):
+        repo_utils.clone_repo(self.repo_url, self.dest_path, 'main', self.commit)
+        with raises(BranchNotFoundError):
+            repo_utils.fetch_branch(self.dest_path, 'nobranch')
+
+    @mark.parametrize('git_str',
+                      ["fatal: couldn't find remote ref",
+                       "fatal: Couldn't find remote ref"])
+    @mock.patch('subprocess.Popen')
+    def test_fetch_branch_different_git_versions(self, mock_popen, git_str):
+        """
+        Newer git versions return a lower case string
+        See: https://github.com/git/git/commit/0b9c3afdbfb629363
+        """
+        branch_name = 'nobranch'
+        process_mock = mock.Mock()
+        attrs = {
+            'wait.return_value': 1,
+            'stdout.read.return_value': f"{git_str} {branch_name}".encode(),
+        }
+        process_mock.configure_mock(**attrs)
+        mock_popen.return_value = process_mock
+        with raises(BranchNotFoundError):
+            repo_utils.fetch_branch('', branch_name)
+
+    def test_enforce_existing_branch(self):
+        repo_utils.enforce_repo_state(self.repo_url, self.dest_path,
+                                      'main')
+        assert os.path.exists(self.dest_path)
+
+    def test_enforce_existing_commit(self):
+        import logging
+        logging.getLogger().info(subprocess.check_output("git branch", shell=True, cwd=self.src_path))
+        repo_utils.enforce_repo_state(self.repo_url, self.dest_path,
+                                      'main', self.commit)
+        assert os.path.exists(self.dest_path)
+
+    def test_enforce_non_existing_branch(self):
+        with raises(BranchNotFoundError):
+            repo_utils.enforce_repo_state(self.repo_url, self.dest_path,
+                                          'blah', self.commit)
+        assert not os.path.exists(self.dest_path)
+
+    def test_enforce_non_existing_commit(self):
+        with raises(CommitNotFoundError):
+            repo_utils.enforce_repo_state(self.repo_url, self.dest_path,
+                                          'main', 'c69e90807d222c1719c45c8c758bf6fac3d985f1')
+        assert not os.path.exists(self.dest_path)
+
+    def test_enforce_multiple_calls_same_branch(self):
+        repo_utils.enforce_repo_state(self.repo_url, self.dest_path,
+                                      'main', self.commit)
+        assert os.path.exists(self.dest_path)
+        repo_utils.enforce_repo_state(self.repo_url, self.dest_path,
+                                      'main', self.commit)
+        assert os.path.exists(self.dest_path)
+        repo_utils.enforce_repo_state(self.repo_url, self.dest_path,
+                                      'main', self.commit)
+        assert os.path.exists(self.dest_path)
+
+    def test_enforce_multiple_calls_different_branches(self):
+        with raises(BranchNotFoundError):
+            repo_utils.enforce_repo_state(self.repo_url, self.dest_path,
+                                          'blah1')
+        assert not os.path.exists(self.dest_path)
+        repo_utils.enforce_repo_state(self.repo_url, self.dest_path,
+                                      'main', self.commit)
+        assert os.path.exists(self.dest_path)
+        repo_utils.enforce_repo_state(self.repo_url, self.dest_path,
+                                      'main', self.commit)
+        assert os.path.exists(self.dest_path)
+        with raises(BranchNotFoundError):
+            repo_utils.enforce_repo_state(self.repo_url, self.dest_path,
+                                          'blah2')
+        assert not os.path.exists(self.dest_path)
+        repo_utils.enforce_repo_state(self.repo_url, self.dest_path,
+                                      'main', self.commit)
+        assert os.path.exists(self.dest_path)
+
+    def test_enforce_invalid_branch(self):
+        with raises(ValueError):
+            repo_utils.enforce_repo_state(self.repo_url, self.dest_path, 'a b', self.commit)
+
+    def test_simultaneous_access(self):
+        count = 5
+        with parallel.parallel() as p:
+            for i in range(count):
+                p.spawn(repo_utils.enforce_repo_state, self.repo_url,
+                        self.dest_path, 'main', self.commit)
+            for result in p:
+                assert result is None
+
+    def test_simultaneous_access_different_branches(self):
+        branches = [('main', self.commit),  ('main', self.commit), ('nobranch', 'nocommit'),
+                    ('nobranch', 'nocommit'), ('main', self.commit), ('nobranch', 'nocommit')]
+
+        with parallel.parallel() as p:
+            for branch, commit in branches:
+                if branch == 'main':
+                    p.spawn(repo_utils.enforce_repo_state, self.repo_url,
+                            self.dest_path, branch, commit)
+                else:
+                    dest_path = self.dest_path + '_' + branch
+
+                    def func():
+                        repo_utils.enforce_repo_state(
+                            self.repo_url, dest_path,
+                            branch, commit)
+                    p.spawn(
+                        raises,
+                        BranchNotFoundError,
+                        func,
+                    )
+            for result in p:
+                pass
+
+    URLS_AND_DIRNAMES = [
+        ('git@git.ceph.com/ceph-qa-suite.git', 'git.ceph.com_ceph-qa-suite'),
+        ('git://git.ceph.com/ceph-qa-suite.git', 'git.ceph.com_ceph-qa-suite'),
+        ('https://github.com/ceph/ceph', 'github.com_ceph_ceph'),
+        ('https://github.com/liewegas/ceph.git', 'github.com_liewegas_ceph'),
+        ('file:///my/dir/has/ceph.git', 'my_dir_has_ceph'),
+    ]
+
+    @mark.parametrize("input_, expected", URLS_AND_DIRNAMES)
+    def test_url_to_dirname(self, input_, expected):
+        assert repo_utils.url_to_dirname(input_) == expected
+
+    def test_current_branch(self):
+        repo_utils.clone_repo(self.repo_url, self.dest_path, 'main', self.commit)
+        assert repo_utils.current_branch(self.dest_path) == "main"
diff --git a/tests/test_report.py b/tests/test_report.py
new file mode 100644 (file)
index 0000000..bd40754
--- /dev/null
@@ -0,0 +1,86 @@
+import json
+import pytest
+import yaml
+
+from tests import fake_archive
+from teuthology import report
+
+
+@pytest.fixture
+def archive(tmp_path):
+    archive = fake_archive.FakeArchive(archive_base=str(tmp_path))
+    yield archive
+    archive.teardown()
+
+
+@pytest.fixture(autouse=True)
+def reporter(archive):
+    archive.setup()
+    return report.ResultsReporter(archive_base=archive.archive_base)
+
+
+def test_all_runs_one_run(archive, reporter):
+    run_name = "test_all_runs"
+    yaml_path = "examples/3node_ceph.yaml"
+    job_count = 3
+    archive.create_fake_run(run_name, job_count, yaml_path)
+    assert [run_name] == reporter.serializer.all_runs
+
+
+def test_all_runs_three_runs(archive, reporter):
+    run_count = 3
+    runs = {}
+    for i in range(run_count):
+        run_name = "run #%s" % i
+        yaml_path = "examples/3node_ceph.yaml"
+        job_count = 3
+        job_ids = archive.create_fake_run(
+            run_name,
+            job_count,
+            yaml_path)
+        runs[run_name] = job_ids
+    assert sorted(runs.keys()) == sorted(reporter.serializer.all_runs)
+
+
+def test_jobs_for_run(archive, reporter):
+    run_name = "test_jobs_for_run"
+    yaml_path = "examples/3node_ceph.yaml"
+    job_count = 3
+    jobs = archive.create_fake_run(run_name, job_count, yaml_path)
+    job_ids = [str(job['job_id']) for job in jobs]
+
+    got_jobs = reporter.serializer.jobs_for_run(run_name)
+    assert sorted(job_ids) == sorted(got_jobs.keys())
+
+
+def test_running_jobs_for_run(archive, reporter):
+    run_name = "test_jobs_for_run"
+    yaml_path = "examples/3node_ceph.yaml"
+    job_count = 10
+    num_hung = 3
+    archive.create_fake_run(run_name, job_count, yaml_path,
+                                 num_hung=num_hung)
+
+    got_jobs = reporter.serializer.running_jobs_for_run(run_name)
+    assert len(got_jobs) == num_hung
+
+
+def test_json_for_job(archive, reporter):
+    run_name = "test_json_for_job"
+    yaml_path = "examples/3node_ceph.yaml"
+    job_count = 1
+    jobs = archive.create_fake_run(run_name, job_count, yaml_path)
+    job = jobs[0]
+
+    with open(yaml_path) as yaml_file:
+        obj_from_yaml = yaml.safe_load(yaml_file)
+    full_obj = obj_from_yaml.copy()
+    full_obj.update(job['info'])
+    full_obj.update(job['summary'])
+
+    out_json = reporter.serializer.json_for_job(
+        run_name, str(job['job_id']))
+    out_obj = json.loads(out_json)
+    assert full_obj == out_obj
+
+
diff --git a/tests/test_results.py b/tests/test_results.py
new file mode 100644 (file)
index 0000000..3d7ba93
--- /dev/null
@@ -0,0 +1,155 @@
+import textwrap
+from teuthology.config import config
+from teuthology import results
+from teuthology import report
+
+from unittest.mock import patch, DEFAULT
+
+
+class TestResultsEmail(object):
+    reference = {
+        'run_name': 'test_name',
+        'jobs': [
+            # Running
+            {'description': 'description for job with name test_name',
+             'job_id': 30481,
+             'name': 'test_name',
+             'log_href': 'http://qa-proxy.ceph.com/teuthology/test_name/30481/teuthology.log',  # noqa
+             'owner': 'job@owner',
+             'duration': None,
+             'status': 'running',
+             },
+            # Waiting
+            {'description': 'description for job with name test_name',
+             'job_id': 62965,
+             'name': 'test_name',
+             'log_href': 'http://qa-proxy.ceph.com/teuthology/test_name/30481/teuthology.log',  # noqa
+             'owner': 'job@owner',
+             'duration': None,
+             'status': 'waiting',
+             },
+            # Queued
+            {'description': 'description for job with name test_name',
+             'job_id': 79063,
+             'name': 'test_name',
+             'log_href': 'http://qa-proxy.ceph.com/teuthology/test_name/30481/teuthology.log',  # noqa
+             'owner': 'job@owner',
+             'duration': None,
+             'status': 'queued',
+             },
+            # Failed
+            {'description': 'description for job with name test_name',
+             'job_id': 88979,
+             'name': 'test_name',
+             'log_href': 'http://qa-proxy.ceph.com/teuthology/test_name/88979/teuthology.log',  # noqa
+             'owner': 'job@owner',
+             'duration': 35190,
+             'success': False,
+             'status': 'fail',
+             'failure_reason': 'Failure reason!',
+             },
+            # Dead
+            {'description': 'description for job with name test_name',
+             'job_id': 69152,
+             'name': 'test_name',
+             'log_href': 'http://qa-proxy.ceph.com/teuthology/test_name/69152/teuthology.log',  # noqa
+             'owner': 'job@owner',
+             'duration': 5225,
+             'success': False,
+             'status': 'dead',
+             'failure_reason': 'Dead reason!',
+             },
+            # Passed
+            {'description': 'description for job with name test_name',
+             'job_id': 68369,
+             'name': 'test_name',
+             'log_href': 'http://qa-proxy.ceph.com/teuthology/test_name/68369/teuthology.log',  # noqa
+             'owner': 'job@owner',
+             'duration': 33771,
+             'success': True,
+             'status': 'pass',
+             },
+        ],
+        'subject': '1 fail, 1 dead, 1 running, 1 waiting, 1 queued, 1 pass in test_name',  # noqa
+        'body': textwrap.dedent("""
+    Test Run: test_name
+    =================================================================
+    info:    http://example.com/test_name/
+    logs:    http://qa-proxy.ceph.com/teuthology/test_name/
+    failed:  1
+    dead:    1
+    running: 1
+    waiting: 1
+    queued:  1
+    passed:  1
+
+
+    Fail
+    =================================================================
+    [88979]  description for job with name test_name
+    -----------------------------------------------------------------
+    time:   09:46:30
+    info:   http://example.com/test_name/88979/
+    log:    http://qa-proxy.ceph.com/teuthology/test_name/88979/
+
+        Failure reason!
+
+
+
+    Dead
+    =================================================================
+    [69152]  description for job with name test_name
+    -----------------------------------------------------------------
+    time:   01:27:05
+    info:   http://example.com/test_name/69152/
+    log:    http://qa-proxy.ceph.com/teuthology/test_name/69152/
+
+        Dead reason!
+
+
+
+    Running
+    =================================================================
+    [30481] description for job with name test_name
+    info:   http://example.com/test_name/30481/
+
+
+
+    Waiting
+    =================================================================
+    [62965] description for job with name test_name
+    info:   http://example.com/test_name/62965/
+
+
+
+    Queued
+    =================================================================
+    [79063] description for job with name test_name
+    info:   http://example.com/test_name/79063/
+
+
+
+    Pass
+    =================================================================
+    [68369] description for job with name test_name
+    time:   09:22:51
+    info:   http://example.com/test_name/68369/
+    """).strip(),
+    }
+
+    def setup_method(self):
+        config.results_ui_server = "http://example.com/"
+        config.archive_server = "http://qa-proxy.ceph.com/teuthology/"
+
+    def test_build_email_body(self):
+        run_name = self.reference['run_name']
+        with patch.multiple(
+                report,
+                ResultsReporter=DEFAULT,
+                ):
+            reporter = report.ResultsReporter()
+            reporter.get_jobs.return_value = self.reference['jobs']
+            (subject, body) = results.build_email_body(
+                run_name, _reporter=reporter)
+        assert subject == self.reference['subject']
+        assert body == self.reference['body']
diff --git a/tests/test_run.py b/tests/test_run.py
new file mode 100644 (file)
index 0000000..7ca40d2
--- /dev/null
@@ -0,0 +1,287 @@
+import os
+import pytest
+import docopt
+
+from unittest.mock import patch, call
+
+from teuthology import run
+from scripts import run as scripts_run
+from tests import skipif_teuthology_process
+
+
+class TestRun(object):
+    """ Tests for teuthology.run """
+
+    @patch("teuthology.log.setLevel")
+    @patch("teuthology.setup_log_file")
+    @patch("os.mkdir")
+    def test_set_up_logging(self, m_mkdir, m_setup_log_file, m_setLevel):
+        run.set_up_logging(True, "path/to/archive")
+        m_mkdir.assert_called_with("path/to/archive")
+        m_setup_log_file.assert_called_with("path/to/archive/teuthology.log")
+        assert m_setLevel.called
+
+    # because of how we import things, mock merge_configs from run - where it's used
+    # see: http://www.voidspace.org.uk/python/mock/patch.html#where-to-patch
+    @patch("teuthology.run.merge_configs")
+    def test_setup_config(self, m_merge_configs):
+        config = {"job_id": 1, "foo": "bar"}
+        m_merge_configs.return_value = config
+        result = run.setup_config(["some/config.yaml"])
+        assert m_merge_configs.called
+        assert result["job_id"] == "1"
+        assert result["foo"] == "bar"
+
+    @patch("teuthology.run.merge_configs")
+    def test_setup_config_targets_ok(self, m_merge_configs):
+        config = {"targets": list(range(4)), "roles": list(range(2))}
+        m_merge_configs.return_value = config
+        result = run.setup_config(["some/config.yaml"])
+        assert result["targets"] == [0, 1, 2, 3]
+        assert result["roles"] == [0, 1]
+
+    @patch("teuthology.run.merge_configs")
+    def test_setup_config_targets_invalid(self, m_merge_configs):
+        config = {"targets": range(2), "roles": range(4)}
+        m_merge_configs.return_value = config
+        with pytest.raises(AssertionError):
+            run.setup_config(["some/config.yaml"])
+
+    @patch("teuthology.run.open")
+    def test_write_initial_metadata(self, m_open):
+        config = {"job_id": "123", "foo": "bar"}
+        run.write_initial_metadata(
+            "some/archive/dir",
+            config,
+            "the_name",
+            "the_description",
+            "the_owner",
+        )
+        expected = [
+            call('some/archive/dir/pid', 'w'),
+            call('some/archive/dir/owner', 'w'),
+            call('some/archive/dir/orig.config.yaml', 'w'),
+            call('some/archive/dir/info.yaml', 'w')
+        ]
+        assert m_open.call_args_list == expected
+
+    def test_get_machine_type(self):
+        result = run.get_machine_type(None, {"machine-type": "the_machine_type"})
+        assert result == "the_machine_type"
+
+    def test_get_summary(self):
+        result = run.get_summary("the_owner", "the_description")
+        assert result == {"owner": "the_owner", "description": "the_description", "success": True}
+        result = run.get_summary("the_owner", None)
+        assert result == {"owner": "the_owner", "success": True}
+
+    def test_validate_tasks_invalid(self):
+        config = {"tasks": [{"kernel": "can't be here"}]}
+        with pytest.raises(AssertionError) as excinfo:
+            run.validate_tasks(config)
+        assert excinfo.value.args[0].startswith("kernel installation")
+
+    def test_validate_task_no_tasks(self):
+        result = run.validate_tasks({})
+        assert result == []
+
+    def test_validate_tasks_valid(self):
+        expected = [{"foo": "bar"}, {"bar": "foo"}]
+        result = run.validate_tasks({"tasks": expected})
+        assert result == expected
+
+    def test_validate_tasks_is_list(self):
+        with pytest.raises(AssertionError) as excinfo:
+            run.validate_tasks({"tasks": {"foo": "bar"}})
+        assert excinfo.value.args[0].startswith("Expected list")
+
+    def test_get_initial_tasks_invalid(self):
+        with pytest.raises(AssertionError) as excinfo:
+            run.get_initial_tasks(True, {"targets": "can't be here",
+                                         "roles": "roles" }, "machine_type")
+        assert excinfo.value.args[0].startswith("You cannot")
+
+    def test_get_inital_tasks(self):
+        config = {"roles": range(2), "kernel": "the_kernel", "use_existing_cluster": False}
+        result = run.get_initial_tasks(True, config, "machine_type")
+        assert {"internal.lock_machines": (2, "machine_type")} in result
+        assert {"kernel": "the_kernel"} in result
+        # added because use_existing_cluster == False
+        assert {'internal.vm_setup': None} in result
+        assert {'internal.buildpackages_prep': None} in result
+
+    # When tests are run in a teuthology process using the py.test 
+    # API, tasks will have already been imported. Patching sys.path 
+    # (and even calling sys.path_importer_cache.clear()) doesn't seem 
+    # to help "forget" where the tasks are, keeping this test from 
+    # passing. The test isn't critical to run in every single 
+    # environment, so skip.
+    @skipif_teuthology_process
+    @patch("teuthology.run.fetch_qa_suite")
+    def test_fetch_tasks_if_needed(self, m_fetch_qa_suite):
+        config = {"suite_path": "/some/suite/path", "suite_branch": "feature_branch",
+                  "suite_sha1": "commit"}
+        m_fetch_qa_suite.return_value = "/some/other/suite/path"
+        result = run.fetch_tasks_if_needed(config)
+        m_fetch_qa_suite.assert_called_with("feature_branch", commit="commit")
+        assert result == "/some/other/suite/path/qa"
+
+    @patch("teuthology.run.get_status")
+    @patch("yaml.safe_dump")
+    @patch("teuthology.report.try_push_job_info")
+    @patch("teuthology.run.email_results")
+    @patch("teuthology.run.open")
+    @patch("sys.exit")
+    def test_report_outcome(self, m_sys_exit, m_open, m_email_results, m_try_push_job_info, m_safe_dump, m_get_status):
+        m_get_status.return_value = "fail"
+        summary = {"failure_reason": "reasons"}
+        summary_dump = "failure_reason: reasons\n"
+        config = {"email-on-error": True}
+        config_dump = "email-on-error: true\n"
+        m_safe_dump.side_effect = [None, summary_dump, config_dump]
+        run.report_outcome(config, "the/archive/path", summary)
+        m_try_push_job_info.assert_called_with(config, summary)
+        m_open.assert_called_with("the/archive/path/summary.yaml", "w")
+        assert m_email_results.called
+        assert m_open.called
+        assert m_sys_exit.called
+
+    @patch("teuthology.run.set_up_logging")
+    @patch("teuthology.run.setup_config")
+    @patch("teuthology.run.get_user")
+    @patch("teuthology.run.write_initial_metadata")
+    @patch("teuthology.report.try_push_job_info")
+    @patch("teuthology.run.get_machine_type")
+    @patch("teuthology.run.get_summary")
+    @patch("yaml.safe_dump")
+    @patch("teuthology.run.validate_tasks")
+    @patch("teuthology.run.get_initial_tasks")
+    @patch("teuthology.run.fetch_tasks_if_needed")
+    @patch("teuthology.run.run_tasks")
+    @patch("teuthology.run.report_outcome")
+    def test_main(self, m_report_outcome, m_run_tasks, m_fetch_tasks_if_needed, m_get_initial_tasks, m_validate_tasks,
+                  m_safe_dump, m_get_summary, m_get_machine_type, m_try_push_job_info, m_write_initial_metadata,
+                  m_get_user, m_setup_config, m_set_up_logging):
+        """ This really should be an integration test of some sort. """
+        config = {"job_id": 1}
+        m_setup_config.return_value = config
+        m_get_machine_type.return_value = "machine_type"
+        doc = scripts_run.__doc__
+        args = docopt.docopt(doc, [
+            "--verbose",
+            "--archive", "some/archive/dir",
+            "--description", "the_description",
+            "--lock",
+            "--os-type", "os_type",
+            "--os-version", "os_version",
+            "--block",
+            "--name", "the_name",
+            "--suite-path", "some/suite/dir",
+            "path/to/config.yml",
+        ])
+        m_get_user.return_value = "the_owner"
+        m_get_summary.return_value = dict(success=True, owner="the_owner", description="the_description")
+        m_validate_tasks.return_value = ['task3']
+        m_get_initial_tasks.return_value = ['task1', 'task2']
+        m_fetch_tasks_if_needed.return_value = "some/suite/dir"
+        run.main(args)
+        m_set_up_logging.assert_called_with(True, "some/archive/dir")
+        m_setup_config.assert_called_with(["path/to/config.yml"])
+        m_write_initial_metadata.assert_called_with(
+            "some/archive/dir",
+            config,
+            "the_name",
+            "the_description",
+            "the_owner"
+        )
+        m_try_push_job_info.assert_called_with(config, dict(status='running', pid=os.getpid()))
+        m_get_machine_type.assert_called_with(None, config)
+        m_get_summary.assert_called_with("the_owner", "the_description")
+        m_get_initial_tasks.assert_called_with(True, config, "machine_type")
+        m_fetch_tasks_if_needed.assert_called_with(config)
+        assert m_report_outcome.called
+        args, kwargs = m_run_tasks.call_args
+        fake_ctx = kwargs["ctx"]._conf
+        # fields that must be in ctx for the tasks to behave
+        expected_ctx = ["verbose", "archive", "description", "owner", "lock", "machine_type", "os_type", "os_version",
+                        "block", "name", "suite_path", "config", "summary"]
+        for key in expected_ctx:
+            assert key in fake_ctx
+        assert isinstance(fake_ctx["config"], dict)
+        assert isinstance(fake_ctx["summary"], dict)
+        assert "tasks" in fake_ctx["config"]
+        # ensures that values missing in args are added with the correct value
+        assert fake_ctx["owner"] == "the_owner"
+        assert fake_ctx["machine_type"] == "machine_type"
+        # ensures os_type and os_version are property overwritten
+        assert fake_ctx["config"]["os_type"] == "os_type"
+        assert fake_ctx["config"]["os_version"] == "os_version"
+
+    @patch("teuthology.run.set_up_logging")
+    @patch("teuthology.run.setup_config")
+    @patch("teuthology.run.get_user")
+    @patch("teuthology.run.write_initial_metadata")
+    @patch("teuthology.report.try_push_job_info")
+    @patch("teuthology.run.get_machine_type")
+    @patch("teuthology.run.get_summary")
+    @patch("yaml.safe_dump")
+    @patch("teuthology.run.validate_tasks")
+    @patch("teuthology.run.get_initial_tasks")
+    @patch("teuthology.run.fetch_tasks_if_needed")
+    @patch("teuthology.run.run_tasks")
+    @patch("teuthology.run.report_outcome")
+    def test_main_interactive(
+        self,
+        m_report_outcome,
+        m_run_tasks,
+        m_fetch_tasks_if_needed,
+        m_get_initial_tasks,
+        m_validate_tasks,
+        m_safe_dump,
+        m_get_summary,
+        m_get_machine_type,
+        m_try_push_job_info,
+        m_write_initial_metadata,
+        m_get_user,
+        m_setup_config,
+        m_set_up_logging,
+    ):
+        config = {"job_id": 1}
+        m_setup_config.return_value = config
+        m_get_machine_type.return_value = "machine_type"
+        doc = scripts_run.__doc__
+        args = docopt.docopt(doc, [
+            "--interactive-on-error",
+            "path/to/config.yml",
+        ])
+        run.main(args)
+        args, kwargs = m_run_tasks.call_args
+        fake_ctx = kwargs["ctx"]._conf
+        assert fake_ctx['interactive_on_error'] is True
+
+    def test_get_teuthology_command(self):
+        doc = scripts_run.__doc__
+        args = docopt.docopt(doc, [
+            "--archive", "some/archive/dir",
+            "--description", "the_description",
+            "--lock",
+            "--block",
+            "--name", "the_name",
+            "--suite-path", "some/suite/dir",
+            "path/to/config.yml", "path/to/config2.yaml",
+        ])
+        result = run.get_teuthology_command(args)
+        result = result.split()
+        expected = [
+            "teuthology",
+            "path/to/config.yml", "path/to/config2.yaml",
+            "--suite-path", "some/suite/dir",
+            "--lock",
+            "--description", "the_description",
+            "--name", "the_name",
+            "--block",
+            "--archive", "some/archive/dir",
+        ]
+        assert len(result) == len(expected)
+        for arg in expected:
+            assert arg in result
diff --git a/tests/test_safepath.py b/tests/test_safepath.py
new file mode 100644 (file)
index 0000000..afc81cd
--- /dev/null
@@ -0,0 +1,55 @@
+from teuthology import safepath
+
+class TestSafepath(object):
+    def test_simple(self):
+        got = safepath.munge('foo')
+        assert got == 'foo'
+
+    def test_empty(self):
+        # really odd corner case
+        got = safepath.munge('')
+        assert got == '_'
+
+    def test_slash(self):
+        got = safepath.munge('/')
+        assert got == '_'
+
+    def test_slashslash(self):
+        got = safepath.munge('//')
+        assert got == '_'
+
+    def test_absolute(self):
+        got = safepath.munge('/evil')
+        assert got == 'evil'
+
+    def test_absolute_subdir(self):
+        got = safepath.munge('/evil/here')
+        assert got == 'evil/here'
+
+    def test_dot_leading(self):
+        got = safepath.munge('./foo')
+        assert got == 'foo'
+
+    def test_dot_middle(self):
+        got = safepath.munge('evil/./foo')
+        assert got == 'evil/foo'
+
+    def test_dot_trailing(self):
+        got = safepath.munge('evil/foo/.')
+        assert got == 'evil/foo'
+
+    def test_dotdot(self):
+        got = safepath.munge('../evil/foo')
+        assert got == '_./evil/foo'
+
+    def test_dotdot_subdir(self):
+        got = safepath.munge('evil/../foo')
+        assert got == 'evil/_./foo'
+
+    def test_hidden(self):
+        got = safepath.munge('.evil')
+        assert got == '_evil'
+
+    def test_hidden_subdir(self):
+        got = safepath.munge('foo/.evil')
+        assert got == 'foo/_evil'
diff --git a/tests/test_schedule.py b/tests/test_schedule.py
new file mode 100644 (file)
index 0000000..dd0a68f
--- /dev/null
@@ -0,0 +1,45 @@
+from teuthology.schedule import build_config
+from teuthology.misc import get_user
+
+
+class TestSchedule(object):
+    basic_args = {
+        '--verbose': False,
+        '--owner': 'OWNER',
+        '--description': 'DESC',
+        '--email': 'EMAIL',
+        '--first-in-suite': False,
+        '--last-in-suite': True,
+        '--name': 'NAME',
+        '--worker': 'tala',
+        '--timeout': '6',
+        '--priority': '99',
+        # TODO: make this work regardless of $PWD
+        #'<conf_file>': ['../../examples/3node_ceph.yaml',
+        #                '../../examples/3node_rgw.yaml'],
+        }
+
+    def test_basic(self):
+        expected = {
+            'description': 'DESC',
+            'email': 'EMAIL',
+            'first_in_suite': False,
+            'last_in_suite': True,
+            'machine_type': 'tala',
+            'name': 'NAME',
+            'owner': 'OWNER',
+            'priority': 99,
+            'results_timeout': '6',
+            'verbose': False,
+            'tube': 'tala',
+        }
+
+        job_dict = build_config(self.basic_args)
+        assert job_dict == expected
+
+    def test_owner(self):
+        args = self.basic_args
+        args['--owner'] = None
+        job_dict = build_config(self.basic_args)
+        assert job_dict['owner'] == 'scheduled_%s' % get_user()
+
diff --git a/tests/test_scrape.py b/tests/test_scrape.py
new file mode 100644 (file)
index 0000000..ed281b4
--- /dev/null
@@ -0,0 +1,205 @@
+from __future__ import with_statement
+
+import glob
+import gzip
+import os
+import shutil
+import tempfile
+import yaml
+from teuthology import scrape
+
+class FakeResultDir(object):
+    """Mocks a Result Directory"""
+
+    def __init__(self,
+                failure_reason="Dummy reason",
+                assertion="FAILED assert 1 == 2\n",
+                blank_backtrace=False,
+                assertion_osd=False,
+    ):
+        self.failure_reason = failure_reason
+        self.assertion = assertion
+        self.blank_backtrace = blank_backtrace
+        self.path = tempfile.mkdtemp()
+        
+        with open(os.path.join(self.path, "config.yaml"), "w") as f:
+            yaml.dump({"description": "Dummy test"}, f)
+        
+        with open(os.path.join(self.path, "summary.yaml"), "w") as f:
+            yaml.dump({
+                "success": "false",
+                "failure_reason": self.failure_reason
+            }, f)
+
+        with open(os.path.join(self.path, "teuthology.log"), "w") as f:
+            if not self.blank_backtrace:
+                f.write(" ceph version 1000\n")
+                f.write(".stderr: Dummy error\n")
+                f.write(self.assertion)
+            f.write(" NOTE: a copy of the executable dummy text\n")
+
+        if assertion_osd:
+            host = "host1"
+            rem_log_dir = os.path.join(self.path, "remote", host, "log")
+            os.makedirs(rem_log_dir, exist_ok=True)
+            ceph_mon_log = os.path.join(rem_log_dir, "ceph-osd.0.log")
+            with open(ceph_mon_log, "w") as f:
+                f.write("ceph version 1000\n")
+                f.write(self.assertion)
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, exc_typ, exc_val, exc_tb):
+        shutil.rmtree(self.path)
+
+class TestScrape(object):
+    """Tests for teuthology.scrape"""
+
+    def test_grep(self):
+        with FakeResultDir() as d:
+            filepath = os.path.join(d.path, "scrapetest.txt")
+            with open(filepath, 'w') as f:
+                f.write("Ceph is an open-source software storage platform\n\
+                        Teuthology is used for testing.")
+
+            #System level grep is called
+            value1 = scrape.grep(filepath, "software")
+            value2 = scrape.grep(filepath, "device")
+
+            assert value1 ==\
+                ['Ceph is an open-source software storage platform', '']
+            assert value2 == []
+
+    def test_job(self):
+        with FakeResultDir() as d:
+            job = scrape.Job(d.path, 1)
+            assert job.get_success() == "false"
+            assert job.get_assertion() == "FAILED assert 1 == 2"
+            assert job.get_last_tlog_line() ==\
+                b"NOTE: a copy of the executable dummy text"
+            assert job.get_failure_reason() == "Dummy reason"
+
+    def test_timeoutreason(self):
+        with FakeResultDir(failure_reason=\
+            "status 124:  timeout '123 /home/ubuntu/cephtest/workunit.client.0/cephtool/test.sh'") as d:
+            job = scrape.Job(d.path, 1)
+            assert scrape.TimeoutReason.could_be(job)
+            assert scrape.TimeoutReason(job).match(job)
+
+    def test_deadreason(self):
+        with FakeResultDir() as d:
+            job = scrape.Job(d.path, 1)
+            #Summary is present
+            #So this cannot be a DeadReason
+            assert not scrape.DeadReason.could_be(job)
+
+    def test_lockdepreason(self):
+        lkReason = None
+        with FakeResultDir(assertion=\
+            "FAILED assert common/lockdep reason\n") as d:
+            job = scrape.Job(d.path, 1)
+            assert scrape.LockdepReason.could_be(job)
+
+            lkReason = scrape.LockdepReason(job)
+            #Backtraces of same jobs must match 100%
+            assert lkReason.match(job)
+        with FakeResultDir(blank_backtrace=True) as d:
+            #Corresponding to 0% match
+            assert not lkReason.match(scrape.Job(d.path, 2))
+
+    def test_assertionreason(self):
+        with FakeResultDir() as d:
+            job = scrape.Job(d.path, 1)
+            assert scrape.AssertionReason.could_be(job)
+
+    def test_genericreason(self):
+        d1 = FakeResultDir(blank_backtrace=True)
+        d2 = FakeResultDir(failure_reason="Dummy dummy")
+        d3 = FakeResultDir()
+
+        job1 = scrape.Job(d1.path, 1)
+        job2 = scrape.Job(d2.path, 2)
+        job3 = scrape.Job(d3.path, 3)
+
+        reason = scrape.GenericReason(job3)
+
+        assert reason.match(job2)
+        assert not reason.match(job1)
+
+        shutil.rmtree(d1.path)
+        shutil.rmtree(d2.path)
+        shutil.rmtree(d3.path)
+
+    def test_valgrindreason(self):
+        vreason = None
+        with FakeResultDir(
+            failure_reason="saw valgrind issues",
+            assertion="2014-08-22T20:07:18.668 ERROR:tasks.ceph:saw valgrind issue   <kind>Leak_DefinitelyLost</kind> in /var/log/ceph/valgrind/osd.3.log.gz\n"
+        ) as d:
+            job = scrape.Job(d.path, 1)
+            assert scrape.ValgrindReason.could_be(job)
+            
+            vreason = scrape.ValgrindReason(job)
+            assert vreason.match(job)
+
+    def test_give_me_a_reason(self):
+        with FakeResultDir() as d:
+            job = scrape.Job(d.path, 1)
+            
+            assert type(scrape.give_me_a_reason(job)) == scrape.AssertionReason
+
+        #Test the lockdep ordering
+        with FakeResultDir(assertion=\
+        "FAILED assert common/lockdep reason\n") as d:
+            job = scrape.Job(d.path, 1)
+            assert type(scrape.give_me_a_reason(job)) == scrape.LockdepReason
+
+    def test_scraper(self):
+        d = FakeResultDir()
+        os.mkdir(os.path.join(d.path, "test"))
+        shutil.move(
+            os.path.join(d.path, "config.yaml"),
+            os.path.join(d.path, "test", "config.yaml")
+        )
+        shutil.move(
+            os.path.join(d.path, "summary.yaml"),
+            os.path.join(d.path, "test", "summary.yaml")
+        )
+        shutil.move(
+            os.path.join(d.path, "teuthology.log"),
+            os.path.join(d.path, "test", "teuthology.log")
+        )
+
+        scrape.Scraper(d.path).analyze()
+
+        #scrape.log should be created
+        assert os.path.exists(os.path.join(d.path, "scrape.log"))
+
+        shutil.rmtree(d.path)
+
+    def test_gzip_backtrace_decode(self):
+        with FakeResultDir(assertion="FAILED assert dummy backtrace line",
+                        blank_backtrace=True,
+                        assertion_osd=True) as d:
+
+            with open(os.path.join(d.path, "teuthology.log"), "a") as root_log:
+                root_log.write(
+                    "command crashed with signal SIGSEGV tasks.ceph.osd.0.host1.stderr\n"
+                )
+
+            pattern = os.path.join(d.path, "**", "ceph-osd.0.log")
+            raws = glob.glob(pattern, recursive=True)
+            assert len(raws) == 1, f"expected one raw log, found: {raws}"
+            raw_log = raws[0]
+            gz_log = raw_log + ".gz"
+
+            with gzip.open(gz_log, "wb") as out:
+                out.write(open(raw_log, "rb").read())
+            os.remove(raw_log)
+
+            assert not os.path.exists(raw_log)
+            assert os.path.exists(gz_log)
+
+            job = scrape.Job(d.path, 1)
+            assert job.get_assertion() == "FAILED assert dummy backtrace line"
\ No newline at end of file
diff --git a/tests/test_timer.py b/tests/test_timer.py
new file mode 100644 (file)
index 0000000..312a9c8
--- /dev/null
@@ -0,0 +1,80 @@
+from teuthology import timer
+
+from unittest.mock import MagicMock, patch, mock_open
+from time import time
+
+
+class TestTimer(object):
+    def test_data_empty(self):
+        self.timer = timer.Timer()
+        assert self.timer.data == dict()
+
+    def test_data_one_mark(self):
+        self.timer = timer.Timer()
+        # Avoid failing if ~1ms elapses between these two calls
+        self.timer.precision = 2
+        self.timer.mark()
+        assert len(self.timer.data['marks']) == 1
+        assert self.timer.data['marks'][0]['interval'] == 0
+        assert self.timer.data['marks'][0]['message'] == ''
+
+    def test_data_five_marks(self):
+        self.timer = timer.Timer()
+        for i in range(5):
+            self.timer.mark(str(i))
+        assert len(self.timer.data['marks']) == 5
+        assert [m['message'] for m in self.timer.data['marks']] == \
+            ['0', '1', '2', '3', '4']
+
+    def test_intervals(self):
+        fake_time = MagicMock()
+        with patch('teuthology.timer.time.time', fake_time):
+            self.timer = timer.Timer()
+            now = start_time = fake_time.return_value = time()
+            intervals = [0, 1, 1, 2, 3, 5, 8]
+            for i in intervals:
+                now += i
+                fake_time.return_value = now
+                self.timer.mark(str(i))
+
+        summed_intervals = [sum(intervals[:x + 1]) for x in range(len(intervals))]
+        result_intervals = [m['interval'] for m in self.timer.data['marks']]
+        assert result_intervals == summed_intervals
+        assert self.timer.data['start'] == \
+            self.timer.get_datetime_string(start_time)
+        assert self.timer.data['end'] == \
+            self.timer.get_datetime_string(start_time + summed_intervals[-1])
+        assert [m['message'] for m in self.timer.data['marks']] == \
+            [str(i) for i in intervals]
+        assert self.timer.data['elapsed'] == summed_intervals[-1]
+
+    def test_write(self):
+        _path = '/path'
+        _safe_dump = MagicMock(name='safe_dump')
+        with patch('teuthology.timer.yaml.safe_dump', _safe_dump):
+            with patch('teuthology.timer.open', mock_open(), create=True) as _open:
+                self.timer = timer.Timer(path=_path)
+                assert self.timer.path == _path
+                self.timer.write()
+                _open.assert_called_once_with(_path, 'w')
+                _safe_dump.assert_called_once_with(
+                    dict(),
+                    _open.return_value.__enter__.return_value,
+                    default_flow_style=False,
+                )
+
+    def test_sync(self):
+        _path = '/path'
+        _safe_dump = MagicMock(name='safe_dump')
+        with patch('teuthology.timer.yaml.safe_dump', _safe_dump):
+            with patch('teuthology.timer.open', mock_open(), create=True) as _open:
+                self.timer = timer.Timer(path=_path, sync=True)
+                assert self.timer.path == _path
+                assert self.timer.sync is True
+                self.timer.mark()
+                _open.assert_called_once_with(_path, 'w')
+                _safe_dump.assert_called_once_with(
+                    self.timer.data,
+                    _open.return_value.__enter__.return_value,
+                    default_flow_style=False,
+                )
diff --git a/tests/test_vps_os_vers_parameter_checking.py b/tests/test_vps_os_vers_parameter_checking.py
new file mode 100644 (file)
index 0000000..8acfed0
--- /dev/null
@@ -0,0 +1,84 @@
+from unittest.mock import patch, Mock
+
+import teuthology.lock.util
+from teuthology import provision
+
+
+class TestVpsOsVersionParamCheck(object):
+
+    def setup_method(self):
+        self.fake_ctx = Mock()
+        self.fake_ctx.machine_type = 'vps'
+        self.fake_ctx.num_to_lock = 1
+        self.fake_ctx.lock = False
+
+        def fake_downburst_executable():
+            return ''
+
+        self.fake_downburst_executable = fake_downburst_executable
+
+    def test_ubuntu_noble(self):
+        self.fake_ctx.os_type = 'ubuntu'
+        self.fake_ctx.os_version = 'noble'
+        with patch.multiple(
+            provision.downburst,
+            downburst_executable=self.fake_downburst_executable,
+        ):
+            check_value = teuthology.lock.util.vps_version_or_type_valid(
+                self.fake_ctx.machine_type,
+                self.fake_ctx.os_type,
+                self.fake_ctx.os_version)
+
+        assert check_value
+
+    def test_ubuntu_number(self):
+        self.fake_ctx.os_type = 'ubuntu'
+        self.fake_ctx.os_version = '24.04'
+        with patch.multiple(
+            provision.downburst,
+            downburst_executable=self.fake_downburst_executable,
+        ):
+            check_value = teuthology.lock.util.vps_version_or_type_valid(
+                self.fake_ctx.machine_type,
+                self.fake_ctx.os_type,
+                self.fake_ctx.os_version)
+        assert check_value
+
+    def test_mixup(self):
+        self.fake_ctx.os_type = '6.5'
+        self.fake_ctx.os_version = 'rhel'
+        with patch.multiple(
+            provision.downburst,
+            downburst_executable=self.fake_downburst_executable,
+        ):
+            check_value = teuthology.lock.util.vps_version_or_type_valid(
+                self.fake_ctx.machine_type,
+                self.fake_ctx.os_type,
+                self.fake_ctx.os_version)
+        assert not check_value
+
+    def test_bad_type(self):
+        self.fake_ctx.os_type = 'aardvark'
+        self.fake_ctx.os_version = '6.5'
+        with patch.multiple(
+            provision.downburst,
+            downburst_executable=self.fake_downburst_executable,
+        ):
+            check_value = teuthology.lock.util.vps_version_or_type_valid(
+                self.fake_ctx.machine_type,
+                self.fake_ctx.os_type,
+                self.fake_ctx.os_version)
+        assert not check_value
+
+    def test_bad_version(self):
+        self.fake_ctx.os_type = 'ubuntu'
+        self.fake_ctx.os_version = 'vampire_bat'
+        with patch.multiple(
+            provision.downburst,
+            downburst_executable=self.fake_downburst_executable,
+        ):
+            check_value = teuthology.lock.util.vps_version_or_type_valid(
+                self.fake_ctx.machine_type,
+                self.fake_ctx.os_type,
+                self.fake_ctx.os_version)
+        assert not check_value
diff --git a/tests/util/files/test_unit_test.xml b/tests/util/files/test_unit_test.xml
new file mode 100644 (file)
index 0000000..bd9c734
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<testsuite name="nosetests" tests="644" errors="0" failures="1" skip="79">
+<testcase classname="s3tests_boto3.functional.test_s3" name="test_cors_origin_response" time="3.205"></testcase>
+<testcase classname="s3tests_boto3.functional.test_s3" name="test_cors_origin_wildcard" time="3.081"></testcase>
+<testcase classname="s3tests_boto3.functional.test_s3" name="test_cors_header_option" time="3.119"></testcase>
+<testcase classname="s3tests_boto3.functional.test_s3" name="test_set_bucket_tagging" time="0.059"><failure type="builtins.AssertionError" message="'NoSuchTagSetError' != 'NoSuchTagSet'"></failure></testcase>
+</testsuite>
\ No newline at end of file
diff --git a/tests/util/files/test_valgrind.xml b/tests/util/files/test_valgrind.xml
new file mode 100644 (file)
index 0000000..41bf837
--- /dev/null
@@ -0,0 +1,31 @@
+<?xml version="1.0"?>
+<valgrindoutput>
+<error>
+  <unique>0x870fc</unique>
+  <tid>1</tid>
+  <kind>Leak_DefinitelyLost</kind>
+  <xwhat>
+    <text>1,234 bytes in 1 blocks are definitely lost in loss record 198 of 201</text>
+    <leakedbytes>1234</leakedbytes>
+    <leakedblocks>1</leakedblocks>
+  </xwhat>
+  <stack>
+    <frame>
+      <ip>0x4C39B6F</ip>
+      <obj>/usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so</obj>
+      <fn>operator new[](unsigned long)</fn>
+      <dir>/builddir/build/BUILD/valgrind-3.19.0/coregrind/m_replacemalloc</dir>
+      <file>vg_replace_malloc.c</file>
+      <line>640</line>
+    </frame>
+    <frame>
+      <ip>0xF3F4B5</ip>
+      <obj>/usr/bin/ceph-osd</obj>
+      <fn>ceph::common::leak_some_memory()</fn>
+      <dir>/usr/src/debug/ceph-18.0.0-5567.g64a4fc94.el8.x86_64/src/common</dir>
+      <file>ceph_context.cc</file>
+      <line>510</line>
+    </frame>
+  </stack>
+</error>
+</valgrindoutput>
diff --git a/tests/util/test_scanner.py b/tests/util/test_scanner.py
new file mode 100644 (file)
index 0000000..928d430
--- /dev/null
@@ -0,0 +1,191 @@
+from mock import patch, MagicMock
+
+from io import BytesIO
+import os, io
+
+from teuthology.orchestra import remote
+from teuthology.util.scanner import UnitTestScanner, ValgrindScanner
+
+
+class MockFile(io.StringIO):
+    def close(self):
+        pass
+
+
+class TestUnitTestScanner(object):
+
+    def setup_method(self):
+        self.remote = remote.Remote(
+            name='jdoe@xyzzy.example.com', ssh=MagicMock())
+        self.test_values = {
+            "xml_path": os.path.dirname(__file__) + "/files/test_unit_test.xml",
+            "error_msg": "FAILURE: Test `test_set_bucket_tagging` of `s3tests_boto3.functional.test_s3`. \
+Reason: 'NoSuchTagSetError' != 'NoSuchTagSet'.",
+            "summary_data": [{'failed_testsuites': {'s3tests_boto3.functional.test_s3': 
+                                [{'kind': 'failure', 'testcase': 'test_set_bucket_tagging', 
+                                  'message': "'NoSuchTagSetError' != 'NoSuchTagSet'"}]}, 
+                              'num_of_failures': 1, 
+                              'file_path': f'{os.path.dirname(__file__)}/files/test_unit_test.xml'}],
+            "yaml_data": r"""- failed_testsuites:
+    s3tests_boto3.functional.test_s3:
+    - kind: failure
+      message: '''NoSuchTagSetError'' != ''NoSuchTagSet'''
+      testcase: test_set_bucket_tagging
+  file_path: {file_dir}/files/test_unit_test.xml
+  num_of_failures: 1
+""".format(file_dir=os.path.dirname(__file__))
+        }
+
+    @patch('teuthology.util.scanner.UnitTestScanner.write_summary')
+    def test_scan_and_write(self, m_write_summary):
+        xml_path = self.test_values["xml_path"]
+        self.remote.ssh.exec_command.return_value = (None, BytesIO(xml_path.encode('utf-8')), None)
+        m_open = MagicMock()
+        m_open.return_value = open(xml_path, "rb")
+        self.remote._sftp_open_file = m_open
+        result = UnitTestScanner(remote=self.remote).scan_and_write(xml_path, "test_summary.yaml")
+        assert result == "(total 1 failed) " + self.test_values["error_msg"]
+
+    def test_parse(self):
+        xml_content = b'<?xml version="1.0" encoding="UTF-8"?>\n<testsuite name="xyz" tests="1" \
+errors="0" failures="1">\n<testcase classname="xyz" name="abc" time="0.059"><failure \
+type="builtins.AssertionError" message="error_msg"></failure></testcase>\n</testsuite>'
+        scanner = UnitTestScanner(self.remote)
+        result = scanner._parse(xml_content)
+        assert result == (
+            'FAILURE: Test `abc` of `xyz`. Reason: error_msg.', 
+            {'failed_testsuites': {'xyz': 
+                [{'kind': 'failure','message': 'error_msg','testcase': 'abc'}]},
+            'num_of_failures': 1
+            }
+        )
+    
+    def test_scan_file(self):
+        xml_path = self.test_values["xml_path"]
+        m_open = MagicMock()
+        m_open.return_value = open(xml_path, "rb")
+        self.remote._sftp_open_file = m_open
+        scanner = UnitTestScanner(remote=self.remote)
+        result = scanner.scan_file(xml_path)
+        assert result == self.test_values["error_msg"]
+        assert scanner.summary_data == self.test_values["summary_data"]
+
+    def test_scan_all_files(self):
+        xml_path = self.test_values["xml_path"]
+        self.remote.ssh.exec_command.return_value = (None, BytesIO(xml_path.encode('utf-8')), None)
+        m_open = MagicMock()
+        m_open.return_value = open(xml_path, "rb")
+        self.remote._sftp_open_file = m_open
+        scanner = UnitTestScanner(remote=self.remote)
+        result = scanner.scan_all_files(xml_path)
+        assert result == [self.test_values["error_msg"]]
+                          
+    @patch('builtins.open')
+    def test_write_summary(self, m_open):
+        scanner = UnitTestScanner(self.remote)
+        mock_yaml_file = MockFile()
+        scanner.summary_data = self.test_values["summary_data"]
+        m_open.return_value = mock_yaml_file
+        scanner.write_summary("path/file.yaml")
+        written_content = mock_yaml_file.getvalue()             
+        assert written_content == self.test_values["yaml_data"]    
+
+
+class TestValgrindScanner(object):
+
+    def setup_method(self):
+        self.remote = remote.Remote(
+            name='jdoe@xyzzy.example.com', ssh=MagicMock())
+        self.test_values = {
+            "xml_path": os.path.dirname(__file__) + "/files/test_valgrind.xml",
+            "error_msg": "valgrind error: Leak_DefinitelyLost\noperator new[]\
+(unsigned long)\nceph::common::leak_some_memory()",
+            "summary_data": [{'kind': 'Leak_DefinitelyLost', 'traceback': [{'file': 
+                '/builddir/build/BUILD/valgrind-3.19.0/coregrind/m_replacemalloc/vg_replace_malloc.c', 
+                'line': '640', 'function': 'operator new[](unsigned long)'}, 
+                {'file': '/usr/src/debug/ceph-18.0.0-5567.g64a4fc94.el8.x86_64/src/common/ceph_context.cc', 
+                 'line': '510', 'function': 'ceph::common::leak_some_memory()'}], 'file_path': 
+                 f'{os.path.dirname(__file__)}/files/test_valgrind.xml'}],
+            "yaml_data": r"""- file_path: {file_dir}/files/test_valgrind.xml
+  kind: Leak_DefinitelyLost
+  traceback:
+  - file: /builddir/build/BUILD/valgrind-3.19.0/coregrind/m_replacemalloc/vg_replace_malloc.c
+    function: operator new[](unsigned long)
+    line: '640'
+  - file: /usr/src/debug/ceph-18.0.0-5567.g64a4fc94.el8.x86_64/src/common/ceph_context.cc
+    function: ceph::common::leak_some_memory()
+    line: '510'
+""".format(file_dir=os.path.dirname(__file__))
+        }
+
+    def test_parse_with_traceback(self):
+        xml_content = b'''<?xml version="1.0"?>
+<valgrindoutput>
+<error>
+  <kind>Leak_DefinitelyLost</kind>
+  <stack>
+    <frame>
+      <fn>func()</fn>
+      <dir>/dir</dir>
+      <file>file1.ext</file>
+      <line>640</line>
+    </frame>
+  </stack>
+</error>
+</valgrindoutput>
+'''
+        scanner = ValgrindScanner(self.remote)
+        result = scanner._parse(xml_content)
+        assert result == (
+            'valgrind error: Leak_DefinitelyLost\nfunc()', 
+            {'kind': 'Leak_DefinitelyLost', 'traceback': 
+                [{'file': '/dir/file1.ext', 'line': '640', 'function': 'func()'}]
+            }
+        )
+
+    def test_parse_without_trackback(self):
+        xml_content = b'''<?xml version="1.0"?>
+<valgrindoutput>
+<error>
+  <kind>Leak_DefinitelyLost</kind>
+  <stack>
+  </stack>
+</error>
+</valgrindoutput>
+'''
+        scanner = ValgrindScanner(self.remote)
+        result = scanner._parse(xml_content)
+        assert result == (
+            'valgrind error: Leak_DefinitelyLost\n', 
+            {'kind': 'Leak_DefinitelyLost', 'traceback': []}
+        )
+    
+    def test_scan_file(self):
+        xml_path = self.test_values["xml_path"]
+        m_open = MagicMock()
+        m_open.return_value = open(xml_path, "rb")
+        self.remote._sftp_open_file = m_open
+        scanner = ValgrindScanner(remote=self.remote)
+        result = scanner.scan_file(xml_path)
+        assert result == self.test_values["error_msg"]
+        assert scanner.summary_data == self.test_values["summary_data"]
+
+    def test_scan_all_files(self):
+        xml_path = self.test_values["xml_path"]
+        self.remote.ssh.exec_command.return_value = (None, BytesIO(xml_path.encode('utf-8')), None)
+        m_open = MagicMock()
+        m_open.return_value = open(xml_path, "rb")
+        self.remote._sftp_open_file = m_open
+        scanner = ValgrindScanner(remote=self.remote)
+        result = scanner.scan_all_files(xml_path)
+        assert result == [self.test_values["error_msg"]]
+                          
+    @patch('builtins.open')
+    def test_write_summary(self, m_open):
+        scanner = ValgrindScanner(self.remote)
+        mock_yaml_file = MockFile()
+        scanner.summary_data = self.test_values["summary_data"]
+        m_open.return_value = mock_yaml_file
+        scanner.write_summary("path/file.yaml")
+        written_content = mock_yaml_file.getvalue()             
+        assert written_content == self.test_values["yaml_data"]
\ No newline at end of file
diff --git a/tests/util/test_time.py b/tests/util/test_time.py
new file mode 100644 (file)
index 0000000..c37b5f2
--- /dev/null
@@ -0,0 +1,54 @@
+import pytest
+
+from datetime import datetime, timedelta, timezone
+from typing import Type
+
+from teuthology.util import time
+
+
+@pytest.mark.parametrize(
+    ["timestamp", "result"],
+    [
+        ["1999-12-31_23:59:59", datetime(1999, 12, 31, 23, 59, 59, tzinfo=timezone.utc)],
+        ["1999-12-31_23:59", datetime(1999, 12, 31, 23, 59, 0, tzinfo=timezone.utc)],
+        ["1999-12-31T23:59:59", datetime(1999, 12, 31, 23, 59, 59, tzinfo=timezone.utc)],
+        ["1999-12-31T23:59:59+00:00", datetime(1999, 12, 31, 23, 59, 59, tzinfo=timezone.utc)],
+        ["1999-12-31T17:59:59-06:00", datetime(1999, 12, 31, 23, 59, 59, tzinfo=timezone.utc)],
+        ["2024-01-01", datetime(2024, 1, 1, 0, 0, tzinfo=timezone.utc)],
+        ["tomorrow", ValueError],
+        ["1d", ValueError],
+        ["", ValueError],
+        ["2024", ValueError],
+
+    ]
+)
+def test_parse_timestamp(timestamp: str, result: datetime | Type[Exception]):
+    if isinstance(result, datetime):
+        assert time.parse_timestamp(timestamp) == result
+    else:
+        with pytest.raises(result):
+            time.parse_timestamp(timestamp)
+
+
+@pytest.mark.parametrize(
+    ["offset", "result"],
+    [
+        ["1s", timedelta(seconds=1)],
+        ["1m", timedelta(minutes=1)],
+        ["1h", timedelta(hours=1)],
+        ["1d", timedelta(days=1)],
+        ["1w", timedelta(weeks=1)],
+        ["365d", timedelta(days=365)],
+        ["1x", ValueError],
+        ["-1m", ValueError],
+        ["0xde", ValueError],
+        ["frog", ValueError],
+        ["7dwarfs", ValueError],
+    ]
+)
+def test_parse_offset(offset: str, result: timedelta | Type[Exception]):
+    if isinstance(result, timedelta):
+        assert time.parse_offset(offset) == result
+    else:
+        with pytest.raises(result):
+            time.parse_offset(offset)
diff --git a/teuthology/dispatcher/test/test_dispatcher.py b/teuthology/dispatcher/test/test_dispatcher.py
deleted file mode 100644 (file)
index cf35aff..0000000
+++ /dev/null
@@ -1,169 +0,0 @@
-import datetime
-import pytest
-
-from unittest.mock import patch, Mock, MagicMock
-
-from teuthology import dispatcher
-from teuthology.config import FakeNamespace
-from teuthology.contextutil import MaxWhileTries
-from teuthology.util.time import TIMESTAMP_FMT
-
-
-class TestDispatcher(object):
-    @pytest.fixture(autouse=True)
-    def setup_method(self, tmp_path):
-        self.ctx = FakeNamespace()
-        self.ctx.verbose = True
-        self.ctx.archive_dir = str(tmp_path / "archive/dir")
-        self.ctx.log_dir = str(tmp_path / "log/dir")
-        self.ctx.tube = 'tube'
-
-    @patch("teuthology.repo_utils.ls_remote")
-    @patch("os.path.isdir")
-    @patch("teuthology.repo_utils.fetch_teuthology")
-    @patch("teuthology.dispatcher.teuth_config")
-    @patch("teuthology.repo_utils.fetch_qa_suite")
-    def test_prep_job(self, m_fetch_qa_suite, m_teuth_config,
-            m_fetch_teuthology, m_isdir, m_ls_remote):
-        config = dict(
-            name="the_name",
-            job_id="1",
-            suite_sha1="suite_hash",
-        )
-        m_fetch_teuthology.return_value = '/teuth/path'
-        m_fetch_qa_suite.return_value = '/suite/path'
-        m_ls_remote.return_value = 'teuth_hash'
-        m_isdir.return_value = True
-        m_teuth_config.teuthology_path = None
-        got_config, teuth_bin_path = dispatcher.prep_job(config)
-        assert got_config['teuthology_branch'] == 'main'
-        m_fetch_teuthology.assert_called_once_with(branch='main', commit='teuth_hash')
-        assert teuth_bin_path == '/teuth/path/.venv/bin'
-        m_fetch_qa_suite.assert_called_once_with('main', 'suite_hash')
-        assert got_config['suite_path'] == '/suite/path'
-
-    def build_fake_jobs(self, m_connection, m_job, job_bodies):
-        """
-        Given patched copies of:
-            beanstalkc.Connection
-            beanstalkc.Job
-        And a list of basic job bodies, return a list of mocked Job objects
-        """
-        # Make sure instantiating m_job returns a new object each time
-        jobs = []
-        job_id = 0
-        for job_body in job_bodies:
-            job_id += 1
-            job = MagicMock(conn=m_connection, jid=job_id, body=job_body)
-            job.jid = job_id
-            job.body = job_body
-            jobs.append(job)
-        return jobs
-
-    @patch("teuthology.dispatcher.find_dispatcher_processes")
-    @patch("teuthology.repo_utils.ls_remote")
-    @patch("teuthology.dispatcher.report.try_push_job_info")
-    @patch("teuthology.dispatcher.supervisor.run_job")
-    @patch("beanstalkc.Job", autospec=True)
-    @patch("teuthology.repo_utils.fetch_qa_suite")
-    @patch("teuthology.repo_utils.fetch_teuthology")
-    @patch("teuthology.dispatcher.beanstalk.watch_tube")
-    @patch("teuthology.dispatcher.beanstalk.connect")
-    @patch("os.path.isdir", return_value=True)
-    @patch("teuthology.dispatcher.setup_log_file")
-    def test_main_loop(
-        self, m_setup_log_file, m_isdir, m_connect, m_watch_tube,
-        m_fetch_teuthology, m_fetch_qa_suite, m_job, m_run_job,
-        m_try_push_job_info, m_ls_remote, m_find_dispatcher_processes,
-                       ):
-        m_find_dispatcher_processes.return_value = {}
-        m_connection = Mock()
-        jobs = self.build_fake_jobs(
-            m_connection,
-            m_job,
-            [
-                'name: name\nfoo: bar',
-                'name: name\nstop_worker: true',
-            ],
-        )
-        m_connection.reserve.side_effect = jobs
-        m_connect.return_value = m_connection
-        dispatcher.main(self.ctx)
-        # There should be one reserve call per item in the jobs list
-        expected_reserve_calls = [
-            dict(timeout=60) for i in range(len(jobs))
-        ]
-        got_reserve_calls = [
-            call[1] for call in m_connection.reserve.call_args_list
-        ]
-        assert got_reserve_calls == expected_reserve_calls
-        for job in jobs:
-            job.bury.assert_called_once_with()
-            job.delete.assert_called_once_with()
-
-    @patch("teuthology.dispatcher.find_dispatcher_processes")
-    @patch("teuthology.repo_utils.ls_remote")
-    @patch("teuthology.dispatcher.report.try_push_job_info")
-    @patch("teuthology.dispatcher.supervisor.run_job")
-    @patch("beanstalkc.Job", autospec=True)
-    @patch("teuthology.repo_utils.fetch_qa_suite")
-    @patch("teuthology.repo_utils.fetch_teuthology")
-    @patch("teuthology.dispatcher.beanstalk.watch_tube")
-    @patch("teuthology.dispatcher.beanstalk.connect")
-    @patch("os.path.isdir", return_value=True)
-    @patch("teuthology.dispatcher.setup_log_file")
-    def test_main_loop_13925(
-        self, m_setup_log_file, m_isdir, m_connect, m_watch_tube,
-        m_fetch_teuthology, m_fetch_qa_suite, m_job, m_run_job,
-        m_try_push_job_info, m_ls_remote, m_find_dispatcher_processes,
-                       ):
-        m_find_dispatcher_processes.return_value = {}
-        m_connection = Mock()
-        jobs = self.build_fake_jobs(
-            m_connection,
-            m_job,
-            [
-                'name: name',
-                'name: name\nstop_worker: true',
-            ],
-        )
-        m_connection.reserve.side_effect = jobs
-        m_connect.return_value = m_connection
-        m_fetch_qa_suite.side_effect = [
-            MaxWhileTries(),
-            MaxWhileTries(),
-        ]
-        dispatcher.main(self.ctx)
-        assert len(m_run_job.call_args_list) == 0
-        assert len(m_try_push_job_info.call_args_list) == len(jobs)
-        for i in range(len(jobs)):
-            push_call = m_try_push_job_info.call_args_list[i]
-            assert push_call[0][1]['status'] == 'dead'
-
-    @pytest.mark.parametrize(
-        ["timestamp", "expire", "skip"],
-        [
-            [datetime.timedelta(days=-1), None, False],
-            [datetime.timedelta(days=-30), None, True],
-            [None, datetime.timedelta(days=1), False],
-            [None, datetime.timedelta(days=-1), True],
-            [datetime.timedelta(days=-1), datetime.timedelta(days=1), False],
-            [datetime.timedelta(days=1), datetime.timedelta(days=-1), True],
-        ]
-    )
-    @patch("teuthology.dispatcher.report.try_push_job_info")
-    def test_check_job_expiration(self, _, timestamp, expire, skip):
-        now = datetime.datetime.now(datetime.timezone.utc)
-        job_config = dict(
-            job_id="1",
-            name="job_name",
-        )
-        if timestamp:
-            job_config["timestamp"] = (now + timestamp).strftime(TIMESTAMP_FMT)
-        if expire:
-            job_config["expire"] = (now + expire).strftime(TIMESTAMP_FMT)
-        if skip:
-            with pytest.raises(dispatcher.SkipJob):
-                dispatcher.check_job_expiration(job_config)
-        else:
-            dispatcher.check_job_expiration(job_config)
diff --git a/teuthology/dispatcher/test/test_reimage_error_mark_machine_down.py b/teuthology/dispatcher/test/test_reimage_error_mark_machine_down.py
deleted file mode 100644 (file)
index 63ec8eb..0000000
+++ /dev/null
@@ -1,104 +0,0 @@
-from teuthology.dispatcher import supervisor
-from unittest.mock import patch
-
-class TestCheckReImageFailureMarkDown(object):
-    def setup_method(self):
-        self.the_function = supervisor.check_for_reimage_failures_and_mark_down
-
-    def create_n_out_of_10_reimage_failed_jobs(self, n):
-        ret_list = []
-        for i in range(n):
-            obj1 = {
-              "failure_reason":"Error reimaging machines: Manually raised error"
-              }
-            ret_list.append(obj1)
-        for j in range(10-n):
-            obj2 = {"failure_reason":"Error something else: dummy"}
-            ret_list.append(obj2)
-        return ret_list
-
-    @patch('teuthology.dispatcher.supervisor.shortname')
-    @patch('teuthology.lock.ops.update_lock')
-    @patch('teuthology.dispatcher.supervisor.requests')
-    @patch('teuthology.dispatcher.supervisor.urljoin')
-    @patch('teuthology.dispatcher.supervisor.teuth_config')
-    def test_one_machine_ten_reimage_failed_jobs(
-        self,
-        m_t_config,
-        m_urljoin,
-        m_requests,
-        mark_down,
-        shortname
-        ):
-        targets = {'fakeos@rmachine061.front.sepia.ceph.com': 'ssh-ed25519'}
-        m_requests.get.return_value.json.return_value = \
-            self.create_n_out_of_10_reimage_failed_jobs(10)
-        shortname.return_value = 'rmachine061'
-        self.the_function(targets)
-        assert mark_down.called
-
-    @patch('teuthology.dispatcher.supervisor.shortname')
-    @patch('teuthology.lock.ops.update_lock')
-    @patch('teuthology.dispatcher.supervisor.requests')
-    @patch('teuthology.dispatcher.supervisor.urljoin')
-    @patch('teuthology.dispatcher.supervisor.teuth_config')
-    def test_one_machine_seven_reimage_failed_jobs(
-        self,
-        m_t_config,
-        m_urljoin,
-        m_requests,
-        mark_down,
-        shortname,
-        ):
-        targets = {'fakeos@rmachine061.front.sepia.ceph.com': 'ssh-ed25519'}
-        m_requests.get.return_value.json.return_value = \
-            self.create_n_out_of_10_reimage_failed_jobs(7)
-        shortname.return_value = 'rmachine061'
-        self.the_function(targets)
-        assert mark_down.called is False
-
-    @patch('teuthology.dispatcher.supervisor.shortname')
-    @patch('teuthology.lock.ops.update_lock')
-    @patch('teuthology.dispatcher.supervisor.requests')
-    @patch('teuthology.dispatcher.supervisor.urljoin')
-    @patch('teuthology.dispatcher.supervisor.teuth_config')
-    def test_two_machine_all_reimage_failed_jobs(
-        self,
-        m_t_config,
-        m_urljoin,
-        m_requests,
-        mark_down,
-        shortname,
-        ):
-        targets = {'fakeos@rmachine061.front.sepia.ceph.com': 'ssh-ed25519',
-                   'fakeos@rmachine179.back.sepia.ceph.com': 'ssh-ed45333'}
-        m_requests.get.return_value.json.side_effect = \
-            [self.create_n_out_of_10_reimage_failed_jobs(10),
-            self.create_n_out_of_10_reimage_failed_jobs(10)]
-        shortname.return_value.side_effect = ['rmachine061', 'rmachine179']
-        self.the_function(targets)
-        assert mark_down.call_count == 2
-
-    @patch('teuthology.dispatcher.supervisor.shortname')
-    @patch('teuthology.lock.ops.update_lock')
-    @patch('teuthology.dispatcher.supervisor.requests')
-    @patch('teuthology.dispatcher.supervisor.urljoin')
-    @patch('teuthology.dispatcher.supervisor.teuth_config')
-    def test_two_machine_one_healthy_one_reimage_failure(
-        self,
-        m_t_config,
-        m_urljoin,
-        m_requests,
-        mark_down,
-        shortname,
-        ):
-        targets = {'fakeos@rmachine061.front.sepia.ceph.com': 'ssh-ed25519',
-                   'fakeos@rmachine179.back.sepia.ceph.com': 'ssh-ed45333'}
-        m_requests.get.return_value.json.side_effect = \
-            [self.create_n_out_of_10_reimage_failed_jobs(0),
-            self.create_n_out_of_10_reimage_failed_jobs(10)]
-        shortname.return_value.side_effect = ['rmachine061', 'rmachine179']
-        self.the_function(targets)
-        assert mark_down.call_count == 1
-        assert mark_down.call_args_list[0][0][0].startswith('rmachine179')
-
diff --git a/teuthology/dispatcher/test/test_supervisor.py b/teuthology/dispatcher/test/test_supervisor.py
deleted file mode 100644 (file)
index 0a524a5..0000000
+++ /dev/null
@@ -1,121 +0,0 @@
-from subprocess import DEVNULL
-from unittest.mock import patch, Mock, MagicMock
-
-from teuthology.dispatcher import supervisor
-
-
-class TestSuperviser(object):
-    @patch("teuthology.dispatcher.supervisor.run_with_watchdog")
-    @patch("teuthology.dispatcher.supervisor.teuth_config")
-    @patch("subprocess.Popen")
-    @patch("os.environ")
-    @patch("os.mkdir")
-    @patch("yaml.safe_dump")
-    @patch("tempfile.NamedTemporaryFile")
-    def test_run_job_with_watchdog(self, m_tempfile, m_safe_dump, m_mkdir,
-                                   m_environ, m_popen, m_t_config,
-                                   m_run_watchdog):
-        config = {
-            "suite_path": "suite/path",
-            "config": {"foo": "bar"},
-            "verbose": True,
-            "owner": "the_owner",
-            "archive_path": "archive/path",
-            "name": "the_name",
-            "description": "the_description",
-            "job_id": "1",
-        }
-        m_tmp = MagicMock()
-        temp_file = Mock()
-        temp_file.name = "the_name"
-        m_tmp.__enter__.return_value = temp_file
-        m_tempfile.return_value = m_tmp
-        m_p = Mock()
-        m_p.returncode = 0
-        m_popen.return_value = m_p
-        m_t_config.results_server = True
-        config_path = "archive/path/initial.config.yaml"
-        supervisor.run_job(config, config_path, "teuth/bin/path", "archive/dir", verbose=False)
-        m_run_watchdog.assert_called_with(m_p, config)
-        expected_args = [
-            'teuth/bin/path/teuthology',
-            '-v',
-            '--owner', 'the_owner',
-            '--archive', 'archive/path',
-            '--name', 'the_name',
-            '--description',
-            'the_description',
-            '--',
-            config_path,
-        ]
-        m_popen.assert_called_with(args=expected_args, stderr=DEVNULL, stdout=DEVNULL)
-
-    @patch("time.sleep")
-    @patch("teuthology.dispatcher.supervisor.teuth_config")
-    @patch("subprocess.Popen")
-    @patch("os.environ")
-    @patch("os.mkdir")
-    @patch("yaml.safe_dump")
-    @patch("tempfile.NamedTemporaryFile")
-    def test_run_job_no_watchdog(self, m_tempfile, m_safe_dump, m_mkdir,
-                                 m_environ, m_popen, m_t_config,
-                                 m_sleep):
-        config = {
-            "suite_path": "suite/path",
-            "config": {"foo": "bar"},
-            "verbose": True,
-            "owner": "the_owner",
-            "archive_path": "archive/path",
-            "name": "the_name",
-            "description": "the_description",
-            "job_id": "1",
-        }
-        m_tmp = MagicMock()
-        temp_file = Mock()
-        temp_file.name = "the_name"
-        m_tmp.__enter__.return_value = temp_file
-        m_tempfile.return_value = m_tmp
-        env = dict(PYTHONPATH="python/path")
-        m_environ.copy.return_value = env
-        m_p = Mock()
-        m_p.returncode = 1
-        m_popen.return_value = m_p
-        m_t_config.results_server = False
-        config_path = "archive/path/initial.config.yaml"
-        supervisor.run_job(config, config_path, "teuth/bin/path", "archive/dir", verbose=False)
-
-    @patch("teuthology.dispatcher.supervisor.report.try_push_job_info")
-    @patch("time.sleep")
-    def test_run_with_watchdog_no_reporting(self, m_sleep, m_try_push):
-        config = {
-            "name": "the_name",
-            "job_id": "1",
-            "archive_path": "archive/path",
-            "teuthology_branch": "main",
-            "owner": "the_owner",
-        }
-        process = Mock()
-        process.poll.return_value = "not None"
-        supervisor.run_with_watchdog(process, config)
-        m_try_push.assert_called_with(
-            dict(name=config["name"], job_id=config["job_id"]),
-            dict(status='dead')
-        )
-
-    @patch("subprocess.Popen")
-    @patch("time.sleep")
-    @patch("teuthology.dispatcher.supervisor.report.try_push_job_info")
-    def test_run_with_watchdog_with_reporting(self, m_tpji, m_sleep, m_popen):
-        config = {
-            "name": "the_name",
-            "job_id": "1",
-            "archive_path": "archive/path",
-            "teuthology_branch": "jewel",
-            "owner": "the_owner",
-        }
-        process = Mock()
-        process.poll.return_value = "not None"
-        m_proc = Mock()
-        m_proc.poll.return_value = "not None"
-        m_popen.return_value = m_proc
-        supervisor.run_with_watchdog(process, config)
diff --git a/teuthology/lock/test/__init__.py b/teuthology/lock/test/__init__.py
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/teuthology/lock/test/test_lock.py b/teuthology/lock/test/test_lock.py
deleted file mode 100644 (file)
index 5f1679a..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-import teuthology.lock.util
-
-class TestLock(object):
-
-    def test_locked_since_seconds(self):
-        node = { "locked_since": "2013-02-07 19:33:55.000000" }
-        assert teuthology.lock.util.locked_since_seconds(node) > 3600
diff --git a/teuthology/openstack/test/__init__.py b/teuthology/openstack/test/__init__.py
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/teuthology/openstack/test/archive-on-error.yaml b/teuthology/openstack/test/archive-on-error.yaml
deleted file mode 100644 (file)
index f9f5247..0000000
+++ /dev/null
@@ -1 +0,0 @@
-archive-on-error: true
diff --git a/teuthology/openstack/test/noop.yaml b/teuthology/openstack/test/noop.yaml
deleted file mode 100644 (file)
index 6aae7ec..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-stop_worker: true
-machine_type: openstack
-os_type: ubuntu
-os_version: "14.04"
-roles:
-- - mon.a
-  - osd.0
-tasks:
-- exec:
-    mon.a:
-      - echo "Well done !"
-
diff --git a/teuthology/openstack/test/openstack-integration.py b/teuthology/openstack/test/openstack-integration.py
deleted file mode 100644 (file)
index 5d1a266..0000000
+++ /dev/null
@@ -1,243 +0,0 @@
-#
-# Copyright (c) 2015, 2016 Red Hat, Inc.
-#
-# Author: Loic Dachary <loic@dachary.org>
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-# THE SOFTWARE.
-#
-import argparse
-import logging
-import json
-import os
-import subprocess
-import tempfile
-import shutil
-
-import teuthology.lock
-import teuthology.lock.cli
-import teuthology.lock.query
-import teuthology.lock.util
-import teuthology.misc
-import teuthology.schedule
-import teuthology.suite
-import teuthology.openstack
-import scripts.schedule
-import scripts.lock
-import scripts.suite
-from teuthology.config import config as teuth_config
-from teuthology.config import set_config_attr
-
-
-class Integration(object):
-
-    @classmethod
-    def setup_class(self):
-        teuthology.log.setLevel(logging.DEBUG)
-        set_config_attr(argparse.Namespace())
-        self.teardown_class()
-
-    @classmethod
-    def teardown_class(self):
-        os.system("sudo /etc/init.d/beanstalkd restart")
-        # if this fails it will not show the error but some weird
-        # INTERNALERROR> IndexError: list index out of range
-        # move that to def tearDown for debug and when it works move it
-        # back in tearDownClass so it is not called on every test
-        ownedby = "ownedby='" + teuth_config.openstack['ip']
-        all_instances = teuthology.openstack.OpenStack().run(
-            "server list -f json --long")
-        for instance in json.loads(all_instances):
-            if ownedby in instance['Properties']:
-                teuthology.openstack.OpenStack().run(
-                    "server delete --wait " + instance['ID'])
-
-    def setup_worker(self):
-        self.logs = self.d + "/log"
-        os.mkdir(self.logs, 0o755)
-        self.archive = self.d + "/archive"
-        os.mkdir(self.archive, 0o755)
-        self.worker_cmd = ("teuthology-worker --tube openstack " +
-                           "-l " + self.logs + " "
-                           "--archive-dir " + self.archive + " ")
-        logging.info(self.worker_cmd)
-        self.worker = subprocess.Popen(self.worker_cmd,
-                                       stdout=subprocess.PIPE,
-                                       stderr=subprocess.PIPE,
-                                       shell=True)
-
-    def wait_worker(self):
-        if not self.worker:
-            return
-
-        (stdoutdata, stderrdata) = self.worker.communicate()
-        stdoutdata = stdoutdata.decode('utf-8')
-        stderrdata = stderrdata.decode('utf-8')
-        logging.info(self.worker_cmd + ":" +
-                     " stdout " + stdoutdata +
-                     " stderr " + stderrdata + " end ")
-        assert self.worker.returncode == 0
-        self.worker = None
-
-    def get_teuthology_log(self):
-        # the archive is removed before each test, there must
-        # be only one run and one job
-        run = os.listdir(self.archive)[0]
-        job = os.listdir(os.path.join(self.archive, run))[0]
-        path = os.path.join(self.archive, run, job, 'teuthology.log')
-        return open(path, 'r').read()
-
-class TestSuite(Integration):
-
-    def setup_method(self):
-        self.d = tempfile.mkdtemp()
-        self.setup_worker()
-        logging.info("TestSuite: done worker")
-
-    def teardown(self):
-        self.wait_worker()
-        shutil.rmtree(self.d)
-
-    def test_suite_noop(self):
-        cwd = os.getcwd()
-        os.mkdir(self.d + '/upload', 0o755)
-        upload = 'localhost:' + self.d + '/upload'
-        args = ['--suite', 'noop',
-                '--suite-dir', cwd + '/teuthology/openstack/test',
-                '--machine-type', 'openstack',
-                '--archive-upload', upload,
-                '--verbose']
-        logging.info("TestSuite:test_suite_noop")
-        scripts.suite.main(args)
-        self.wait_worker()
-        log = self.get_teuthology_log()
-        assert "teuthology.run:pass" in log
-        assert "Well done" in log
-        upload_key = teuth_config.archive_upload_key
-        if upload_key:
-            ssh = "RSYNC_RSH='ssh -i " + upload_key + "'"
-        else:
-            ssh = ''
-        assert 'teuthology.log' in teuthology.misc.sh(ssh + " rsync -av " + upload)
-
-class TestSchedule(Integration):
-
-    def setup_method(self):
-        self.d = tempfile.mkdtemp()
-        self.setup_worker()
-
-    def teardown(self):
-        self.wait_worker()
-        shutil.rmtree(self.d)
-
-    def test_schedule_stop_worker(self):
-        job = 'teuthology/openstack/test/stop_worker.yaml'
-        args = ['--name', 'fake',
-                '--verbose',
-                '--owner', 'test@test.com',
-                '--worker', 'openstack',
-                job]
-        scripts.schedule.main(args)
-        self.wait_worker()
-
-    def test_schedule_noop(self):
-        job = 'teuthology/openstack/test/noop.yaml'
-        args = ['--name', 'fake',
-                '--verbose',
-                '--owner', 'test@test.com',
-                '--worker', 'openstack',
-                job]
-        scripts.schedule.main(args)
-        self.wait_worker()
-        log = self.get_teuthology_log()
-        assert "teuthology.run:pass" in log
-        assert "Well done" in log
-
-    def test_schedule_resources_hint(self):
-        """It is tricky to test resources hint in a provider agnostic way. The
-        best way seems to ask for at least 1GB of RAM and 10GB
-        disk. Some providers do not offer a 1GB RAM flavor (OVH for
-        instance) and the 2GB RAM will be chosen instead. It however
-        seems unlikely that a 4GB RAM will be chosen because it would
-        mean such a provider has nothing under that limit and it's a
-        little too high.
-
-        Since the default when installing is to ask for 7000 MB, we
-        can reasonably assume that the hint has been taken into
-        account if the instance has less than 4GB RAM.
-        """
-        try:
-            teuthology.openstack.OpenStack().run("volume list")
-            job = 'teuthology/openstack/test/resources_hint.yaml'
-            has_cinder = True
-        except subprocess.CalledProcessError:
-            job = 'teuthology/openstack/test/resources_hint_no_cinder.yaml'
-            has_cinder = False
-        args = ['--name', 'fake',
-                '--verbose',
-                '--owner', 'test@test.com',
-                '--worker', 'openstack',
-                job]
-        scripts.schedule.main(args)
-        self.wait_worker()
-        log = self.get_teuthology_log()
-        assert "teuthology.run:pass" in log
-        assert "RAM size ok" in log
-        if has_cinder:
-            assert "Disk size ok" in log
-
-class TestLock(Integration):
-
-    def setup_method(self):
-        self.options = ['--verbose',
-                        '--machine-type', 'openstack' ]
-
-    def test_main(self):
-        args = scripts.lock.parse_args(self.options + ['--lock'])
-        assert teuthology.lock.cli.main(args) == 0
-
-    def test_lock_unlock(self):
-        default_archs = teuthology.openstack.OpenStack().get_available_archs()
-        if 'TEST_IMAGES' in os.environ:
-            images = os.environ['TEST_IMAGES'].split()
-        else:
-            images = teuthology.openstack.OpenStack.image2url.keys()
-        for image in images:
-            (os_type, os_version, arch) = image.split('-')
-            if arch not in default_archs:
-                logging.info("skipping " + image + " because arch " +
-                             " is not supported (" + str(default_archs) + ")")
-                continue
-            args = scripts.lock.parse_args(self.options +
-                                           ['--lock-many', '1',
-                                            '--os-type', os_type,
-                                            '--os-version', os_version,
-                                            '--arch', arch])
-            assert teuthology.lock.cli.main(args) == 0
-            locks = teuthology.lock.query.list_locks(locked=True)
-            assert len(locks) == 1
-            args = scripts.lock.parse_args(self.options +
-                                           ['--unlock', locks[0]['name']])
-            assert teuthology.lock.cli.main(args) == 0
-
-    def test_list(self, capsys):
-        args = scripts.lock.parse_args(self.options + ['--list', '--all'])
-        teuthology.lock.cli.main(args)
-        out, err = capsys.readouterr()
-        assert 'machine_type' in out
-        assert 'openstack' in out
diff --git a/teuthology/openstack/test/resources_hint.yaml b/teuthology/openstack/test/resources_hint.yaml
deleted file mode 100644 (file)
index b8f5959..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-stop_worker: true
-machine_type: openstack
-openstack:
-  - machine:
-     disk: 10 # GB
-     ram: 10000 # MB
-     cpus: 1
-    volumes:
-     count: 1
-     size: 2 # GB
-os_type: ubuntu
-os_version: "14.04"
-roles:
-- - mon.a
-  - osd.0
-tasks:
-- exec:
-    mon.a:
-      - test $(sed -n -e 's/MemTotal.* \([0-9][0-9]*\).*/\1/p' < /proc/meminfo) -ge 10000000 && echo "RAM" "size" "ok"
-      - cat /proc/meminfo
-# wait for the attached volume to show up
-      - for delay in 1 2 4 8 16 32 64 128 256 512 ; do if test -e /sys/block/vdb/size ; then break ; else sleep $delay ; fi ; done
-# 4000000 because 512 bytes sectors
-      - test $(cat /sys/block/vdb/size) -gt 4000000 && echo "Disk" "size" "ok"
-      - cat /sys/block/vdb/size
diff --git a/teuthology/openstack/test/resources_hint_no_cinder.yaml b/teuthology/openstack/test/resources_hint_no_cinder.yaml
deleted file mode 100644 (file)
index c603804..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-stop_worker: true
-machine_type: openstack
-openstack:
-  - machine:
-     disk: 10 # GB
-     ram: 10000 # MB
-     cpus: 1
-    volumes:
-     count: 0
-     size: 2 # GB
-os_type: ubuntu
-os_version: "14.04"
-roles:
-- - mon.a
-  - osd.0
-tasks:
-- exec:
-    mon.a:
-      - cat /proc/meminfo
-      - test $(sed -n -e 's/MemTotal.* \([0-9][0-9]*\).*/\1/p' < /proc/meminfo) -ge 10000000 && echo "RAM" "size" "ok"
diff --git a/teuthology/openstack/test/stop_worker.yaml b/teuthology/openstack/test/stop_worker.yaml
deleted file mode 100644 (file)
index 45133bb..0000000
+++ /dev/null
@@ -1 +0,0 @@
-stop_worker: true
diff --git a/teuthology/openstack/test/suites/noop/+ b/teuthology/openstack/test/suites/noop/+
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/teuthology/openstack/test/suites/noop/noop.yaml b/teuthology/openstack/test/suites/noop/noop.yaml
deleted file mode 100644 (file)
index 49497c2..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-stop_worker: true
-roles:
-- - mon.a
-  - osd.0
-tasks:
-- exec:
-    mon.a:
-      - echo "Well done !"
-
diff --git a/teuthology/openstack/test/suites/nuke/+ b/teuthology/openstack/test/suites/nuke/+
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/teuthology/openstack/test/test_config.py b/teuthology/openstack/test/test_config.py
deleted file mode 100644 (file)
index 530aea9..0000000
+++ /dev/null
@@ -1,35 +0,0 @@
-from teuthology.config import config
-
-
-class TestOpenStack(object):
-
-    def setup_method(self):
-        self.openstack_config = config['openstack']
-
-    def test_config_clone(self):
-        assert 'clone' in self.openstack_config
-
-    def test_config_user_data(self):
-        os_type = 'rhel'
-        os_version = '7.0'
-        template_path = self.openstack_config['user-data'].format(
-            os_type=os_type,
-            os_version=os_version)
-        assert os_type in template_path
-        assert os_version in template_path
-
-    def test_config_ip(self):
-        assert 'ip' in self.openstack_config
-
-    def test_config_machine(self):
-        assert 'machine' in self.openstack_config
-        machine_config = self.openstack_config['machine']
-        assert 'disk' in machine_config
-        assert 'ram' in machine_config
-        assert 'cpus' in machine_config
-
-    def test_config_volumes(self):
-        assert 'volumes' in self.openstack_config
-        volumes_config = self.openstack_config['volumes']
-        assert 'count' in volumes_config
-        assert 'size' in volumes_config
diff --git a/teuthology/openstack/test/test_openstack.py b/teuthology/openstack/test/test_openstack.py
deleted file mode 100644 (file)
index 1f34d82..0000000
+++ /dev/null
@@ -1,1695 +0,0 @@
-#
-# Copyright (c) 2015,2016 Red Hat, Inc.
-#
-# Author: Loic Dachary <loic@dachary.org>
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-# THE SOFTWARE.
-#
-import argparse
-import logging
-import os
-import pytest
-import subprocess
-import tempfile
-import time
-from mock import patch
-
-import teuthology
-from teuthology import misc
-from teuthology.config import set_config_attr
-from teuthology.openstack import TeuthologyOpenStack, OpenStack, OpenStackInstance
-from teuthology.openstack import NoFlavorException
-import scripts.openstack
-
-
-class TestOpenStackBase(object):
-
-    def setup_method(self):
-        OpenStack.token = None
-        OpenStack.token_expires = None
-        self.environ = {}
-        for k in os.environ.keys():
-            if k.startswith('OS_'):
-                self.environ[k] = os.environ[k]
-
-    def teardown_method(self):
-        OpenStack.token = None
-        OpenStack.token_expires = None
-        for k in os.environ.keys():
-            if k.startswith('OS_'):
-                if k in self.environ:
-                    os.environ[k] = self.environ[k]
-                else:
-                    del os.environ[k]
-
-class TestOpenStackInstance(TestOpenStackBase):
-
-    teuthology_instance = """
-{
-  "OS-EXT-STS:task_state": null,
-  "addresses": "Ext-Net=167.114.233.32",
-  "image": "Ubuntu 14.04 (0d315a8d-75e3-418a-80e4-48e62d599627)",
-  "OS-EXT-STS:vm_state": "active",
-  "OS-SRV-USG:launched_at": "2015-08-17T12:22:13.000000",
-  "flavor": "vps-ssd-1 (164fcc7e-7771-414f-a607-b388cb7b7aa0)",
-  "id": "f3ca32d7-212b-458b-a0d4-57d1085af953",
-  "security_groups": [
-    {
-      "name": "default"
-    }
-  ],
-  "user_id": "3a075820e5d24fda96cd340b87fd94e9",
-  "OS-DCF:diskConfig": "AUTO",
-  "accessIPv4": "",
-  "accessIPv6": "",
-  "progress": 0,
-  "OS-EXT-STS:power_state": 1,
-  "project_id": "62cf1be03cec403c8ed8e64df55732ea",
-  "config_drive": "",
-  "status": "ACTIVE",
-  "updated": "2015-11-03T13:48:53Z",
-  "hostId": "bcdf964b6f724e573c07156ff85b4db1707f6f0969f571cf33e0468d",
-  "OS-SRV-USG:terminated_at": null,
-  "key_name": "loic",
-  "properties": "",
-  "OS-EXT-AZ:availability_zone": "nova",
-  "name": "mrdarkdragon",
-  "created": "2015-08-17T12:21:31Z",
-  "os-extended-volumes:volumes_attached": [{"id": "627e2631-fbb3-48cd-b801-d29cd2a76f74"}, {"id": "09837649-0881-4ee2-a560-adabefc28764"}, {"id": "44e5175b-6044-40be-885a-c9ddfb6f75bb"}]
-}
-    """
-
-    teuthology_instance_no_addresses = """
-{
-  "OS-EXT-STS:task_state": null,
-  "addresses": "",
-  "image": "Ubuntu 14.04 (0d315a8d-75e3-418a-80e4-48e62d599627)",
-  "OS-EXT-STS:vm_state": "active",
-  "OS-SRV-USG:launched_at": "2015-08-17T12:22:13.000000",
-  "flavor": "vps-ssd-1 (164fcc7e-7771-414f-a607-b388cb7b7aa0)",
-  "id": "f3ca32d7-212b-458b-a0d4-57d1085af953",
-  "security_groups": [
-    {
-      "name": "default"
-    }
-  ],
-  "user_id": "3a075820e5d24fda96cd340b87fd94e9",
-  "OS-DCF:diskConfig": "AUTO",
-  "accessIPv4": "",
-  "accessIPv6": "",
-  "progress": 0,
-  "OS-EXT-STS:power_state": 1,
-  "project_id": "62cf1be03cec403c8ed8e64df55732ea",
-  "config_drive": "",
-  "status": "ACTIVE",
-  "updated": "2015-11-03T13:48:53Z",
-  "hostId": "bcdf964b6f724e573c07156ff85b4db1707f6f0969f571cf33e0468d",
-  "OS-SRV-USG:terminated_at": null,
-  "key_name": "loic",
-  "properties": "",
-  "OS-EXT-AZ:availability_zone": "nova",
-  "name": "mrdarkdragon",
-  "created": "2015-08-17T12:21:31Z",
-  "os-extended-volumes:volumes_attached": []
-}
-    """
-
-    @classmethod
-    def setup_class(self):
-        if 'OS_AUTH_URL' not in os.environ:
-            pytest.skip('no OS_AUTH_URL environment variable')
-
-    def test_init(self):
-        with patch.multiple(
-                misc,
-                sh=lambda cmd: self.teuthology_instance,
-        ):
-            o = OpenStackInstance('NAME')
-            assert o['id'] == 'f3ca32d7-212b-458b-a0d4-57d1085af953'
-        o = OpenStackInstance('NAME', {"id": "OTHER"})
-        assert o['id'] == "OTHER"
-
-    def test_get_created(self):
-        with patch.multiple(
-                misc,
-                sh=lambda cmd: self.teuthology_instance,
-        ):
-            o = OpenStackInstance('NAME')
-            assert o.get_created() > 0
-
-    def test_exists(self):
-        with patch.multiple(
-                misc,
-                sh=lambda cmd: self.teuthology_instance,
-        ):
-            o = OpenStackInstance('NAME')
-            assert o.exists()
-        def sh_raises(cmd):
-            raise subprocess.CalledProcessError('FAIL', 'BAD')
-        with patch.multiple(
-                misc,
-                sh=sh_raises,
-        ):
-            o = OpenStackInstance('NAME')
-            assert not o.exists()
-
-    def test_volumes(self):
-        with patch.multiple(
-                misc,
-                sh=lambda cmd: self.teuthology_instance,
-        ):
-            o = OpenStackInstance('NAME')
-            assert len(o.get_volumes()) == 3
-
-    def test_get_addresses(self):
-        answers = [
-            self.teuthology_instance_no_addresses,
-            self.teuthology_instance,
-        ]
-        def sh(self):
-            return answers.pop(0)
-        with patch.multiple(
-                misc,
-                sh=sh,
-        ):
-            o = OpenStackInstance('NAME')
-            assert o.get_addresses() == 'Ext-Net=167.114.233.32'
-
-    def test_get_ip_neutron(self):
-        instance_id = '8e1fd70a-3065-46f8-9c30-84dc028c1834'
-        ip = '10.10.10.4'
-        def sh(cmd):
-            if 'neutron subnet-list' in cmd:
-                return """
-[
-  {
-    "ip_version": 6,
-    "id": "c45b9661-b2ba-4817-9e3a-f8f63bf32989"
-  },
-  {
-    "ip_version": 4,
-    "id": "e03a3dbc-afc8-4b52-952e-7bf755397b50"
-  }
-]
-                """
-            elif 'neutron port-list' in cmd:
-                return ("""
-[
-  {
-    "device_id": "915504ad-368b-4cce-be7c-4f8a83902e28",
-    "fixed_ips": "{\\"subnet_id\\": \\"e03a3dbc-afc8-4b52-952e-7bf755397b50\\", \\"ip_address\\": \\"10.10.10.1\\"}\\n{\\"subnet_id\\": \\"c45b9661-b2ba-4817-9e3a-f8f63bf32989\\", \\"ip_address\\": \\"2607:f298:6050:9afc::1\\"}"
-  },
-  {
-    "device_id": "{instance_id}",
-    "fixed_ips": "{\\"subnet_id\\": \\"e03a3dbc-afc8-4b52-952e-7bf755397b50\\", \\"ip_address\\": \\"{ip}\\"}\\n{\\"subnet_id\\": \\"c45b9661-b2ba-4817-9e3a-f8f63bf32989\\", \\"ip_address\\": \\"2607:f298:6050:9afc:f816:3eff:fe07:76c1\\"}"
-  },
-  {
-    "device_id": "17e4a968-4caa-4cee-8e4b-f950683a02bd",
-    "fixed_ips": "{\\"subnet_id\\": \\"e03a3dbc-afc8-4b52-952e-7bf755397b50\\", \\"ip_address\\": \\"10.10.10.5\\"}\\n{\\"subnet_id\\": \\"c45b9661-b2ba-4817-9e3a-f8f63bf32989\\", \\"ip_address\\": \\"2607:f298:6050:9afc:f816:3eff:fe9c:37f0\\"}"
-  }
-]
-                """.replace('{instance_id}', instance_id).
-                        replace('{ip}', ip))
-            else:
-                raise Exception("unexpected " + cmd)
-        with patch.multiple(
-                misc,
-                sh=sh,
-        ):
-            assert ip == OpenStackInstance(
-                instance_id,
-                { 'id': instance_id },
-            ).get_ip_neutron()
-
-class TestOpenStack(TestOpenStackBase):
-
-    flavors = """[
-  {
-    "Name": "eg-120-ssd",
-    "RAM": 120000,
-    "Ephemeral": 0,
-    "VCPUs": 32,
-    "Is Public": true,
-    "Disk": 800,
-    "ID": "008f75de-c467-4d15-8f70-79c8fbe19538"
-  },
-  {
-    "Name": "hg-60",
-    "RAM": 60000,
-    "Ephemeral": 0,
-    "VCPUs": 16,
-    "Is Public": true,
-    "Disk": 1600,
-    "ID": "0297d7ac-fe6f-4ff1-b6e7-0b8b0908c94f"
-  },
-  {
-    "Name": "win-sp-120-ssd-flex",
-    "RAM": 120000,
-    "Ephemeral": 0,
-    "VCPUs": 8,
-    "Is Public": true,
-    "Disk": 50,
-    "ID": "039e31f2-6541-46c8-85cf-7f47fab7ad78"
-  },
-  {
-    "Name": "win-sp-60",
-    "RAM": 60000,
-    "Ephemeral": 0,
-    "VCPUs": 4,
-    "Is Public": true,
-    "Disk": 400,
-    "ID": "0417a0e6-f68a-4b8b-a642-ca5ecb9652f7"
-  },
-  {
-    "Name": "hg-120-ssd",
-    "RAM": 120000,
-    "Ephemeral": 0,
-    "VCPUs": 32,
-    "Is Public": true,
-    "Disk": 800,
-    "ID": "042aefc6-b713-4a7e-ada5-3ff81daa1960"
-  },
-  {
-    "Name": "win-sp-60-flex",
-    "RAM": 60000,
-    "Ephemeral": 0,
-    "VCPUs": 4,
-    "Is Public": true,
-    "Disk": 50,
-    "ID": "0609290c-ad2a-40f0-8c66-c755dd38fe3f"
-  },
-  {
-    "Name": "win-eg-120",
-    "RAM": 120000,
-    "Ephemeral": 0,
-    "VCPUs": 32,
-    "Is Public": true,
-    "Disk": 800,
-    "ID": "0651080f-5d07-44b1-a759-7ea4594b669e"
-  },
-  {
-    "Name": "win-sp-240",
-    "RAM": 240000,
-    "Ephemeral": 0,
-    "VCPUs": 16,
-    "Is Public": true,
-    "Disk": 1600,
-    "ID": "07885848-8831-486d-8525-91484c09cc7e"
-  },
-  {
-    "Name": "win-hg-60-ssd",
-    "RAM": 60000,
-    "Ephemeral": 0,
-    "VCPUs": 16,
-    "Is Public": true,
-    "Disk": 800,
-    "ID": "079aa0a2-5e48-4e58-8205-719bc962736e"
-  },
-  {
-    "Name": "eg-120",
-    "RAM": 120000,
-    "Ephemeral": 0,
-    "VCPUs": 32,
-    "Is Public": true,
-    "Disk": 1600,
-    "ID": "090f8b8c-673c-4ab8-9a07-6e54a8776e7b"
-  },
-  {
-    "Name": "win-hg-15-ssd-flex",
-    "RAM": 15000,
-    "Ephemeral": 0,
-    "VCPUs": 4,
-    "Is Public": true,
-    "Disk": 50,
-    "ID": "10e10c58-d29f-4ff6-a1fd-085c35a3bd9b"
-  },
-  {
-    "Name": "eg-15-ssd",
-    "RAM": 15000,
-    "Ephemeral": 0,
-    "VCPUs": 4,
-    "Is Public": true,
-    "Disk": 200,
-    "ID": "1340a920-0f2f-4c1b-8d74-e2502258da73"
-  },
-  {
-    "Name": "win-eg-30-ssd-flex",
-    "RAM": 30000,
-    "Ephemeral": 0,
-    "VCPUs": 8,
-    "Is Public": true,
-    "Disk": 50,
-    "ID": "13e54752-fbd0-47a6-aa93-e5a67dfbc743"
-  },
-  {
-    "Name": "eg-120-ssd-flex",
-    "RAM": 120000,
-    "Ephemeral": 0,
-    "VCPUs": 32,
-    "Is Public": true,
-    "Disk": 50,
-    "ID": "15c07a54-2dfb-41d9-aa73-6989fd8cafc2"
-  },
-  {
-    "Name": "win-eg-120-ssd-flex",
-    "RAM": 120000,
-    "Ephemeral": 0,
-    "VCPUs": 32,
-    "Is Public": true,
-    "Disk": 50,
-    "ID": "15e0dfcc-10f4-4e70-8ac1-30bc323273e2"
-  },
-  {
-    "Name": "vps-ssd-1",
-    "RAM": 2000,
-    "Ephemeral": 0,
-    "VCPUs": 1,
-    "Is Public": true,
-    "Disk": 10,
-    "ID": "164fcc7e-7771-414f-a607-b388cb7b7aa0"
-  },
-  {
-    "Name": "win-sp-120-flex",
-    "RAM": 120000,
-    "Ephemeral": 0,
-    "VCPUs": 8,
-    "Is Public": true,
-    "Disk": 50,
-    "ID": "169415e1-0979-4527-94fb-638c885bbd8c"
-  },
-  {
-    "Name": "win-hg-60-flex",
-    "RAM": 60000,
-    "Ephemeral": 0,
-    "VCPUs": 16,
-    "Is Public": true,
-    "Disk": 50,
-    "ID": "16f13d5b-be27-4b8b-88da-959d3904d3ba"
-  },
-  {
-    "Name": "win-sp-30-ssd",
-    "RAM": 30000,
-    "Ephemeral": 0,
-    "VCPUs": 2,
-    "Is Public": true,
-    "Disk": 100,
-    "ID": "1788102b-ab80-4a0c-b819-541deaca7515"
-  },
-  {
-    "Name": "win-sp-240-flex",
-    "RAM": 240000,
-    "Ephemeral": 0,
-    "VCPUs": 16,
-    "Is Public": true,
-    "Disk": 50,
-    "ID": "17bcfa14-135f-442f-9397-a4dc25265560"
-  },
-  {
-    "Name": "win-eg-60-ssd-flex",
-    "RAM": 60000,
-    "Ephemeral": 0,
-    "VCPUs": 16,
-    "Is Public": true,
-    "Disk": 50,
-    "ID": "194ca9ba-04af-4d86-ba37-d7da883a7eab"
-  },
-  {
-    "Name": "win-eg-60-flex",
-    "RAM": 60000,
-    "Ephemeral": 0,
-    "VCPUs": 16,
-    "Is Public": true,
-    "Disk": 50,
-    "ID": "19ff8837-4751-4f6c-a82b-290bc53c83c1"
-  },
-  {
-    "Name": "win-eg-30-flex",
-    "RAM": 30000,
-    "Ephemeral": 0,
-    "VCPUs": 8,
-    "Is Public": true,
-    "Disk": 50,
-    "ID": "1aaef5e5-4df9-4462-80d3-701683ab9ff0"
-  },
-  {
-    "Name": "eg-15",
-    "RAM": 15000,
-    "Ephemeral": 0,
-    "VCPUs": 4,
-    "Is Public": true,
-    "Disk": 400,
-    "ID": "1cd85b81-5e4d-477a-a127-eb496b1d75de"
-  },
-  {
-    "Name": "hg-120",
-    "RAM": 120000,
-    "Ephemeral": 0,
-    "VCPUs": 32,
-    "Is Public": true,
-    "Disk": 1600,
-    "ID": "1f1efedf-ec91-4a42-acd7-f5cf64b02d3c"
-  },
-  {
-    "Name": "hg-15-ssd-flex",
-    "RAM": 15000,
-    "Ephemeral": 0,
-    "VCPUs": 4,
-    "Is Public": true,
-    "Disk": 50,
-    "ID": "20347a07-a289-4c07-a645-93cb5e8e2d30"
-  },
-  {
-    "Name": "win-eg-7-ssd",
-    "RAM": 7000,
-    "Ephemeral": 0,
-    "VCPUs": 2,
-    "Is Public": true,
-    "Disk": 100,
-    "ID": "20689394-bd77-4f4d-900e-52cc8a86aeb4"
-  },
-  {
-    "Name": "win-sp-60-ssd-flex",
-    "RAM": 60000,
-    "Ephemeral": 0,
-    "VCPUs": 4,
-    "Is Public": true,
-    "Disk": 50,
-    "ID": "21104d99-ba7b-47a0-9133-7e884710089b"
-  },
-  {
-    "Name": "win-sp-120-ssd",
-    "RAM": 120000,
-    "Ephemeral": 0,
-    "VCPUs": 8,
-    "Is Public": true,
-    "Disk": 400,
-    "ID": "23c21ecc-9ee8-4ad3-bd9f-aa17a3faf84e"
-  },
-  {
-    "Name": "win-hg-15-ssd",
-    "RAM": 15000,
-    "Ephemeral": 0,
-    "VCPUs": 4,
-    "Is Public": true,
-    "Disk": 200,
-    "ID": "24e293ed-bc54-4f26-8fb7-7b9457d08e66"
-  },
-  {
-    "Name": "eg-15-ssd-flex",
-    "RAM": 15000,
-    "Ephemeral": 0,
-    "VCPUs": 4,
-    "Is Public": true,
-    "Disk": 50,
-    "ID": "25f3534a-89e5-489d-aa8b-63f62e76875b"
-  },
-  {
-    "Name": "win-eg-60",
-    "RAM": 60000,
-    "Ephemeral": 0,
-    "VCPUs": 16,
-    "Is Public": true,
-    "Disk": 800,
-    "ID": "291173f1-ea1d-410b-8045-667361a4addb"
-  },
-  {
-    "Name": "sp-30-ssd-flex",
-    "RAM": 30000,
-    "Ephemeral": 0,
-    "VCPUs": 2,
-    "Is Public": true,
-    "Disk": 50,
-    "ID": "2b646463-2efa-428b-94ed-4059923c3636"
-  },
-  {
-    "Name": "win-eg-120-flex",
-    "RAM": 120000,
-    "Ephemeral": 0,
-    "VCPUs": 32,
-    "Is Public": true,
-    "Disk": 50,
-    "ID": "2c74df82-29d2-4b1a-a32c-d5633e7359b4"
-  },
-  {
-    "Name": "win-eg-15-ssd",
-    "RAM": 15000,
-    "Ephemeral": 0,
-    "VCPUs": 4,
-    "Is Public": true,
-    "Disk": 200,
-    "ID": "2fe4344f-d701-4bc4-8dcd-6d0b5d83fa13"
-  },
-  {
-    "Name": "sp-30-flex",
-    "RAM": 30000,
-    "Ephemeral": 0,
-    "VCPUs": 2,
-    "Is Public": true,
-    "Disk": 50,
-    "ID": "31487b30-eeb6-472f-a9b6-38ace6587ebc"
-  },
-  {
-    "Name": "win-sp-240-ssd",
-    "RAM": 240000,
-    "Ephemeral": 0,
-    "VCPUs": 16,
-    "Is Public": true,
-    "Disk": 800,
-    "ID": "325b602f-ecc4-4444-90bd-5a2cf4e0da53"
-  },
-  {
-    "Name": "win-hg-7",
-    "RAM": 7000,
-    "Ephemeral": 0,
-    "VCPUs": 2,
-    "Is Public": true,
-    "Disk": 200,
-    "ID": "377ded36-491f-4ad7-9eb4-876798b2aea9"
-  },
-  {
-    "Name": "sp-30-ssd",
-    "RAM": 30000,
-    "Ephemeral": 0,
-    "VCPUs": 2,
-    "Is Public": true,
-    "Disk": 100,
-    "ID": "382f2831-4dba-40c4-bb7a-6fadff71c4db"
-  },
-  {
-    "Name": "hg-30",
-    "RAM": 30000,
-    "Ephemeral": 0,
-    "VCPUs": 8,
-    "Is Public": true,
-    "Disk": 800,
-    "ID": "3c1d6170-0097-4b5c-a3b3-adff1b7a86e0"
-  },
-  {
-    "Name": "hg-60-flex",
-    "RAM": 60000,
-    "Ephemeral": 0,
-    "VCPUs": 16,
-    "Is Public": true,
-    "Disk": 50,
-    "ID": "3c669730-b5cd-4e44-8bd2-bc8d9f984ab2"
-  },
-  {
-    "Name": "sp-240-ssd-flex",
-    "RAM": 240000,
-    "Ephemeral": 0,
-    "VCPUs": 16,
-    "Is Public": true,
-    "Disk": 50,
-    "ID": "3d66fea3-26f2-4195-97ab-fdea3b836099"
-  },
-  {
-    "Name": "sp-240-flex",
-    "RAM": 240000,
-    "Ephemeral": 0,
-    "VCPUs": 16,
-    "Is Public": true,
-    "Disk": 50,
-    "ID": "40c781f7-d7a7-4b0d-bcca-5304aeabbcd9"
-  },
-  {
-    "Name": "hg-7-flex",
-    "RAM": 7000,
-    "Ephemeral": 0,
-    "VCPUs": 2,
-    "Is Public": true,
-    "Disk": 50,
-    "ID": "42730e52-147d-46b8-9546-18e31e5ac8a9"
-  },
-  {
-    "Name": "eg-30-ssd",
-    "RAM": 30000,
-    "Ephemeral": 0,
-    "VCPUs": 8,
-    "Is Public": true,
-    "Disk": 400,
-    "ID": "463f30e9-7d7a-4693-944f-142067cf553b"
-  },
-  {
-    "Name": "hg-15-flex",
-    "RAM": 15000,
-    "Ephemeral": 0,
-    "VCPUs": 4,
-    "Is Public": true,
-    "Disk": 50,
-    "ID": "534f07c6-91af-44c8-9e62-156360fe8359"
-  },
-  {
-    "Name": "win-sp-30-flex",
-    "RAM": 30000,
-    "Ephemeral": 0,
-    "VCPUs": 2,
-    "Is Public": true,
-    "Disk": 50,
-    "ID": "55533fdf-ad57-4aa7-a2c6-ee31bb94e77b"
-  },
-  {
-    "Name": "win-hg-60-ssd-flex",
-    "RAM": 60000,
-    "Ephemeral": 0,
-    "VCPUs": 16,
-    "Is Public": true,
-    "Disk": 50,
-    "ID": "58b24234-3804-4c4f-9eb6-5406a3a13758"
-  },
-  {
-    "Name": "hg-7-ssd-flex",
-    "RAM": 7000,
-    "Ephemeral": 0,
-    "VCPUs": 2,
-    "Is Public": true,
-    "Disk": 50,
-    "ID": "596c1276-8e53-40a0-b183-cdd9e9b1907d"
-  },
-  {
-    "Name": "win-hg-30-ssd",
-    "RAM": 30000,
-    "Ephemeral": 0,
-    "VCPUs": 8,
-    "Is Public": true,
-    "Disk": 400,
-    "ID": "5c54dc08-28b9-4860-9f24-a2451b2a28ec"
-  },
-  {
-    "Name": "eg-7",
-    "RAM": 7000,
-    "Ephemeral": 0,
-    "VCPUs": 2,
-    "Is Public": true,
-    "Disk": 200,
-    "ID": "5e409dbc-3f4b-46e8-a629-a418c8497922"
-  },
-  {
-    "Name": "hg-30-flex",
-    "RAM": 30000,
-    "Ephemeral": 0,
-    "VCPUs": 8,
-    "Is Public": true,
-    "Disk": 50,
-    "ID": "656423ea-0551-48c6-9e0f-ec6e15952029"
-  },
-  {
-    "Name": "hg-15",
-    "RAM": 15000,
-    "Ephemeral": 0,
-    "VCPUs": 4,
-    "Is Public": true,
-    "Disk": 400,
-    "ID": "675558ea-04fe-47a2-83de-40be9b2eacd4"
-  },
-  {
-    "Name": "eg-60-flex",
-    "RAM": 60000,
-    "Ephemeral": 0,
-    "VCPUs": 16,
-    "Is Public": true,
-    "Disk": 50,
-    "ID": "68a8e4e1-d291-46e8-a724-fbb1c4b9b051"
-  },
-  {
-    "Name": "hg-30-ssd",
-    "RAM": 30000,
-    "Ephemeral": 0,
-    "VCPUs": 8,
-    "Is Public": true,
-    "Disk": 400,
-    "ID": "6ab72807-e0a5-4e9f-bbb9-7cbbf0038b26"
-  },
-  {
-    "Name": "win-hg-30",
-    "RAM": 30000,
-    "Ephemeral": 0,
-    "VCPUs": 8,
-    "Is Public": true,
-    "Disk": 800,
-    "ID": "6e12cae3-0492-483c-aa39-54a0dcaf86dd"
-  },
-  {
-    "Name": "win-hg-7-ssd",
-    "RAM": 7000,
-    "Ephemeral": 0,
-    "VCPUs": 2,
-    "Is Public": true,
-    "Disk": 100,
-    "ID": "6ead771c-e8b9-424c-afa0-671280416422"
-  },
-  {
-    "Name": "win-hg-30-flex",
-    "RAM": 30000,
-    "Ephemeral": 0,
-    "VCPUs": 8,
-    "Is Public": true,
-    "Disk": 50,
-    "ID": "70ded741-8f58-4bb9-8cfd-5e838b66b5f3"
-  },
-  {
-    "Name": "win-sp-30-ssd-flex",
-    "RAM": 30000,
-    "Ephemeral": 0,
-    "VCPUs": 2,
-    "Is Public": true,
-    "Disk": 50,
-    "ID": "7284d104-a260-421d-8cee-6dc905107b25"
-  },
-  {
-    "Name": "win-eg-120-ssd",
-    "RAM": 120000,
-    "Ephemeral": 0,
-    "VCPUs": 32,
-    "Is Public": true,
-    "Disk": 800,
-    "ID": "72c0b262-855d-40bb-a3e9-fd989a1bc466"
-  },
-  {
-    "Name": "win-hg-7-flex",
-    "RAM": 7000,
-    "Ephemeral": 0,
-    "VCPUs": 2,
-    "Is Public": true,
-    "Disk": 50,
-    "ID": "73961591-c5f1-436f-b641-1a506eddaef4"
-  },
-  {
-    "Name": "sp-240-ssd",
-    "RAM": 240000,
-    "Ephemeral": 0,
-    "VCPUs": 16,
-    "Is Public": true,
-    "Disk": 800,
-    "ID": "7568d834-3b16-42ce-a2c1-0654e0781160"
-  },
-  {
-    "Name": "win-eg-60-ssd",
-    "RAM": 60000,
-    "Ephemeral": 0,
-    "VCPUs": 16,
-    "Is Public": true,
-    "Disk": 800,
-    "ID": "75f7fe5c-f87a-41d8-a961-a0169d02c268"
-  },
-  {
-    "Name": "eg-7-ssd-flex",
-    "RAM": 7000,
-    "Ephemeral": 0,
-    "VCPUs": 2,
-    "Is Public": true,
-    "Disk": 50,
-    "ID": "77e1db73-0b36-4e37-8e47-32c2d2437ca9"
-  },
-  {
-    "Name": "eg-60-ssd-flex",
-    "RAM": 60000,
-    "Ephemeral": 0,
-    "VCPUs": 16,
-    "Is Public": true,
-    "Disk": 50,
-    "ID": "78df4e30-98ca-4362-af68-037d958edaf0"
-  },
-  {
-    "Name": "vps-ssd-2",
-    "RAM": 4000,
-    "Ephemeral": 0,
-    "VCPUs": 1,
-    "Is Public": true,
-    "Disk": 20,
-    "ID": "7939cc5c-79b1-45c0-be2d-aa935d92faa1"
-  },
-  {
-    "Name": "sp-60",
-    "RAM": 60000,
-    "Ephemeral": 0,
-    "VCPUs": 4,
-    "Is Public": true,
-    "Disk": 400,
-    "ID": "80d8510a-79cc-4307-8db7-d1965c9e8ddb"
-  },
-  {
-    "Name": "win-hg-120-ssd-flex",
-    "RAM": 120000,
-    "Ephemeral": 0,
-    "VCPUs": 32,
-    "Is Public": true,
-    "Disk": 50,
-    "ID": "835e734a-46b6-4cb2-be68-e8678fd71059"
-  },
-  {
-    "Name": "win-eg-7",
-    "RAM": 7000,
-    "Ephemeral": 0,
-    "VCPUs": 2,
-    "Is Public": true,
-    "Disk": 200,
-    "ID": "84869b00-b43a-4523-babd-d47d206694e9"
-  },
-  {
-    "Name": "win-eg-7-ssd-flex",
-    "RAM": 7000,
-    "Ephemeral": 0,
-    "VCPUs": 2,
-    "Is Public": true,
-    "Disk": 50,
-    "ID": "852308f8-b8bf-44a4-af41-cbc27437b275"
-  },
-  {
-    "Name": "win-sp-30",
-    "RAM": 30000,
-    "Ephemeral": 0,
-    "VCPUs": 2,
-    "Is Public": true,
-    "Disk": 200,
-    "ID": "8be9dc29-3eca-499b-ae2d-e3c99699131a"
-  },
-  {
-    "Name": "win-hg-7-ssd-flex",
-    "RAM": 7000,
-    "Ephemeral": 0,
-    "VCPUs": 2,
-    "Is Public": true,
-    "Disk": 50,
-    "ID": "8d704cfd-05b2-4d4a-add2-e2868bcc081f"
-  },
-  {
-    "Name": "eg-30",
-    "RAM": 30000,
-    "Ephemeral": 0,
-    "VCPUs": 8,
-    "Is Public": true,
-    "Disk": 800,
-    "ID": "901f77c2-73f6-4fae-b28a-18b829b55a17"
-  },
-  {
-    "Name": "sp-60-ssd-flex",
-    "RAM": 60000,
-    "Ephemeral": 0,
-    "VCPUs": 4,
-    "Is Public": true,
-    "Disk": 50,
-    "ID": "944b92fb-9a0c-406d-bb9f-a1d93cda9f01"
-  },
-  {
-    "Name": "eg-30-flex",
-    "RAM": 30000,
-    "Ephemeral": 0,
-    "VCPUs": 8,
-    "Is Public": true,
-    "Disk": 50,
-    "ID": "965472c7-eb54-4d4d-bd6e-01ebb694a631"
-  },
-  {
-    "Name": "sp-120-ssd",
-    "RAM": 120000,
-    "Ephemeral": 0,
-    "VCPUs": 8,
-    "Is Public": true,
-    "Disk": 400,
-    "ID": "97824a8c-e683-49a8-a70a-ead64240395c"
-  },
-  {
-    "Name": "hg-60-ssd",
-    "RAM": 60000,
-    "Ephemeral": 0,
-    "VCPUs": 16,
-    "Is Public": true,
-    "Disk": 800,
-    "ID": "9831d7f1-3e79-483d-8958-88e3952c7ea2"
-  },
-  {
-    "Name": "eg-60",
-    "RAM": 60000,
-    "Ephemeral": 0,
-    "VCPUs": 16,
-    "Is Public": true,
-    "Disk": 1600,
-    "ID": "9e1f13d0-4fcc-4abc-a9e6-9c76d662c92d"
-  },
-  {
-    "Name": "win-eg-30-ssd",
-    "RAM": 30000,
-    "Ephemeral": 0,
-    "VCPUs": 8,
-    "Is Public": true,
-    "Disk": 400,
-    "ID": "9e6b85fa-6f37-45ce-a3d6-11ab40a28fad"
-  },
-  {
-    "Name": "hg-120-flex",
-    "RAM": 120000,
-    "Ephemeral": 0,
-    "VCPUs": 32,
-    "Is Public": true,
-    "Disk": 50,
-    "ID": "9ed787cc-a0db-400b-8cc1-49b6384a1000"
-  },
-  {
-    "Name": "sp-120-flex",
-    "RAM": 120000,
-    "Ephemeral": 0,
-    "VCPUs": 8,
-    "Is Public": true,
-    "Disk": 50,
-    "ID": "9f3cfdf7-b850-47cc-92be-33aefbfd2b05"
-  },
-  {
-    "Name": "hg-60-ssd-flex",
-    "RAM": 60000,
-    "Ephemeral": 0,
-    "VCPUs": 16,
-    "Is Public": true,
-    "Disk": 50,
-    "ID": "a37bdf17-e1b1-41cc-a67f-ed665a120446"
-  },
-  {
-    "Name": "win-hg-120-ssd",
-    "RAM": 120000,
-    "Ephemeral": 0,
-    "VCPUs": 32,
-    "Is Public": true,
-    "Disk": 800,
-    "ID": "aa753e73-dadb-4528-9c4a-24e36fc41bf4"
-  },
-  {
-    "Name": "win-sp-240-ssd-flex",
-    "RAM": 240000,
-    "Ephemeral": 0,
-    "VCPUs": 16,
-    "Is Public": true,
-    "Disk": 50,
-    "ID": "abc007b8-cc44-4b6b-9606-fd647b03e101"
-  },
-  {
-    "Name": "sp-120",
-    "RAM": 120000,
-    "Ephemeral": 0,
-    "VCPUs": 8,
-    "Is Public": true,
-    "Disk": 800,
-    "ID": "ac74cb45-d895-47dd-b9cf-c17778033d83"
-  },
-  {
-    "Name": "win-hg-15",
-    "RAM": 15000,
-    "Ephemeral": 0,
-    "VCPUs": 4,
-    "Is Public": true,
-    "Disk": 400,
-    "ID": "ae900175-72bd-4fbc-8ab2-4673b468aa5b"
-  },
-  {
-    "Name": "win-eg-15-ssd-flex",
-    "RAM": 15000,
-    "Ephemeral": 0,
-    "VCPUs": 4,
-    "Is Public": true,
-    "Disk": 50,
-    "ID": "aeb37dbf-d7c9-4fd7-93f1-f3818e488ede"
-  },
-  {
-    "Name": "hg-7-ssd",
-    "RAM": 7000,
-    "Ephemeral": 0,
-    "VCPUs": 2,
-    "Is Public": true,
-    "Disk": 100,
-    "ID": "b1dc776c-b6e3-4a96-b230-850f570db3d5"
-  },
-  {
-    "Name": "sp-60-ssd",
-    "RAM": 60000,
-    "Ephemeral": 0,
-    "VCPUs": 4,
-    "Is Public": true,
-    "Disk": 200,
-    "ID": "b24df495-10f3-466e-95ab-26f0f6839a2f"
-  },
-  {
-    "Name": "win-hg-120",
-    "RAM": 120000,
-    "Ephemeral": 0,
-    "VCPUs": 32,
-    "Is Public": true,
-    "Disk": 1600,
-    "ID": "b798e44e-bf71-488c-9335-f20bf5976547"
-  },
-  {
-    "Name": "eg-7-ssd",
-    "RAM": 7000,
-    "Ephemeral": 0,
-    "VCPUs": 2,
-    "Is Public": true,
-    "Disk": 100,
-    "ID": "b94e6623-913d-4147-b2a3-34ccf6fe7a5e"
-  },
-  {
-    "Name": "eg-15-flex",
-    "RAM": 15000,
-    "Ephemeral": 0,
-    "VCPUs": 4,
-    "Is Public": true,
-    "Disk": 50,
-    "ID": "bb5fdda8-34ec-40c8-a4e3-308b9e2c9ee2"
-  },
-  {
-    "Name": "win-eg-7-flex",
-    "RAM": 7000,
-    "Ephemeral": 0,
-    "VCPUs": 2,
-    "Is Public": true,
-    "Disk": 50,
-    "ID": "c65384f6-4665-461a-a292-2f3f5a016244"
-  },
-  {
-    "Name": "eg-60-ssd",
-    "RAM": 60000,
-    "Ephemeral": 0,
-    "VCPUs": 16,
-    "Is Public": true,
-    "Disk": 800,
-    "ID": "c678f1a8-6542-4f9d-89af-ffc98715d674"
-  },
-  {
-    "Name": "hg-30-ssd-flex",
-    "RAM": 30000,
-    "Ephemeral": 0,
-    "VCPUs": 8,
-    "Is Public": true,
-    "Disk": 50,
-    "ID": "d147a094-b653-41e7-9250-8d4da3044334"
-  },
-  {
-    "Name": "sp-30",
-    "RAM": 30000,
-    "Ephemeral": 0,
-    "VCPUs": 2,
-    "Is Public": true,
-    "Disk": 200,
-    "ID": "d1acf88d-6f55-4c5c-a914-4ecbdbd50d6b"
-  },
-  {
-    "Name": "sp-120-ssd-flex",
-    "RAM": 120000,
-    "Ephemeral": 0,
-    "VCPUs": 8,
-    "Is Public": true,
-    "Disk": 50,
-    "ID": "d2d33e8e-58b1-4661-8141-826c47f82166"
-  },
-  {
-    "Name": "hg-120-ssd-flex",
-    "RAM": 120000,
-    "Ephemeral": 0,
-    "VCPUs": 32,
-    "Is Public": true,
-    "Disk": 50,
-    "ID": "d7322c37-9881-4a57-9b40-2499fe2e8f42"
-  },
-  {
-    "Name": "win-hg-15-flex",
-    "RAM": 15000,
-    "Ephemeral": 0,
-    "VCPUs": 4,
-    "Is Public": true,
-    "Disk": 50,
-    "ID": "daf597ea-fbbc-4c71-a35e-5b41d33ccc6c"
-  },
-  {
-    "Name": "win-hg-30-ssd-flex",
-    "RAM": 30000,
-    "Ephemeral": 0,
-    "VCPUs": 8,
-    "Is Public": true,
-    "Disk": 50,
-    "ID": "dcfd834c-3932-47a3-8b4b-cdfeecdfde2c"
-  },
-  {
-    "Name": "win-hg-60",
-    "RAM": 60000,
-    "Ephemeral": 0,
-    "VCPUs": 16,
-    "Is Public": true,
-    "Disk": 1600,
-    "ID": "def75cbd-a4b1-4f82-9152-90c65df9587b"
-  },
-  {
-    "Name": "eg-30-ssd-flex",
-    "RAM": 30000,
-    "Ephemeral": 0,
-    "VCPUs": 8,
-    "Is Public": true,
-    "Disk": 50,
-    "ID": "e04c7ad6-a5de-45f5-93c9-f3343bdfe8d1"
-  },
-  {
-    "Name": "vps-ssd-3",
-    "RAM": 8000,
-    "Ephemeral": 0,
-    "VCPUs": 2,
-    "Is Public": true,
-    "Disk": 40,
-    "ID": "e43d7458-6b82-4a78-a712-3a4dc6748cf4"
-  },
-  {
-    "Name": "win-eg-15-flex",
-    "RAM": 15000,
-    "Ephemeral": 0,
-    "VCPUs": 4,
-    "Is Public": true,
-    "Disk": 50,
-    "ID": "e8bd3402-7310-4a0f-8b99-d9212359c957"
-  },
-  {
-    "Name": "win-eg-30",
-    "RAM": 30000,
-    "Ephemeral": 0,
-    "VCPUs": 8,
-    "Is Public": true,
-    "Disk": 800,
-    "ID": "ebf7a997-e2f8-42f4-84f7-33a3d53d1af9"
-  },
-  {
-    "Name": "eg-120-flex",
-    "RAM": 120000,
-    "Ephemeral": 0,
-    "VCPUs": 32,
-    "Is Public": true,
-    "Disk": 50,
-    "ID": "ec852ed3-1e42-4c59-abc3-12bcd26abec8"
-  },
-  {
-    "Name": "sp-240",
-    "RAM": 240000,
-    "Ephemeral": 0,
-    "VCPUs": 16,
-    "Is Public": true,
-    "Disk": 1600,
-    "ID": "ed286e2c-769f-4c47-ac52-b8de7a4891f6"
-  },
-  {
-    "Name": "win-sp-60-ssd",
-    "RAM": 60000,
-    "Ephemeral": 0,
-    "VCPUs": 4,
-    "Is Public": true,
-    "Disk": 200,
-    "ID": "ed835a73-d9a0-43ee-bd89-999c51d8426d"
-  },
-  {
-    "Name": "win-eg-15",
-    "RAM": 15000,
-    "Ephemeral": 0,
-    "VCPUs": 4,
-    "Is Public": true,
-    "Disk": 400,
-    "ID": "f06056c1-a2d4-40e7-a7d8-e5bfabada72e"
-  },
-  {
-    "Name": "win-sp-120",
-    "RAM": 120000,
-    "Ephemeral": 0,
-    "VCPUs": 8,
-    "Is Public": true,
-    "Disk": 800,
-    "ID": "f247dc56-395b-49de-9a62-93ccc4fff4ed"
-  },
-  {
-    "Name": "eg-7-flex",
-    "RAM": 7000,
-    "Ephemeral": 0,
-    "VCPUs": 2,
-    "Is Public": true,
-    "Disk": 50,
-    "ID": "f476f959-ffa6-46f2-94d8-72293570604d"
-  },
-  {
-    "Name": "sp-60-flex",
-    "RAM": 60000,
-    "Ephemeral": 0,
-    "VCPUs": 4,
-    "Is Public": true,
-    "Disk": 50,
-    "ID": "f52db47a-315f-49d4-bc5c-67dd118e7ac0"
-  },
-  {
-    "Name": "win-hg-120-flex",
-    "RAM": 120000,
-    "Ephemeral": 0,
-    "VCPUs": 32,
-    "Is Public": true,
-    "Disk": 50,
-    "ID": "f6cb8144-5d98-4057-b44f-46da342fb571"
-  },
-  {
-    "Name": "hg-7",
-    "RAM": 7000,
-    "Ephemeral": 0,
-    "VCPUs": 2,
-    "Is Public": true,
-    "Disk": 200,
-    "ID": "fa3cc551-0358-4170-be64-56ea432b064c"
-  },
-  {
-    "Name": "hg-15-ssd",
-    "RAM": 15000,
-    "Ephemeral": 0,
-    "VCPUs": 4,
-    "Is Public": true,
-    "Disk": 200,
-    "ID": "ff48c2cf-c17f-4682-aaf6-31d66786f808"
-  }
-    ]"""
-
-    @classmethod
-    def setup_class(self):
-        if 'OS_AUTH_URL' not in os.environ:
-            pytest.skip('no OS_AUTH_URL environment variable')
-
-    @patch('teuthology.misc.sh')
-    def test_sorted_flavors(self, m_sh):
-        o = OpenStack()
-        select = '^(vps|hg)-.*ssd'
-        m_sh.return_value = TestOpenStack.flavors
-        flavors = o.get_sorted_flavors('arch', select)
-        assert [u'vps-ssd-1',
-                u'vps-ssd-2',
-                u'hg-7-ssd-flex',
-                u'hg-7-ssd',
-                u'vps-ssd-3',
-                u'hg-15-ssd-flex',
-                u'hg-15-ssd',
-                u'hg-30-ssd-flex',
-                u'hg-30-ssd',
-                u'hg-60-ssd-flex',
-                u'hg-60-ssd',
-                u'hg-120-ssd-flex',
-                u'hg-120-ssd',
-        ] == [ f['Name'] for f in flavors ]
-        m_sh.assert_called_with("openstack --quiet flavor list -f json")
-
-    def test_flavor(self):
-        def get_sorted_flavors(self, arch, select):
-            return [
-                {
-                    'Name': 'too_small',
-                    'RAM': 2048,
-                    'Disk': 50,
-                    'VCPUs': 1,
-                },
-            ]
-        with patch.multiple(
-                OpenStack,
-                get_sorted_flavors=get_sorted_flavors,
-        ):
-            with pytest.raises(NoFlavorException):
-                hint = { 'ram': 1000, 'disk': 40, 'cpus': 2 }
-                OpenStack().flavor(hint, 'arch')
-
-        flavor = 'good-flavor'
-        def get_sorted_flavors(self, arch, select):
-            return [
-                {
-                    'Name': flavor,
-                    'RAM': 2048,
-                    'Disk': 50,
-                    'VCPUs': 2,
-                },
-            ]
-        with patch.multiple(
-                OpenStack,
-                get_sorted_flavors=get_sorted_flavors,
-        ):
-            hint = { 'ram': 1000, 'disk': 40, 'cpus': 2 }
-            assert flavor == OpenStack().flavor(hint, 'arch')
-
-    def test_flavor_range(self):
-        flavors = [
-                {
-                    'Name': 'too_small',
-                    'RAM': 2048,
-                    'Disk': 50,
-                    'VCPUs': 1,
-                },
-        ]
-        def get_sorted_flavors(self, arch, select):
-            return flavors
-
-        min = { 'ram': 1000, 'disk': 40, 'cpus': 2 }
-        good = { 'ram': 4000, 'disk': 40, 'cpus': 2 }
-
-        #
-        # there are no flavors in the required range
-        #
-        with patch.multiple(
-                OpenStack,
-                get_sorted_flavors=get_sorted_flavors,
-        ):
-            with pytest.raises(NoFlavorException):
-                OpenStack().flavor_range(min, good, 'arch')
-
-        #
-        # there is one flavor in the required range
-        #
-        flavors.append({
-                    'Name': 'min',
-                    'RAM': 2048,
-                    'Disk': 40,
-                    'VCPUs': 2,
-        })
-
-        with patch.multiple(
-                OpenStack,
-                get_sorted_flavors=get_sorted_flavors,
-        ):
-
-            assert 'min' == OpenStack().flavor_range(min, good, 'arch')
-
-        #
-        # out of the two flavors in the required range, get the bigger one
-        #
-        flavors.append({
-                    'Name': 'good',
-                    'RAM': 3000,
-                    'Disk': 40,
-                    'VCPUs': 2,
-        })
-
-        with patch.multiple(
-                OpenStack,
-                get_sorted_flavors=get_sorted_flavors,
-        ):
-
-            assert 'good' == OpenStack().flavor_range(min, good, 'arch')
-
-        #
-        # there is one flavor bigger or equal to good, get this one
-        #
-        flavors.append({
-                    'Name': 'best',
-                    'RAM': 4000,
-                    'Disk': 40,
-                    'VCPUs': 2,
-        })
-
-        with patch.multiple(
-                OpenStack,
-                get_sorted_flavors=get_sorted_flavors,
-        ):
-
-            assert 'best' == OpenStack().flavor_range(min, good, 'arch')
-
-        #
-        # there are two flavors bigger or equal to good, get the smallest one
-        #
-        flavors.append({
-                    'Name': 'too_big',
-                    'RAM': 30000,
-                    'Disk': 400,
-                    'VCPUs': 20,
-        })
-
-        with patch.multiple(
-                OpenStack,
-                get_sorted_flavors=get_sorted_flavors,
-        ):
-
-            assert 'best' == OpenStack().flavor_range(min, good, 'arch')
-
-
-    def test_interpret_hints(self):
-        defaults = {
-            'machine': {
-                'ram': 0,
-                'disk': 0,
-                'cpus': 0,
-            },
-            'volumes': {
-                'count': 0,
-                'size': 0,
-            },
-        }
-        expected_disk = 10 # first hint larger than the second
-        expected_ram = 20 # second hint larger than the first
-        expected_cpus = 0 # not set, hence zero by default
-        expected_count = 30 # second hint larger than the first
-        expected_size = 40 # does not exist in the first hint
-        hints = [
-            {
-                'machine': {
-                    'ram': 2,
-                    'disk': expected_disk,
-                },
-                'volumes': {
-                    'count': 9,
-                    'size': expected_size,
-                },
-            },
-            {
-                'machine': {
-                    'ram': expected_ram,
-                    'disk': 3,
-                },
-                'volumes': {
-                    'count': expected_count,
-                },
-            },
-        ]
-        hint = OpenStack().interpret_hints(defaults, hints)
-        assert hint == {
-            'machine': {
-                'ram': expected_ram,
-                'disk': expected_disk,
-                'cpus': expected_cpus,
-            },
-            'volumes': {
-                'count': expected_count,
-                'size': expected_size,
-            }
-        }
-        assert defaults == OpenStack().interpret_hints(defaults, None)
-
-    def test_get_provider(self):
-        auth = os.environ.get('OS_AUTH_URL', None)
-        os.environ['OS_AUTH_URL'] = 'cloud.ovh.net'
-        assert OpenStack().get_provider() == 'ovh'
-        if auth != None:
-            os.environ['OS_AUTH_URL'] = auth
-        else:
-            del os.environ['OS_AUTH_URL']
-
-    def test_get_os_url(self):
-        o = OpenStack()
-        #
-        # Only for OVH
-        #
-        o.provider = 'something'
-        assert "" == o.get_os_url("server ")
-        o.provider = 'ovh'
-        assert "" == o.get_os_url("unknown ")
-        type2cmd = {
-            'compute': ('server', 'flavor'),
-            'network': ('ip', 'security', 'network'),
-            'image': ('image',),
-            'volume': ('volume',),
-        }
-        os.environ['OS_REGION_NAME'] = 'REGION'
-        os.environ['OS_TENANT_ID'] = 'TENANT'
-        for (type, cmds) in type2cmd.items():
-            for cmd in cmds:
-                assert ("//" + type) in o.get_os_url(cmd + " ")
-        for type in type2cmd.keys():
-            assert ("//" + type) in o.get_os_url("whatever ", type=type)
-
-    @patch('teuthology.misc.sh')
-    def test_cache_token(self, m_sh):
-        token = 'TOKEN VALUE'
-        m_sh.return_value = token
-        OpenStack.token = None
-        o = OpenStack()
-        #
-        # Only for OVH
-        #
-        o.provider = 'something'
-        assert False == o.cache_token()
-        o.provider = 'ovh'
-        #
-        # Set the environment with the token
-        #
-        assert 'OS_TOKEN_VALUE' not in os.environ
-        assert 'OS_TOKEN_EXPIRES' not in os.environ
-        assert True == o.cache_token()
-        m_sh.assert_called_with('openstack -q token issue -c id -f value')
-        assert token == os.environ['OS_TOKEN_VALUE']
-        assert token == OpenStack.token
-        assert time.time() < int(os.environ['OS_TOKEN_EXPIRES'])
-        assert time.time() < OpenStack.token_expires
-        #
-        # Reset after it expires
-        #
-        token_expires = int(time.time()) - 2000
-        OpenStack.token_expires = token_expires
-        assert True == o.cache_token()
-        assert time.time() < int(os.environ['OS_TOKEN_EXPIRES'])
-        assert time.time() < OpenStack.token_expires
-
-    @patch('teuthology.misc.sh')
-    def test_cache_token_from_environment(self, m_sh):
-        OpenStack.token = None
-        o = OpenStack()
-        o.provider = 'ovh'
-        token = 'TOKEN VALUE'
-        os.environ['OS_TOKEN_VALUE'] = token
-        token_expires = int(time.time()) + OpenStack.token_cache_duration
-        os.environ['OS_TOKEN_EXPIRES'] = str(token_expires)
-        assert True == o.cache_token()
-        assert token == OpenStack.token
-        assert token_expires == OpenStack.token_expires
-        m_sh.assert_not_called()
-        
-    @patch('teuthology.misc.sh')
-    def test_cache_token_expired_environment(self, m_sh):
-        token = 'TOKEN VALUE'
-        m_sh.return_value = token
-        OpenStack.token = None
-        o = OpenStack()
-        o.provider = 'ovh'
-        os.environ['OS_TOKEN_VALUE'] = token
-        token_expires = int(time.time()) - 2000
-        os.environ['OS_TOKEN_EXPIRES'] = str(token_expires)
-        assert True == o.cache_token()
-        m_sh.assert_called_with('openstack -q token issue -c id -f value')
-        assert token == os.environ['OS_TOKEN_VALUE']
-        assert token == OpenStack.token
-        assert time.time() < int(os.environ['OS_TOKEN_EXPIRES'])
-        assert time.time() < OpenStack.token_expires
-
-class TestTeuthologyOpenStack(TestOpenStackBase):
-
-    @classmethod
-    def setup_class(self):
-        if 'OS_AUTH_URL' not in os.environ:
-            pytest.skip('no OS_AUTH_URL environment variable')
-
-        teuthology.log.setLevel(logging.DEBUG)
-        set_config_attr(argparse.Namespace())
-
-        ip = TeuthologyOpenStack.create_floating_ip()
-        if ip:
-            ip_id = TeuthologyOpenStack.get_floating_ip_id(ip)
-            OpenStack().run("ip floating delete " + ip_id)
-            self.can_create_floating_ips = True
-        else:
-            self.can_create_floating_ips = False
-
-    def setup_method(self):
-        super(TestTeuthologyOpenStack, self).setup_method()
-        self.key_filename = tempfile.mktemp()
-        self.key_name = 'teuthology-test'
-        self.name = 'teuthology-test'
-        self.clobber()
-        misc.sh("""
-openstack keypair create {key_name} > {key_filename}
-chmod 600 {key_filename}
-        """.format(key_filename=self.key_filename,
-                   key_name=self.key_name))
-        self.options = ['--key-name', self.key_name,
-                        '--key-filename', self.key_filename,
-                        '--name', self.name,
-                        '--verbose']
-
-    def teardown_method(self):
-        super(TestTeuthologyOpenStack, self).teardown_method()
-        self.clobber()
-        os.unlink(self.key_filename)
-
-    def clobber(self):
-        misc.sh("""
-openstack server delete {name} --wait || true
-openstack keypair delete {key_name} || true
-        """.format(key_name=self.key_name,
-                   name=self.name))
-
-    def test_create(self, caplog):
-        teuthology_argv = [
-            '--suite', 'upgrade/hammer',
-            '--dry-run',
-            '--ceph', 'main',
-            '--kernel', 'distro',
-            '--flavor', 'gcov',
-            '--distro', 'ubuntu',
-            '--suite-branch', 'hammer',
-            '--email', 'loic@dachary.org',
-            '--num', '10',
-            '--limit', '23',
-            '--subset', '1/2',
-            '--priority', '101',
-            '--timeout', '234',
-            '--filter', 'trasher',
-            '--filter-out', 'erasure-code',
-            '--throttle', '3',
-        ]
-        archive_upload = 'user@archive:/tmp'
-        argv = (self.options +
-                ['--teuthology-git-url', 'TEUTHOLOGY_URL',
-                 '--teuthology-branch', 'TEUTHOLOGY_BRANCH',
-                 '--ceph-workbench-git-url', 'CEPH_WORKBENCH_URL',
-                 '--ceph-workbench-branch', 'CEPH_WORKBENCH_BRANCH',
-                 '--upload',
-                 '--archive-upload', archive_upload] +
-                teuthology_argv)
-        args = scripts.openstack.parse_args(argv)
-        teuthology_argv.extend([
-            '--archive-upload', archive_upload,
-            '--archive-upload-url', args.archive_upload_url,
-        ])
-        teuthology = TeuthologyOpenStack(args, None, argv)
-        teuthology.user_data = 'teuthology/openstack/test/user-data-test1.txt'
-        teuthology.teuthology_suite = 'echo --'
-
-        teuthology.main()
-        assert 0 == teuthology.ssh("lsb_release -a")
-        assert 0 == teuthology.ssh("grep 'substituded variables' /var/log/cloud-init.log")
-        l = caplog.text
-        assert 'Ubuntu 14.04' in l
-        assert "nworkers=" + str(args.simultaneous_jobs) in l
-        assert "username=" + teuthology.username in l
-        assert "upload=--archive-upload user@archive:/tmp" in l
-        assert ("ceph_workbench="
-                " --ceph-workbench-branch CEPH_WORKBENCH_BRANCH"
-                " --ceph-workbench-git-url CEPH_WORKBENCH_URL") in l
-        assert "clone=git clone -b TEUTHOLOGY_BRANCH TEUTHOLOGY_URL" in l
-        assert os.environ['OS_AUTH_URL'] in l
-        assert " ".join(teuthology_argv) in l
-
-        if self.can_create_floating_ips:
-            ip = teuthology.get_floating_ip(self.name)
-        teuthology.teardown()
-        if self.can_create_floating_ips:
-            assert teuthology.get_floating_ip_id(ip) == None
-
-    def test_floating_ip(self):
-        if not self.can_create_floating_ips:
-            pytest.skip('unable to create floating ips')
-
-        expected = TeuthologyOpenStack.create_floating_ip()
-        ip = TeuthologyOpenStack.get_unassociated_floating_ip()
-        assert expected == ip
-        ip_id = TeuthologyOpenStack.get_floating_ip_id(ip)
-        OpenStack().run("ip floating delete " + ip_id)
diff --git a/teuthology/openstack/test/user-data-test1.txt b/teuthology/openstack/test/user-data-test1.txt
deleted file mode 100644 (file)
index 4e3e466..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-#cloud-config
-system_info:
-  default_user:
-    name: ubuntu
-final_message: "teuthology is up and running after $UPTIME seconds, substituded variables nworkers=NWORKERS openrc=OPENRC username=TEUTHOLOGY_USERNAME upload=UPLOAD ceph_workbench=CEPH_WORKBENCH clone=CLONE_OPENSTACK"
diff --git a/teuthology/orchestra/test/__init__.py b/teuthology/orchestra/test/__init__.py
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/teuthology/orchestra/test/files/daemon-systemdstate-pid-ps-ef.output b/teuthology/orchestra/test/files/daemon-systemdstate-pid-ps-ef.output
deleted file mode 100644 (file)
index ddddf57..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-ceph       658     1  0 Jun08 ?        00:07:43 /usr/bin/ceph-mgr -f --cluster ceph --id host1 --setuser ceph --setgroup ceph
-ceph      1634     1  0 Jun08 ?        00:02:17 /usr/bin/ceph-mds -f --cluster ceph --id host1 --setuser ceph --setgroup ceph
-ceph     31555     1  0 Jun08 ?        01:13:50 /usr/bin/ceph-mon -f --cluster ceph --id host1 --setuser ceph --setgroup ceph
-ceph     31765     1  0 Jun08 ?        00:48:42 /usr/bin/radosgw -f --cluster ceph --name client.rgw.host1.rgw0 --setuser ceph --setgroup ceph
-ceph     97427     1  0 Jun17 ?        00:41:39 /usr/bin/ceph-osd -f --cluster ceph --id 0 --setuser ceph --setgroup ceph
\ No newline at end of file
diff --git a/teuthology/orchestra/test/integration/__init__.py b/teuthology/orchestra/test/integration/__init__.py
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/teuthology/orchestra/test/integration/test_integration.py b/teuthology/orchestra/test/integration/test_integration.py
deleted file mode 100644 (file)
index 5adeb6d..0000000
+++ /dev/null
@@ -1,94 +0,0 @@
-from teuthology.orchestra import monkey
-monkey.patch_all()
-
-from io import StringIO
-
-import os
-from teuthology.orchestra import connection, remote, run
-from teuthology.orchestra.test.util import assert_raises
-from teuthology.exceptions import CommandCrashedError, ConnectionLostError
-
-from pytest import skip
-
-HOST = None
-
-
-class TestIntegration():
-    def setup_method(self):
-        try:
-            host = os.environ['ORCHESTRA_TEST_HOST']
-        except KeyError:
-            skip('To run integration tests, set environment ' +
-                 'variable ORCHESTRA_TEST_HOST to user@host to use.')
-        global HOST
-        HOST = host
-
-    def test_crash(self):
-        ssh = connection.connect(HOST)
-        e = assert_raises(
-            CommandCrashedError,
-            run.run,
-            client=ssh,
-            args=['sh', '-c', 'kill -ABRT $$'],
-            )
-        assert e.command == "sh -c 'kill -ABRT $$'"
-        assert str(e) == "Command crashed: \"sh -c 'kill -ABRT $$'\""
-
-    def test_lost(self):
-        ssh = connection.connect(HOST)
-        e = assert_raises(
-            ConnectionLostError,
-            run.run,
-            client=ssh,
-            args=['sh', '-c', 'kill -ABRT $PPID'],
-            name=HOST,
-            )
-        assert e.command == "sh -c 'kill -ABRT $PPID'"
-        assert str(e) == \
-            "SSH connection to {host} was lost: ".format(host=HOST) + \
-            "\"sh -c 'kill -ABRT $PPID'\""
-
-    def test_pipe(self):
-        ssh = connection.connect(HOST)
-        r = run.run(
-            client=ssh,
-            args=['cat'],
-            stdin=run.PIPE,
-            stdout=StringIO(),
-            wait=False,
-            )
-        assert r.stdout.getvalue() == ''
-        r.stdin.write('foo\n')
-        r.stdin.write('bar\n')
-        r.stdin.close()
-
-        r.wait()
-        got = r.exitstatus
-        assert got == 0
-        assert r.stdout.getvalue() == 'foo\nbar\n'
-
-    def test_and(self):
-        ssh = connection.connect(HOST)
-        r = run.run(
-            client=ssh,
-            args=['true', run.Raw('&&'), 'echo', 'yup'],
-            stdout=StringIO(),
-            )
-        assert r.stdout.getvalue() == 'yup\n'
-
-    def test_os(self):
-        rem = remote.Remote(HOST)
-        assert rem.os.name
-        assert rem.os.version
-
-    def test_17102(self, caplog):
-        # http://tracker.ceph.com/issues/17102
-        rem = remote.Remote(HOST)
-        interval = 3
-        rem.run(args="echo before; sleep %s; echo after" % interval)
-        for record in caplog.records:
-            if record.msg == 'before':
-                before_time = record.created
-            elif record.msg == 'after':
-                after_time = record.created
-        assert int(round(after_time - before_time)) == interval
diff --git a/teuthology/orchestra/test/test_cluster.py b/teuthology/orchestra/test/test_cluster.py
deleted file mode 100644 (file)
index 27bef8b..0000000
+++ /dev/null
@@ -1,232 +0,0 @@
-import pytest
-
-from mock import patch, Mock
-
-from teuthology.orchestra import cluster, remote, run
-
-
-class TestCluster(object):
-    def test_init_empty(self):
-        c = cluster.Cluster()
-        assert c.remotes == {}
-
-    def test_init(self):
-        r1 = Mock()
-        r2 = Mock()
-        c = cluster.Cluster(
-            remotes=[
-                (r1, ['foo', 'bar']),
-                (r2, ['baz']),
-                ],
-            )
-        r3 = Mock()
-        c.add(r3, ['xyzzy', 'thud', 'foo'])
-        assert c.remotes == {
-            r1: ['foo', 'bar'],
-            r2: ['baz'],
-            r3: ['xyzzy', 'thud', 'foo'],
-        }
-
-    def test_repr(self):
-        r1 = remote.Remote('r1', ssh=Mock())
-        r2 = remote.Remote('r2', ssh=Mock())
-        c = cluster.Cluster(
-            remotes=[
-                (r1, ['foo', 'bar']),
-                (r2, ['baz']),
-                ],
-            )
-        assert repr(c) == \
-            "Cluster(remotes=[[Remote(name='r1'), ['foo', 'bar']], " \
-            "[Remote(name='r2'), ['baz']]])"
-
-    def test_str(self):
-        r1 = remote.Remote('r1', ssh=Mock())
-        r2 = remote.Remote('r2', ssh=Mock())
-        c = cluster.Cluster(
-            remotes=[
-                (r1, ['foo', 'bar']),
-                (r2, ['baz']),
-                ],
-            )
-        assert str(c) == "r1[foo,bar] r2[baz]"
-
-    def test_run_all(self):
-        r1 = Mock(spec=remote.Remote)
-        r1.configure_mock(name='r1')
-        ret1 = Mock(spec=run.RemoteProcess)
-        r1.run.return_value = ret1
-        r2 = Mock(spec=remote.Remote)
-        r2.configure_mock(name='r2')
-        ret2 = Mock(spec=run.RemoteProcess)
-        r2.run.return_value = ret2
-        c = cluster.Cluster(
-            remotes=[
-                (r1, ['foo', 'bar']),
-                (r2, ['baz']),
-                ],
-            )
-        got = c.run(args=['test'])
-        r1.run.assert_called_once_with(args=['test'], wait=True)
-        r2.run.assert_called_once_with(args=['test'], wait=True)
-        assert len(got) == 2
-        assert got, [ret1 == ret2]
-        # check identity not equality
-        assert got[0] is ret1
-        assert got[1] is ret2
-
-    def test_only_one(self):
-        r1 = Mock()
-        r2 = Mock()
-        r3 = Mock()
-        c = cluster.Cluster(
-            remotes=[
-                (r1, ['foo', 'bar']),
-                (r2, ['bar']),
-                (r3, ['foo']),
-                ],
-            )
-        c_foo = c.only('foo')
-        assert c_foo.remotes == {r1: ['foo', 'bar'], r3: ['foo']}
-
-    def test_only_two(self):
-        r1 = Mock()
-        r2 = Mock()
-        r3 = Mock()
-        c = cluster.Cluster(
-            remotes=[
-                (r1, ['foo', 'bar']),
-                (r2, ['bar']),
-                (r3, ['foo']),
-                ],
-            )
-        c_both = c.only('foo', 'bar')
-        assert c_both.remotes, {r1: ['foo' == 'bar']}
-
-    def test_only_none(self):
-        r1 = Mock()
-        r2 = Mock()
-        r3 = Mock()
-        c = cluster.Cluster(
-            remotes=[
-                (r1, ['foo', 'bar']),
-                (r2, ['bar']),
-                (r3, ['foo']),
-                ],
-            )
-        c_none = c.only('impossible')
-        assert c_none.remotes == {}
-
-    def test_only_match(self):
-        r1 = Mock()
-        r2 = Mock()
-        r3 = Mock()
-        c = cluster.Cluster(
-            remotes=[
-                (r1, ['foo', 'bar']),
-                (r2, ['bar']),
-                (r3, ['foo']),
-                ],
-            )
-        c_foo = c.only('foo', lambda role: role.startswith('b'))
-        assert c_foo.remotes, {r1: ['foo' == 'bar']}
-
-    def test_exclude_one(self):
-        r1 = Mock()
-        r2 = Mock()
-        r3 = Mock()
-        c = cluster.Cluster(
-            remotes=[
-                (r1, ['foo', 'bar']),
-                (r2, ['bar']),
-                (r3, ['foo']),
-                ],
-            )
-        c_foo = c.exclude('foo')
-        assert c_foo.remotes == {r2: ['bar']}
-
-    def test_exclude_two(self):
-        r1 = Mock()
-        r2 = Mock()
-        r3 = Mock()
-        c = cluster.Cluster(
-            remotes=[
-                (r1, ['foo', 'bar']),
-                (r2, ['bar']),
-                (r3, ['foo']),
-                ],
-            )
-        c_both = c.exclude('foo', 'bar')
-        assert c_both.remotes == {r2: ['bar'], r3: ['foo']}
-
-    def test_exclude_none(self):
-        r1 = Mock()
-        r2 = Mock()
-        r3 = Mock()
-        c = cluster.Cluster(
-            remotes=[
-                (r1, ['foo', 'bar']),
-                (r2, ['bar']),
-                (r3, ['foo']),
-                ],
-            )
-        c_none = c.exclude('impossible')
-        assert c_none.remotes == {r1: ['foo', 'bar'], r2: ['bar'], r3: ['foo']}
-
-    def test_exclude_match(self):
-        r1 = Mock()
-        r2 = Mock()
-        r3 = Mock()
-        c = cluster.Cluster(
-            remotes=[
-                (r1, ['foo', 'bar']),
-                (r2, ['bar']),
-                (r3, ['foo']),
-                ],
-            )
-        c_foo = c.exclude('foo', lambda role: role.startswith('b'))
-        assert c_foo.remotes == {r2: ['bar'], r3: ['foo']}
-
-    def test_filter(self):
-        r1 = Mock(_name='r1')
-        r2 = Mock(_name='r2')
-        def func(r):
-            return r._name == "r1"
-        c = cluster.Cluster(remotes=[
-                (r1, ['foo']),
-                (r2, ['bar']),
-            ])
-        assert c.filter(func).remotes == {
-            r1: ['foo']
-        }
-
-
-class TestWriteFile(object):
-    """ Tests for cluster.write_file """
-    def setup_method(self):
-        self.r1 = remote.Remote('r1', ssh=Mock())
-        self.c = cluster.Cluster(
-            remotes=[
-                (self.r1, ['foo', 'bar']),
-            ],
-        )
-
-    @patch("teuthology.orchestra.remote.RemoteShell.write_file")
-    def test_write_file(self, m_write_file):
-        self.c.write_file("filename", "content")
-        m_write_file.assert_called_with("filename", "content")
-
-    @patch("teuthology.orchestra.remote.RemoteShell.write_file")
-    def test_fails_with_invalid_perms(self, m_write_file):
-        with pytest.raises(ValueError):
-            self.c.write_file("filename", "content", sudo=False, perms="invalid")
-
-    @patch("teuthology.orchestra.remote.RemoteShell.write_file")
-    def test_fails_with_invalid_owner(self, m_write_file):
-        with pytest.raises(ValueError):
-            self.c.write_file("filename", "content", sudo=False, owner="invalid")
-
-    @patch("teuthology.orchestra.remote.RemoteShell.write_file")
-    def test_with_sudo(self, m_write_file):
-        self.c.write_file("filename", "content", sudo=True)
-        m_write_file.assert_called_with("filename", "content", sudo=True, owner=None, mode=None)
diff --git a/teuthology/orchestra/test/test_connection.py b/teuthology/orchestra/test/test_connection.py
deleted file mode 100644 (file)
index b9f7296..0000000
+++ /dev/null
@@ -1,120 +0,0 @@
-from mock import patch, Mock
-
-from teuthology import config
-from teuthology.orchestra import connection
-from teuthology.orchestra.test.util import assert_raises
-
-
-class TestConnection(object):
-    def setup_method(self):
-        self.start_patchers()
-
-    def teardown_method(self):
-        self.stop_patchers()
-
-    def start_patchers(self):
-        self.patcher_sleep = patch(
-            'time.sleep',
-        )
-        self.patcher_sleep.start()
-        self.m_ssh = Mock()
-        self.patcher_ssh = patch(
-            'teuthology.orchestra.connection.paramiko.SSHClient',
-            self.m_ssh,
-        )
-        self.patcher_ssh.start()
-
-    def stop_patchers(self):
-        self.patcher_ssh.stop()
-        self.patcher_sleep.stop()
-
-    def clear_config(self):
-        config.config.teuthology_yaml = ''
-        config.config.load()
-        config.config.ssh_key = None
-
-    def test_split_user_just_host(self):
-        got = connection.split_user('somehost.example.com')
-        assert got == (None, 'somehost.example.com')
-
-    def test_split_user_both(self):
-        got = connection.split_user('jdoe@somehost.example.com')
-        assert got == ('jdoe', 'somehost.example.com')
-
-    def test_split_user_empty_user(self):
-        s = '@somehost.example.com'
-        e = assert_raises(AssertionError, connection.split_user, s)
-        assert str(e) == 'Bad input to split_user: {s!r}'.format(s=s)
-
-    def test_connect(self):
-        self.clear_config()
-        config.config.verify_host_keys = True
-        m_ssh_instance = self.m_ssh.return_value = Mock();
-        m_transport = Mock()
-        m_ssh_instance.get_transport.return_value = m_transport
-        got = connection.connect(
-            'jdoe@orchestra.test.newdream.net.invalid',
-            _SSHClient=self.m_ssh,
-        )
-        self.m_ssh.assert_called_once()
-        m_ssh_instance.set_missing_host_key_policy.assert_called_once()
-        m_ssh_instance.load_system_host_keys.assert_called_once_with()
-        m_ssh_instance.connect.assert_called_once_with(
-            hostname='orchestra.test.newdream.net.invalid',
-            username='jdoe',
-            timeout=60,
-        )
-        m_transport.set_keepalive.assert_called_once_with(False)
-        assert got is m_ssh_instance
-
-    def test_connect_no_verify_host_keys(self):
-        self.clear_config()
-        config.config.verify_host_keys = False
-        m_ssh_instance = self.m_ssh.return_value = Mock();
-        m_transport = Mock()
-        m_ssh_instance.get_transport.return_value = m_transport
-        got = connection.connect(
-            'jdoe@orchestra.test.newdream.net.invalid',
-            _SSHClient=self.m_ssh,
-        )
-        self.m_ssh.assert_called_once()
-        m_ssh_instance.set_missing_host_key_policy.assert_called_once()
-        assert not m_ssh_instance.load_system_host_keys.called
-        m_ssh_instance.connect.assert_called_once_with(
-            hostname='orchestra.test.newdream.net.invalid',
-            username='jdoe',
-            timeout=60,
-        )
-        m_transport.set_keepalive.assert_called_once_with(False)
-        assert got is m_ssh_instance
-
-    def test_connect_override_hostkeys(self):
-        self.clear_config()
-        m_ssh_instance = self.m_ssh.return_value = Mock();
-        m_transport = Mock()
-        m_ssh_instance.get_transport.return_value = m_transport
-        m_host_keys = Mock()
-        m_ssh_instance.get_host_keys.return_value = m_host_keys
-        m_create_key = Mock()
-        m_create_key.return_value = "frobnitz"
-        got = connection.connect(
-            'jdoe@orchestra.test.newdream.net.invalid',
-            host_key='ssh-rsa testkey',
-            _SSHClient=self.m_ssh,
-            _create_key=m_create_key,
-            )
-        self.m_ssh.assert_called_once()
-        m_ssh_instance.get_host_keys.assert_called_once()
-        m_host_keys.add.assert_called_once_with(
-            hostname='orchestra.test.newdream.net.invalid',
-            keytype='ssh-rsa',
-            key='frobnitz',
-        )
-        m_create_key.assert_called_once_with('ssh-rsa', 'testkey')
-        m_ssh_instance.connect.assert_called_once_with(
-            hostname='orchestra.test.newdream.net.invalid',
-            username='jdoe',
-            timeout=60,
-        )
-        m_transport.set_keepalive.assert_called_once_with(False)
-        assert got is m_ssh_instance
diff --git a/teuthology/orchestra/test/test_console.py b/teuthology/orchestra/test/test_console.py
deleted file mode 100644 (file)
index fe0399b..0000000
+++ /dev/null
@@ -1,217 +0,0 @@
-from mock import patch
-
-from teuthology.config import config as teuth_config
-
-from teuthology.orchestra import console
-
-
-class TestConsole(object):
-    pass
-
-
-class TestPhysicalConsole(TestConsole):
-    klass = console.PhysicalConsole
-    ipmi_cmd_templ = 'ipmitool -H {h}.{d} -I lanplus -U {u} -P {p} {c}'
-    conserver_cmd_templ = 'console -M {m} -p {p} {mode} {h}'
-
-    def setup_method(self):
-        self.hostname = 'host'
-        teuth_config.ipmi_domain = 'ipmi_domain'
-        teuth_config.ipmi_user = 'ipmi_user'
-        teuth_config.ipmi_password = 'ipmi_pass'
-        teuth_config.conserver_master = 'conserver_master'
-        teuth_config.conserver_port = 3109
-        teuth_config.use_conserver = True
-
-    def test_has_ipmi_creds(self):
-        cons = self.klass(self.hostname)
-        assert cons.has_ipmi_credentials is True
-        teuth_config.ipmi_domain = None
-        cons = self.klass(self.hostname)
-        assert cons.has_ipmi_credentials is False
-
-    def test_console_command_conserver(self):
-        cons = self.klass(
-            self.hostname,
-            teuth_config.ipmi_user,
-            teuth_config.ipmi_password,
-            teuth_config.ipmi_domain,
-        )
-        cons.has_conserver = True
-        console_cmd = cons._console_command()
-        assert console_cmd == self.conserver_cmd_templ.format(
-            m=teuth_config.conserver_master,
-            p=teuth_config.conserver_port,
-            mode='-s',
-            h=self.hostname,
-        )
-        console_cmd = cons._console_command(readonly=False)
-        assert console_cmd == self.conserver_cmd_templ.format(
-            m=teuth_config.conserver_master,
-            p=teuth_config.conserver_port,
-            mode='-f',
-            h=self.hostname,
-        )
-
-    def test_console_command_ipmi(self):
-        teuth_config.conserver_master = None
-        cons = self.klass(
-            self.hostname,
-            teuth_config.ipmi_user,
-            teuth_config.ipmi_password,
-            teuth_config.ipmi_domain,
-        )
-        sol_cmd = cons._console_command()
-        assert sol_cmd == self.ipmi_cmd_templ.format(
-            h=self.hostname,
-            d=teuth_config.ipmi_domain,
-            u=teuth_config.ipmi_user,
-            p=teuth_config.ipmi_password,
-            c='sol activate',
-        )
-
-    def test_ipmi_command_ipmi(self):
-        cons = self.klass(
-            self.hostname,
-            teuth_config.ipmi_user,
-            teuth_config.ipmi_password,
-            teuth_config.ipmi_domain,
-        )
-        pc_cmd = cons._ipmi_command('power cycle')
-        assert pc_cmd == self.ipmi_cmd_templ.format(
-            h=self.hostname,
-            d=teuth_config.ipmi_domain,
-            u=teuth_config.ipmi_user,
-            p=teuth_config.ipmi_password,
-            c='power cycle',
-        )
-
-    def test_spawn_log_conserver(self):
-        with patch(
-            'teuthology.orchestra.console.psutil.subprocess.Popen',
-            autospec=True,
-        ) as m_popen:
-            m_popen.return_value.pid = 42
-            m_popen.return_value.returncode = 0
-            m_popen.return_value.wait.return_value = 0
-            cons = self.klass(self.hostname)
-            assert cons.has_conserver is True
-            m_popen.reset_mock()
-            m_popen.return_value.poll.return_value = None
-            cons.spawn_sol_log('/fake/path')
-            assert m_popen.call_count == 1
-            call_args = m_popen.call_args_list[0][0][0]
-            assert any(
-                [teuth_config.conserver_master in arg for arg in call_args]
-            )
-
-    def test_spawn_log_ipmi(self):
-        with patch(
-            'teuthology.orchestra.console.psutil.subprocess.Popen',
-            autospec=True,
-        ) as m_popen:
-            m_popen.return_value.pid = 42
-            m_popen.return_value.returncode = 1
-            m_popen.return_value.wait.return_value = 1
-            cons = self.klass(self.hostname)
-            assert cons.has_conserver is False
-            m_popen.reset_mock()
-            m_popen.return_value.poll.return_value = 1
-            cons.spawn_sol_log('/fake/path')
-            assert m_popen.call_count == 1
-            call_args = m_popen.call_args_list[0][0][0]
-            assert any(
-                ['ipmitool' in arg for arg in call_args]
-            )
-
-    def test_spawn_log_fallback(self):
-        with patch(
-            'teuthology.orchestra.console.psutil.subprocess.Popen',
-            autospec=True,
-        ) as m_popen:
-            m_popen.return_value.pid = 42
-            m_popen.return_value.returncode = 0
-            m_popen.return_value.wait.return_value = 0
-            cons = self.klass(self.hostname)
-            assert cons.has_conserver is True
-            m_popen.reset_mock()
-            m_popen.return_value.poll.return_value = 1
-            cons.spawn_sol_log('/fake/path')
-            assert cons.has_conserver is False
-            assert m_popen.call_count == 2
-            call_args = m_popen.call_args_list[1][0][0]
-            assert any(
-                ['ipmitool' in arg for arg in call_args]
-            )
-
-    def test_get_console_conserver(self):
-        with patch(
-            'teuthology.orchestra.console.psutil.subprocess.Popen',
-            autospec=True,
-        ) as m_popen:
-            m_popen.return_value.pid = 42
-            m_popen.return_value.returncode = 0
-            m_popen.return_value.wait.return_value = 0
-            cons = self.klass(self.hostname)
-        assert cons.has_conserver is True
-        with patch(
-            'teuthology.orchestra.console.pexpect.spawn',
-            autospec=True,
-        ) as m_spawn:
-            cons._get_console()
-            assert m_spawn.call_count == 1
-            assert teuth_config.conserver_master in \
-                m_spawn.call_args_list[0][0][0]
-
-    def test_get_console_ipmitool(self):
-        with patch(
-            'teuthology.orchestra.console.psutil.subprocess.Popen',
-            autospec=True,
-        ) as m_popen:
-            m_popen.return_value.pid = 42
-            m_popen.return_value.returncode = 0
-            m_popen.return_value.wait.return_value = 0
-            cons = self.klass(self.hostname)
-        assert cons.has_conserver is True
-        with patch(
-            'teuthology.orchestra.console.pexpect.spawn',
-            autospec=True,
-        ) as m_spawn:
-            cons.has_conserver = False
-            cons._get_console()
-            assert m_spawn.call_count == 1
-            assert 'ipmitool' in m_spawn.call_args_list[0][0][0]
-
-    def test_get_console_fallback(self):
-        with patch(
-            'teuthology.orchestra.console.psutil.subprocess.Popen',
-            autospec=True,
-        ) as m_popen:
-            m_popen.return_value.pid = 42
-            m_popen.return_value.returncode = 0
-            m_popen.return_value.wait.return_value = 0
-            cons = self.klass(self.hostname)
-        assert cons.has_conserver is True
-        with patch(
-            'teuthology.orchestra.console.pexpect.spawn',
-            autospec=True,
-        ) as m_spawn:
-            cons.has_conserver = True
-            m_spawn.return_value.isalive.return_value = False
-            cons._get_console()
-            assert m_spawn.return_value.isalive.call_count == 1
-            assert m_spawn.call_count == 2
-            assert cons.has_conserver is False
-            assert 'ipmitool' in m_spawn.call_args_list[1][0][0]
-
-    def test_disable_conserver(self):
-        with patch(
-            'teuthology.orchestra.console.psutil.subprocess.Popen',
-            autospec=True,
-        ) as m_popen:
-            m_popen.return_value.pid = 42
-            m_popen.return_value.returncode = 0
-            m_popen.return_value.wait.return_value = 0
-            teuth_config.use_conserver = False
-            cons = self.klass(self.hostname)
-            assert cons.has_conserver is False
diff --git a/teuthology/orchestra/test/test_opsys.py b/teuthology/orchestra/test/test_opsys.py
deleted file mode 100644 (file)
index b69d08a..0000000
+++ /dev/null
@@ -1,408 +0,0 @@
-from textwrap import dedent
-from teuthology.orchestra.opsys import OS
-import pytest
-
-
-class TestOS(object):
-    str_centos_9_os_release = dedent("""
-        NAME="CentOS Stream"
-        VERSION="9"
-        ID="centos"
-        ID_LIKE="rhel fedora"
-        VERSION_ID="9"
-        PLATFORM_ID="platform:el9"
-        PRETTY_NAME="CentOS Stream 9"
-        ANSI_COLOR="0;31"
-        LOGO="fedora-logo-icon"
-        CPE_NAME="cpe:/o:centos:centos:9"
-        HOME_URL="https://centos.org/"
-        BUG_REPORT_URL="https://issues.redhat.com/"
-        REDHAT_SUPPORT_PRODUCT="Red Hat Enterprise Linux 9"
-        REDHAT_SUPPORT_PRODUCT_VERSION="CentOS Stream"
-    """)
-
-    str_centos_7_os_release = dedent("""
-        NAME="CentOS Linux"
-        VERSION="7 (Core)"
-        ID="centos"
-        ID_LIKE="rhel fedora"
-        VERSION_ID="7"
-        PRETTY_NAME="CentOS Linux 7 (Core)"
-        ANSI_COLOR="0;31"
-        CPE_NAME="cpe:/o:centos:centos:7"
-        HOME_URL="https://www.centos.org/"
-        BUG_REPORT_URL="https://bugs.centos.org/"
-    """)
-
-    str_centos_7_os_release_newer = dedent("""
-        NAME="CentOS Linux"
-        VERSION="7 (Core)"
-        ID="centos"
-        ID_LIKE="rhel fedora"
-        VERSION_ID="7"
-        PRETTY_NAME="CentOS Linux 7 (Core)"
-        ANSI_COLOR="0;31"
-        CPE_NAME="cpe:/o:centos:centos:7"
-        HOME_URL="https://www.centos.org/"
-        BUG_REPORT_URL="https://bugs.centos.org/"
-
-        CENTOS_MANTISBT_PROJECT="CentOS-7"
-        CENTOS_MANTISBT_PROJECT_VERSION="7"
-        REDHAT_SUPPORT_PRODUCT="centos"
-        REDHAT_SUPPORT_PRODUCT_VERSION="7"
-    """)
-
-    str_debian_7_lsb_release = dedent("""
-        Distributor ID: Debian
-        Description:    Debian GNU/Linux 7.1 (wheezy)
-        Release:        7.1
-        Codename:       wheezy
-    """)
-
-    str_debian_7_os_release = dedent("""
-        PRETTY_NAME="Debian GNU/Linux 7 (wheezy)"
-        NAME="Debian GNU/Linux"
-        VERSION_ID="7"
-        VERSION="7 (wheezy)"
-        ID=debian
-        ANSI_COLOR="1;31"
-        HOME_URL="http://www.debian.org/"
-        SUPPORT_URL="http://www.debian.org/support/"
-        BUG_REPORT_URL="http://bugs.debian.org/"
-    """)
-
-    str_debian_8_os_release = dedent("""
-        PRETTY_NAME="Debian GNU/Linux 8 (jessie)"
-        NAME="Debian GNU/Linux"
-        VERSION_ID="8"
-        VERSION="8 (jessie)"
-        ID=debian
-        HOME_URL="http://www.debian.org/"
-        SUPPORT_URL="http://www.debian.org/support/"
-        BUG_REPORT_URL="https://bugs.debian.org/"
-    """)
-
-    str_debian_9_os_release = dedent("""
-        PRETTY_NAME="Debian GNU/Linux 9 (stretch)"
-        NAME="Debian GNU/Linux"
-        VERSION_ID="9"
-        VERSION="9 (stretch)"
-        ID=debian
-        HOME_URL="https://www.debian.org/"
-        SUPPORT_URL="https://www.debian.org/support"
-        BUG_REPORT_URL="https://bugs.debian.org/"
-    """)
-
-    str_ubuntu_12_04_lsb_release = dedent("""
-        Distributor ID: Ubuntu
-        Description:    Ubuntu 12.04.4 LTS
-        Release:        12.04
-        Codename:       precise
-    """)
-
-    str_ubuntu_12_04_os_release = dedent("""
-        NAME="Ubuntu"
-        VERSION="12.04.4 LTS, Precise Pangolin"
-        ID=ubuntu
-        ID_LIKE=debian
-        PRETTY_NAME="Ubuntu precise (12.04.4 LTS)"
-        VERSION_ID="12.04"
-    """)
-
-    str_ubuntu_14_04_os_release = dedent("""
-        NAME="Ubuntu"
-        VERSION="14.04.4 LTS, Trusty Tahr"
-        ID=ubuntu
-        ID_LIKE=debian
-        PRETTY_NAME="Ubuntu 14.04.4 LTS"
-        VERSION_ID="14.04"
-        HOME_URL="http://www.ubuntu.com/"
-        SUPPORT_URL="http://help.ubuntu.com/"
-        BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"
-    """)
-
-    str_ubuntu_16_04_os_release = dedent("""
-        NAME="Ubuntu"
-        VERSION="16.04 LTS (Xenial Xerus)"
-        ID=ubuntu
-        ID_LIKE=debian
-        PRETTY_NAME="Ubuntu 16.04 LTS"
-        VERSION_ID="16.04"
-        HOME_URL="http://www.ubuntu.com/"
-        SUPPORT_URL="http://help.ubuntu.com/"
-        BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"
-        UBUNTU_CODENAME=xenial
-    """)
-
-    str_ubuntu_18_04_os_release = dedent("""
-        NAME="Ubuntu"
-        VERSION="18.04 LTS (Bionic Beaver)"
-        ID=ubuntu
-        ID_LIKE=debian
-        PRETTY_NAME="Ubuntu 18.04 LTS"
-        VERSION_ID="18.04"
-        HOME_URL="https://www.ubuntu.com/"
-        SUPPORT_URL="https://help.ubuntu.com/"
-        BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
-        PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
-        VERSION_CODENAME=bionic
-        UBUNTU_CODENAME=bionic
-    """)
-
-    str_rhel_6_4_lsb_release = dedent("""
-        LSB Version:    :base-4.0-amd64:base-4.0-noarch:core-4.0-amd64:core-4.0-noarch:graphics-4.0-amd64:graphics-4.0-noarch:printing-4.0-amd64:printing-4.0-noarch
-        Distributor ID: RedHatEnterpriseServer
-        Description:    Red Hat Enterprise Linux Server release 6.4 (Santiago)
-        Release:        6.4
-        Codename:       Santiago
-    """)
-
-    str_rhel_7_lsb_release = dedent("""
-        LSB Version:    :core-4.1-amd64:core-4.1-noarch:cxx-4.1-amd64:cxx-4.1-noarch:desktop-4.1-amd64:desktop-4.1-noarch:languages-4.1-amd64:languages-4.1-noarch:printing-4.1-amd64:printing-4.1-noarch
-        Distributor ID: RedHatEnterpriseServer
-        Description:    Red Hat Enterprise Linux Server release 7.0 (Maipo)
-        Release:        7.0
-        Codename:       Maipo
-    """)
-
-    str_rhel_7_os_release = dedent("""
-        NAME="Red Hat Enterprise Linux Server"
-        VERSION="7.0 (Maipo)"
-        ID="rhel"
-        ID_LIKE="fedora"
-        VERSION_ID="7.0"
-        PRETTY_NAME="Red Hat Enterprise Linux Server 7.0 (Maipo)"
-        ANSI_COLOR="0;31"
-        CPE_NAME="cpe:/o:redhat:enterprise_linux:7.0:GA:server"
-        HOME_URL="https://www.redhat.com/"
-        BUG_REPORT_URL="https://bugzilla.redhat.com/"
-
-        REDHAT_BUGZILLA_PRODUCT="Red Hat Enterprise Linux 7"
-        REDHAT_BUGZILLA_PRODUCT_VERSION=7.0
-        REDHAT_SUPPORT_PRODUCT="Red Hat Enterprise Linux"
-        REDHAT_SUPPORT_PRODUCT_VERSION=7.0
-    """)
-
-    str_fedora_26_os_release = dedent("""
-        NAME=Fedora
-        VERSION="26 (Twenty Six)"
-        ID=fedora
-        VERSION_ID=26
-        PRETTY_NAME="Fedora 26 (Twenty Six)"
-        ANSI_COLOR="0;34"
-        CPE_NAME="cpe:/o:fedoraproject:fedora:26"
-        HOME_URL="https://fedoraproject.org/"
-        BUG_REPORT_URL="https://bugzilla.redhat.com/"
-        REDHAT_BUGZILLA_PRODUCT="Fedora"
-        REDHAT_BUGZILLA_PRODUCT_VERSION=26
-        REDHAT_SUPPORT_PRODUCT="Fedora"
-        REDHAT_SUPPORT_PRODUCT_VERSION=26
-        PRIVACY_POLICY_URL=https://fedoraproject.org/wiki/Legal:PrivacyPolicy
-    """)
-
-    str_opensuse_42_3_os_release = dedent("""
-        NAME="openSUSE Leap"
-        VERSION="42.3"
-        ID=opensuse
-        ID_LIKE="suse"
-        VERSION_ID="42.3"
-        PRETTY_NAME="openSUSE Leap 42.3"
-        ANSI_COLOR="0;32"
-        CPE_NAME="cpe:/o:opensuse:leap:42.3"
-        BUG_REPORT_URL="https://bugs.opensuse.org"
-        HOME_URL="https://www.opensuse.org/"
-    """)
-
-    str_opensuse_15_0_os_release = dedent("""
-        NAME="openSUSE Leap"
-        VERSION="15.0"
-        ID="opensuse-leap"
-        ID_LIKE="suse opensuse"
-        VERSION_ID="15.0"
-        PRETTY_NAME="openSUSE Leap 15.0"
-        ANSI_COLOR="0;32"
-        CPE_NAME="cpe:/o:opensuse:leap:15.0"
-        BUG_REPORT_URL="https://bugs.opensuse.org"
-        HOME_URL="https://www.opensuse.org/"
-    """)
-
-    str_opensuse_15_1_os_release = dedent("""
-        NAME="openSUSE Leap"
-        VERSION="15.1"
-        ID="opensuse-leap"
-        ID_LIKE="suse opensuse"
-        VERSION_ID="15.1"
-        PRETTY_NAME="openSUSE Leap 15.1"
-        ANSI_COLOR="0;32"
-        CPE_NAME="cpe:/o:opensuse:leap:15.1"
-        BUG_REPORT_URL="https://bugs.opensuse.org"
-        HOME_URL="https://www.opensuse.org/"
-    """)
-
-    def test_centos_9_os_release(self):
-        os = OS.from_os_release(self.str_centos_9_os_release)
-        assert os.name == 'centos'
-        assert os.version == '9.stream'
-        assert os.codename == 'stream'
-        assert os.package_type == 'rpm'
-
-    def test_centos_7_os_release(self):
-        os = OS.from_os_release(self.str_centos_7_os_release)
-        assert os.name == 'centos'
-        assert os.version == '7'
-        assert os.codename == 'core'
-        assert os.package_type == 'rpm'
-
-    def test_centos_7_os_release_newer(self):
-        os = OS.from_os_release(self.str_centos_7_os_release_newer)
-        assert os.name == 'centos'
-        assert os.version == '7'
-        assert os.codename == 'core'
-        assert os.package_type == 'rpm'
-
-    def test_debian_7_lsb_release(self):
-        os = OS.from_lsb_release(self.str_debian_7_lsb_release)
-        assert os.name == 'debian'
-        assert os.version == '7.1'
-        assert os.codename == 'wheezy'
-        assert os.package_type == 'deb'
-
-    def test_debian_7_os_release(self):
-        os = OS.from_os_release(self.str_debian_7_os_release)
-        assert os.name == 'debian'
-        assert os.version == '7'
-        assert os.codename == 'wheezy'
-        assert os.package_type == 'deb'
-
-    def test_debian_8_os_release(self):
-        os = OS.from_os_release(self.str_debian_8_os_release)
-        assert os.name == 'debian'
-        assert os.version == '8'
-        assert os.codename == 'jessie'
-        assert os.package_type == 'deb'
-
-    def test_debian_9_os_release(self):
-        os = OS.from_os_release(self.str_debian_9_os_release)
-        assert os.name == 'debian'
-        assert os.version == '9'
-        assert os.codename == 'stretch'
-        assert os.package_type == 'deb'
-
-    def test_ubuntu_12_04_lsb_release(self):
-        os = OS.from_lsb_release(self.str_ubuntu_12_04_lsb_release)
-        assert os.name == 'ubuntu'
-        assert os.version == '12.04'
-        assert os.codename == 'precise'
-        assert os.package_type == 'deb'
-
-    def test_ubuntu_12_04_os_release(self):
-        os = OS.from_os_release(self.str_ubuntu_12_04_os_release)
-        assert os.name == 'ubuntu'
-        assert os.version == '12.04'
-        assert os.codename == 'precise'
-        assert os.package_type == 'deb'
-
-    def test_ubuntu_14_04_os_release(self):
-        os = OS.from_os_release(self.str_ubuntu_14_04_os_release)
-        assert os.name == 'ubuntu'
-        assert os.version == '14.04'
-        assert os.codename == 'trusty'
-        assert os.package_type == 'deb'
-
-    def test_ubuntu_16_04_os_release(self):
-        os = OS.from_os_release(self.str_ubuntu_16_04_os_release)
-        assert os.name == 'ubuntu'
-        assert os.version == '16.04'
-        assert os.codename == 'xenial'
-        assert os.package_type == 'deb'
-
-    def test_ubuntu_18_04_os_release(self):
-        os = OS.from_os_release(self.str_ubuntu_18_04_os_release)
-        assert os.name == 'ubuntu'
-        assert os.version == '18.04'
-        assert os.codename == 'bionic'
-        assert os.package_type == 'deb'
-
-    def test_rhel_6_4_lsb_release(self):
-        os = OS.from_lsb_release(self.str_rhel_6_4_lsb_release)
-        assert os.name == 'rhel'
-        assert os.version == '6.4'
-        assert os.codename == 'santiago'
-        assert os.package_type == 'rpm'
-
-    def test_rhel_7_lsb_release(self):
-        os = OS.from_lsb_release(self.str_rhel_7_lsb_release)
-        assert os.name == 'rhel'
-        assert os.version == '7.0'
-        assert os.codename == 'maipo'
-        assert os.package_type == 'rpm'
-
-    def test_rhel_7_os_release(self):
-        os = OS.from_os_release(self.str_rhel_7_os_release)
-        assert os.name == 'rhel'
-        assert os.version == '7.0'
-        assert os.codename == 'maipo'
-        assert os.package_type == 'rpm'
-
-    def test_fedora_26_os_release(self):
-        os = OS.from_os_release(self.str_fedora_26_os_release)
-        assert os.name == 'fedora'
-        assert os.version == '26'
-        assert os.codename == '26'
-        assert os.package_type == 'rpm'
-
-    def test_opensuse_42_3_os_release(self):
-        os = OS.from_os_release(self.str_opensuse_42_3_os_release)
-        assert os.name == 'opensuse'
-        assert os.version == '42.3'
-        assert os.codename == 'leap'
-        assert os.package_type == 'rpm'
-
-    def test_opensuse_15_0_os_release(self):
-        os = OS.from_os_release(self.str_opensuse_15_0_os_release)
-        assert os.name == 'opensuse'
-        assert os.version == '15.0'
-        assert os.codename == 'leap'
-        assert os.package_type == 'rpm'
-
-    def test_opensuse_15_1_os_release(self):
-        os = OS.from_os_release(self.str_opensuse_15_1_os_release)
-        assert os.name == 'opensuse'
-        assert os.version == '15.1'
-        assert os.codename == 'leap'
-        assert os.package_type == 'rpm'
-
-    def test_version_codename_success(self):
-        assert OS.version_codename('ubuntu', '14.04') == ('14.04', 'trusty')
-        assert OS.version_codename('ubuntu', 'trusty') == ('14.04', 'trusty')
-
-    def test_version_codename_failure(self):
-        with pytest.raises(KeyError) as excinfo:
-            OS.version_codename('ubuntu', 'frog')
-        assert excinfo.type == KeyError
-        assert 'frog' in excinfo.value.args[0]
-
-    def test_repr(self):
-        os = OS(name='NAME', version='0.1.2', codename='code')
-        assert repr(os) == "OS(name='NAME', version='0.1.2', codename='code')"
-
-    def test_to_dict(self):
-        os = OS(name='NAME', version='0.1.2', codename='code')
-        ref_dict = dict(name='NAME', version='0.1.2', codename='code')
-        assert os.to_dict() == ref_dict
-
-    def test_version_no_codename(self):
-        os = OS(name='ubuntu', version='16.04')
-        assert os.codename == 'xenial'
-
-    def test_codename_no_version(self):
-        os = OS(name='ubuntu', codename='trusty')
-        assert os.version == '14.04'
-
-    def test_eq_equal(self):
-        os = OS(name='ubuntu', codename='trusty', version='14.04')
-        assert OS(name='ubuntu', codename='trusty', version='14.04') == os
-
-    def test_eq_not_equal(self):
-        os = OS(name='ubuntu', codename='trusty', version='16.04')
-        assert OS(name='ubuntu', codename='trusty', version='14.04') != os
diff --git a/teuthology/orchestra/test/test_remote.py b/teuthology/orchestra/test/test_remote.py
deleted file mode 100644 (file)
index 1328bc8..0000000
+++ /dev/null
@@ -1,259 +0,0 @@
-from mock import patch, Mock, MagicMock
-from pytest import raises
-
-from io import BytesIO
-
-from teuthology.orchestra import remote
-from teuthology.orchestra import opsys
-from teuthology.orchestra.run import RemoteProcess
-from teuthology.exceptions import CommandFailedError, UnitTestError
-
-
-class TestRemote(object):
-
-    def setup_method(self):
-        self.start_patchers()
-
-    def teardown_method(self):
-        self.stop_patchers()
-
-    def start_patchers(self):
-        self.m_ssh = MagicMock()
-        self.patcher_ssh = patch(
-            'teuthology.orchestra.connection.paramiko.SSHClient',
-            self.m_ssh,
-        )
-        self.patcher_ssh.start()
-
-    def stop_patchers(self):
-        self.patcher_ssh.stop()
-
-    def test_shortname(self):
-        r = remote.Remote(
-            name='jdoe@xyzzy.example.com',
-            shortname='xyz',
-            ssh=self.m_ssh,
-            )
-        assert r.shortname == 'xyz'
-        assert str(r) == 'jdoe@xyzzy.example.com'
-
-    def test_shortname_default(self):
-        r = remote.Remote(
-            name='jdoe@xyzzy.example.com',
-            ssh=self.m_ssh,
-            )
-        assert r.shortname == 'xyzzy'
-        assert str(r) == 'jdoe@xyzzy.example.com'
-
-    def test_run(self):
-        m_transport = MagicMock()
-        m_transport.getpeername.return_value = ('name', 22)
-        self.m_ssh.get_transport.return_value = m_transport
-        m_run = MagicMock()
-        args = [
-            'something',
-            'more',
-            ]
-        proc = RemoteProcess(
-            client=self.m_ssh,
-            args=args,
-            )
-        m_run.return_value = proc
-        rem = remote.Remote(name='jdoe@xyzzy.example.com', ssh=self.m_ssh)
-        rem._runner = m_run
-        result = rem.run(args=args)
-        m_transport.getpeername.assert_called_once_with()
-        m_run_call_kwargs = m_run.call_args_list[0][1]
-        assert m_run_call_kwargs['args'] == args
-        assert result is proc
-        assert result.remote is rem
-
-    @patch('teuthology.util.scanner.UnitTestScanner.scan_and_write')
-    def test_run_unit_test(self, m_scan_and_write):
-        m_transport = MagicMock()
-        m_transport.getpeername.return_value = ('name', 22)
-        self.m_ssh.get_transport.return_value = m_transport
-        m_run = MagicMock(name="run", side_effect=CommandFailedError('mocked error', 1, 'smithi'))
-        args = [
-            'something',
-            'more',
-        ]
-        rem = remote.Remote(name='jdoe@xyzzy.example.com', ssh=self.m_ssh)
-        rem._runner = m_run
-        m_scan_and_write.return_value = "Error Message"
-        with raises(UnitTestError) as exc:
-            rem.run_unit_test(args=args, xml_path_regex="xml_path", output_yaml="yaml_path")
-        assert str(exc.value) == "Unit test failed on smithi with status 1: 'Error Message'"
-
-    def test_hostname(self):
-        m_transport = MagicMock()
-        m_transport.getpeername.return_value = ('name', 22)
-        self.m_ssh.get_transport.return_value = m_transport
-        m_run = MagicMock()
-        args = [
-            'hostname',
-            '--fqdn',
-            ]
-        stdout = BytesIO(b'test_hostname')
-        stdout.seek(0)
-        proc = RemoteProcess(
-            client=self.m_ssh,
-            args=args,
-            )
-        proc.stdout = stdout
-        proc._stdout_buf = Mock()
-        proc._stdout_buf.channel.recv_exit_status.return_value = 0
-        r = remote.Remote(name='xyzzy.example.com', ssh=self.m_ssh)
-        m_run.return_value = proc
-        r._runner = m_run
-        assert r.hostname == 'test_hostname'
-
-    def test_arch(self):
-        m_transport = MagicMock()
-        m_transport.getpeername.return_value = ('name', 22)
-        self.m_ssh.get_transport.return_value = m_transport
-        m_run = MagicMock()
-        args = [
-            'uname',
-            '-m',
-            ]
-        stdout = BytesIO(b'test_arch')
-        stdout.seek(0)
-        proc = RemoteProcess(
-            client=self.m_ssh,
-            args=args,
-            )
-        proc._stdout_buf = Mock()
-        proc._stdout_buf.channel = Mock()
-        proc._stdout_buf.channel.recv_exit_status.return_value = 0
-        proc._stdout_buf.channel.expects('recv_exit_status').returns(0)
-        proc.stdout = stdout
-        m_run.return_value = proc
-        r = remote.Remote(name='jdoe@xyzzy.example.com', ssh=self.m_ssh)
-        r._runner = m_run
-        assert r.arch == 'test_arch'
-        assert len(m_run.call_args_list) == 1
-        m_run_call_kwargs = m_run.call_args_list[0][1]
-        assert m_run_call_kwargs['client'] == self.m_ssh
-        assert m_run_call_kwargs['name'] == r.shortname
-        assert m_run_call_kwargs['args'] == ' '.join(args)
-
-    def test_host_key(self):
-        m_key = MagicMock()
-        m_key.get_name.return_value = 'key_type'
-        m_key.get_base64.return_value = 'test ssh key'
-        m_transport = MagicMock()
-        m_transport.get_remote_server_key.return_value = m_key
-        self.m_ssh.get_transport.return_value = m_transport
-        r = remote.Remote(name='jdoe@xyzzy.example.com', ssh=self.m_ssh)
-        assert r.host_key == 'key_type test ssh key'
-        self.m_ssh.get_transport.assert_called_once_with()
-        m_transport.get_remote_server_key.assert_called_once_with()
-
-    def test_inventory_info(self):
-        r = remote.Remote('user@host', host_key='host_key')
-        r._arch = 'arch'
-        r._os = opsys.OS(name='os_name', version='1.2.3', codename='code')
-        inv_info = r.inventory_info
-        assert inv_info == dict(
-            name='host',
-            user='user',
-            arch='arch',
-            os_type='os_name',
-            os_version='1.2',
-            ssh_pub_key='host_key',
-            up=True,
-        )
-
-    def test_sftp_open_file(self):
-        m_file_obj = MagicMock()
-        m_stat = Mock()
-        m_stat.st_size = 42
-        m_file_obj.stat.return_value = m_stat
-        m_open = MagicMock()
-        m_open.return_value = m_file_obj
-        m_open.return_value.__enter__.return_value = m_file_obj
-        with patch.object(remote.Remote, '_sftp_open_file', new=m_open):
-            rem = remote.Remote(name='jdoe@xyzzy.example.com', ssh=self.m_ssh)
-            assert rem._sftp_open_file('x') is m_file_obj
-            assert rem._sftp_open_file('x').stat() is m_stat
-            assert rem._sftp_open_file('x').stat().st_size == 42
-            with rem._sftp_open_file('x') as f:
-                assert f == m_file_obj
-
-    def test_sftp_get_size(self):
-        m_file_obj = MagicMock()
-        m_stat = Mock()
-        m_stat.st_size = 42
-        m_file_obj.stat.return_value = m_stat
-        m_open = MagicMock()
-        m_open.return_value = m_file_obj
-        m_open.return_value.__enter__.return_value = m_file_obj
-        with patch.object(remote.Remote, '_sftp_open_file', new=m_open):
-            rem = remote.Remote(name='jdoe@xyzzy.example.com', ssh=self.m_ssh)
-            assert rem._sftp_get_size('/fake/file') == 42
-
-    def test_format_size(self):
-        assert remote.Remote._format_size(1023).strip() == '1023B'
-        assert remote.Remote._format_size(1024).strip() == '1KB'
-        assert remote.Remote._format_size(1024**2).strip() == '1MB'
-        assert remote.Remote._format_size(1024**5).strip() == '1TB'
-        assert remote.Remote._format_size(1021112).strip() == '997KB'
-        assert remote.Remote._format_size(1021112**2).strip() == '971GB'
-
-    def test_is_container(self):
-        m_transport = MagicMock()
-        m_transport.getpeername.return_value = ('name', 22)
-        self.m_ssh.get_transport.return_value = m_transport
-        m_run = MagicMock()
-        args = []
-        proc = RemoteProcess(
-            client=self.m_ssh,
-            args=args,
-        )
-        proc.returncode = 0
-        m_run.return_value = proc
-        rem = remote.Remote(name='jdoe@xyzzy.example.com', ssh=self.m_ssh)
-        rem._runner = m_run
-        assert rem.is_container
-        proc.returncode = 1
-        rem2 = remote.Remote(name='jdoe@xyzzy.example.com', ssh=self.m_ssh)
-        rem2._runner = m_run
-        assert not rem2.is_container
-
-    @patch("teuthology.orchestra.remote.Remote.sh")
-    def test_resolve_ip(self, m_sh):
-        r = remote.Remote(name="jdoe@xyzzy.example.com", ssh=self.m_ssh)
-        m_sh.return_value = "smithi001.front.sepia.ceph.com has address 172.21.15.1\n"
-        ip4 = remote.Remote.resolve_ip(r, 'smithi001')
-        assert ip4 == "172.21.15.1"
-        m_sh.return_value = "\n"
-        try:
-            ip4 = remote.Remote.resolve_ip(r, 'smithi001')
-        except Exception as e:
-            assert 'Cannot get IPv4 address' in str(e)
-        try:
-            ip4 = remote.Remote.resolve_ip(r, 'smithi001', 5)
-        except Exception as e:
-            assert 'Unknown IP version' in str(e)
-        
-        m_sh.return_value = ("google.com has address 142.251.37.14\n"
-                             "google.com has IPv6 address 2a00:1450:4016:80b::200e\n"
-                             "google.com mail is handled by 10 smtp.google.com.\n")
-        ip4 = remote.Remote.resolve_ip(r, 'google.com')
-        assert ip4 == "142.251.37.14"
-        ip6 = remote.Remote.resolve_ip(r, 'google.com', '6')
-        assert ip6 == "2a00:1450:4016:80b::200e"
-
-
-    @patch("teuthology.orchestra.remote.Remote.run")
-    def test_write_file(self, m_run):
-        file = "fakefile"
-        contents = "fakecontents"
-        rem = remote.Remote(name='jdoe@xyzzy.example.com', ssh=self.m_ssh)
-
-        remote.Remote.write_file(rem, file, contents, bs=1, offset=1024)
-        m_run.assert_called_with(args=f"set -ex\ndd of={file} bs=1 seek=1024", stdin=contents, quiet=True)
-
-        remote.Remote.write_file(rem, file, contents, sync=True)
-        m_run.assert_called_with(args=f"set -ex\ndd of={file} conv=sync", stdin=contents, quiet=True)
diff --git a/teuthology/orchestra/test/test_run.py b/teuthology/orchestra/test/test_run.py
deleted file mode 100644 (file)
index e8051cc..0000000
+++ /dev/null
@@ -1,286 +0,0 @@
-from io import BytesIO
-
-import paramiko
-import socket
-
-from mock import MagicMock, patch
-from pytest import raises
-
-from teuthology.orchestra import run
-from teuthology.exceptions import (CommandCrashedError, CommandFailedError,
-                                   ConnectionLostError)
-
-def set_buffer_contents(buf, contents):
-    buf.seek(0)
-    if isinstance(contents, bytes):
-        buf.write(contents)
-    elif isinstance(contents, (list, tuple)):
-        buf.writelines(contents)
-    elif isinstance(contents, str):
-        buf.write(contents.encode())
-    else:
-        raise TypeError(
-            "%s is a %s; should be a byte string, list or tuple" % (
-                contents, type(contents)
-            )
-        )
-    buf.seek(0)
-
-
-class TestRun(object):
-    def setup_method(self):
-        self.start_patchers()
-
-    def teardown_method(self):
-        self.stop_patchers()
-
-    def start_patchers(self):
-        self.m_remote_process = MagicMock(wraps=run.RemoteProcess)
-        self.patcher_remote_proc = patch(
-            'teuthology.orchestra.run.RemoteProcess',
-            self.m_remote_process,
-        )
-        self.m_channel = MagicMock(spec=paramiko.Channel)()
-        """
-        self.m_channelfile = MagicMock(wraps=paramiko.ChannelFile)
-        self.m_stdin_buf = self.m_channelfile(self.m_channel())
-        self.m_stdout_buf = self.m_channelfile(self.m_channel())
-        self.m_stderr_buf = self.m_channelfile(self.m_channel())
-        """
-        class M_ChannelFile(BytesIO):
-            channel = MagicMock(spec=paramiko.Channel)()
-
-        self.m_channelfile = M_ChannelFile
-        self.m_stdin_buf = self.m_channelfile()
-        self.m_stdout_buf = self.m_channelfile()
-        self.m_stderr_buf = self.m_channelfile()
-        self.m_ssh = MagicMock()
-        self.m_ssh.exec_command.return_value = (
-            self.m_stdin_buf,
-            self.m_stdout_buf,
-            self.m_stderr_buf,
-        )
-        self.m_transport = MagicMock()
-        self.m_transport.getpeername.return_value = ('name', 22)
-        self.m_ssh.get_transport.return_value = self.m_transport
-        self.patcher_ssh = patch(
-            'teuthology.orchestra.connection.paramiko.SSHClient',
-            self.m_ssh,
-        )
-        self.patcher_ssh.start()
-        # Tests must start this if they wish to use it
-        # self.patcher_remote_proc.start()
-
-    def stop_patchers(self):
-        # If this patcher wasn't started, it's ok
-        try:
-            self.patcher_remote_proc.stop()
-        except RuntimeError:
-            pass
-        self.patcher_ssh.stop()
-
-    def test_exitstatus(self):
-        self.m_stdout_buf.channel.recv_exit_status.return_value = 0
-        proc = run.run(
-            client=self.m_ssh,
-            args=['foo', 'bar baz'],
-        )
-        assert proc.exitstatus == 0
-
-    def test_run_cwd(self):
-        self.m_stdout_buf.channel.recv_exit_status.return_value = 0
-        run.run(
-            client=self.m_ssh,
-            args=['foo_bar_baz'],
-            cwd='/cwd/test',
-        )
-        self.m_ssh.exec_command.assert_called_with('(cd /cwd/test && exec foo_bar_baz)')
-
-    def test_capture_stdout(self):
-        output = 'foo\nbar'
-        set_buffer_contents(self.m_stdout_buf, output)
-        self.m_stdout_buf.channel.recv_exit_status.return_value = 0
-        stdout = BytesIO()
-        proc = run.run(
-            client=self.m_ssh,
-            args=['foo', 'bar baz'],
-            stdout=stdout,
-        )
-        assert proc.stdout is stdout
-        assert proc.stdout.read().decode() == output
-        assert proc.stdout.getvalue().decode() == output
-
-    def test_capture_stderr_newline(self):
-        output = 'foo\nbar\n'
-        set_buffer_contents(self.m_stderr_buf, output)
-        self.m_stderr_buf.channel.recv_exit_status.return_value = 0
-        stderr = BytesIO()
-        proc = run.run(
-            client=self.m_ssh,
-            args=['foo', 'bar baz'],
-            stderr=stderr,
-        )
-        assert proc.stderr is stderr
-        assert proc.stderr.read().decode() == output
-        assert proc.stderr.getvalue().decode() == output
-
-    def test_status_bad(self):
-        self.m_stdout_buf.channel.recv_exit_status.return_value = 42
-        with raises(CommandFailedError) as exc:
-            run.run(
-                client=self.m_ssh,
-                args=['foo'],
-            )
-        assert str(exc.value) == "Command failed on name with status 42: 'foo'"
-
-    def test_status_bad_nocheck(self):
-        self.m_stdout_buf.channel.recv_exit_status.return_value = 42
-        proc = run.run(
-            client=self.m_ssh,
-            args=['foo'],
-            check_status=False,
-        )
-        assert proc.exitstatus == 42
-
-    def test_status_crash(self):
-        self.m_stdout_buf.channel.recv_exit_status.return_value = -1
-        with raises(CommandCrashedError) as exc:
-            run.run(
-                client=self.m_ssh,
-                args=['foo'],
-            )
-        assert str(exc.value) == "Command crashed: 'foo'"
-
-    def test_status_crash_nocheck(self):
-        self.m_stdout_buf.channel.recv_exit_status.return_value = -1
-        proc = run.run(
-            client=self.m_ssh,
-            args=['foo'],
-            check_status=False,
-        )
-        assert proc.exitstatus == -1
-
-    def test_status_lost(self):
-        m_transport = MagicMock()
-        m_transport.getpeername.return_value = ('name', 22)
-        m_transport.is_active.return_value = False
-        self.m_stdout_buf.channel.recv_exit_status.return_value = -1
-        self.m_ssh.get_transport.return_value = m_transport
-        with raises(ConnectionLostError) as exc:
-            run.run(
-                client=self.m_ssh,
-                args=['foo'],
-            )
-        assert str(exc.value) == "SSH connection to name was lost: 'foo'"
-
-    def test_status_lost_socket(self):
-        m_transport = MagicMock()
-        m_transport.getpeername.side_effect = socket.error
-        self.m_ssh.get_transport.return_value = m_transport
-        with raises(ConnectionLostError) as exc:
-            run.run(
-                client=self.m_ssh,
-                args=['foo'],
-            )
-        assert str(exc.value) == "SSH connection was lost: 'foo'"
-
-    def test_status_lost_nocheck(self):
-        m_transport = MagicMock()
-        m_transport.getpeername.return_value = ('name', 22)
-        m_transport.is_active.return_value = False
-        self.m_stdout_buf.channel.recv_exit_status.return_value = -1
-        self.m_ssh.get_transport.return_value = m_transport
-        proc = run.run(
-            client=self.m_ssh,
-            args=['foo'],
-            check_status=False,
-        )
-        assert proc.exitstatus == -1
-
-    def test_status_bad_nowait(self):
-        self.m_stdout_buf.channel.recv_exit_status.return_value = 42
-        proc = run.run(
-            client=self.m_ssh,
-            args=['foo'],
-            wait=False,
-        )
-        with raises(CommandFailedError) as exc:
-            proc.wait()
-        assert proc.returncode == 42
-        assert str(exc.value) == "Command failed on name with status 42: 'foo'"
-
-    def test_stdin_pipe(self):
-        self.m_stdout_buf.channel.recv_exit_status.return_value = 0
-        proc = run.run(
-            client=self.m_ssh,
-            args=['foo'],
-            stdin=run.PIPE,
-            wait=False
-        )
-        assert proc.poll() == 0
-        code = proc.wait()
-        assert code == 0
-        assert proc.exitstatus == 0
-
-    def test_stdout_pipe(self):
-        self.m_stdout_buf.channel.recv_exit_status.return_value = 0
-        lines = [b'one\n', b'two', b'']
-        set_buffer_contents(self.m_stdout_buf, lines)
-        proc = run.run(
-            client=self.m_ssh,
-            args=['foo'],
-            stdout=run.PIPE,
-            wait=False
-        )
-        assert proc.poll() == 0
-        assert proc.stdout.readline() == lines[0]
-        assert proc.stdout.readline() == lines[1]
-        assert proc.stdout.readline() == lines[2]
-        code = proc.wait()
-        assert code == 0
-        assert proc.exitstatus == 0
-
-    def test_stderr_pipe(self):
-        self.m_stdout_buf.channel.recv_exit_status.return_value = 0
-        lines = [b'one\n', b'two', b'']
-        set_buffer_contents(self.m_stderr_buf, lines)
-        proc = run.run(
-            client=self.m_ssh,
-            args=['foo'],
-            stderr=run.PIPE,
-            wait=False
-        )
-        assert proc.poll() == 0
-        assert proc.stderr.readline() == lines[0]
-        assert proc.stderr.readline() == lines[1]
-        assert proc.stderr.readline() == lines[2]
-        code = proc.wait()
-        assert code == 0
-        assert proc.exitstatus == 0
-
-    def test_copy_and_close(self):
-        run.copy_and_close(None, MagicMock())
-        run.copy_and_close('', MagicMock())
-        run.copy_and_close(b'', MagicMock())
-
-
-class TestQuote(object):
-    def test_quote_simple(self):
-        got = run.quote(['a b', ' c', 'd e '])
-        assert got == "'a b' ' c' 'd e '"
-
-    def test_quote_and_quote(self):
-        got = run.quote(['echo', 'this && is embedded', '&&',
-                         'that was standalone'])
-        assert got == "echo 'this && is embedded' '&&' 'that was standalone'"
-
-    def test_quote_and_raw(self):
-        got = run.quote(['true', run.Raw('&&'), 'echo', 'yay'])
-        assert got == "true && echo yay"
-
-
-class TestRaw(object):
-    def test_eq(self):
-        str_ = "I am a raw something or other"
-        raw = run.Raw(str_)
-        assert raw == run.Raw(str_)
diff --git a/teuthology/orchestra/test/test_systemd.py b/teuthology/orchestra/test/test_systemd.py
deleted file mode 100644 (file)
index c7cb342..0000000
+++ /dev/null
@@ -1,54 +0,0 @@
-import argparse
-import os
-
-from logging import debug
-from teuthology import misc
-from teuthology.orchestra import cluster
-from teuthology.orchestra.run import quote
-from teuthology.orchestra.daemon.group import DaemonGroup
-import subprocess
-
-
-class FakeRemote(object):
-    pass
-
-
-def test_pid():
-    ctx = argparse.Namespace()
-    ctx.daemons = DaemonGroup(use_systemd=True)
-    remote = FakeRemote()
-
-    ps_ef_output_path = os.path.join(
-        os.path.dirname(__file__),
-        "files/daemon-systemdstate-pid-ps-ef.output"
-    )
-
-    # patching ps -ef command output using a file
-    def sh(args):
-        args[0:2] = ["cat", ps_ef_output_path]
-        debug(args)
-        return subprocess.getoutput(quote(args))
-
-    remote.sh = sh
-    remote.init_system = 'systemd'
-    remote.shortname = 'host1'
-
-    ctx.cluster = cluster.Cluster(
-        remotes=[
-            (remote, ['rgw.0', 'mon.a', 'mgr.a', 'mds.a', 'osd.0'])
-        ],
-    )
-
-    for remote, roles in ctx.cluster.remotes.items():
-        for role in roles:
-            _, rol, id_ = misc.split_role(role)
-            if any(rol.startswith(x) for x in ['mon', 'mgr', 'mds']):
-                ctx.daemons.register_daemon(remote, rol, remote.shortname)
-            else:
-                ctx.daemons.register_daemon(remote, rol, id_)
-
-    for _, daemons in ctx.daemons.daemons.items():
-        for daemon in daemons.values():
-            pid = daemon.pid
-            debug(pid)
-            assert pid
diff --git a/teuthology/orchestra/test/util.py b/teuthology/orchestra/test/util.py
deleted file mode 100644 (file)
index 4aedc2e..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-def assert_raises(excClass, callableObj, *args, **kwargs):
-    """
-    Like unittest.TestCase.assertRaises, but returns the exception.
-    """
-    try:
-        callableObj(*args, **kwargs)
-    except excClass as e:
-        return e
-    else:
-        if hasattr(excClass,'__name__'): excName = excClass.__name__
-        else: excName = str(excClass)
-        raise AssertionError("%s not raised" % excName)
diff --git a/teuthology/provision/cloud/test/test_base.py b/teuthology/provision/cloud/test/test_base.py
deleted file mode 100644 (file)
index 67e8380..0000000
+++ /dev/null
@@ -1,90 +0,0 @@
-from libcloud.compute.providers import get_driver
-from mock import patch
-
-from teuthology.config import config
-from teuthology.provision import cloud
-
-from test_cloud_init import dummy_config, dummy_drivers
-
-
-class TestBase(object):
-    def setup_method(self):
-        config.load()
-        config.libcloud = dummy_config
-        cloud.supported_drivers['dummy'] = dummy_drivers
-
-    def teardown_method(self):
-        del cloud.supported_drivers['dummy']
-
-
-class TestProvider(TestBase):
-    def test_init(self):
-        obj = cloud.get_provider('my_provider')
-        assert obj.name == 'my_provider'
-        assert obj.driver_name == 'dummy'
-        assert obj.conf == dummy_config['providers']['my_provider']
-
-    def test_driver(self):
-        obj = cloud.get_provider('my_provider')
-        assert isinstance(obj.driver, get_driver('dummy'))
-
-
-class TestProvisioner(TestBase):
-    klass = cloud.base.Provisioner
-
-    def get_obj(
-            self, name='node_name', os_type='ubuntu', os_version='ubuntu'):
-        return cloud.get_provisioner(
-            'my_provider',
-            'node_name',
-            'ubuntu',
-            '16.04',
-        )
-
-    def test_init_provider_string(self):
-        obj = self.klass('my_provider', 'ubuntu', '16.04')
-        assert obj.provider.name == 'my_provider'
-
-    def test_create(self):
-        obj = self.get_obj()
-        with patch.object(
-            self.klass,
-            '_create',
-        ) as m_create:
-            for val in [True, False]:
-                m_create.return_value = val
-                res = obj.create()
-                assert res is val
-                m_create.assert_called_once_with()
-                m_create.reset_mock()
-            m_create.side_effect = RuntimeError
-            res = obj.create()
-            assert res is False
-        assert obj.create() is None
-
-    def test_destroy(self):
-        obj = self.get_obj()
-        with patch.object(
-            self.klass,
-            '_destroy',
-        ) as m_destroy:
-            for val in [True, False]:
-                m_destroy.return_value = val
-                res = obj.destroy()
-                assert res is val
-                m_destroy.assert_called_once_with()
-                m_destroy.reset_mock()
-            m_destroy.side_effect = RuntimeError
-            res = obj.destroy()
-            assert res is False
-        assert obj.destroy() is None
-
-    def test_remote(self):
-        obj = self.get_obj()
-        assert obj.remote.shortname == 'node_name'
-
-    def test_repr(self):
-        obj = self.get_obj()
-        assert repr(obj) == \
-            "Provisioner(provider='my_provider', name='node_name', os_type='ubuntu', os_version='16.04')"  # noqa
-
diff --git a/teuthology/provision/cloud/test/test_cloud_init.py b/teuthology/provision/cloud/test/test_cloud_init.py
deleted file mode 100644 (file)
index 1b0df96..0000000
+++ /dev/null
@@ -1,60 +0,0 @@
-from teuthology.config import config
-from teuthology.provision import cloud
-
-dummy_config = dict(
-    providers=dict(
-        my_provider=dict(
-            driver='dummy',
-            driver_args=dict(
-                creds=0,
-            ),
-            conf_1='1',
-            conf_2='2',
-        )
-    )
-)
-
-
-class DummyProvider(cloud.base.Provider):
-        # For libcloud's dummy driver
-        _driver_posargs = ['creds']
-
-dummy_drivers = dict(
-    provider=DummyProvider,
-    provisioner=cloud.base.Provisioner,
-)
-
-
-class TestInit(object):
-    def setup_method(self):
-        config.load()
-        config.libcloud = dummy_config
-        cloud.supported_drivers['dummy'] = dummy_drivers
-
-    def teardown_method(self):
-        del cloud.supported_drivers['dummy']
-
-    def test_get_types(self):
-        assert list(cloud.get_types()) == ['my_provider']
-
-    def test_get_provider_conf(self):
-        expected = dummy_config['providers']['my_provider']
-        assert cloud.get_provider_conf('my_provider') == expected
-
-    def test_get_provider(self):
-        obj = cloud.get_provider('my_provider')
-        assert obj.name == 'my_provider'
-        assert obj.driver_name == 'dummy'
-
-    def test_get_provisioner(self):
-        obj = cloud.get_provisioner(
-            'my_provider',
-            'node_name',
-            'ubuntu',
-            '16.04',
-            dict(foo='bar'),
-        )
-        assert obj.provider.name == 'my_provider'
-        assert obj.name == 'node_name'
-        assert obj.os_type == 'ubuntu'
-        assert obj.os_version == '16.04'
diff --git a/teuthology/provision/cloud/test/test_cloud_util.py b/teuthology/provision/cloud/test/test_cloud_util.py
deleted file mode 100644 (file)
index 3093d2c..0000000
+++ /dev/null
@@ -1,172 +0,0 @@
-import datetime
-import dateutil
-import json
-
-from mock import patch, mock_open
-from pytest import mark
-
-from teuthology.provision.cloud import util
-
-
-@mark.parametrize(
-    'path, exists',
-    [
-        ('/fake/path', True),
-        ('/fake/path', False),
-    ]
-)
-def test_get_user_ssh_pubkey(path, exists):
-    with patch('os.path.exists') as m_exists:
-        m_exists.return_value = exists
-        with patch('teuthology.provision.cloud.util.open', mock_open(), create=True) as m_open:
-            util.get_user_ssh_pubkey(path)
-            if exists:
-                m_open.assert_called_once_with(path)
-
-
-@mark.parametrize(
-    'input_, func, expected',
-    [
-        [
-            [
-                dict(sub0=dict(key0=0, key1=0)),
-                dict(sub0=dict(key1=1, key2=2)),
-            ],
-            lambda x, y: x > y,
-            dict(sub0=dict(key0=0, key1=1, key2=2))
-        ],
-        [
-            [
-                dict(),
-                dict(sub0=dict(key1=1, key2=2)),
-            ],
-            lambda x, y: x > y,
-            dict(sub0=dict(key1=1, key2=2))
-        ],
-        [
-            [
-                dict(sub0=dict(key1=1, key2=2)),
-                dict(),
-            ],
-            lambda x, y: x > y,
-            dict(sub0=dict(key1=1, key2=2))
-        ],
-        [
-            [
-                dict(sub0=dict(key0=0, key1=0, key2=0)),
-                dict(sub0=dict(key0=1, key2=3), sub1=dict(key0=0)),
-                dict(sub0=dict(key0=3, key1=2, key2=1)),
-                dict(sub0=dict(key1=3),
-                     sub1=dict(key0=3, key1=0)),
-            ],
-            lambda x, y: x > y,
-            dict(sub0=dict(key0=3, key1=3, key2=3),
-                 sub1=dict(key0=3, key1=0))
-        ],
-    ]
-)
-def test_combine_dicts(input_, func, expected):
-    assert util.combine_dicts(input_, func) == expected
-
-
-def get_datetime(offset_hours=0):
-    delta = datetime.timedelta(hours=offset_hours)
-    return datetime.datetime.now(dateutil.tz.tzutc()) + delta
-
-
-def get_datetime_string(offset_hours=0):
-    obj = get_datetime(offset_hours)
-    return obj.strftime(util.AuthToken.time_format)
-
-
-class TestAuthToken(object):
-    klass = util.AuthToken
-
-    def setup_method(self):
-        default_expires = get_datetime_string(0)
-        self.test_data = dict(
-            value='token_value',
-            endpoint='endpoint',
-            expires=default_expires,
-        )
-        self.patchers = dict()
-        self.patchers['m_open'] = patch(
-            'teuthology.provision.cloud.util.open'
-        )
-        self.patchers['m_exists'] = patch(
-            'os.path.exists'
-        )
-        self.patchers['m_file_lock'] = patch(
-            'teuthology.provision.cloud.util.FileLock'
-        )
-        self.mocks = dict()
-        for name, patcher in self.patchers.items():
-            self.mocks[name] = patcher.start()
-
-    def teardown_method(self):
-        for patcher in self.patchers.values():
-            patcher.stop()
-
-    def get_obj(self, name='name', directory='/fake/directory'):
-        return self.klass(
-            name=name,
-            directory=directory,
-        )
-
-    def test_no_token(self):
-        obj = self.get_obj()
-        self.mocks['m_exists'].return_value = False
-        with obj:
-            assert obj.value is None
-            assert obj.expired is True
-
-    @mark.parametrize(
-        'test_data, expired',
-        [
-            [
-                dict(
-                    value='token_value',
-                    endpoint='endpoint',
-                    expires=get_datetime_string(-1),
-                ),
-                True
-            ],
-            [
-                dict(
-                    value='token_value',
-                    endpoint='endpoint',
-                    expires=get_datetime_string(1),
-                ),
-                False
-            ],
-        ]
-    )
-    def test_token_read(self, test_data, expired):
-        obj = self.get_obj()
-        self.mocks['m_exists'].return_value = True
-        self.mocks['m_open'].return_value.__enter__.return_value.read.return_value = \
-            json.dumps(test_data)
-        with obj:
-            if expired:
-                assert obj.value is None
-                assert obj.expired is True
-            else:
-                assert obj.value == test_data['value']
-
-    def test_token_write(self):
-        obj = self.get_obj()
-        datetime_obj = get_datetime(0)
-        datetime_string = get_datetime_string(0)
-        self.mocks['m_exists'].return_value = False
-        with obj:
-            obj.write('value', datetime_obj, 'endpoint')
-        m_open = self.mocks['m_open']
-        write_calls = m_open.return_value.__enter__.return_value.write\
-            .call_args_list
-        assert len(write_calls) == 1
-        expected = json.dumps(dict(
-            value='value',
-            expires=datetime_string,
-            endpoint='endpoint',
-        ))
-        assert write_calls[0][0][0] == expected
diff --git a/teuthology/provision/cloud/test/test_openstack.py b/teuthology/provision/cloud/test/test_openstack.py
deleted file mode 100644 (file)
index c152105..0000000
+++ /dev/null
@@ -1,781 +0,0 @@
-import socket
-import yaml
-import os
-
-from teuthology.util.compat import parse_qs
-
-from copy import deepcopy
-from libcloud.compute.providers import get_driver
-from mock import patch, Mock, DEFAULT
-from pytest import raises, mark
-
-from teuthology.config import config
-from teuthology.exceptions import MaxWhileTries
-from teuthology.provision import cloud
-
-test_config = dict(
-    providers=dict(
-        my_provider=dict(
-            driver='openstack',
-            driver_args=dict(
-                username='user',
-                password='password',
-                ex_force_auth_url='http://127.0.0.1:9999/v2.0/tokens',
-            ),
-        ),
-        image_exclude_provider=dict(
-            driver='openstack',
-            exclude_image=['.*-exclude1', '.*-exclude2'],
-            driver_args=dict(
-                username='user',
-                password='password',
-                ex_force_auth_url='http://127.0.0.1:9999/v2.0/tokens',
-            ),
-        )
-    )
-)
-
-
-@patch('time.sleep')
-def test_retry(m_sleep):
-    orig_exceptions = cloud.openstack.RETRY_EXCEPTIONS
-    new_exceptions = orig_exceptions + (RuntimeError, )
-
-    class test_cls(object):
-        def __init__(self, min_val):
-            self.min_val = min_val
-            self.cur_val = 0
-
-        def func(self):
-            self.cur_val += 1
-            if self.cur_val < self.min_val:
-                raise RuntimeError
-            return self.cur_val
-
-    with patch.object(
-        cloud.openstack,
-        'RETRY_EXCEPTIONS',
-        new=new_exceptions,
-    ):
-        test_obj = test_cls(min_val=5)
-        assert cloud.openstack.retry(test_obj.func) == 5
-        test_obj = test_cls(min_val=1000)
-        with raises(MaxWhileTries):
-            cloud.openstack.retry(test_obj.func)
-
-
-def get_fake_obj(mock_args=None, attributes=None):
-    if mock_args is None:
-        mock_args = dict()
-    if attributes is None:
-        attributes = dict()
-    obj = Mock(**mock_args)
-    for name, value in attributes.items():
-        setattr(obj, name, value)
-    return obj
-
-
-class TestOpenStackBase(object):
-    def setup_method(self):
-        config.load(dict(libcloud=deepcopy(test_config)))
-        self.start_patchers()
-
-    def start_patchers(self):
-        self.patchers = dict()
-        self.patchers['m_list_images'] = patch(
-            'libcloud.compute.drivers.openstack'
-            '.OpenStackNodeDriver.list_images'
-        )
-        self.patchers['m_list_sizes'] = patch(
-            'libcloud.compute.drivers.openstack'
-            '.OpenStackNodeDriver.list_sizes'
-        )
-        self.patchers['m_ex_list_networks'] = patch(
-            'libcloud.compute.drivers.openstack'
-            '.OpenStack_1_1_NodeDriver.ex_list_networks'
-        )
-        self.patchers['m_ex_list_security_groups'] = patch(
-            'libcloud.compute.drivers.openstack'
-            '.OpenStack_1_1_NodeDriver.ex_list_security_groups'
-        )
-        self.patchers['m_get_user_ssh_pubkey'] = patch(
-            'teuthology.provision.cloud.util.get_user_ssh_pubkey'
-        )
-        self.patchers['m_list_nodes'] = patch(
-            'libcloud.compute.drivers.openstack'
-            '.OpenStackNodeDriver.list_nodes'
-        )
-        self.patchers['m_create_node'] = patch(
-            'libcloud.compute.drivers.openstack'
-            '.OpenStack_1_1_NodeDriver.create_node'
-        )
-        self.patchers['m_wait_until_running'] = patch(
-            'libcloud.compute.drivers.openstack'
-            '.OpenStackNodeDriver.wait_until_running'
-        )
-        self.patchers['m_create_volume'] = patch(
-            'libcloud.compute.drivers.openstack'
-            '.OpenStackNodeDriver.create_volume'
-        )
-        self.patchers['m_attach_volume'] = patch(
-            'libcloud.compute.drivers.openstack'
-            '.OpenStackNodeDriver.attach_volume'
-        )
-        self.patchers['m_detach_volume'] = patch(
-            'libcloud.compute.drivers.openstack'
-            '.OpenStackNodeDriver.detach_volume'
-        )
-        self.patchers['m_list_volumes'] = patch(
-            'libcloud.compute.drivers.openstack'
-            '.OpenStackNodeDriver.list_volumes'
-        )
-        self.patchers['m_destroy_volume'] = patch(
-            'libcloud.compute.drivers.openstack'
-            '.OpenStackNodeDriver.destroy_volume'
-        )
-        self.patchers['m_get_service_catalog'] = patch(
-            'libcloud.common.openstack'
-            '.OpenStackBaseConnection.get_service_catalog'
-        )
-        self.patchers['m_auth_token'] = patch(
-            'teuthology.provision.cloud.util.AuthToken'
-        )
-        self.patchers['m_get_endpoint'] = patch(
-            'libcloud.common.openstack'
-            '.OpenStackBaseConnection.get_endpoint',
-        )
-        self.patchers['m_connect'] = patch(
-            'libcloud.common.base'
-            '.Connection.connect',
-        )
-        self.patchers['m_sleep'] = patch(
-            'time.sleep'
-        )
-        self.patchers['m_get'] = patch(
-            'requests.get'
-        )
-        self.mocks = dict()
-        for name, patcher in self.patchers.items():
-            self.mocks[name] = patcher.start()
-        self.mocks['m_get_endpoint'].return_value = 'endpoint'
-
-    def teardown_method(self):
-        for patcher in self.patchers.values():
-            patcher.stop()
-
-
-class TestOpenStackProvider(TestOpenStackBase):
-    klass = cloud.openstack.OpenStackProvider
-
-    def test_init(self):
-        obj = cloud.get_provider('my_provider')
-        assert obj.name == 'my_provider'
-        assert obj.driver_name == 'openstack'
-        assert obj.conf == test_config['providers']['my_provider']
-
-    def test_driver(self):
-        token = self.mocks['m_auth_token'].return_value
-        self.mocks['m_auth_token'].return_value.__enter__.return_value = token
-        token.value = None
-        obj = cloud.get_provider('my_provider')
-        assert isinstance(obj.driver, get_driver('openstack'))
-        assert obj._auth_token.value is None
-
-    def test_images(self):
-        obj = cloud.get_provider('my_provider')
-        self.mocks['m_list_images'].return_value = [
-                get_fake_obj(attributes=dict(name=_))
-                    for _ in ['image0', 'image1']]
-        assert not hasattr(obj, '_images')
-        assert [_.name for _ in obj.images] == ['image0', 'image1']
-        assert hasattr(obj, '_images')
-
-    def test_exclude_image(self):
-        obj = cloud.get_provider('image_exclude_provider')
-        self.mocks['m_list_images'].return_value = [
-                get_fake_obj(attributes=dict(name=_))
-                    for _ in ['image0', 'image1',
-                        'image2-exclude1', 'image3-exclude2']]
-        assert not hasattr(obj, '_images')
-        assert [_.name for _ in obj.images] == ['image0', 'image1']
-        assert hasattr(obj, '_images')
-
-    def test_sizes(self):
-        obj = cloud.get_provider('my_provider')
-        fake_sizes = [get_fake_obj(attributes=dict(name='size%s' % i)) for
-                      i in range(2)]
-        self.mocks['m_list_sizes'].return_value = fake_sizes
-        assert not hasattr(obj, '_sizes')
-        assert [s.name for s in obj.sizes] == ['size0', 'size1']
-        assert hasattr(obj, '_sizes')
-
-    def test_networks(self):
-        obj = cloud.get_provider('my_provider')
-        nets = [get_fake_obj(attributes=dict(name=i)) for i in ['net0', 'net1']]
-        self.mocks['m_ex_list_networks'].return_value = nets
-        assert not hasattr(obj, '_networks')
-        assert [i.name for i in obj.networks] == [i.name for i in nets]
-        assert hasattr(obj, '_networks')
-        self.mocks['m_ex_list_networks'].side_effect = AttributeError
-        obj = cloud.get_provider('my_provider')
-        assert not hasattr(obj, '_networks')
-        assert obj.networks == list()
-        assert hasattr(obj, '_networks')
-
-    def test_security_groups(self):
-        obj = cloud.get_provider('my_provider')
-        self.mocks['m_ex_list_security_groups'].return_value = ['sg0', 'sg1']
-        assert not hasattr(obj, '_security_groups')
-        assert obj.security_groups == ['sg0', 'sg1']
-        assert hasattr(obj, '_security_groups')
-        self.mocks['m_ex_list_security_groups'].side_effect = AttributeError
-        obj = cloud.get_provider('my_provider')
-        assert not hasattr(obj, '_security_groups')
-        assert obj.security_groups == list()
-        assert hasattr(obj, '_security_groups')
-
-
-class TestOpenStackCustomProvisioner(TestOpenStackBase):
-    klass = cloud.openstack.OpenStackProvisioner
-    def get_obj(
-            self, name='node_name', os_type='ubuntu',
-            os_version='16.04', conf=None, test_conf=None):
-
-        if test_conf:
-            yaml_file = os.path.dirname(__file__) + '/' + test_conf
-            print("Reading conf: %s" % yaml_file)
-            with open(yaml_file) as f:
-                teuth_conf=yaml.safe_load(f)
-            print(teuth_conf)
-            config.libcloud = deepcopy(teuth_conf['libcloud'] or test_config)
-        else:
-            config.libcloud = deepcopy(test_config)
-        return cloud.get_provisioner(
-            node_type='my_provider',
-            name=name,
-            os_type=os_type,
-            os_version=os_version,
-            conf=conf,
-        )
-
-    @mark.parametrize(
-        "conf",
-        [
-            dict(
-                path='test_openstack_userdata_conf.yaml',
-                runcmd_head=['uptime', 'date'],
-                ssh_authorized_keys=['user_public_key1', 'user_public_key2'],
-                user_ssh_pubkey='my_ssh_key',
-                os_version='16.04',
-                os_type='ubuntu',
-            ),
-            dict(
-                path='test_openstack_userdata_conf.yaml',
-                runcmd_head=['uptime', 'date'],
-                ssh_authorized_keys=['user_public_key1', 'user_public_key2'],
-                user_ssh_pubkey=None,
-                os_version='16.04',
-                os_type='ubuntu',
-            ),
-            dict(
-                os_version='16.04',
-                os_type='ubuntu',
-                path=None,
-                user_ssh_pubkey=None,
-            ),
-        ]
-    )
-    def test_userdata_conf(self, conf):
-        self.mocks['m_get_user_ssh_pubkey'].return_value = conf['user_ssh_pubkey']
-        obj = self.get_obj(os_version=conf['os_version'],
-                           os_type=conf['os_type'],
-                           test_conf=conf['path'])
-        userdata = yaml.safe_load(obj.userdata)
-        print(">>>> ", obj.conf)
-        print(">>>> ", obj.provider.conf)
-        print(">>>> ", obj.provider)
-        print(obj.userdata)
-        if conf and 'path' in conf and conf['path']:
-            assert userdata['runcmd'][0:len(conf['runcmd_head'])] == conf['runcmd_head']
-            assert userdata['bootcmd'] == [
-                'SuSEfirewall2 stop || true',
-                'service firewalld stop || true',
-            ]
-            assert 'packages' not in userdata
-        else:
-            assert 'bootcmd' not in userdata
-            assert userdata['packages'] == ['git', 'wget', 'python', 'ntp']
-        assert userdata['user'] == obj.user
-        assert userdata['hostname'] == obj.hostname
-        if 'user_ssh_pubkey' in conf and conf['user_ssh_pubkey']:
-            assert userdata['ssh_authorized_keys'][-1] == conf['user_ssh_pubkey']
-            if 'ssh_authorized_keys' in conf:
-                keys = conf['ssh_authorized_keys']
-                assert userdata['ssh_authorized_keys'][0:len(keys)] == keys
-        else:
-            if 'ssh_authorized_keys' in conf:
-                keys = conf['ssh_authorized_keys']
-                assert userdata['ssh_authorized_keys'][0:len(keys)] == keys
-            else:
-                assert 'ssh_authorized_keys' not in userdata
-
-    @mark.parametrize(
-        "conf",
-        [
-            dict(
-                path='test_openstack_userdata_conf.yaml',
-                runcmd_head=['uptime', 'date'],
-            ),
-            dict(
-                path=None,
-            ),
-        ]
-    )
-    def test_userdata_conf_runcmd(self, conf):
-        self.mocks['m_get_user_ssh_pubkey'].return_value = None
-        obj = self.get_obj(test_conf=conf['path'])
-        userdata = yaml.safe_load(obj.userdata)
-        assert userdata['runcmd'][-2:] == [['passwd', '-d', 'ubuntu'], ['touch', '/.teuth_provisioned']]
-
-    @mark.parametrize(
-        "conf",
-        [
-            dict(
-                path='test_openstack_userdata_conf.yaml',
-                packages=None,
-            ),
-            dict(
-                path=None,
-                packages=['git', 'wget', 'python', 'ntp']
-            ),
-        ]
-    )
-    def test_userdata_conf_packages(self, conf):
-        self.mocks['m_get_user_ssh_pubkey'].return_value = None
-        obj = self.get_obj(test_conf=conf['path'])
-        userdata = yaml.safe_load(obj.userdata)
-        assert userdata.get('packages', None) == conf['packages']
-
-class TestOpenStackProvisioner(TestOpenStackBase):
-    klass = cloud.openstack.OpenStackProvisioner
-
-    def get_obj(
-            self, name='node_name', os_type='ubuntu',
-            os_version='16.04', conf=None):
-        return cloud.get_provisioner(
-            node_type='my_provider',
-            name=name,
-            os_type=os_type,
-            os_version=os_version,
-            conf=conf,
-        )
-
-    def test_init(self):
-        with patch.object(
-            self.klass,
-            '_read_conf',
-        ) as m_read_conf:
-            self.get_obj()
-            assert len(m_read_conf.call_args_list) == 1
-
-    @mark.parametrize(
-        'input_conf',
-        [
-            dict(machine=dict(
-                disk=42,
-                ram=9001,
-                cpus=3,
-            )),
-            dict(volumes=dict(
-                count=3,
-                size=100,
-            )),
-            dict(),
-            dict(
-                machine=dict(
-                    disk=1,
-                    ram=2,
-                    cpus=3,
-                ),
-                volumes=dict(
-                    count=4,
-                    size=5,
-                )
-            ),
-            dict(
-                machine=dict(
-                    disk=100,
-                ),
-            ),
-        ]
-    )
-    def test_read_conf(self, input_conf):
-        obj = self.get_obj(conf=input_conf)
-        for topic in ['machine', 'volumes']:
-            combined = cloud.util.combine_dicts(
-                [input_conf, config.openstack],
-                lambda x, y: x > y,
-            )
-            assert obj.conf[topic] == combined[topic]
-
-    @mark.parametrize(
-        'input_conf, expected_machine, expected_vols',
-        [
-            [
-                dict(openstack=[
-                    dict(machine=dict(disk=64, ram=10000, cpus=3)),
-                    dict(volumes=dict(count=1, size=1)),
-                ]),
-                dict(disk=64, ram=10000, cpus=3),
-                dict(count=1, size=1),
-            ],
-            [
-                dict(openstack=[
-                    dict(machine=dict(cpus=3)),
-                    dict(machine=dict(disk=1, ram=9000)),
-                    dict(machine=dict(disk=50, ram=2, cpus=1)),
-                    dict(machine=dict()),
-                    dict(volumes=dict()),
-                    dict(volumes=dict(count=0, size=0)),
-                    dict(volumes=dict(count=1, size=0)),
-                    dict(volumes=dict(size=1)),
-                ]),
-                dict(disk=50, ram=9000, cpus=3),
-                dict(count=1, size=1),
-            ],
-            [
-                dict(openstack=[
-                    dict(volumes=dict(count=3, size=30)),
-                    dict(volumes=dict(size=50)),
-                ]),
-                None,
-                dict(count=3, size=50),
-            ],
-            [
-                dict(openstack=[
-                    dict(machine=dict(disk=100)),
-                    dict(volumes=dict(count=3, size=30)),
-                ]),
-                dict(disk=100, ram=8000, cpus=1),
-                dict(count=3, size=30),
-            ],
-        ]
-    )
-    def test_read_conf_legacy(
-            self, input_conf, expected_machine, expected_vols):
-        obj = self.get_obj(conf=input_conf)
-        if expected_machine is not None:
-            assert obj.conf['machine'] == expected_machine
-        else:
-            assert obj.conf['machine'] == config.openstack['machine']
-        if expected_vols is not None:
-            assert obj.conf['volumes'] == expected_vols
-
-    @mark.parametrize(
-        "os_type, os_version, should_find",
-        [
-            ('centos', '7', True),
-            ('BeOS', '42', False),
-        ]
-    )
-    def test_image(self, os_type, os_version, should_find):
-        image_attrs = [
-            dict(name='ubuntu-14.04'),
-            dict(name='ubuntu-16.04'),
-            dict(name='centos-7.0'),
-        ]
-        fake_images = list()
-        for item in image_attrs:
-            fake_images.append(
-                get_fake_obj(attributes=item)
-            )
-        obj = self.get_obj(os_type=os_type, os_version=os_version)
-        self.mocks['m_list_images'].return_value = fake_images
-        if should_find:
-            assert obj.os_version in obj.image.name
-            assert obj.image in fake_images
-        else:
-            with raises(RuntimeError):
-                obj.image
-
-    @mark.parametrize(
-        "input_attrs, func_or_exc",
-        [
-            (dict(ram=2**16),
-             lambda s: s.ram == 2**16),
-            (dict(disk=9999),
-             lambda s: s.disk == 9999),
-            (dict(cpus=99),
-             lambda s: s.vcpus == 99),
-            (dict(ram=2**16, disk=9999, cpus=99),
-             IndexError),
-        ]
-    )
-    def test_size(self, input_attrs, func_or_exc):
-        size_attrs = [
-            dict(ram=8000, disk=9999, vcpus=99, name='s0'),
-            dict(ram=2**16, disk=20, vcpus=99, name='s1'),
-            dict(ram=2**16, disk=9999, vcpus=1, name='s2'),
-        ]
-        fake_sizes = list()
-        for item in size_attrs:
-            fake_sizes.append(
-                get_fake_obj(attributes=item)
-            )
-        base_spec = dict(machine=dict(
-            ram=1,
-            disk=1,
-            cpus=1,
-        ))
-        spec = deepcopy(base_spec)
-        spec['machine'].update(input_attrs)
-        obj = self.get_obj(conf=spec)
-        self.mocks['m_list_sizes'].return_value = fake_sizes
-        if isinstance(func_or_exc, type):
-            with raises(func_or_exc):
-                obj.size
-        else:
-            assert obj.size in fake_sizes
-            assert func_or_exc(obj.size) is True
-
-    @mark.parametrize(
-        "wanted_groups",
-        [
-            ['group1'],
-            ['group0', 'group2'],
-            [],
-        ]
-    )
-    def test_security_groups(self, wanted_groups):
-        group_names = ['group0', 'group1', 'group2']
-        fake_groups = list()
-        for name in group_names:
-            fake_groups.append(
-                get_fake_obj(attributes=dict(name=name))
-            )
-        self.mocks['m_ex_list_security_groups'].return_value = fake_groups
-        obj = self.get_obj()
-        assert obj.security_groups is None
-        obj = self.get_obj()
-        obj.provider.conf['security_groups'] = wanted_groups
-        assert [g.name for g in obj.security_groups] == wanted_groups
-
-    def test_security_groups_exc(self):
-        fake_groups = [
-            get_fake_obj(attributes=dict(name='sg')) for i in range(2)
-        ]
-        obj = self.get_obj()
-        obj.provider.conf['security_groups'] = ['sg']
-        with raises(RuntimeError):
-            obj.security_groups
-        self.mocks['m_ex_list_security_groups'].return_value = fake_groups
-        obj = self.get_obj()
-        obj.provider.conf['security_groups'] = ['sg']
-        with raises(RuntimeError):
-            obj.security_groups
-
-    @mark.parametrize(
-        "ssh_key",
-        [
-            'my_ssh_key',
-            None,
-        ]
-    )
-    def test_userdata(self, ssh_key):
-        self.mocks['m_get_user_ssh_pubkey'].return_value = ssh_key
-        obj = self.get_obj()
-        userdata = yaml.safe_load(obj.userdata)
-        assert userdata['user'] == obj.user
-        assert userdata['hostname'] == obj.hostname
-        if ssh_key:
-            assert userdata['ssh_authorized_keys'] == [ssh_key]
-        else:
-            assert 'ssh_authorized_keys' not in userdata
-
-    @mark.parametrize(
-        'wanted_name, should_find, exception',
-        [
-            ('node0', True, None),
-            ('node1', True, None),
-            ('node2', False, RuntimeError),
-            ('node3', False, None),
-        ]
-    )
-    def test_node(self, wanted_name, should_find, exception):
-        node_names = ['node0', 'node1', 'node2', 'node2']
-        fake_nodes = list()
-        for name in node_names:
-            fake_nodes.append(
-                get_fake_obj(attributes=dict(name=name))
-            )
-        self.mocks['m_list_nodes'].return_value = fake_nodes
-        obj = self.get_obj(name=wanted_name)
-        if should_find:
-            assert obj.node.name == wanted_name
-        elif exception:
-            with raises(exception) as excinfo:
-                obj.node
-                assert excinfo.value.message
-        else:
-            assert obj.node is None
-
-    @mark.parametrize(
-        'networks, security_groups',
-        [
-            ([], []),
-            (['net0'], []),
-            ([], ['sg0']),
-            (['net0'], ['sg0']),
-        ]
-    )
-    def test_create(self, networks, security_groups):
-        node_name = 'node0'
-        fake_sizes = [
-            get_fake_obj(
-                attributes=dict(ram=2**16, disk=9999, vcpus=99, name='s0')),
-        ]
-        fake_security_groups = [
-            get_fake_obj(attributes=dict(name=name))
-            for name in security_groups
-        ]
-        self.mocks['m_ex_list_networks'].return_value = networks
-        self.mocks['m_ex_list_security_groups'].return_value = \
-            fake_security_groups
-        self.mocks['m_list_sizes'].return_value = fake_sizes
-        fake_images = [
-            get_fake_obj(attributes=dict(name='ubuntu-16.04')),
-        ]
-        self.mocks['m_list_images'].return_value = fake_images
-        self.mocks['m_get_user_ssh_pubkey'].return_value = 'ssh_key'
-        fake_node = get_fake_obj(attributes=dict(name=node_name))
-        fake_ips = ['555.123.4.0']
-        self.mocks['m_create_node'].return_value = fake_node
-        self.mocks['m_wait_until_running'].return_value = \
-            [(fake_node, fake_ips)]
-        obj = self.get_obj(name=node_name)
-        obj._networks = networks
-        obj.provider.conf['security_groups'] = security_groups
-        p_wait_for_ready = patch(
-            'teuthology.provision.cloud.openstack.OpenStackProvisioner'
-            '._wait_for_ready'
-        )
-        with p_wait_for_ready:
-            res = obj.create()
-        assert res is obj.node
-        # Test once again to ensure that if volume creation/attachment fails,
-        # we destroy any remaining volumes and consider the node creation to
-        # have failed as well.
-        del obj._node
-        with p_wait_for_ready:
-            obj.conf['volumes']['count'] = 1
-            obj.provider.driver.create_volume.side_effect = Exception
-            with patch.object(obj, '_destroy_volumes'):
-                assert obj.create() is False
-                obj._destroy_volumes.assert_called_once_with()
-
-    def test_update_dns(self):
-        config.nsupdate_url = 'nsupdate_url'
-        obj = self.get_obj()
-        obj.name = 'x'
-        obj.ips = ['y']
-        obj._update_dns()
-        call_args = self.mocks['m_get'].call_args_list
-        assert len(call_args) == 1
-        url_base, query_string = call_args[0][0][0].split('?')
-        assert url_base == 'nsupdate_url'
-        parsed_query = parse_qs(query_string)
-        assert parsed_query == dict(name=['x'], ip=['y'])
-
-    @mark.parametrize(
-        'nodes',
-        [[], [Mock()], [Mock(), Mock()]]
-    )
-    def test_destroy(self, nodes):
-        with patch(
-            'teuthology.provision.cloud.openstack.'
-            'OpenStackProvisioner._find_nodes'
-        ) as m_find_nodes:
-            m_find_nodes.return_value = nodes
-            obj = self.get_obj()
-            result = obj.destroy()
-            if not all(nodes):
-                assert result is True
-            else:
-                for node in nodes:
-                    node.destroy.assert_called_once_with()
-
-    _volume_matrix = (
-        'count, size, should_succeed',
-        [
-            (1, 10, True),
-            (0, 10, True),
-            (10, 1, True),
-            (1, 10, False),
-            (10, 1, False),
-        ]
-    )
-
-    @mark.parametrize(*_volume_matrix)
-    def test_create_volumes(self, count, size, should_succeed):
-        obj_conf = dict(volumes=dict(count=count, size=size))
-        obj = self.get_obj(conf=obj_conf)
-        node = get_fake_obj()
-        if not should_succeed:
-            obj.provider.driver.create_volume.side_effect = Exception
-        obj._node = node
-        result = obj._create_volumes()
-        assert result is should_succeed
-        if should_succeed:
-            create_calls = obj.provider.driver.create_volume.call_args_list
-            attach_calls = obj.provider.driver.attach_volume.call_args_list
-            assert len(create_calls) == count
-            assert len(attach_calls) == count
-            for i in range(count):
-                vol_size, vol_name = create_calls[i][0]
-                assert vol_size == size
-                assert vol_name == '%s_%s' % (obj.name, i)
-                assert attach_calls[i][0][0] is obj._node
-                assert attach_calls[i][1]['device'] is None
-
-    @mark.parametrize(*_volume_matrix)
-    def test_destroy_volumes(self, count, size, should_succeed):
-        obj_conf = dict(volumes=dict(count=count, size=size))
-        obj = self.get_obj(conf=obj_conf)
-        fake_volumes = list()
-        for i in range(count):
-            vol_name = '%s_%s' % (obj.name, i)
-            fake_volumes.append(
-                get_fake_obj(attributes=dict(name=vol_name))
-            )
-        obj.provider.driver.list_volumes.return_value = fake_volumes
-        obj._destroy_volumes()
-        detach_calls = obj.provider.driver.detach_volume.call_args_list
-        destroy_calls = obj.provider.driver.destroy_volume.call_args_list
-        assert len(detach_calls) == count
-        assert len(destroy_calls) == count
-        assert len(obj.provider.driver.detach_volume.call_args_list) == count
-        assert len(obj.provider.driver.destroy_volume.call_args_list) == count
-        obj.provider.driver.detach_volume.reset_mock()
-        obj.provider.driver.destroy_volume.reset_mock()
-        obj.provider.driver.detach_volume.side_effect = Exception
-        obj.provider.driver.destroy_volume.side_effect = Exception
-        obj._destroy_volumes()
-        assert len(obj.provider.driver.detach_volume.call_args_list) == count
-        assert len(obj.provider.driver.destroy_volume.call_args_list) == count
-
-    def test_destroy_volumes_exc(self):
-        obj = self.get_obj()
-        obj.provider.driver.detach_volume.side_effect = Exception
-
-    def test_wait_for_ready(self):
-        obj = self.get_obj()
-        obj._node = get_fake_obj(attributes=dict(name='node_name'))
-        with patch.multiple(
-            'teuthology.orchestra.remote.Remote',
-            connect=DEFAULT,
-            run=DEFAULT,
-        ) as mocks:
-            obj._wait_for_ready()
-            mocks['connect'].side_effect = socket.error
-            with raises(MaxWhileTries):
-                obj._wait_for_ready()
diff --git a/teuthology/provision/cloud/test/test_openstack_userdata_conf.yaml b/teuthology/provision/cloud/test/test_openstack_userdata_conf.yaml
deleted file mode 100644 (file)
index f3e87a8..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-libcloud:
-  providers:
-    my_provider:
-      allow_networks:
-        - sesci
-      userdata:
-        'ubuntu-16.04':
-            bootcmd:
-              - 'SuSEfirewall2 stop || true'
-              - 'service firewalld stop || true'
-            runcmd:
-              - 'uptime'
-              - 'date'
-              - 'zypper in -y lsb-release make gcc gcc-c++ chrony || true'
-              - 'systemctl enable chronyd.service || true'
-              - 'systemctl start chronyd.service || true'
-            ssh_authorized_keys:
-              - user_public_key1
-              - user_public_key2
-      driver: openstack
-      driver_args:
-          username: user
-          password: password
-          ex_force_auth_url: 'http://127.0.0.1:9999/v2.0/tokens'
diff --git a/teuthology/provision/test/test_downburst.py b/teuthology/provision/test/test_downburst.py
deleted file mode 100644 (file)
index 4ba7d9a..0000000
+++ /dev/null
@@ -1,105 +0,0 @@
-from mock import Mock, MagicMock, patch
-
-from teuthology import provision
-
-
-class TestDownburst(object):
-    def setup_method(self):
-        self.ctx = Mock()
-        self.ctx.os_type = 'rhel'
-        self.ctx.os_version = '7.0'
-        self.ctx.config = dict()
-        self.name = 'vpm999'
-        self.status = dict(
-            vm_host=dict(name='host999'),
-            is_vm=True,
-            machine_type='mtype',
-            locked_by='user@a',
-            description="desc",
-        )
-
-    def test_create_if_vm_success(self):
-        name = self.name
-        ctx = self.ctx
-        status = self.status
-
-        dbrst = provision.downburst.Downburst(
-            name, ctx.os_type, ctx.os_version, status)
-        dbrst.executable = '/fake/path'
-        dbrst.build_config = MagicMock(name='build_config')
-        dbrst._run_create = MagicMock(name='_run_create')
-        dbrst._run_create.return_value = (0, '', '')
-        remove_config = MagicMock(name='remove_config')
-        dbrst.remove_config = remove_config
-
-        result = provision.create_if_vm(ctx, name, dbrst)
-        assert result is True
-
-        dbrst._run_create.assert_called_with()
-        dbrst.build_config.assert_called_with()
-        del dbrst
-        remove_config.assert_called_with()
-
-    def test_destroy_if_vm_success(self):
-        name = self.name
-        ctx = self.ctx
-        status = self.status
-
-        dbrst = provision.downburst.Downburst(
-            name, ctx.os_type, ctx.os_version, status)
-        dbrst.destroy = MagicMock(name='destroy')
-        dbrst.destroy.return_value = True
-
-        result = provision.destroy_if_vm(name, user="user@a", _downburst=dbrst)
-        assert result is True
-
-        dbrst.destroy.assert_called_with()
-
-    def test_destroy_if_vm_wrong_owner(self):
-        name = self.name
-        ctx = self.ctx
-        status = self.status
-
-        dbrst = provision.downburst.Downburst(
-            name, ctx.os_type, ctx.os_version, status)
-        dbrst.destroy = MagicMock(name='destroy', side_effect=RuntimeError)
-
-        result = provision.destroy_if_vm(name, user='user@b',
-                                         _downburst=dbrst)
-        assert result is False
-
-    def test_destroy_if_vm_wrong_description(self):
-        name = self.name
-        ctx = self.ctx
-        status = self.status
-
-        dbrst = provision.downburst.Downburst(
-            name, ctx.os_type, ctx.os_version, status)
-        dbrst.destroy = MagicMock(name='destroy')
-        dbrst.destroy = MagicMock(name='destroy', side_effect=RuntimeError)
-
-        result = provision.destroy_if_vm(name, description='desc_b',
-                                         _downburst=dbrst)
-        assert result is False
-
-    @patch('teuthology.provision.downburst.downburst_executable')
-    def test_create_fails_without_executable(self, m_exec):
-        name = self.name
-        ctx = self.ctx
-        status = self.status
-        m_exec.return_value = ''
-        dbrst = provision.downburst.Downburst(
-            name, ctx.os_type, ctx.os_version, status)
-        result = dbrst.create()
-        assert result is False
-
-    @patch('teuthology.provision.downburst.downburst_executable')
-    def test_destroy_fails_without_executable(self, m_exec):
-        name = self.name
-        ctx = self.ctx
-        status = self.status
-        m_exec.return_value = ''
-        dbrst = provision.downburst.Downburst(
-            name, ctx.os_type, ctx.os_version, status)
-        result = dbrst.destroy()
-        assert result is False
diff --git a/teuthology/provision/test/test_fog.py b/teuthology/provision/test/test_fog.py
deleted file mode 100644 (file)
index 3d0baa7..0000000
+++ /dev/null
@@ -1,339 +0,0 @@
-import datetime
-
-from copy import deepcopy
-from mock import patch, DEFAULT, PropertyMock
-from pytest import raises, mark
-
-from teuthology.config import config
-from teuthology.exceptions import MaxWhileTries, CommandFailedError
-from teuthology.provision import fog
-
-
-test_config = dict(
-    fog=dict(
-        endpoint='http://fog.example.com/fog',
-        api_token='API_TOKEN',
-        user_token='USER_TOKEN',
-        machine_types='type1,type2',
-    ),
-    fog_reimage_timeout=1800,
-)
-
-
-class TestFOG(object):
-    klass = fog.FOG
-
-    def setup_method(self):
-        config.load()
-        config.update(deepcopy(test_config))
-        self.start_patchers()
-
-    def start_patchers(self):
-        self.patchers = dict()
-        self.patchers['m_sleep'] = patch(
-            'time.sleep',
-        )
-        self.patchers['m_requests_Session_send'] = patch(
-            'requests.Session.send',
-        )
-        self.patchers['m_Remote_connect'] = patch(
-            'teuthology.orchestra.remote.Remote.connect'
-        )
-        self.patchers['m_Remote_run'] = patch(
-            'teuthology.orchestra.remote.Remote.run'
-        )
-        self.patchers['m_Remote_console'] = patch(
-            'teuthology.orchestra.remote.Remote.console',
-            new_callable=PropertyMock,
-        )
-        self.patchers['m_Remote_hostname'] = patch(
-            'teuthology.orchestra.remote.Remote.hostname',
-            new_callable=PropertyMock,
-        )
-        self.patchers['m_Remote_machine_type'] = patch(
-            'teuthology.orchestra.remote.Remote.machine_type',
-            new_callable=PropertyMock,
-        )
-        self.mocks = dict()
-        for name, patcher in self.patchers.items():
-            self.mocks[name] = patcher.start()
-
-    def teardown_method(self):
-        for patcher in self.patchers.values():
-            patcher.stop()
-
-    @mark.parametrize('enabled', [True, False])
-    def test_get_types(self, enabled):
-        with patch('teuthology.provision.fog.enabled') as m_enabled:
-            m_enabled.return_value = enabled
-            types = fog.get_types()
-        if enabled:
-            assert types == test_config['fog']['machine_types'].split(',')
-        else:
-            assert types == []
-
-    def test_disabled(self):
-        config.fog['endpoint'] = None
-        obj = self.klass('name.fqdn', 'type', '1.0')
-        with raises(RuntimeError):
-            obj.create()
-
-    def test_init(self):
-        self.mocks['m_Remote_hostname'].return_value = 'name.fqdn'
-        obj = self.klass('name.fqdn', 'type', '1.0')
-        assert obj.name == 'name.fqdn'
-        assert obj.shortname == 'name'
-        assert obj.os_type == 'type'
-        assert obj.os_version == '1.0'
-
-    @mark.parametrize('success', [True, False])
-    def test_create(self, success):
-        self.mocks['m_Remote_hostname'].return_value = 'name.fqdn'
-        self.mocks['m_Remote_machine_type'].return_value = 'type1'
-        obj = self.klass('name.fqdn', 'type', '1.0')
-        host_id = 99
-        task_id = 1234
-        with patch.multiple(
-            'teuthology.provision.fog.FOG',
-            get_host_data=DEFAULT,
-            set_image=DEFAULT,
-            schedule_deploy_task=DEFAULT,
-            wait_for_deploy_task=DEFAULT,
-            cancel_deploy_task=DEFAULT,
-            _wait_for_ready=DEFAULT,
-            _fix_hostname=DEFAULT,
-            _verify_installed_os=DEFAULT,
-        ) as local_mocks:
-            local_mocks['get_host_data'].return_value = dict(id=host_id)
-            local_mocks['schedule_deploy_task'].return_value = task_id
-            if not success:
-                local_mocks['wait_for_deploy_task'].side_effect = RuntimeError
-                with raises(RuntimeError):
-                    obj.create()
-            else:
-                obj.create()
-            local_mocks['get_host_data'].assert_called_once_with()
-            local_mocks['set_image'].assert_called_once_with(host_id)
-            local_mocks['schedule_deploy_task'].assert_called_once_with(host_id)
-            local_mocks['wait_for_deploy_task'].assert_called_once_with(task_id)
-            if success:
-                local_mocks['_wait_for_ready'].assert_called_once_with()
-                local_mocks['_fix_hostname'].assert_called_once_with()
-            else:
-                assert len(local_mocks['cancel_deploy_task'].call_args_list) == 1
-        self.mocks['m_Remote_console'].return_value.power_off.assert_called_once_with()
-        self.mocks['m_Remote_console'].return_value.power_on.assert_called_once_with()
-
-    def test_do_request(self):
-        obj = self.klass('name.fqdn', 'type', '1.0')
-        obj.do_request('test_url', data='DATA', method='GET')
-        assert len(self.mocks['m_requests_Session_send'].call_args_list) == 1
-        req = self.mocks['m_requests_Session_send'].call_args_list[0][0][0]
-        assert req.url == test_config['fog']['endpoint'] + 'test_url'
-        assert req.method == 'GET'
-        assert req.headers['fog-api-token'] == test_config['fog']['api_token']
-        assert req.headers['fog-user-token'] == test_config['fog']['user_token']
-        assert req.body == 'DATA'
-
-    @mark.parametrize(
-        'count',
-        [0, 1, 2],
-    )
-    def test_get_host_data(self, count):
-        host_objs = [dict(id=i) for i in range(count)]
-        resp_obj = dict(count=count, hosts=host_objs)
-        self.mocks['m_requests_Session_send']\
-            .return_value.json.return_value = resp_obj
-        obj = self.klass('name.fqdn', 'type', '1.0')
-        if count != 1:
-            with raises(RuntimeError):
-                result = obj.get_host_data()
-            return
-        result = obj.get_host_data()
-        assert len(self.mocks['m_requests_Session_send'].call_args_list) == 1
-        req = self.mocks['m_requests_Session_send'].call_args_list[0][0][0]
-        assert req.url == test_config['fog']['endpoint'] + '/host'
-        assert req.body == '{"name": "name"}'
-        assert result == host_objs[0]
-
-    @mark.parametrize(
-        'count',
-        [0, 1, 2],
-    )
-    def test_get_image_data(self, count):
-        img_objs = [dict(id=i) for i in range(count)]
-        resp_obj = dict(count=count, images=img_objs)
-        self.mocks['m_requests_Session_send']\
-            .return_value.json.return_value = resp_obj
-        self.mocks['m_Remote_machine_type'].return_value = 'type1'
-        obj = self.klass('name.fqdn', 'windows', 'xp')
-        if count < 1:
-            with raises(RuntimeError):
-                result = obj.get_image_data()
-            return
-        result = obj.get_image_data()
-        assert len(self.mocks['m_requests_Session_send'].call_args_list) == 1
-        req = self.mocks['m_requests_Session_send'].call_args_list[0][0][0]
-        assert req.url == test_config['fog']['endpoint'] + '/image'
-        assert req.body == '{"name": "type1_windows_xp"}'
-        assert result == img_objs[0]
-
-    def test_suggest_image_names(self):
-        data = {'images': [
-            {'name': 'mira_rhel_9.1'},
-            {'name': 'mira_rhel_9.2'},
-        ]}
-        self.mocks['m_requests_Session_send']\
-            .return_value.json.return_value = data
-        self.mocks['m_Remote_machine_type'].return_value = 'mira'
-        # Not sure what this klass() is for here:
-        obj = self.klass('name.fqdn', 'mira', '1.0')
-        result = obj.suggest_image_names()
-        assert len(self.mocks['m_requests_Session_send'].call_args_list) == 1
-        req = self.mocks['m_requests_Session_send'].call_args_list[0][0][0]
-        assert req.url == test_config['fog']['endpoint'] + '/image/search/mira'
-        assert result == ['mira_rhel_9.1', 'mira_rhel_9.2']
-
-    def test_set_image(self):
-        self.mocks['m_Remote_hostname'].return_value = 'name.fqdn'
-        self.mocks['m_Remote_machine_type'].return_value = 'type1'
-        host_id = 999
-        obj = self.klass('name.fqdn', 'type', '1.0')
-        with patch.multiple(
-            'teuthology.provision.fog.FOG',
-            get_image_data=DEFAULT,
-            do_request=DEFAULT,
-        ) as local_mocks:
-            local_mocks['get_image_data'].return_value = dict(id='13')
-            obj.set_image(host_id)
-            local_mocks['do_request'].assert_called_once_with(
-                '/host/999', method='PUT', data='{"imageID": 13}',
-            )
-
-    def test_schedule_deploy_task(self):
-        host_id = 12
-        tasktype_id = 6
-        task_id = 5
-        tasktype_result = dict(tasktypes=[dict(name='deploy', id=tasktype_id)])
-        schedule_result = dict()
-        host_tasks = [dict(
-            createdTime=datetime.datetime.strftime(
-                datetime.datetime.now(datetime.timezone.utc),
-                self.klass.timestamp_format
-            ),
-            id=task_id,
-        )]
-        self.mocks['m_requests_Session_send']\
-            .return_value.json.side_effect = [
-            tasktype_result, schedule_result,
-        ]
-        with patch.multiple(
-            'teuthology.provision.fog.FOG',
-            get_deploy_tasks=DEFAULT,
-        ) as local_mocks:
-            local_mocks['get_deploy_tasks'].return_value = host_tasks
-            obj = self.klass('name.fqdn', 'type', '1.0')
-            result = obj.schedule_deploy_task(host_id)
-            assert len(local_mocks['get_deploy_tasks'].call_args_list) == 2
-        assert len(self.mocks['m_requests_Session_send'].call_args_list) == 3
-        assert result == task_id
-
-    def test_get_deploy_tasks(self):
-        obj = self.klass('name.fqdn', 'type', '1.0')
-        resp_obj = dict(
-            count=2,
-            tasks=[
-                dict(host=dict(name='notme')),
-                dict(host=dict(name='name')),
-            ]
-        )
-        self.mocks['m_requests_Session_send']\
-            .return_value.json.return_value = resp_obj
-        result = obj.get_deploy_tasks()
-        assert result[0]['host']['name'] == 'name'
-
-    @mark.parametrize(
-        'active_ids',
-        [
-            [2, 4, 6, 8],
-            [1],
-            [],
-        ]
-    )
-    def test_deploy_task_active(self, active_ids):
-        our_task_id = 4
-        result_objs = [dict(id=task_id) for task_id in active_ids]
-        obj = self.klass('name.fqdn', 'type', '1.0')
-        with patch.multiple(
-            'teuthology.provision.fog.FOG',
-            get_deploy_tasks=DEFAULT,
-        ) as local_mocks:
-            local_mocks['get_deploy_tasks'].return_value = result_objs
-            result = obj.deploy_task_active(our_task_id)
-            assert result is (our_task_id in active_ids)
-
-    @mark.parametrize(
-        'tries',
-        [3, 121],
-    )
-    def test_wait_for_deploy_task(self, tries):
-        wait_results = [True for i in range(tries)] + [False]
-        obj = self.klass('name.fqdn', 'type', '1.0')
-        with patch.multiple(
-            'teuthology.provision.fog.FOG',
-            deploy_task_active=DEFAULT,
-        ) as local_mocks:
-            local_mocks['deploy_task_active'].side_effect = wait_results
-            if tries >= 60:
-                with raises(MaxWhileTries):
-                    obj.wait_for_deploy_task(9)
-                return
-            obj.wait_for_deploy_task(9)
-            assert len(local_mocks['deploy_task_active'].call_args_list) == \
-                tries + 1
-
-    def test_cancel_deploy_task(self):
-        obj = self.klass('name.fqdn', 'type', '1.0')
-        with patch.multiple(
-            'teuthology.provision.fog.FOG',
-            do_request=DEFAULT,
-        ) as local_mocks:
-            obj.cancel_deploy_task(10)
-            local_mocks['do_request'].assert_called_once_with(
-                '/task/cancel',
-                method='DELETE',
-                data='{"id": 10}',
-            )
-
-    @mark.parametrize(
-        'tries',
-        [1, 101],
-    )
-    def test_wait_for_ready_tries(self, tries):
-        connect_results = [MaxWhileTries for i in range(tries)] + [True]
-        obj = self.klass('name.fqdn', 'type', '1.0')
-        self.mocks['m_Remote_connect'].side_effect = connect_results
-        if tries >= 100:
-            with raises(MaxWhileTries):
-                obj._wait_for_ready()
-            return
-        obj._wait_for_ready()
-        assert len(self.mocks['m_Remote_connect'].call_args_list) == tries + 1
-
-    @mark.parametrize(
-        'sentinel_present',
-        ([False, True]),
-    )
-    def test_wait_for_ready_sentinel(self, sentinel_present):
-        config.fog['sentinel_file'] = '/a_file'
-        obj = self.klass('name.fqdn', 'type', '1.0')
-        if not sentinel_present:
-            self.mocks['m_Remote_run'].side_effect = [
-                CommandFailedError(command='cmd', exitstatus=1)]
-            with raises(CommandFailedError):
-                obj._wait_for_ready()
-        else:
-            obj._wait_for_ready()
-        assert len(self.mocks['m_Remote_run'].call_args_list) == 1
-        assert "'/a_file'" in \
-            self.mocks['m_Remote_run'].call_args_list[0][1]['args']
diff --git a/teuthology/provision/test/test_init_provision.py b/teuthology/provision/test/test_init_provision.py
deleted file mode 100644 (file)
index e63c5aa..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-from copy import deepcopy
-from pytest import raises
-from teuthology.config import config
-
-import teuthology.provision
-
-test_config = dict(
-    pelagos=dict(
-        endpoint='http://pelagos.example:5000/',
-        machine_types='ptype1,ptype2,common_type',
-    ),
-    fog=dict(
-        endpoint='http://fog.example.com/fog',
-        api_token='API_TOKEN',
-        user_token='USER_TOKEN',
-        machine_types='ftype1,ftype2,common_type',
-    )
-)
-
-class TestInitProvision(object):
-
-    def setup_method(self):
-        config.load(deepcopy(test_config))
-
-    def test_get_reimage_types(self):
-        reimage_types = teuthology.provision.get_reimage_types()
-        assert reimage_types == ["ptype1", "ptype2", "common_type",
-                                 "ftype1", "ftype2", "common_type"]
-
-    def test_reimage(self):
-        class context:
-            pass
-        ctx = context()
-        ctx.os_type = 'sle'
-        ctx.os_version = '15.1'
-        with raises(Exception) as e_info:
-            teuthology.provision.reimage(ctx, 'f.q.d.n.org', 'not-defined-type')
-        e_str = str(e_info)
-        print("Caught exception: " +  e_str)
-        assert e_str.find(r"configured\sprovisioners") == -1
-
-        with raises(Exception) as e_info:
-            teuthology.provision.reimage(ctx, 'f.q.d.n.org', 'common_type')
-        e_str = str(e_info)
-        print("Caught exception: " +  e_str)
-        assert e_str.find(r"used\swith\sone\sprovisioner\sonly") == -1
diff --git a/teuthology/provision/test/test_maas.py b/teuthology/provision/test/test_maas.py
deleted file mode 100644 (file)
index ae47e0c..0000000
+++ /dev/null
@@ -1,311 +0,0 @@
-from copy import deepcopy
-from mock import patch, DEFAULT, PropertyMock
-from pytest import raises, mark
-from requests.models import Response
-
-from teuthology.config import config
-from teuthology.orchestra.opsys import OS
-from teuthology.provision import maas
-
-
-test_config = dict(
-    maas=dict(
-        api_url="http://maas.example.com:5240/MAAS/api/2.0/",
-        api_key="CONSUMER_KEY:ACCESS_TOKEN:SECRET",
-        machine_types=["typeA", "typeB"],
-        timeout=900,
-        user_data="teuthology/maas/maas-{os_type}-{os_version}-user-data.txt"
-    )
-)
-
-class TestMaas(object):
-    klass = maas.MAAS
-
-    def setup_method(self):
-        config.load()
-        config.update(deepcopy(test_config))
-        self.start_patchers()
-
-    def start_patchers(self):
-        self.patchers = dict()
-        self.patchers["m_Remote_hostname"] = patch(
-            "teuthology.orchestra.remote.Remote.hostname",
-            new_callable=PropertyMock,
-        )
-        self.patchers["m_Remote_machine_os"] = patch(
-            "teuthology.orchestra.remote.Remote.os",
-            new_callable=PropertyMock,
-        )
-        self.patchers["m_Remote_opsys_os"] = patch(
-            "teuthology.orchestra.opsys.OS",
-            new_callable=PropertyMock,
-        )
-
-        self.mocks = dict()
-        for name, patcher in self.patchers.items():
-            self.mocks[name] = patcher.start()
-
-    def teardown_method(self):
-        for patcher in self.patchers.values():
-            patcher.stop()
-
-    def _get_mock_response(self, status_code=200, content=None, headers=None):
-        response = Response()
-        response.status_code = status_code
-        response._content = content
-        if headers is not None:
-            response.headers = headers
-        return response
-
-    def test_api_url_missing(self):
-        config.maas["api_url"] = None
-        with raises(RuntimeError):
-            self.klass("name.fqdn")
-
-    def test_api_key_missing(self):
-        config.maas["api_key"] = None
-        with raises(RuntimeError):
-            self.klass("name.fqdn")
-
-    @mark.parametrize("enabled", [True, False])
-    def test_get_types(self, enabled):
-        with patch("teuthology.provision.maas.enabled") as m_enabled:
-            m_enabled.return_value = enabled
-            types = maas.get_types()
-
-        if enabled:
-            assert types == test_config["maas"]["machine_types"]
-        else:
-            assert types == []
-
-    @mark.parametrize("status_name, osystem, distro_series", [
-        ("ready", "ubuntu", "jammy"),
-        ("deployed", "ubuntu", "jammy"),
-    ])
-    def test_init(self, status_name, osystem, distro_series):
-        self.mocks["m_Remote_hostname"].return_value = "name.fqdn"
-        with patch(
-            "teuthology.provision.maas.MAAS.get_machines_data"
-        ) as maas_machine:
-            maas_machine.return_value = {
-                "status_name": status_name,
-                "system_id": "abc123",
-                "osystem": osystem,
-                "distro_series": distro_series,
-                "architecture": "amd64/generic"
-            }
-            if not (osystem and distro_series):
-                with raises(RuntimeError):
-                    obj = self.klass(
-                        name="name.fqdn",
-                        os_type=osystem,
-                        os_version=distro_series
-                    )
-            else:
-                obj = self.klass(
-                    name="name.fqdn",
-                    os_type=osystem,
-                    os_version=distro_series
-                )
-                assert obj.name == "name.fqdn"
-                assert obj.shortname == "name"
-                assert obj.os_type == osystem
-                assert obj.os_version == distro_series
-                assert obj.system_id == "abc123"
-
-    @mark.parametrize("os_name, os_version, codename", [
-        ("ubuntu", "24.04", "noble"), ("centos", "9.stream", "centos9-stream"),
-    ])
-    def test_get_image_data(self, os_name, os_version, codename):
-        with patch.multiple(
-            "teuthology.provision.maas.MAAS",
-            do_request=DEFAULT,
-            get_machines_data=DEFAULT,
-        ) as local_mocks:
-            local_mocks["do_request"].return_value = self._get_mock_response(
-                content=b'[{"name": "%s/%s", "architecture": "amd64/generic"}]' % (
-                    os_name.encode(), codename.encode()
-                )
-            )
-            local_mocks["get_machines_data"].return_value = {
-                "status_name": "ready",
-                "architecture": "amd64/generic",
-            }
-            obj = self.klass(
-                name="name.fqdn", os_type=os_name, os_version=os_version
-            )
-            assert obj.get_image_data() == {
-                "name": f"{os_name}/{codename}",
-                "architecture": "amd64/generic",
-            }
-
-    def test_lock_machine(self):
-        with patch.multiple(
-            "teuthology.provision.maas.MAAS",
-            do_request=DEFAULT,
-            get_machines_data=DEFAULT,
-        ) as local_mocks:
-            local_mocks["get_machines_data"].return_value = {
-                "system_id": "1234abc",
-                "architecture": "amd64/generic",
-            }
-            local_mocks["do_request"].return_value = self._get_mock_response(
-                content=b'{"locked": "true"}'
-            )
-            assert self.klass(name="name.fqdn").lock_machine() is None
-
-    def test_unlock_machine(self):
-        self.mocks["m_Remote_hostname"].return_value = "name.fqdn"
-        with patch.multiple(
-            "teuthology.provision.maas.MAAS",
-            do_request=DEFAULT,
-            get_machines_data=DEFAULT,
-        ) as local_mocks:
-            local_mocks["get_machines_data"].return_value = {
-                "system_id": "1234abc",
-                "architecture": "amd64/generic",
-            }
-            local_mocks["do_request"].return_value = self._get_mock_response(
-                content=b'{"locked": "true"}'
-            )
-            with raises(RuntimeError):
-                self.klass(name="name.fqdn").unlock_machine()
-
-    @mark.parametrize("type, status_name", [
-        ("uploaded", ""), ("synced", "deploying"), ("synced", "ready"),
-    ])
-    def test_deploy_machine(self, type, status_name):
-        with patch.multiple(
-            "teuthology.provision.maas.MAAS",
-            get_image_data=DEFAULT,
-            _get_user_data=DEFAULT,
-            get_machines_data=DEFAULT,
-            do_request=DEFAULT,
-        ) as local_mocks:
-            local_mocks["get_machines_data"].return_value = {
-                "system_id": "1234abc",
-                "architecture": "amd64/generic",
-            }
-            local_mocks["get_image_data"].return_value = {
-                "name": "ubuntu/noble", "type": type
-            }
-            local_mocks["_get_user_data"].return_value = "init-host-data"
-            local_mocks["do_request"].return_value = self._get_mock_response(
-                content=b'{"status_name": "%s"}' % status_name.encode()
-            )
-            obj = self.klass(name="name.fqdn")
-            if (status_name != "deploying" or type != "synced"):
-                with raises(RuntimeError):
-                    obj.deploy_machine()
-            else:
-                assert obj.deploy_machine() is None
-
-    @mark.parametrize("response", [b'{}', ])
-    def test_release_machine(self, response):
-        with patch.multiple(
-            "teuthology.provision.maas.MAAS",
-            get_machines_data=DEFAULT,
-            do_request=DEFAULT,
-        ) as local_mocks:
-            local_mocks["get_machines_data"].return_value = {
-                "system_id": "1234abc",
-                "architecture": "amd64/generic",
-                "status_name": "ready"
-            }
-            local_mocks["do_request"].return_value = self._get_mock_response(
-                content=b'{"status_name": "disk erasing"}'
-            )
-            assert self.klass(name="name.fqdn").release_machine() is None
-
-    @mark.parametrize("status_name", ["deploying", "ready"])
-    def test_abort_deploy(self, status_name):
-        with patch.multiple(
-            "teuthology.provision.maas.MAAS",
-            get_machines_data=DEFAULT,
-            do_request=DEFAULT,
-        ) as local_mocks:
-            local_mocks["get_machines_data"].return_value = {
-                "system_id": "1234abc",
-                "architecture": "amd64/generic",
-            }
-            if status_name == "deploying":
-                local_mocks["do_request"].return_value = self._get_mock_response(
-                    content=b'{"status_name": "allocated"}'
-                )
-
-            assert self.klass(name="name.fqdn").abort_deploy() is None
-
-    @mark.parametrize("lock_status, machine_status", [
-        (False, "ready"), (False, "allocated"),
-        (True, "deployed"), (False, "deployed"),
-        (False, "releasing"),
-    ])
-    def test_release(self, lock_status, machine_status):
-        with patch.multiple(
-            "teuthology.provision.maas.MAAS",
-            get_machines_data=DEFAULT,
-            unlock_machine=DEFAULT,
-            release_machine=DEFAULT,
-            _wait_for_status=DEFAULT,
-        ) as local_mocks:
-            local_mocks["get_machines_data"].return_value = {
-                "status_name": machine_status,
-                "locked": lock_status,
-                "system_id": "1234abc",
-                "architecture": "amd64/generic",
-            }
-            obj = self.klass(name="name.fqdn")
-            if machine_status in ["ready", "allocated"]:
-                obj.release()
-
-            elif machine_status == "deployed":
-                local_mocks["unlock_machine"].return_value = None
-                local_mocks["release_machine"].return_value = None
-                local_mocks["_wait_for_status"].return_value = None
-                obj.release()
-
-            else:
-                with raises(RuntimeError):
-                    obj.release()
-
-    @mark.parametrize("user_data", [True, False])
-    def test_get_user_data(self, user_data):
-        with patch(
-            "teuthology.provision.maas.MAAS.get_machines_data"
-        ) as maas_machine:
-            maas_machine.return_value = {
-                "system_id": "1234abc",
-                "architecture": "amd64/generic",
-            }
-            obj = self.klass(name="name.fqdn")
-            if user_data:
-                assert user_data is not None
-            else:
-                config.maas["user_data"] = None
-                user_data = obj._get_user_data()
-                assert user_data is None
-    
-    @mark.parametrize(
-        "os_type, os_version, remote_os_type, remote_os_version", [
-        ("centos", "8", "centos", "8"),
-        ("", "", "ubuntu", "22.04")
-    ])
-    def test_verify_installed_os(
-        self, os_type, os_version, remote_os_type, remote_os_version
-    ):
-        self.mocks["m_Remote_machine_os"].return_value = OS(
-            name=remote_os_type, version=remote_os_version
-        )
-        with patch(
-            "teuthology.provision.maas.MAAS.get_machines_data"
-        ) as maas_machine:
-            maas_machine.return_value = {
-                "system_id": "1234abc",
-                "architecture": "amd64/generic"
-            }
-            if os_type and os_version:
-                self.klass(
-                    name="name.fqdn", os_type=os_type, os_version=os_version
-                )._verify_installed_os()
-            else:
-                self.klass(name="name.fqdn")._verify_installed_os()
diff --git a/teuthology/provision/test/test_pelagos.py b/teuthology/provision/test/test_pelagos.py
deleted file mode 100644 (file)
index 43d4741..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-from copy import deepcopy
-from pytest import raises
-from teuthology.config import config
-from teuthology.provision import pelagos
-
-import teuthology.provision
-
-
-test_config = dict(
-    pelagos=dict(
-        endpoint='http://pelagos.example:5000/',
-        machine_types='ptype1,ptype2',
-    ),
-)
-
-class TestPelagos(object):
-
-    def setup_method(self):
-        config.load(deepcopy(test_config))
-
-    def teardown_method(self):
-        pass
-
-    def test_get_types(self):
-        #klass = pelagos.Pelagos
-        types = pelagos.get_types()
-        assert types == ["ptype1", "ptype2"]
-
-    def test_disabled(self):
-        config.pelagos['endpoint'] = None
-        enabled = pelagos.enabled()
-        assert enabled == False
-
-    def test_pelagos(self):
-            class context:
-                pass
-
-            ctx = context()
-            ctx.os_type ='sle'
-            ctx.os_version = '15.1'
-            with raises(Exception) as e_info:
-                teuthology.provision.reimage(ctx, 'f.q.d.n.org', 'ptype1')
-            e_str = str(e_info)
-            print("Caught exception: " +  e_str)
-            assert e_str.find(r"Name\sor\sservice\snot\sknown") == -1
-
diff --git a/teuthology/suite/test/conftest.py b/teuthology/suite/test/conftest.py
deleted file mode 100644 (file)
index 4285bdc..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-from teuthology.config import config
-
-def pytest_runtest_setup():
-    config.load({})
diff --git a/teuthology/suite/test/suites/noop/noop.yaml b/teuthology/suite/test/suites/noop/noop.yaml
deleted file mode 100644 (file)
index fb674b1..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-roles:
-- - mon.a
-  - osd.0
-tasks:
-- exec:
-    mon.a:
-      - echo "Well done !"
diff --git a/teuthology/suite/test/test_build_matrix.py b/teuthology/suite/test/test_build_matrix.py
deleted file mode 100644 (file)
index ad710d2..0000000
+++ /dev/null
@@ -1,815 +0,0 @@
-import os
-import random
-
-from mock import patch, MagicMock
-
-from teuthology.suite import build_matrix
-from teuthology.test.fake_fs import make_fake_fstools
-
-
-class TestBuildMatrixSimple(object):
-    def test_combine_path(self):
-        result = build_matrix.combine_path("/path/to/left", "right/side")
-        assert result == "/path/to/left/right/side"
-
-    def test_combine_path_no_right(self):
-        result = build_matrix.combine_path("/path/to/left", None)
-        assert result == "/path/to/left"
-
-
-class TestBuildMatrix(object):
-
-    patchpoints = [
-        'os.path.exists',
-        'os.listdir',
-        'os.path.isfile',
-        'os.path.isdir',
-        'builtins.open',
-    ]
-
-    def setup_method(self):
-        self.mocks = dict()
-        self.patchers = dict()
-        for ppoint in self.__class__.patchpoints:
-            self.mocks[ppoint] = MagicMock()
-            self.patchers[ppoint] = patch(ppoint, self.mocks[ppoint])
-
-    def start_patchers(self, fake_fs):
-        fake_fns = make_fake_fstools(fake_fs)
-        # N.B.: relies on fake_fns being in same order as patchpoints
-        for ppoint, fn in zip(self.__class__.patchpoints, fake_fns):
-            self.mocks[ppoint].side_effect = fn
-            self.patchers[ppoint].start()
-
-    def stop_patchers(self):
-        for patcher in self.patchers.values():
-            patcher.stop()
-
-    def teardown_method(self):
-        self.patchers.clear()
-        self.mocks.clear()
-
-    def fragment_occurences(self, jobs, fragment):
-        # What fraction of jobs contain fragment?
-        count = 0
-        for (description, fragment_list) in jobs:
-            for item in fragment_list:
-                if item.endswith(fragment):
-                    count += 1
-        return count / float(len(jobs))
-
-    def test_concatenate_1x2x3(self):
-        fake_fs = {
-            'd0_0': {
-                '+': None,
-                'd1_0': {
-                    'd1_0_0.yaml': None,
-                },
-                'd1_1': {
-                    'd1_1_0.yaml': None,
-                    'd1_1_1.yaml': None,
-                },
-                'd1_2': {
-                    'd1_2_0.yaml': None,
-                    'd1_2_1.yaml': None,
-                    'd1_2_2.yaml': None,
-                },
-            },
-        }
-        self.start_patchers(fake_fs)
-        try:
-            result = build_matrix.build_matrix('d0_0')
-        finally:
-            self.stop_patchers()
-        assert len(result) == 1
-
-    def test_convolve_2x2(self):
-        fake_fs = {
-            'd0_0': {
-                '%': None,
-                'd1_0': {
-                    'd1_0_0.yaml': None,
-                    'd1_0_1.yaml': None,
-                },
-                'd1_1': {
-                    'd1_1_0.yaml': None,
-                    'd1_1_1.yaml': None,
-                },
-            },
-        }
-        self.start_patchers(fake_fs)
-        try:
-            result = build_matrix.build_matrix('d0_0')
-        finally:
-            self.stop_patchers()
-        assert len(result) == 4
-        assert self.fragment_occurences(result, 'd1_1_1.yaml') == 0.5
-
-    def test_convolve_2x2x2(self):
-        fake_fs = {
-            'd0_0': {
-                '%': None,
-                'd1_0': {
-                    'd1_0_0.yaml': None,
-                    'd1_0_1.yaml': None,
-                },
-                'd1_1': {
-                    'd1_1_0.yaml': None,
-                    'd1_1_1.yaml': None,
-                },
-                'd1_2': {
-                    'd1_2_0.yaml': None,
-                    'd1_2_1.yaml': None,
-                },
-            },
-        }
-        self.start_patchers(fake_fs)
-        try:
-            result = build_matrix.build_matrix('d0_0')
-        finally:
-            self.stop_patchers()
-        assert len(result) == 8
-        assert self.fragment_occurences(result, 'd1_2_0.yaml') == 0.5
-
-    def test_convolve_1x2x4(self):
-        fake_fs = {
-            'd0_0': {
-                '%': None,
-                'd1_0': {
-                    'd1_0_0.yaml': None,
-                },
-                'd1_1': {
-                    'd1_1_0.yaml': None,
-                    'd1_1_1.yaml': None,
-                },
-                'd1_2': {
-                    'd1_2_0.yaml': None,
-                    'd1_2_1.yaml': None,
-                    'd1_2_2.yaml': None,
-                    'd1_2_3.yaml': None,
-                },
-            },
-        }
-        self.start_patchers(fake_fs)
-        try:
-            result = build_matrix.build_matrix('d0_0')
-        finally:
-            self.stop_patchers()
-        assert len(result) == 8
-        assert self.fragment_occurences(result, 'd1_2_2.yaml') == 0.25
-
-    def test_convolve_with_concat(self):
-        fake_fs = {
-            'd0_0': {
-                '%': None,
-                'd1_0': {
-                    'd1_0_0.yaml': None,
-                },
-                'd1_1': {
-                    'd1_1_0.yaml': None,
-                    'd1_1_1.yaml': None,
-                },
-                'd1_2': {
-                    '+': None,
-                    'd1_2_0.yaml': None,
-                    'd1_2_1.yaml': None,
-                    'd1_2_2.yaml': None,
-                    'd1_2_3.yaml': None,
-                },
-            },
-        }
-        self.start_patchers(fake_fs)
-        try:
-            result = build_matrix.build_matrix('d0_0')
-        finally:
-            self.stop_patchers()
-        assert len(result) == 2
-        for i in result:
-            assert 'd0_0/d1_2/d1_2_0.yaml' in i[1]
-            assert 'd0_0/d1_2/d1_2_1.yaml' in i[1]
-            assert 'd0_0/d1_2/d1_2_2.yaml' in i[1]
-            assert 'd0_0/d1_2/d1_2_3.yaml' in i[1]
-
-    def test_convolve_nested(self):
-        fake_fs = {
-            'd0_0': {
-                '%': None,
-                'd1_0': {
-                    'd1_0_0.yaml': None,
-                    '%': '2',
-                    'd1_0_1': {
-                        'd1_0_1_0.yaml': None,
-                        'd1_0_1_1.yaml': None,
-                    },
-                    'd1_0_2': {
-                        'd1_0_2_0.yaml': None,
-                        'd1_0_2_1.yaml': None,
-                    },
-                },
-                'd1_2': {
-                    'd1_2_0.yaml': None,
-                    'd1_2_1.yaml': None,
-                    'd1_2_2.yaml': None,
-                    'd1_2_3.yaml': None,
-                },
-            },
-        }
-        self.start_patchers(fake_fs)
-        try:
-            result = build_matrix.build_matrix('d0_0')
-        finally:
-            self.stop_patchers()
-        assert len(result) == 8
-        assert self.fragment_occurences(result, 'd1_0_0.yaml') == 1
-        assert self.fragment_occurences(result, 'd1_0_1_0.yaml') == 0.5
-        assert self.fragment_occurences(result, 'd1_0_1_1.yaml') == 0.5
-        assert self.fragment_occurences(result, 'd1_0_2_0.yaml') == 0.5
-        assert self.fragment_occurences(result, 'd1_0_2_1.yaml') == 0.5
-        assert self.fragment_occurences(result, 'd1_2_0.yaml') == 0.25
-        assert self.fragment_occurences(result, 'd1_2_1.yaml') == 0.25
-        assert self.fragment_occurences(result, 'd1_2_2.yaml') == 0.25
-        assert self.fragment_occurences(result, 'd1_2_3.yaml') == 0.25
-
-
-    def test_random_dollar_sign_2x2x3(self):
-        fake_fs = {
-            'd0_0': {
-                '$': None,
-                'd1_0': {
-                    'd1_0_0.yaml': None,
-                    'd1_0_1.yaml': None,
-                },
-                'd1_1': {
-                    'd1_1_0.yaml': None,
-                    'd1_1_1.yaml': None,
-                },
-                'd1_2': {
-                    'd1_2_0.yaml': None,
-                    'd1_2_1.yaml': None,
-                    'd1_2_2.yaml': None,
-                },
-            },
-        }
-        fake_fs1 = {
-            'd0_0$': {
-                'd1_0': {
-                    'd1_0_0.yaml': None,
-                    'd1_0_1.yaml': None,
-                },
-                'd1_1': {
-                    'd1_1_0.yaml': None,
-                    'd1_1_1.yaml': None,
-                },
-                'd1_2': {
-                    'd1_2_0.yaml': None,
-                    'd1_2_1.yaml': None,
-                    'd1_2_2.yaml': None,
-                },
-            },
-        }
-        self.start_patchers(fake_fs)
-        try:
-            result = build_matrix.build_matrix('d0_0')
-        finally:
-            self.stop_patchers()
-        assert len(result) == 1
-        self.start_patchers(fake_fs1)
-        try:
-            result = build_matrix.build_matrix('d0_0$')
-        finally:
-            self.stop_patchers()
-        assert len(result) == 1
-
-    def test_random_dollar_sign_with_concat(self):
-        fake_fs = {
-            'd0_0': {
-                '$': None,
-                'd1_0': {
-                    'd1_0_0.yaml': None,
-                },
-                'd1_1': {
-                    'd1_1_0.yaml': None,
-                    'd1_1_1.yaml': None,
-                },
-                'd1_2': {
-                    '+': None,
-                    'd1_2_0.yaml': None,
-                    'd1_2_1.yaml': None,
-                    'd1_2_2.yaml': None,
-                    'd1_2_3.yaml': None,
-                },
-            },
-        }
-        fake_fs1 = {
-            'd0_0$': {
-                'd1_0': {
-                    'd1_0_0.yaml': None,
-                },
-                'd1_1': {
-                    'd1_1_0.yaml': None,
-                    'd1_1_1.yaml': None,
-                },
-                'd1_2': {
-                    '+': None,
-                    'd1_2_0.yaml': None,
-                    'd1_2_1.yaml': None,
-                    'd1_2_2.yaml': None,
-                    'd1_2_3.yaml': None,
-                },
-            },
-        }
-        for fs, root in [(fake_fs,'d0_0'), (fake_fs1,'d0_0$')]:
-            self.start_patchers(fs)
-            try:
-                result = build_matrix.build_matrix(root)
-            finally:
-                self.stop_patchers()
-            assert len(result) == 1
-            if result[0][0][1:].startswith('d1_2'):
-                for i in result:
-                    assert os.path.join(root, 'd1_2/d1_2_0.yaml') in i[1]
-                    assert os.path.join(root, 'd1_2/d1_2_1.yaml') in i[1]
-                    assert os.path.join(root, 'd1_2/d1_2_2.yaml') in i[1]
-                    assert os.path.join(root, 'd1_2/d1_2_3.yaml') in i[1]
-
-    def test_random_dollar_sign_with_convolve(self):
-        fake_fs = {
-            'd0_0': {
-                '%': None,
-                'd1_0': {
-                    'd1_0_0.yaml': None,
-                    'd1_0_1.yaml': None,
-                },
-                'd1_1': {
-                    'd1_1_0.yaml': None,
-                    'd1_1_1.yaml': None,
-                },
-                'd1_2': {
-                    '$': None,
-                    'd1_2_0.yaml': None,
-                    'd1_2_1.yaml': None,
-                    'd1_2_2.yaml': None,
-                },
-            },
-        }
-        self.start_patchers(fake_fs)
-        try:
-            result = build_matrix.build_matrix('d0_0')
-        finally:
-            self.stop_patchers()
-        assert len(result) == 4
-        fake_fs1 = {
-            'd0_0': {
-                '%': None,
-                'd1_0': {
-                    'd1_0_0.yaml': None,
-                    'd1_0_1.yaml': None,
-                },
-                'd1_1': {
-                    'd1_1_0.yaml': None,
-                    'd1_1_1.yaml': None,
-                },
-                'd1_2$': {
-                    'd1_2_0.yaml': None,
-                    'd1_2_1.yaml': None,
-                    'd1_2_2.yaml': None,
-                },
-            },
-        }
-        self.start_patchers(fake_fs1)
-        try:
-            result = build_matrix.build_matrix('d0_0')
-        finally:
-            self.stop_patchers()
-        assert len(result) == 4
-
-    def test_emulate_teuthology_noceph(self):
-        fake_fs = {
-            'teuthology': {
-                'no-ceph': {
-                    '%': None,
-                    'clusters': {
-                        'single.yaml': None,
-                    },
-                    'distros': {
-                        'baremetal.yaml': None,
-                        'rhel7.0.yaml': None,
-                        'ubuntu12.04.yaml': None,
-                        'ubuntu14.04.yaml': None,
-                        'vps.yaml': None,
-                        'vps_centos6.5.yaml': None,
-                        'vps_debian7.yaml': None,
-                        'vps_rhel6.4.yaml': None,
-                        'vps_rhel6.5.yaml': None,
-                        'vps_rhel7.0.yaml': None,
-                        'vps_ubuntu14.04.yaml': None,
-                    },
-                    'tasks': {
-                        'teuthology.yaml': None,
-                    },
-                },
-            },
-        }
-        self.start_patchers(fake_fs)
-        try:
-            result = build_matrix.build_matrix('teuthology/no-ceph')
-        finally:
-            self.stop_patchers()
-        assert len(result) == 11
-        assert self.fragment_occurences(result, 'vps.yaml') == 1 / 11.0
-
-    def test_empty_dirs(self):
-        fake_fs = {
-            'teuthology': {
-                'no-ceph': {
-                    '%': None,
-                    'clusters': {
-                        'single.yaml': None,
-                    },
-                    'distros': {
-                        'baremetal.yaml': None,
-                        'rhel7.0.yaml': None,
-                        'ubuntu12.04.yaml': None,
-                        'ubuntu14.04.yaml': None,
-                        'vps.yaml': None,
-                        'vps_centos6.5.yaml': None,
-                        'vps_debian7.yaml': None,
-                        'vps_rhel6.4.yaml': None,
-                        'vps_rhel6.5.yaml': None,
-                        'vps_rhel7.0.yaml': None,
-                        'vps_ubuntu14.04.yaml': None,
-                    },
-                    'tasks': {
-                        'teuthology.yaml': None,
-                    },
-                },
-            },
-        }
-        self.start_patchers(fake_fs)
-        try:
-            result = build_matrix.build_matrix('teuthology/no-ceph')
-        finally:
-            self.stop_patchers()
-
-        fake_fs2 = {
-            'teuthology': {
-                'no-ceph': {
-                    '%': None,
-                    'clusters': {
-                        'single.yaml': None,
-                    },
-                    'distros': {
-                        'empty': {},
-                        'baremetal.yaml': None,
-                        'rhel7.0.yaml': None,
-                        'ubuntu12.04.yaml': None,
-                        'ubuntu14.04.yaml': None,
-                        'vps.yaml': None,
-                        'vps_centos6.5.yaml': None,
-                        'vps_debian7.yaml': None,
-                        'vps_rhel6.4.yaml': None,
-                        'vps_rhel6.5.yaml': None,
-                        'vps_rhel7.0.yaml': None,
-                        'vps_ubuntu14.04.yaml': None,
-                    },
-                    'tasks': {
-                        'teuthology.yaml': None,
-                    },
-                    'empty': {},
-                },
-            },
-        }
-        self.start_patchers(fake_fs2)
-        try:
-            result2 = build_matrix.build_matrix('teuthology/no-ceph')
-        finally:
-            self.stop_patchers()
-        assert len(result) == 11
-        assert len(result2) == len(result)
-
-    def test_hidden(self):
-        fake_fs = {
-            'teuthology': {
-                'no-ceph': {
-                    '%': None,
-                    '.qa': None,
-                    'clusters': {
-                        'single.yaml': None,
-                        '.qa': None,
-                    },
-                    'distros': {
-                        '.qa': None,
-                        'baremetal.yaml': None,
-                        'rhel7.0.yaml': None,
-                        'ubuntu12.04.yaml': None,
-                        'ubuntu14.04.yaml': None,
-                        'vps.yaml': None,
-                        'vps_centos6.5.yaml': None,
-                        'vps_debian7.yaml': None,
-                        'vps_rhel6.4.yaml': None,
-                        'vps_rhel6.5.yaml': None,
-                        'vps_rhel7.0.yaml': None,
-                        'vps_ubuntu14.04.yaml': None,
-                    },
-                    'tasks': {
-                        '.qa': None,
-                        'teuthology.yaml': None,
-                    },
-                    '.foo': {
-                        '.qa': None,
-                        'teuthology.yaml': None,
-                    },
-                },
-            },
-        }
-        self.start_patchers(fake_fs)
-        try:
-            result = build_matrix.build_matrix('teuthology/no-ceph')
-        finally:
-            self.stop_patchers()
-
-        fake_fs2 = {
-            'teuthology': {
-                'no-ceph': {
-                    '%': None,
-                    'clusters': {
-                        'single.yaml': None,
-                    },
-                    'distros': {
-                        'baremetal.yaml': None,
-                        'rhel7.0.yaml': None,
-                        'ubuntu12.04.yaml': None,
-                        'ubuntu14.04.yaml': None,
-                        'vps.yaml': None,
-                        'vps_centos6.5.yaml': None,
-                        'vps_debian7.yaml': None,
-                        'vps_rhel6.4.yaml': None,
-                        'vps_rhel6.5.yaml': None,
-                        'vps_rhel7.0.yaml': None,
-                        'vps_ubuntu14.04.yaml': None,
-                    },
-                    'tasks': {
-                        'teuthology.yaml': None,
-                    },
-                },
-            },
-        }
-        self.start_patchers(fake_fs2)
-        try:
-            result2 = build_matrix.build_matrix('teuthology/no-ceph')
-        finally:
-            self.stop_patchers()
-        assert len(result) == 11
-        assert len(result2) == len(result)
-
-    def test_disable_extension(self):
-        fake_fs = {
-            'teuthology': {
-                'no-ceph': {
-                    '%': None,
-                    'clusters': {
-                        'single.yaml': None,
-                    },
-                    'distros': {
-                        'baremetal.yaml': None,
-                        'rhel7.0.yaml': None,
-                        'ubuntu12.04.yaml': None,
-                        'ubuntu14.04.yaml': None,
-                        'vps.yaml': None,
-                        'vps_centos6.5.yaml': None,
-                        'vps_debian7.yaml': None,
-                        'vps_rhel6.4.yaml': None,
-                        'vps_rhel6.5.yaml': None,
-                        'vps_rhel7.0.yaml': None,
-                        'vps_ubuntu14.04.yaml': None,
-                    },
-                    'tasks': {
-                        'teuthology.yaml': None,
-                    },
-                },
-            },
-        }
-        self.start_patchers(fake_fs)
-        try:
-            result = build_matrix.build_matrix('teuthology/no-ceph')
-        finally:
-            self.stop_patchers()
-
-        fake_fs2 = {
-            'teuthology': {
-                'no-ceph': {
-                    '%': None,
-                    'clusters': {
-                        'single.yaml': None,
-                    },
-                    'distros': {
-                        'baremetal.yaml': None,
-                        'rhel7.0.yaml': None,
-                        'ubuntu12.04.yaml': None,
-                        'ubuntu14.04.yaml': None,
-                        'vps.yaml': None,
-                        'vps_centos6.5.yaml': None,
-                        'vps_debian7.yaml': None,
-                        'vps_rhel6.4.yaml': None,
-                        'vps_rhel6.5.yaml': None,
-                        'vps_rhel7.0.yaml': None,
-                        'vps_ubuntu14.04.yaml': None,
-                        'forcefilevps_ubuntu14.04.yaml.disable': None,
-                        'forcefilevps_ubuntu14.04.yaml.anotherextension': None,
-                    },
-                    'tasks': {
-                        'teuthology.yaml': None,
-                        'forcefilevps_ubuntu14.04notyaml': None,
-                    },
-                    'forcefilevps_ubuntu14.04notyaml': None,
-                    'tasks.disable': {
-                        'teuthology2.yaml': None,
-                        'forcefilevps_ubuntu14.04notyaml': None,
-                    },
-                },
-            },
-        }
-        self.start_patchers(fake_fs2)
-        try:
-            result2 = build_matrix.build_matrix('teuthology/no-ceph')
-        finally:
-            self.stop_patchers()
-        assert len(result) == 11
-        assert len(result2) == len(result)
-
-    def test_sort_order(self):
-        # This test ensures that 'ceph' comes before 'ceph-thrash' when yaml
-        # fragments are sorted.
-        fake_fs = {
-            'thrash': {
-                '%': None,
-                'ceph-thrash': {'default.yaml': None},
-                'ceph': {'base.yaml': None},
-                'clusters': {'mds-1active-1standby.yaml': None},
-                'debug': {'mds_client.yaml': None},
-                'fs': {'btrfs.yaml': None},
-                'msgr-failures': {'none.yaml': None},
-                'overrides': {'allowlist_wrongly_marked_down.yaml': None},
-                'tasks': {'cfuse_workunit_suites_fsstress.yaml': None},
-            },
-        }
-        self.start_patchers(fake_fs)
-        try:
-            result = build_matrix.build_matrix('thrash')
-        finally:
-            self.stop_patchers()
-        assert len(result) == 1
-        assert self.fragment_occurences(result, 'base.yaml') == 1
-        fragments = result[0][1]
-        assert fragments[0] == 'thrash/ceph/base.yaml'
-        assert fragments[1] == 'thrash/ceph-thrash/default.yaml'
-
-class TestSubset(object):
-    patchpoints = [
-        'os.path.exists',
-        'os.listdir',
-        'os.path.isfile',
-        'os.path.isdir',
-        'builtins.open',
-    ]
-
-    def setup_method(self):
-        self.mocks = dict()
-        self.patchers = dict()
-        for ppoint in self.__class__.patchpoints:
-            self.mocks[ppoint] = MagicMock()
-            self.patchers[ppoint] = patch(ppoint, self.mocks[ppoint])
-
-    def start_patchers(self, fake_fs):
-        fake_fns = make_fake_fstools(fake_fs)
-        # N.B.: relies on fake_fns being in same order as patchpoints
-        for ppoint, fn in zip(self.__class__.patchpoints, fake_fns):
-            self.mocks[ppoint].side_effect = fn
-            self.patchers[ppoint].start()
-
-    def stop_patchers(self):
-        for patcher in self.patchers.values():
-            patcher.stop()
-
-    def teardown_method(self):
-        self.patchers.clear()
-        self.mocks.clear()
-
-    MAX_FACETS = 10
-    MAX_FANOUT = 3
-    MAX_DEPTH = 3
-    MAX_SUBSET = 10
-    @staticmethod
-    def generate_fake_fs(max_facets, max_fanout, max_depth):
-        def yamilify(name):
-            return name + ".yaml"
-        def name_generator():
-            x = 0
-            while True:
-                yield(str(x))
-                x += 1
-        def generate_tree(
-                max_facets, max_fanout, max_depth, namegen, top=True):
-            if max_depth == 0:
-                return None
-            if max_facets == 0:
-                return None
-            items = random.choice(range(max_fanout))
-            if items == 0 and top:
-                items = 1
-            if items == 0:
-                return None
-            sub_max_facets = max_facets / items
-            tree = {}
-            for i in range(items):
-                subtree = generate_tree(
-                    sub_max_facets, max_fanout,
-                    max_depth - 1, namegen, top=False)
-                if subtree is not None:
-                    tree['d' + next(namegen)] = subtree
-                else:
-                    tree[yamilify('f' + next(namegen))] = None
-            random.choice([
-                lambda: tree.update({'%': None}),
-                lambda: None])()
-            return tree
-        return {
-            'root':  generate_tree(
-                max_facets, max_fanout, max_depth, name_generator())
-        }
-
-    @staticmethod
-    def generate_subset(maxsub):
-        divisions = random.choice(range(maxsub-1))+1
-        return (random.choice(range(divisions)), divisions)
-
-    @staticmethod
-    def generate_description_list(tree, subset):
-        mat, first, matlimit = build_matrix._get_matrix(
-            'root', subset=subset)
-        return [i[0] for i in build_matrix.generate_combinations(
-            'root', mat, first, matlimit)], mat, first, matlimit
-
-    @staticmethod
-    def verify_facets(tree, description_list, subset, mat, first, matlimit):
-        def flatten(tree):
-            for k,v in tree.items():
-                if v is None and '.yaml' in k:
-                    yield k
-                elif v is not None and '.disable' not in k:
-                    for x in flatten(v):
-                        yield x
-
-        def pptree(tree, tabs=0):
-            ret = ""
-            for k, v in tree.items():
-                if v is None:
-                    ret += ('\t'*tabs) + k.ljust(10) + "\n"
-                else:
-                    ret += ('\t'*tabs) + (k + ':').ljust(10) + "\n"
-                    ret += pptree(v, tabs+1)
-            return ret
-        def deyamlify(name):
-            if name.endswith('.yaml'):
-                return name[:-5]
-            else:
-                return name
-        for facet in (deyamlify(_) for _ in flatten(tree)):
-            found = False
-            for i in description_list:
-                if facet in i:
-                    found = True
-                    break
-            if not found:
-                print("tree\n{tree}\ngenerated list\n{desc}\n\nfrom matrix\n\n{matrix}\nsubset {subset} without facet {fac}".format(
-                    tree=pptree(tree),
-                    desc='\n'.join(description_list),
-                    subset=subset,
-                    matrix=str(mat),
-                    fac=facet))
-                all_desc = build_matrix.generate_combinations(
-                    'root',
-                    mat,
-                    0,
-                    mat.size())
-                for i, desc in zip(range(mat.size()), all_desc):
-                    if i == first:
-                        print('==========')
-                    print("{} {}".format(i, desc))
-                    if i + 1 == matlimit:
-                        print('==========')
-            assert found
-
-    def test_random(self):
-        for i in range(10000):
-            tree = self.generate_fake_fs(
-                self.MAX_FACETS,
-                self.MAX_FANOUT,
-                self.MAX_DEPTH)
-            subset = self.generate_subset(self.MAX_SUBSET)
-            self.start_patchers(tree)
-            try:
-                dlist, mat, first, matlimit = self.generate_description_list(tree, subset)
-            finally:
-                self.stop_patchers()
-            self.verify_facets(tree, dlist, subset, mat, first, matlimit)
diff --git a/teuthology/suite/test/test_init.py b/teuthology/suite/test/test_init.py
deleted file mode 100644 (file)
index 6e91eef..0000000
+++ /dev/null
@@ -1,267 +0,0 @@
-import os
-
-from copy import deepcopy
-
-from mock import patch, Mock, DEFAULT
-
-from teuthology import suite
-from scripts.suite import main
-from teuthology.config import config
-
-import pytest
-import time
-
-from teuthology.exceptions import ScheduleFailError
-
-def get_fake_time_and_sleep():
-    # Below we set m_time.side_effect, but we also set m_time.return_value.
-    # The reason for this is that we need to store a 'fake time' that
-    # increments when m_sleep() is called; we could use any variable name we
-    # wanted for the return value, but since 'return_value' is already a
-    # standard term in mock, and since setting side_effect causes return_value
-    # to be ignored, it's safe to just reuse the name here.
-    m_time = Mock()
-    m_time.return_value = time.time()
-
-    def m_time_side_effect():
-        # Fake the slow passage of time
-        m_time.return_value += 0.1
-        return m_time.return_value
-    m_time.side_effect = m_time_side_effect
-
-    def f_sleep(seconds):
-        m_time.return_value += seconds
-    m_sleep = Mock(wraps=f_sleep)
-    return m_time, m_sleep
-
-
-def setup_module():
-    global m_time
-    global m_sleep
-    m_time, m_sleep = get_fake_time_and_sleep()
-    global patcher_time_sleep
-    patcher_time_sleep = patch.multiple(
-        'teuthology.suite.time',
-        time=m_time,
-        sleep=m_sleep,
-    )
-    patcher_time_sleep.start()
-
-
-def teardown_module():
-    patcher_time_sleep.stop()
-
-
-@patch.object(suite.ResultsReporter, 'get_jobs')
-def test_wait_success(m_get_jobs, caplog):
-    results = [
-        [{'status': 'queued', 'job_id': '2'}],
-        [],
-    ]
-    final = [
-        {'status': 'pass', 'job_id': '1',
-         'description': 'DESC1', 'log_href': 'http://URL1'},
-        {'status': 'fail', 'job_id': '2',
-         'description': 'DESC2', 'log_href': 'http://URL2'},
-        {'status': 'pass', 'job_id': '3',
-         'description': 'DESC3', 'log_href': 'http://URL3'},
-    ]
-
-    def get_jobs(name, **kwargs):
-        if kwargs['fields'] == ['job_id', 'status']:
-            return in_progress.pop(0)
-        else:
-            return final
-    m_get_jobs.side_effect = get_jobs
-    suite.Run.WAIT_PAUSE = 1
-
-    in_progress = deepcopy(results)
-    assert 0 == suite.wait('name', 1, 'http://UPLOAD_URL')
-    m_get_jobs.assert_any_call('name', fields=['job_id', 'status'])
-    assert 0 == len(in_progress)
-    assert 'fail http://UPLOAD_URL/name/2' in caplog.text
-
-    m_get_jobs.reset_mock()
-    in_progress = deepcopy(results)
-    assert 0 == suite.wait('name', 1, None)
-    m_get_jobs.assert_any_call('name', fields=['job_id', 'status'])
-    assert 0 == len(in_progress)
-    assert 'fail http://URL2' in caplog.text
-
-
-@patch.object(suite.ResultsReporter, 'get_jobs')
-def test_wait_fails(m_get_jobs):
-    results = []
-    results.append([{'status': 'queued', 'job_id': '2'}])
-    results.append([{'status': 'queued', 'job_id': '2'}])
-    results.append([{'status': 'queued', 'job_id': '2'}])
-
-    def get_jobs(name, **kwargs):
-        return results.pop(0)
-    m_get_jobs.side_effect = get_jobs
-    suite.Run.WAIT_PAUSE = 1
-    suite.Run.WAIT_MAX_JOB_TIME = 1
-    with pytest.raises(suite.WaitException):
-        suite.wait('name', 1, None)
-
-
-REPO_SHORTHAND = [
-    ['https://github.com/dude/foo', 'bar',
-     'https://github.com/dude/bar.git'],
-    ['https://github.com/dude/foo/', 'bar',
-     'https://github.com/dude/bar.git'],
-    ['https://github.com/ceph/ceph', 'ceph',
-     'https://github.com/ceph/ceph.git'],
-    ['https://github.com/ceph/ceph/', 'ceph',
-     'https://github.com/ceph/ceph.git'],
-    ['https://github.com/ceph/ceph.git', 'ceph',
-     'https://github.com/ceph/ceph.git'],
-    ['https://github.com/ceph/ceph', 'ceph-ci',
-     'https://github.com/ceph/ceph-ci.git'],
-    ['https://github.com/ceph/ceph-ci', 'ceph',
-     'https://github.com/ceph/ceph.git'],
-    ['git://git.ceph.com/ceph.git', 'ceph',
-     'git://git.ceph.com/ceph.git'],
-    ['git://git.ceph.com/ceph.git', 'ceph-ci',
-     'git://git.ceph.com/ceph-ci.git'],
-    ['git://git.ceph.com/ceph-ci.git', 'ceph',
-     'git://git.ceph.com/ceph.git'],
-    ['https://github.com/ceph/ceph.git', 'ceph/ceph-ci',
-     'https://github.com/ceph/ceph-ci.git'],
-    ['https://github.com/ceph/ceph.git', 'https://github.com/ceph/ceph-ci',
-     'https://github.com/ceph/ceph-ci'],
-    ['https://github.com/ceph/ceph.git', 'https://github.com/ceph/ceph-ci/',
-     'https://github.com/ceph/ceph-ci/'],
-    ['https://github.com/ceph/ceph.git', 'https://github.com/ceph/ceph-ci.git',
-     'https://github.com/ceph/ceph-ci.git'],
-]
-
-
-@pytest.mark.parametrize(['orig', 'shorthand', 'result'], REPO_SHORTHAND)
-def test_expand_short_repo_name(orig, shorthand, result):
-    assert suite.expand_short_repo_name(shorthand, orig) == result
-
-
-class TestSuiteMain(object):
-    def test_main(self):
-        suite_name = 'SUITE'
-        throttle = '3'
-        machine_type = 'burnupi'
-
-        def prepare_and_schedule(obj):
-            assert obj.base_config.suite == suite_name
-            assert obj.args.throttle == throttle
-
-        def fake_str(*args, **kwargs):
-            return 'fake'
-
-        def fake_bool(*args, **kwargs):
-            return True
-
-        def fake_false(*args, **kwargs):
-            return False
-
-        with patch.multiple(
-                'teuthology.suite.run.util',
-                fetch_repos=DEFAULT,
-                package_version_for_hash=fake_str,
-                git_branch_exists=fake_bool,
-                git_ls_remote=fake_str,
-                ):
-            with patch.multiple(
-                'teuthology.suite.run.Run',
-                prepare_and_schedule=prepare_and_schedule,
-            ), patch.multiple(
-                'teuthology.suite.run.os.path',
-                exists=fake_false,
-            ):
-                main([
-                    '--ceph', 'main',
-                    '--suite', suite_name,
-                    '--throttle', throttle,
-                    '--machine-type', machine_type,
-                ])
-
-    @patch('teuthology.suite.util.smtplib.SMTP')
-    def test_machine_type_multi_error(self, m_smtp):
-        config.results_email = "example@example.com"
-        with pytest.raises(ScheduleFailError) as exc:
-            main([
-                    '--ceph', 'main',
-                    '--suite', 'suite_name',
-                    '--throttle', '3',
-                    '--machine-type', 'multi',
-                    '--dry-run',
-            ])
-        assert str(exc.value) == "Scheduling failed: 'multi' is not a valid machine_type. \
-Maybe you want 'gibba,smithi,mira' or similar"
-        m_smtp.assert_not_called()
-
-    @patch('teuthology.suite.util.smtplib.SMTP')
-    def test_machine_type_none_error(self, m_smtp):
-        config.result_email = 'example@example.com'
-        with pytest.raises(ScheduleFailError) as exc:
-            main([
-                    '--ceph', 'main',
-                    '--suite', 'suite_name',
-                    '--throttle', '3',
-                    '--machine-type', 'None',
-                    '--dry-run',
-            ])
-        assert str(exc.value) == "Scheduling failed: Must specify a machine_type"
-        m_smtp.assert_not_called()
-
-    def test_schedule_suite_noverify(self):
-        suite_name = 'noop'
-        suite_dir = os.path.dirname(__file__)
-        throttle = '3'
-        machine_type = 'burnupi'
-
-        with patch.multiple(
-            'teuthology.suite.util',
-            fetch_repos=DEFAULT,
-            teuthology_schedule=DEFAULT,
-            get_arch=lambda x: 'x86_64',
-            get_gitbuilder_hash=DEFAULT,
-            git_ls_remote=lambda *args: '1234',
-            package_version_for_hash=DEFAULT,
-        ) as m:
-            m['package_version_for_hash'].return_value = 'fake-9.5'
-            config.suite_verify_ceph_hash = False
-            main([
-                '--ceph', 'main',
-                '--suite', suite_name,
-                '--suite-dir', suite_dir,
-                '--suite-relpath', '',
-                '--throttle', throttle,
-                '--machine-type', machine_type
-            ])
-            m_sleep.assert_called_with(int(throttle))
-            m['get_gitbuilder_hash'].assert_not_called()
-
-    def test_schedule_suite(self):
-        suite_name = 'noop'
-        suite_dir = os.path.dirname(__file__)
-        throttle = '3'
-        machine_type = 'burnupi'
-
-        with patch.multiple(
-            'teuthology.suite.util',
-            fetch_repos=DEFAULT,
-            teuthology_schedule=DEFAULT,
-            get_arch=lambda x: 'x86_64',
-            get_gitbuilder_hash=DEFAULT,
-            git_ls_remote=lambda *args: '12345',
-            package_version_for_hash=DEFAULT,
-        ) as m:
-            m['package_version_for_hash'].return_value = 'fake-9.5'
-            config.suite_verify_ceph_hash = True
-            main([
-                '--ceph', 'main',
-                '--suite', suite_name,
-                '--suite-dir', suite_dir,
-                '--suite-relpath', '',
-                '--throttle', throttle,
-                '--machine-type', machine_type
-            ])
-            m_sleep.assert_called_with(int(throttle))
diff --git a/teuthology/suite/test/test_matrix.py b/teuthology/suite/test/test_matrix.py
deleted file mode 100644 (file)
index 596bb37..0000000
+++ /dev/null
@@ -1,82 +0,0 @@
-from teuthology.suite import matrix
-
-
-def verify_matrix_output_diversity(res):
-    """
-    Verifies that the size of the matrix passed matches the number of unique
-    outputs from res.index
-    """
-    sz = res.size()
-    s = frozenset([matrix.generate_lists(res.index(i)) for i in range(sz)])
-    for i in range(res.size()):
-        assert sz == len(s)
-
-
-def mbs(num, l):
-    return matrix.Sum(num*10, [matrix.Base(i + (100*num)) for i in l])
-
-
-class TestMatrix(object):
-    def test_simple(self):
-        verify_matrix_output_diversity(mbs(1, range(6)))
-
-    def test_simple2(self):
-        verify_matrix_output_diversity(mbs(1, range(5)))
-
-    # The test_product* tests differ by the degree by which dimension
-    # sizes share prime factors
-    def test_product_simple(self):
-        verify_matrix_output_diversity(
-            matrix.Product(1, [mbs(1, range(6)), mbs(2, range(2))]))
-
-    def test_product_3_facets_2_prime_factors(self):
-        verify_matrix_output_diversity(matrix.Product(1, [
-                    mbs(1, range(6)),
-                    mbs(2, range(2)),
-                    mbs(3, range(3)),
-                    ]))
-
-    def test_product_3_facets_2_prime_factors_one_larger(self):
-        verify_matrix_output_diversity(matrix.Product(1, [
-                    mbs(1, range(2)),
-                    mbs(2, range(5)),
-                    mbs(4, range(4)),
-                    ]))
-
-    def test_product_4_facets_2_prime_factors(self):
-        verify_matrix_output_diversity(matrix.Sum(1, [
-                    mbs(1, range(6)),
-                    mbs(3, range(3)),
-                    mbs(2, range(2)),
-                    mbs(4, range(9)),
-                    ]))
-
-    def test_product_2_facets_2_prime_factors(self):
-        verify_matrix_output_diversity(matrix.Sum(1, [
-                    mbs(1, range(2)),
-                    mbs(2, range(5)),
-                    ]))
-
-    def test_product_with_sum(self):
-        verify_matrix_output_diversity(matrix.Sum(
-                9,
-                [
-                    mbs(10, range(6)),
-                    matrix.Product(1, [
-                            mbs(1, range(2)),
-                            mbs(2, range(5)),
-                            mbs(4, range(4))]),
-                    matrix.Product(8, [
-                            mbs(7, range(2)),
-                            mbs(6, range(5)),
-                            mbs(5, range(4))])
-                    ]
-                ))
-
-    def test_product_with_pick_random(self):
-        verify_matrix_output_diversity(matrix.PickRandom(1, [
-                    mbs(1, range(6)),
-                    mbs(3, range(3)),
-                    mbs(2, range(2)),
-                    mbs(4, range(9)),
-                    ]))
diff --git a/teuthology/suite/test/test_merge.py b/teuthology/suite/test/test_merge.py
deleted file mode 100644 (file)
index 82a0bb6..0000000
+++ /dev/null
@@ -1,231 +0,0 @@
-import logging
-from textwrap import dedent
-
-from mock import patch, MagicMock
-
-from teuthology.suite import build_matrix
-from teuthology.suite.merge import config_merge
-from teuthology.test.fake_fs import make_fake_fstools
-
-log = logging.getLogger(__name__)
-
-class TestMerge:
-    patchpoints = [
-        'os.path.exists',
-        'os.listdir',
-        'os.path.isfile',
-        'os.path.isdir',
-        'builtins.open',
-    ]
-
-    def setup_method(self):
-        self.mocks = dict()
-        self.patchers = dict()
-        for ppoint in self.__class__.patchpoints:
-            self.mocks[ppoint] = MagicMock()
-            self.patchers[ppoint] = patch(ppoint, self.mocks[ppoint])
-
-    def start_patchers(self, fake_fs):
-        fake_fns = make_fake_fstools(fake_fs)
-        # N.B.: relies on fake_fns being in same order as patchpoints
-        for ppoint, fn in zip(self.__class__.patchpoints, fake_fns):
-            self.mocks[ppoint].side_effect = fn
-            self.patchers[ppoint].start()
-
-    def stop_patchers(self):
-        for patcher in self.patchers.values():
-            patcher.stop()
-
-    def teardown_method(self):
-        self.patchers.clear()
-        self.mocks.clear()
-
-    def test_premerge(self):
-        fake_fs = {
-            'd0_0': {
-                '%': None,
-                'd1_0': {
-                  'a.yaml': dedent("""
-                  teuthology:
-                    premerge: reject()
-                  foo: bar
-                  """),
-                },
-                'c.yaml': dedent("""
-                top: pot
-                """),
-            },
-        }
-        self.start_patchers(fake_fs)
-        try:
-            result = build_matrix.build_matrix('d0_0')
-            assert 1 == len(result)
-            configs = list(config_merge(result))
-            assert 1 == len(configs)
-            desc, frags, yaml = configs[0]
-            assert "top" in yaml
-            assert "foo" not in yaml
-        finally:
-            self.stop_patchers()
-
-    def test_postmerge(self):
-        fake_fs = {
-            'd0_0': {
-                '%': None,
-                'd1_0': {
-                  'a.yaml': dedent("""
-                  teuthology:
-                    postmerge:
-                      - reject()
-                  foo: bar
-                  """),
-                  'b.yaml': dedent("""
-                  baz: zab
-                  """),
-                },
-                'c.yaml': dedent("""
-                top: pot
-                """),
-            },
-        }
-        self.start_patchers(fake_fs)
-        try:
-            result = build_matrix.build_matrix('d0_0')
-            assert 2 == len(result)
-            configs = list(config_merge(result))
-            assert 1 == len(configs)
-            desc, frags, yaml = configs[0]
-            assert "top" in yaml
-            assert "baz" in yaml
-            assert "foo" not in yaml
-        finally:
-            self.stop_patchers()
-
-    def test_postmerge_concat(self):
-        fake_fs = {
-            'd0_0': {
-                '%': None,
-                'd1_0': {
-                  'a.yaml': dedent("""
-                  teuthology:
-                    postmerge:
-                      - local a = 1
-                  foo: bar
-                  """),
-                  'b.yaml': dedent("""
-                  teuthology:
-                    postmerge:
-                      - local a = 2
-                  baz: zab
-                  """),
-                },
-                'z.yaml': dedent("""
-                 teuthology:
-                   postmerge:
-                     - if a == 1 then reject() end
-                 top: pot
-                 """),
-            },
-        }
-        self.start_patchers(fake_fs)
-        try:
-            result = build_matrix.build_matrix('d0_0')
-            assert 2 == len(result)
-            configs = list(config_merge(result))
-            assert 1 == len(configs)
-            desc, frags, yaml = configs[0]
-            assert "top" in yaml
-            assert "baz" in yaml
-            assert "foo" not in yaml
-        finally:
-            self.stop_patchers()
-
-
-    def test_yaml_mutation(self):
-        fake_fs = {
-            'd0_0': {
-                '%': None,
-                'c.yaml': dedent("""
-                teuthology:
-                  postmerge:
-                    - |
-                      yaml["test"] = py_dict()
-                top: pot
-                """),
-            },
-        }
-        self.start_patchers(fake_fs)
-        try:
-            result = build_matrix.build_matrix('d0_0')
-            assert 1 == len(result)
-            configs = list(config_merge(result))
-            assert 1 == len(configs)
-            desc, frags, yaml = configs[0]
-            assert "test" in yaml
-            assert {} == yaml["test"]
-        finally:
-            self.stop_patchers()
-
-    def test_sandbox(self):
-        fake_fs = {
-            'd0_0': {
-                '%': None,
-                'c.yaml': dedent("""
-                teuthology:
-                  postmerge:
-                    - |
-                      log.debug(_ENV)
-                      log.debug("_ENV contains:")
-                      for k,v in pairs(_ENV) do
-                        log.debug("_ENV['%s'] = %s", tostring(k), tostring(v))
-                      end
-                      local check = {
-                        "assert",
-                        "error",
-                        "ipairs",
-                        "next",
-                        "pairs",
-                        "tonumber",
-                        "tostring",
-                        "py_attrgetter",
-                        "py_dict",
-                        "py_list",
-                        "py_tuple",
-                        "py_enumerate",
-                        "py_iterex",
-                        "py_itemgetter",
-                        "math",
-                        "reject",
-                        "accept",
-                        "deep_merge",
-                        "log",
-                        "reject",
-                        "yaml_load",
-                      }
-                      for _,v in ipairs(check) do
-                        log.debug("checking %s", tostring(v))
-                        assert(_ENV[v])
-                      end
-                      local block = {
-                        "coroutine",
-                        "debug",
-                        "io",
-                        "os",
-                        "package",
-                      }
-                      for _,v in ipairs(block) do
-                        log.debug("checking %s", tostring(v))
-                        assert(_ENV[v] == nil)
-                      end
-                top: pot
-                """),
-            },
-        }
-        self.start_patchers(fake_fs)
-        try:
-            result = build_matrix.build_matrix('d0_0')
-            assert 1 == len(result)
-            configs = list(config_merge(result))
-            assert 1 == len(configs)
-        finally:
-            self.stop_patchers()
diff --git a/teuthology/suite/test/test_placeholder.py b/teuthology/suite/test/test_placeholder.py
deleted file mode 100644 (file)
index 31b5175..0000000
+++ /dev/null
@@ -1,57 +0,0 @@
-from teuthology.suite.placeholder import (
-    substitute_placeholders, dict_templ, Placeholder
-)
-
-
-class TestPlaceholder(object):
-    def test_substitute_placeholders(self):
-        suite_hash = 'suite_hash'
-        input_dict = dict(
-            suite='suite',
-            suite_branch='suite_branch',
-            suite_hash=suite_hash,
-            ceph_branch='ceph_branch',
-            ceph_hash='ceph_hash',
-            teuthology_branch='teuthology_branch',
-            teuthology_sha1='teuthology_sha1',
-            machine_type='machine_type',
-            distro='distro',
-            distro_version='distro_version',
-            archive_upload='archive_upload',
-            archive_upload_key='archive_upload_key',
-            suite_repo='https://example.com/ceph/suite.git',
-            suite_relpath='',
-            ceph_repo='https://example.com/ceph/ceph.git',
-            flavor='default',
-            expire='expire',
-        )
-        output_dict = substitute_placeholders(dict_templ, input_dict)
-        assert output_dict['suite'] == 'suite'
-        assert output_dict['suite_sha1'] == suite_hash
-        assert isinstance(dict_templ['suite'], Placeholder)
-        assert isinstance(
-            dict_templ['overrides']['admin_socket']['branch'],
-            Placeholder)
-
-    def test_null_placeholders_dropped(self):
-        input_dict = dict(
-            suite='suite',
-            suite_branch='suite_branch',
-            suite_hash='suite_hash',
-            ceph_branch='ceph_branch',
-            ceph_hash='ceph_hash',
-            teuthology_branch='teuthology_branch',
-            teuthology_sha1='teuthology_sha1',
-            machine_type='machine_type',
-            archive_upload='archive_upload',
-            archive_upload_key='archive_upload_key',
-            distro=None,
-            distro_version=None,
-            suite_repo='https://example.com/ceph/suite.git',
-            suite_relpath='',
-            ceph_repo='https://example.com/ceph/ceph.git',
-            flavor=None,
-            expire='expire',
-        )
-        output_dict = substitute_placeholders(dict_templ, input_dict)
-        assert 'os_type' not in output_dict
diff --git a/teuthology/suite/test/test_run_.py b/teuthology/suite/test/test_run_.py
deleted file mode 100644 (file)
index d1c8a06..0000000
+++ /dev/null
@@ -1,642 +0,0 @@
-import logging
-import os
-import pytest
-import requests
-import contextlib
-import yaml
-
-from datetime import datetime, timedelta, timezone
-from mock import patch, call, ANY
-from io import StringIO
-from io import BytesIO
-
-from teuthology.config import config, YamlConfig
-from teuthology.exceptions import ScheduleFailError
-from teuthology.suite import run
-from teuthology.util.time import TIMESTAMP_FMT
-
-log = logging.getLogger(__name__)
-
-class TestRun(object):
-    klass = run.Run
-
-    def setup_method(self):
-        self.args_dict = dict(
-            suite='suite',
-            suite_branch='suite_branch',
-            suite_relpath='',
-            ceph_branch='ceph_branch',
-            ceph_sha1='ceph_sha1',
-            email='address@example.com',
-            teuthology_branch='teuthology_branch',
-            kernel_branch=None,
-            flavor='flavor',
-            distro='ubuntu',
-            machine_type='machine_type',
-            base_yaml_paths=list(),
-        )
-        self.args = YamlConfig.from_dict(self.args_dict)
-
-    @patch('teuthology.suite.run.util.fetch_repos')
-    @patch('teuthology.suite.run.util.git_ls_remote')
-    @patch('teuthology.suite.run.util.git_validate_sha1')
-    def test_email_addr(self, m_git_validate_sha1,
-                        m_git_ls_remote, _):
-        # neuter choose_X_branch
-        m_git_validate_sha1.return_value = self.args_dict['ceph_sha1']
-        self.args_dict['teuthology_branch'] = 'main'
-        self.args_dict['suite_branch'] = 'main'
-        m_git_ls_remote.return_value = 'suite_sha1'
-
-        runobj = self.klass(self.args)
-        assert runobj.base_config.email == self.args_dict['email']
-
-    @patch('teuthology.suite.run.util.fetch_repos')
-    def test_name(self, m_fetch_repos):
-        stamp = datetime.now().strftime(TIMESTAMP_FMT)
-        with patch.object(run.Run, 'create_initial_config',
-                          return_value=run.JobConfig()):
-            name = run.Run(self.args).name
-        assert str(stamp) in name
-
-    @patch('teuthology.suite.run.util.fetch_repos')
-    def test_name_owner(self, m_fetch_repos):
-        self.args.owner = 'USER'
-        with patch.object(run.Run, 'create_initial_config',
-                          return_value=run.JobConfig()):
-            name = run.Run(self.args).name
-        assert name.startswith('USER-')
-
-    @patch('teuthology.suite.run.util.git_branch_exists')
-    @patch('teuthology.suite.run.util.package_version_for_hash')
-    @patch('teuthology.suite.run.util.git_ls_remote')
-    def test_branch_nonexistent(
-        self,
-        m_git_ls_remote,
-        m_package_version_for_hash,
-        m_git_branch_exists,
-    ):
-        config.gitbuilder_host = 'example.com'
-        m_git_ls_remote.side_effect = [
-            # First call will be for the ceph hash
-            None,
-            # Second call will be for the suite hash
-            'suite_hash',
-        ]
-        m_package_version_for_hash.return_value = 'a_version'
-        m_git_branch_exists.return_value = True
-        self.args.ceph_branch = 'ceph_sha1'
-        self.args.ceph_sha1 = None
-        with pytest.raises(ScheduleFailError):
-            self.klass(self.args)
-
-    @pytest.mark.parametrize(
-        ["expire", "delta", "result"],
-        [
-            [None, timedelta(), False],
-            ["1m", timedelta(), True],
-            ["1m", timedelta(minutes=-2), False],
-            ["1m", timedelta(minutes=2), True],
-            ["7d", timedelta(days=-14), False],
-        ]
-    )
-    @patch('teuthology.repo_utils.fetch_repo')
-    @patch('teuthology.suite.run.util.git_branch_exists')
-    @patch('teuthology.suite.run.util.package_version_for_hash')
-    @patch('teuthology.suite.run.util.git_ls_remote')
-    def test_get_expiration(
-        self,
-        m_git_ls_remote,
-        m_package_version_for_hash,
-        m_git_branch_exists,
-        m_fetch_repo,
-        expire,
-        delta,
-        result,
-    ):
-        m_git_ls_remote.side_effect = 'hash'
-        m_package_version_for_hash.return_value = 'a_version'
-        m_git_branch_exists.return_value = True
-        self.args.expire = expire
-        obj = self.klass(self.args)
-        now = datetime.now(timezone.utc)
-        expires_result = obj.get_expiration(_base_time=now + delta)
-        if expire is None:
-            assert expires_result is None
-            assert obj.base_config['expire'] is None
-        else:
-            assert expires_result is not None
-            assert (now < expires_result) is result
-            assert obj.base_config['expire']
-
-    @patch('teuthology.suite.run.util.fetch_repos')
-    @patch('requests.head')
-    @patch('teuthology.suite.run.util.git_branch_exists')
-    @patch('teuthology.suite.run.util.package_version_for_hash')
-    @patch('teuthology.suite.run.util.git_ls_remote')
-    def test_sha1_exists(
-        self,
-        m_git_ls_remote,
-        m_package_version_for_hash,
-        m_git_branch_exists,
-        m_requests_head,
-        m_fetch_repos,
-    ):
-        config.gitbuilder_host = 'example.com'
-        m_package_version_for_hash.return_value = 'ceph_hash'
-        m_git_branch_exists.return_value = True
-        resp = requests.Response()
-        resp.reason = 'OK'
-        resp.status_code = 200
-        m_requests_head.return_value = resp
-        # only one call to git_ls_remote in this case
-        m_git_ls_remote.return_value = "suite_branch"
-        run = self.klass(self.args)
-        assert run.base_config.sha1 == 'ceph_sha1'
-        assert run.base_config.branch == 'ceph_branch'
-
-    @patch('teuthology.suite.run.util.git_ls_remote')
-    @patch('requests.head')
-    @patch('teuthology.suite.util.git_branch_exists')
-    @patch('teuthology.suite.util.package_version_for_hash')
-    def test_sha1_nonexistent(
-        self,
-        m_git_ls_remote,
-        m_package_version_for_hash,
-        m_git_branch_exists,
-        m_requests_head,
-    ):
-        config.gitbuilder_host = 'example.com'
-        m_package_version_for_hash.return_value = 'ceph_hash'
-        m_git_branch_exists.return_value = True
-        resp = requests.Response()
-        resp.reason = 'Not Found'
-        resp.status_code = 404
-        m_requests_head.return_value = resp
-        self.args.ceph_sha1 = 'ceph_hash_dne'
-        with pytest.raises(ScheduleFailError):
-            self.klass(self.args)
-
-    @patch('teuthology.suite.util.smtplib.SMTP')
-    @patch('teuthology.suite.util.git_ls_remote')
-    @patch('teuthology.suite.util.package_version_for_hash')
-    def test_teuthology_branch_nonexistent(
-        self,
-        m_pvfh,
-        m_git_ls_remote,
-        m_smtp,
-    ):
-        m_git_ls_remote.return_value = None
-        config.teuthology_path = None
-        config.results_email = "example@example.com"
-        self.args.dry_run = True
-        self.args.teuthology_branch = 'no_branch'
-        with pytest.raises(ScheduleFailError):
-            self.klass(self.args)
-        m_smtp.assert_not_called()
-
-    @patch('teuthology.suite.run.util.fetch_repos')
-    @patch('teuthology.suite.util.git_ls_remote')
-    @patch('teuthology.suite.run.util.package_version_for_hash')
-    def test_os_type(self, m_pvfh, m_git_ls_remote, m_fetch_repos):
-        m_git_ls_remote.return_value = "sha1"
-        del self.args['distro']
-        run_ = run.Run(self.args)
-        run_.base_args = run_.build_base_args()
-        run_.base_config = run_.build_base_config()
-        configs = [
-            ["desc", [], {"os_type": "debian", "os_version": "8.0"}],
-            ["desc", [], {"os_type": "ubuntu", "os_version": "24.0"}],
-        ]
-        missing, to_schedule = run_.collect_jobs('x86_64', configs, False, False)
-        assert to_schedule[0]['yaml']['os_type'] == "debian"
-        assert to_schedule[0]['yaml']['os_version'] == "8.0"
-        assert to_schedule[1]['yaml']['os_type'] == "ubuntu"
-        assert to_schedule[1]['yaml']['os_version'] == "24.0"
-
-    @patch('teuthology.suite.run.util.fetch_repos')
-    @patch('teuthology.suite.util.git_ls_remote')
-    @patch('teuthology.suite.run.util.package_version_for_hash')
-    def test_sha1(self, m_pvfh, m_git_ls_remote, m_fetch_repos):
-        m_git_ls_remote.return_value = "sha1"
-        del self.args['distro']
-        run_ = run.Run(self.args)
-        run_.base_args = run_.build_base_args()
-        for i in range(5): # mock backtracking
-            run_.config_input['ceph_hash'] = f"boo{i}"
-            run_.config_input['suite_hash'] = f"bar{i}"
-            run_.base_config = run_.build_base_config()
-        configs = [
-            ["desc", [], {"os_type": "debian", "os_version": "8.0", 
-                          "sha1": "old_sha", "suite_sha1": "old_sha",
-                          "overrides": { "workunit": {"sha1": "old_sha"}, "ceph": {"sha1": "old_sha"} }
-                          }],
-        ]
-        missing, to_schedule = run_.collect_jobs('x86_64', configs, False, False)
-        assert to_schedule[0]['yaml']['sha1'] == "boo4"
-        assert to_schedule[0]['yaml']['suite_sha1'] == "bar4"
-        assert to_schedule[0]['yaml']['overrides']['workunit']["sha1"] == "bar4"
-        assert to_schedule[0]['yaml']['overrides']['ceph']["sha1"] == "boo4"
-
-class TestScheduleSuite(object):
-    klass = run.Run
-
-    def setup_method(self):
-        self.args_dict = dict(
-            suite='suite',
-            suite_relpath='',
-            suite_dir='suite_dir',
-            suite_branch='main',
-            suite_repo='ceph',
-            ceph_repo='ceph',
-            ceph_branch='main',
-            ceph_sha1='ceph_sha1',
-            teuthology_branch='main',
-            kernel_branch=None,
-            flavor='flavor',
-            distro='ubuntu',
-            distro_version='14.04',
-            machine_type='machine_type',
-            base_yaml_paths=list(),
-        )
-        self.args = YamlConfig.from_dict(self.args_dict)
-
-    @patch('teuthology.suite.run.Run.schedule_jobs')
-    @patch('teuthology.suite.run.Run.write_rerun_memo')
-    @patch('teuthology.suite.util.get_install_task_flavor')
-    @patch('teuthology.suite.merge.open')
-    @patch('teuthology.suite.run.build_matrix')
-    @patch('teuthology.suite.util.git_ls_remote')
-    @patch('teuthology.suite.util.package_version_for_hash')
-    @patch('teuthology.suite.util.git_validate_sha1')
-    @patch('teuthology.suite.util.get_arch')
-    def test_successful_schedule(
-        self,
-        m_get_arch,
-        m_git_validate_sha1,
-        m_package_version_for_hash,
-        m_git_ls_remote,
-        m_build_matrix,
-        m_open,
-        m_get_install_task_flavor,
-        m_write_rerun_memo,
-        m_schedule_jobs,
-    ):
-        m_get_arch.return_value = 'x86_64'
-        m_git_validate_sha1.return_value = self.args.ceph_sha1
-        m_package_version_for_hash.return_value = 'ceph_version'
-        m_git_ls_remote.return_value = 'suite_hash'
-        build_matrix_desc = 'desc'
-        build_matrix_frags = ['frag1.yml', 'frag2.yml']
-        build_matrix_output = [
-            (build_matrix_desc, build_matrix_frags),
-        ]
-        m_build_matrix.return_value = build_matrix_output
-        frag1_read_output = 'field1: val1'
-        frag2_read_output = 'field2: val2'
-        m_open.side_effect = [
-            StringIO(frag1_read_output),
-            StringIO(frag2_read_output),
-            contextlib.closing(BytesIO())
-        ]
-        m_get_install_task_flavor.return_value = 'default'
-        m_package_version_for_hash.return_value = "v1"
-        # schedule_jobs() is just neutered; check calls below
-
-        self.args.newest = 0
-        self.args.num = 42
-        runobj = self.klass(self.args)
-        runobj.base_args = list()
-        count = runobj.schedule_suite()
-        assert(count == 1)
-        assert runobj.base_config['suite_sha1'] == 'suite_hash'
-        m_package_version_for_hash.assert_has_calls(
-            [call('ceph_sha1', 'default', 'ubuntu', '14.04', 'machine_type')],
-        )
-        y = {
-          'field1': 'val1',
-          'field2': 'val2'
-        }
-        teuthology_keys = [
-          'branch',
-          'machine_type',
-          'name',
-          'os_type',
-          'os_version',
-          'overrides',
-          'priority',
-          'repo',
-          'seed',
-          'sha1',
-          'sleep_before_teardown',
-          'suite',
-          'suite_branch',
-          'suite_relpath',
-          'suite_repo',
-          'suite_sha1',
-          'tasks',
-          'teuthology_branch',
-          'teuthology_sha1',
-          'timestamp',
-          'user',
-          'teuthology',
-          'flavor',
-        ]
-        for t in teuthology_keys:
-            y[t] = ANY
-        expected_job = dict(
-            yaml=y,
-            sha1='ceph_sha1',
-            args=[
-                '--num',
-                '42',
-                '--description',
-                os.path.join(self.args.suite, build_matrix_desc),
-                '--',
-                '-'
-            ],
-            stdin=ANY,
-            desc=os.path.join(self.args.suite, build_matrix_desc),
-        )
-
-        m_schedule_jobs.assert_has_calls(
-            [call([], [expected_job], runobj.name)],
-        )
-        args = m_schedule_jobs.call_args.args
-        log.debug("args =\n%s", args)
-        jobargs  = args[1][0]
-        stdin_yaml = yaml.safe_load(jobargs['stdin'])
-        for k in y:
-            assert y[k] == stdin_yaml[k]
-        for k in teuthology_keys:
-            assert k in stdin_yaml
-        m_write_rerun_memo.assert_called_once_with()
-
-    @patch('teuthology.suite.util.find_git_parents')
-    @patch('teuthology.suite.run.Run.schedule_jobs')
-    @patch('teuthology.suite.util.get_install_task_flavor')
-    @patch('teuthology.suite.run.config_merge')
-    @patch('teuthology.suite.run.build_matrix')
-    @patch('teuthology.suite.util.git_ls_remote')
-    @patch('teuthology.suite.util.package_version_for_hash')
-    @patch('teuthology.suite.util.git_validate_sha1')
-    @patch('teuthology.suite.util.get_arch')
-    def test_newest_failure(
-        self,
-        m_get_arch,
-        m_git_validate_sha1,
-        m_package_version_for_hash,
-        m_git_ls_remote,
-        m_build_matrix,
-        m_config_merge,
-        m_get_install_task_flavor,
-        m_schedule_jobs,
-        m_find_git_parents,
-    ):
-        m_get_arch.return_value = 'x86_64'
-        m_git_validate_sha1.return_value = self.args.ceph_sha1
-        m_package_version_for_hash.return_value = None
-        m_git_ls_remote.return_value = 'suite_hash'
-        build_matrix_desc = 'desc'
-        build_matrix_frags = ['frag.yml']
-        build_matrix_output = [
-            (build_matrix_desc, build_matrix_frags),
-        ]
-        m_build_matrix.return_value = build_matrix_output
-        m_config_merge.return_value = [(a, b, {}) for a, b in build_matrix_output]
-        m_get_install_task_flavor.return_value = 'default'
-
-        m_find_git_parents.side_effect = lambda proj, sha1, count: [f"{sha1}_{i}" for i in range(11)]
-
-        self.args.newest = 10
-        runobj = self.klass(self.args)
-        runobj.base_args = list()
-        with pytest.raises(ScheduleFailError) as exc:
-            runobj.schedule_suite()
-        assert 'Exceeded 10 backtracks' in str(exc.value)
-        m_find_git_parents.assert_has_calls(
-            [call('ceph', 'ceph_sha1', 10)]
-        )
-
-    @patch('teuthology.suite.util.find_git_parents')
-    @patch('teuthology.suite.run.Run.schedule_jobs')
-    @patch('teuthology.suite.run.Run.write_rerun_memo')
-    @patch('teuthology.suite.util.get_install_task_flavor')
-    @patch('teuthology.suite.run.config_merge')
-    @patch('teuthology.suite.run.build_matrix')
-    @patch('teuthology.suite.util.git_ls_remote')
-    @patch('teuthology.suite.util.package_version_for_hash')
-    @patch('teuthology.suite.util.git_validate_sha1')
-    @patch('teuthology.suite.util.get_arch')
-    def test_newest_success_same_branch_same_repo(
-        self,
-        m_get_arch,
-        m_git_validate_sha1,
-        m_package_version_for_hash,
-        m_git_ls_remote,
-        m_build_matrix,
-        m_config_merge,
-        m_get_install_task_flavor,
-        m_write_rerun_memo,
-        m_schedule_jobs,
-        m_find_git_parents,
-    ):
-        """
-        Test that we can successfully schedule a job with newest
-        backtracking when the ceph and suite branches are the same
-        and the ceph_sha1 is not supplied. We should expect that the
-        ceph_hash and suite_hash will be updated to the working sha1
-        """
-        m_get_arch.return_value = 'x86_64'
-        # rig has_packages_for_distro to fail this many times, so
-        # everything will run NUM_FAILS+1 times
-        NUM_FAILS = 5
-        # Here we just assume that even fi ceph_sha1 is not supplied,
-        # in git_valid_sha1, util.git_ls_remote will give us ceph_sha1
-        m_git_validate_sha1.return_value = self.args.ceph_sha1
-        # Here we know that in create_initial_config, we call
-        # git_ls_remote 3 times, choose_ceph_hash, choose_suite_hash,
-        # and choose_teuthology_branch
-        sha1_side_effect = [
-            self.args.ceph_sha1,  # ceph_sha1
-            'suite_sha1',        # suite_sha1
-            'teuthology_sha1',   # teuthology_sha1
-        ]
-        m_git_ls_remote.side_effect = sha1_side_effect
-        build_matrix_desc = 'desc'
-        build_matrix_frags = ['frag.yml']
-        build_matrix_output = [
-            (build_matrix_desc, build_matrix_frags),
-        ]
-        m_build_matrix.return_value = build_matrix_output
-        m_config_merge.return_value = [(a, b, {}) for a, b in build_matrix_output]
-        m_get_install_task_flavor.return_value = 'default'
-
-        # Generate backtracked parent sha1s
-        parent_sha1s = [f"ceph_sha1_{i}" for i in range(NUM_FAILS)]
-        assert len(parent_sha1s)
-        # Last sha1 will be the one that works!
-        working_sha1 = parent_sha1s[-1]
-
-        # NUM_FAILS attempts, then success on the last parent sha1
-        m_package_version_for_hash.side_effect = \
-            [None for i in range(NUM_FAILS)] + ["ceph_version"]
-
-        m_find_git_parents.return_value = parent_sha1s
-
-        self.args.newest = 10
-        runobj = self.klass(self.args)
-        runobj.base_args = list()
-
-        # Call schedule_suite()
-        count = runobj.schedule_suite()
-        # Epect only 1 job to be scheduled
-        assert count == 1
-        # Expect that we called package_version_for_hash NUM_FAILS times + 1 for the working sha1
-        m_package_version_for_hash.assert_has_calls(
-            [call(self.args.ceph_sha1, 'default', 'ubuntu', '14.04', 'machine_type')] +
-            [call(f"ceph_sha1_{i}", 'default', 'ubuntu', '14.04', 'machine_type')
-             for i in range(0, NUM_FAILS)]
-        )
-        # (ceph, base_config.sha1, newest) called once to get grab the backtrace
-        m_find_git_parents.assert_called_once_with('ceph', 'ceph_sha1', 10)
-
-        # Verify that base_config was updated with the working SHA1
-        assert runobj.base_config.sha1 == working_sha1
-
-        # Verify that config_input's ceph_hash and suite_hash was updated
-        assert runobj.config_input['ceph_hash'] == working_sha1
-        assert runobj.config_input['suite_hash'] == working_sha1
-
-        # Verify that config_input's ceph_hash and suite_hash are not the same as the original sha1s
-        assert runobj.config_input['ceph_hash'] != sha1_side_effect[0]  # ceph_sha1
-        assert runobj.config_input['suite_hash'] != sha1_side_effect[1]  # suite_sha1
-
-        # Verify the sha1 in scheduled jobs
-        args = m_schedule_jobs.call_args.args
-        scheduled_jobs = args[1]
-
-        # Check each job has the correct SHA1
-        for job in scheduled_jobs:
-            assert job['sha1'] == working_sha1
-
-            # Parse YAML from stdin to check for sha1 and suite_hash
-            if 'stdin' in job:
-                job_yaml = yaml.safe_load(job['stdin'])
-                assert job_yaml.get('sha1') == working_sha1
-                assert job_yaml.get('suite_sha1') == working_sha1
-
-    @patch('teuthology.suite.util.find_git_parents')
-    @patch('teuthology.suite.run.Run.schedule_jobs')
-    @patch('teuthology.suite.run.Run.write_rerun_memo')
-    @patch('teuthology.suite.util.get_install_task_flavor')
-    @patch('teuthology.suite.run.config_merge')
-    @patch('teuthology.suite.run.build_matrix')
-    @patch('teuthology.suite.util.git_ls_remote')
-    @patch('teuthology.suite.util.package_version_for_hash')
-    @patch('teuthology.suite.util.git_validate_sha1')
-    @patch('teuthology.suite.util.get_arch')
-    def test_newest_success_diff_branch_diff_repo(
-        self,
-        m_get_arch,
-        m_git_validate_sha1,
-        m_package_version_for_hash,
-        m_git_ls_remote,
-        m_build_matrix,
-        m_config_merge,
-        m_get_install_task_flavor,
-        m_write_rerun_memo,
-        m_schedule_jobs,
-        m_find_git_parents,
-    ):
-        """
-        Test that we can successfully schedule a job with newest
-        backtracking when the ceph and suite branches are different
-        and the ceph_sha1 is not supplied. We should expect that the
-        ceph_hash will be updated to the working sha1,
-        but the suite_hash will remain the original suite_sha1.
-        """
-        m_get_arch.return_value = 'x86_64'
-        # Set different branches
-        self.args.ceph_branch = 'ceph_different_branch'
-        self.args.suite_branch = 'suite_different_branch'
-
-        # rig has_packages_for_distro to fail this many times, so
-        # everything will run NUM_FAILS+1 times
-        NUM_FAILS = 5
-        # Here we just assume that even fi ceph_sha1 is not supplied,
-        # in git_valid_sha1, util.git_ls_remote will give us ceph_sha1
-        m_git_validate_sha1.return_value = self.args.ceph_sha1
-        # Here we know that in create_initial_config, we call
-        # git_ls_remote 3 times, choose_ceph_hash, choose_suite_hash,
-        # and choose_teuthology_branch
-        sha1_side_effect = [
-            self.args.ceph_sha1,  # ceph_sha1
-            'suite_sha1',        # suite_sha1
-            'teuthology_sha1',   # teuthology_sha1
-        ]
-        m_git_ls_remote.side_effect = sha1_side_effect
-        build_matrix_desc = 'desc'
-        build_matrix_frags = ['frag.yml']
-        build_matrix_output = [
-            (build_matrix_desc, build_matrix_frags),
-        ]
-        m_build_matrix.return_value = build_matrix_output
-        m_config_merge.return_value = [(a, b, {}) for a, b in build_matrix_output]
-        m_get_install_task_flavor.return_value = 'default'
-
-        # Generate backtracked parent sha1s
-        parent_sha1s = [f"ceph_sha1_{i}" for i in range(NUM_FAILS)]
-        assert len(parent_sha1s)
-        # Last sha1 will be the one that works!
-        working_sha1 = parent_sha1s[-1]
-
-        # NUM_FAILS attempts, then success on the last parent sha1
-        m_package_version_for_hash.side_effect = \
-            [None for i in range(NUM_FAILS)] + ["ceph_version"]
-
-        m_find_git_parents.return_value = parent_sha1s
-
-        self.args.newest = 10
-        runobj = self.klass(self.args)
-        runobj.base_args = list()
-
-        # Call schedule_suite()
-        count = runobj.schedule_suite()
-        # Epect only 1 job to be scheduled
-        assert count == 1
-        # Expect that we called package_version_for_hash NUM_FAILS times + 1 for the working sha1
-        m_package_version_for_hash.assert_has_calls(
-            [call(self.args.ceph_sha1, 'default', 'ubuntu', '14.04', 'machine_type')] +
-            [call(f"ceph_sha1_{i}", 'default', 'ubuntu', '14.04', 'machine_type')
-             for i in range(0, NUM_FAILS)]
-        )
-        # (ceph, base_config.sha1, newest) called once to get grab the backtrace
-        m_find_git_parents.assert_called_once_with('ceph', 'ceph_sha1', 10)
-
-        # Verify that base_config was updated with the working SHA1
-        assert runobj.base_config.sha1 == working_sha1
-
-        # Verify that config_input's ceph_hash was updated,
-        # but suite_hash is still the original suite_sha1
-        assert runobj.config_input['ceph_hash'] == working_sha1
-        assert runobj.config_input['suite_hash'] != working_sha1
-
-        # Verify that config_input's ceph_hash is not the same as the original sha1s
-        # but suite_hash is still the original suite_sha1
-        assert runobj.config_input['ceph_hash'] != sha1_side_effect[0]  # ceph_sha1
-        assert runobj.config_input['suite_hash'] == sha1_side_effect[1]  # suite_sha1
-
-        # Verify the sha1 in scheduled jobs
-        args = m_schedule_jobs.call_args.args
-        scheduled_jobs = args[1]
-
-        # Check each job has the correct SHA1
-        for job in scheduled_jobs:
-            assert job['sha1'] == working_sha1
-
-            # Parse YAML from stdin to check for sha1 and suite_hash
-            if 'stdin' in job:
-                job_yaml = yaml.safe_load(job['stdin'])
-                assert job_yaml.get('sha1') == working_sha1
-                assert job_yaml.get('suite_sha1') == sha1_side_effect[1]
diff --git a/teuthology/suite/test/test_util.py b/teuthology/suite/test/test_util.py
deleted file mode 100644 (file)
index daa5830..0000000
+++ /dev/null
@@ -1,267 +0,0 @@
-import os
-import pytest
-import tempfile
-
-from mock import Mock, patch
-
-from teuthology.config import config
-from teuthology.orchestra.opsys import OS
-from teuthology.suite import util
-from teuthology.exceptions import BranchNotFoundError, ScheduleFailError
-
-
-REPO_PROJECTS_AND_URLS = [
-    'ceph',
-    'https://github.com/not_ceph/ceph.git',
-]
-
-
-@pytest.mark.parametrize('project_or_url', REPO_PROJECTS_AND_URLS)
-@patch('subprocess.check_output')
-def test_git_branch_exists(m_check_output, project_or_url):
-    m_check_output.return_value = ''
-    assert False == util.git_branch_exists(
-        project_or_url, 'nobranchnowaycanthappen')
-    m_check_output.return_value = b'HHH branch'
-    assert True == util.git_branch_exists(project_or_url, 'main')
-
-
-@pytest.fixture
-def git_repository(request):
-    d = tempfile.mkdtemp()
-    os.system("""
-    cd {d}
-    git init
-    touch A
-    git config user.email 'you@example.com'
-    git config user.name 'Your Name'
-    git add A
-    git commit -m 'A' A
-    git rev-parse --abbrev-ref main || git checkout -b main
-    """.format(d=d))
-
-    def fin():
-        os.system("rm -fr " + d)
-    request.addfinalizer(fin)
-    return d
-
-
-class TestUtil(object):
-    @patch('teuthology.suite.util.smtplib.SMTP')
-    def test_schedule_fail(self, m_smtp):
-        config.results_email = "example@example.com"
-        with pytest.raises(ScheduleFailError) as exc:
-            util.schedule_fail(message="error msg", dry_run=False)
-        assert str(exc.value) == "Scheduling failed: error msg"
-        m_smtp.assert_called()
-
-    @patch('teuthology.suite.util.smtplib.SMTP')
-    def test_schedule_fail_dryrun(self, m_smtp):
-        config.results_email = "example@example.com"
-        with pytest.raises(ScheduleFailError) as exc:
-            util.schedule_fail(message="error msg", dry_run=True)
-        assert str(exc.value) == "Scheduling failed: error msg"
-        m_smtp.assert_not_called()
-
-    @patch('teuthology.suite.util.fetch_qa_suite')
-    @patch('teuthology.suite.util.smtplib.SMTP')
-    def test_fetch_repo_no_branch(self, m_smtp, m_fetch_qa_suite):
-        m_fetch_qa_suite.side_effect = BranchNotFoundError(
-            "no-branch", "https://github.com/ceph/ceph-ci.git")
-        config.results_email = "example@example.com"
-        with pytest.raises(ScheduleFailError) as exc:
-            util.fetch_repos("no-branch", "test1", dry_run=False)
-        assert str(exc.value) == "Scheduling test1 failed: \
-Branch 'no-branch' not found in repo: https://github.com/ceph/ceph-ci.git!"
-        m_smtp.assert_called()
-
-    @patch('teuthology.suite.util.fetch_qa_suite')
-    @patch('teuthology.suite.util.smtplib.SMTP')
-    def test_fetch_repo_no_branch_dryrun(self, m_smtp, m_fetch_qa_suite):
-        m_fetch_qa_suite.side_effect = BranchNotFoundError(
-            "no-branch", "https://github.com/ceph/ceph-ci.git")
-        config.results_email = "example@example.com"
-        with pytest.raises(ScheduleFailError) as exc:
-            util.fetch_repos("no-branch", "test1", dry_run=True)
-        assert str(exc.value) == "Scheduling test1 failed: \
-Branch 'no-branch' not found in repo: https://github.com/ceph/ceph-ci.git!"
-        m_smtp.assert_not_called()
-
-    @patch('requests.get')
-    def test_get_branch_info(self, m_get):
-        mock_resp = Mock()
-        mock_resp.ok = True
-        mock_resp.json.return_value = "some json"
-        m_get.return_value = mock_resp
-        result = util.get_branch_info("teuthology", "main")
-        m_get.assert_called_with(
-            "https://api.github.com/repos/ceph/teuthology/git/refs/heads/main"
-        )
-        assert result == "some json"
-
-    @patch('teuthology.lock.query')
-    def test_get_arch_fail(self, m_query):
-        m_query.list_locks.return_value = False
-        util.get_arch('magna')
-        m_query.list_locks.assert_called_with(machine_type="magna", count=1, tries=1)
-
-    @patch('teuthology.lock.query')
-    def test_get_arch_success(self, m_query):
-        m_query.list_locks.return_value = [{"arch": "arch"}]
-        result = util.get_arch('magna')
-        m_query.list_locks.assert_called_with(
-            machine_type="magna",
-            count=1, tries=1
-        )
-        assert result == "arch"
-
-    def test_build_git_url_github(self):
-        assert 'project' in util.build_git_url('project')
-        owner = 'OWNER'
-        git_url = util.build_git_url('project', project_owner=owner)
-        assert owner in git_url
-
-    @patch('teuthology.config.TeuthologyConfig.get_ceph_qa_suite_git_url')
-    def test_build_git_url_ceph_qa_suite_custom(
-            self,
-            m_get_ceph_qa_suite_git_url):
-        url = 'http://foo.com/some'
-        m_get_ceph_qa_suite_git_url.return_value = url + '.git'
-        assert url == util.build_git_url('ceph-qa-suite')
-
-    @patch('teuthology.config.TeuthologyConfig.get_ceph_git_url')
-    def test_build_git_url_ceph_custom(self, m_get_ceph_git_url):
-        url = 'http://foo.com/some'
-        m_get_ceph_git_url.return_value = url + '.git'
-        assert url == util.build_git_url('ceph')
-
-    @patch('teuthology.config.TeuthologyConfig.get_ceph_cm_ansible_git_url')
-    def test_build_git_url_ceph_cm_ansible_custom(self, m_get_ceph_cm_ansible_git_url):
-        url = 'http://foo.com/some'
-        m_get_ceph_cm_ansible_git_url.return_value = url + '.git'
-        assert url == util.build_git_url('ceph-cm-ansible')
-
-    @patch('teuthology.config.TeuthologyConfig.get_ceph_git_url')
-    def test_git_ls_remote(self, m_get_ceph_git_url, git_repository):
-        m_get_ceph_git_url.return_value = git_repository
-        assert util.git_ls_remote('ceph', 'nobranch') is None
-        assert util.git_ls_remote('ceph', 'main') is not None
-
-    @patch('teuthology.suite.util.requests.get')
-    def test_find_git_parents(self, m_requests_get):
-        history_resp = Mock(ok=True)
-        history_resp.json.return_value = {'sha1s': ['sha1', 'sha1_p']}
-        m_requests_get.return_value = history_resp
-        parent_sha1s = util.find_git_parents('ceph', 'sha1')
-        assert m_requests_get.call_count == 1
-        assert parent_sha1s == ['sha1_p']
-
-
-class TestFlavor(object):
-
-    def test_get_install_task_flavor_bare(self):
-        config = dict(
-            tasks=[
-                dict(
-                    install=dict(),
-                ),
-            ],
-        )
-        assert util.get_install_task_flavor(config) == 'default'
-
-    def test_get_install_task_flavor_simple(self):
-        config = dict(
-            tasks=[
-                dict(
-                    install=dict(
-                        flavor='notcmalloc',
-                    ),
-                ),
-            ],
-        )
-        assert util.get_install_task_flavor(config) == 'notcmalloc'
-
-    def test_get_install_task_flavor_override_simple(self):
-        config = dict(
-            tasks=[
-                dict(install=dict()),
-            ],
-            overrides=dict(
-                install=dict(
-                    flavor='notcmalloc',
-                ),
-            ),
-        )
-        assert util.get_install_task_flavor(config) == 'notcmalloc'
-
-    def test_get_install_task_flavor_override_project(self):
-        config = dict(
-            tasks=[
-                dict(install=dict()),
-            ],
-            overrides=dict(
-                install=dict(
-                    ceph=dict(
-                        flavor='notcmalloc',
-                    ),
-                ),
-            ),
-        )
-        assert util.get_install_task_flavor(config) == 'notcmalloc'
-
-
-class TestMissingPackages(object):
-    """
-    Tests the functionality that checks to see if a
-    scheduled job will have missing packages in shaman.
-    """
-    @patch("teuthology.packaging.ShamanProject._get_package_version")
-    def test_distro_has_packages(self, m_gpv):
-        m_gpv.return_value = "v1"
-        result = util.package_version_for_hash(
-            "sha1",
-            "basic",
-            "ubuntu",
-            "14.04",
-            "mtype",
-        )
-        assert result
-
-    @patch("teuthology.packaging.ShamanProject._get_package_version")
-    def test_distro_does_not_have_packages(self, m_gpv):
-        m_gpv.return_value = None
-        result = util.package_version_for_hash(
-            "sha1",
-            "basic",
-            "rhel",
-            "7.0",
-            "mtype",
-        )
-        assert not result
-
-
-class TestDistroDefaults(object):
-    def test_distro_defaults_plana(self):
-        expected = ('x86_64', 'ubuntu/22.04',
-                    OS(name='ubuntu', version='22.04', codename='jammy'))
-        assert util.get_distro_defaults('ubuntu', 'plana') == expected
-
-    def test_distro_defaults_debian(self):
-        expected = ('x86_64', 'debian/8.0',
-                    OS(name='debian', version='8.0', codename='jessie'))
-        assert util.get_distro_defaults('debian', 'magna') == expected
-
-    def test_distro_defaults_centos(self):
-        expected = ('x86_64', 'centos/9',
-                    OS(name='centos', version='9.stream', codename='stream'))
-        assert util.get_distro_defaults('centos', 'magna') == expected
-
-    def test_distro_defaults_fedora(self):
-        expected = ('x86_64', 'fedora/25',
-                    OS(name='fedora', version='25', codename='25'))
-        assert util.get_distro_defaults('fedora', 'magna') == expected
-
-    def test_distro_defaults_default(self):
-        expected = ('x86_64', 'centos/9',
-                    OS(name='centos', version='9.stream', codename='stream'))
-        assert util.get_distro_defaults('rhel', 'magna') == expected
index b558341cf6949e7c385f515c2f33363734e1c4c7..0773a266de8ed72b5cdd2f0e06d04439f8056b3c 100644 (file)
@@ -137,7 +137,7 @@ class Tests(Task):
         super().end()
 
     def run_cli(self):
-        pytest_args = self.base_args + ['./teuthology/test', './scripts']
+        pytest_args = self.base_args + ['./tests/']
         if len(self.cluster.remotes):
             pytest_args.append('./teuthology/task/tests')
         self.log.info(f"pytest args: {pytest_args}")
@@ -153,7 +153,7 @@ class Tests(Task):
         return status, []
 
     def run_py(self):
-        pytest_args = self.base_args + ['--pyargs', 'teuthology', 'scripts']
+        pytest_args = self.base_args + ['--pyargs', './tests/']
         if len(self.cluster.remotes):
             pytest_args.append(__name__)
         self.log.info(f"pytest args: {pytest_args}")
diff --git a/teuthology/test/__init__.py b/teuthology/test/__init__.py
deleted file mode 100644 (file)
index 1eb9e81..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-import os
-import pytest
-import sys
-
-skipif_teuthology_process = pytest.mark.skipif(
-    os.path.basename(sys.argv[0]) == "teuthology",
-    reason="Skipped because this test cannot pass when run in a teuthology " \
-        "process (as opposed to py.test)"
-)
\ No newline at end of file
diff --git a/teuthology/test/fake_archive.py b/teuthology/test/fake_archive.py
deleted file mode 100644 (file)
index 76a944f..0000000
+++ /dev/null
@@ -1,107 +0,0 @@
-import os
-import shutil
-import yaml
-import random
-
-
-class FakeArchive(object):
-    def __init__(self, archive_base="./test_archive"):
-        self.archive_base = archive_base
-
-    def get_random_metadata(self, run_name, job_id=None, hung=False):
-        """
-        Generate a random info dict for a fake job. If 'hung' is not True, also
-        generate a summary dict.
-
-        :param run_name:   Run name e.g. 'test_foo'
-        :param job_id: Job ID e.g. '12345'
-        :param hung:   Simulate a hung job e.g. don't return a summary.yaml
-        :return:       A dict with keys 'job_id', 'info' and possibly
-                       'summary', with corresponding values
-        """
-        rand = random.Random()
-
-        description = 'description for job with id %s' % job_id
-        owner = 'job@owner'
-        duration = rand.randint(1, 36000)
-        pid = rand.randint(1000, 99999)
-        job_id = rand.randint(1, 99999)
-
-        info = {
-            'description': description,
-            'job_id': job_id,
-            'run_name': run_name,
-            'owner': owner,
-            'pid': pid,
-        }
-
-        metadata = {
-            'info': info,
-            'job_id': job_id,
-        }
-
-        if not hung:
-            success = True if rand.randint(0, 1) != 1 else False
-
-            summary = {
-                'description': description,
-                'duration': duration,
-                'owner': owner,
-                'success': success,
-            }
-
-            if not success:
-                summary['failure_reason'] = 'Failure reason!'
-            metadata['summary'] = summary
-
-        return metadata
-
-    def setup(self):
-        if os.path.exists(self.archive_base):
-            shutil.rmtree(self.archive_base)
-        os.mkdir(self.archive_base)
-
-    def teardown(self):
-        shutil.rmtree(self.archive_base)
-
-    def populate_archive(self, run_name, jobs):
-        run_archive_dir = os.path.join(self.archive_base, run_name)
-        os.mkdir(run_archive_dir)
-        for job in jobs:
-            archive_dir = os.path.join(run_archive_dir, str(job['job_id']))
-            os.mkdir(archive_dir)
-
-            with open(os.path.join(archive_dir, 'info.yaml'), 'w') as yfile:
-                yaml.safe_dump(job['info'], yfile)
-
-            if 'summary' in job:
-                summary_path = os.path.join(archive_dir, 'summary.yaml')
-                with open(summary_path, 'w') as yfile:
-                    yaml.safe_dump(job['summary'], yfile)
-
-    def create_fake_run(self, run_name, job_count, yaml_path, num_hung=0):
-        """
-        Creates a fake run using run_name. Uses the YAML specified for each
-        job's config.yaml
-
-        Returns a list of job_ids
-        """
-        assert os.path.exists(yaml_path)
-        assert job_count > 0
-        jobs = []
-        made_hung = 0
-        for i in range(job_count):
-            if made_hung < num_hung:
-                jobs.append(self.get_random_metadata(run_name, hung=True))
-                made_hung += 1
-            else:
-                jobs.append(self.get_random_metadata(run_name, hung=False))
-            #job_config = yaml.safe_load(yaml_path)
-        self.populate_archive(run_name, jobs)
-        for job in jobs:
-            job_id = job['job_id']
-            job_yaml_path = os.path.join(self.archive_base, run_name,
-                                         str(job_id), 'config.yaml')
-            shutil.copyfile(yaml_path, job_yaml_path)
-        return jobs
-
diff --git a/teuthology/test/fake_fs.py b/teuthology/test/fake_fs.py
deleted file mode 100644 (file)
index c5cb6e4..0000000
+++ /dev/null
@@ -1,90 +0,0 @@
-from io import BytesIO
-from contextlib import closing
-
-
-try:
-     FileNotFoundError, NotADirectoryError
-except NameError:
-     FileNotFoundError = NotADirectoryError = OSError
-
-
-def make_fake_fstools(fake_filesystem):
-    """
-    Build fake versions of os.listdir(), os.isfile(), etc. for use in
-    unit tests
-
-    An example fake_filesystem value:
-        >>> fake_fs = {\
-            'a_directory': {\
-                'another_directory': {\
-                    'empty_file': None,\
-                    'another_empty_file': None,\
-                },\
-                'random_file': None,\
-                'yet_another_directory': {\
-                    'empty_directory': {},\
-                },\
-                'file_with_contents': 'data',\
-            },\
-        }
-        >>> fake_listdir, fake_isfile, _, _ = \
-                make_fake_fstools(fake_fs)
-        >>> fake_listdir('a_directory/yet_another_directory')
-        ['empty_directory']
-        >>> fake_isfile('a_directory/yet_another_directory')
-        False
-
-    :param fake_filesystem: A dict representing a filesystem
-    """
-    assert isinstance(fake_filesystem, dict)
-
-    def fake_listdir(path, fsdict=False):
-        if fsdict is False:
-            fsdict = fake_filesystem
-
-        remainder = path.strip('/') + '/'
-        subdict = fsdict
-        while '/' in remainder:
-            next_dir, remainder = remainder.split('/', 1)
-            if next_dir not in subdict:
-                raise FileNotFoundError(
-                    '[Errno 2] No such file or directory: %s' % next_dir)
-            subdict = subdict.get(next_dir)
-            if not isinstance(subdict, dict):
-                raise NotADirectoryError('[Errno 20] Not a directory: %s' % next_dir)
-            if subdict and not remainder:
-                return list(subdict)
-        return []
-
-    def fake_isfile(path, fsdict=False):
-        if fsdict is False:
-            fsdict = fake_filesystem
-
-        components = path.strip('/').split('/')
-        subdict = fsdict
-        for component in components:
-            if component not in subdict:
-                raise FileNotFoundError('[Errno 2] No such file or directory: %s' % component)
-            subdict = subdict.get(component)
-        return subdict is None or isinstance(subdict, str)
-
-    def fake_isdir(path, fsdict=False):
-        return not fake_isfile(path)
-
-    def fake_exists(path, fsdict=False):
-        return fake_isfile(path, fsdict) or fake_isdir(path, fsdict)
-
-    def fake_open(path, mode=None, buffering=None):
-        components = path.strip('/').split('/')
-        subdict = fake_filesystem
-        for component in components:
-            if component not in subdict:
-                raise FileNotFoundError('[Errno 2] No such file or directory: %s' % component)
-            subdict = subdict.get(component)
-        if isinstance(subdict, dict):
-            raise IOError('[Errno 21] Is a directory: %s' % path)
-        elif subdict is None:
-            return closing(BytesIO(b''))
-        return closing(BytesIO(subdict.encode()))
-
-    return fake_exists, fake_listdir, fake_isfile, fake_isdir, fake_open
diff --git a/teuthology/test/integration/__init__.py b/teuthology/test/integration/__init__.py
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/teuthology/test/integration/test_suite.py b/teuthology/test/integration/test_suite.py
deleted file mode 100644 (file)
index a618c4c..0000000
+++ /dev/null
@@ -1,86 +0,0 @@
-import os
-import requests
-from pytest import raises, skip
-
-from teuthology.config import config
-from teuthology import suite
-
-
-class TestSuiteOnline(object):
-    def setup_method(self):
-        if 'TEST_ONLINE' not in os.environ:
-            skip("To run these sets, set the environment variable TEST_ONLINE")
-
-    def test_ceph_hash_simple(self):
-        resp = requests.get(
-            'https://api.github.com/repos/ceph/ceph/git/refs/heads/main')
-        ref_hash = resp.json()['object']['sha']
-        assert suite.get_hash('ceph') == ref_hash
-
-    def test_kernel_hash_saya(self):
-        # We don't currently have these packages.
-        assert suite.get_hash('kernel', 'main', 'default', 'saya') is None
-
-    def test_all_main_branches(self):
-        # Don't attempt to send email
-        config.results_email = None
-        job_config = suite.create_initial_config('suite', 'main',
-                                                 'main', 'main', 'testing',
-                                                 'default', 'centos', 'plana')
-        assert ((job_config.branch, job_config.teuthology_branch,
-                 job_config.suite_branch) == ('main', 'main', 'main'))
-
-    def test_config_bogus_kernel_branch(self):
-        # Don't attempt to send email
-        config.results_email = None
-        with raises(suite.ScheduleFailError):
-            suite.create_initial_config('s', None, 'main', 't',
-                                        'bogus_kernel_branch', 'f', 'd', 'm')
-
-    def test_config_bogus_flavor(self):
-        # Don't attempt to send email
-        config.results_email = None
-        with raises(suite.ScheduleFailError):
-            suite.create_initial_config('s', None, 'main', 't', 'k',
-                                        'bogus_flavor', 'd', 'm')
-
-    def test_config_bogus_ceph_branch(self):
-        # Don't attempt to send email
-        config.results_email = None
-        with raises(suite.ScheduleFailError):
-            suite.create_initial_config('s', None, 'bogus_ceph_branch', 't',
-                                        'k', 'f', 'd', 'm')
-
-    def test_config_bogus_suite_branch(self):
-        # Don't attempt to send email
-        config.results_email = None
-        with raises(suite.ScheduleFailError):
-            suite.create_initial_config('s', 'bogus_suite_branch', 'main',
-                                        't', 'k', 'f', 'd', 'm')
-
-    def test_config_bogus_teuthology_branch(self):
-        # Don't attempt to send email
-        config.results_email = None
-        with raises(suite.ScheduleFailError):
-            suite.create_initial_config('s', None, 'main',
-                                        'bogus_teuth_branch', 'k', 'f', 'd',
-                                        'm')
-
-    def test_config_substitution(self):
-        # Don't attempt to send email
-        config.results_email = None
-        job_config = suite.create_initial_config('MY_SUITE', 'main',
-                                                 'main', 'main', 'testing',
-                                                 'default', 'centos', 'plana')
-        assert job_config['suite'] == 'MY_SUITE'
-
-    def test_config_kernel_section(self):
-        # Don't attempt to send email
-        config.results_email = None
-        job_config = suite.create_initial_config('MY_SUITE', 'main',
-                                                 'main', 'main', 'testing',
-                                                 'default', 'centos', 'plana')
-        assert job_config['kernel']['kdb'] is True
-
-
-# maybe use notario for the above?
diff --git a/teuthology/test/task/__init__.py b/teuthology/test/task/__init__.py
deleted file mode 100644 (file)
index d000cff..0000000
+++ /dev/null
@@ -1,205 +0,0 @@
-from mock import patch, DEFAULT
-from pytest import raises
-
-from teuthology.config import FakeNamespace
-from teuthology.orchestra.cluster import Cluster
-from teuthology.orchestra.remote import Remote
-from teuthology.task import Task
-
-
-class TestTask(object):
-    klass = Task
-    task_name = 'task'
-
-    def setup_method(self):
-        self.ctx = FakeNamespace()
-        self.ctx.config = dict()
-        self.task_config = dict()
-
-    def test_overrides(self):
-        self.ctx.config['overrides'] = dict()
-        self.ctx.config['overrides'][self.task_name] = dict(
-            key_1='overridden',
-        )
-        self.task_config.update(dict(
-            key_1='default',
-            key_2='default',
-        ))
-        with patch.multiple(
-            self.klass,
-            begin=DEFAULT,
-            end=DEFAULT,
-        ):
-            with self.klass(self.ctx, self.task_config) as task:
-                assert task.config['key_1'] == 'overridden'
-                assert task.config['key_2'] == 'default'
-
-    def test_hosts_no_filter(self):
-        self.ctx.cluster = Cluster()
-        self.ctx.cluster.add(Remote('user@remote1'), ['role1'])
-        self.ctx.cluster.add(Remote('user@remote2'), ['role2'])
-        with patch.multiple(
-            self.klass,
-            begin=DEFAULT,
-            end=DEFAULT,
-        ):
-            with self.klass(self.ctx, self.task_config) as task:
-                task_hosts = list(task.cluster.remotes)
-                assert len(task_hosts) == 2
-                assert sorted(host.shortname for host in task_hosts) == \
-                    ['remote1', 'remote2']
-
-    def test_hosts_no_results(self):
-        self.ctx.cluster = Cluster()
-        self.ctx.cluster.add(Remote('user@remote1'), ['role1'])
-        self.task_config.update(dict(
-            hosts=['role2'],
-        ))
-        with patch.multiple(
-            self.klass,
-            begin=DEFAULT,
-            end=DEFAULT,
-        ):
-            with raises(RuntimeError):
-                with self.klass(self.ctx, self.task_config):
-                    pass
-
-    def test_hosts_one_role(self):
-        self.ctx.cluster = Cluster()
-        self.ctx.cluster.add(Remote('user@remote1'), ['role1'])
-        self.ctx.cluster.add(Remote('user@remote2'), ['role2'])
-        self.task_config.update(dict(
-            hosts=['role1'],
-        ))
-        with patch.multiple(
-            self.klass,
-            begin=DEFAULT,
-            end=DEFAULT,
-        ):
-            with self.klass(self.ctx, self.task_config) as task:
-                task_hosts = list(task.cluster.remotes)
-                assert len(task_hosts) == 1
-                assert task_hosts[0].shortname == 'remote1'
-
-    def test_hosts_two_roles(self):
-        self.ctx.cluster = Cluster()
-        self.ctx.cluster.add(Remote('user@remote1'), ['role1'])
-        self.ctx.cluster.add(Remote('user@remote2'), ['role2'])
-        self.ctx.cluster.add(Remote('user@remote3'), ['role3'])
-        self.task_config.update(dict(
-            hosts=['role1', 'role3'],
-        ))
-        with patch.multiple(
-            self.klass,
-            begin=DEFAULT,
-            end=DEFAULT,
-        ):
-            with self.klass(self.ctx, self.task_config) as task:
-                task_hosts = list(task.cluster.remotes)
-                assert len(task_hosts) == 2
-                hostnames = [host.shortname for host in task_hosts]
-                assert sorted(hostnames) == ['remote1', 'remote3']
-
-    def test_hosts_two_hostnames(self):
-        self.ctx.cluster = Cluster()
-        self.ctx.cluster.add(Remote('user@remote1.example.com'), ['role1'])
-        self.ctx.cluster.add(Remote('user@remote2.example.com'), ['role2'])
-        self.ctx.cluster.add(Remote('user@remote3.example.com'), ['role3'])
-        self.task_config.update(dict(
-            hosts=['remote1', 'remote2.example.com'],
-        ))
-        with patch.multiple(
-            self.klass,
-            begin=DEFAULT,
-            end=DEFAULT,
-        ):
-            with self.klass(self.ctx, self.task_config) as task:
-                task_hosts = list(task.cluster.remotes)
-                assert len(task_hosts) == 2
-                hostnames = [host.hostname for host in task_hosts]
-                assert sorted(hostnames) == ['remote1.example.com',
-                                             'remote2.example.com']
-
-    def test_hosts_one_role_one_hostname(self):
-        self.ctx.cluster = Cluster()
-        self.ctx.cluster.add(Remote('user@remote1.example.com'), ['role1'])
-        self.ctx.cluster.add(Remote('user@remote2.example.com'), ['role2'])
-        self.ctx.cluster.add(Remote('user@remote3.example.com'), ['role3'])
-        self.task_config.update(dict(
-            hosts=['role1', 'remote2.example.com'],
-        ))
-        with patch.multiple(
-            self.klass,
-            begin=DEFAULT,
-            end=DEFAULT,
-        ):
-            with self.klass(self.ctx, self.task_config) as task:
-                task_hosts = list(task.cluster.remotes)
-                assert len(task_hosts) == 2
-                hostnames = [host.hostname for host in task_hosts]
-                assert sorted(hostnames) == ['remote1.example.com',
-                                             'remote2.example.com']
-
-    def test_setup_called(self):
-        with patch.multiple(
-            self.klass,
-            setup=DEFAULT,
-            begin=DEFAULT,
-            end=DEFAULT,
-            teardown=DEFAULT,
-        ):
-            with self.klass(self.ctx, self.task_config) as task:
-                task.setup.assert_called_once_with()
-
-    def test_begin_called(self):
-        with patch.multiple(
-            self.klass,
-            setup=DEFAULT,
-            begin=DEFAULT,
-            end=DEFAULT,
-            teardown=DEFAULT,
-        ):
-            with self.klass(self.ctx, self.task_config) as task:
-                task.begin.assert_called_once_with()
-
-    def test_end_called(self):
-        self.task_config.update(dict())
-        with patch.multiple(
-            self.klass,
-            begin=DEFAULT,
-            end=DEFAULT,
-        ):
-            with self.klass(self.ctx, self.task_config) as task:
-                pass
-            task.end.assert_called_once_with()
-
-    def test_teardown_called(self):
-        self.task_config.update(dict())
-        with patch.multiple(
-            self.klass,
-            setup=DEFAULT,
-            begin=DEFAULT,
-            end=DEFAULT,
-            teardown=DEFAULT,
-        ):
-            with self.klass(self.ctx, self.task_config) as task:
-                pass
-            task.teardown.assert_called_once_with()
-
-    def test_skip_teardown(self):
-        self.task_config.update(dict(
-            skip_teardown=True,
-        ))
-
-        def fake_teardown(self):
-            assert False
-
-        with patch.multiple(
-            self.klass,
-            setup=DEFAULT,
-            begin=DEFAULT,
-            end=DEFAULT,
-            teardown=fake_teardown,
-        ):
-            with self.klass(self.ctx, self.task_config):
-                pass
diff --git a/teuthology/test/task/test_ansible.py b/teuthology/test/task/test_ansible.py
deleted file mode 100644 (file)
index 1b7afc0..0000000
+++ /dev/null
@@ -1,659 +0,0 @@
-import json
-import os
-import yaml
-
-from unittest.mock import patch, DEFAULT, Mock, mock_open
-from pytest import raises, mark
-from teuthology.util.compat import PY3
-if PY3:
-    from io import StringIO as StringIO
-else:
-    from io import BytesIO as StringIO
-
-from teuthology.config import config, FakeNamespace
-from teuthology.exceptions import CommandFailedError
-from teuthology.orchestra.cluster import Cluster
-from teuthology.orchestra.remote import Remote
-from teuthology.task import ansible
-from teuthology.task.ansible import Ansible, CephLab, FailureAnalyzer
-
-from teuthology.test.task import TestTask
-
-
-class TestFailureAnalyzer:
-    klass = FailureAnalyzer
-
-    @mark.parametrize(
-        'line,result',
-        [
-            [
-                "W: --force-yes is deprecated, use one of the options starting with --allow instead.",
-                "",
-            ],
-            [
-                "E: Unable to fetch some archives, maybe run apt-get update or try with --fix-missing?",
-                "",
-            ],
-            [
-                "E: Failed to fetch http://security.ubuntu.com/ubuntu/pool/main/a/apache2/apache2-bin_2.4.41-4ubuntu3.14_amd64.deb  Unable to connect to archive.ubuntu.com:http:",
-                "Unable to connect to archive.ubuntu.com:http:"
-            ],
-            [
-                "E: Failed to fetch http://archive.ubuntu.com/ubuntu/pool/main/libb/libb-hooks-op-check-perl/libb-hooks-op-check-perl_0.22-1build2_amd64.deb  Temporary failure resolving 'archive.ubuntu.com'",
-                "Temporary failure resolving 'archive.ubuntu.com'"
-            ],
-            [
-                "Data could not be sent to remote host \"smithi068.front.sepia.ceph.com\".",
-                "Data could not be sent to remote host \"smithi068.front.sepia.ceph.com\"."
-            ],
-            [
-                "Permissions 0644 for '/root/.ssh/id_rsa' are too open.",
-                "Permissions 0644 for '/root/.ssh/id_rsa' are too open."
-            ],
-        ]
-    )
-    def test_lines(self, line, result):
-        obj = self.klass()
-        assert obj.analyze_line(line) == result
-
-
-class TestAnsibleTask(TestTask):
-    klass = Ansible
-    task_name = 'ansible'
-
-    def setup_method(self):
-        self.ctx = FakeNamespace()
-        self.ctx.cluster = Cluster()
-        self.ctx.cluster.add(Remote('user@remote1'), ['role1'])
-        self.ctx.cluster.add(Remote('user@remote2'), ['role2'])
-        self.ctx.config = dict()
-        self.ctx.summary = dict()
-        self.ctx.archive = ""
-        self.task_config = dict(playbook=[])
-        self.start_patchers()
-
-    def start_patchers(self):
-        self.patchers = dict()
-        self.mocks = dict()
-        self.patchers['mkdtemp'] = patch(
-            'teuthology.task.ansible.mkdtemp', return_value='/tmp/'
-        )
-        m_NTF = Mock()
-        m_file = Mock()
-        m_file.name = 'file_name'
-        m_NTF.return_value = m_file
-        self.patchers['NTF'] = patch(
-            'teuthology.task.ansible.NamedTemporaryFile',
-            m_NTF,
-        )
-        self.patchers['file'] = patch(
-            'teuthology.task.ansible.open', create=True)
-        self.patchers['os_mkdir'] = patch(
-            'teuthology.task.ansible.os.mkdir',
-        )
-        self.patchers['os_remove'] = patch(
-            'teuthology.task.ansible.os.remove',
-        )
-        self.patchers['shutil_rmtree'] = patch(
-            'teuthology.task.ansible.shutil.rmtree',
-        )
-        for name in self.patchers.keys():
-            self.start_patcher(name)
-
-    def start_patcher(self, name):
-        if name not in self.mocks.keys():
-            self.mocks[name] = self.patchers[name].start()
-
-    def teardown_method(self, method):
-        self.stop_patchers()
-
-    def stop_patchers(self):
-        for name in list(self.mocks):
-            self.stop_patcher(name)
-
-    def stop_patcher(self, name):
-        self.patchers[name].stop()
-        del self.mocks[name]
-
-    def test_setup(self):
-        self.task_config.update(dict(
-            playbook=[]
-        ))
-
-        def fake_get_playbook(self):
-            self.playbook_file = 'fake'
-
-        with patch.multiple(
-            self.klass,
-            find_repo=DEFAULT,
-            get_playbook=fake_get_playbook,
-            get_inventory=DEFAULT,
-            generate_inventory=DEFAULT,
-            generate_playbook=Mock(side_effect=Exception),
-        ):
-            task = self.klass(self.ctx, self.task_config)
-            task.setup()
-
-    def test_setup_generate_playbook(self):
-        self.task_config.update(dict(
-            playbook=[]
-        ))
-        with patch.multiple(
-            self.klass,
-            find_repo=DEFAULT,
-            get_playbook=DEFAULT,
-            get_inventory=DEFAULT,
-            generate_inventory=DEFAULT,
-            generate_playbook=DEFAULT,
-        ):
-            task = self.klass(self.ctx, self.task_config)
-            task.setup()
-            task.generate_playbook.assert_called_once_with()
-
-    def test_find_repo_path(self):
-        self.task_config.update(dict(
-            repo='~/my/repo',
-        ))
-        task = self.klass(self.ctx, self.task_config)
-        task.find_repo()
-        assert task.repo_path == os.path.expanduser(self.task_config['repo'])
-
-    @patch('teuthology.repo_utils.fetch_repo')
-    def test_find_repo_path_remote(self, m_fetch_repo):
-        self.task_config.update(dict(
-            repo='git://fake_host/repo.git',
-        ))
-        m_fetch_repo.return_value = '/tmp/repo'
-        task = self.klass(self.ctx, self.task_config)
-        task.find_repo()
-        assert task.repo_path == os.path.expanduser('/tmp/repo')
-
-    @patch('teuthology.repo_utils.fetch_repo')
-    def test_find_repo_http(self, m_fetch_repo):
-        self.task_config.update(dict(
-            repo='http://example.com/my/repo',
-        ))
-        task = self.klass(self.ctx, self.task_config)
-        task.find_repo()
-        m_fetch_repo.assert_called_once_with(self.task_config['repo'],
-                                             'main')
-
-    @patch('teuthology.repo_utils.fetch_repo')
-    def test_find_repo_git(self, m_fetch_repo):
-        self.task_config.update(dict(
-            repo='git@example.com/my/repo',
-        ))
-        task = self.klass(self.ctx, self.task_config)
-        task.find_repo()
-        m_fetch_repo.assert_called_once_with(self.task_config['repo'],
-                                             'main')
-
-    def test_playbook_none(self):
-        del self.task_config['playbook']
-        task = self.klass(self.ctx, self.task_config)
-        with raises(KeyError):
-            task.get_playbook()
-
-    def test_playbook_wrong_type(self):
-        self.task_config.update(dict(
-            playbook=dict(),
-        ))
-        task = self.klass(self.ctx, self.task_config)
-        with raises(TypeError):
-            task.get_playbook()
-
-    def test_playbook_list(self):
-        playbook = [
-            dict(
-                roles=['role1'],
-            ),
-        ]
-        self.task_config.update(dict(
-            playbook=playbook,
-        ))
-        task = self.klass(self.ctx, self.task_config)
-        task.get_playbook()
-        assert task.playbook == playbook
-
-    @patch.object(ansible.requests, 'get')
-    def test_playbook_http(self, m_get):
-        m_get.return_value = Mock()
-        m_get.return_value.text = 'fake playbook text'
-        playbook = "http://example.com/my_playbook.yml"
-        self.task_config.update(dict(
-            playbook=playbook,
-        ))
-        task = self.klass(self.ctx, self.task_config)
-        task.get_playbook()
-        m_get.assert_called_once_with(playbook)
-
-    def test_playbook_file(self):
-        fake_playbook = [dict(fake_playbook=True)]
-        fake_playbook_obj = StringIO(yaml.safe_dump(fake_playbook))
-        self.task_config.update(dict(
-            playbook='~/fake/playbook',
-        ))
-        task = self.klass(self.ctx, self.task_config)
-        self.mocks['file'].return_value = fake_playbook_obj
-        task.get_playbook()
-        assert task.playbook == fake_playbook
-
-    def test_playbook_file_missing(self):
-        self.task_config.update(dict(
-            playbook='~/fake/playbook',
-        ))
-        task = self.klass(self.ctx, self.task_config)
-        self.mocks['file'].side_effect = IOError
-        with raises(IOError):
-            task.get_playbook()
-
-    def test_inventory_none(self):
-        self.task_config.update(dict(
-            playbook=[]
-        ))
-        task = self.klass(self.ctx, self.task_config)
-        with patch.object(ansible.os.path, 'exists') as m_exists:
-            m_exists.return_value = False
-            task.get_inventory()
-        assert task.inventory is None
-
-    def test_inventory_path(self):
-        inventory = '/my/inventory'
-        self.task_config.update(dict(
-            playbook=[],
-            inventory=inventory,
-        ))
-        task = self.klass(self.ctx, self.task_config)
-        task.get_inventory()
-        assert task.inventory == inventory
-        assert task.generated_inventory is False
-
-    def test_inventory_etc(self):
-        self.task_config.update(dict(
-            playbook=[]
-        ))
-        task = self.klass(self.ctx, self.task_config)
-        with patch.object(ansible.os.path, 'exists') as m_exists:
-            m_exists.return_value = True
-            task.get_inventory()
-        assert task.inventory == '/etc/ansible/hosts'
-        assert task.generated_inventory is False
-
-    @mark.parametrize(
-        'group_vars',
-        [
-            dict(),
-            dict(all=dict(var0=0, var1=1)),
-            dict(foo=dict(var0=0), bar=dict(var0=1)),
-        ]
-    )
-    def test_generate_inventory(self, group_vars):
-        self.task_config.update(dict(
-            playbook=[]
-        ))
-        if group_vars:
-            self.task_config.update(dict(group_vars=group_vars))
-        task = self.klass(self.ctx, self.task_config)
-        hosts_file_path = '/my/hosts/inventory'
-        hosts_file_obj = StringIO()
-        hosts_file_obj.name = hosts_file_path
-        inventory_dir = os.path.dirname(hosts_file_path)
-        gv_dir = os.path.join(inventory_dir, 'group_vars')
-        self.mocks['mkdtemp'].return_value = inventory_dir
-        m_file = self.mocks['file']
-        fake_files = [hosts_file_obj]
-        # Create StringIO object for each group_vars file
-        if group_vars:
-            fake_files += [StringIO() for i in sorted(group_vars)]
-        m_file.side_effect = fake_files
-        task.generate_inventory()
-        file_calls = m_file.call_args_list
-        # Verify the inventory file was created
-        assert file_calls[0][0][0] == hosts_file_path
-        # Verify each group_vars file was created
-        for gv_name, call_obj in zip(sorted(group_vars), file_calls[1:]):
-            gv_path = call_obj[0][0]
-            assert gv_path == os.path.join(gv_dir, '%s.yml' % gv_name)
-        # Verify the group_vars dir was created
-        if group_vars:
-            mkdir_call = self.mocks['os_mkdir'].call_args_list
-            assert mkdir_call[0][0][0] == gv_dir
-        assert task.generated_inventory is True
-        assert task.inventory == inventory_dir
-        # Verify the content of the inventory *file*
-        hosts_file_obj.seek(0)
-        assert hosts_file_obj.readlines() == [
-            'remote1\n',
-            'remote2\n',
-        ]
-        # Verify the contents of each group_vars file
-        gv_names = sorted(group_vars)
-        for i in range(len(gv_names)):
-            gv_name = gv_names[i]
-            in_val = group_vars[gv_name]
-            gv_stringio = fake_files[1 + i]
-            gv_stringio.seek(0)
-            out_val = yaml.safe_load(gv_stringio)
-            assert in_val == out_val
-
-    def test_generate_playbook(self):
-        playbook = [
-            dict(
-                roles=['role1', 'role2'],
-            ),
-        ]
-        self.task_config.update(dict(
-            playbook=playbook
-        ))
-        task = self.klass(self.ctx, self.task_config)
-        playbook_file_path = '/my/playbook/file'
-        playbook_file_obj = StringIO()
-        playbook_file_obj.name = playbook_file_path
-        with patch.object(ansible, 'NamedTemporaryFile') as m_NTF:
-            m_NTF.return_value = playbook_file_obj
-            task.find_repo()
-            task.get_playbook()
-            task.generate_playbook()
-            m_NTF.assert_called_once_with(
-                prefix="teuth_ansible_playbook_",
-                dir=task.repo_path,
-                delete=False,
-            )
-        assert task.generated_playbook is True
-        assert task.playbook_file == playbook_file_obj
-        playbook_file_obj.seek(0)
-        playbook_result = yaml.safe_load(playbook_file_obj)
-        assert playbook_result == playbook
-
-    def test_execute_playbook(self):
-        playbook = '/my/playbook'
-        self.task_config.update(dict(
-            playbook=playbook
-        ))
-        fake_playbook = [dict(fake_playbook=True)]
-        fake_playbook_obj = StringIO(yaml.safe_dump(fake_playbook))
-        fake_playbook_obj.name = playbook
-        self.mocks['mkdtemp'].return_value = '/inventory/dir'
-
-        task = self.klass(self.ctx, self.task_config)
-        self.mocks['file'].return_value = fake_playbook_obj
-        task.setup()
-        args = task._build_args()
-        logger = StringIO()
-        with patch.object(ansible.pexpect, 'run') as m_run:
-            m_run.return_value = ('', 0)
-            with patch.object(Remote, 'reconnect') as m_reconnect:
-                m_reconnect.return_value = True
-                task.execute_playbook(_logfile=logger)
-            m_run.assert_called_once_with(
-                ' '.join(args),
-                cwd=task.repo_path,
-                logfile=logger,
-                withexitstatus=True,
-                timeout=None,
-            )
-
-    def test_execute_playbook_fail(self):
-        self.task_config.update(dict(
-            playbook=[],
-        ))
-        self.mocks['mkdtemp'].return_value = '/inventory/dir'
-        task = self.klass(self.ctx, self.task_config)
-        task.setup()
-        with patch.object(ansible.pexpect, 'run') as m_run:
-            with patch('teuthology.task.ansible.open', mock_open()):
-                m_run.return_value = ('', 1)
-                with raises(CommandFailedError):
-                    task.execute_playbook()
-                assert task.ctx.summary.get('status') is None
-
-    def test_build_args_no_tags(self):
-        self.task_config.update(dict(
-            playbook=[],
-        ))
-        task = self.klass(self.ctx, self.task_config)
-        task.setup()
-        args = task._build_args()
-        assert '--tags' not in args
-
-    def test_build_args_tags(self):
-        self.task_config.update(dict(
-            playbook=[],
-            tags="user,pubkeys"
-        ))
-        task = self.klass(self.ctx, self.task_config)
-        task.setup()
-        args = task._build_args()
-        assert args.count('--tags') == 1
-        assert args[args.index('--tags') + 1] == 'user,pubkeys'
-
-    def test_build_args_skip_tags(self):
-        self.task_config.update(dict(
-            playbook=[],
-            skip_tags="user,pubkeys"
-        ))
-        task = self.klass(self.ctx, self.task_config)
-        task.setup()
-        args = task._build_args()
-        assert args.count('--skip-tags') == 1
-        assert args[args.index('--skip-tags') + 1] == 'user,pubkeys'
-
-    def test_build_args_no_vars(self):
-        self.task_config.update(dict(
-            playbook=[],
-        ))
-        task = self.klass(self.ctx, self.task_config)
-        task.setup()
-        args = task._build_args()
-        assert args.count('--extra-vars') == 1
-        vars_str = args[args.index('--extra-vars') + 1].strip("'")
-        extra_vars = json.loads(vars_str)
-        assert list(extra_vars) == ['ansible_ssh_user']
-
-    def test_build_args_vars(self):
-        extra_vars = dict(
-            string1='value1',
-            list1=['item1'],
-            dict1=dict(key='value'),
-        )
-
-        self.task_config.update(dict(
-            playbook=[],
-            vars=extra_vars,
-        ))
-        task = self.klass(self.ctx, self.task_config)
-        task.setup()
-        args = task._build_args()
-        assert args.count('--extra-vars') == 1
-        vars_str = args[args.index('--extra-vars') + 1].strip("'")
-        got_extra_vars = json.loads(vars_str)
-        assert 'ansible_ssh_user' in got_extra_vars
-        assert got_extra_vars['string1'] == extra_vars['string1']
-        assert got_extra_vars['list1'] == extra_vars['list1']
-        assert got_extra_vars['dict1'] == extra_vars['dict1']
-
-    def test_teardown_inventory(self):
-        self.task_config.update(dict(
-            playbook=[],
-        ))
-        task = self.klass(self.ctx, self.task_config)
-        task.generated_inventory = True
-        task.inventory = 'fake'
-        with patch.object(ansible.shutil, 'rmtree') as m_rmtree:
-            task.teardown()
-            m_rmtree.assert_called_once_with('fake')
-
-    def test_teardown_playbook(self):
-        self.task_config.update(dict(
-            playbook=[],
-        ))
-        task = self.klass(self.ctx, self.task_config)
-        task.generated_playbook = True
-        task.playbook_file = Mock()
-        task.playbook_file.name = 'fake'
-        with patch.object(ansible.os, 'remove') as m_remove:
-            task.teardown()
-            m_remove.assert_called_once_with('fake')
-
-    def test_teardown_cleanup_with_vars(self):
-        self.task_config.update(dict(
-            playbook=[],
-            cleanup=True,
-            vars=dict(yum_repos="testing"),
-        ))
-        task = self.klass(self.ctx, self.task_config)
-        task.inventory = "fake"
-        task.generated_playbook = True
-        task.playbook_file = Mock()
-        task.playbook_file.name = 'fake'
-        with patch.object(self.klass, 'execute_playbook') as m_execute:
-            with patch.object(ansible.os, 'remove'):
-                task.teardown()
-            task._build_args()
-            assert m_execute.called
-            assert 'cleanup' in task.config['vars']
-            assert 'yum_repos' in task.config['vars']
-
-    def test_teardown_cleanup_with_no_vars(self):
-        self.task_config.update(dict(
-            playbook=[],
-            cleanup=True,
-        ))
-        task = self.klass(self.ctx, self.task_config)
-        task.inventory = "fake"
-        task.generated_playbook = True
-        task.playbook_file = Mock()
-        task.playbook_file.name = 'fake'
-        with patch.object(self.klass, 'execute_playbook') as m_execute:
-            with patch.object(ansible.os, 'remove'):
-                task.teardown()
-            task._build_args()
-            assert m_execute.called
-            assert 'cleanup' in task.config['vars']
-
-    def test_no_remotes(self):
-        self.task_config.update(dict(
-            playbook=[],
-        ))
-        self.ctx.cluster.remotes = dict()
-        task = self.klass(self.ctx, self.task_config)
-        with patch.object(ansible.pexpect, 'run') as m_run:
-            task.setup()
-            task.begin()
-        assert not m_run.called
-
-
-class TestCephLabTask(TestAnsibleTask):
-    klass = CephLab
-    task_name = 'ansible.cephlab'
-
-    def setup_method(self):
-        super(TestCephLabTask, self).setup_method()
-        self.task_config = dict()
-
-    def start_patchers(self):
-        super(TestCephLabTask, self).start_patchers()
-        self.patchers['fetch_repo'] = patch(
-            'teuthology.repo_utils.fetch_repo',
-        )
-        self.patchers['fetch_repo'].return_value = 'PATH'
-
-        def fake_get_playbook(self):
-            self.playbook_file = Mock()
-            self.playbook_file.name = 'cephlab.yml'
-
-        self.patchers['get_playbook'] = patch(
-            'teuthology.task.ansible.CephLab.get_playbook',
-            new=fake_get_playbook,
-        )
-        for name in self.patchers.keys():
-            self.start_patcher(name)
-
-    @patch('teuthology.repo_utils.fetch_repo')
-    def test_find_repo_http(self, m_fetch_repo):
-        repo = os.path.join(config.ceph_git_base_url,
-                            'ceph-cm-ansible.git')
-        task = self.klass(self.ctx, dict())
-        task.find_repo()
-        m_fetch_repo.assert_called_once_with(repo, 'main')
-
-    def test_playbook_file(self):
-        fake_playbook = [dict(fake_playbook=True)]
-        fake_playbook_obj = StringIO(yaml.safe_dump(fake_playbook))
-        playbook = 'cephlab.yml'
-        fake_playbook_obj.name = playbook
-        task = self.klass(self.ctx, dict())
-        task.repo_path = '/tmp/fake/repo'
-        self.mocks['file'].return_value = fake_playbook_obj
-        task.get_playbook()
-        assert task.playbook_file.name == playbook
-
-    def test_generate_inventory(self):
-        self.task_config.update(dict(
-            playbook=[]
-        ))
-        task = self.klass(self.ctx, self.task_config)
-        hosts_file_path = '/my/hosts/file'
-        hosts_file_obj = StringIO()
-        hosts_file_obj.name = hosts_file_path
-        self.mocks['mkdtemp'].return_value = os.path.dirname(hosts_file_path)
-        self.mocks['file'].return_value = hosts_file_obj
-        task.generate_inventory()
-        assert task.generated_inventory is True
-        assert task.inventory == os.path.dirname(hosts_file_path)
-        hosts_file_obj.seek(0)
-        assert hosts_file_obj.readlines() == [
-            '[testnodes]\n',
-            'remote1\n',
-            'remote2\n',
-        ]
-
-    def test_fail_status_dead(self):
-        self.task_config.update(dict(
-            playbook=[],
-        ))
-        self.mocks['mkdtemp'].return_value = '/inventory/dir'
-        task = self.klass(self.ctx, self.task_config)
-        task.ctx.summary = dict()
-        task.setup()
-        with patch.object(ansible.pexpect, 'run') as m_run:
-            with patch('teuthology.task.ansible.open', mock_open()):
-                m_run.return_value = ('', 1)
-                with raises(CommandFailedError):
-                    task.execute_playbook()
-                assert task.ctx.summary.get('status') == 'dead'
-
-    def test_execute_playbook_fail(self):
-        self.mocks['mkdtemp'].return_value = '/inventory/dir'
-        task = self.klass(self.ctx, self.task_config)
-        task.setup()
-        with patch.object(ansible.pexpect, 'run') as m_run:
-            with patch('teuthology.task.ansible.open', mock_open()):
-                m_run.return_value = ('', 1)
-                with raises(CommandFailedError):
-                    task.execute_playbook()
-                assert task.ctx.summary.get('status') == 'dead'
-
-    @mark.skip("Unsupported")
-    def test_generate_playbook(self):
-        pass
-
-    @mark.skip("Unsupported")
-    def test_playbook_http(self):
-        pass
-
-    @mark.skip("Unsupported")
-    def test_playbook_none(self):
-        pass
-
-    @mark.skip("Unsupported")
-    def test_playbook_wrong_type(self):
-        pass
-
-    @mark.skip("Unsupported")
-    def test_playbook_list(self):
-        pass
-
-    @mark.skip("Test needs to be reimplemented for this class")
-    def test_playbook_file_missing(self):
-        pass
diff --git a/teuthology/test/task/test_ceph_ansible.py b/teuthology/test/task/test_ceph_ansible.py
deleted file mode 100644 (file)
index 29359c1..0000000
+++ /dev/null
@@ -1,177 +0,0 @@
-from mock import patch, MagicMock
-from pytest import skip
-from teuthology.util.compat import PY3
-if PY3:
-    from io import StringIO as StringIO
-else:
-    from io import BytesIO as StringIO
-
-from teuthology.config import FakeNamespace
-from teuthology.orchestra.cluster import Cluster
-from teuthology.orchestra.remote import Remote
-from teuthology.task import ceph_ansible
-from teuthology.task.ceph_ansible import CephAnsible
-
-from teuthology.test.task import TestTask
-
-SKIP_IRRELEVANT = "Not relevant to this subclass"
-
-
-class TestCephAnsibleTask(TestTask):
-    klass = CephAnsible
-    task_name = 'ceph_ansible'
-
-    def setup_method(self):
-        self.ctx = FakeNamespace()
-        self.ctx.cluster = Cluster()
-        self.ctx.cluster.add(Remote('user@remote1'), ['mon.0'])
-        self.ctx.cluster.add(Remote('user@remote2'), ['mds.0'])
-        self.ctx.cluster.add(Remote('user@remote3'), ['osd.0'])
-        self.ctx.summary = dict()
-        self.ctx.config = dict()
-        self.ctx.archive = '../'
-        self.task_config = dict()
-        self.start_patchers()
-
-    def start_patchers(self):
-        m_fetch_repo = MagicMock()
-        m_fetch_repo.return_value = 'PATH'
-
-        def fake_get_scratch_devices(remote):
-            return ['/dev/%s' % remote.shortname]
-
-        self.patcher_get_scratch_devices = patch(
-            'teuthology.task.ceph_ansible.get_scratch_devices',
-            fake_get_scratch_devices,
-        )
-        self.patcher_get_scratch_devices.start()
-
-        self.patcher_teardown = patch(
-            'teuthology.task.ceph_ansible.CephAnsible.teardown',
-        )
-        self.patcher_teardown.start()
-
-        def fake_set_iface_and_cidr(self):
-            self._interface = 'eth0'
-            self._cidr = '172.21.0.0/20'
-
-        self.patcher_remote = patch.multiple(
-            Remote,
-            _set_iface_and_cidr=fake_set_iface_and_cidr,
-        )
-        self.patcher_remote.start()
-
-    def stop_patchers(self):
-        self.patcher_get_scratch_devices.stop()
-        self.patcher_remote.stop()
-        self.patcher_teardown.stop()
-
-    def test_playbook_none(self):
-        skip(SKIP_IRRELEVANT)
-
-    def test_inventory_none(self):
-        skip(SKIP_IRRELEVANT)
-
-    def test_inventory_path(self):
-        skip(SKIP_IRRELEVANT)
-
-    def test_inventory_etc(self):
-        skip(SKIP_IRRELEVANT)
-
-    def test_generate_hosts_file(self):
-        self.task_config.update(dict(
-            playbook=[],
-            vars=dict(
-                osd_auto_discovery=True,
-                monitor_interface='eth0',
-                radosgw_interface='eth0',
-                public_network='172.21.0.0/20',
-            ),
-        ))
-        task = self.klass(self.ctx, self.task_config)
-        hosts_file_path = '/my/hosts/file'
-        hosts_file_obj = StringIO()
-        hosts_file_obj.name = hosts_file_path
-        with patch.object(ceph_ansible, 'NamedTemporaryFile') as m_NTF:
-            m_NTF.return_value = hosts_file_obj
-            task.generate_hosts_file()
-            m_NTF.assert_called_once_with(prefix="teuth_ansible_hosts_",
-                                          mode='w+',
-                                          delete=False)
-        assert task.generated_inventory is True
-        assert task.inventory == hosts_file_path
-        hosts_file_obj.seek(0)
-        assert hosts_file_obj.read() == '\n'.join([
-            '[mdss]',
-            'remote2',
-            '',
-            '[mons]',
-            'remote1',
-            '',
-            '[osds]',
-            'remote3',
-        ])
-
-    def test_generate_hosts_file_with_devices(self):
-        self.task_config.update(dict(
-            playbook=[],
-            vars=dict(
-                monitor_interface='eth0',
-                radosgw_interface='eth0',
-                public_network='172.21.0.0/20',
-            ),
-        ))
-        task = self.klass(self.ctx, self.task_config)
-        hosts_file_path = '/my/hosts/file'
-        hosts_file_obj = StringIO()
-        hosts_file_obj.name = hosts_file_path
-        with patch.object(ceph_ansible, 'NamedTemporaryFile') as m_NTF:
-            m_NTF.return_value = hosts_file_obj
-            task.generate_hosts_file()
-            m_NTF.assert_called_once_with(prefix="teuth_ansible_hosts_",
-                                          mode='w+',
-                                          delete=False)
-        assert task.generated_inventory is True
-        assert task.inventory == hosts_file_path
-        hosts_file_obj.seek(0)
-        assert hosts_file_obj.read() == '\n'.join([
-            '[mdss]',
-            'remote2 devices=\'[]\'',
-            '',
-            '[mons]',
-            'remote1 devices=\'[]\'',
-            '',
-            '[osds]',
-            'remote3 devices=\'["/dev/remote3"]\'',
-        ])
-
-    def test_generate_hosts_file_with_network(self):
-        self.task_config.update(dict(
-            playbook=[],
-            vars=dict(
-                osd_auto_discovery=True,
-            ),
-        ))
-        task = self.klass(self.ctx, self.task_config)
-        hosts_file_path = '/my/hosts/file'
-        hosts_file_obj = StringIO()
-        hosts_file_obj.name = hosts_file_path
-        with patch.object(ceph_ansible, 'NamedTemporaryFile') as m_NTF:
-            m_NTF.return_value = hosts_file_obj
-            task.generate_hosts_file()
-            m_NTF.assert_called_once_with(prefix="teuth_ansible_hosts_",
-                                          mode='w+',
-                                          delete=False)
-        assert task.generated_inventory is True
-        assert task.inventory == hosts_file_path
-        hosts_file_obj.seek(0)
-        assert hosts_file_obj.read() == '\n'.join([
-            '[mdss]',
-            "remote2 monitor_interface='eth0' public_network='172.21.0.0/20' radosgw_interface='eth0'",
-            '',
-            '[mons]',
-            "remote1 monitor_interface='eth0' public_network='172.21.0.0/20' radosgw_interface='eth0'",
-            '',
-            '[osds]',
-            "remote3 monitor_interface='eth0' public_network='172.21.0.0/20' radosgw_interface='eth0'",
-        ])
diff --git a/teuthology/test/task/test_console_log.py b/teuthology/test/task/test_console_log.py
deleted file mode 100644 (file)
index e2ad698..0000000
+++ /dev/null
@@ -1,92 +0,0 @@
-import os
-
-from mock import patch
-
-from teuthology.config import FakeNamespace
-from teuthology.config import config as teuth_config
-from teuthology.orchestra.cluster import Cluster
-from teuthology.orchestra.remote import Remote
-from teuthology.task.console_log import ConsoleLog
-
-from teuthology.test.task import TestTask
-
-
-class TestConsoleLog(TestTask):
-    klass = ConsoleLog
-    task_name = 'console_log'
-
-    def setup_method(self):
-        teuth_config.ipmi_domain = 'ipmi.domain'
-        teuth_config.ipmi_user = 'ipmi_user'
-        teuth_config.ipmi_password = 'ipmi_pass'
-        self.ctx = FakeNamespace()
-        self.ctx.cluster = Cluster()
-        self.ctx.cluster.add(Remote('user@remote1'), ['role1'])
-        self.ctx.cluster.add(Remote('user@remote2'), ['role2'])
-        self.ctx.config = dict()
-        self.ctx.archive = '/fake/path'
-        self.task_config = dict()
-        self.start_patchers()
-
-    def start_patchers(self):
-        self.patchers = dict()
-        self.patchers['makedirs'] = patch(
-            'teuthology.task.console_log.os.makedirs',
-        )
-        self.patchers['is_vm'] = patch(
-            'teuthology.lock.query.is_vm',
-        )
-        self.patchers['is_vm'].return_value = False
-        self.patchers['get_status'] = patch(
-            'teuthology.lock.query.get_status',
-        )
-        self.mocks = dict()
-        for name, patcher in self.patchers.items():
-            self.mocks[name] = patcher.start()
-        self.mocks['is_vm'].return_value = False
-
-    def teardown_method(self):
-        for patcher in self.patchers.values():
-            patcher.stop()
-
-    def test_enabled(self):
-        task = self.klass(self.ctx, self.task_config)
-        assert task.enabled is True
-
-    def test_disabled_noarchive(self):
-        self.ctx.archive = None
-        task = self.klass(self.ctx, self.task_config)
-        assert task.enabled is False
-
-    def test_has_ipmi_credentials(self):
-        for remote in self.ctx.cluster.remotes.keys():
-            remote.console.has_ipmi_credentials = False
-            remote.console.has_conserver = False
-        task = self.klass(self.ctx, self.task_config)
-        assert len(task.cluster.remotes.keys()) == 0
-
-    def test_remotes(self):
-        with self.klass(self.ctx, self.task_config) as task:
-            assert len(task.cluster.remotes) == len(self.ctx.cluster.remotes)
-
-    @patch('teuthology.orchestra.console.PhysicalConsole')
-    def test_begin(self, m_pconsole):
-        with self.klass(self.ctx, self.task_config) as task:
-            assert len(task.processes) == len(self.ctx.cluster.remotes)
-            expected_log_paths = []
-            for remote in task.cluster.remotes.keys():
-                expected_log_paths.append(
-                    os.path.join(self.ctx.archive, 'console_logs', '%s.log' % remote.shortname)
-                )
-            assert len(m_pconsole().spawn_sol_log.call_args_list) == len(task.cluster.remotes)
-            got_log_paths = [c[0][0] for c in m_pconsole().spawn_sol_log.call_args_list]
-            assert got_log_paths == expected_log_paths
-
-    @patch('teuthology.orchestra.console.PhysicalConsole')
-    def test_end(self, m_pconsole):
-        m_proc = m_pconsole().spawn_sol_log.return_value
-        m_proc.poll.return_value = None
-        with self.klass(self.ctx, self.task_config):
-            pass
-        assert len(m_proc.terminate.call_args_list) == len(self.ctx.cluster.remotes)
-        assert len(m_proc.kill.call_args_list) == len(self.ctx.cluster.remotes)
diff --git a/teuthology/test/task/test_install.py b/teuthology/test/task/test_install.py
deleted file mode 100644 (file)
index 1bbb501..0000000
+++ /dev/null
@@ -1,446 +0,0 @@
-import os
-import pytest
-import yaml
-
-from mock import patch, Mock
-from pytest import mark
-
-from teuthology.task import install
-
-
-class TestInstall(object):
-
-    def _get_default_package_list(self, project='ceph', debug=False):
-        path = os.path.join(
-            os.path.dirname(__file__),
-            '..', '..', 'task', 'install', 'packages.yaml',
-        )
-        pkgs = yaml.safe_load(open(path))[project]
-        if not debug:
-            pkgs['deb'] = [p for p in pkgs['deb']
-                                        if not p.endswith('-dbg')]
-            pkgs['rpm'] = [p for p in pkgs['rpm']
-                                        if not p.endswith('-debuginfo')]
-        return pkgs
-
-    def test_get_package_list_debug(self):
-        default_pkgs = self._get_default_package_list(debug=True)
-        default_pkgs['rpm'].sort()
-        default_pkgs['deb'].sort()
-        config = dict(debuginfo=True)
-        result = install.get_package_list(ctx=None, config=config)
-        result['rpm'].sort()
-        result['deb'].sort()
-        assert result == default_pkgs
-
-    def test_get_package_list_no_debug(self):
-        default_pkgs = self._get_default_package_list(debug=False)
-        default_pkgs['rpm'].sort()
-        default_pkgs['deb'].sort()
-        config = dict(debuginfo=False)
-        result = install.get_package_list(ctx=None, config=config)
-        result['rpm'].sort()
-        result['deb'].sort()
-        assert result == default_pkgs
-
-    def test_get_package_list_custom_rpm(self):
-        default_pkgs = self._get_default_package_list(debug=False)
-        default_pkgs['rpm'].sort()
-        default_pkgs['deb'].sort()
-        rpms = ['rpm1', 'rpm2', 'rpm2-debuginfo']
-        config = dict(packages=dict(rpm=rpms))
-        result = install.get_package_list(ctx=None, config=config)
-        result['rpm'].sort()
-        result['deb'].sort()
-        assert result['rpm'] == ['rpm1', 'rpm2']
-        assert result['deb'] == default_pkgs['deb']
-
-    @patch("teuthology.task.install._get_builder_project")
-    @patch("teuthology.task.install.packaging.get_package_version")
-    def test_get_upgrade_version(self, m_get_package_version,
-                                         m_gitbuilder_project):
-        gb = Mock()
-        gb.version = "11.0.0"
-        gb.project = "ceph"
-        m_gitbuilder_project.return_value = gb
-        m_get_package_version.return_value = "11.0.0"
-        install.get_upgrade_version(Mock(), Mock(), Mock())
-
-    @patch("teuthology.task.install._get_builder_project")
-    @patch("teuthology.task.install.packaging.get_package_version")
-    def test_verify_ceph_version_success(self, m_get_package_version,
-                                         m_gitbuilder_project):
-        gb = Mock()
-        gb.version = "0.89.0"
-        gb.project = "ceph"
-        m_gitbuilder_project.return_value = gb
-        m_get_package_version.return_value = "0.89.0"
-        config = dict()
-        install.verify_package_version(Mock(), config, Mock())
-
-    @patch("teuthology.task.install._get_builder_project")
-    @patch("teuthology.task.install.packaging.get_package_version")
-    def test_verify_ceph_version_failed(self, m_get_package_version,
-                                        m_gitbuilder_project):
-        gb = Mock()
-        gb.version = "0.89.0"
-        gb.project = "ceph"
-        m_gitbuilder_project.return_value = gb
-        m_get_package_version.return_value = "0.89.1"
-        config = dict()
-        with pytest.raises(RuntimeError):
-            install.verify_package_version(Mock(), config, Mock())
-
-    def test_get_flavor_default(self):
-        config = dict()
-        assert install.get_flavor(config) == 'default'
-
-    def test_get_flavor_simple(self):
-        config = dict(
-            flavor='notcmalloc'
-        )
-        assert install.get_flavor(config) == 'notcmalloc'
-
-    def test_get_flavor_valgrind(self):
-        config = dict(
-            valgrind=True
-        )
-        assert install.get_flavor(config) == 'notcmalloc'
-
-    def test_upgrade_is_downgrade(self):
-        assert_ok_vals = [
-            ('9.0.0', '10.0.0'),
-            ('10.2.2-63-g8542898-1trusty', '10.2.2-64-gabcdef1-1trusty'),
-            ('11.0.0-918.g13c13c7', '11.0.0-2165.gabcdef1')
-        ]
-        for t in assert_ok_vals:
-            assert install._upgrade_is_downgrade(t[0], t[1]) == False
-
-    @patch("teuthology.packaging.get_package_version")
-    @patch("teuthology.misc.get_system_type")
-    @patch("teuthology.task.install.verify_package_version")
-    @patch("teuthology.task.install.get_upgrade_version")
-    def test_upgrade_common(self,
-                            m_get_upgrade_version,
-                            m_verify_package_version,
-                            m_get_system_type,
-                            m_get_package_version):
-        expected_system_type = 'deb'
-        def make_remote():
-            remote = Mock()
-            remote.arch = 'x86_64'
-            remote.os = Mock()
-            remote.os.name = 'ubuntu'
-            remote.os.version = '14.04'
-            remote.os.codename = 'trusty'
-            remote.system_type = expected_system_type
-            return remote
-        ctx = Mock()
-        class cluster:
-            remote1 = make_remote()
-            remote2 = make_remote()
-            remotes = {
-                remote1: ['client.0'],
-                remote2: ['mon.a','osd.0'],
-            }
-            def only(self, role):
-                result = Mock()
-                if role in ('client.0',):
-                    result.remotes = { cluster.remote1: None }
-                if role in ('osd.0', 'mon.a'):
-                    result.remotes = { cluster.remote2: None }
-                return result
-        ctx.cluster = cluster()
-        config = {
-            'client.0': {
-                'sha1': 'expectedsha1',
-            },
-        }
-        ctx.config = {
-            'roles': [ ['client.0'], ['mon.a','osd.0'] ],
-            'tasks': [
-                {
-                    'install.upgrade': config,
-                },
-            ],
-        }
-        m_get_upgrade_version.return_value = "11.0.0"
-        m_get_package_version.return_value = "10.2.4"
-        m_get_system_type.return_value = "deb"
-        def upgrade(ctx, node, remote, pkgs, system_type):
-            assert system_type == expected_system_type
-        assert install.upgrade_common(ctx, config, upgrade) == 1
-        expected_config = {
-            'project': 'ceph',
-            'sha1': 'expectedsha1',
-        }
-        m_verify_package_version.assert_called_with(ctx,
-                                                    expected_config,
-                                                    cluster.remote1)
-    def test_upgrade_remote_to_config(self):
-        expected_system_type = 'deb'
-        def make_remote():
-            remote = Mock()
-            remote.arch = 'x86_64'
-            remote.os = Mock()
-            remote.os.name = 'ubuntu'
-            remote.os.version = '14.04'
-            remote.os.codename = 'trusty'
-            remote.system_type = expected_system_type
-            return remote
-        ctx = Mock()
-        class cluster:
-            remote1 = make_remote()
-            remote2 = make_remote()
-            remotes = {
-                remote1: ['client.0'],
-                remote2: ['mon.a','osd.0'],
-            }
-            def only(self, role):
-                result = Mock()
-                if role in ('client.0',):
-                    result.remotes = { cluster.remote1: None }
-                elif role in ('osd.0', 'mon.a'):
-                    result.remotes = { cluster.remote2: None }
-                else:
-                    result.remotes = None
-                return result
-        ctx.cluster = cluster()
-        ctx.config = {
-            'roles': [ ['client.0'], ['mon.a','osd.0'] ],
-        }
-
-        # nothing -> nothing
-        assert install.upgrade_remote_to_config(ctx, {}) == {}
-
-        # select the remote for the osd.0 role
-        # the 'ignored' role does not exist and is ignored
-        # the remote for mon.a is the same as for osd.0 and
-        # is silently ignored (actually it could be the other
-        # way around, depending on how the keys are hashed)
-        config = {
-            'osd.0': {
-                'sha1': 'expectedsha1',
-            },
-            'ignored': None,
-            'mon.a': {
-                'sha1': 'expectedsha1',
-            },
-        }
-        expected_config = {
-            cluster.remote2: {
-                'project': 'ceph',
-                'sha1': 'expectedsha1',
-            },
-        }
-        assert install.upgrade_remote_to_config(ctx, config) == expected_config
-
-        # select all nodes, regardless
-        config = {
-            'all': {
-                'sha1': 'expectedsha1',
-            },
-        }
-        expected_config = {
-            cluster.remote1: {
-                'project': 'ceph',
-                'sha1': 'expectedsha1',
-            },
-            cluster.remote2: {
-                'project': 'ceph',
-                'sha1': 'expectedsha1',
-            },
-        }
-        assert install.upgrade_remote_to_config(ctx, config) == expected_config
-
-        # verify that install overrides are used as default
-        # values for the upgrade task, not as override
-        ctx.config['overrides'] = {
-            'install': {
-                'ceph': {
-                    'sha1': 'overridesha1',
-                    'tag': 'overridetag',
-                    'branch': 'overridebranch',
-                },
-            },
-        }
-        config = {
-            'client.0': {
-                'sha1': 'expectedsha1',
-            },
-            'osd.0': {
-            },
-        }
-        expected_config = {
-            cluster.remote1: {
-                'project': 'ceph',
-                'sha1': 'expectedsha1',
-            },
-            cluster.remote2: {
-                'project': 'ceph',
-                'sha1': 'overridesha1',
-                'tag': 'overridetag',
-                'branch': 'overridebranch',
-            },
-        }
-        assert install.upgrade_remote_to_config(ctx, config) == expected_config
-
-
-    @patch("teuthology.task.install.packaging.get_package_version")
-    @patch("teuthology.task.install.redhat.set_deb_repo")
-    def test_rh_install_deb_pkgs(self, m_set_rh_deb_repo, m_get_pkg_version):
-        ctx = Mock()
-        remote = Mock()
-        version = '1.3.2'
-        rh_ds_yaml = dict()
-        rh_ds_yaml = {
-            'versions': {'deb': {'mapped': {'1.3.2': '0.94.5'}}},
-            'pkgs': {'deb': ['pkg1', 'pkg2']},
-            'extra_system_packages': {'deb': ['es_pkg1', 'es_pkg2']},
-            'extra_packages': {'deb': ['e_pkg1', 'e_pkg2']},
-        }
-        m_get_pkg_version.return_value = "0.94.5"
-        install.redhat.install_deb_pkgs(ctx, remote, version, rh_ds_yaml)
-
-    @patch("teuthology.task.install.packaging.get_package_version")
-    def test_rh_install_pkgs(self, m_get_pkg_version):
-        ctx = Mock()
-        remote = Mock()
-        version = '1.3.2'
-        rh_ds_yaml = dict()
-        rh_ds_yaml = {
-            'versions': {'rpm': {'mapped': {'1.3.2': '0.94.5',
-                                            '1.3.1': '0.94.3'}}},
-            'pkgs': {'rpm': ['pkg1', 'pkg2']},
-            'extra_system_packages': {'rpm': ['es_pkg1', 'es_pkg2']},
-            'extra_packages': {'rpm': ['e_pkg1', 'e_pkg2']},
-        }
-
-        m_get_pkg_version.return_value = "0.94.5"
-        install.redhat.install_pkgs(ctx, remote, version, rh_ds_yaml)
-        version = '1.3.1'
-        with pytest.raises(RuntimeError) as e:
-            install.redhat.install_pkgs(ctx, remote, version, rh_ds_yaml)
-        assert "Version check failed" in str(e)
-
-    @mark.parametrize(
-        'conf, expect',
-        [
-            [
-                {
-                    'tasks': [ { 'install': { 'clean': True, }, }, ],
-                    'overrides': {
-                        'install': {
-                            'ceph': {
-                                 'extra_system_packages': ['alpha'],
-                                 'flavor': 'default',
-                                 'sha1': '0123456789abcdef0123456789abcdef01234567',
-                             },
-                            'extra_system_packages': {
-                                'deb': [],
-                                'rpm': ['xerxes', 'yellow'],
-                            },
-                        },
-                    },
-                },
-                {
-                    'deb': ['alpha'],
-                    'rpm': ['alpha', 'xerxes', 'yellow'],
-                }
-            ],
-            [
-                {
-                    'tasks': [ { 'install': { 'clean': True, }, }, ],
-                    'overrides': {
-                        'install': {
-                            'ceph': {
-                                 'extra_system_packages': {
-                                     'deb': [],
-                                     'rpm': ['xerxes', 'yellow'],
-                                 },
-                                 'flavor': 'default',
-                                 'sha1': '0123456789abcdef0123456789abcdef01234567',
-                             },
-                            'extra_system_packages': ['alpha'],
-                        },
-                    },
-                },
-                {
-                    'deb': ['alpha'],
-                    'rpm': ['xerxes', 'yellow', 'alpha'],
-                }
-            ],
-            [
-                {
-                    'tasks': [ { 'install': { 'clean': True, }, }, ],
-                    'overrides': {
-                        'install': {
-                            'ceph': {
-                                 'flavor': 'default',
-                                 'sha1': '0123456789abcdef0123456789abcdef01234567',
-                             },
-                            'extra_system_packages': {
-                                'deb': [],
-                                'rpm': ['xerxes', 'yellow'],
-                            },
-                        },
-                    },
-                },
-                {
-                    'deb': [],
-                    'rpm': ['xerxes', 'yellow'],
-                }
-            ],
-            [
-                {
-                    'tasks': [ { 'install': { 'clean': True, }, }, ],
-                    'overrides': {
-                        'install': {
-                            'ceph': {
-                                 'flavor': 'default',
-                                 'sha1': '0123456789abcdef0123456789abcdef01234567',
-                             },
-                            'extra_system_packages': ['xerxes', 'yellow'],
-                        },
-                    },
-                },
-                {
-                    'deb': ['xerxes', 'yellow'],
-                    'rpm': ['xerxes', 'yellow'],
-                }
-            ],
-            [
-                {
-                    'tasks': [ { 'install': { 'clean': True, }, }, ],
-                    'overrides': { 'install': {
-                            'ceph': { 'flavor': 'default', 'sha1': '012345', },
-                            'extra_system_packages': { 'rpm': ['xerxes', 'yellow'], },
-                        },
-                    },
-                },
-                { 'deb': [], 'rpm': ['xerxes', 'yellow'], }
-
-            ],
-            [
-                {
-                    'tasks': [ { 'install': { 'clean': True,
-                                             'extra_system_packages': { 'deb': ['alpha'], 'rpm': ['bravo'] },
-                                             }, }, ],
-                    'overrides': { 'install': {
-                            'ceph': { 'flavor': 'default', 'sha1': '012345', },
-                            'extra_system_packages': { 'rpm': ['xerxes', 'yellow'], },
-                        },
-                    },
-                },
-                { 'deb': ['alpha'], 'rpm': ['bravo', 'xerxes', 'yellow'], }
-
-            ],
-
-        ]
-    )
-    def test_install_extra_system_packages(self, conf, expect):
-        install_task = conf.get('tasks')[0].get('install')
-        install_overrides = conf.get('overrides').get('install', {})
-        install._override_extra_system_packages(install_task, 'ceph', install_overrides)
-
-        assert install_task.get('extra_system_packages') == expect
diff --git a/teuthology/test/task/test_internal.py b/teuthology/test/task/test_internal.py
deleted file mode 100644 (file)
index 1340b18..0000000
+++ /dev/null
@@ -1,57 +0,0 @@
-from teuthology.config import FakeNamespace
-from teuthology.task import internal
-
-
-class TestInternal(object):
-    def setup_method(self):
-        self.ctx = FakeNamespace()
-        self.ctx.config = dict()
-
-    def test_buildpackages_prep(self):
-        #
-        # no buildpackages nor install tasks
-        #
-        self.ctx.config = { 'tasks': [] }
-        assert internal.buildpackages_prep(self.ctx,
-                                           self.ctx.config) == internal.BUILDPACKAGES_NOTHING
-        #
-        # make the buildpackages tasks the first to run
-        #
-        self.ctx.config = {
-            'tasks': [ { 'atask': None },
-                       { 'internal.buildpackages_prep': None },
-                       { 'btask': None },
-                       { 'install': None },
-                       { 'buildpackages': None } ],
-        }
-        assert internal.buildpackages_prep(self.ctx,
-                                           self.ctx.config) == internal.BUILDPACKAGES_FIRST
-        assert self.ctx.config == {
-            'tasks': [ { 'atask': None },
-                       { 'internal.buildpackages_prep': None },
-                       { 'buildpackages': None },
-                       { 'btask': None },
-                       { 'install': None } ],
-        }
-        #
-        # the buildpackages task already the first task to run
-        #
-        assert internal.buildpackages_prep(self.ctx,
-                                           self.ctx.config) == internal.BUILDPACKAGES_OK
-        #
-        # no buildpackages task
-        #
-        self.ctx.config = {
-            'tasks': [ { 'install': None } ],
-        }
-        assert internal.buildpackages_prep(self.ctx,
-                                           self.ctx.config) == internal.BUILDPACKAGES_NOTHING
-        #
-        # no install task: the buildpackages task must be removed
-        #
-        self.ctx.config = {
-            'tasks': [ { 'buildpackages': None } ],
-        }
-        assert internal.buildpackages_prep(self.ctx,
-                                           self.ctx.config) == internal.BUILDPACKAGES_REMOVED
-        assert self.ctx.config == {'tasks': []}
diff --git a/teuthology/test/task/test_kernel.py b/teuthology/test/task/test_kernel.py
deleted file mode 100644 (file)
index 593b204..0000000
+++ /dev/null
@@ -1,243 +0,0 @@
-from teuthology.config import FakeNamespace
-from teuthology.orchestra.cluster import Cluster
-from teuthology.orchestra.remote import Remote
-from teuthology.task.kernel import (
-    normalize_and_apply_overrides,
-    CONFIG_DEFAULT,
-    TIMEOUT_DEFAULT,
-)
-
-class TestKernelNormalizeAndApplyOverrides(object):
-
-    def setup_method(self):
-        self.ctx = FakeNamespace()
-        self.ctx.cluster = Cluster()
-        self.ctx.cluster.add(Remote('remote1'), ['mon.a', 'client.0'])
-        self.ctx.cluster.add(Remote('remote2'), ['osd.0', 'osd.1', 'osd.2'])
-        self.ctx.cluster.add(Remote('remote3'), ['client.1'])
-
-    def test_default(self):
-        config = {}
-        overrides = {}
-        config, t = normalize_and_apply_overrides(self.ctx, config, overrides)
-        assert config == {
-            'mon.a': CONFIG_DEFAULT,
-            'osd.0': CONFIG_DEFAULT,
-            'osd.1': CONFIG_DEFAULT,
-            'osd.2': CONFIG_DEFAULT,
-            'client.0': CONFIG_DEFAULT,
-            'client.1': CONFIG_DEFAULT,
-        }
-        assert t == TIMEOUT_DEFAULT
-
-    def test_timeout_default(self):
-        config = {
-            'client.0': {'branch': 'testing'},
-        }
-        overrides = {}
-        config, t = normalize_and_apply_overrides(self.ctx, config, overrides)
-        assert config == {
-            'client.0': {'branch': 'testing'},
-        }
-        assert t == TIMEOUT_DEFAULT
-
-    def test_timeout(self):
-        config = {
-            'client.0': {'branch': 'testing'},
-            'timeout': 100,
-        }
-        overrides = {}
-        config, t = normalize_and_apply_overrides(self.ctx, config, overrides)
-        assert config == {
-            'client.0': {'branch': 'testing'},
-        }
-        assert t == 100
-
-    def test_override_timeout(self):
-        config = {
-            'client.0': {'branch': 'testing'},
-            'timeout': 100,
-        }
-        overrides = {
-            'timeout': 200,
-        }
-        config, t = normalize_and_apply_overrides(self.ctx, config, overrides)
-        assert config == {
-            'client.0': {'branch': 'testing'},
-        }
-        assert t == 200
-
-    def test_override_same_version_key(self):
-        config = {
-            'client.0': {'branch': 'testing'},
-        }
-        overrides = {
-            'client.0': {'branch': 'wip-foobar'},
-        }
-        config, t = normalize_and_apply_overrides(self.ctx, config, overrides)
-        assert config == {
-            'client.0': {'branch': 'wip-foobar'},
-        }
-        assert t == TIMEOUT_DEFAULT
-
-    def test_override_different_version_key(self):
-        config = {
-            'client.0': {'branch': 'testing'},
-        }
-        overrides = {
-            'client.0': {'tag': 'v4.1'},
-        }
-        config, t = normalize_and_apply_overrides(self.ctx, config, overrides)
-        assert config == {
-            'client.0': {'tag': 'v4.1'},
-        }
-        assert t == TIMEOUT_DEFAULT
-
-    def test_override_actual(self):
-        config = {
-            'osd.1': {'tag': 'v4.1'},
-            'client.0': {'branch': 'testing'},
-        }
-        overrides = {
-            'osd.1': {'koji': 1234, 'kdb': True},
-        }
-        config, t = normalize_and_apply_overrides(self.ctx, config, overrides)
-        assert config == {
-            'osd.1': {'koji': 1234, 'kdb': True},
-            'client.0': {'branch': 'testing'},
-        }
-        assert t == TIMEOUT_DEFAULT
-
-    def test_override_actual_with_generic(self):
-        config = {
-            'osd.1': {'tag': 'v4.1', 'kdb': False},
-            'client.0': {'branch': 'testing'},
-        }
-        overrides = {
-            'osd': {'koji': 1234},
-        }
-        config, t = normalize_and_apply_overrides(self.ctx, config, overrides)
-        assert config == {
-            'osd.0': {'koji': 1234},
-            'osd.1': {'koji': 1234, 'kdb': False},
-            'osd.2': {'koji': 1234},
-            'client.0': {'branch': 'testing'},
-        }
-        assert t == TIMEOUT_DEFAULT
-
-    def test_override_actual_with_top_level(self):
-        config = {
-            'osd.1': {'tag': 'v4.1'},
-            'client.0': {'branch': 'testing', 'kdb': False},
-        }
-        overrides = {'koji': 1234, 'kdb': True}
-        config, t = normalize_and_apply_overrides(self.ctx, config, overrides)
-        assert config == {
-            'mon.a': {'koji': 1234, 'kdb': True},
-            'osd.0': {'koji': 1234, 'kdb': True},
-            'osd.1': {'koji': 1234, 'kdb': True},
-            'osd.2': {'koji': 1234, 'kdb': True},
-            'client.0': {'koji': 1234, 'kdb': True},
-            'client.1': {'koji': 1234, 'kdb': True},
-        }
-        assert t == TIMEOUT_DEFAULT
-
-    def test_override_generic(self):
-        config = {
-            'osd': {'tag': 'v4.1'},
-            'client': {'branch': 'testing'},
-        }
-        overrides = {
-            'client': {'koji': 1234, 'kdb': True},
-        }
-        config, t = normalize_and_apply_overrides(self.ctx, config, overrides)
-        assert config == {
-            'osd.0': {'tag': 'v4.1'},
-            'osd.1': {'tag': 'v4.1'},
-            'osd.2': {'tag': 'v4.1'},
-            'client.0': {'koji': 1234, 'kdb': True},
-            'client.1': {'koji': 1234, 'kdb': True},
-        }
-        assert t == TIMEOUT_DEFAULT
-
-    def test_override_generic_with_top_level(self):
-        config = {
-            'osd': {'tag': 'v4.1'},
-            'client': {'branch': 'testing', 'kdb': False},
-        }
-        overrides = {
-            'client': {'koji': 1234},
-        }
-        config, t = normalize_and_apply_overrides(self.ctx, config, overrides)
-        assert config == {
-            'osd.0': {'tag': 'v4.1'},
-            'osd.1': {'tag': 'v4.1'},
-            'osd.2': {'tag': 'v4.1'},
-            'client.0': {'koji': 1234, 'kdb': False},
-            'client.1': {'koji': 1234, 'kdb': False},
-        }
-        assert t == TIMEOUT_DEFAULT
-
-    def test_override_generic_with_actual(self):
-        config = {
-            'osd': {'tag': 'v4.1', 'kdb': False},
-            'client': {'branch': 'testing'},
-        }
-        overrides = {
-            'osd.2': {'koji': 1234, 'kdb': True},
-        }
-        config, t = normalize_and_apply_overrides(self.ctx, config, overrides)
-        assert config == {
-            'osd.0': {'tag': 'v4.1', 'kdb': False},
-            'osd.1': {'tag': 'v4.1', 'kdb': False},
-            'osd.2': {'koji': 1234, 'kdb': True},
-            'client.0': {'branch': 'testing'},
-            'client.1': {'branch': 'testing'},
-        }
-        assert t == TIMEOUT_DEFAULT
-
-    def test_override_top_level(self):
-        config = {'branch': 'testing'}
-        overrides = {'koji': 1234, 'kdb': True}
-        config, t = normalize_and_apply_overrides(self.ctx, config, overrides)
-        assert config == {
-            'mon.a': {'koji': 1234, 'kdb': True},
-            'osd.0': {'koji': 1234, 'kdb': True},
-            'osd.1': {'koji': 1234, 'kdb': True},
-            'osd.2': {'koji': 1234, 'kdb': True},
-            'client.0': {'koji': 1234, 'kdb': True},
-            'client.1': {'koji': 1234, 'kdb': True},
-        }
-        assert t == TIMEOUT_DEFAULT
-
-    def test_override_top_level_with_actual(self):
-        config = {'branch': 'testing', 'kdb': False}
-        overrides = {
-            'mon.a': {'koji': 1234},
-        }
-        config, t = normalize_and_apply_overrides(self.ctx, config, overrides)
-        assert config == {
-            'mon.a': {'koji': 1234, 'kdb': False},
-            'osd.0': {'branch': 'testing', 'kdb': False},
-            'osd.1': {'branch': 'testing', 'kdb': False},
-            'osd.2': {'branch': 'testing', 'kdb': False},
-            'client.0': {'branch': 'testing', 'kdb': False},
-            'client.1': {'branch': 'testing', 'kdb': False},
-        }
-        assert t == TIMEOUT_DEFAULT
-
-    def test_override_top_level_with_generic(self):
-        config = {'branch': 'testing', 'kdb': False}
-        overrides = {
-            'client': {'koji': 1234, 'kdb': True},
-        }
-        config, t = normalize_and_apply_overrides(self.ctx, config, overrides)
-        assert config == {
-            'mon.a': {'branch': 'testing', 'kdb': False},
-            'osd.0': {'branch': 'testing', 'kdb': False},
-            'osd.1': {'branch': 'testing', 'kdb': False},
-            'osd.2': {'branch': 'testing', 'kdb': False},
-            'client.0': {'koji': 1234, 'kdb': True},
-            'client.1': {'koji': 1234, 'kdb': True},
-        }
-        assert t == TIMEOUT_DEFAULT
diff --git a/teuthology/test/task/test_pcp.py b/teuthology/test/task/test_pcp.py
deleted file mode 100644 (file)
index 7777407..0000000
+++ /dev/null
@@ -1,379 +0,0 @@
-import os
-import requests
-
-from teuthology.util.compat import parse_qs, urljoin
-
-from mock import patch, DEFAULT, Mock, mock_open, call
-from pytest import raises
-
-from teuthology.config import config, FakeNamespace
-from teuthology.orchestra.cluster import Cluster
-from teuthology.orchestra.remote import Remote
-from teuthology.orchestra.run import Raw
-from teuthology.task.pcp import (PCPDataSource, PCPArchive, PCPGrapher,
-                                 GrafanaGrapher, GraphiteGrapher, PCP)
-
-from teuthology.test.task import TestTask
-
-pcp_host = 'http://pcp.front.sepia.ceph.com:44323/'
-
-
-class TestPCPDataSource(object):
-    klass = PCPDataSource
-
-    def setup_method(self):
-        config.pcp_host = pcp_host
-
-    def test_init(self):
-        hosts = ['host1', 'host2']
-        time_from = 'now-2h'
-        time_until = 'now'
-        obj = self.klass(
-            hosts=hosts,
-            time_from=time_from,
-            time_until=time_until,
-        )
-        assert obj.hosts == hosts
-        assert obj.time_from == time_from
-        assert obj.time_until == time_until
-
-
-class TestPCPArchive(TestPCPDataSource):
-    klass = PCPArchive
-
-    def test_get_archive_input_dir(self):
-        hosts = ['host1', 'host2']
-        time_from = 'now-1d'
-        obj = self.klass(
-            hosts=hosts,
-            time_from=time_from,
-        )
-        assert obj.get_archive_input_dir('host1') == \
-            '/var/log/pcp/pmlogger/host1'
-
-    def test_get_pmlogextract_cmd(self):
-        obj = self.klass(
-            hosts=['host1'],
-            time_from='now-3h',
-            time_until='now-1h',
-        )
-        expected = [
-            'pmlogextract',
-            '-S', 'now-3h',
-            '-T', 'now-1h',
-            Raw('/var/log/pcp/pmlogger/host1/*.0'),
-        ]
-        assert obj.get_pmlogextract_cmd('host1') == expected
-
-    def test_format_time(self):
-        assert self.klass._format_time(1462893484) == \
-            '@ Tue May 10 15:18:04 2016'
-
-    def test_format_time_now(self):
-        assert self.klass._format_time('now-1h') == 'now-1h'
-
-
-class TestPCPGrapher(TestPCPDataSource):
-    klass = PCPGrapher
-
-    def test_init(self):
-        hosts = ['host1', 'host2']
-        time_from = 'now-2h'
-        time_until = 'now'
-        obj = self.klass(
-            hosts=hosts,
-            time_from=time_from,
-            time_until=time_until,
-        )
-        assert obj.hosts == hosts
-        assert obj.time_from == time_from
-        assert obj.time_until == time_until
-        expected_url = urljoin(config.pcp_host, self.klass._endpoint)
-        assert obj.base_url == expected_url
-
-
-class TestGrafanaGrapher(TestPCPGrapher):
-    klass = GrafanaGrapher
-
-    def test_build_graph_url(self):
-        hosts = ['host1']
-        time_from = 'now-3h'
-        time_until = 'now-1h'
-        obj = self.klass(
-            hosts=hosts,
-            time_from=time_from,
-            time_until=time_until,
-        )
-        base_url = urljoin(
-            config.pcp_host,
-            'grafana/index.html#/dashboard/script/index.js',
-        )
-        assert obj.base_url == base_url
-        got_url = obj.build_graph_url()
-        parsed_query = parse_qs(got_url.split('?')[1])
-        assert parsed_query['hosts'] == hosts
-        assert len(parsed_query['time_from']) == 1
-        assert parsed_query['time_from'][0] == time_from
-        assert len(parsed_query['time_to']) == 1
-        assert parsed_query['time_to'][0] == time_until
-
-    def test_format_time(self):
-        assert self.klass._format_time(1462893484) == \
-            '2016-05-10T15:18:04'
-
-    def test_format_time_now(self):
-        assert self.klass._format_time('now-1h') == 'now-1h'
-
-
-class TestGraphiteGrapher(TestPCPGrapher):
-    klass = GraphiteGrapher
-
-    def test_build_graph_urls(self):
-        obj = self.klass(
-            hosts=['host1', 'host2'],
-            time_from='now-3h',
-            time_until='now-1h',
-        )
-        expected_urls = [obj.get_graph_url(m) for m in obj.metrics]
-        obj.build_graph_urls()
-        built_urls = []
-        for metric in obj.graphs.keys():
-            built_urls.append(obj.graphs[metric]['url'])
-        assert len(built_urls) == len(expected_urls)
-        assert sorted(built_urls) == sorted(expected_urls)
-
-    def test_check_dest_dir(self):
-        obj = self.klass(
-            hosts=['host1'],
-            time_from='now-3h',
-        )
-        assert obj.dest_dir is None
-        with raises(RuntimeError):
-            obj._check_dest_dir()
-
-    def test_generate_html_dynamic(self):
-        obj = self.klass(
-            hosts=['host1'],
-            time_from='now-3h',
-        )
-        html = obj.generate_html()
-        assert config.pcp_host in html
-
-    def test_download_graphs(self):
-        dest_dir = '/fake/path'
-        obj = self.klass(
-            hosts=['host1'],
-            time_from='now-3h',
-            dest_dir=dest_dir,
-        )
-        _format = obj.graph_defaults.get('format')
-        with patch('teuthology.task.pcp.requests.get', create=True) as m_get:
-            m_resp = Mock()
-            m_resp.ok = True
-            m_get.return_value = m_resp
-            with patch('teuthology.task.pcp.open', mock_open(), create=True):
-                obj.download_graphs()
-        expected_filenames = []
-        for metric in obj.metrics:
-            expected_filenames.append(
-                "{}.{}".format(
-                    os.path.join(
-                        dest_dir,
-                        obj._sanitize_metric_name(metric),
-                    ),
-                    _format,
-                )
-            )
-        graph_filenames = []
-        for metric in obj.graphs.keys():
-            graph_filenames.append(obj.graphs[metric]['file'])
-        assert sorted(graph_filenames) == sorted(expected_filenames)
-
-    def test_generate_html_static(self):
-        obj = self.klass(
-            hosts=['host1'],
-            time_from='now-3h',
-            dest_dir='/fake/path',
-        )
-        with patch('teuthology.task.pcp.requests.get', create=True) as m_get:
-            m_resp = Mock()
-            m_resp.ok = True
-            m_get.return_value = m_resp
-            with patch('teuthology.task.pcp.open', mock_open(), create=True):
-                obj.download_graphs()
-        html = obj.generate_html(mode='static')
-        assert config.pcp_host not in html
-
-    def test_sanitize_metric_name(self):
-        sanitized_metrics = {
-            'foo.bar': 'foo.bar',
-            'foo.*': 'foo._all_',
-            'foo.bar baz': 'foo.bar_baz',
-            'foo.*.bar baz': 'foo._all_.bar_baz',
-        }
-        for in_, out in sanitized_metrics.items():
-            assert self.klass._sanitize_metric_name(in_) == out
-
-    def test_get_target_globs(self):
-        obj = self.klass(
-            hosts=['host1'],
-            time_from='now-3h',
-        )
-        assert obj.get_target_globs() == ['*host1*']
-        assert obj.get_target_globs('a.metric') == ['*host1*.a.metric']
-        obj.hosts.append('host2')
-        assert obj.get_target_globs() == ['*host1*', '*host2*']
-        assert obj.get_target_globs('a.metric') == \
-            ['*host1*.a.metric', '*host2*.a.metric']
-
-
-class TestPCPTask(TestTask):
-    klass = PCP
-    task_name = 'pcp'
-
-    def setup_method(self):
-        self.ctx = FakeNamespace()
-        self.ctx.cluster = Cluster()
-        self.ctx.cluster.add(Remote('user@remote1'), ['role1'])
-        self.ctx.cluster.add(Remote('user@remote2'), ['role2'])
-        self.ctx.config = dict()
-        self.task_config = dict()
-        config.pcp_host = pcp_host
-
-    def test_init(self):
-        task = self.klass(self.ctx, self.task_config)
-        assert task.stop_time == 'now'
-
-    def test_disabled(self):
-        config.pcp_host = None
-        with self.klass(self.ctx, self.task_config) as task:
-            assert task.enabled is False
-            assert not hasattr(task, 'grafana')
-            assert not hasattr(task, 'graphite')
-            assert not hasattr(task, 'archiver')
-
-    def test_setup(self):
-        with patch.multiple(
-            self.klass,
-            setup_collectors=DEFAULT,
-            begin=DEFAULT,
-            end=DEFAULT,
-        ):
-            with self.klass(self.ctx, self.task_config) as task:
-                task.setup_collectors.assert_called_once_with()
-                assert isinstance(task.start_time, int)
-
-    def test_setup_collectors(self):
-        with patch.multiple(
-            self.klass,
-            begin=DEFAULT,
-            end=DEFAULT,
-        ):
-            with self.klass(self.ctx, self.task_config) as task:
-                assert hasattr(task, 'grafana')
-                assert not hasattr(task, 'graphite')
-                assert not hasattr(task, 'archiver')
-            self.task_config['grafana'] = False
-            with self.klass(self.ctx, self.task_config) as task:
-                assert not hasattr(task, 'grafana')
-
-    @patch('os.makedirs')
-    def test_setup_grafana(self, m_makedirs):
-        with patch.multiple(
-            self.klass,
-            begin=DEFAULT,
-            end=DEFAULT,
-        ):
-            self.ctx.archive = '/fake/path'
-            with self.klass(self.ctx, self.task_config) as task:
-                assert hasattr(task, 'grafana')
-            self.task_config['grafana'] = False
-            with self.klass(self.ctx, self.task_config) as task:
-                assert not hasattr(task, 'grafana')
-
-    @patch('os.makedirs')
-    @patch('teuthology.task.pcp.GraphiteGrapher')
-    def test_setup_graphite(self, m_graphite_grapher, m_makedirs):
-        with patch.multiple(
-            self.klass,
-            begin=DEFAULT,
-            end=DEFAULT,
-        ):
-            with self.klass(self.ctx, self.task_config) as task:
-                assert not hasattr(task, 'graphite')
-            self.task_config['graphite'] = False
-            with self.klass(self.ctx, self.task_config) as task:
-                assert not hasattr(task, 'graphite')
-            self.ctx.archive = '/fake/path'
-            self.task_config['graphite'] = True
-            with self.klass(self.ctx, self.task_config) as task:
-                assert hasattr(task, 'graphite')
-            self.task_config['graphite'] = False
-            with self.klass(self.ctx, self.task_config) as task:
-                assert not hasattr(task, 'graphite')
-
-    @patch('os.makedirs')
-    @patch('teuthology.task.pcp.PCPArchive')
-    def test_setup_archiver(self, m_archive, m_makedirs):
-        with patch.multiple(
-            self.klass,
-            begin=DEFAULT,
-            end=DEFAULT,
-        ):
-            self.task_config['fetch_archives'] = True
-            with self.klass(self.ctx, self.task_config) as task:
-                assert not hasattr(task, 'archiver')
-            self.task_config['fetch_archives'] = False
-            with self.klass(self.ctx, self.task_config) as task:
-                assert not hasattr(task, 'archiver')
-            self.ctx.archive = '/fake/path'
-            self.task_config['fetch_archives'] = True
-            with self.klass(self.ctx, self.task_config) as task:
-                assert hasattr(task, 'archiver')
-            self.task_config['fetch_archives'] = False
-            with self.klass(self.ctx, self.task_config) as task:
-                assert not hasattr(task, 'archiver')
-
-    @patch('os.makedirs')
-    @patch('teuthology.task.pcp.GrafanaGrapher')
-    @patch('teuthology.task.pcp.GraphiteGrapher')
-    def test_begin(self, m_grafana, m_graphite, m_makedirs):
-        with patch.multiple(
-            self.klass,
-            end=DEFAULT,
-        ):
-            with self.klass(self.ctx, self.task_config) as task:
-                task.grafana.build_graph_url.assert_called_once_with()
-            self.task_config['graphite'] = True
-            self.ctx.archive = '/fake/path'
-            with self.klass(self.ctx, self.task_config) as task:
-                task.graphite.write_html.assert_called_once_with()
-
-    @patch('os.makedirs')
-    @patch('teuthology.task.pcp.GrafanaGrapher')
-    @patch('teuthology.task.pcp.GraphiteGrapher')
-    def test_end(self, m_grafana, m_graphite, m_makedirs):
-        self.ctx.archive = '/fake/path'
-        with self.klass(self.ctx, self.task_config) as task:
-            # begin() should have called write_html() once by now, with no args
-            task.graphite.write_html.assert_called_once_with()
-        # end() should have called write_html() a second time by now, with
-        # mode=static
-        second_call = task.graphite.write_html.call_args_list[1]
-        assert second_call[1]['mode'] == 'static'
-        assert isinstance(task.stop_time, int)
-
-    @patch('os.makedirs')
-    @patch('teuthology.task.pcp.GrafanaGrapher')
-    @patch('teuthology.task.pcp.GraphiteGrapher')
-    def test_end_16049(self, m_grafana, m_graphite, m_makedirs):
-        # http://tracker.ceph.com/issues/16049
-        # Jobs were failing if graph downloading failed. We don't want that.
-        self.ctx.archive = '/fake/path'
-        with self.klass(self.ctx, self.task_config) as task:
-            task.graphite.download_graphs.side_effect = \
-                requests.ConnectionError
-        # Even though downloading graphs failed, we should have called
-        # write_html() a second time, again with no args
-        assert task.graphite.write_html.call_args_list == [call(), call()]
-        assert isinstance(task.stop_time, int)
diff --git a/teuthology/test/task/test_selinux.py b/teuthology/test/task/test_selinux.py
deleted file mode 100644 (file)
index 68c8e9c..0000000
+++ /dev/null
@@ -1,35 +0,0 @@
-from mock import patch, Mock, DEFAULT
-
-from teuthology.config import FakeNamespace
-from teuthology.orchestra.cluster import Cluster
-from teuthology.orchestra.remote import Remote
-from teuthology.task.selinux import SELinux
-
-
-class TestSELinux(object):
-    def setup_method(self):
-        self.ctx = FakeNamespace()
-        self.ctx.config = dict()
-
-    def test_host_exclusion(self):
-        with patch.multiple(
-            Remote,
-            os=DEFAULT,
-            run=DEFAULT,
-        ):
-            self.ctx.cluster = Cluster()
-            remote1 = Remote('remote1')
-            remote1.os = Mock()
-            remote1.os.package_type = 'rpm'
-            remote1._is_vm = False
-            self.ctx.cluster.add(remote1, ['role1'])
-            remote2 = Remote('remote1')
-            remote2.os = Mock()
-            remote2.os.package_type = 'deb'
-            remote2._is_vm = False
-            self.ctx.cluster.add(remote2, ['role2'])
-            task_config = dict()
-            with SELinux(self.ctx, task_config) as task:
-                remotes = list(task.cluster.remotes)
-                assert remotes == [remote1]
-
diff --git a/teuthology/test/test_config.py b/teuthology/test/test_config.py
deleted file mode 100644 (file)
index 2620008..0000000
+++ /dev/null
@@ -1,189 +0,0 @@
-import pytest
-
-from teuthology import config
-
-
-class TestYamlConfig(object):
-    def setup_method(self):
-        self.test_class = config.YamlConfig
-
-    def test_set_multiple(self):
-        conf_obj = self.test_class()
-        conf_obj.foo = 'foo'
-        conf_obj.bar = 'bar'
-        assert conf_obj.foo == 'foo'
-        assert conf_obj.bar == 'bar'
-        assert conf_obj.to_dict()['foo'] == 'foo'
-
-    def test_from_dict(self):
-        in_dict = dict(foo='bar')
-        conf_obj = self.test_class.from_dict(in_dict)
-        assert conf_obj.foo == 'bar'
-
-    def test_contains(self):
-        in_dict = dict(foo='bar')
-        conf_obj = self.test_class.from_dict(in_dict)
-        conf_obj.bar = "foo"
-        assert "bar" in conf_obj
-        assert "foo" in conf_obj
-        assert "baz" not in conf_obj
-
-    def test_to_dict(self):
-        in_dict = dict(foo='bar')
-        conf_obj = self.test_class.from_dict(in_dict)
-        assert conf_obj.to_dict() == in_dict
-
-    def test_from_str(self):
-        in_str = "foo: bar"
-        conf_obj = self.test_class.from_str(in_str)
-        assert conf_obj.foo == 'bar'
-
-    def test_to_str(self):
-        in_str = "foo: bar"
-        conf_obj = self.test_class.from_str(in_str)
-        assert conf_obj.to_str() == in_str
-
-    def test_update(self):
-        conf_obj = self.test_class(dict())
-        conf_obj.foo = 'foo'
-        conf_obj.bar = 'bar'
-        conf_obj.update(dict(bar='baz'))
-        assert conf_obj.foo == 'foo'
-        assert conf_obj.bar == 'baz'
-
-    def test_delattr(self):
-        conf_obj = self.test_class()
-        conf_obj.foo = 'bar'
-        assert conf_obj.foo == 'bar'
-        del conf_obj.foo
-        assert conf_obj.foo is None
-
-    def test_assignment(self):
-        conf_obj = self.test_class()
-        conf_obj["foo"] = "bar"
-        assert conf_obj["foo"] == "bar"
-        assert conf_obj.foo == "bar"
-
-    def test_used_with_update(self):
-        d = dict()
-        conf_obj = self.test_class.from_dict({"foo": "bar"})
-        d.update(conf_obj)
-        assert d["foo"] == "bar"
-
-    def test_get(self):
-        conf_obj = self.test_class()
-        assert conf_obj.get('foo') is None
-        assert conf_obj.get('foo', 'bar') == 'bar'
-        conf_obj.foo = 'baz'
-        assert conf_obj.get('foo') == 'baz'
-
-
-class TestTeuthologyConfig(TestYamlConfig):
-    def setup_method(self):
-        self.test_class = config.TeuthologyConfig
-
-    def test_get_ceph_git_base_default(self):
-        conf_obj = self.test_class()
-        conf_obj.yaml_path = ''
-        conf_obj.load()
-        assert conf_obj.ceph_git_base_url == "https://github.com/ceph/"
-
-    def test_set_ceph_git_base_via_private(self):
-        conf_obj = self.test_class()
-        conf_obj._conf['ceph_git_base_url'] = \
-            "git://git.ceph.com/"
-        assert conf_obj.ceph_git_base_url == "git://git.ceph.com/"
-
-    def test_get_reserve_machines_default(self):
-        conf_obj = self.test_class()
-        conf_obj.yaml_path = ''
-        conf_obj.load()
-        assert conf_obj.reserve_machines == 5
-
-    def test_set_reserve_machines_via_private(self):
-        conf_obj = self.test_class()
-        conf_obj._conf['reserve_machines'] = 2
-        assert conf_obj.reserve_machines == 2
-
-    def test_set_nonstandard(self):
-        conf_obj = self.test_class()
-        conf_obj.something = 'something else'
-        assert conf_obj.something == 'something else'
-
-
-class TestJobConfig(TestYamlConfig):
-    def setup_method(self):
-        self.test_class = config.JobConfig
-
-
-class TestFakeNamespace(TestYamlConfig):
-    def setup_method(self):
-        self.test_class = config.FakeNamespace
-
-    def test_docopt_dict(self):
-        """
-        Tests if a dict in the format that docopt returns can
-        be parsed correctly.
-        """
-        d = {
-            "--verbose": True,
-            "--an-option": "some_option",
-            "<an_arg>": "the_arg",
-            "something": "some_thing",
-        }
-        conf_obj = self.test_class(d)
-        assert conf_obj.verbose
-        assert conf_obj.an_option == "some_option"
-        assert conf_obj.an_arg == "the_arg"
-        assert conf_obj.something == "some_thing"
-
-    def test_config(self):
-        """
-        Tests that a teuthology_config property is automatically added
-        to the conf_obj
-        """
-        conf_obj = self.test_class(dict(foo="bar"))
-        assert conf_obj["foo"] == "bar"
-        assert conf_obj.foo == "bar"
-        assert conf_obj.teuthology_config.get("fake key") is None
-
-    def test_getattr(self):
-        conf_obj = self.test_class.from_dict({"foo": "bar"})
-        result = getattr(conf_obj, "not_there", "default")
-        assert result == "default"
-        result = getattr(conf_obj, "foo")
-        assert result == "bar"
-
-    def test_none(self):
-        conf_obj = self.test_class.from_dict(dict(null=None))
-        assert conf_obj.null is None
-
-    def test_delattr(self):
-        conf_obj = self.test_class()
-        conf_obj.foo = 'bar'
-        assert conf_obj.foo == 'bar'
-        del conf_obj.foo
-        with pytest.raises(AttributeError):
-            conf_obj.foo
-
-    def test_to_str(self):
-        in_str = "foo: bar"
-        conf_obj = self.test_class.from_str(in_str)
-        assert conf_obj.to_str() == "{'foo': 'bar'}"
-
-    def test_multiple_access(self):
-        """
-        Test that config.config and FakeNamespace.teuthology_config reflect
-        each others' modifications
-        """
-        in_str = "foo: bar"
-        conf_obj = self.test_class.from_str(in_str)
-        assert config.config.get('test_key_1') is None
-        assert conf_obj.teuthology_config.get('test_key_1') is None
-        config.config.test_key_1 = 'test value'
-        assert conf_obj.teuthology_config['test_key_1'] == 'test value'
-
-        assert config.config.get('test_key_2') is None
-        assert conf_obj.teuthology_config.get('test_key_2') is None
-        conf_obj.teuthology_config['test_key_2'] = 'test value'
-        assert config.config['test_key_2'] == 'test value'
diff --git a/teuthology/test/test_contextutil.py b/teuthology/test/test_contextutil.py
deleted file mode 100644 (file)
index 980415e..0000000
+++ /dev/null
@@ -1,97 +0,0 @@
-from pytest import raises
-from teuthology import contextutil
-from logging import ERROR
-
-
-class TestSafeWhile(object):
-
-    def setup_method(self):
-        contextutil.log.setLevel(ERROR)
-        self.fake_sleep = lambda s: True
-        self.s_while = contextutil.safe_while
-
-    def test_6_5_10_deal(self):
-        with raises(contextutil.MaxWhileTries):
-            with self.s_while(_sleeper=self.fake_sleep) as proceed:
-                while proceed():
-                    pass
-
-    def test_6_0_1_deal(self):
-        with raises(contextutil.MaxWhileTries) as error:
-            with self.s_while(
-                tries=1,
-                _sleeper=self.fake_sleep
-            ) as proceed:
-                while proceed():
-                    pass
-
-        assert 'waiting for 6 seconds' in str(error)
-
-    def test_1_0_10_deal(self):
-        with raises(contextutil.MaxWhileTries) as error:
-            with self.s_while(
-                sleep=1,
-                _sleeper=self.fake_sleep
-            ) as proceed:
-                while proceed():
-                    pass
-
-        assert 'waiting for 10 seconds' in str(error)
-
-    def test_6_1_10_deal(self):
-        with raises(contextutil.MaxWhileTries) as error:
-            with self.s_while(
-                increment=1,
-                _sleeper=self.fake_sleep
-            ) as proceed:
-                while proceed():
-                    pass
-
-        assert 'waiting for 105 seconds' in str(error)
-
-    def test_timeout(self):
-        # series of sleep, increment, timeout params to test
-        params = [(10, 0, 100),
-                  (1, 2, 30),
-                  (10, 0.5, 100),
-                  (2, 0, 5),
-                  (2, 3, 5),
-                  (10, 0, 15),
-                  (20, 10, 60)]
-        for sleep, increment, timeout in params:
-            print("trying ", sleep, increment, timeout)
-            with raises(contextutil.MaxWhileTries) as error:
-                with self.s_while(
-                        sleep=sleep,
-                        increment=increment,
-                        timeout=timeout,
-                        _sleeper=self.fake_sleep
-                ) as proceed:
-                    while proceed():
-                        pass
-
-            assert 'waiting for {timeout}'.format(timeout=timeout) in str(error)
-
-    def test_action(self):
-        with raises(contextutil.MaxWhileTries) as error:
-            with self.s_while(
-                action='doing the thing',
-                _sleeper=self.fake_sleep
-            ) as proceed:
-                while proceed():
-                    pass
-
-        assert "'doing the thing' reached maximum tries" in str(error)
-
-    def test_no_raise(self):
-        with self.s_while(_raise=False, _sleeper=self.fake_sleep) as proceed:
-            while proceed():
-                pass
-
-        assert True
-
-    def test_tries(self):
-        attempts = 0
-        with self.s_while(tries=-1, _sleeper=self.fake_sleep) as proceed:
-            while attempts < 100 and proceed():
-                attempts += 1
diff --git a/teuthology/test/test_describe_tests.py b/teuthology/test/test_describe_tests.py
deleted file mode 100644 (file)
index 1363553..0000000
+++ /dev/null
@@ -1,316 +0,0 @@
-# -*- coding: utf-8 -*-
-import pytest
-
-from teuthology.test.fake_fs import make_fake_fstools
-from teuthology.describe_tests import (tree_with_info, extract_info,
-                                       get_combinations)
-from teuthology.exceptions import ParseError
-from mock import MagicMock, patch
-
-realistic_fs = {
-    'basic': {
-        '%': None,
-        'base': {
-            'install.yaml':
-            """meta:
-- desc: install ceph
-install:
-"""
-        },
-        'clusters': {
-            'fixed-1.yaml':
-            """meta:
-- desc: single node cluster
-roles:
-- [osd.0, osd.1, osd.2, mon.a, mon.b, mon.c, client.0]
-""",
-            'fixed-2.yaml':
-            """meta:
-- desc: couple node cluster
-roles:
-- [osd.0, osd.1, osd.2, mon.a, mon.b, mon.c]
-- [client.0]
-""",
-            'fixed-3.yaml':
-            """meta:
-- desc: triple node cluster
-roles:
-- [osd.0, osd.1, osd.2, mon.a, mon.b, mon.c]
-- [client.0]
-- [client.1]
-"""
-        },
-        'workloads': {
-            'rbd_api_tests_old_format.yaml':
-            """meta:
-- desc: c/c++ librbd api tests with format 1 images
-  rbd_features: none
-overrides:
-  ceph:
-    conf:
-      client:
-        rbd default format: 1
-tasks:
-- workunit:
-    env:
-      RBD_FEATURES: 0
-    clients:
-      client.0:
-        - rbd/test_librbd.sh
-""",
-            'rbd_api_tests.yaml':
-            """meta:
-- desc: c/c++ librbd api tests with default settings
-  rbd_features: default
-tasks:
-- workunit:
-    clients:
-      client.0:
-        - rbd/test_librbd.sh
-""",
-        },
-    },
-}
-
-
-expected_tree = """├── %
-├── base
-│   â””── install.yaml
-├── clusters
-│   â”œâ”€â”€ fixed-1.yaml
-│   â”œâ”€â”€ fixed-2.yaml
-│   â””── fixed-3.yaml
-└── workloads
-    â”œâ”€â”€ rbd_api_tests.yaml
-    â””── rbd_api_tests_old_format.yaml""".split('\n')
-
-
-expected_facets = [
-    '',
-    '',
-    'base',
-    '',
-    'clusters',
-    'clusters',
-    'clusters',
-    '',
-    'workloads',
-    'workloads',
-]
-
-
-expected_desc = [
-    '',
-    '',
-    'install ceph',
-    '',
-    'single node cluster',
-    'couple node cluster',
-    'triple node cluster',
-    '',
-    'c/c++ librbd api tests with default settings',
-    'c/c++ librbd api tests with format 1 images',
-]
-
-
-expected_rbd_features = [
-    '',
-    '',
-    '',
-    '',
-    '',
-    '',
-    '',
-    '',
-    'default',
-    'none',
-]
-
-
-class TestDescribeTests(object):
-
-    def setup_method(self):
-        self.mocks = dict()
-        self.patchers = dict()
-        exists, listdir, isfile, isdir, open = make_fake_fstools(realistic_fs)
-        for ppoint, fn in {
-             'os.listdir': listdir,
-             'os.path.isdir': isdir,
-             'teuthology.describe_tests.open': open,
-             'builtins.open': open,
-             'os.path.exists': exists,
-             'os.path.isfile': isfile,
-         }.items():
-            mockobj = MagicMock()
-            patcher = patch(ppoint, mockobj)
-            mockobj.side_effect = fn
-            patcher.start()
-            self.mocks[ppoint] = mockobj
-            self.patchers[ppoint] = patcher
-
-    def stop_patchers(self):
-        for patcher in self.patchers.values():
-            patcher.stop()
-
-    def teardown_method(self):
-        self.stop_patchers()
-
-    @staticmethod
-    def assert_expected_combo_headers(headers):
-        assert headers == (['subsuite depth 0'] +
-                           sorted(set(filter(bool, expected_facets))))
-
-    def test_no_filters(self):
-        rows = tree_with_info('basic', [], False, '', [])
-        assert rows == [[x] for x in expected_tree]
-
-    def test_single_filter(self):
-        rows = tree_with_info('basic', ['desc'], False, '', [])
-        assert rows == [list(_) for _ in zip(expected_tree, expected_desc)]
-
-        rows = tree_with_info('basic', ['rbd_features'], False, '', [])
-        assert rows == [list(_) for _ in zip(expected_tree, expected_rbd_features)]
-
-    def test_single_filter_with_facets(self):
-        rows = tree_with_info('basic', ['desc'], True, '', [])
-        assert rows == [list(_) for _ in zip(expected_tree, expected_facets,
-                                     expected_desc)]
-
-        rows = tree_with_info('basic', ['rbd_features'], True, '', [])
-        assert rows == [list(_) for _ in zip(expected_tree, expected_facets,
-                                     expected_rbd_features)]
-
-    def test_no_matching(self):
-        rows = tree_with_info('basic', ['extra'], False, '', [])
-        assert rows == [list(_) for _ in zip(expected_tree, [''] * len(expected_tree))]
-
-        rows = tree_with_info('basic', ['extra'], True, '', [])
-        assert rows == [list(_) for _ in zip(expected_tree, expected_facets,
-                                     [''] * len(expected_tree))]
-
-    def test_multiple_filters(self):
-        rows = tree_with_info('basic', ['desc', 'rbd_features'], False, '', [])
-        assert rows == [list(_) for _ in zip(expected_tree,
-                                     expected_desc,
-                                     expected_rbd_features)]
-
-        rows = tree_with_info('basic', ['rbd_features', 'desc'], False, '', [])
-        assert rows == [list(_) for _ in zip(expected_tree,
-                                     expected_rbd_features,
-                                     expected_desc)]
-
-    def test_multiple_filters_with_facets(self):
-        rows = tree_with_info('basic', ['desc', 'rbd_features'], True, '', [])
-        assert rows == [list(_) for _ in zip(expected_tree,
-                                     expected_facets,
-                                     expected_desc,
-                                     expected_rbd_features)]
-
-        rows = tree_with_info('basic', ['rbd_features', 'desc'], True, '', [])
-        assert rows == [list(_) for _ in zip(expected_tree,
-                                     expected_facets,
-                                     expected_rbd_features,
-                                     expected_desc)]
-
-    def test_combinations_only_facets(self):
-        headers, rows = get_combinations('basic',
-                fields=[], subset=None, limit=1,
-                filter_in=None, filter_out=None, filter_all=None,
-                include_facet=True)
-        self.assert_expected_combo_headers(headers)
-        assert rows == [['basic', 'install', 'fixed-1', 'rbd_api_tests']]
-
-    def test_combinations_desc_features(self):
-        headers, rows = get_combinations('basic',
-                fields=['desc', 'rbd_features'], subset=None, limit=1,
-                filter_in=None, filter_out=None, filter_all=None,
-                include_facet=False)
-        assert headers == ['desc', 'rbd_features']
-        descriptions = '\n'.join([
-            'install ceph',
-            'single node cluster',
-            'c/c++ librbd api tests with default settings',
-        ])
-        assert rows == [[descriptions, 'default']]
-
-    def test_combinations_filter_in(self):
-        headers, rows = get_combinations('basic',
-                fields=[], subset=None, limit=0,
-                filter_in=['old_format'], filter_out=None, filter_all=None,
-                include_facet=True)
-        self.assert_expected_combo_headers(headers)
-        assert rows == [
-            ['basic', 'install', 'fixed-1', 'rbd_api_tests_old_format'],
-            ['basic', 'install', 'fixed-2', 'rbd_api_tests_old_format'],
-            ['basic', 'install', 'fixed-3', 'rbd_api_tests_old_format'],
-        ]
-
-    def test_combinations_filter_out(self):
-        headers, rows = get_combinations('basic',
-                fields=[], subset=None, limit=0,
-                filter_in=None, filter_out=['old_format'], filter_all=None,
-                include_facet=True)
-        self.assert_expected_combo_headers(headers)
-        assert rows == [
-            ['basic', 'install', 'fixed-1', 'rbd_api_tests'],
-            ['basic', 'install', 'fixed-2', 'rbd_api_tests'],
-            ['basic', 'install', 'fixed-3', 'rbd_api_tests'],
-        ]
-
-    def test_combinations_filter_all(self):
-        headers, rows = get_combinations('basic',
-                fields=[], subset=None, limit=0,
-                filter_in=None, filter_out=None,
-                filter_all=['fixed-2', 'old_format'],
-                include_facet=True)
-        self.assert_expected_combo_headers(headers)
-        assert rows == [
-            ['basic', 'install', 'fixed-2', 'rbd_api_tests_old_format']
-        ]
-
-
-@patch('teuthology.describe_tests.open')
-@patch('os.path.isdir')
-def test_extract_info_dir(m_isdir, m_open):
-    simple_fs = {'a': {'b.yaml': 'meta: [{foo: c}]'}}
-    _, _, _, m_isdir.side_effect, m_open.side_effect = \
-        make_fake_fstools(simple_fs)
-    info = extract_info('a', [])
-    assert info == {}
-
-    info = extract_info('a', ['foo', 'bar'])
-    assert info == {'foo': '', 'bar': ''}
-
-    info = extract_info('a/b.yaml', ['foo', 'bar'])
-    assert info == {'foo': 'c', 'bar': ''}
-
-
-@patch('teuthology.describe_tests.open')
-@patch('os.path.isdir')
-def check_parse_error(fs, m_isdir, m_open):
-    _, _, _, m_isdir.side_effect, m_open.side_effect = make_fake_fstools(fs)
-    with pytest.raises(ParseError):
-        a = extract_info('a.yaml', ['a'])
-        raise Exception(str(a))
-
-
-def test_extract_info_too_many_elements():
-    check_parse_error({'a.yaml': 'meta: [{a: b}, {b: c}]'})
-
-
-def test_extract_info_not_a_list():
-    check_parse_error({'a.yaml': 'meta: {a: b}'})
-
-
-def test_extract_info_not_a_dict():
-    check_parse_error({'a.yaml': 'meta: [[a, b]]'})
-
-
-@patch('teuthology.describe_tests.open')
-@patch('os.path.isdir')
-def test_extract_info_empty_file(m_isdir, m_open):
-    simple_fs = {'a.yaml': ''}
-    _, _, _, m_isdir.side_effect, m_open.side_effect = \
-        make_fake_fstools(simple_fs)
-    info = extract_info('a.yaml', [])
-    assert info == {}
diff --git a/teuthology/test/test_email_sleep_before_teardown.py b/teuthology/test/test_email_sleep_before_teardown.py
deleted file mode 100644 (file)
index 60fcd24..0000000
+++ /dev/null
@@ -1,81 +0,0 @@
-from humanfriendly import format_timespan
-from mock import Mock, patch
-from pytest import mark
-from teuthology.config import config
-from teuthology.run_tasks import build_email_body as email_body
-from textwrap import dedent
-
-class TestSleepBeforeTeardownEmail(object):
-    def setup_method(self):
-        config.results_ui_server = "http://example.com/"
-        config.archive_server = "http://qa-proxy.ceph.com/teuthology/"
-
-    @mark.parametrize(
-        ['status', 'owner', 'suite_name', 'run_name', 'job_id', 'dura'],
-        [
-            [
-                'pass',
-                'noreply@host',
-                'dummy',
-                'run-name',
-                123,
-                3600,
-            ],
-            [
-                'fail',
-                'noname',
-                'yummy',
-                'next-run',
-                1000,
-                99999,
-            ],
-        ]
-    )
-    @patch("teuthology.run_tasks.time.time")
-    def test_sleep_before_teardown_email_body(self, m_time, status, owner,
-                                              suite_name, run_name, job_id, dura):
-        ctx = Mock()
-        archive_path='archive/path'
-        archive_dir='/archive/dir'
-        date_sec=3661
-        date_str='1970-01-01 01:01:01'
-        m_time.return_value=float(date_sec)
-        duration_sec=dura
-        duration_str=format_timespan(duration_sec)
-        ref_body=dedent("""
-            Teuthology job {run}/{job} has fallen asleep at {date} for {duration_str}
-
-            Owner: {owner}
-            Suite Name: {suite}
-            Sleep Date: {date}
-            Sleep Time: {duration_sec} seconds ({duration_str})
-            Job Info: http://example.com/{run}/
-            Job Logs: http://qa-proxy.ceph.com/teuthology/path/{job}/
-            Task Stack: a/b/c
-            Current Status: {status}"""
-            .format(duration_sec=duration_sec, duration_str=duration_str,
-                owner=owner, suite=suite_name, run=run_name,
-                job=job_id, status=status, date=date_str))
-        print(ref_body)
-        ctx.config = dict(
-            archive_path=archive_path,
-            job_id=job_id,
-            suite=suite_name,
-            )
-        if status == 'pass':
-            ctx.summary = dict(
-                success=True,
-            )
-        elif status == 'fail':
-            ctx.summary = dict(
-                success=False,
-            )
-        else:
-            ctx.summary = dict()
-
-        ctx.owner   = owner
-        ctx.name    = run_name
-        ctx.archive_dir = archive_dir
-        tasks = [('a', None), ('b', None), ('c', None)]
-        (subj, body) = email_body(ctx, tasks, dura)
-        assert body == ref_body.lstrip('\n')
diff --git a/teuthology/test/test_exit.py b/teuthology/test/test_exit.py
deleted file mode 100644 (file)
index e8c22bf..0000000
+++ /dev/null
@@ -1,90 +0,0 @@
-import os
-import random
-
-from unittest.mock import patch, Mock
-
-from teuthology import exit
-
-
-class TestExiter(object):
-    klass = exit.Exiter
-
-    def setup_method(self):
-        self.pid = os.getpid()
-
-        # Below, we patch os.kill() in such a way that the first time it is
-        # invoked it does actually send the signal. Any subsequent invocation
-        # won't send any signal - this is so we don't kill the process running
-        # our unit tests!
-        self.patcher_kill = patch(
-            'teuthology.exit.os.kill',
-            wraps=os.kill,
-        )
-
-        #Keep a copy of the unpatched kill and call this in place of os.kill
-        #In the Exiter objects, the os.kill calls are patched.
-        #So the call_count should be 1.
-        self.kill_unpatched = os.kill
-        self.m_kill = self.patcher_kill.start()
-
-        def m_kill_unwrap(pid, sig):
-            # Setting return_value of a mocked object disables the wrapping
-            if self.m_kill.call_count > 1:
-                self.m_kill.return_value = None
-
-        self.m_kill.side_effect = m_kill_unwrap
-
-    def teardown_method(self):
-        self.patcher_kill.stop()
-        del self.m_kill
-
-    def test_basic(self):
-        sig = 15
-        obj = self.klass()
-        m_func = Mock()
-        obj.add_handler(sig, m_func)
-        assert len(obj.handlers) == 1
-        self.kill_unpatched(self.pid, sig)
-        assert m_func.call_count == 1
-        assert self.m_kill.call_count == 1
-        for arg_list in self.m_kill.call_args_list:
-            assert arg_list[0] == (self.pid, sig)
-
-    def test_remove_handlers(self):
-        sig = [1, 15]
-        send_sig = random.choice(sig)
-        n = 3
-        obj = self.klass()
-        handlers = list()
-        for i in range(n):
-            m_func = Mock(name="handler %s" % i)
-            handlers.append(obj.add_handler(sig, m_func))
-        assert obj.handlers == handlers
-        for handler in handlers:
-            handler.remove()
-        assert obj.handlers == list()
-        self.kill_unpatched(self.pid, send_sig)
-        assert self.m_kill.call_count == 1
-        for handler in handlers:
-            assert handler.func.call_count == 0
-
-    def test_n_handlers(self, n=10, sig=11):
-        if isinstance(sig, int):
-            send_sig = sig
-        else:
-            send_sig = random.choice(sig)
-        obj = self.klass()
-        handlers = list()
-        for i in range(n):
-            m_func = Mock(name="handler %s" % i)
-            handlers.append(obj.add_handler(sig, m_func))
-        assert obj.handlers == handlers
-        self.kill_unpatched(self.pid, send_sig)
-        for i in range(n):
-            assert handlers[i].func.call_count == 1
-        assert self.m_kill.call_count == 1
-        for arg_list in self.m_kill.call_args_list:
-            assert arg_list[0] == (self.pid, send_sig)
-
-    def test_multiple_signals(self):
-        self.test_n_handlers(n=3, sig=[1, 6, 11, 15])
diff --git a/teuthology/test/test_get_distro.py b/teuthology/test/test_get_distro.py
deleted file mode 100644 (file)
index b03ba7b..0000000
+++ /dev/null
@@ -1,47 +0,0 @@
-from teuthology.misc import get_distro
-
-
-class Mock:
-    pass
-
-
-class TestGetDistro(object):
-
-    def setup_method(self):
-        self.fake_ctx = Mock()
-        self.fake_ctx.config = {}
-        # os_type in ctx will always default to None
-        self.fake_ctx.os_type = None
-
-    def test_default_distro(self):
-        distro = get_distro(self.fake_ctx)
-        assert distro == 'ubuntu'
-
-    def test_argument(self):
-        # we don't want fake_ctx to have a config
-        self.fake_ctx = Mock()
-        self.fake_ctx.os_type = 'centos'
-        distro = get_distro(self.fake_ctx)
-        assert distro == 'centos'
-
-    def test_teuth_config(self):
-        self.fake_ctx.config = {'os_type': 'fedora'}
-        distro = get_distro(self.fake_ctx)
-        assert distro == 'fedora'
-
-    def test_argument_takes_precedence(self):
-        self.fake_ctx.config = {'os_type': 'fedora'}
-        self.fake_ctx.os_type = "centos"
-        distro = get_distro(self.fake_ctx)
-        assert distro == 'centos'
-
-    def test_no_config_or_os_type(self):
-        self.fake_ctx = Mock()
-        self.fake_ctx.os_type = None
-        distro = get_distro(self.fake_ctx)
-        assert distro == 'ubuntu'
-
-    def test_config_os_type_is_none(self):
-        self.fake_ctx.config["os_type"] = None
-        distro = get_distro(self.fake_ctx)
-        assert distro == 'ubuntu'
diff --git a/teuthology/test/test_get_distro_version.py b/teuthology/test/test_get_distro_version.py
deleted file mode 100644 (file)
index 8a77e39..0000000
+++ /dev/null
@@ -1,47 +0,0 @@
-from teuthology.misc import get_distro_version
-
-
-class Mock:
-    pass
-
-
-class TestGetDistroVersion(object):
-
-    def setup_method(self):
-        self.fake_ctx = Mock()
-        self.fake_ctx.config = {}
-        self.fake_ctx_noarg = Mock()
-        self.fake_ctx_noarg.config = {}
-        self.fake_ctx_noarg.os_version = None
-        self.fake_ctx.os_type = None
-        self.fake_ctx_noarg.os_type = None
-
-    def test_default_distro_version(self):
-        # Default distro is ubuntu, default version of ubuntu is 20.04
-        self.fake_ctx.os_version = None
-        distroversion = get_distro_version(self.fake_ctx)
-        assert distroversion == '22.04'
-
-    def test_argument_version(self):
-        self.fake_ctx.os_version = '13.04'
-        distroversion = get_distro_version(self.fake_ctx)
-        assert distroversion == '13.04'
-
-    def test_teuth_config_version(self):
-        #Argument takes precidence.
-        self.fake_ctx.os_version = '13.04'
-        self.fake_ctx.config = {'os_version': '13.10'}
-        distroversion = get_distro_version(self.fake_ctx)
-        assert distroversion == '13.04'
-
-    def test_teuth_config_noarg_version(self):
-        self.fake_ctx_noarg.config = {'os_version': '13.04'}
-        distroversion = get_distro_version(self.fake_ctx_noarg)
-        assert distroversion == '13.04'
-
-    def test_no_teuth_config(self):
-        self.fake_ctx = Mock()
-        self.fake_ctx.os_type = None
-        self.fake_ctx.os_version = '13.04'
-        distroversion = get_distro_version(self.fake_ctx)
-        assert distroversion == '13.04'
diff --git a/teuthology/test/test_get_multi_machine_types.py b/teuthology/test/test_get_multi_machine_types.py
deleted file mode 100644 (file)
index 32a6b02..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-from teuthology import misc as teuthology
-
-class Mock: pass
-
-class TestGetMultiMachineTypes(object):
-
-    def test_space(self):
-        give = 'burnupi plana vps'
-        expect = ['burnupi','plana','vps']
-        assert teuthology.get_multi_machine_types(give) == expect
-
-    def test_tab(self):
-        give = 'burnupi        plana   vps'
-        expect = ['burnupi','plana','vps']
-        assert teuthology.get_multi_machine_types(give) == expect
-
-    def test_comma(self):
-        give = 'burnupi,plana,vps'
-        expect = ['burnupi','plana','vps']
-        assert teuthology.get_multi_machine_types(give) == expect
-
-    def test_single(self):
-        give = 'burnupi'
-        expect = ['burnupi']
-        assert teuthology.get_multi_machine_types(give) == expect
-
-
diff --git a/teuthology/test/test_imports.py b/teuthology/test/test_imports.py
deleted file mode 100644 (file)
index cb25c8d..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-import importlib
-import pytest
-import sys
-
-from pathlib import Path
-from typing import List
-
-root = Path("./teuthology")
-
-
-def find_modules() -> List[str]:
-    modules = []
-    for path in root.rglob("*.py"):
-        if path.name.startswith("test_"):
-            continue
-        if "-" in path.name:
-            continue
-        if path.name == "__init__.py":
-            path = path.parent
-
-        path_name = str(path).replace("/", ".")
-        if path_name.endswith(".py"):
-            path_name = path_name[:-3]
-        modules.append(path_name)
-    return sorted(modules)
-
-
-@pytest.mark.parametrize("module", find_modules())
-def test_import_modules(module):
-    importlib.import_module(module)
-    assert module in sys.modules
diff --git a/teuthology/test/test_job_status.py b/teuthology/test/test_job_status.py
deleted file mode 100644 (file)
index ee1b764..0000000
+++ /dev/null
@@ -1,60 +0,0 @@
-from teuthology import job_status
-
-
-class TestJobStatus(object):
-    def test_get_only_success_true(self):
-        summary = dict(success=True)
-        status = job_status.get_status(summary)
-        assert status == 'pass'
-
-    def test_get_only_success_false(self):
-        summary = dict(success=False)
-        status = job_status.get_status(summary)
-        assert status == 'fail'
-
-    def test_get_status_pass(self):
-        summary = dict(status='pass')
-        status = job_status.get_status(summary)
-        assert status == 'pass'
-
-    def test_get_status_fail(self):
-        summary = dict(status='fail')
-        status = job_status.get_status(summary)
-        assert status == 'fail'
-
-    def test_get_status_dead(self):
-        summary = dict(status='dead')
-        status = job_status.get_status(summary)
-        assert status == 'dead'
-
-    def test_get_status_none(self):
-        summary = dict()
-        status = job_status.get_status(summary)
-        assert status is None
-
-    def test_set_status_pass(self):
-        summary = dict()
-        job_status.set_status(summary, 'pass')
-        assert summary == dict(status='pass', success=True)
-
-    def test_set_status_dead(self):
-        summary = dict()
-        job_status.set_status(summary, 'dead')
-        assert summary == dict(status='dead', success=False)
-
-    def test_set_then_get_status_dead(self):
-        summary = dict()
-        job_status.set_status(summary, 'dead')
-        status = job_status.get_status(summary)
-        assert status == 'dead'
-
-    def test_set_status_none(self):
-        summary = dict()
-        job_status.set_status(summary, None)
-        assert summary == dict()
-
-    def test_legacy_fail(self):
-        summary = dict(success=True)
-        summary['success'] = False
-        status = job_status.get_status(summary)
-        assert status == 'fail'
diff --git a/teuthology/test/test_kill.py b/teuthology/test/test_kill.py
deleted file mode 100644 (file)
index 21ec997..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-from unittest.mock import patch
-
-from teuthology.kill import find_targets
-
-
-class TestFindTargets(object):
-    """ Tests for teuthology.kill.find_targets """
-
-    @patch('teuthology.kill.report.ResultsReporter.get_jobs')
-    def test_missing_run_find_targets(self, m_get_jobs):
-        m_get_jobs.return_value = [] 
-        run_targets = find_targets("run-name")
-        assert run_targets == {}
-    
-    @patch('teuthology.kill.report.ResultsReporter.get_jobs')
-    def test_missing_job_find_targets(self, m_get_jobs):
-        m_get_jobs.return_value = {} 
-        job_targets = find_targets("run-name", "3")
-        assert job_targets == {}
-
-    @patch('teuthology.kill.report.ResultsReporter.get_jobs')
-    def test_missing_run_targets_find_targets(self, m_get_jobs):
-        m_get_jobs.return_value = [{"targets": None, "status": "waiting"}] 
-        run_targets = find_targets("run-name")
-        assert run_targets == {}
-    
-    @patch('teuthology.kill.report.ResultsReporter.get_jobs')
-    def test_missing_job_targets_find_targets(self, m_get_jobs):
-        m_get_jobs.return_value = {"targets": None} 
-        job_targets = find_targets("run-name", "3")
-        assert job_targets == {}
-
-    @patch('teuthology.kill.report.ResultsReporter.get_jobs')
-    def test_run_find_targets(self, m_get_jobs):
-        m_get_jobs.return_value = [{"targets": {"node1": ""}, "status": "running"}]
-        run_targets = find_targets("run-name")
-        assert run_targets == {"node1": ""}
-        m_get_jobs.return_value = [{"targets": {"node1": ""}}]
-        run_targets = find_targets("run-name")
-        assert run_targets == {}
-
-    @patch('teuthology.kill.report.ResultsReporter.get_jobs')
-    def test_job_find_targets(self, m_get_jobs):
-        m_get_jobs.return_value = {"targets": {"node1": ""}}
-        job_targets = find_targets("run-name", "3")
-        assert job_targets == {"node1": ""}
diff --git a/teuthology/test/test_ls.py b/teuthology/test/test_ls.py
deleted file mode 100644 (file)
index 631dcfd..0000000
+++ /dev/null
@@ -1,48 +0,0 @@
-import pytest
-
-from unittest.mock import patch, Mock
-
-from teuthology import ls
-
-
-class TestLs(object):
-    """ Tests for teuthology.ls """
-
-    @patch('os.path.isdir')
-    @patch('os.listdir')
-    def test_get_jobs(self, m_listdir, m_isdir):
-        m_listdir.return_value = ["1", "a", "3"]
-        m_isdir.return_value = True
-        results = ls.get_jobs("some/archive/dir")
-        assert results == ["1", "3"]
-
-    @patch("yaml.safe_load_all")
-    @patch("teuthology.ls.get_jobs")
-    def test_ls(self, m_get_jobs,  m_safe_load_all):
-        m_get_jobs.return_value = ["1", "2"]
-        m_safe_load_all.return_value = [{"failure_reason": "reasons"}]
-        ls.ls("some/archive/div", True)
-
-    @patch("teuthology.ls.open")
-    @patch("teuthology.ls.get_jobs")
-    def test_ls_ioerror(self, m_get_jobs, m_open):
-        m_get_jobs.return_value = ["1", "2"]
-        m_open.side_effect = IOError()
-        with pytest.raises(IOError):
-            ls.ls("some/archive/dir", True)
-
-    @patch("teuthology.ls.open")
-    @patch("os.popen")
-    @patch("os.path.isdir")
-    @patch("os.path.isfile")
-    def test_print_debug_info(self, m_isfile, m_isdir, m_popen, m_open):
-        m_isfile.return_value = True
-        m_isdir.return_value = True
-        m_popen.return_value = Mock()
-        cmdline = Mock()
-        cmdline.find = Mock(return_value=0)
-        m1 = Mock()
-        m2 = Mock()
-        m2.read = Mock(return_value=cmdline)
-        m_open.side_effect = [m1, m2]
-        ls.print_debug_info("the_job", "job/dir", "some/archive/dir")
diff --git a/teuthology/test/test_misc.py b/teuthology/test/test_misc.py
deleted file mode 100644 (file)
index ca75336..0000000
+++ /dev/null
@@ -1,387 +0,0 @@
-import argparse
-import pytest
-import subprocess
-
-from unittest.mock import Mock, patch
-
-from teuthology import misc
-from teuthology.config import config
-from teuthology.orchestra import cluster
-from teuthology.orchestra.remote import Remote
-
-
-class FakeRemote(object):
-    pass
-
-
-def test_sh_normal(caplog):
-    assert misc.sh("/bin/echo ABC") == "ABC\n"
-    assert "truncated" not in caplog.text
-
-
-def test_sh_truncate(caplog):
-    assert misc.sh("/bin/echo -n AB ; /bin/echo C", 2) == "ABC\n"
-    assert "truncated" in caplog.text
-    assert "ABC" not in caplog.text
-
-
-def test_sh_fail(caplog):
-    with pytest.raises(subprocess.CalledProcessError) as excinfo:
-        misc.sh("/bin/echo -n AB ; /bin/echo C ; exit 111", 2)
-    assert excinfo.value.returncode == 111
-    for record in caplog.records:
-        if record.levelname == 'ERROR':
-            assert ('replay full' in record.message or
-                    'ABC\n' == record.message)
-
-def test_sh_progress(caplog):
-    assert misc.sh("echo AB ; sleep 0.1 ; /bin/echo C", 2) == "AB\nC\n"
-    records = caplog.records
-    assert ':sh: ' in records[0].message
-    assert 'AB' == records[1].message
-    assert 'C' == records[2].message
-    assert records[2].created > records[1].created
-
-
-def test_wait_until_osds_up():
-    ctx = argparse.Namespace()
-    ctx.daemons = Mock()
-    ctx.daemons.iter_daemons_of_role.return_value = list()
-    remote = Mock(spec=Remote)
-    remote.sh.return_value = 'IGNORED\n{"osds":[{"state":["up"]}]}'
-    ctx.cluster = cluster.Cluster(
-        remotes=[
-            (remote, ['osd.0', 'client.1'])
-        ],
-    )
-    with patch.multiple(
-            misc,
-            get_testdir=lambda _: "TESTDIR",
-    ):
-        misc.wait_until_osds_up(ctx, ctx.cluster, remote)
-
-
-def test_get_clients_simple():
-    ctx = argparse.Namespace()
-    remote = FakeRemote()
-    ctx.cluster = cluster.Cluster(
-        remotes=[
-            (remote, ['client.0', 'client.1'])
-        ],
-    )
-    g = misc.get_clients(ctx=ctx, roles=['client.1'])
-    got = next(g)
-    assert len(got) == 2
-    assert got[0] == ('1')
-    assert got[1] is remote
-    with pytest.raises(StopIteration):
-        next(g)
-
-
-def test_get_mon_names():
-    expected = [
-        ([['mon.a', 'osd.0', 'mon.c']], 'ceph', ['mon.a', 'mon.c']),
-        ([['ceph.mon.a', 'osd.0', 'ceph.mon.c']], 'ceph', ['ceph.mon.a', 'ceph.mon.c']),
-        ([['mon.a', 'osd.0', 'mon.c'], ['ceph.mon.b']], 'ceph', ['mon.a', 'mon.c', 'ceph.mon.b']),
-        ([['mon.a', 'osd.0', 'mon.c'], ['foo.mon.a']], 'ceph', ['mon.a', 'mon.c']),
-        ([['mon.a', 'osd.0', 'mon.c'], ['foo.mon.a']], 'foo', ['foo.mon.a']),
-    ]
-    for remote_roles, cluster_name, expected_mons in expected:
-        ctx = argparse.Namespace()
-        ctx.cluster = Mock()
-        ctx.cluster.remotes = {i: roles for i, roles in enumerate(remote_roles)}
-        mons = misc.get_mon_names(ctx, cluster_name)
-        assert expected_mons == mons
-
-
-def test_get_first_mon():
-    expected = [
-        ([['mon.a', 'osd.0', 'mon.c']], 'ceph', 'mon.a'),
-        ([['ceph.mon.a', 'osd.0', 'ceph.mon.c']], 'ceph', 'ceph.mon.a'),
-        ([['mon.a', 'osd.0', 'mon.c'], ['ceph.mon.b']], 'ceph', 'ceph.mon.b'),
-        ([['mon.a', 'osd.0', 'mon.c'], ['foo.mon.a']], 'ceph', 'mon.a'),
-        ([['foo.mon.b', 'osd.0', 'mon.c'], ['foo.mon.a']], 'foo', 'foo.mon.a'),
-    ]
-    for remote_roles, cluster_name, expected_mon in expected:
-        ctx = argparse.Namespace()
-        ctx.cluster = Mock()
-        ctx.cluster.remotes = {i: roles for i, roles in enumerate(remote_roles)}
-        mon = misc.get_first_mon(ctx, None, cluster_name)
-        assert expected_mon == mon
-
-
-def test_roles_of_type():
-    expected = [
-        (['client.0', 'osd.0', 'ceph.osd.1'], 'osd', ['0', '1']),
-        (['client.0', 'osd.0', 'ceph.osd.1'], 'client', ['0']),
-        (['foo.client.1', 'bar.client.2.3', 'baz.osd.1'], 'mon', []),
-        (['foo.client.1', 'bar.client.2.3', 'baz.osd.1'], 'client',
-         ['1', '2.3']),
-        ]
-    for roles_for_host, type_, expected_ids in expected:
-        ids = list(misc.roles_of_type(roles_for_host, type_))
-        assert ids == expected_ids
-
-
-def test_cluster_roles_of_type():
-    expected = [
-        (['client.0', 'osd.0', 'ceph.osd.1'], 'osd', 'ceph',
-         ['osd.0', 'ceph.osd.1']),
-        (['client.0', 'osd.0', 'ceph.osd.1'], 'client', 'ceph',
-         ['client.0']),
-        (['foo.client.1', 'bar.client.2.3', 'baz.osd.1'], 'mon', None, []),
-        (['foo.client.1', 'bar.client.2.3', 'baz.osd.1'], 'client', None,
-         ['foo.client.1', 'bar.client.2.3']),
-        (['foo.client.1', 'bar.client.2.3', 'baz.osd.1'], 'client', 'bar',
-         ['bar.client.2.3']),
-        ]
-    for roles_for_host, type_, cluster_, expected_roles in expected:
-        roles = list(misc.cluster_roles_of_type(roles_for_host, type_, cluster_))
-        assert roles == expected_roles
-
-
-def test_all_roles_of_type():
-    expected = [
-        ([['client.0', 'osd.0', 'ceph.osd.1'], ['bar.osd.2']],
-         'osd', ['0', '1', '2']),
-        ([['client.0', 'osd.0', 'ceph.osd.1'], ['bar.osd.2', 'baz.client.1']],
-         'client', ['0', '1']),
-        ([['foo.client.1', 'bar.client.2.3'], ['baz.osd.1']], 'mon', []),
-        ([['foo.client.1', 'bar.client.2.3'], ['baz.osd.1', 'ceph.client.bar']],
-         'client', ['1', '2.3', 'bar']),
-        ]
-    for host_roles, type_, expected_ids in expected:
-        cluster_ = Mock()
-        cluster_.remotes = dict(enumerate(host_roles))
-        ids = list(misc.all_roles_of_type(cluster_, type_))
-        assert ids == expected_ids
-
-
-def test_get_http_log_path():
-    # Fake configuration
-    archive_server = "http://example.com/server_root"
-    config.archive_server = archive_server
-    archive_dir = "/var/www/archives"
-
-    path = misc.get_http_log_path(archive_dir)
-    assert path == "http://example.com/server_root/archives/"
-
-    job_id = '12345'
-    path = misc.get_http_log_path(archive_dir, job_id)
-    assert path == "http://example.com/server_root/archives/12345/"
-
-    # Inktank configuration
-    archive_server = "http://qa-proxy.ceph.com/teuthology/"
-    config.archive_server = archive_server
-    archive_dir = "/var/lib/teuthworker/archive/teuthology-2013-09-12_11:49:50-ceph-deploy-main-testing-basic-vps"
-    job_id = 31087
-    path = misc.get_http_log_path(archive_dir, job_id)
-    assert path == "http://qa-proxy.ceph.com/teuthology/teuthology-2013-09-12_11:49:50-ceph-deploy-main-testing-basic-vps/31087/"
-
-    path = misc.get_http_log_path(archive_dir)
-    assert path == "http://qa-proxy.ceph.com/teuthology/teuthology-2013-09-12_11:49:50-ceph-deploy-main-testing-basic-vps/"
-
-
-def test_is_type():
-    is_client = misc.is_type('client')
-    assert is_client('client.0')
-    assert is_client('ceph.client.0')
-    assert is_client('foo.client.0')
-    assert is_client('foo.client.bar.baz')
-
-    with pytest.raises(ValueError):
-        is_client('')
-        is_client('client')
-    assert not is_client('foo.bar.baz')
-    assert not is_client('ceph.client')
-    assert not is_client('hadoop.main.0')
-
-
-def test_is_type_in_cluster():
-    is_c1_osd = misc.is_type('osd', 'c1')
-    with pytest.raises(ValueError):
-        is_c1_osd('')
-    assert not is_c1_osd('osd.0')
-    assert not is_c1_osd('ceph.osd.0')
-    assert not is_c1_osd('ceph.osd.0')
-    assert not is_c1_osd('c11.osd.0')
-    assert is_c1_osd('c1.osd.0')
-    assert is_c1_osd('c1.osd.999')
-
-
-def test_get_mons():
-    ips = ['1.1.1.1', '2.2.2.2', '3.3.3.3']
-    addrs = ['1.1.1.1:6789', '1.1.1.1:6790', '1.1.1.1:6791']
-
-    mons = misc.get_mons([['mon.a']], ips)
-    assert mons == {'mon.a': addrs[0]}
-
-    mons = misc.get_mons([['cluster-a.mon.foo', 'client.b'], ['osd.0']], ips)
-    assert mons == {'cluster-a.mon.foo': addrs[0]}
-
-    mons = misc.get_mons([['mon.a', 'mon.b', 'ceph.mon.c']], ips)
-    assert mons == {'mon.a': addrs[0],
-                    'mon.b': addrs[1],
-                    'ceph.mon.c': addrs[2]}
-
-    mons = misc.get_mons([['mon.a'], ['mon.b'], ['ceph.mon.c']], ips)
-    assert mons == {'mon.a': addrs[0],
-                    'mon.b': ips[1] + ':6789',
-                    'ceph.mon.c': ips[2] + ':6789'}
-
-
-def test_split_role():
-    expected = {
-        'client.0': ('ceph', 'client', '0'),
-        'foo.client.0': ('foo', 'client', '0'),
-        'bar.baz.x.y.z': ('bar', 'baz', 'x.y.z'),
-        'mds.a-s-b': ('ceph', 'mds', 'a-s-b'),
-    }
-
-    for role, expected_split in expected.items():
-        actual_split = misc.split_role(role)
-        assert actual_split == expected_split
-
-def test_update_key():
-    a = { "sha": "foo", "workunit": { "sha": "foo" }, "tasks": [{"task1": "ceph"}], "overrides": [{"sha": "foo"}] }
-    b = { "sha": "blah", "workunit": { "sha": "bar" }, "tasks": [] }
-
-    misc.update_key("sha", a, b)
-    assert a == { "sha": "blah", "workunit": { "sha": "bar" }, "tasks": [{"task1": "ceph"}], "overrides": [{"sha": "foo"}] } 
-
-class TestHostnames(object):
-    def setup_method(self):
-        config._conf = dict()
-
-    def teardown_method(self):
-        config.load()
-
-    def test_canonicalize_hostname(self):
-        host_base = 'box1'
-        result = misc.canonicalize_hostname(host_base)
-        assert result == 'ubuntu@box1.front.sepia.ceph.com'
-
-    def test_decanonicalize_hostname(self):
-        host = 'ubuntu@box1.front.sepia.ceph.com'
-        result = misc.decanonicalize_hostname(host)
-        assert result == 'box1'
-
-    def test_canonicalize_hostname_nouser(self):
-        host_base = 'box1'
-        result = misc.canonicalize_hostname(host_base, user=None)
-        assert result == 'box1.front.sepia.ceph.com'
-
-    def test_decanonicalize_hostname_nouser(self):
-        host = 'box1.front.sepia.ceph.com'
-        result = misc.decanonicalize_hostname(host)
-        assert result == 'box1'
-
-    def test_canonicalize_hostname_otherlab(self):
-        config.lab_domain = 'example.com'
-        host_base = 'box1'
-        result = misc.canonicalize_hostname(host_base)
-        assert result == 'ubuntu@box1.example.com'
-
-    def test_decanonicalize_hostname_otherlab(self):
-        config.lab_domain = 'example.com'
-        host = 'ubuntu@box1.example.com'
-        result = misc.decanonicalize_hostname(host)
-        assert result == 'box1'
-
-    def test_canonicalize_hostname_nodomain(self):
-        config.lab_domain = ''
-        host = 'box2'
-        result = misc.canonicalize_hostname(host)
-        assert result == 'ubuntu@' + host
-
-    def test_decanonicalize_hostname_nodomain(self):
-        config.lab_domain = ''
-        host = 'ubuntu@box2'
-        result = misc.decanonicalize_hostname(host)
-        assert result == 'box2'
-
-    def test_canonicalize_hostname_full_other_user(self):
-        config.lab_domain = 'example.com'
-        host = 'user1@box1.example.come'
-        result = misc.canonicalize_hostname(host)
-        assert result == 'user1@box1.example.com'
-
-    def test_decanonicalize_hostname_full_other_user(self):
-        config.lab_domain = 'example.com'
-        host = 'user1@box1.example.come'
-        result = misc.decanonicalize_hostname(host)
-        assert result == 'box1'
-
-class TestMergeConfigs(object):
-    """ Tests merge_config and deep_merge in teuthology.misc """
-
-    @patch("os.path.exists")
-    @patch("yaml.safe_load")
-    @patch("teuthology.misc.open")
-    def test_merge_configs(self, m_open, m_safe_load, m_exists):
-        """ Only tests with one yaml file being passed, mainly just to test
-            the loop logic.  The actual merge will be tested in subsequent
-            tests.
-        """
-        expected = {"a": "b", "b": "c"}
-        m_exists.return_value = True
-        m_safe_load.return_value = expected
-        result = misc.merge_configs(["path/to/config1"])
-        assert result == expected
-        m_open.assert_called_once_with("path/to/config1")
-
-    def test_merge_configs_empty(self):
-        assert misc.merge_configs([]) == {}
-
-    def test_deep_merge(self):
-        a = {"a": "b"}
-        b = {"b": "c"}
-        result = misc.deep_merge(a, b)
-        assert result == {"a": "b", "b": "c"}
-
-    def test_overwrite_deep_merge(self):
-        a = {"a": "b"}
-        b = {"a": "overwritten", "b": "c"}
-        result = misc.deep_merge(a, b)
-        assert result == {"a": "overwritten", "b": "c"}
-
-    def test_list_deep_merge(self):
-        a = [1, 2]
-        b = [3, 4]
-        result = misc.deep_merge(a, b)
-        assert result == [1, 2, 3, 4]
-
-    def test_missing_list_deep_merge(self):
-        a = [1, 2]
-        b = "not a list"
-        with pytest.raises(AssertionError):
-            misc.deep_merge(a, b)
-
-    def test_missing_a_deep_merge(self):
-        result = misc.deep_merge(None, [1, 2])
-        assert result == [1, 2]
-
-    def test_missing_b_deep_merge(self):
-        result = misc.deep_merge([1, 2], None)
-        assert result == [1, 2]
-
-    def test_invalid_b_deep_merge(self):
-        with pytest.raises(AssertionError):
-            misc.deep_merge({"a": "b"}, "invalid")
-
-
-class TestIsInDict(object):
-    def test_simple_membership(self):
-        assert misc.is_in_dict('a', 'foo', {'a':'foo', 'b':'bar'})
-
-    def test_dict_membership(self):
-        assert misc.is_in_dict(
-            'a', {'sub1':'key1', 'sub2':'key2'},
-            {'a':{'sub1':'key1', 'sub2':'key2', 'sub3':'key3'}}
-        )
-
-    def test_simple_nonmembership(self):
-        assert not misc.is_in_dict('a', 'foo', {'a':'bar', 'b':'foo'})
-
-    def test_nonmembership_with_presence_at_lower_level(self):
-        assert not misc.is_in_dict('a', 'foo', {'a':{'a': 'foo'}})
diff --git a/teuthology/test/test_packaging.py b/teuthology/test/test_packaging.py
deleted file mode 100644 (file)
index 046105e..0000000
+++ /dev/null
@@ -1,1086 +0,0 @@
-import pytest
-
-from unittest.mock import patch, Mock, PropertyMock
-
-from teuthology import packaging
-from teuthology.exceptions import VersionNotFoundError
-
-KOJI_TASK_RPMS_MATRIX = [
-    ('tasks/6745/9666745/kernel-4.1.0-0.rc2.git2.1.fc23.x86_64.rpm', 'kernel'),
-    ('tasks/6745/9666745/kernel-modules-4.1.0-0.rc2.git2.1.fc23.x86_64.rpm', 'kernel-modules'),
-    ('tasks/6745/9666745/kernel-tools-4.1.0-0.rc2.git2.1.fc23.x86_64.rpm', 'kernel-tools'),
-    ('tasks/6745/9666745/kernel-tools-libs-devel-4.1.0-0.rc2.git2.1.fc23.x86_64.rpm', 'kernel-tools-libs-devel'),
-    ('tasks/6745/9666745/kernel-headers-4.1.0-0.rc2.git2.1.fc23.x86_64.rpm', 'kernel-headers'),
-    ('tasks/6745/9666745/kernel-tools-debuginfo-4.1.0-0.rc2.git2.1.fc23.x86_64.rpm', 'kernel-tools-debuginfo'),
-    ('tasks/6745/9666745/kernel-debuginfo-common-x86_64-4.1.0-0.rc2.git2.1.fc23.x86_64.rpm', 'kernel-debuginfo-common-x86_64'),
-    ('tasks/6745/9666745/perf-debuginfo-4.1.0-0.rc2.git2.1.fc23.x86_64.rpm', 'perf-debuginfo'),
-    ('tasks/6745/9666745/kernel-modules-extra-4.1.0-0.rc2.git2.1.fc23.x86_64.rpm', 'kernel-modules-extra'),
-    ('tasks/6745/9666745/kernel-tools-libs-4.1.0-0.rc2.git2.1.fc23.x86_64.rpm', 'kernel-tools-libs'),
-    ('tasks/6745/9666745/kernel-core-4.1.0-0.rc2.git2.1.fc23.x86_64.rpm', 'kernel-core'),
-    ('tasks/6745/9666745/kernel-debuginfo-4.1.0-0.rc2.git2.1.fc23.x86_64.rpm', 'kernel-debuginfo'),
-    ('tasks/6745/9666745/python-perf-4.1.0-0.rc2.git2.1.fc23.x86_64.rpm', 'python-perf'),
-    ('tasks/6745/9666745/kernel-devel-4.1.0-0.rc2.git2.1.fc23.x86_64.rpm', 'kernel-devel'),
-    ('tasks/6745/9666745/python-perf-debuginfo-4.1.0-0.rc2.git2.1.fc23.x86_64.rpm', 'python-perf-debuginfo'),
-    ('tasks/6745/9666745/perf-4.1.0-0.rc2.git2.1.fc23.x86_64.rpm', 'perf'),
-]
-
-KOJI_TASK_RPMS = [rpm[0] for rpm in KOJI_TASK_RPMS_MATRIX]
-
-
-class TestPackaging(object):
-
-    def test_get_package_name_deb(self):
-        remote = Mock()
-        remote.os.package_type = "deb"
-        assert packaging.get_package_name('sqlite', remote) == "sqlite3"
-
-    def test_get_package_name_rpm(self):
-        remote = Mock()
-        remote.os.package_type = "rpm"
-        assert packaging.get_package_name('sqlite', remote) is None
-
-    def test_get_package_name_not_found(self):
-        remote = Mock()
-        remote.os.package_type = "rpm"
-        assert packaging.get_package_name('notthere', remote) is None
-
-    def test_get_service_name_deb(self):
-        remote = Mock()
-        remote.os.package_type = "deb"
-        assert packaging.get_service_name('httpd', remote) == 'apache2'
-
-    def test_get_service_name_rpm(self):
-        remote = Mock()
-        remote.os.package_type = "rpm"
-        assert packaging.get_service_name('httpd', remote) == 'httpd'
-
-    def test_get_service_name_not_found(self):
-        remote = Mock()
-        remote.os.package_type = "rpm"
-        assert packaging.get_service_name('notthere', remote) is None
-
-    def test_install_package_deb(self):
-        m_remote = Mock()
-        m_remote.os.package_type = "deb"
-        expected = [
-            'DEBIAN_FRONTEND=noninteractive',
-            'sudo',
-            '-E',
-            'apt-get',
-            '-y',
-            '--force-yes',
-            'install',
-            'apache2'
-        ]
-        packaging.install_package('apache2', m_remote)
-        m_remote.run.assert_called_with(args=expected)
-
-    def test_install_package_rpm(self):
-        m_remote = Mock()
-        m_remote.os.package_type = "rpm"
-        expected = [
-            'sudo',
-            'yum',
-            '-y',
-            'install',
-            'httpd'
-        ]
-        packaging.install_package('httpd', m_remote)
-        m_remote.run.assert_called_with(args=expected)
-
-    def test_remove_package_deb(self):
-        m_remote = Mock()
-        m_remote.os.package_type = "deb"
-        expected = [
-            'DEBIAN_FRONTEND=noninteractive',
-            'sudo',
-            '-E',
-            'apt-get',
-            '-y',
-            'purge',
-            'apache2'
-        ]
-        packaging.remove_package('apache2', m_remote)
-        m_remote.run.assert_called_with(args=expected)
-
-    def test_remove_package_rpm(self):
-        m_remote = Mock()
-        m_remote.os.package_type = "rpm"
-        expected = [
-            'sudo',
-            'yum',
-            '-y',
-            'erase',
-            'httpd'
-        ]
-        packaging.remove_package('httpd', m_remote)
-        m_remote.run.assert_called_with(args=expected)
-
-    def test_get_koji_package_name(self):
-        build_info = dict(version="3.10.0", release="123.20.1")
-        result = packaging.get_koji_package_name("kernel", build_info)
-        assert result == "kernel-3.10.0-123.20.1.x86_64.rpm"
-
-    @patch("teuthology.packaging.config")
-    def test_get_kojiroot_base_url(self, m_config):
-        m_config.kojiroot_url = "http://kojiroot.com"
-        build_info = dict(
-            package_name="kernel",
-            version="3.10.0",
-            release="123.20.1",
-        )
-        result = packaging.get_kojiroot_base_url(build_info)
-        expected = "http://kojiroot.com/kernel/3.10.0/123.20.1/x86_64/"
-        assert result == expected
-
-    @patch("teuthology.packaging.config")
-    def test_get_koji_build_info_success(self, m_config):
-        m_config.kojihub_url = "http://kojihub.com"
-        m_proc = Mock()
-        expected = dict(foo="bar")
-        m_proc.exitstatus = 0
-        m_proc.stdout.getvalue.return_value = str(expected)
-        m_remote = Mock()
-        m_remote.run.return_value = m_proc
-        result = packaging.get_koji_build_info(1, m_remote, dict())
-        assert result == expected
-        args, kwargs = m_remote.run.call_args
-        expected_args = [
-            'python', '-c',
-            'import koji; '
-            'hub = koji.ClientSession("http://kojihub.com"); '
-            'print(hub.getBuild(1))',
-        ]
-        assert expected_args == kwargs['args']
-
-    @patch("teuthology.packaging.config")
-    def test_get_koji_build_info_fail(self, m_config):
-        m_config.kojihub_url = "http://kojihub.com"
-        m_proc = Mock()
-        m_proc.exitstatus = 1
-        m_remote = Mock()
-        m_remote.run.return_value = m_proc
-        m_ctx = Mock()
-        m_ctx.summary = dict()
-        with pytest.raises(RuntimeError):
-            packaging.get_koji_build_info(1, m_remote, m_ctx)
-
-    @patch("teuthology.packaging.config")
-    def test_get_koji_task_result_success(self, m_config):
-        m_config.kojihub_url = "http://kojihub.com"
-        m_proc = Mock()
-        expected = dict(foo="bar")
-        m_proc.exitstatus = 0
-        m_proc.stdout.getvalue.return_value = str(expected)
-        m_remote = Mock()
-        m_remote.run.return_value = m_proc
-        result = packaging.get_koji_task_result(1, m_remote, dict())
-        assert result == expected
-        args, kwargs = m_remote.run.call_args
-        expected_args = [
-            'python', '-c',
-            'import koji; '
-            'hub = koji.ClientSession("http://kojihub.com"); '
-            'print(hub.getTaskResult(1))',
-        ]
-        assert expected_args == kwargs['args']
-
-    @patch("teuthology.packaging.config")
-    def test_get_koji_task_result_fail(self, m_config):
-        m_config.kojihub_url = "http://kojihub.com"
-        m_proc = Mock()
-        m_proc.exitstatus = 1
-        m_remote = Mock()
-        m_remote.run.return_value = m_proc
-        m_ctx = Mock()
-        m_ctx.summary = dict()
-        with pytest.raises(RuntimeError):
-            packaging.get_koji_task_result(1, m_remote, m_ctx)
-
-    @patch("teuthology.packaging.config")
-    def test_get_koji_task_rpm_info_success(self, m_config):
-        m_config.koji_task_url = "http://kojihub.com/work"
-        expected = dict(
-            base_url="http://kojihub.com/work/tasks/6745/9666745/",
-            version="4.1.0-0.rc2.git2.1.fc23.x86_64",
-            rpm_name="kernel-4.1.0-0.rc2.git2.1.fc23.x86_64.rpm",
-            package_name="kernel",
-        )
-        result = packaging.get_koji_task_rpm_info('kernel', KOJI_TASK_RPMS)
-        assert expected == result
-
-    @patch("teuthology.packaging.config")
-    def test_get_koji_task_rpm_info_fail(self, m_config):
-        m_config.koji_task_url = "http://kojihub.com/work"
-        with pytest.raises(RuntimeError):
-            packaging.get_koji_task_rpm_info('ceph', KOJI_TASK_RPMS)
-
-    def test_get_package_version_deb_found(self):
-        remote = Mock()
-        remote.os.package_type = "deb"
-        proc = Mock()
-        proc.exitstatus = 0
-        proc.stdout.getvalue.return_value = "2.2"
-        remote.run.return_value = proc
-        result = packaging.get_package_version(remote, "apache2")
-        assert result == "2.2"
-
-    def test_get_package_version_deb_command(self):
-        remote = Mock()
-        remote.os.package_type = "deb"
-        packaging.get_package_version(remote, "apache2")
-        args, kwargs = remote.run.call_args
-        expected_args = ['dpkg-query', '-W', '-f', '${Version}', 'apache2']
-        assert expected_args == kwargs['args']
-
-    def test_get_package_version_rpm_found(self):
-        remote = Mock()
-        remote.os.package_type = "rpm"
-        proc = Mock()
-        proc.exitstatus = 0
-        proc.stdout.getvalue.return_value = "2.2"
-        remote.run.return_value = proc
-        result = packaging.get_package_version(remote, "httpd")
-        assert result == "2.2"
-
-    def test_get_package_version_rpm_command(self):
-        remote = Mock()
-        remote.os.package_type = "rpm"
-        packaging.get_package_version(remote, "httpd")
-        args, kwargs = remote.run.call_args
-        expected_args = ['rpm', '-q', 'httpd', '--qf', '%{VERSION}-%{RELEASE}']
-        assert expected_args == kwargs['args']
-
-    def test_get_package_version_not_found(self):
-        remote = Mock()
-        remote.os.package_type = "rpm"
-        proc = Mock()
-        proc.exitstatus = 1
-        proc.stdout.getvalue.return_value = "not installed"
-        remote.run.return_value = proc
-        result = packaging.get_package_version(remote, "httpd")
-        assert result is None
-
-    def test_get_package_version_invalid_version(self):
-        # this tests the possibility that the package is not found
-        # but the exitstatus is still 0.  Not entirely sure we'll ever
-        # hit this condition, but I want to test the codepath regardless
-        remote = Mock()
-        remote.os.package_type = "rpm"
-        proc = Mock()
-        proc.exitstatus = 0
-        proc.stdout.getvalue.return_value = "not installed"
-        remote.run.return_value = proc
-        result = packaging.get_package_version(remote, "httpd")
-        assert result is None
-
-    @pytest.mark.parametrize("input, expected", KOJI_TASK_RPMS_MATRIX)
-    def test_get_koji_task_result_package_name(self, input, expected):
-        assert packaging._get_koji_task_result_package_name(input) == expected
-
-    @patch("requests.get")
-    def test_get_response_success(self, m_get):
-        resp = Mock()
-        resp.ok = True
-        m_get.return_value = resp
-        result = packaging._get_response("google.com")
-        assert result == resp
-
-    @patch("requests.get")
-    def test_get_response_failed_wait(self, m_get):
-        resp = Mock()
-        resp.ok = False
-        m_get.return_value = resp
-        packaging._get_response("google.com", wait=True, sleep=1, tries=2)
-        assert m_get.call_count == 2
-
-    @patch("requests.get")
-    def test_get_response_failed_no_wait(self, m_get):
-        resp = Mock()
-        resp.ok = False
-        m_get.return_value = resp
-        packaging._get_response("google.com", sleep=1, tries=2)
-        assert m_get.call_count == 1
-
-    @patch('teuthology.packaging.config')
-    def test_get_builder_project_pulp(self, m_config):
-        m_config.package_source = 'pulp'
-        assert packaging.get_builder_project() is packaging.PulpProject
-
-
-class TestBuilderProject(object):
-    klass = None
-
-    def setup_method(self):
-        if self.klass is None:
-            pytest.skip()
-
-    def _get_remote(self, arch="x86_64", system_type="deb", distro="ubuntu",
-                    codename="focal", version="20.04"):
-        rem = Mock()
-        rem.system_type = system_type
-        rem.os.name = distro
-        rem.os.codename = codename
-        rem.os.version = version
-        rem.arch = arch
-
-        return rem
-
-    def test_init_from_remote_base_url(self, expected=None):
-        assert expected is not None
-        rem = self._get_remote()
-        ctx = dict(foo="bar")
-        gp = self.klass("ceph", {}, ctx=ctx, remote=rem)
-        result = gp.base_url
-        assert result == expected
-
-    def test_init_from_remote_base_url_debian(self, expected=None):
-        assert expected is not None
-        # remote.os.codename returns and empty string on debian
-        rem = self._get_remote(distro="debian", codename='', version="7.1")
-        ctx = dict(foo="bar")
-        gp = self.klass("ceph", {}, ctx=ctx, remote=rem)
-        result = gp.base_url
-        assert result == expected
-
-    def test_init_from_config_base_url(self, expected=None):
-        assert expected is not None
-        config = dict(
-            os_type="ubuntu",
-            os_version="20.04",
-            sha1="sha1",
-        )
-        gp = self.klass("ceph", config)
-        result = gp.base_url
-        print(self.m_get.call_args_list)
-        assert result == expected
-
-    def test_init_from_config_branch_ref(self):
-        config = dict(
-            os_type="ubuntu",
-            os_version="20.04",
-            branch='jewel',
-        )
-        gp = self.klass("ceph", config)
-        result = gp.uri_reference
-        expected = 'ref/jewel'
-        assert result == expected
-
-    def test_init_from_config_tag_ref(self):
-        config = dict(
-            os_type="ubuntu",
-            os_version="20.04",
-            tag='v10.0.1',
-        )
-        gp = self.klass("ceph", config)
-        result = gp.uri_reference
-        expected = 'ref/v10.0.1'
-        assert result == expected
-
-    def test_init_from_config_tag_overrides_branch_ref(self, caplog):
-        config = dict(
-            os_type="ubuntu",
-            os_version="20.04",
-            branch='jewel',
-            tag='v10.0.1',
-        )
-        gp = self.klass("ceph", config)
-        result = gp.uri_reference
-        expected = 'ref/v10.0.1'
-        assert result == expected
-        expected_log = 'More than one of ref, tag, branch, or sha1 supplied; using tag'
-        assert expected_log in caplog.text
-        return gp
-
-    def test_init_from_config_branch_overrides_sha1(self, caplog):
-        config = dict(
-            os_type="ubuntu",
-            os_version="20.04",
-            branch='jewel',
-            sha1='sha1',
-        )
-        gp = self.klass("ceph", config)
-        result = gp.uri_reference
-        expected = 'ref/jewel'
-        assert result == expected
-        expected_log = 'More than one of ref, tag, branch, or sha1 supplied; using branch'
-        assert expected_log in caplog.text
-        return gp
-
-    REFERENCE_MATRIX = [
-        ('the_ref', 'the_tag', 'the_branch', 'the_sha1', dict(ref='the_ref')),
-        (None, 'the_tag', 'the_branch', 'the_sha1', dict(tag='the_tag')),
-        (None, None, 'the_branch', 'the_sha1', dict(branch='the_branch')),
-        (None, None, None, 'the_sha1', dict(sha1='the_sha1')),
-        (None, None, 'the_branch', None, dict(branch='the_branch')),
-    ]
-
-    @pytest.mark.parametrize(
-        "ref, tag, branch, sha1, expected",
-        REFERENCE_MATRIX,
-    )
-    def test_choose_reference(self, ref, tag, branch, sha1, expected):
-        config = dict(
-            os_type='ubuntu',
-            os_version='18.04',
-        )
-        if ref:
-            config['ref'] = ref
-        if tag:
-            config['tag'] = tag
-        if branch:
-            config['branch'] = branch
-        if sha1:
-            config['sha1'] = sha1
-        gp = self.klass("ceph", config)
-        assert gp._choose_reference() == expected
-
-    def test_get_package_version_found(self):
-        rem = self._get_remote()
-        ctx = dict(foo="bar")
-        gp = self.klass("ceph", {}, ctx=ctx, remote=rem)
-        assert gp.version == "0.90.0"
-
-    @patch("teuthology.packaging._get_response")
-    def test_get_package_version_not_found(self, m_get_response):
-        rem = self._get_remote()
-        ctx = dict(foo="bar")
-        resp = Mock()
-        resp.ok = False
-        m_get_response.return_value = resp
-        gp = self.klass("ceph", {}, ctx=ctx, remote=rem)
-        with pytest.raises(VersionNotFoundError):
-            gp.version
-
-    def test_get_package_sha1_fetched_found(self):
-        rem = self._get_remote()
-        ctx = dict(foo="bar")
-        gp = self.klass("ceph", {}, ctx=ctx, remote=rem)
-        assert gp.sha1 == "the_sha1"
-
-    def test_get_package_sha1_fetched_not_found(self):
-        rem = self._get_remote()
-        ctx = dict(foo="bar")
-        gp = self.klass("ceph", {}, ctx=ctx, remote=rem)
-        assert not gp.sha1
-
-    DISTRO_MATRIX = [None] * 12
-
-    @pytest.mark.parametrize(
-        "matrix_index",
-        range(len(DISTRO_MATRIX)),
-    )
-    def test_get_distro_remote(self, matrix_index):
-        (distro, version, codename, expected) = \
-            self.DISTRO_MATRIX[matrix_index]
-        rem = self._get_remote(distro=distro, version=version,
-                               codename=codename)
-        ctx = dict(foo="bar")
-        gp = self.klass("ceph", {}, ctx=ctx, remote=rem)
-        assert gp.distro == expected
-
-    DISTRO_MATRIX_NOVER = [
-        ('rhel', None, None, 'centos8'),
-        ('centos', None, None, 'centos8'),
-        ('fedora', None, None, 'fedora25'),
-        ('ubuntu', None, None, 'focal'),
-        ('debian', None, None, 'jessie'),
-    ]
-
-    @pytest.mark.parametrize(
-        "matrix_index",
-        range(len(DISTRO_MATRIX) + len(DISTRO_MATRIX_NOVER)),
-    )
-    def test_get_distro_config(self, matrix_index):
-        (distro, version, codename, expected) = \
-            (self.DISTRO_MATRIX + self.DISTRO_MATRIX_NOVER)[matrix_index]
-        config = dict(
-            os_type=distro,
-            os_version=version
-        )
-        gp = self.klass("ceph", config)
-        assert gp.distro == expected
-
-    DIST_RELEASE_MATRIX = [
-        ('rhel', '7.0', None, 'el7'),
-        ('centos', '6.5', None, 'el6'),
-        ('centos', '7.0', None, 'el7'),
-        ('centos', '7.1', None, 'el7'),
-        ('centos', '8.1', None, 'el8'),
-        ('fedora', '20', None, 'fc20'),
-        ('debian', '7.0', None, 'debian'),
-        ('debian', '7', None, 'debian'),
-        ('debian', '7.1', None, 'debian'),
-        ('ubuntu', '12.04', None, 'ubuntu'),
-        ('ubuntu', '14.04', None, 'ubuntu'),
-        ('ubuntu', '16.04', None, 'ubuntu'),
-        ('ubuntu', '18.04', None, 'ubuntu'),
-        ('ubuntu', '20.04', None, 'ubuntu'),
-    ]
-
-    @pytest.mark.parametrize(
-        "matrix_index",
-        range(len(DIST_RELEASE_MATRIX)),
-    )
-    def test_get_dist_release(self, matrix_index):
-        (distro, version, codename, expected) = \
-            (self.DIST_RELEASE_MATRIX)[matrix_index]
-        rem = self._get_remote(distro=distro, version=version,
-                               codename=codename)
-        ctx = dict(foo="bar")
-        gp = self.klass("ceph", {}, ctx=ctx, remote=rem)
-        assert gp.dist_release == expected
-
-
-class TestShamanProject(TestBuilderProject):
-    klass = packaging.ShamanProject
-
-    def setup_method(self):
-        self.p_config = patch('teuthology.packaging.config')
-        self.m_config = self.p_config.start()
-        self.m_config.package_source = 'shaman'
-        self.m_config.shaman = {'endpoint': 'https://shaman.ceph.com/api/'}
-        self.p_get_config_value = \
-            patch('teuthology.packaging._get_config_value_for_remote')
-        self.m_get_config_value = self.p_get_config_value.start()
-        self.m_get_config_value.return_value = None
-        self.p_get = patch('requests.get')
-        self.m_get = self.p_get.start()
-
-    def teardown_method(self):
-        self.p_config.stop()
-        self.p_get_config_value.stop()
-        self.p_get.stop()
-
-    def test_init_from_remote_base_url(self):
-        # Here, we really just need to make sure ShamanProject._search()
-        # queries the right URL. So let's make _get_base_url() just pass that
-        # URL through and test that value.
-        def m_get_base_url(obj):
-            obj._search()
-            return self.m_get.call_args_list[0][0][0]
-        with patch(
-            'teuthology.packaging.ShamanProject._get_base_url',
-            new=m_get_base_url,
-        ):
-            super(TestShamanProject, self)\
-                .test_init_from_remote_base_url(
-                    "https://shaman.ceph.com/api/search?status=ready"
-                    "&project=ceph&flavor=default"
-                    "&distros=ubuntu%2F20.04%2Fx86_64&ref=main"
-                )
-
-    def test_init_from_remote_base_url_debian(self):
-        # Here, we really just need to make sure ShamanProject._search()
-        # queries the right URL. So let's make _get_base_url() just pass that
-        # URL through and test that value.
-        def m_get_base_url(obj):
-            obj._search()
-            return self.m_get.call_args_list[0][0][0]
-        with patch(
-            'teuthology.packaging.ShamanProject._get_base_url',
-            new=m_get_base_url,
-        ):
-            super(TestShamanProject, self)\
-                .test_init_from_remote_base_url_debian(
-                    "https://shaman.ceph.com/api/search?status=ready"
-                    "&project=ceph&flavor=default"
-                    "&distros=debian%2F7.1%2Fx86_64&ref=main"
-                )
-
-    def test_init_from_config_base_url(self):
-        # Here, we really just need to make sure ShamanProject._search()
-        # queries the right URL. So let's make _get_base_url() just pass that
-        # URL through and test that value.
-        def m_get_base_url(obj):
-            obj._search()
-            return self.m_get.call_args_list[0][0][0]
-        with patch(
-            'teuthology.packaging.ShamanProject._get_base_url',
-            new=m_get_base_url,
-        ):
-            super(TestShamanProject, self).test_init_from_config_base_url(
-                "https://shaman.ceph.com/api/search?status=ready&project=ceph" \
-                "&flavor=default&distros=ubuntu%2F20.04%2Fx86_64&sha1=sha1"
-            )
-
-    @patch('teuthology.packaging.ShamanProject._get_package_sha1')
-    def test_init_from_config_tag_ref(self, m_get_package_sha1):
-        m_get_package_sha1.return_value = 'the_sha1'
-        super(TestShamanProject, self).test_init_from_config_tag_ref()
-
-    def test_init_from_config_tag_overrides_branch_ref(self, caplog):
-        with patch(
-            'teuthology.packaging.repo_utils.ls_remote',
-        ) as m_ls_remote:
-            m_ls_remote.return_value = 'sha1_from_my_tag'
-            obj = super(TestShamanProject, self)\
-                .test_init_from_config_tag_overrides_branch_ref(caplog)
-            search_uri = obj._search_uri
-        assert 'sha1=sha1_from_my_tag' in search_uri
-        assert 'jewel' not in search_uri
-
-    def test_init_from_config_branch_overrides_sha1(self, caplog):
-        obj = super(TestShamanProject, self)\
-            .test_init_from_config_branch_overrides_sha1(caplog)
-        search_uri = obj._search_uri
-        assert 'jewel' in search_uri
-        assert 'sha1' not in search_uri
-
-    def test_get_package_version_found(self):
-        resp = Mock()
-        resp.ok = True
-        resp.json.return_value = [
-            dict(
-                sha1='the_sha1',
-                extra=dict(package_manager_version='0.90.0'),
-            )
-        ]
-        self.m_get.return_value = resp
-        super(TestShamanProject, self)\
-            .test_get_package_version_found()
-
-    def test_get_package_sha1_fetched_found(self):
-        resp = Mock()
-        resp.ok = True
-        resp.json.return_value = [dict(sha1='the_sha1')]
-        self.m_get.return_value = resp
-        super(TestShamanProject, self)\
-            .test_get_package_sha1_fetched_found()
-
-    def test_get_package_sha1_fetched_not_found(self):
-        resp = Mock()
-        resp.json.return_value = []
-        self.m_get.return_value = resp
-        super(TestShamanProject, self)\
-            .test_get_package_sha1_fetched_not_found()
-
-    SHAMAN_SEARCH_RESPONSE = [
-        {
-            "status": "ready",
-            "sha1": "534fc6d936bd506119f9e0921ff8cf8d47caa323",
-            "extra": {
-                "build_url": "https://jenkins.ceph.com/job/ceph-dev-build/ARCH=x86_64,AVAILABLE_ARCH=x86_64,AVAILABLE_DIST=centos8,DIST=centos8,MACHINE_SIZE=gigantic/48556/",
-                "root_build_cause": "SCMTRIGGER",
-                "version": "17.0.0-8856-g534fc6d9",
-                "node_name": "172.21.2.7+braggi07",
-                "job_name": "ceph-dev-build/ARCH=x86_64,AVAILABLE_ARCH=x86_64,AVAILABLE_DIST=centos8,DIST=centos8,MACHINE_SIZE=gigantic",
-                "package_manager_version": "17.0.0-8856.g534fc6d9"
-            },
-            "url": "https://3.chacra.ceph.com/r/ceph/main/534fc6d936bd506119f9e0921ff8cf8d47caa323/centos/8/flavors/default/",
-            "modified": "2021-11-06 21:40:40.669823",
-            "distro_version": "8",
-            "project": "ceph",
-            "flavor": "default",
-            "ref": "main",
-            "chacra_url": "https://3.chacra.ceph.com/repos/ceph/main/534fc6d936bd506119f9e0921ff8cf8d47caa323/centos/8/flavors/default/",
-            "archs": [
-                "x86_64",
-                "arm64",
-                "source"
-            ],
-            "distro": "centos"
-          }
-    ]
-
-    SHAMAN_BUILDS_RESPONSE = [
-        {
-            "status": "completed",
-            "sha1": "534fc6d936bd506119f9e0921ff8cf8d47caa323",
-            "distro_arch": "arm64",
-            "started": "2021-11-06 20:20:15.121203",
-            "completed": "2021-11-06 22:36:27.115950",
-            "extra": {
-                "node_name": "172.21.4.66+confusa04",
-                "version": "17.0.0-8856-g534fc6d9",
-                "build_user": "",
-                "root_build_cause": "SCMTRIGGER",
-
-                "job_name": "ceph-dev-build/ARCH=arm64,AVAILABLE_ARCH=arm64,AVAILABLE_DIST=centos8,DIST=centos8,MACHINE_SIZE=gigantic"
-            },
-            "modified": "2021-11-06 22:36:27.118043",
-            "distro_version": "8",
-            "project": "ceph",
-            "url": "https://jenkins.ceph.com/job/ceph-dev-build/ARCH=arm64,AVAILABLE_ARCH=arm64,AVAILABLE_DIST=centos8,DIST=centos8,MACHINE_SIZE=gigantic/48556/",
-            "log_url": "https://jenkins.ceph.com/job/ceph-dev-build/ARCH=arm64,AVAILABLE_ARCH=arm64,AVAILABLE_DIST=centos8,DIST=centos8,MACHINE_SIZE=gigantic/48556//consoleFull",
-            "flavor": "default",
-            "ref": "main",
-            "distro": "centos"
-        },
-        {
-            "status": "completed",
-            "sha1": "534fc6d936bd506119f9e0921ff8cf8d47caa323",
-            "distro_arch": "x86_64",
-            "started": "2021-11-06 20:20:06.740692",
-            "completed": "2021-11-06 21:43:51.711970",
-            "extra": {
-                "node_name": "172.21.2.7+braggi07",
-                "version": "17.0.0-8856-g534fc6d9",
-                "build_user": "",
-                "root_build_cause": "SCMTRIGGER",
-                "job_name": "ceph-dev-build/ARCH=x86_64,AVAILABLE_ARCH=x86_64,AVAILABLE_DIST=centos8,DIST=centos8,MACHINE_SIZE=gigantic"
-            },
-            "modified": "2021-11-06 21:43:51.713487",
-            "distro_version": "8",
-            "project": "ceph",
-            "url": "https://jenkins.ceph.com/job/ceph-dev-build/ARCH=x86_64,AVAILABLE_ARCH=x86_64,AVAILABLE_DIST=centos8,DIST=centos8,MACHINE_SIZE=gigantic/48556/",
-            "log_url": "https://jenkins.ceph.com/job/ceph-dev-build/ARCH=x86_64,AVAILABLE_ARCH=x86_64,AVAILABLE_DIST=centos8,DIST=centos8,MACHINE_SIZE=gigantic/48556//consoleFull",
-            "flavor": "default",
-            "ref": "main",
-            "distro": "centos"
-        }
-    ]
-
-    def test_build_complete_success(self):
-        config = dict(
-            os_type="centos",
-            os_version="8",
-            branch='main',
-            arch='x86_64',
-            flavor='default',
-        )
-        builder = self.klass("ceph", config)
-
-        search_resp = Mock()
-        search_resp.ok = True
-        search_resp.json.return_value = self.SHAMAN_SEARCH_RESPONSE
-        self.m_get.return_value = search_resp
-        # cause builder to call requests.get and cache search_resp
-        builder.assert_result()
-
-        build_resp = Mock()
-        build_resp.ok = True
-        self.m_get.return_value = build_resp
-
-        # both archs completed, so x86_64 build is complete
-        builds = build_resp.json.return_value = self.SHAMAN_BUILDS_RESPONSE
-        assert builder.build_complete
-
-        # mark the arm64 build failed, x86_64 should still be complete
-        builds[0]['status'] = "failed"
-        build_resp.json.return_value = builds
-        assert builder.build_complete
-
-        # mark the x86_64 build failed, should show incomplete
-        builds[1]['status'] = "failed"
-        build_resp.json.return_value = builds
-        assert not builder.build_complete
-
-        # mark the arm64 build complete again, x86_64 still incomplete
-        builds[0]['status'] = "completed"
-        build_resp.json.return_value = builds
-        assert not builder.build_complete
-
-    DISTRO_MATRIX = [
-        ('rhel', '7.0', None, 'centos/7'),
-        ('alma', '9.7', None, 'alma/9'),
-        ('rocky', '9.7', None, 'rocky/9'),
-        ('rocky', '10.1', None, 'rocky/10'),
-        ('centos', '6.5', None, 'centos/6'),
-        ('centos', '7.0', None, 'centos/7'),
-        ('centos', '7.1', None, 'centos/7'),
-        ('centos', '8.1', None, 'centos/8'),
-        ('fedora', '20', None, 'fedora/20'),
-        ('ubuntu', '14.04', 'trusty', 'ubuntu/14.04'),
-        ('ubuntu', '14.04', None, 'ubuntu/14.04'),
-        ('debian', '7.0', None, 'debian/7.0'),
-        ('debian', '7', None, 'debian/7'),
-        ('debian', '7.1', None, 'debian/7.1'),
-        ('ubuntu', '12.04', None, 'ubuntu/12.04'),
-        ('ubuntu', '14.04', None, 'ubuntu/14.04'),
-        ('ubuntu', '16.04', None, 'ubuntu/16.04'),
-        ('ubuntu', '18.04', None, 'ubuntu/18.04'),
-        ('ubuntu', '20.04', None, 'ubuntu/20.04'),
-        ('opensuse', '15.6', None, 'opensuse/15.6'),
-        ('sle', '15.5', None, 'sle/15.5'),
-    ]
-    @pytest.mark.parametrize(
-        "matrix_index",
-        range(len(DISTRO_MATRIX)),
-    )
-    def test_get_distro_remote(self, matrix_index):
-        super().test_get_distro_remote(matrix_index)
-
-    DISTRO_MATRIX_NOVER = [
-        ('rhel', None, None, 'centos/8'),
-        ('centos', None, None, 'centos/9'),
-        ('fedora', None, None, 'fedora/25'),
-        ('ubuntu', None, None, 'ubuntu/22.04'),
-        ('alma', None, None, 'alma/9'),
-        ('rocky', None, None, 'rocky/9'),
-    ]
-    @pytest.mark.parametrize(
-        "matrix_index",
-        range(len(DISTRO_MATRIX) + len(DISTRO_MATRIX_NOVER)),
-    )
-    def test_get_distro_config(self, matrix_index):
-        super().test_get_distro_config(matrix_index)
-
-
-class TestPulpProject(TestBuilderProject):
-    klass = packaging.PulpProject
-
-    def setup_method(self):
-        self.p_config = patch('teuthology.packaging.config')
-        self.m_config = self.p_config.start()
-        self.m_config.package_source = 'pulp'
-        self.m_config.pulp = {
-            'endpoint': 'https://pulp.example.com/pulp/api/v3/',
-            'username': 'u',
-            'password': 'p',
-        }
-        self.p_get_config_value = \
-            patch('teuthology.packaging._get_config_value_for_remote')
-        self.m_get_config_value = self.p_get_config_value.start()
-        self.m_get_config_value.return_value = None
-        self.p_get = patch('requests.get')
-        self.m_get = self.p_get.start()
-
-        resp = Mock()
-        resp.ok = True
-        resp.json.return_value = {
-            'results': [{
-                'base_url': 'dist/ceph/main/sha/centos/8/flavors/default/',
-                'pulp_labels': {
-                    'sha1': 'the_sha1',
-                    'version': '0.90.0',
-                },
-            }],
-        }
-        self.m_get.return_value = resp
-
-    def teardown_method(self):
-        self.p_config.stop()
-        self.p_get_config_value.stop()
-        self.p_get.stop()
-
-    def test_init_requires_pulp_credentials(self):
-        self.m_config.pulp = {'username': '', 'password': 'p'}
-        with pytest.raises(ValueError, match='Pulp username and password are required'):
-            packaging.PulpProject('ceph', {}, ctx=dict(foo='bar'),
-                                  remote=self._get_remote())
-
-        self.m_config.pulp = {'username': 'u', 'password': ''}
-        with pytest.raises(ValueError, match='Pulp username and password are required'):
-            packaging.PulpProject('ceph', {}, ctx=dict(foo='bar'),
-                                  remote=self._get_remote())
-
-    def test_init_from_remote_base_url(self):
-        def m_get_base_url(obj):
-            obj._search()
-            return self.m_get.call_args_list[0][0][0]
-        with patch(
-            'teuthology.packaging.PulpProject._get_base_url',
-            new=m_get_base_url,
-        ):
-            super(TestPulpProject, self).test_init_from_remote_base_url(
-                'https://pulp.example.com/pulp/api/v3/distributions/deb/apt',
-            )
-
-    def test_init_from_remote_base_url_debian(self):
-        def m_get_base_url(obj):
-            obj._search()
-            return self.m_get.call_args_list[0][0][0]
-        with patch(
-            'teuthology.packaging.PulpProject._get_base_url',
-            new=m_get_base_url,
-        ):
-            super(TestPulpProject, self).test_init_from_remote_base_url_debian(
-                'https://pulp.example.com/pulp/api/v3/distributions/deb/apt',
-            )
-
-    def test_init_from_config_base_url(self):
-        def m_get_base_url(obj):
-            obj._search()
-            return self.m_get.call_args_list[0][0][0]
-        with patch(
-            'teuthology.packaging.PulpProject._get_base_url',
-            new=m_get_base_url,
-        ):
-            super(TestPulpProject, self).test_init_from_config_base_url(
-                'https://pulp.example.com/pulp/api/v3/distributions/deb/apt',
-            )
-
-    @patch('teuthology.packaging.PulpProject._get_package_sha1')
-    def test_init_from_config_tag_ref(self, m_get_package_sha1):
-        m_get_package_sha1.return_value = 'the_sha1'
-        super(TestPulpProject, self).test_init_from_config_tag_ref()
-
-    def test_init_from_config_tag_overrides_branch_ref(self, caplog):
-        obj = super(TestPulpProject, self)\
-            .test_init_from_config_tag_overrides_branch_ref(caplog)
-        obj._search()
-        labels = self.m_get.call_args[1]['params']['pulp_label_select']
-        assert 'tag=v10.0.1' in labels
-        assert 'branch=jewel' not in labels
-
-    def test_init_from_config_branch_overrides_sha1(self, caplog):
-        obj = super(TestPulpProject, self)\
-            .test_init_from_config_branch_overrides_sha1(caplog)
-        obj._search()
-        labels = self.m_get.call_args[1]['params']['pulp_label_select']
-        assert 'branch=jewel' in labels
-        assert 'sha1=sha1' not in labels
-
-    def test_get_package_version_found(self):
-        resp = Mock()
-        resp.ok = True
-        resp.json.return_value = {
-            'results': [{
-                'base_url': 'a/b/c/d',
-                'pulp_labels': {'version': '0.90.0'},
-            }],
-        }
-        self.m_get.return_value = resp
-        super(TestPulpProject, self).test_get_package_version_found()
-
-    def test_get_package_version_not_found(self):
-        rem = self._get_remote()
-        ctx = dict(foo="bar")
-        resp = Mock()
-        resp.ok = False
-        self.m_get.return_value = resp
-        gp = self.klass("ceph", {}, ctx=ctx, remote=rem)
-        with pytest.raises(VersionNotFoundError):
-            gp.version
-
-    def test_get_package_sha1_fetched_found(self):
-        resp = Mock()
-        resp.ok = True
-        resp.json.return_value = {
-            'results': [{'base_url': 'a/b/c/d', 'pulp_labels': {'sha1': 'the_sha1'}}],
-        }
-        self.m_get.return_value = resp
-        super(TestPulpProject, self).test_get_package_sha1_fetched_found()
-
-    def test_get_package_sha1_fetched_not_found(self):
-        resp = Mock()
-        resp.ok = True
-        resp.json.return_value = {
-            'results': [{'base_url': 'a/b/c/d', 'pulp_labels': {}}],
-        }
-        self.m_get.return_value = resp
-        super(TestPulpProject, self).test_get_package_sha1_fetched_not_found()
-
-    DISTRO_MATRIX = [
-        ('rhel', '7.0', None, 'centos/7'),
-        ('alma', '9.7', None, 'alma/9'),
-        ('rocky', '10.1', None, 'rocky/10'),
-        ('centos', '8.1', None, 'centos/8'),
-        ('fedora', '20', None, 'fedora/20'),
-        ('ubuntu', '20.04', 'focal', 'ubuntu/focal'),
-    ]
-
-    @pytest.mark.parametrize(
-        "matrix_index",
-        range(len(DISTRO_MATRIX)),
-    )
-    def test_get_distro_remote(self, matrix_index):
-        super().test_get_distro_remote(matrix_index)
-
-    DISTRO_MATRIX_NOVER = [
-        ('rhel', None, None, 'centos/8'),
-        ('centos', None, None, 'centos/9'),
-        ('fedora', None, None, 'fedora/25'),
-        ('ubuntu', None, None, 'ubuntu/jammy'),
-        ('alma', None, None, 'alma/9'),
-        ('rocky', None, None, 'rocky/9'),
-    ]
-
-    DISTRO_MATRIX_CONFIG = [
-        ('rhel', '7.0', None, 'centos/7'),
-        ('alma', '9.7', None, 'alma/9'),
-        ('rocky', '10.1', None, 'rocky/10'),
-        ('centos', '8.1', None, 'centos/8'),
-        ('fedora', '20', None, 'fedora/20'),
-        ('ubuntu', '20.04', None, 'ubuntu/focal'),
-    ]
-
-    @pytest.mark.parametrize(
-        "matrix_index",
-        range(len(DISTRO_MATRIX_CONFIG) + len(DISTRO_MATRIX_NOVER)),
-    )
-    def test_get_distro_config(self, matrix_index):
-        (distro, version, _, expected) = (
-            self.DISTRO_MATRIX_CONFIG + self.DISTRO_MATRIX_NOVER
-        )[matrix_index]
-        config = dict(
-            os_type=distro,
-            os_version=version
-        )
-        gp = self.klass("ceph", config)
-        assert gp.distro == expected
-
-    def test_get_rpm_repo_content_uses_pulp_base_url(self):
-        pulp_base = (
-            'https://pulp.example.com/pulp/content/repos/ceph/tentacle/'
-            'abc123/rocky/10/flavors/default'
-        )
-        rem = self._get_remote()
-        with patch(
-            'teuthology.packaging.PulpProject.base_url',
-            new_callable=PropertyMock,
-            return_value=pulp_base,
-        ):
-            gp = self.klass('ceph', {}, ctx=dict(foo='bar'), remote=rem)
-            content = gp._get_rpm_repo_content()
-        assert f'baseurl={pulp_base}/$basearch' in content
-        assert f'baseurl={pulp_base}/noarch' in content
-        assert f'baseurl={pulp_base}/SRPMS' in content
-        assert 'chacra' not in content
-
-    @patch('teuthology.packaging.sudo_write_file')
-    def test_install_rpm_repo_writes_repo_file(self, m_sudo_write_file):
-        pulp_base = (
-            'https://pulp.example.com/pulp/content/repos/ceph/tentacle/'
-            'abc123/rocky/10/flavors/default'
-        )
-        rem = self._get_remote()
-        with patch(
-            'teuthology.packaging.PulpProject.base_url',
-            new_callable=PropertyMock,
-            return_value=pulp_base,
-        ):
-            gp = self.klass('ceph', {}, ctx=dict(foo='bar'), remote=rem)
-            gp._install_rpm_repo()
-        m_sudo_write_file.assert_called_once()
-        assert m_sudo_write_file.call_args[0][1] == '/etc/yum.repos.d/ceph.repo'
-        assert pulp_base in m_sudo_write_file.call_args[0][2]
-
-    def test_get_deb_repo_content_uses_pulp_distribution_url(self):
-        pulp_dist = (
-            'https://pulp.example.com/pulp/content/repos/ceph/tentacle/'
-            'ff309bba/ubuntu/jammy/flavors/default/x86_64/'
-        )
-        rem = self._get_remote(distro='ubuntu', version='22.04', codename='jammy')
-        with patch(
-            'teuthology.packaging.PulpProject.repo_url',
-            new_callable=PropertyMock,
-            return_value=pulp_dist,
-        ):
-            gp = self.klass('ceph', {}, ctx=dict(foo='bar'), remote=rem)
-            content = gp._get_deb_repo_content()
-        assert content == (
-            'deb [trusted=yes] '
-            'https://pulp.example.com/pulp/content/repos/ceph/tentacle/'
-            'ff309bba/ubuntu/jammy/flavors/default/x86_64/   default all\n'
-        )
-
-    @patch('teuthology.packaging.sudo_write_file')
-    def test_install_deb_repo_writes_sources_list(self, m_sudo_write_file):
-        pulp_dist = (
-            'https://pulp.example.com/pulp/content/repos/ceph/tentacle/'
-            'ff309bba/ubuntu/jammy/flavors/default/x86_64/'
-        )
-        rem = self._get_remote(distro='ubuntu', version='22.04', codename='jammy')
-        with patch(
-            'teuthology.packaging.PulpProject.repo_url',
-            new_callable=PropertyMock,
-            return_value=pulp_dist,
-        ):
-            gp = self.klass('ceph', {}, ctx=dict(foo='bar'), remote=rem)
-            gp._install_deb_repo()
-        m_sudo_write_file.assert_called_once()
-        assert m_sudo_write_file.call_args[0][1] == '/etc/apt/sources.list.d/ceph.list'
-        assert '[trusted=yes]' in m_sudo_write_file.call_args[0][2]
-        assert '/x86_64/   default all' in m_sudo_write_file.call_args[0][2]
diff --git a/teuthology/test/test_parallel.py b/teuthology/test/test_parallel.py
deleted file mode 100644 (file)
index bba1d57..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-from teuthology.parallel import parallel
-
-
-def identity(item, input_set=None, remove=False):
-    if input_set is not None:
-        assert item in input_set
-        if remove:
-            input_set.remove(item)
-    return item
-
-
-class TestParallel(object):
-    def test_basic(self):
-        in_set = set(range(10))
-        with parallel() as para:
-            for i in in_set:
-                para.spawn(identity, i, in_set, remove=True)
-                assert para.any_spawned is True
-            assert para.count == len(in_set)
-
-    def test_result(self):
-        in_set = set(range(10))
-        with parallel() as para:
-            for i in in_set:
-                para.spawn(identity, i, in_set)
-            for result in para:
-                in_set.remove(result)
-
diff --git a/teuthology/test/test_repo_utils.py b/teuthology/test/test_repo_utils.py
deleted file mode 100644 (file)
index 969f825..0000000
+++ /dev/null
@@ -1,250 +0,0 @@
-import logging
-import unittest.mock as mock
-import os
-import os.path
-from pytest import raises, mark
-import re
-import shutil
-import subprocess
-import tempfile
-from packaging.version import parse
-
-from teuthology.exceptions import BranchNotFoundError, CommitNotFoundError
-from teuthology import repo_utils
-from teuthology import parallel
-repo_utils.log.setLevel(logging.WARNING)
-
-
-class TestRepoUtils(object):
-
-    @classmethod
-    def setup_class(cls):
-        cls.temp_path = tempfile.mkdtemp(prefix='test_repo-')
-        cls.dest_path = cls.temp_path + '/empty_dest'
-        cls.src_path = cls.temp_path + '/empty_src'
-
-        if 'TEST_ONLINE' in os.environ:
-            cls.repo_url = 'https://github.com/ceph/empty.git'
-            cls.commit = '71245d8e454a06a38a00bff09d8f19607c72e8bf'
-        else:
-            cls.repo_url = 'file://' + cls.src_path
-            cls.commit = None
-
-        cls.git_version = parse(cls.get_system_git_version())
-
-    @classmethod
-    def teardown_class(cls):
-        shutil.rmtree(cls.temp_path)
-
-    @classmethod
-    def get_system_git_version(cls):
-        # parsing following patterns
-        # 1) git version 2.45.2
-        # 2) git version 2.39.3 (Apple Git-146)
-        git_version = subprocess.check_output(('git', 'version')).decode()
-        m = re.match(r"git version (?P<ver>\d+.\d+.\d+) ?", git_version)
-        return m['ver']
-
-    def setup_method(self, method):
-        # In git 2.28.0, the --initial-branch flag was added.
-        if self.git_version >= parse("2.28.0"):
-            subprocess.check_call(
-                ('git', 'init', '--initial-branch', 'main', self.src_path)
-            )
-        else:
-            subprocess.check_call(('git', 'init', self.src_path))
-            subprocess.check_call(
-                ('git', 'checkout', '-b', 'main'),
-                cwd=self.src_path,
-            )
-        proc = subprocess.Popen(
-            ('git', 'config', 'user.email', 'test@ceph.com'),
-            cwd=self.src_path,
-            stdout=subprocess.PIPE,
-        )
-        assert proc.wait() == 0
-        proc = subprocess.Popen(
-            ('git', 'config', 'user.name', 'Test User'),
-            cwd=self.src_path,
-            stdout=subprocess.PIPE,
-        )
-        assert proc.wait() == 0
-        proc = subprocess.Popen(
-            ('git', 'commit', '--allow-empty', '--allow-empty-message',
-             '--no-edit'),
-            cwd=self.src_path,
-            stdout=subprocess.PIPE,
-        )
-        assert proc.wait() == 0
-        if not self.commit:
-            result = subprocess.check_output(
-                'git rev-parse HEAD',
-                shell=True,
-                cwd=self.src_path,
-            ).split()
-            assert result
-            self.commit = result[0].decode()
-
-    def teardown_method(self, method):
-        shutil.rmtree(self.src_path, ignore_errors=True)
-        shutil.rmtree(self.dest_path, ignore_errors=True)
-
-    def test_clone_repo_existing_branch(self):
-        repo_utils.clone_repo(self.repo_url, self.dest_path, 'main', self.commit)
-        assert os.path.exists(self.dest_path)
-
-    def test_clone_repo_non_existing_branch(self):
-        with raises(BranchNotFoundError):
-            repo_utils.clone_repo(self.repo_url, self.dest_path, 'nobranch', self.commit)
-        assert not os.path.exists(self.dest_path)
-
-    def test_fetch_no_repo(self):
-        fake_dest_path = self.temp_path + '/not_a_repo'
-        assert not os.path.exists(fake_dest_path)
-        with raises(OSError):
-            repo_utils.fetch(fake_dest_path)
-        assert not os.path.exists(fake_dest_path)
-
-    def test_fetch_noop(self):
-        repo_utils.clone_repo(self.repo_url, self.dest_path, 'main', self.commit)
-        repo_utils.fetch(self.dest_path)
-        assert os.path.exists(self.dest_path)
-
-    def test_fetch_branch_no_repo(self):
-        fake_dest_path = self.temp_path + '/not_a_repo'
-        assert not os.path.exists(fake_dest_path)
-        with raises(OSError):
-            repo_utils.fetch_branch(fake_dest_path, 'main')
-        assert not os.path.exists(fake_dest_path)
-
-    def test_fetch_branch_fake_branch(self):
-        repo_utils.clone_repo(self.repo_url, self.dest_path, 'main', self.commit)
-        with raises(BranchNotFoundError):
-            repo_utils.fetch_branch(self.dest_path, 'nobranch')
-
-    @mark.parametrize('git_str',
-                      ["fatal: couldn't find remote ref",
-                       "fatal: Couldn't find remote ref"])
-    @mock.patch('subprocess.Popen')
-    def test_fetch_branch_different_git_versions(self, mock_popen, git_str):
-        """
-        Newer git versions return a lower case string
-        See: https://github.com/git/git/commit/0b9c3afdbfb629363
-        """
-        branch_name = 'nobranch'
-        process_mock = mock.Mock()
-        attrs = {
-            'wait.return_value': 1,
-            'stdout.read.return_value': f"{git_str} {branch_name}".encode(),
-        }
-        process_mock.configure_mock(**attrs)
-        mock_popen.return_value = process_mock
-        with raises(BranchNotFoundError):
-            repo_utils.fetch_branch('', branch_name)
-
-    def test_enforce_existing_branch(self):
-        repo_utils.enforce_repo_state(self.repo_url, self.dest_path,
-                                      'main')
-        assert os.path.exists(self.dest_path)
-
-    def test_enforce_existing_commit(self):
-        import logging
-        logging.getLogger().info(subprocess.check_output("git branch", shell=True, cwd=self.src_path))
-        repo_utils.enforce_repo_state(self.repo_url, self.dest_path,
-                                      'main', self.commit)
-        assert os.path.exists(self.dest_path)
-
-    def test_enforce_non_existing_branch(self):
-        with raises(BranchNotFoundError):
-            repo_utils.enforce_repo_state(self.repo_url, self.dest_path,
-                                          'blah', self.commit)
-        assert not os.path.exists(self.dest_path)
-
-    def test_enforce_non_existing_commit(self):
-        with raises(CommitNotFoundError):
-            repo_utils.enforce_repo_state(self.repo_url, self.dest_path,
-                                          'main', 'c69e90807d222c1719c45c8c758bf6fac3d985f1')
-        assert not os.path.exists(self.dest_path)
-
-    def test_enforce_multiple_calls_same_branch(self):
-        repo_utils.enforce_repo_state(self.repo_url, self.dest_path,
-                                      'main', self.commit)
-        assert os.path.exists(self.dest_path)
-        repo_utils.enforce_repo_state(self.repo_url, self.dest_path,
-                                      'main', self.commit)
-        assert os.path.exists(self.dest_path)
-        repo_utils.enforce_repo_state(self.repo_url, self.dest_path,
-                                      'main', self.commit)
-        assert os.path.exists(self.dest_path)
-
-    def test_enforce_multiple_calls_different_branches(self):
-        with raises(BranchNotFoundError):
-            repo_utils.enforce_repo_state(self.repo_url, self.dest_path,
-                                          'blah1')
-        assert not os.path.exists(self.dest_path)
-        repo_utils.enforce_repo_state(self.repo_url, self.dest_path,
-                                      'main', self.commit)
-        assert os.path.exists(self.dest_path)
-        repo_utils.enforce_repo_state(self.repo_url, self.dest_path,
-                                      'main', self.commit)
-        assert os.path.exists(self.dest_path)
-        with raises(BranchNotFoundError):
-            repo_utils.enforce_repo_state(self.repo_url, self.dest_path,
-                                          'blah2')
-        assert not os.path.exists(self.dest_path)
-        repo_utils.enforce_repo_state(self.repo_url, self.dest_path,
-                                      'main', self.commit)
-        assert os.path.exists(self.dest_path)
-
-    def test_enforce_invalid_branch(self):
-        with raises(ValueError):
-            repo_utils.enforce_repo_state(self.repo_url, self.dest_path, 'a b', self.commit)
-
-    def test_simultaneous_access(self):
-        count = 5
-        with parallel.parallel() as p:
-            for i in range(count):
-                p.spawn(repo_utils.enforce_repo_state, self.repo_url,
-                        self.dest_path, 'main', self.commit)
-            for result in p:
-                assert result is None
-
-    def test_simultaneous_access_different_branches(self):
-        branches = [('main', self.commit),  ('main', self.commit), ('nobranch', 'nocommit'),
-                    ('nobranch', 'nocommit'), ('main', self.commit), ('nobranch', 'nocommit')]
-
-        with parallel.parallel() as p:
-            for branch, commit in branches:
-                if branch == 'main':
-                    p.spawn(repo_utils.enforce_repo_state, self.repo_url,
-                            self.dest_path, branch, commit)
-                else:
-                    dest_path = self.dest_path + '_' + branch
-
-                    def func():
-                        repo_utils.enforce_repo_state(
-                            self.repo_url, dest_path,
-                            branch, commit)
-                    p.spawn(
-                        raises,
-                        BranchNotFoundError,
-                        func,
-                    )
-            for result in p:
-                pass
-
-    URLS_AND_DIRNAMES = [
-        ('git@git.ceph.com/ceph-qa-suite.git', 'git.ceph.com_ceph-qa-suite'),
-        ('git://git.ceph.com/ceph-qa-suite.git', 'git.ceph.com_ceph-qa-suite'),
-        ('https://github.com/ceph/ceph', 'github.com_ceph_ceph'),
-        ('https://github.com/liewegas/ceph.git', 'github.com_liewegas_ceph'),
-        ('file:///my/dir/has/ceph.git', 'my_dir_has_ceph'),
-    ]
-
-    @mark.parametrize("input_, expected", URLS_AND_DIRNAMES)
-    def test_url_to_dirname(self, input_, expected):
-        assert repo_utils.url_to_dirname(input_) == expected
-
-    def test_current_branch(self):
-        repo_utils.clone_repo(self.repo_url, self.dest_path, 'main', self.commit)
-        assert repo_utils.current_branch(self.dest_path) == "main"
diff --git a/teuthology/test/test_report.py b/teuthology/test/test_report.py
deleted file mode 100644 (file)
index a8535df..0000000
+++ /dev/null
@@ -1,86 +0,0 @@
-import json
-import pytest
-import yaml
-
-from teuthology.test import fake_archive
-from teuthology import report
-
-
-@pytest.fixture
-def archive(tmp_path):
-    archive = fake_archive.FakeArchive(archive_base=str(tmp_path))
-    yield archive
-    archive.teardown()
-
-
-@pytest.fixture(autouse=True)
-def reporter(archive):
-    archive.setup()
-    return report.ResultsReporter(archive_base=archive.archive_base)
-
-
-def test_all_runs_one_run(archive, reporter):
-    run_name = "test_all_runs"
-    yaml_path = "examples/3node_ceph.yaml"
-    job_count = 3
-    archive.create_fake_run(run_name, job_count, yaml_path)
-    assert [run_name] == reporter.serializer.all_runs
-
-
-def test_all_runs_three_runs(archive, reporter):
-    run_count = 3
-    runs = {}
-    for i in range(run_count):
-        run_name = "run #%s" % i
-        yaml_path = "examples/3node_ceph.yaml"
-        job_count = 3
-        job_ids = archive.create_fake_run(
-            run_name,
-            job_count,
-            yaml_path)
-        runs[run_name] = job_ids
-    assert sorted(runs.keys()) == sorted(reporter.serializer.all_runs)
-
-
-def test_jobs_for_run(archive, reporter):
-    run_name = "test_jobs_for_run"
-    yaml_path = "examples/3node_ceph.yaml"
-    job_count = 3
-    jobs = archive.create_fake_run(run_name, job_count, yaml_path)
-    job_ids = [str(job['job_id']) for job in jobs]
-
-    got_jobs = reporter.serializer.jobs_for_run(run_name)
-    assert sorted(job_ids) == sorted(got_jobs.keys())
-
-
-def test_running_jobs_for_run(archive, reporter):
-    run_name = "test_jobs_for_run"
-    yaml_path = "examples/3node_ceph.yaml"
-    job_count = 10
-    num_hung = 3
-    archive.create_fake_run(run_name, job_count, yaml_path,
-                                 num_hung=num_hung)
-
-    got_jobs = reporter.serializer.running_jobs_for_run(run_name)
-    assert len(got_jobs) == num_hung
-
-
-def test_json_for_job(archive, reporter):
-    run_name = "test_json_for_job"
-    yaml_path = "examples/3node_ceph.yaml"
-    job_count = 1
-    jobs = archive.create_fake_run(run_name, job_count, yaml_path)
-    job = jobs[0]
-
-    with open(yaml_path) as yaml_file:
-        obj_from_yaml = yaml.safe_load(yaml_file)
-    full_obj = obj_from_yaml.copy()
-    full_obj.update(job['info'])
-    full_obj.update(job['summary'])
-
-    out_json = reporter.serializer.json_for_job(
-        run_name, str(job['job_id']))
-    out_obj = json.loads(out_json)
-    assert full_obj == out_obj
-
-
diff --git a/teuthology/test/test_results.py b/teuthology/test/test_results.py
deleted file mode 100644 (file)
index 3d7ba93..0000000
+++ /dev/null
@@ -1,155 +0,0 @@
-import textwrap
-from teuthology.config import config
-from teuthology import results
-from teuthology import report
-
-from unittest.mock import patch, DEFAULT
-
-
-class TestResultsEmail(object):
-    reference = {
-        'run_name': 'test_name',
-        'jobs': [
-            # Running
-            {'description': 'description for job with name test_name',
-             'job_id': 30481,
-             'name': 'test_name',
-             'log_href': 'http://qa-proxy.ceph.com/teuthology/test_name/30481/teuthology.log',  # noqa
-             'owner': 'job@owner',
-             'duration': None,
-             'status': 'running',
-             },
-            # Waiting
-            {'description': 'description for job with name test_name',
-             'job_id': 62965,
-             'name': 'test_name',
-             'log_href': 'http://qa-proxy.ceph.com/teuthology/test_name/30481/teuthology.log',  # noqa
-             'owner': 'job@owner',
-             'duration': None,
-             'status': 'waiting',
-             },
-            # Queued
-            {'description': 'description for job with name test_name',
-             'job_id': 79063,
-             'name': 'test_name',
-             'log_href': 'http://qa-proxy.ceph.com/teuthology/test_name/30481/teuthology.log',  # noqa
-             'owner': 'job@owner',
-             'duration': None,
-             'status': 'queued',
-             },
-            # Failed
-            {'description': 'description for job with name test_name',
-             'job_id': 88979,
-             'name': 'test_name',
-             'log_href': 'http://qa-proxy.ceph.com/teuthology/test_name/88979/teuthology.log',  # noqa
-             'owner': 'job@owner',
-             'duration': 35190,
-             'success': False,
-             'status': 'fail',
-             'failure_reason': 'Failure reason!',
-             },
-            # Dead
-            {'description': 'description for job with name test_name',
-             'job_id': 69152,
-             'name': 'test_name',
-             'log_href': 'http://qa-proxy.ceph.com/teuthology/test_name/69152/teuthology.log',  # noqa
-             'owner': 'job@owner',
-             'duration': 5225,
-             'success': False,
-             'status': 'dead',
-             'failure_reason': 'Dead reason!',
-             },
-            # Passed
-            {'description': 'description for job with name test_name',
-             'job_id': 68369,
-             'name': 'test_name',
-             'log_href': 'http://qa-proxy.ceph.com/teuthology/test_name/68369/teuthology.log',  # noqa
-             'owner': 'job@owner',
-             'duration': 33771,
-             'success': True,
-             'status': 'pass',
-             },
-        ],
-        'subject': '1 fail, 1 dead, 1 running, 1 waiting, 1 queued, 1 pass in test_name',  # noqa
-        'body': textwrap.dedent("""
-    Test Run: test_name
-    =================================================================
-    info:    http://example.com/test_name/
-    logs:    http://qa-proxy.ceph.com/teuthology/test_name/
-    failed:  1
-    dead:    1
-    running: 1
-    waiting: 1
-    queued:  1
-    passed:  1
-
-
-    Fail
-    =================================================================
-    [88979]  description for job with name test_name
-    -----------------------------------------------------------------
-    time:   09:46:30
-    info:   http://example.com/test_name/88979/
-    log:    http://qa-proxy.ceph.com/teuthology/test_name/88979/
-
-        Failure reason!
-
-
-
-    Dead
-    =================================================================
-    [69152]  description for job with name test_name
-    -----------------------------------------------------------------
-    time:   01:27:05
-    info:   http://example.com/test_name/69152/
-    log:    http://qa-proxy.ceph.com/teuthology/test_name/69152/
-
-        Dead reason!
-
-
-
-    Running
-    =================================================================
-    [30481] description for job with name test_name
-    info:   http://example.com/test_name/30481/
-
-
-
-    Waiting
-    =================================================================
-    [62965] description for job with name test_name
-    info:   http://example.com/test_name/62965/
-
-
-
-    Queued
-    =================================================================
-    [79063] description for job with name test_name
-    info:   http://example.com/test_name/79063/
-
-
-
-    Pass
-    =================================================================
-    [68369] description for job with name test_name
-    time:   09:22:51
-    info:   http://example.com/test_name/68369/
-    """).strip(),
-    }
-
-    def setup_method(self):
-        config.results_ui_server = "http://example.com/"
-        config.archive_server = "http://qa-proxy.ceph.com/teuthology/"
-
-    def test_build_email_body(self):
-        run_name = self.reference['run_name']
-        with patch.multiple(
-                report,
-                ResultsReporter=DEFAULT,
-                ):
-            reporter = report.ResultsReporter()
-            reporter.get_jobs.return_value = self.reference['jobs']
-            (subject, body) = results.build_email_body(
-                run_name, _reporter=reporter)
-        assert subject == self.reference['subject']
-        assert body == self.reference['body']
diff --git a/teuthology/test/test_run.py b/teuthology/test/test_run.py
deleted file mode 100644 (file)
index 50a9e22..0000000
+++ /dev/null
@@ -1,287 +0,0 @@
-import os
-import pytest
-import docopt
-
-from unittest.mock import patch, call
-
-from teuthology import run
-from scripts import run as scripts_run
-from teuthology.test import skipif_teuthology_process
-
-
-class TestRun(object):
-    """ Tests for teuthology.run """
-
-    @patch("teuthology.log.setLevel")
-    @patch("teuthology.setup_log_file")
-    @patch("os.mkdir")
-    def test_set_up_logging(self, m_mkdir, m_setup_log_file, m_setLevel):
-        run.set_up_logging(True, "path/to/archive")
-        m_mkdir.assert_called_with("path/to/archive")
-        m_setup_log_file.assert_called_with("path/to/archive/teuthology.log")
-        assert m_setLevel.called
-
-    # because of how we import things, mock merge_configs from run - where it's used
-    # see: http://www.voidspace.org.uk/python/mock/patch.html#where-to-patch
-    @patch("teuthology.run.merge_configs")
-    def test_setup_config(self, m_merge_configs):
-        config = {"job_id": 1, "foo": "bar"}
-        m_merge_configs.return_value = config
-        result = run.setup_config(["some/config.yaml"])
-        assert m_merge_configs.called
-        assert result["job_id"] == "1"
-        assert result["foo"] == "bar"
-
-    @patch("teuthology.run.merge_configs")
-    def test_setup_config_targets_ok(self, m_merge_configs):
-        config = {"targets": list(range(4)), "roles": list(range(2))}
-        m_merge_configs.return_value = config
-        result = run.setup_config(["some/config.yaml"])
-        assert result["targets"] == [0, 1, 2, 3]
-        assert result["roles"] == [0, 1]
-
-    @patch("teuthology.run.merge_configs")
-    def test_setup_config_targets_invalid(self, m_merge_configs):
-        config = {"targets": range(2), "roles": range(4)}
-        m_merge_configs.return_value = config
-        with pytest.raises(AssertionError):
-            run.setup_config(["some/config.yaml"])
-
-    @patch("teuthology.run.open")
-    def test_write_initial_metadata(self, m_open):
-        config = {"job_id": "123", "foo": "bar"}
-        run.write_initial_metadata(
-            "some/archive/dir",
-            config,
-            "the_name",
-            "the_description",
-            "the_owner",
-        )
-        expected = [
-            call('some/archive/dir/pid', 'w'),
-            call('some/archive/dir/owner', 'w'),
-            call('some/archive/dir/orig.config.yaml', 'w'),
-            call('some/archive/dir/info.yaml', 'w')
-        ]
-        assert m_open.call_args_list == expected
-
-    def test_get_machine_type(self):
-        result = run.get_machine_type(None, {"machine-type": "the_machine_type"})
-        assert result == "the_machine_type"
-
-    def test_get_summary(self):
-        result = run.get_summary("the_owner", "the_description")
-        assert result == {"owner": "the_owner", "description": "the_description", "success": True}
-        result = run.get_summary("the_owner", None)
-        assert result == {"owner": "the_owner", "success": True}
-
-    def test_validate_tasks_invalid(self):
-        config = {"tasks": [{"kernel": "can't be here"}]}
-        with pytest.raises(AssertionError) as excinfo:
-            run.validate_tasks(config)
-        assert excinfo.value.args[0].startswith("kernel installation")
-
-    def test_validate_task_no_tasks(self):
-        result = run.validate_tasks({})
-        assert result == []
-
-    def test_validate_tasks_valid(self):
-        expected = [{"foo": "bar"}, {"bar": "foo"}]
-        result = run.validate_tasks({"tasks": expected})
-        assert result == expected
-
-    def test_validate_tasks_is_list(self):
-        with pytest.raises(AssertionError) as excinfo:
-            run.validate_tasks({"tasks": {"foo": "bar"}})
-        assert excinfo.value.args[0].startswith("Expected list")
-
-    def test_get_initial_tasks_invalid(self):
-        with pytest.raises(AssertionError) as excinfo:
-            run.get_initial_tasks(True, {"targets": "can't be here",
-                                         "roles": "roles" }, "machine_type")
-        assert excinfo.value.args[0].startswith("You cannot")
-
-    def test_get_inital_tasks(self):
-        config = {"roles": range(2), "kernel": "the_kernel", "use_existing_cluster": False}
-        result = run.get_initial_tasks(True, config, "machine_type")
-        assert {"internal.lock_machines": (2, "machine_type")} in result
-        assert {"kernel": "the_kernel"} in result
-        # added because use_existing_cluster == False
-        assert {'internal.vm_setup': None} in result
-        assert {'internal.buildpackages_prep': None} in result
-
-    # When tests are run in a teuthology process using the py.test 
-    # API, tasks will have already been imported. Patching sys.path 
-    # (and even calling sys.path_importer_cache.clear()) doesn't seem 
-    # to help "forget" where the tasks are, keeping this test from 
-    # passing. The test isn't critical to run in every single 
-    # environment, so skip.
-    @skipif_teuthology_process
-    @patch("teuthology.run.fetch_qa_suite")
-    def test_fetch_tasks_if_needed(self, m_fetch_qa_suite):
-        config = {"suite_path": "/some/suite/path", "suite_branch": "feature_branch",
-                  "suite_sha1": "commit"}
-        m_fetch_qa_suite.return_value = "/some/other/suite/path"
-        result = run.fetch_tasks_if_needed(config)
-        m_fetch_qa_suite.assert_called_with("feature_branch", commit="commit")
-        assert result == "/some/other/suite/path/qa"
-
-    @patch("teuthology.run.get_status")
-    @patch("yaml.safe_dump")
-    @patch("teuthology.report.try_push_job_info")
-    @patch("teuthology.run.email_results")
-    @patch("teuthology.run.open")
-    @patch("sys.exit")
-    def test_report_outcome(self, m_sys_exit, m_open, m_email_results, m_try_push_job_info, m_safe_dump, m_get_status):
-        m_get_status.return_value = "fail"
-        summary = {"failure_reason": "reasons"}
-        summary_dump = "failure_reason: reasons\n"
-        config = {"email-on-error": True}
-        config_dump = "email-on-error: true\n"
-        m_safe_dump.side_effect = [None, summary_dump, config_dump]
-        run.report_outcome(config, "the/archive/path", summary)
-        m_try_push_job_info.assert_called_with(config, summary)
-        m_open.assert_called_with("the/archive/path/summary.yaml", "w")
-        assert m_email_results.called
-        assert m_open.called
-        assert m_sys_exit.called
-
-    @patch("teuthology.run.set_up_logging")
-    @patch("teuthology.run.setup_config")
-    @patch("teuthology.run.get_user")
-    @patch("teuthology.run.write_initial_metadata")
-    @patch("teuthology.report.try_push_job_info")
-    @patch("teuthology.run.get_machine_type")
-    @patch("teuthology.run.get_summary")
-    @patch("yaml.safe_dump")
-    @patch("teuthology.run.validate_tasks")
-    @patch("teuthology.run.get_initial_tasks")
-    @patch("teuthology.run.fetch_tasks_if_needed")
-    @patch("teuthology.run.run_tasks")
-    @patch("teuthology.run.report_outcome")
-    def test_main(self, m_report_outcome, m_run_tasks, m_fetch_tasks_if_needed, m_get_initial_tasks, m_validate_tasks,
-                  m_safe_dump, m_get_summary, m_get_machine_type, m_try_push_job_info, m_write_initial_metadata,
-                  m_get_user, m_setup_config, m_set_up_logging):
-        """ This really should be an integration test of some sort. """
-        config = {"job_id": 1}
-        m_setup_config.return_value = config
-        m_get_machine_type.return_value = "machine_type"
-        doc = scripts_run.__doc__
-        args = docopt.docopt(doc, [
-            "--verbose",
-            "--archive", "some/archive/dir",
-            "--description", "the_description",
-            "--lock",
-            "--os-type", "os_type",
-            "--os-version", "os_version",
-            "--block",
-            "--name", "the_name",
-            "--suite-path", "some/suite/dir",
-            "path/to/config.yml",
-        ])
-        m_get_user.return_value = "the_owner"
-        m_get_summary.return_value = dict(success=True, owner="the_owner", description="the_description")
-        m_validate_tasks.return_value = ['task3']
-        m_get_initial_tasks.return_value = ['task1', 'task2']
-        m_fetch_tasks_if_needed.return_value = "some/suite/dir"
-        run.main(args)
-        m_set_up_logging.assert_called_with(True, "some/archive/dir")
-        m_setup_config.assert_called_with(["path/to/config.yml"])
-        m_write_initial_metadata.assert_called_with(
-            "some/archive/dir",
-            config,
-            "the_name",
-            "the_description",
-            "the_owner"
-        )
-        m_try_push_job_info.assert_called_with(config, dict(status='running', pid=os.getpid()))
-        m_get_machine_type.assert_called_with(None, config)
-        m_get_summary.assert_called_with("the_owner", "the_description")
-        m_get_initial_tasks.assert_called_with(True, config, "machine_type")
-        m_fetch_tasks_if_needed.assert_called_with(config)
-        assert m_report_outcome.called
-        args, kwargs = m_run_tasks.call_args
-        fake_ctx = kwargs["ctx"]._conf
-        # fields that must be in ctx for the tasks to behave
-        expected_ctx = ["verbose", "archive", "description", "owner", "lock", "machine_type", "os_type", "os_version",
-                        "block", "name", "suite_path", "config", "summary"]
-        for key in expected_ctx:
-            assert key in fake_ctx
-        assert isinstance(fake_ctx["config"], dict)
-        assert isinstance(fake_ctx["summary"], dict)
-        assert "tasks" in fake_ctx["config"]
-        # ensures that values missing in args are added with the correct value
-        assert fake_ctx["owner"] == "the_owner"
-        assert fake_ctx["machine_type"] == "machine_type"
-        # ensures os_type and os_version are property overwritten
-        assert fake_ctx["config"]["os_type"] == "os_type"
-        assert fake_ctx["config"]["os_version"] == "os_version"
-
-    @patch("teuthology.run.set_up_logging")
-    @patch("teuthology.run.setup_config")
-    @patch("teuthology.run.get_user")
-    @patch("teuthology.run.write_initial_metadata")
-    @patch("teuthology.report.try_push_job_info")
-    @patch("teuthology.run.get_machine_type")
-    @patch("teuthology.run.get_summary")
-    @patch("yaml.safe_dump")
-    @patch("teuthology.run.validate_tasks")
-    @patch("teuthology.run.get_initial_tasks")
-    @patch("teuthology.run.fetch_tasks_if_needed")
-    @patch("teuthology.run.run_tasks")
-    @patch("teuthology.run.report_outcome")
-    def test_main_interactive(
-        self,
-        m_report_outcome,
-        m_run_tasks,
-        m_fetch_tasks_if_needed,
-        m_get_initial_tasks,
-        m_validate_tasks,
-        m_safe_dump,
-        m_get_summary,
-        m_get_machine_type,
-        m_try_push_job_info,
-        m_write_initial_metadata,
-        m_get_user,
-        m_setup_config,
-        m_set_up_logging,
-    ):
-        config = {"job_id": 1}
-        m_setup_config.return_value = config
-        m_get_machine_type.return_value = "machine_type"
-        doc = scripts_run.__doc__
-        args = docopt.docopt(doc, [
-            "--interactive-on-error",
-            "path/to/config.yml",
-        ])
-        run.main(args)
-        args, kwargs = m_run_tasks.call_args
-        fake_ctx = kwargs["ctx"]._conf
-        assert fake_ctx['interactive_on_error'] is True
-
-    def test_get_teuthology_command(self):
-        doc = scripts_run.__doc__
-        args = docopt.docopt(doc, [
-            "--archive", "some/archive/dir",
-            "--description", "the_description",
-            "--lock",
-            "--block",
-            "--name", "the_name",
-            "--suite-path", "some/suite/dir",
-            "path/to/config.yml", "path/to/config2.yaml",
-        ])
-        result = run.get_teuthology_command(args)
-        result = result.split()
-        expected = [
-            "teuthology",
-            "path/to/config.yml", "path/to/config2.yaml",
-            "--suite-path", "some/suite/dir",
-            "--lock",
-            "--description", "the_description",
-            "--name", "the_name",
-            "--block",
-            "--archive", "some/archive/dir",
-        ]
-        assert len(result) == len(expected)
-        for arg in expected:
-            assert arg in result
diff --git a/teuthology/test/test_safepath.py b/teuthology/test/test_safepath.py
deleted file mode 100644 (file)
index afc81cd..0000000
+++ /dev/null
@@ -1,55 +0,0 @@
-from teuthology import safepath
-
-class TestSafepath(object):
-    def test_simple(self):
-        got = safepath.munge('foo')
-        assert got == 'foo'
-
-    def test_empty(self):
-        # really odd corner case
-        got = safepath.munge('')
-        assert got == '_'
-
-    def test_slash(self):
-        got = safepath.munge('/')
-        assert got == '_'
-
-    def test_slashslash(self):
-        got = safepath.munge('//')
-        assert got == '_'
-
-    def test_absolute(self):
-        got = safepath.munge('/evil')
-        assert got == 'evil'
-
-    def test_absolute_subdir(self):
-        got = safepath.munge('/evil/here')
-        assert got == 'evil/here'
-
-    def test_dot_leading(self):
-        got = safepath.munge('./foo')
-        assert got == 'foo'
-
-    def test_dot_middle(self):
-        got = safepath.munge('evil/./foo')
-        assert got == 'evil/foo'
-
-    def test_dot_trailing(self):
-        got = safepath.munge('evil/foo/.')
-        assert got == 'evil/foo'
-
-    def test_dotdot(self):
-        got = safepath.munge('../evil/foo')
-        assert got == '_./evil/foo'
-
-    def test_dotdot_subdir(self):
-        got = safepath.munge('evil/../foo')
-        assert got == 'evil/_./foo'
-
-    def test_hidden(self):
-        got = safepath.munge('.evil')
-        assert got == '_evil'
-
-    def test_hidden_subdir(self):
-        got = safepath.munge('foo/.evil')
-        assert got == 'foo/_evil'
diff --git a/teuthology/test/test_schedule.py b/teuthology/test/test_schedule.py
deleted file mode 100644 (file)
index dd0a68f..0000000
+++ /dev/null
@@ -1,45 +0,0 @@
-from teuthology.schedule import build_config
-from teuthology.misc import get_user
-
-
-class TestSchedule(object):
-    basic_args = {
-        '--verbose': False,
-        '--owner': 'OWNER',
-        '--description': 'DESC',
-        '--email': 'EMAIL',
-        '--first-in-suite': False,
-        '--last-in-suite': True,
-        '--name': 'NAME',
-        '--worker': 'tala',
-        '--timeout': '6',
-        '--priority': '99',
-        # TODO: make this work regardless of $PWD
-        #'<conf_file>': ['../../examples/3node_ceph.yaml',
-        #                '../../examples/3node_rgw.yaml'],
-        }
-
-    def test_basic(self):
-        expected = {
-            'description': 'DESC',
-            'email': 'EMAIL',
-            'first_in_suite': False,
-            'last_in_suite': True,
-            'machine_type': 'tala',
-            'name': 'NAME',
-            'owner': 'OWNER',
-            'priority': 99,
-            'results_timeout': '6',
-            'verbose': False,
-            'tube': 'tala',
-        }
-
-        job_dict = build_config(self.basic_args)
-        assert job_dict == expected
-
-    def test_owner(self):
-        args = self.basic_args
-        args['--owner'] = None
-        job_dict = build_config(self.basic_args)
-        assert job_dict['owner'] == 'scheduled_%s' % get_user()
-
diff --git a/teuthology/test/test_scrape.py b/teuthology/test/test_scrape.py
deleted file mode 100644 (file)
index ed281b4..0000000
+++ /dev/null
@@ -1,205 +0,0 @@
-from __future__ import with_statement
-
-import glob
-import gzip
-import os
-import shutil
-import tempfile
-import yaml
-from teuthology import scrape
-
-class FakeResultDir(object):
-    """Mocks a Result Directory"""
-
-    def __init__(self,
-                failure_reason="Dummy reason",
-                assertion="FAILED assert 1 == 2\n",
-                blank_backtrace=False,
-                assertion_osd=False,
-    ):
-        self.failure_reason = failure_reason
-        self.assertion = assertion
-        self.blank_backtrace = blank_backtrace
-        self.path = tempfile.mkdtemp()
-        
-        with open(os.path.join(self.path, "config.yaml"), "w") as f:
-            yaml.dump({"description": "Dummy test"}, f)
-        
-        with open(os.path.join(self.path, "summary.yaml"), "w") as f:
-            yaml.dump({
-                "success": "false",
-                "failure_reason": self.failure_reason
-            }, f)
-
-        with open(os.path.join(self.path, "teuthology.log"), "w") as f:
-            if not self.blank_backtrace:
-                f.write(" ceph version 1000\n")
-                f.write(".stderr: Dummy error\n")
-                f.write(self.assertion)
-            f.write(" NOTE: a copy of the executable dummy text\n")
-
-        if assertion_osd:
-            host = "host1"
-            rem_log_dir = os.path.join(self.path, "remote", host, "log")
-            os.makedirs(rem_log_dir, exist_ok=True)
-            ceph_mon_log = os.path.join(rem_log_dir, "ceph-osd.0.log")
-            with open(ceph_mon_log, "w") as f:
-                f.write("ceph version 1000\n")
-                f.write(self.assertion)
-
-    def __enter__(self):
-        return self
-
-    def __exit__(self, exc_typ, exc_val, exc_tb):
-        shutil.rmtree(self.path)
-
-class TestScrape(object):
-    """Tests for teuthology.scrape"""
-
-    def test_grep(self):
-        with FakeResultDir() as d:
-            filepath = os.path.join(d.path, "scrapetest.txt")
-            with open(filepath, 'w') as f:
-                f.write("Ceph is an open-source software storage platform\n\
-                        Teuthology is used for testing.")
-
-            #System level grep is called
-            value1 = scrape.grep(filepath, "software")
-            value2 = scrape.grep(filepath, "device")
-
-            assert value1 ==\
-                ['Ceph is an open-source software storage platform', '']
-            assert value2 == []
-
-    def test_job(self):
-        with FakeResultDir() as d:
-            job = scrape.Job(d.path, 1)
-            assert job.get_success() == "false"
-            assert job.get_assertion() == "FAILED assert 1 == 2"
-            assert job.get_last_tlog_line() ==\
-                b"NOTE: a copy of the executable dummy text"
-            assert job.get_failure_reason() == "Dummy reason"
-
-    def test_timeoutreason(self):
-        with FakeResultDir(failure_reason=\
-            "status 124:  timeout '123 /home/ubuntu/cephtest/workunit.client.0/cephtool/test.sh'") as d:
-            job = scrape.Job(d.path, 1)
-            assert scrape.TimeoutReason.could_be(job)
-            assert scrape.TimeoutReason(job).match(job)
-
-    def test_deadreason(self):
-        with FakeResultDir() as d:
-            job = scrape.Job(d.path, 1)
-            #Summary is present
-            #So this cannot be a DeadReason
-            assert not scrape.DeadReason.could_be(job)
-
-    def test_lockdepreason(self):
-        lkReason = None
-        with FakeResultDir(assertion=\
-            "FAILED assert common/lockdep reason\n") as d:
-            job = scrape.Job(d.path, 1)
-            assert scrape.LockdepReason.could_be(job)
-
-            lkReason = scrape.LockdepReason(job)
-            #Backtraces of same jobs must match 100%
-            assert lkReason.match(job)
-        with FakeResultDir(blank_backtrace=True) as d:
-            #Corresponding to 0% match
-            assert not lkReason.match(scrape.Job(d.path, 2))
-
-    def test_assertionreason(self):
-        with FakeResultDir() as d:
-            job = scrape.Job(d.path, 1)
-            assert scrape.AssertionReason.could_be(job)
-
-    def test_genericreason(self):
-        d1 = FakeResultDir(blank_backtrace=True)
-        d2 = FakeResultDir(failure_reason="Dummy dummy")
-        d3 = FakeResultDir()
-
-        job1 = scrape.Job(d1.path, 1)
-        job2 = scrape.Job(d2.path, 2)
-        job3 = scrape.Job(d3.path, 3)
-
-        reason = scrape.GenericReason(job3)
-
-        assert reason.match(job2)
-        assert not reason.match(job1)
-
-        shutil.rmtree(d1.path)
-        shutil.rmtree(d2.path)
-        shutil.rmtree(d3.path)
-
-    def test_valgrindreason(self):
-        vreason = None
-        with FakeResultDir(
-            failure_reason="saw valgrind issues",
-            assertion="2014-08-22T20:07:18.668 ERROR:tasks.ceph:saw valgrind issue   <kind>Leak_DefinitelyLost</kind> in /var/log/ceph/valgrind/osd.3.log.gz\n"
-        ) as d:
-            job = scrape.Job(d.path, 1)
-            assert scrape.ValgrindReason.could_be(job)
-            
-            vreason = scrape.ValgrindReason(job)
-            assert vreason.match(job)
-
-    def test_give_me_a_reason(self):
-        with FakeResultDir() as d:
-            job = scrape.Job(d.path, 1)
-            
-            assert type(scrape.give_me_a_reason(job)) == scrape.AssertionReason
-
-        #Test the lockdep ordering
-        with FakeResultDir(assertion=\
-        "FAILED assert common/lockdep reason\n") as d:
-            job = scrape.Job(d.path, 1)
-            assert type(scrape.give_me_a_reason(job)) == scrape.LockdepReason
-
-    def test_scraper(self):
-        d = FakeResultDir()
-        os.mkdir(os.path.join(d.path, "test"))
-        shutil.move(
-            os.path.join(d.path, "config.yaml"),
-            os.path.join(d.path, "test", "config.yaml")
-        )
-        shutil.move(
-            os.path.join(d.path, "summary.yaml"),
-            os.path.join(d.path, "test", "summary.yaml")
-        )
-        shutil.move(
-            os.path.join(d.path, "teuthology.log"),
-            os.path.join(d.path, "test", "teuthology.log")
-        )
-
-        scrape.Scraper(d.path).analyze()
-
-        #scrape.log should be created
-        assert os.path.exists(os.path.join(d.path, "scrape.log"))
-
-        shutil.rmtree(d.path)
-
-    def test_gzip_backtrace_decode(self):
-        with FakeResultDir(assertion="FAILED assert dummy backtrace line",
-                        blank_backtrace=True,
-                        assertion_osd=True) as d:
-
-            with open(os.path.join(d.path, "teuthology.log"), "a") as root_log:
-                root_log.write(
-                    "command crashed with signal SIGSEGV tasks.ceph.osd.0.host1.stderr\n"
-                )
-
-            pattern = os.path.join(d.path, "**", "ceph-osd.0.log")
-            raws = glob.glob(pattern, recursive=True)
-            assert len(raws) == 1, f"expected one raw log, found: {raws}"
-            raw_log = raws[0]
-            gz_log = raw_log + ".gz"
-
-            with gzip.open(gz_log, "wb") as out:
-                out.write(open(raw_log, "rb").read())
-            os.remove(raw_log)
-
-            assert not os.path.exists(raw_log)
-            assert os.path.exists(gz_log)
-
-            job = scrape.Job(d.path, 1)
-            assert job.get_assertion() == "FAILED assert dummy backtrace line"
\ No newline at end of file
diff --git a/teuthology/test/test_timer.py b/teuthology/test/test_timer.py
deleted file mode 100644 (file)
index 312a9c8..0000000
+++ /dev/null
@@ -1,80 +0,0 @@
-from teuthology import timer
-
-from unittest.mock import MagicMock, patch, mock_open
-from time import time
-
-
-class TestTimer(object):
-    def test_data_empty(self):
-        self.timer = timer.Timer()
-        assert self.timer.data == dict()
-
-    def test_data_one_mark(self):
-        self.timer = timer.Timer()
-        # Avoid failing if ~1ms elapses between these two calls
-        self.timer.precision = 2
-        self.timer.mark()
-        assert len(self.timer.data['marks']) == 1
-        assert self.timer.data['marks'][0]['interval'] == 0
-        assert self.timer.data['marks'][0]['message'] == ''
-
-    def test_data_five_marks(self):
-        self.timer = timer.Timer()
-        for i in range(5):
-            self.timer.mark(str(i))
-        assert len(self.timer.data['marks']) == 5
-        assert [m['message'] for m in self.timer.data['marks']] == \
-            ['0', '1', '2', '3', '4']
-
-    def test_intervals(self):
-        fake_time = MagicMock()
-        with patch('teuthology.timer.time.time', fake_time):
-            self.timer = timer.Timer()
-            now = start_time = fake_time.return_value = time()
-            intervals = [0, 1, 1, 2, 3, 5, 8]
-            for i in intervals:
-                now += i
-                fake_time.return_value = now
-                self.timer.mark(str(i))
-
-        summed_intervals = [sum(intervals[:x + 1]) for x in range(len(intervals))]
-        result_intervals = [m['interval'] for m in self.timer.data['marks']]
-        assert result_intervals == summed_intervals
-        assert self.timer.data['start'] == \
-            self.timer.get_datetime_string(start_time)
-        assert self.timer.data['end'] == \
-            self.timer.get_datetime_string(start_time + summed_intervals[-1])
-        assert [m['message'] for m in self.timer.data['marks']] == \
-            [str(i) for i in intervals]
-        assert self.timer.data['elapsed'] == summed_intervals[-1]
-
-    def test_write(self):
-        _path = '/path'
-        _safe_dump = MagicMock(name='safe_dump')
-        with patch('teuthology.timer.yaml.safe_dump', _safe_dump):
-            with patch('teuthology.timer.open', mock_open(), create=True) as _open:
-                self.timer = timer.Timer(path=_path)
-                assert self.timer.path == _path
-                self.timer.write()
-                _open.assert_called_once_with(_path, 'w')
-                _safe_dump.assert_called_once_with(
-                    dict(),
-                    _open.return_value.__enter__.return_value,
-                    default_flow_style=False,
-                )
-
-    def test_sync(self):
-        _path = '/path'
-        _safe_dump = MagicMock(name='safe_dump')
-        with patch('teuthology.timer.yaml.safe_dump', _safe_dump):
-            with patch('teuthology.timer.open', mock_open(), create=True) as _open:
-                self.timer = timer.Timer(path=_path, sync=True)
-                assert self.timer.path == _path
-                assert self.timer.sync is True
-                self.timer.mark()
-                _open.assert_called_once_with(_path, 'w')
-                _safe_dump.assert_called_once_with(
-                    self.timer.data,
-                    _open.return_value.__enter__.return_value,
-                    default_flow_style=False,
-                )
diff --git a/teuthology/test/test_vps_os_vers_parameter_checking.py b/teuthology/test/test_vps_os_vers_parameter_checking.py
deleted file mode 100644 (file)
index 8acfed0..0000000
+++ /dev/null
@@ -1,84 +0,0 @@
-from unittest.mock import patch, Mock
-
-import teuthology.lock.util
-from teuthology import provision
-
-
-class TestVpsOsVersionParamCheck(object):
-
-    def setup_method(self):
-        self.fake_ctx = Mock()
-        self.fake_ctx.machine_type = 'vps'
-        self.fake_ctx.num_to_lock = 1
-        self.fake_ctx.lock = False
-
-        def fake_downburst_executable():
-            return ''
-
-        self.fake_downburst_executable = fake_downburst_executable
-
-    def test_ubuntu_noble(self):
-        self.fake_ctx.os_type = 'ubuntu'
-        self.fake_ctx.os_version = 'noble'
-        with patch.multiple(
-            provision.downburst,
-            downburst_executable=self.fake_downburst_executable,
-        ):
-            check_value = teuthology.lock.util.vps_version_or_type_valid(
-                self.fake_ctx.machine_type,
-                self.fake_ctx.os_type,
-                self.fake_ctx.os_version)
-
-        assert check_value
-
-    def test_ubuntu_number(self):
-        self.fake_ctx.os_type = 'ubuntu'
-        self.fake_ctx.os_version = '24.04'
-        with patch.multiple(
-            provision.downburst,
-            downburst_executable=self.fake_downburst_executable,
-        ):
-            check_value = teuthology.lock.util.vps_version_or_type_valid(
-                self.fake_ctx.machine_type,
-                self.fake_ctx.os_type,
-                self.fake_ctx.os_version)
-        assert check_value
-
-    def test_mixup(self):
-        self.fake_ctx.os_type = '6.5'
-        self.fake_ctx.os_version = 'rhel'
-        with patch.multiple(
-            provision.downburst,
-            downburst_executable=self.fake_downburst_executable,
-        ):
-            check_value = teuthology.lock.util.vps_version_or_type_valid(
-                self.fake_ctx.machine_type,
-                self.fake_ctx.os_type,
-                self.fake_ctx.os_version)
-        assert not check_value
-
-    def test_bad_type(self):
-        self.fake_ctx.os_type = 'aardvark'
-        self.fake_ctx.os_version = '6.5'
-        with patch.multiple(
-            provision.downburst,
-            downburst_executable=self.fake_downburst_executable,
-        ):
-            check_value = teuthology.lock.util.vps_version_or_type_valid(
-                self.fake_ctx.machine_type,
-                self.fake_ctx.os_type,
-                self.fake_ctx.os_version)
-        assert not check_value
-
-    def test_bad_version(self):
-        self.fake_ctx.os_type = 'ubuntu'
-        self.fake_ctx.os_version = 'vampire_bat'
-        with patch.multiple(
-            provision.downburst,
-            downburst_executable=self.fake_downburst_executable,
-        ):
-            check_value = teuthology.lock.util.vps_version_or_type_valid(
-                self.fake_ctx.machine_type,
-                self.fake_ctx.os_type,
-                self.fake_ctx.os_version)
-        assert not check_value
diff --git a/teuthology/util/test/files/test_unit_test.xml b/teuthology/util/test/files/test_unit_test.xml
deleted file mode 100644 (file)
index bd9c734..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<testsuite name="nosetests" tests="644" errors="0" failures="1" skip="79">
-<testcase classname="s3tests_boto3.functional.test_s3" name="test_cors_origin_response" time="3.205"></testcase>
-<testcase classname="s3tests_boto3.functional.test_s3" name="test_cors_origin_wildcard" time="3.081"></testcase>
-<testcase classname="s3tests_boto3.functional.test_s3" name="test_cors_header_option" time="3.119"></testcase>
-<testcase classname="s3tests_boto3.functional.test_s3" name="test_set_bucket_tagging" time="0.059"><failure type="builtins.AssertionError" message="'NoSuchTagSetError' != 'NoSuchTagSet'"></failure></testcase>
-</testsuite>
\ No newline at end of file
diff --git a/teuthology/util/test/files/test_valgrind.xml b/teuthology/util/test/files/test_valgrind.xml
deleted file mode 100644 (file)
index 41bf837..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-<?xml version="1.0"?>
-<valgrindoutput>
-<error>
-  <unique>0x870fc</unique>
-  <tid>1</tid>
-  <kind>Leak_DefinitelyLost</kind>
-  <xwhat>
-    <text>1,234 bytes in 1 blocks are definitely lost in loss record 198 of 201</text>
-    <leakedbytes>1234</leakedbytes>
-    <leakedblocks>1</leakedblocks>
-  </xwhat>
-  <stack>
-    <frame>
-      <ip>0x4C39B6F</ip>
-      <obj>/usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so</obj>
-      <fn>operator new[](unsigned long)</fn>
-      <dir>/builddir/build/BUILD/valgrind-3.19.0/coregrind/m_replacemalloc</dir>
-      <file>vg_replace_malloc.c</file>
-      <line>640</line>
-    </frame>
-    <frame>
-      <ip>0xF3F4B5</ip>
-      <obj>/usr/bin/ceph-osd</obj>
-      <fn>ceph::common::leak_some_memory()</fn>
-      <dir>/usr/src/debug/ceph-18.0.0-5567.g64a4fc94.el8.x86_64/src/common</dir>
-      <file>ceph_context.cc</file>
-      <line>510</line>
-    </frame>
-  </stack>
-</error>
-</valgrindoutput>
diff --git a/teuthology/util/test/test_scanner.py b/teuthology/util/test/test_scanner.py
deleted file mode 100644 (file)
index 928d430..0000000
+++ /dev/null
@@ -1,191 +0,0 @@
-from mock import patch, MagicMock
-
-from io import BytesIO
-import os, io
-
-from teuthology.orchestra import remote
-from teuthology.util.scanner import UnitTestScanner, ValgrindScanner
-
-
-class MockFile(io.StringIO):
-    def close(self):
-        pass
-
-
-class TestUnitTestScanner(object):
-
-    def setup_method(self):
-        self.remote = remote.Remote(
-            name='jdoe@xyzzy.example.com', ssh=MagicMock())
-        self.test_values = {
-            "xml_path": os.path.dirname(__file__) + "/files/test_unit_test.xml",
-            "error_msg": "FAILURE: Test `test_set_bucket_tagging` of `s3tests_boto3.functional.test_s3`. \
-Reason: 'NoSuchTagSetError' != 'NoSuchTagSet'.",
-            "summary_data": [{'failed_testsuites': {'s3tests_boto3.functional.test_s3': 
-                                [{'kind': 'failure', 'testcase': 'test_set_bucket_tagging', 
-                                  'message': "'NoSuchTagSetError' != 'NoSuchTagSet'"}]}, 
-                              'num_of_failures': 1, 
-                              'file_path': f'{os.path.dirname(__file__)}/files/test_unit_test.xml'}],
-            "yaml_data": r"""- failed_testsuites:
-    s3tests_boto3.functional.test_s3:
-    - kind: failure
-      message: '''NoSuchTagSetError'' != ''NoSuchTagSet'''
-      testcase: test_set_bucket_tagging
-  file_path: {file_dir}/files/test_unit_test.xml
-  num_of_failures: 1
-""".format(file_dir=os.path.dirname(__file__))
-        }
-
-    @patch('teuthology.util.scanner.UnitTestScanner.write_summary')
-    def test_scan_and_write(self, m_write_summary):
-        xml_path = self.test_values["xml_path"]
-        self.remote.ssh.exec_command.return_value = (None, BytesIO(xml_path.encode('utf-8')), None)
-        m_open = MagicMock()
-        m_open.return_value = open(xml_path, "rb")
-        self.remote._sftp_open_file = m_open
-        result = UnitTestScanner(remote=self.remote).scan_and_write(xml_path, "test_summary.yaml")
-        assert result == "(total 1 failed) " + self.test_values["error_msg"]
-
-    def test_parse(self):
-        xml_content = b'<?xml version="1.0" encoding="UTF-8"?>\n<testsuite name="xyz" tests="1" \
-errors="0" failures="1">\n<testcase classname="xyz" name="abc" time="0.059"><failure \
-type="builtins.AssertionError" message="error_msg"></failure></testcase>\n</testsuite>'
-        scanner = UnitTestScanner(self.remote)
-        result = scanner._parse(xml_content)
-        assert result == (
-            'FAILURE: Test `abc` of `xyz`. Reason: error_msg.', 
-            {'failed_testsuites': {'xyz': 
-                [{'kind': 'failure','message': 'error_msg','testcase': 'abc'}]},
-            'num_of_failures': 1
-            }
-        )
-    
-    def test_scan_file(self):
-        xml_path = self.test_values["xml_path"]
-        m_open = MagicMock()
-        m_open.return_value = open(xml_path, "rb")
-        self.remote._sftp_open_file = m_open
-        scanner = UnitTestScanner(remote=self.remote)
-        result = scanner.scan_file(xml_path)
-        assert result == self.test_values["error_msg"]
-        assert scanner.summary_data == self.test_values["summary_data"]
-
-    def test_scan_all_files(self):
-        xml_path = self.test_values["xml_path"]
-        self.remote.ssh.exec_command.return_value = (None, BytesIO(xml_path.encode('utf-8')), None)
-        m_open = MagicMock()
-        m_open.return_value = open(xml_path, "rb")
-        self.remote._sftp_open_file = m_open
-        scanner = UnitTestScanner(remote=self.remote)
-        result = scanner.scan_all_files(xml_path)
-        assert result == [self.test_values["error_msg"]]
-                          
-    @patch('builtins.open')
-    def test_write_summary(self, m_open):
-        scanner = UnitTestScanner(self.remote)
-        mock_yaml_file = MockFile()
-        scanner.summary_data = self.test_values["summary_data"]
-        m_open.return_value = mock_yaml_file
-        scanner.write_summary("path/file.yaml")
-        written_content = mock_yaml_file.getvalue()             
-        assert written_content == self.test_values["yaml_data"]    
-
-
-class TestValgrindScanner(object):
-
-    def setup_method(self):
-        self.remote = remote.Remote(
-            name='jdoe@xyzzy.example.com', ssh=MagicMock())
-        self.test_values = {
-            "xml_path": os.path.dirname(__file__) + "/files/test_valgrind.xml",
-            "error_msg": "valgrind error: Leak_DefinitelyLost\noperator new[]\
-(unsigned long)\nceph::common::leak_some_memory()",
-            "summary_data": [{'kind': 'Leak_DefinitelyLost', 'traceback': [{'file': 
-                '/builddir/build/BUILD/valgrind-3.19.0/coregrind/m_replacemalloc/vg_replace_malloc.c', 
-                'line': '640', 'function': 'operator new[](unsigned long)'}, 
-                {'file': '/usr/src/debug/ceph-18.0.0-5567.g64a4fc94.el8.x86_64/src/common/ceph_context.cc', 
-                 'line': '510', 'function': 'ceph::common::leak_some_memory()'}], 'file_path': 
-                 f'{os.path.dirname(__file__)}/files/test_valgrind.xml'}],
-            "yaml_data": r"""- file_path: {file_dir}/files/test_valgrind.xml
-  kind: Leak_DefinitelyLost
-  traceback:
-  - file: /builddir/build/BUILD/valgrind-3.19.0/coregrind/m_replacemalloc/vg_replace_malloc.c
-    function: operator new[](unsigned long)
-    line: '640'
-  - file: /usr/src/debug/ceph-18.0.0-5567.g64a4fc94.el8.x86_64/src/common/ceph_context.cc
-    function: ceph::common::leak_some_memory()
-    line: '510'
-""".format(file_dir=os.path.dirname(__file__))
-        }
-
-    def test_parse_with_traceback(self):
-        xml_content = b'''<?xml version="1.0"?>
-<valgrindoutput>
-<error>
-  <kind>Leak_DefinitelyLost</kind>
-  <stack>
-    <frame>
-      <fn>func()</fn>
-      <dir>/dir</dir>
-      <file>file1.ext</file>
-      <line>640</line>
-    </frame>
-  </stack>
-</error>
-</valgrindoutput>
-'''
-        scanner = ValgrindScanner(self.remote)
-        result = scanner._parse(xml_content)
-        assert result == (
-            'valgrind error: Leak_DefinitelyLost\nfunc()', 
-            {'kind': 'Leak_DefinitelyLost', 'traceback': 
-                [{'file': '/dir/file1.ext', 'line': '640', 'function': 'func()'}]
-            }
-        )
-
-    def test_parse_without_trackback(self):
-        xml_content = b'''<?xml version="1.0"?>
-<valgrindoutput>
-<error>
-  <kind>Leak_DefinitelyLost</kind>
-  <stack>
-  </stack>
-</error>
-</valgrindoutput>
-'''
-        scanner = ValgrindScanner(self.remote)
-        result = scanner._parse(xml_content)
-        assert result == (
-            'valgrind error: Leak_DefinitelyLost\n', 
-            {'kind': 'Leak_DefinitelyLost', 'traceback': []}
-        )
-    
-    def test_scan_file(self):
-        xml_path = self.test_values["xml_path"]
-        m_open = MagicMock()
-        m_open.return_value = open(xml_path, "rb")
-        self.remote._sftp_open_file = m_open
-        scanner = ValgrindScanner(remote=self.remote)
-        result = scanner.scan_file(xml_path)
-        assert result == self.test_values["error_msg"]
-        assert scanner.summary_data == self.test_values["summary_data"]
-
-    def test_scan_all_files(self):
-        xml_path = self.test_values["xml_path"]
-        self.remote.ssh.exec_command.return_value = (None, BytesIO(xml_path.encode('utf-8')), None)
-        m_open = MagicMock()
-        m_open.return_value = open(xml_path, "rb")
-        self.remote._sftp_open_file = m_open
-        scanner = ValgrindScanner(remote=self.remote)
-        result = scanner.scan_all_files(xml_path)
-        assert result == [self.test_values["error_msg"]]
-                          
-    @patch('builtins.open')
-    def test_write_summary(self, m_open):
-        scanner = ValgrindScanner(self.remote)
-        mock_yaml_file = MockFile()
-        scanner.summary_data = self.test_values["summary_data"]
-        m_open.return_value = mock_yaml_file
-        scanner.write_summary("path/file.yaml")
-        written_content = mock_yaml_file.getvalue()             
-        assert written_content == self.test_values["yaml_data"]
\ No newline at end of file
diff --git a/teuthology/util/test/test_time.py b/teuthology/util/test/test_time.py
deleted file mode 100644 (file)
index c37b5f2..0000000
+++ /dev/null
@@ -1,54 +0,0 @@
-import pytest
-
-from datetime import datetime, timedelta, timezone
-from typing import Type
-
-from teuthology.util import time
-
-
-@pytest.mark.parametrize(
-    ["timestamp", "result"],
-    [
-        ["1999-12-31_23:59:59", datetime(1999, 12, 31, 23, 59, 59, tzinfo=timezone.utc)],
-        ["1999-12-31_23:59", datetime(1999, 12, 31, 23, 59, 0, tzinfo=timezone.utc)],
-        ["1999-12-31T23:59:59", datetime(1999, 12, 31, 23, 59, 59, tzinfo=timezone.utc)],
-        ["1999-12-31T23:59:59+00:00", datetime(1999, 12, 31, 23, 59, 59, tzinfo=timezone.utc)],
-        ["1999-12-31T17:59:59-06:00", datetime(1999, 12, 31, 23, 59, 59, tzinfo=timezone.utc)],
-        ["2024-01-01", datetime(2024, 1, 1, 0, 0, tzinfo=timezone.utc)],
-        ["tomorrow", ValueError],
-        ["1d", ValueError],
-        ["", ValueError],
-        ["2024", ValueError],
-
-    ]
-)
-def test_parse_timestamp(timestamp: str, result: datetime | Type[Exception]):
-    if isinstance(result, datetime):
-        assert time.parse_timestamp(timestamp) == result
-    else:
-        with pytest.raises(result):
-            time.parse_timestamp(timestamp)
-
-
-@pytest.mark.parametrize(
-    ["offset", "result"],
-    [
-        ["1s", timedelta(seconds=1)],
-        ["1m", timedelta(minutes=1)],
-        ["1h", timedelta(hours=1)],
-        ["1d", timedelta(days=1)],
-        ["1w", timedelta(weeks=1)],
-        ["365d", timedelta(days=365)],
-        ["1x", ValueError],
-        ["-1m", ValueError],
-        ["0xde", ValueError],
-        ["frog", ValueError],
-        ["7dwarfs", ValueError],
-    ]
-)
-def test_parse_offset(offset: str, result: timedelta | Type[Exception]):
-    if isinstance(result, timedelta):
-        assert time.parse_offset(offset) == result
-    else:
-        with pytest.raises(result):
-            time.parse_offset(offset)
diff --git a/tox.ini b/tox.ini
index 09ab4cfa08fb1ee41eb985de0c6bcc10961ffdbe..3d4bc6248919dac34b42673f6000694a52703209 100644 (file)
--- a/tox.ini
+++ b/tox.ini
@@ -12,7 +12,7 @@ passenv = HOME
 extras = test
 log_format = %(asctime)s %(levelname)s %(message)s
 commands =
-    python -m pytest --cov=teuthology --cov-report=term -v {posargs:teuthology scripts}
+    python -m pytest --cov=teuthology --cov-report=term -v {posargs:./tests}
 
 [testenv:docs]
 changedir = docs