Add support for groups to blueprints.

Nothing is currently being done with this information, but it will be
soon.
This commit is contained in:
Chris Lumens 2018-07-10 16:59:41 -04:00
parent d692a7dddd
commit 0f69d2084c
8 changed files with 50 additions and 17 deletions

View File

@ -141,6 +141,18 @@ NOTE: As of lorax-composer-29.2-1 the versions are not used for depsolving,
that is planned for a future release. And currently there are no differences that is planned for a future release. And currently there are no differences
between ``packages`` and ``modules`` in ``lorax-composer``. between ``packages`` and ``modules`` in ``lorax-composer``.
[[groups]]
~~~~~~~~~~
These entries describe a group of packages to be installed into the image. Package groups are
defined in the repository metadata. Each group has a descriptive name used primarily for display
in user interfaces and an ID more commonly used in kickstart files. Here, the ID is the expected
way of listing a group.
Groups have three different ways of categorizing their packages: mandatory, default, and optional.
For purposes of blueprints, mandatory and default packages will be installed. There is no mechanism
for selecting optional packages.
Customizations Customizations
~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~

View File

@ -47,21 +47,24 @@ class Recipe(dict):
and adds a .filename property to return the recipe's filename, and adds a .filename property to return the recipe's filename,
and a .toml() function to return the recipe as a TOML string. and a .toml() function to return the recipe as a TOML string.
""" """
def __init__(self, name, description, version, modules, packages, customizations=None): def __init__(self, name, description, version, modules, packages, groups, customizations=None):
# Check that version is empty or semver compatible # Check that version is empty or semver compatible
if version: if version:
semver.Version(version) semver.Version(version)
# Make sure modules and packages are listed by their case-insensitive names # Make sure modules, packages, and groups are listed by their case-insensitive names
if modules is not None: if modules is not None:
modules = sorted(modules, key=lambda m: m["name"].lower()) modules = sorted(modules, key=lambda m: m["name"].lower())
if packages is not None: if packages is not None:
packages = sorted(packages, key=lambda p: p["name"].lower()) packages = sorted(packages, key=lambda p: p["name"].lower())
if groups is not None:
groups = sorted(groups, key=lambda g: g["name"].lower())
dict.__init__(self, name=name, dict.__init__(self, name=name,
description=description, description=description,
version=version, version=version,
modules=modules, modules=modules,
packages=packages, packages=packages,
groups=groups,
customizations=customizations) customizations=customizations)
# We don't want customizations=None to show up in the TOML so remove it # We don't want customizations=None to show up in the TOML so remove it
@ -88,6 +91,11 @@ class Recipe(dict):
"""Return the names and versions of the modules""" """Return the names and versions of the modules"""
return [(m["name"], m["version"]) for m in self["modules"] or []] return [(m["name"], m["version"]) for m in self["modules"] or []]
@property
def group_names(self):
"""Return the names of the groups. Groups do not have versions."""
return map(lambda g: g["name"], self["groups"] or [])
@property @property
def filename(self): def filename(self):
"""Return the Recipe's filename """Return the Recipe's filename
@ -144,21 +152,25 @@ class Recipe(dict):
""" """
module_names = self.module_names module_names = self.module_names
package_names = self.package_names package_names = self.package_names
group_names = self.group_names
new_modules = [] new_modules = []
new_packages = [] new_packages = []
new_groups = []
for dep in deps: for dep in deps:
if dep["name"] in package_names: if dep["name"] in package_names:
new_packages.append(RecipePackage(dep["name"], dep_evra(dep))) new_packages.append(RecipePackage(dep["name"], dep_evra(dep)))
elif dep["name"] in module_names: elif dep["name"] in module_names:
new_modules.append(RecipeModule(dep["name"], dep_evra(dep))) new_modules.append(RecipeModule(dep["name"], dep_evra(dep)))
elif dep["name"] in group_names:
new_groups.append(RecipeGroup(dep["name"]))
if "customizations" in self: if "customizations" in self:
customizations = self["customizations"] customizations = self["customizations"]
else: else:
customizations = None customizations = None
return Recipe(self["name"], self["description"], self["version"], return Recipe(self["name"], self["description"], self["version"],
new_modules, new_packages, customizations) new_modules, new_packages, new_groups, customizations)
class RecipeModule(dict): class RecipeModule(dict):
def __init__(self, name, version): def __init__(self, name, version):
@ -167,6 +179,10 @@ class RecipeModule(dict):
class RecipePackage(RecipeModule): class RecipePackage(RecipeModule):
pass pass
class RecipeGroup(dict):
def __init__(self, name):
dict.__init__(self, name=name)
def recipe_from_file(recipe_path): def recipe_from_file(recipe_path):
"""Return a recipe file as a Recipe object """Return a recipe file as a Recipe object
@ -210,6 +226,10 @@ def recipe_from_dict(recipe_dict):
packages = [RecipePackage(p.get("name"), p.get("version")) for p in recipe_dict["packages"]] packages = [RecipePackage(p.get("name"), p.get("version")) for p in recipe_dict["packages"]]
else: else:
packages = [] packages = []
if recipe_dict.get("groups"):
groups = [RecipeGroup(g.get("name")) for g in recipe_dict["groups"]]
else:
groups = []
name = recipe_dict["name"] name = recipe_dict["name"]
description = recipe_dict["description"] description = recipe_dict["description"]
version = recipe_dict.get("version", None) version = recipe_dict.get("version", None)
@ -217,7 +237,7 @@ def recipe_from_dict(recipe_dict):
except KeyError as e: except KeyError as e:
raise RecipeError("There was a problem parsing the recipe: %s" % str(e)) raise RecipeError("There was a problem parsing the recipe: %s" % str(e))
return Recipe(name, description, version, modules, packages, customizations) return Recipe(name, description, version, modules, packages, groups, customizations)
def gfile(path): def gfile(path):
"""Convert a string path to GFile for use with Git""" """Convert a string path to GFile for use with Git"""
@ -897,5 +917,6 @@ def recipe_diff(old_recipe, new_recipe):
diffs.extend(diff_items("Module", old_recipe["modules"], new_recipe["modules"])) diffs.extend(diff_items("Module", old_recipe["modules"], new_recipe["modules"]))
diffs.extend(diff_items("Package", old_recipe["packages"], new_recipe["packages"])) diffs.extend(diff_items("Package", old_recipe["packages"], new_recipe["packages"]))
diffs.extend(diff_items("Group", old_recipe["groups"], new_recipe["groups"]))
return diffs return diffs

