diff --git a/Makefile b/Makefile
index 13fe2cf1..69992656 100644
--- a/Makefile
+++ b/Makefile
@@ -48,7 +48,7 @@ test:
@echo "*** Running tests ***"
PYTHONPATH=$(PYTHONPATH):./src/ $(PYTHON) -m nose -v --with-coverage --cover-erase --cover-branches \
--cover-package=pylorax --cover-inclusive \
- ./tests/pylorax/ ./tests/composer/
+ ./tests/pylorax/ ./tests/composer/ ./tests/lifted/
coverage3 report -m
[ -f "/usr/bin/coveralls" ] && [ -n "$(COVERALLS_REPO_TOKEN)" ] && coveralls || echo
diff --git a/lorax.spec b/lorax.spec
index 68af01fd..5df4e720 100644
--- a/lorax.spec
+++ b/lorax.spec
@@ -152,6 +152,7 @@ Requires: python3-rpmfluff
Requires: git
Requires: xz
Requires: createrepo_c
+Requires: python3-ansible-runner
%{?systemd_requires}
BuildRequires: systemd
diff --git a/tests/lifted/__init__.py b/tests/lifted/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/lifted/profiles.py b/tests/lifted/profiles.py
new file mode 100644
index 00000000..e973d86f
--- /dev/null
+++ b/tests/lifted/profiles.py
@@ -0,0 +1,48 @@
+#
+# Copyright (C) 2019 Red Hat, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+
+# test profile settings for each provider
+test_profiles = {
+ "azure": ["azure-profile", {
+ "resource_group": "production",
+ "storage_account_name": "HomerSimpson",
+ "storage_container": "plastic",
+ "subscription_id": "SpringfieldNuclear",
+ "client_id": "DonutGuy",
+ "secret": "I Like sprinkles",
+ "tenant": "Bart",
+ "location": "Springfield"
+ }],
+ "dummy": ["dummy-profile", {}],
+ "openstack": ["openstack-profile", {
+ "auth_url": "https://localhost/auth/url",
+ "username": "ChuckBurns",
+ "password": "Excellent!",
+ "project_name": "Springfield Nuclear",
+ "user_domain_name": "chuck.burns.localhost",
+ "project_domain_name": "springfield.burns.localhost",
+ "is_public": True
+ }],
+ "vsphere": ["vsphere-profile", {
+ "datacenter": "Lisa's Closet",
+ "datastore": "storage-crate-alpha",
+ "host": "marge",
+ "folder": "the.green.one",
+ "username": "LisaSimpson",
+ "password": "EmbraceNothingnes"
+ }]
+}
diff --git a/tests/lifted/test_config.py b/tests/lifted/test_config.py
new file mode 100644
index 00000000..c509a8f6
--- /dev/null
+++ b/tests/lifted/test_config.py
@@ -0,0 +1,52 @@
+#
+# Copyright (C) 2019 Red Hat, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+import unittest
+
+import lifted.config
+import pylorax.api.config
+
+class ConfigTestCase(unittest.TestCase):
+ def test_lifted_config(self):
+ """Test lifted config setup"""
+ config = pylorax.api.config.configure(test_config=True)
+ lifted.config.configure(config)
+
+ self.assertTrue(config.get("upload", "providers_dir").startswith(config.get("composer", "share_dir")))
+ self.assertTrue(config.get("upload", "queue_dir").startswith(config.get("composer", "lib_dir")))
+ self.assertTrue(config.get("upload", "settings_dir").startswith(config.get("composer", "lib_dir")))
+
+ def test_lifted_sharedir_config(self):
+ """Test lifted config setup with custom share_dir"""
+ config = pylorax.api.config.configure(test_config=True)
+ config.set("composer", "share_dir", "/custom/share/path")
+ lifted.config.configure(config)
+
+ self.assertEqual(config.get("composer", "share_dir"), "/custom/share/path")
+ self.assertTrue(config.get("upload", "providers_dir").startswith(config.get("composer", "share_dir")))
+ self.assertTrue(config.get("upload", "queue_dir").startswith(config.get("composer", "lib_dir")))
+ self.assertTrue(config.get("upload", "settings_dir").startswith(config.get("composer", "lib_dir")))
+
+ def test_lifted_libdir_config(self):
+ """Test lifted config setup with custom lib_dir"""
+ config = pylorax.api.config.configure(test_config=True)
+ config.set("composer", "lib_dir", "/custom/lib/path")
+ lifted.config.configure(config)
+
+ self.assertEqual(config.get("composer", "lib_dir"), "/custom/lib/path")
+ self.assertTrue(config.get("upload", "providers_dir").startswith(config.get("composer", "share_dir")))
+ self.assertTrue(config.get("upload", "queue_dir").startswith(config.get("composer", "lib_dir")))
+ self.assertTrue(config.get("upload", "settings_dir").startswith(config.get("composer", "lib_dir")))
diff --git a/tests/lifted/test_providers.py b/tests/lifted/test_providers.py
new file mode 100644
index 00000000..0c73261f
--- /dev/null
+++ b/tests/lifted/test_providers.py
@@ -0,0 +1,95 @@
+#
+# Copyright (C) 2019 Red Hat, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+import os
+import shutil
+import tempfile
+import unittest
+
+import lifted.config
+from lifted.providers import list_providers, resolve_provider, resolve_playbook_path, save_settings
+from lifted.providers import load_profiles, validate_settings
+import pylorax.api.config
+from pylorax.sysutils import joinpaths
+
+from tests.lifted.profiles import test_profiles
+
+class ProvidersTestCase(unittest.TestCase):
+ @classmethod
+ def setUpClass(self):
+ self.root_dir = tempfile.mkdtemp(prefix="lifted.test.")
+ self.config = pylorax.api.config.configure(root_dir=self.root_dir, test_config=True)
+ self.config.set("composer", "share_dir", os.path.realpath("./share/"))
+ lifted.config.configure(self.config)
+
+ @classmethod
+ def tearDownClass(self):
+ shutil.rmtree(self.root_dir)
+
+ def test_list_providers(self):
+ p = list_providers(self.config["upload"])
+ self.assertEqual(p, ['azure', 'dummy', 'openstack', 'vsphere'])
+
+ def test_resolve_provider(self):
+ for p in list_providers(self.config["upload"]):
+ print(p)
+ info = resolve_provider(self.config["upload"], p)
+ self.assertTrue("display" in info)
+ self.assertTrue("supported_types" in info)
+ self.assertTrue("settings-info" in info)
+
+ def test_resolve_playbook_path(self):
+ for p in list_providers(self.config["upload"]):
+ print(p)
+ self.assertTrue(len(resolve_playbook_path(self.config["upload"], p)) > 0)
+
+ def test_resolve_playbook_path_error(self):
+ with self.assertRaises(RuntimeError):
+ resolve_playbook_path(self.config["upload"], "foobar")
+
+ def test_validate_settings(self):
+ for p in list_providers(self.config["upload"]):
+ print(p)
+ validate_settings(self.config["upload"], p, test_profiles[p][1])
+
+ def test_validate_settings_errors(self):
+ with self.assertRaises(ValueError):
+ validate_settings(self.config["upload"], "dummy", test_profiles["dummy"][1], image_name="")
+
+ with self.assertRaises(ValueError):
+ validate_settings(self.config["upload"], "azure", {"wrong-key": "wrong value"})
+
+ with self.assertRaises(ValueError):
+ validate_settings(self.config["upload"], "azure", {"secret": False})
+
+ # TODO - test regex, needs a provider with a regex
+
+ def test_save_settings(self):
+ """Test saving profiles"""
+ for p in list_providers(self.config["upload"]):
+ print(p)
+ save_settings(self.config["upload"], p, test_profiles[p][0], test_profiles[p][1])
+
+ profile_dir = joinpaths(self.config.get("upload", "settings_dir"), p, test_profiles[p][0]+".toml")
+ self.assertTrue(os.path.exists(profile_dir))
+
+ # This *must* run after test_save_settings, _zz_ ensures that happens
+ def test_zz_load_profiles(self):
+ """Test loading profiles"""
+ for p in list_providers(self.config["upload"]):
+ print(p)
+ profile = load_profiles(self.config["upload"], p)
+ self.assertTrue(test_profiles[p][0] in profile)
diff --git a/tests/lifted/test_queue.py b/tests/lifted/test_queue.py
new file mode 100644
index 00000000..7491cb06
--- /dev/null
+++ b/tests/lifted/test_queue.py
@@ -0,0 +1,119 @@
+#
+# Copyright (C) 2019 Red Hat, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+import os
+import shutil
+import tempfile
+import unittest
+
+import lifted.config
+from lifted.providers import list_providers
+from lifted.queue import _write_callback, create_upload, get_all_uploads, get_upload, get_uploads
+from lifted.queue import ready_upload, reset_upload, cancel_upload
+import pylorax.api.config
+
+from tests.lifted.profiles import test_profiles
+
+class QueueTestCase(unittest.TestCase):
+ @classmethod
+ def setUpClass(self):
+ self.root_dir = tempfile.mkdtemp(prefix="lifted.test.")
+ self.config = pylorax.api.config.configure(root_dir=self.root_dir, test_config=True)
+ self.config.set("composer", "share_dir", os.path.realpath("./share/"))
+ lifted.config.configure(self.config)
+
+ self.upload_uuids = []
+
+ @classmethod
+ def tearDownClass(self):
+ shutil.rmtree(self.root_dir)
+
+ # This should run first, it writes uploads to the queue directory
+ def test_00_create_upload(self):
+ """Test creating an upload for each provider"""
+ for p in list_providers(self.config["upload"]):
+ print(p)
+ upload = create_upload(self.config["upload"], p, "test-image", test_profiles[p][1])
+ summary = upload.summary()
+ self.assertEqual(summary["provider_name"], p)
+ self.assertEqual(summary["image_name"], "test-image")
+ self.assertTrue(summary["status"], "WAITING")
+
+ self.upload_uuids.append(summary["uuid"])
+ self.assertTrue(len(self.upload_uuids) > 0)
+ self.assertTrue(len(self.upload_uuids), len(list_providers(self.config["upload"])))
+
+ def test_01_get_all_uploads(self):
+ """Test listing all the uploads"""
+ uploads = get_all_uploads(self.config["upload"])
+ # Should be one upload per provider
+ providers = sorted([u.provider_name for u in uploads])
+ self.assertEqual(providers, list_providers(self.config["upload"]))
+
+ def test_02_get_upload(self):
+ """Test listing specific uploads by uuid"""
+ for uuid in self.upload_uuids:
+ upload = get_upload(self.config["upload"], uuid)
+ self.assertTrue(upload.uuid, uuid)
+
+ def test_02_get_upload_error(self):
+ """Test listing an unknown upload uuid"""
+ with self.assertRaises(RuntimeError):
+ get_upload(self.config["upload"], "not-a-valid-uuid")
+
+ def test_03_get_uploads(self):
+ """Test listing multiple uploads by uuid"""
+ uploads = get_uploads(self.config["upload"], self.upload_uuids)
+ uuids = sorted([u.uuid for u in uploads])
+ self.assertTrue(uuids, sorted(self.upload_uuids))
+
+ def test_04_ready_upload(self):
+ """Test ready_upload"""
+ ready_upload(self.config["upload"], self.upload_uuids[0], "image-test-path")
+ upload = get_upload(self.config["upload"], self.upload_uuids[0])
+ self.assertEqual(upload.image_path, "image-test-path")
+
+ def test_05_reset_upload(self):
+ """Test reset_upload"""
+ # Set the status to FAILED so it can be reset
+ upload = get_upload(self.config["upload"], self.upload_uuids[0])
+ upload.set_status("FAILED", _write_callback(self.config["upload"]))
+
+ reset_upload(self.config["upload"], self.upload_uuids[0])
+ upload = get_upload(self.config["upload"], self.upload_uuids[0])
+ self.assertEqual(upload.status, "READY")
+
+ def test_06_reset_upload_error(self):
+ """Test reset_upload raising an error"""
+ with self.assertRaises(RuntimeError):
+ reset_upload(self.config["upload"], self.upload_uuids[0])
+
+ def test_07_cancel_upload(self):
+ """Test cancel_upload"""
+ cancel_upload(self.config["upload"], self.upload_uuids[0])
+ upload = get_upload(self.config["upload"], self.upload_uuids[0])
+ self.assertEqual(upload.status, "CANCELLED")
+
+ def test_08_cancel_upload_error(self):
+ """Test cancel_upload raises an error"""
+ # Set the status to CANCELED to make sure the cancel will fail
+ upload = get_upload(self.config["upload"], self.upload_uuids[0])
+ upload.set_status("CANCELLED", _write_callback(self.config["upload"]))
+
+ with self.assertRaises(RuntimeError):
+ cancel_upload(self.config["upload"], self.upload_uuids[0])
+
+ # TODO test execute
diff --git a/tests/lifted/test_upload.py b/tests/lifted/test_upload.py
new file mode 100644
index 00000000..a7ed59bb
--- /dev/null
+++ b/tests/lifted/test_upload.py
@@ -0,0 +1,126 @@
+#
+# Copyright (C) 2019 Red Hat, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+import os
+import shutil
+import tempfile
+import unittest
+
+import lifted.config
+from lifted.providers import list_providers, resolve_playbook_path, validate_settings
+from lifted.upload import Upload
+import pylorax.api.config
+
+from tests.lifted.profiles import test_profiles
+
+# Helper function for creating Upload object
+def create_upload(ucfg, provider_name, image_name, settings, status=None, callback=None):
+ validate_settings(ucfg, provider_name, settings, image_name)
+ return Upload(
+ provider_name=provider_name,
+ playbook_path=resolve_playbook_path(ucfg, provider_name),
+ image_name=image_name,
+ settings=settings,
+ status=status,
+ status_callback=callback,
+ )
+
+
+class UploadTestCase(unittest.TestCase):
+ @classmethod
+ def setUpClass(self):
+ self.root_dir = tempfile.mkdtemp(prefix="lifted.test.")
+ self.config = pylorax.api.config.configure(root_dir=self.root_dir, test_config=True)
+ self.config.set("composer", "share_dir", os.path.realpath("./share/"))
+ lifted.config.configure(self.config)
+
+ @classmethod
+ def tearDownClass(self):
+ shutil.rmtree(self.root_dir)
+
+ def test_new_upload(self):
+ for p in list_providers(self.config["upload"]):
+ print(p)
+ upload = create_upload(self.config["upload"], p, "test-image", test_profiles[p][1], status="READY")
+ summary = upload.summary()
+ self.assertEqual(summary["provider_name"], p)
+ self.assertEqual(summary["image_name"], "test-image")
+ self.assertTrue(summary["status"], "WAITING")
+
+ def test_serializable(self):
+ for p in list_providers(self.config["upload"]):
+ print(p)
+ upload = create_upload(self.config["upload"], p, "test-image", test_profiles[p][1], status="READY")
+ self.assertEqual(upload.serializable()["settings"], test_profiles[p][1])
+ self.assertEqual(upload.serializable()["status"], "READY")
+
+ def test_summary(self):
+ for p in list_providers(self.config["upload"]):
+ print(p)
+ upload = create_upload(self.config["upload"], p, "test-image", test_profiles[p][1], status="READY")
+ self.assertEqual(upload.summary()["settings"], test_profiles[p][1])
+ self.assertEqual(upload.summary()["status"], "READY")
+
+ def test_set_status(self):
+ for p in list_providers(self.config["upload"]):
+ print(p)
+ upload = create_upload(self.config["upload"], p, "test-image", test_profiles[p][1], status="READY")
+ self.assertEqual(upload.summary()["status"], "READY")
+ upload.set_status("WAITING")
+ self.assertEqual(upload.summary()["status"], "WAITING")
+
+ def test_ready(self):
+ for p in list_providers(self.config["upload"]):
+ print(p)
+ upload = create_upload(self.config["upload"], p, "test-image", test_profiles[p][1], status="WAITING")
+ self.assertEqual(upload.summary()["status"], "WAITING")
+ upload.ready("test-image-path", status_callback=None)
+ summary = upload.summary()
+ self.assertEqual(summary["status"], "READY")
+ self.assertEqual(summary["image_path"], "test-image-path")
+
+ def test_reset(self):
+ for p in list_providers(self.config["upload"]):
+ print(p)
+ upload = create_upload(self.config["upload"], p, "test-image", test_profiles[p][1], status="CANCELLED")
+ upload.ready("test-image-path", status_callback=None)
+ upload.reset(status_callback=None)
+ self.assertEqual(upload.status, "READY")
+
+ def test_reset_errors(self):
+ for p in list_providers(self.config["upload"]):
+ print(p)
+ upload = create_upload(self.config["upload"], p, "test-image", test_profiles[p][1], status="WAITING")
+ with self.assertRaises(RuntimeError):
+ upload.reset(status_callback=None)
+
+ upload = create_upload(self.config["upload"], p, "test-image", test_profiles[p][1], status="CANCELLED")
+ with self.assertRaises(RuntimeError):
+ upload.reset(status_callback=None)
+
+ def test_cancel(self):
+ for p in list_providers(self.config["upload"]):
+ print(p)
+ upload = create_upload(self.config["upload"], p, "test-image", test_profiles[p][1], status="WAITING")
+ upload.cancel()
+ self.assertEqual(upload.status, "CANCELLED")
+
+ def test_cancel_error(self):
+ for p in list_providers(self.config["upload"]):
+ print(p)
+ upload = create_upload(self.config["upload"], p, "test-image", test_profiles[p][1], status="CANCELLED")
+ with self.assertRaises(RuntimeError):
+ upload.cancel()