lorax-composer: Add the ability to append to the kernel command-line
Sometimes it is necessary to modify the kernel command-line of the image, this adds support for a [customizations.kernel] section to the blueprint: [customizations.kernel] append = "nosmt=force" This will be appended to the kickstart's bootloader --append argument. Includes tests for modifying the bootloader line, the kickstart template, and examining the final-kickstart.ks created for a compose. Related: rhbz#1688335
This commit is contained in:
		
							parent
							
								
									72d1094fb6
								
							
						
					
					
						commit
						9cebd1ddaf
					
				| @ -178,6 +178,18 @@ The ``[[customizations]]`` section can be used to configure the hostname of the | ||||
|     hostname = "baseimage" | ||||
| 
 | ||||
| 
 | ||||
| [customizations.kernel] | ||||
| *********************** | ||||
| 
 | ||||
| This allows you to append arguments to the bootloader's kernel commandline. This will not have any | ||||
| effect on ``tar`` or ``ext4-filesystem`` images since they do not include a bootloader. | ||||
| 
 | ||||
| For example:: | ||||
| 
 | ||||
|     [customizations.kernel] | ||||
|     append = "nosmt=force" | ||||
| 
 | ||||
| 
 | ||||
| [[customizations.sshkey]] | ||||
| ************************* | ||||
| 
 | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018 Red Hat, Inc. | ||||
| # Copyright (C) 2018-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 | ||||
| @ -35,6 +35,7 @@ log = logging.getLogger("lorax-composer") | ||||
| 
 | ||||
| import os | ||||
| from glob import glob | ||||
| from io import StringIO | ||||
| from math import ceil | ||||
| import pytoml as toml | ||||
| import shutil | ||||
| @ -122,6 +123,62 @@ def repo_to_ks(r, url="url"): | ||||
|     return cmd | ||||
| 
 | ||||
| 
 | ||||
| def bootloader_append(line, kernel_append): | ||||
|     """ Insert the kernel_append string into the --append argument | ||||
| 
 | ||||
|     :param line: The bootloader ... line | ||||
|     :type line: str | ||||
|     :param kernel_append: The arguments to append to the --append section | ||||
|     :type kernel_append: str | ||||
| 
 | ||||
|     Using pykickstart to process the line is the best way to make sure it | ||||
|     is parsed correctly, and re-assembled for inclusion into the final kickstart | ||||
|     """ | ||||
|     ks_version = makeVersion() | ||||
|     ks = KickstartParser(ks_version, errorsAreFatal=False, missingIncludeIsFatal=False) | ||||
|     ks.readKickstartFromString(line) | ||||
| 
 | ||||
|     if ks.handler.bootloader.appendLine: | ||||
|         ks.handler.bootloader.appendLine += " %s" % kernel_append | ||||
|     else: | ||||
|         ks.handler.bootloader.appendLine = kernel_append | ||||
| 
 | ||||
|     # Converting back to a string includes a comment, return just the bootloader line | ||||
|     return str(ks.handler.bootloader).splitlines()[-1] | ||||
| 
 | ||||
| 
 | ||||
| def customize_ks_template(ks_template, recipe): | ||||
|     """ Customize the kickstart template and return it | ||||
| 
 | ||||
|     :param ks_template: The kickstart template | ||||
|     :type ks_template: str | ||||
|     :param recipe: | ||||
|     :type recipe: Recipe object | ||||
| 
 | ||||
|     Apply customizations.kernel.append to the bootloader argument in the template. | ||||
|     Add bootloader line if it is missing. | ||||
|     """ | ||||
|     if "customizations" not in recipe or \ | ||||
|        "kernel" not in recipe["customizations"] or \ | ||||
|        "append" not in recipe["customizations"]["kernel"]: | ||||
|         return ks_template | ||||
|     kernel_append = recipe["customizations"]["kernel"]["append"] | ||||
|     found_bootloader = False | ||||
|     output = StringIO() | ||||
|     for line in ks_template.splitlines(): | ||||
|         if not line.startswith("bootloader"): | ||||
|             print(line, file=output) | ||||
|             continue | ||||
|         found_bootloader = True | ||||
|         log.debug("Found bootloader line: %s", line) | ||||
|         print(bootloader_append(line, kernel_append), file=output) | ||||
| 
 | ||||
|     if found_bootloader: | ||||
|         return output.getvalue() | ||||
|     else: | ||||
|         return 'bootloader --append="%s" --location=none' % kernel_append + output.getvalue() | ||||
| 
 | ||||
| 
 | ||||