View File

@ -1 +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'}]}} {'name': 'custom-base', 'description': 'A base system with customizations', 'version': '0.0.1', 'groups': [], 'modules': [], 'packages': [{'name': 'bash', 'version': '4.4.*'}], 'customizations': {'hostname': 'custombase', 'sshkey': [{'user': 'root', 'key': 'A SSH KEY FOR ROOT'}]}}

View File

@ -1 +1 @@
{'description': u'An example http server with PHP and MySQL support.', 'packages': [{'version': u'6.6.*', 'name': u'openssh-server'}, {'version': u'3.0.*', 'name': u'rsync'}, {'version': u'2.2', 'name': u'tmux'}], 'modules': [{'version': u'2.4.*', 'name': u'httpd'}, {'version': u'5.4', 'name': u'mod_auth_kerb'}, {'version': u'2.4.*', 'name': u'mod_ssl'}, {'version': u'5.4.*', 'name': u'php'}, {'version': u'5.4.*', 'name': u'php-mysql'}], 'version': u'0.0.1', 'name': u'http-server'} {'description': u'An example http server with PHP and MySQL support.', 'packages': [{'version': u'6.6.*', 'name': u'openssh-server'}, {'version': u'3.0.*', 'name': u'rsync'}, {'version': u'2.2', 'name': u'tmux'}], 'groups': [], 'modules': [{'version': u'2.4.*', 'name': u'httpd'}, {'version': u'5.4', 'name': u'mod_auth_kerb'}, {'version': u'2.4.*', 'name': u'mod_ssl'}, {'version': u'5.4.*', 'name': u'php'}, {'version': u'5.4.*', 'name': u'php-mysql'}], 'version': u'0.0.1', 'name': u'http-server'}

View File

@ -1 +1 @@
{'description': u'An example http server with PHP and MySQL support.', 'packages': [], 'modules': [], 'version': u'0.0.1', 'name': u'http-server'} {'description': u'An example http server with PHP and MySQL support.', 'packages': [], 'groups': [], 'modules': [], 'version': u'0.0.1', 'name': u'http-server'}

View File

@ -1 +1 @@
{'description': u'An example http server with PHP and MySQL support.', 'packages': [], 'modules': [{'version': u'2.4.*', 'name': u'httpd'}, {'version': u'5.4', 'name': u'mod_auth_kerb'}, {'version': u'2.4.*', 'name': u'mod_ssl'}, {'version': u'5.4.*', 'name': u'php'}, {'version': u'5.4.*', 'name': u'php-mysql'}], 'version': u'0.0.1', 'name': u'http-server'} {'description': u'An example http server with PHP and MySQL support.', 'packages': [], 'groups': [], 'modules': [{'version': u'2.4.*', 'name': u'httpd'}, {'version': u'5.4', 'name': u'mod_auth_kerb'}, {'version': u'2.4.*', 'name': u'mod_ssl'}, {'version': u'5.4.*', 'name': u'php'}, {'version': u'5.4.*', 'name': u'php-mysql'}], 'version': u'0.0.1', 'name': u'http-server'}

View File

