Add support for groups to blueprints.

Nothing is currently being done with this information, but it will be
soon.

(cherry picked from commit 0f69d2084c)
(cherry picked from commit 76d376fe18)
This commit is contained in:
Chris Lumens 2018-07-10 16:59:41 -04:00 committed by Brian C. Lane
parent f5115291bd
commit f1f8980c49
8 changed files with 50 additions and 17 deletions

View File

@ -137,6 +137,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 version globs of the modules""" """Return the names and version globs 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)