From 8839182f430795c87063ec8bdd12d587111efd35 Mon Sep 17 00:00:00 2001 From: "Brian C. Lane" Date: Tue, 8 May 2018 09:25:44 -0700 Subject: [PATCH] Add blueprint customization support for hostname and ssh key This adds support for the optional blueprint section [customizations]. Use it like this: [customizations] hostname = yourhostnamehere [[customizations.sshkey]] user = root key = root user key --- src/pylorax/api/compose.py | 30 ++++++++++++++++++++++- src/pylorax/api/recipes.py | 18 +++++++++++--- tests/pylorax/blueprints/custom-base.toml | 14 +++++++++++ tests/pylorax/results/custom-base.dict | 1 + tests/pylorax/results/custom-base.toml | 14 +++++++++++ tests/pylorax/test_recipes.py | 3 ++- tests/pylorax/test_server.py | 15 +++++++++--- 7 files changed, 86 insertions(+), 9 deletions(-) create mode 100644 tests/pylorax/blueprints/custom-base.toml create mode 100644 tests/pylorax/results/custom-base.dict create mode 100644 tests/pylorax/results/custom-base.toml diff --git a/src/pylorax/api/compose.py b/src/pylorax/api/compose.py index ff35e6a3..533b7011 100644 --- a/src/pylorax/api/compose.py +++ b/src/pylorax/api/compose.py @@ -83,6 +83,33 @@ def repo_to_ks(r, url="url"): return cmd + +def add_customizations(f, recipe): + """ Add customizations to the kickstart file + + :param f: kickstart file object + :type f: open file object + :param recipe: + :type recipe: Recipe object + :returns: None + :raises: RuntimeError if there was a problem writing to the kickstart + """ + if "customizations" not in recipe: + return + customizations = recipe["customizations"] + + if "hostname" in customizations: + f.write("network --hostname=%s\n" % customizations["hostname"]) + + if "sshkey" in customizations: + # This is a list of entries + for sshkey in customizations["sshkey"]: + if "user" not in sshkey or "key" not in sshkey: + log.error("%s is incorrect, skipping", sshkey) + continue + f.write('sshkey --user %s "%s"' % (sshkey["user"], sshkey["key"])) + + def start_build(cfg, dnflock, gitlock, branch, recipe_name, compose_type, test_mode=0): """ Start the build @@ -193,9 +220,10 @@ def start_build(cfg, dnflock, gitlock, branch, recipe_name, compose_type, test_m for d in deps: f.write(dep_nevra(d)+"\n") - f.write("%end\n") + add_customizations(f, recipe) + # Setup the config to pass to novirt_install log_dir = joinpaths(results_dir, "logs/") cfg_args = compose_args(compose_type) diff --git a/src/pylorax/api/recipes.py b/src/pylorax/api/recipes.py index 6889e9dc..4e9af08f 100644 --- a/src/pylorax/api/recipes.py +++ b/src/pylorax/api/recipes.py @@ -47,7 +47,7 @@ class Recipe(dict): and adds a .filename property to return the recipe's filename, and a .toml() function to return the recipe as a TOML string. """ - def __init__(self, name, description, version, modules, packages): + def __init__(self, name, description, version, modules, packages, customizations=None): # Check that version is empty or semver compatible if version: semver.Version(version) @@ -61,7 +61,12 @@ class Recipe(dict): description=description, version=version, modules=modules, - packages=packages) + packages=packages, + customizations=customizations) + + # We don't want customizations=None to show up in the TOML so remove it + if customizations is None: + del self["customizations"] @property def package_names(self): @@ -137,9 +142,13 @@ class Recipe(dict): new_packages.append(RecipePackage(dep["name"], dep_evra(dep))) elif dep["name"] in module_names: new_modules.append(RecipeModule(dep["name"], dep_evra(dep))) + if "customizations" in self: + customizations = self["customizations"] + else: + customizations = None return Recipe(self["name"], self["description"], self["version"], - new_modules, new_packages) + new_modules, new_packages, customizations) class RecipeModule(dict): def __init__(self, name, version): @@ -194,10 +203,11 @@ def recipe_from_dict(recipe_dict): name = recipe_dict["name"] description = recipe_dict["description"] version = recipe_dict.get("version", None) + customizations = recipe_dict.get("customizations", None) except KeyError as e: raise RecipeError("There was a problem parsing the recipe: %s" % str(e)) - return Recipe(name, description, version, modules, packages) + return Recipe(name, description, version, modules, packages, customizations) def gfile(path): """Convert a string path to GFile for use with Git""" diff --git a/tests/pylorax/blueprints/custom-base.toml b/tests/pylorax/blueprints/custom-base.toml new file mode 100644 index 00000000..7528b6b2 --- /dev/null +++ b/tests/pylorax/blueprints/custom-base.toml @@ -0,0 +1,14 @@ +name = "custom-base" +description = "A base system with customizations" +version = "0.0.1" + +[[packages]] +name = "bash" +version = "4.4.*" + +[customizations] +hostname = "custombase" + +[[customizations.sshkey]] +user = "root" +key = "A SSH KEY FOR ROOT" diff --git a/tests/pylorax/results/custom-base.dict b/tests/pylorax/results/custom-base.dict new file mode 100644 index 00000000..0a39c626 --- /dev/null +++ b/tests/pylorax/results/custom-base.dict @@ -0,0 +1 @@ +{'name': 'custom-base', 'description': 'A base system with customizations', 'version': '0.0.1', 'modules': [], 'packages': [{'name': 'bash', 'version': '4.4.*'}], 'customizations': {'hostname': 'custombase', 'sshkey': [{'user': 'root', 'key': 'A SSH KEY FOR ROOT'}]}} diff --git a/tests/pylorax/results/custom-base.toml b/tests/pylorax/results/custom-base.toml new file mode 100644 index 00000000..7528b6b2 --- /dev/null +++ b/tests/pylorax/results/custom-base.toml @@ -0,0 +1,14 @@ +name = "custom-base" +description = "A base system with customizations" +version = "0.0.1" + +[[packages]] +name = "bash" +version = "4.4.*" + +[customizations] +hostname = "custombase" + +[[customizations.sshkey]] +user = "root" +key = "A SSH KEY FOR ROOT" diff --git a/tests/pylorax/test_recipes.py b/tests/pylorax/test_recipes.py index 40144dfd..1b77c800 100644 --- a/tests/pylorax/test_recipes.py +++ b/tests/pylorax/test_recipes.py @@ -31,7 +31,8 @@ class BasicRecipeTest(unittest.TestCase): input_recipes = [("full-recipe.toml", "full-recipe.dict"), ("minimal.toml", "minimal.dict"), ("modules-only.toml", "modules-only.dict"), - ("packages-only.toml", "packages-only.dict")] + ("packages-only.toml", "packages-only.dict"), + ("custom-base.toml", "custom-base.dict")] results_path = "./tests/pylorax/results/" self.input_toml = [] for (recipe_toml, recipe_dict) in input_recipes: diff --git a/tests/pylorax/test_server.py b/tests/pylorax/test_server.py index e85bec1d..63fa030b 100644 --- a/tests/pylorax/test_server.py +++ b/tests/pylorax/test_server.py @@ -54,6 +54,7 @@ class ServerTestCase(unittest.TestCase): server.config['TESTING'] = True self.server = server.test_client() + self.repo_dir = repo_dir self.examples_path = "./tests/pylorax/blueprints/" @@ -81,8 +82,8 @@ class ServerTestCase(unittest.TestCase): def test_02_blueprints_list(self): """Test the /api/v0/blueprints/list route""" - list_dict = {"blueprints":["atlas", "development", "glusterfs", "http-server", "jboss", "kubernetes"], - "limit":20, "offset":0, "total":6} + list_dict = {"blueprints":["atlas", "custom-base", "development", "glusterfs", "http-server", + "jboss", "kubernetes"], "limit":20, "offset":0, "total":7} resp = self.server.get("/api/v0/blueprints/list") data = json.loads(resp.data) self.assertEqual(data, list_dict) @@ -738,7 +739,7 @@ class ServerTestCase(unittest.TestCase): def test_compose_12_create_finished(self): """Test the /api/v0/compose routes with a finished test compose""" - test_compose = {"blueprint_name": "glusterfs", + test_compose = {"blueprint_name": "custom-base", "compose_type": "tar", "branch": "master"} @@ -805,6 +806,14 @@ class ServerTestCase(unittest.TestCase): self.assertEqual(len(resp.data) > 0, True) self.assertEqual(resp.data, b"TEST IMAGE") + # Examine the final-kickstart.ks for the customizations + # A bit kludgy since it examines the filesystem directly, but that's better than unpacking the metadata + final_ks = open(joinpaths(self.repo_dir, "var/lib/lorax/composer/results/", build_id, "final-kickstart.ks")).read() + + # Check for the expected customizations in the kickstart + self.assertTrue("network --hostname=" in final_ks) + self.assertTrue("sshkey --user root" in final_ks) + # Delete the finished build # Test the /api/v0/compose/delete/ route resp = self.server.delete("/api/v0/compose/delete/%s" % build_id)