@ -1 +1 @@
{'description': u'An example http server with PHP and MySQL support.', 'packages': [{'version': u'6.6.*', 'name': u'openssh-server'}, {'version': u'3.0.*', 'name': u'rsync'}, {'version': u'2.2', 'name': u'tmux'}], 'modules': [], 'version': u'0.0.1', 'name': u'http-server'} {'description': u'An example http server with PHP and MySQL support.', 'packages': [{'version': u'6.6.*', 'name': u'openssh-server'}, {'version': u'3.0.*', 'name': u'rsync'}, {'version': u'2.2', 'name': u'tmux'}], 'groups': [], 'modules': [], 'version': u'0.0.1', 'name': u'http-server'}

View File

@ -95,27 +95,27 @@ class BasicRecipeTest(unittest.TestCase):
"""Test the Recipe's version bump function""" """Test the Recipe's version bump function"""
# Neither have a version # Neither have a version
recipe = recipes.Recipe("test-recipe", "A recipe used for testing", None, None, None) recipe = recipes.Recipe("test-recipe", "A recipe used for testing", None, None, None, None)
new_version = recipe.bump_version(None) new_version = recipe.bump_version(None)
self.assertEqual(new_version, "0.0.1") self.assertEqual(new_version, "0.0.1")
# Original has a version, new does not # Original has a version, new does not
recipe = recipes.Recipe("test-recipe", "A recipe used for testing", None, None, None) recipe = recipes.Recipe("test-recipe", "A recipe used for testing", None, None, None, None)
new_version = recipe.bump_version("0.0.1") new_version = recipe.bump_version("0.0.1")
self.assertEqual(new_version, "0.0.2") self.assertEqual(new_version, "0.0.2")
# Original has no version, new does # Original has no version, new does
recipe = recipes.Recipe("test-recipe", "A recipe used for testing", "0.1.0", None, None) recipe = recipes.Recipe("test-recipe", "A recipe used for testing", "0.1.0", None, None, None)
new_version = recipe.bump_version(None) new_version = recipe.bump_version(None)
self.assertEqual(new_version, "0.1.0") self.assertEqual(new_version, "0.1.0")
# New and Original are the same # New and Original are the same
recipe = recipes.Recipe("test-recipe", "A recipe used for testing", "0.0.1", None, None) recipe = recipes.Recipe("test-recipe", "A recipe used for testing", "0.0.1", None, None, None)
new_version = recipe.bump_version("0.0.1") new_version = recipe.bump_version("0.0.1")
self.assertEqual(new_version, "0.0.2") self.assertEqual(new_version, "0.0.2")
# New is different from Original # New is different from Original
recipe = recipes.Recipe("test-recipe", "A recipe used for testing", "0.1.1", None, None) recipe = recipes.Recipe("test-recipe", "A recipe used for testing", "0.1.1", None, None, None)
new_version = recipe.bump_version("0.0.1") new_version = recipe.bump_version("0.0.1")
self.assertEqual(new_version, "0.1.1") self.assertEqual(new_version, "0.1.1")
@ -136,8 +136,8 @@ class BasicRecipeTest(unittest.TestCase):
def recipe_diff_test(self): def recipe_diff_test(self):
"""Test the recipe_diff function""" """Test the recipe_diff function"""
old_recipe = recipes.Recipe("test-recipe", "A recipe used for testing", "0.1.1", self.old_modules, self.old_packages) old_recipe = recipes.Recipe("test-recipe", "A recipe used for testing", "0.1.1", self.old_modules, self.old_packages, [])
new_recipe = recipes.Recipe("test-recipe", "A recipe used for testing", "0.3.1", self.new_modules, self.new_packages) new_recipe = recipes.Recipe("test-recipe", "A recipe used for testing", "0.3.1", self.new_modules, self.new_packages, [])
result = [{'new': {'Version': '0.3.1'}, 'old': {'Version': '0.1.1'}}, result = [{'new': {'Version': '0.3.1'}, 'old': {'Version': '0.1.1'}},
{'new': {'Module': {'name': 'openssh', 'version': '2.8.1'}}, 'old': None}, {'new': {'Module': {'name': 'openssh', 'version': '2.8.1'}}, 'old': None},
{'new': None, 'old': {'Module': {'name': 'bash', 'version': '4.*'}}}, {'new': None, 'old': {'Module': {'name': 'bash', 'version': '4.*'}}},
@ -182,7 +182,7 @@ version = "2.7.*"
def test_02_commit_recipe(self): def test_02_commit_recipe(self):
"""Test committing a Recipe object""" """Test committing a Recipe object"""
recipe = recipes.Recipe("test-recipe", "A recipe used for testing", None, None, None) recipe = recipes.Recipe("test-recipe", "A recipe used for testing", None, None, None, None)
oid = recipes.commit_recipe(self.repo, "master", recipe) oid = recipes.commit_recipe(self.repo, "master", recipe)
self.assertNotEqual(oid, None) self.assertNotEqual(oid, None)