| def write_ks_root(f, user): | ||||
|     """ Write kickstart root password and sshkey entry | ||||
| 
 | ||||
| @ -382,12 +439,14 @@ def start_build(cfg, yumlock, gitlock, branch, recipe_name, compose_type, test_m | ||||
|         # Write the root partition and it's size in MB (rounded up) | ||||
|         f.write('part / --size=%d\n' % ceil(installed_size / 1024**2)) | ||||
| 
 | ||||
|         f.write(ks_template) | ||||
|         # Some customizations modify the template before writing it | ||||
|         f.write(customize_ks_template(ks_template, recipe)) | ||||
| 
 | ||||
|         for d in deps: | ||||
|             f.write(dep_nevra(d)+"\n") | ||||
|         f.write("%end\n") | ||||
| 
 | ||||
|         # Other customizations can be appended to the kickstart | ||||
|         add_customizations(f, recipe) | ||||
| 
 | ||||
|     # Setup the config to pass to novirt_install | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| # | ||||
| # Copyright (C) 2017  Red Hat, Inc. | ||||
| # Copyright (C) 2017-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 | ||||
|  | ||||
							
								
								
									
										18
									
								
								tests/pylorax/blueprints/example-append.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								tests/pylorax/blueprints/example-append.toml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,18 @@ | ||||
| name = "example-append" | ||||
| description = "An example using kernel append customization" | ||||
| version = "0.0.1" | ||||
| 
 | ||||
| [[packages]] | ||||
| name = "tmux" | ||||
| version = "1.8" | ||||
| 
 | ||||
| [[packages]] | ||||
| name = "openssh-server" | ||||
| version = "7.*" | ||||
| 
 | ||||
| [[packages]] | ||||
| name = "rsync" | ||||
| version = "3.1.*" | ||||
| 
 | ||||
| [customizations.kernel] | ||||
| append = "nosmt=force" | ||||
| @ -17,8 +17,10 @@ | ||||
| from StringIO import StringIO | ||||
| import unittest | ||||
| 
 | ||||
| from pylorax.api.compose import add_customizations | ||||
| from pylorax.api.compose import add_customizations, compose_types | ||||
| from pylorax.api.compose import bootloader_append, customize_ks_template | ||||
| from pylorax.api.recipes import recipe_from_toml | ||||
| from pylorax.sysutils import joinpaths | ||||
| 
 | ||||
| BASE_RECIPE = """name = "test-cases" | ||||
| description = "Used for testing" | ||||
| @ -200,3 +202,59 @@ class CustomizationsTestCase(unittest.TestCase): | ||||
|     def test_root_plain_key(self): | ||||
|         self.assertCustomization(ROOT_PLAIN_KEY, 'rootpw --plaintext "plainpassword"') | ||||
|         self.assertCustomization(ROOT_PLAIN_KEY, 'sshkey --user root "A SSH KEY FOR THE USER"') | ||||
|         self.assertNotCustomization(ROOT_PLAIN_KEY, "rootpw --lock") | ||||
| 
 | ||||
|     def test_bootloader_append(self): | ||||
|         """Test bootloader_append function""" | ||||
| 
 | ||||
|         self.assertEqual(bootloader_append("", "nosmt=force"), 'bootloader --append="nosmt=force" --location=none') | ||||
|         self.assertEqual(bootloader_append("", "nosmt=force console=ttyS0,115200n8"), | ||||
|                          'bootloader --append="nosmt=force console=ttyS0,115200n8" --location=none') | ||||
|         self.assertEqual(bootloader_append("bootloader --location=none", "nosmt=force"), | ||||
|                          'bootloader --append="nosmt=force" --location=none') | ||||
|         self.assertEqual(bootloader_append("bootloader --location=none", "console=ttyS0,115200n8 nosmt=force"), | ||||
|                          'bootloader --append="console=ttyS0,115200n8 nosmt=force" --location=none') | ||||
|         self.assertEqual(bootloader_append('bootloader --append="no_timer_check console=ttyS0,115200n8" --location=mbr', "nosmt=force"), | ||||
|                          'bootloader --append="no_timer_check console=ttyS0,115200n8 nosmt=force" --location=mbr') | ||||
|         self.assertEqual(bootloader_append('bootloader --append="console=tty1" --location=mbr --password="BADPASSWORD"', "nosmt=force"), | ||||
|                          'bootloader --append="console=tty1 nosmt=force" --location=mbr --password="BADPASSWORD"') | ||||
| 
 | ||||
|     def _checkBootloader(self, result, append_str): | ||||
|         """Find the bootloader line and make sure append_str is in it""" | ||||
| 
 | ||||
|         for line in result.splitlines(): | ||||
|             if line.startswith("bootloader") and append_str in line: | ||||
|                 return True | ||||
|         return False | ||||
| 
 | ||||
|     def test_customize_ks_template(self): | ||||
|         """Test that [customizations.kernel] works correctly""" | ||||
|         blueprint_data = """name = "test-kernel" | ||||
| description = "test recipe" | ||||
| version = "0.0.1" | ||||
| 
 | ||||
| [customizations.kernel] | ||||
| append="nosmt=force" | ||||
| """ | ||||
|         recipe = recipe_from_toml(blueprint_data) | ||||
| 
 | ||||
|         # Test against a kickstart without bootloader | ||||
|         result = customize_ks_template("firewall --enabled\n", recipe) | ||||
|         self.assertTrue(self._checkBootloader(result, "nosmt=force")) | ||||
| 
 | ||||
|         # Test against all of the available templates | ||||
|         share_dir = "./share/" | ||||
|         errors = [] | ||||
|         for compose_type in compose_types(share_dir): | ||||
|             # Read the kickstart template for this type | ||||
|             ks_template_path = joinpaths(share_dir, "composer", compose_type) + ".ks" | ||||
|             ks_template = open(ks_template_path, "r").read() | ||||
|             result = customize_ks_template(ks_template, recipe) | ||||
|             if not self._checkBootloader(result, "nosmt=force"): | ||||
|                 errors.append(("compose_type %s failed" % compose_type, result)) | ||||
| 
 | ||||
|         # Print the bad results | ||||
|         for e, r in errors: | ||||
|             print("%s:\n%s\n\n" % (e, r)) | ||||
| 
 | ||||
|         self.assertEqual(errors, []) | ||||
|  | ||||
| @ -138,9 +138,9 @@ class ServerTestCase(unittest.TestCase): | ||||
| 
 | ||||
|     def test_02_blueprints_list(self): | ||||
|         """Test the /api/v0/blueprints/list route""" | ||||
|         list_dict = {"blueprints":["example-atlas", "example-custom-base", "example-development", | ||||
|         list_dict = {"blueprints":["example-append", "example-atlas", "example-custom-base", "example-development", | ||||
|                                    "example-glusterfs", "example-http-server", "example-jboss", | ||||
|                                    "example-kubernetes"], "limit":20, "offset":0, "total":7} | ||||
|                                    "example-kubernetes"], "limit":20, "offset":0, "total":8} | ||||
|         resp = self.server.get("/api/v0/blueprints/list") | ||||
|         data = json.loads(resp.data) | ||||
|         self.assertEqual(data, list_dict) | ||||
| @ -1188,6 +1188,53 @@ class ServerTestCase(unittest.TestCase): | ||||
|         self.assertIn(build_id_fail, ids, "Failed build not listed by /compose/status status filter") | ||||
|         self.assertNotIn(build_id_success, "Finished build listed by /compose/status status filter") | ||||
| 
 | ||||
|     def test_compose_14_kernel_append(self): | ||||
|         """Test the /api/v0/compose with kernel append customization""" | ||||
|         test_compose = {"blueprint_name": "example-append", | ||||
|                         "compose_type": "tar", | ||||
|                         "branch": "master"} | ||||
| 
 | ||||
|         resp = self.server.post("/api/v0/compose?test=2", | ||||
|                                 data=json.dumps(test_compose), | ||||
|                                 content_type="application/json") | ||||
|         data = json.loads(resp.data) | ||||
|         self.assertNotEqual(data, None) | ||||
|         self.assertEqual(data["status"], True, "Failed to start test compose: %s" % data) | ||||
| 
 | ||||
|         build_id = data["build_id"] | ||||
| 
 | ||||
|         # Is it in the queue list (either new or run is fine, based on timing) | ||||
|         resp = self.server.get("/api/v0/compose/queue") | ||||
|         data = json.loads(resp.data) | ||||
|         self.assertNotEqual(data, None) | ||||
|         ids = [e["id"] for e in data["new"] + data["run"]] | ||||
|         self.assertEqual(build_id in ids, True, "Failed to add build to the queue") | ||||
| 
 | ||||
|         # Wait for it to start | ||||
|         self.assertEqual(self.wait_for_status(build_id, ["RUNNING"]), True, "Failed to start test compose") | ||||
| 
 | ||||
|         # Wait for it to finish | ||||
|         self.assertEqual(self.wait_for_status(build_id, ["FINISHED"]), True, "Failed to finish test compose") | ||||
| 
 | ||||
|         resp = self.server.get("/api/v0/compose/info/%s" % build_id) | ||||
|         data = json.loads(resp.data) | ||||
|         self.assertNotEqual(data, None) | ||||
|         self.assertEqual(data["queue_status"], "FINISHED", "Build not in FINISHED state") | ||||
| 
 | ||||
|         # 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 | ||||
|         # nosmt=force should be in the bootloader line, find it and check it | ||||
|         bootloader_line = "" | ||||
|         for line in final_ks.splitlines(): | ||||
|             if line.startswith("bootloader"): | ||||
|                 bootloader_line = line | ||||
|                 break | ||||
|         self.assertNotEqual(bootloader_line, "", "No bootloader line found") | ||||
|         self.assertTrue("nosmt=force" in bootloader_line) | ||||
| 
 | ||||
|     def assertInputError(self, resp): | ||||
|         """Check all the conditions for a successful input check error result""" | ||||
|         data = json.loads(resp.data) | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user