lorax-composer: Add timezone support to blueprint

For example:

[customizations.timezone]
timezone = "US/Samoa"
ntpservers = ["0.pool.ntp.org"]

Also includes tests.

This removes the timezone kickstart command from all of the templates
except for google.ks which needs to set it's own ntp servers and timezone.

If timezone isn't included in the blueprint, and it is not already in a
template, it will be set to 'timezone UTC' by default.

If timezone is set in a template it is left as-is, under the assumption
that the image type requires it to boot correctly.

(cherry picked from commit 9bdbb29662)

Related: rhbz#1718473
This commit is contained in:
Brian C. Lane 2019-04-09 16:37:24 -07:00 committed by Alexander Todorov
parent e72debe1d1
commit 956ebfc77c
12 changed files with 289 additions and 67 deletions

View File

@ -21,8 +21,6 @@ selinux --enforcing
logging --level=info logging --level=info
# Shutdown after installation # Shutdown after installation
shutdown shutdown
# System timezone
timezone US/Eastern
# System bootloader configuration # System bootloader configuration
bootloader --location=mbr --append="no_timer_check console=ttyS0,115200n8 console=tty1 net.ifnames=0" bootloader --location=mbr --append="no_timer_check console=ttyS0,115200n8 console=tty1 net.ifnames=0"

View File

@ -16,8 +16,6 @@ selinux --enforcing
logging --level=info logging --level=info
# Shutdown after installation # Shutdown after installation
shutdown shutdown
# System timezone
timezone US/Eastern
# System bootloader configuration (unpartitioned fs image doesn't use a bootloader) # System bootloader configuration (unpartitioned fs image doesn't use a bootloader)
bootloader --location=none bootloader --location=none

View File

@ -21,8 +21,6 @@ logging --level=info
shutdown shutdown
# System services # System services
services --disabled="network,sshd" --enabled="NetworkManager" services --disabled="network,sshd" --enabled="NetworkManager"
# System timezone
timezone US/Eastern
# System bootloader configuration # System bootloader configuration
bootloader --location=mbr bootloader --location=mbr
# Clear the Master Boot Record # Clear the Master Boot Record

View File

@ -16,8 +16,6 @@ selinux --enforcing
logging --level=info logging --level=info
# Shutdown after installation # Shutdown after installation
shutdown shutdown
# System timezone
timezone US/Eastern
# System bootloader configuration # System bootloader configuration
bootloader --location=mbr --append="no_timer_check console=ttyS0,115200n8 console=tty1 net.ifnames=0" bootloader --location=mbr --append="no_timer_check console=ttyS0,115200n8 console=tty1 net.ifnames=0"

View File

@ -16,8 +16,6 @@ selinux --enforcing
logging --level=info logging --level=info
# Shutdown after installation # Shutdown after installation
shutdown shutdown
# System timezone
timezone US/Eastern
# System bootloader configuration # System bootloader configuration
bootloader --location=mbr bootloader --location=mbr
# Clear the Master Boot Record # Clear the Master Boot Record

View File

@ -16,8 +16,6 @@ selinux --enforcing
logging --level=info logging --level=info
# Shutdown after installation # Shutdown after installation
shutdown shutdown
# System timezone
timezone US/Eastern
# System bootloader configuration # System bootloader configuration
bootloader --location=mbr bootloader --location=mbr
# Clear the Master Boot Record # Clear the Master Boot Record

View File

@ -16,8 +16,6 @@ selinux --enforcing
logging --level=info logging --level=info
# Shutdown after installation # Shutdown after installation
shutdown shutdown
# System timezone
timezone US/Eastern
# System bootloader configuration (tar doesn't need a bootloader) # System bootloader configuration (tar doesn't need a bootloader)
bootloader --location=none bootloader --location=none

View File

@ -19,8 +19,6 @@ selinux --enforcing
logging --level=info logging --level=info
# Shutdown after installation # Shutdown after installation
shutdown shutdown
# System timezone
timezone US/Eastern
# System bootloader configuration # System bootloader configuration
bootloader --location=mbr --append="no_timer_check console=ttyS0,115200n8 earlyprintk=ttyS0,115200 rootdelay=300 net.ifnames=0" bootloader --location=mbr --append="no_timer_check console=ttyS0,115200n8 earlyprintk=ttyS0,115200 rootdelay=300 net.ifnames=0"

View File

@ -16,8 +16,6 @@ selinux --enforcing
logging --level=info logging --level=info
# Shutdown after installation # Shutdown after installation
shutdown shutdown
# System timezone
timezone US/Eastern
# System bootloader configuration # System bootloader configuration
bootloader --location=mbr bootloader --location=mbr

View File

@ -170,6 +170,44 @@ def get_kernel_append(recipe):
return recipe["customizations"]["kernel"]["append"] return recipe["customizations"]["kernel"]["append"]
def timezone_cmd(line, settings):
""" Update the timezone line with the settings
:param line: The timezone ... line
:type line: str
:param settings: A dict with timezone and/or ntpservers list
:type settings: dict
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 "timezone" in settings:
ks.handler.timezone.timezone = settings["timezone"]
if "ntpservers" in settings:
ks.handler.timezone.ntpservers = settings["ntpservers"]
# Converting back to a string includes a comment, return just the timezone line
return str(ks.handler.timezone).splitlines()[-1]
def get_timezone_settings(recipe):
"""Return the customizations.timezone dict
:param recipe:
:type recipe: Recipe object
:returns: append value or empty string
:rtype: dict
"""
if "customizations" not in recipe or \
"timezone" not in recipe["customizations"]:
return {}
return recipe["customizations"]["timezone"]
def customize_ks_template(ks_template, recipe): def customize_ks_template(ks_template, recipe):
""" Customize the kickstart template and return it """ Customize the kickstart template and return it
@ -178,26 +216,60 @@ def customize_ks_template(ks_template, recipe):
:param recipe: :param recipe:
:type recipe: Recipe object :type recipe: Recipe object
Apply customizations to existing template commands, or add defaults for ones that are
missing and required.
Apply customizations.kernel.append to the bootloader argument in the template. Apply customizations.kernel.append to the bootloader argument in the template.
Add bootloader line if it is missing. Add bootloader line if it is missing.
Add default timezone if needed. It does NOT replace an existing timezone entry
""" """
kernel_append = get_kernel_append(recipe) # Commands to be modified [NEW-COMMAND-FUNC, NEW-VALUE, DEFAULT, REPLACE]
if not kernel_append: # The function is called with a kickstart command string and the value to replace
return ks_template # The value is specific to the command, and is understood by the function
found_bootloader = False # The default is a complete kickstart command string, suitable for writing to the template
# If REPLACE is False it will not change an existing entry only add a missing one
commands = {"bootloader": [bootloader_append,
get_kernel_append(recipe),
'bootloader --location=none', True],
"timezone": [timezone_cmd,
get_timezone_settings(recipe),
'timezone UTC', False],
}
found = {}
output = StringIO() output = StringIO()
for line in ks_template.splitlines(): for line in ks_template.splitlines():
if not line.startswith("bootloader"): for cmd in commands:
print(line.decode("utf-8"), file=output) (new_command, value, default, replace) = commands[cmd]
continue if line.startswith(cmd):
found_bootloader = True found[cmd] = True
log.debug("Found bootloader line: %s", line) if value and replace:
print(bootloader_append(line, kernel_append).decode("utf-8"), file=output) log.debug("Replacing %s with %s", cmd, value)
print(new_command(line, value), file=output)
if found_bootloader:
return output.getvalue()
else: else:
return 'bootloader --append="%s" --location=none' % kernel_append + output.getvalue() log.debug("Skipping %s", cmd)
print(line, file=output)
break
else:
# No matches, write the line as-is
print(line, file=output)
# Write out defaults for the ones not found
# These must go FIRST because the template still needs to have the packages added
defaults = StringIO()
for cmd in commands:
if cmd in found:
continue
(new_command, value, default, _) = commands[cmd]
if value and default:
log.debug("Setting %s to use %s", cmd, value)
print(new_command(default, value), file=defaults)
elif default:
log.debug("Setting %s to %s", cmd, default)
print(default, file=defaults)
return defaults.getvalue() + output.getvalue()
def write_ks_root(f, user): def write_ks_root(f, user):

View File

@ -18,7 +18,8 @@ from StringIO import StringIO
import unittest import unittest
from pylorax.api.compose import add_customizations, compose_types from pylorax.api.compose import add_customizations, compose_types
from pylorax.api.compose import bootloader_append, customize_ks_template from pylorax.api.compose import timezone_cmd, get_timezone_settings
from pylorax.api.compose import get_kernel_append, bootloader_append, customize_ks_template
from pylorax.api.recipes import recipe_from_toml from pylorax.api.recipes import recipe_from_toml
from pylorax.sysutils import joinpaths from pylorax.sysutils import joinpaths
@ -32,6 +33,10 @@ HOSTNAME = BASE_RECIPE + """[customizations]
hostname = "testhostname" hostname = "testhostname"
""" """
TIMEZONE = BASE_RECIPE + """[customizations]
timezone = "US/Samoa"
"""
SSHKEY = BASE_RECIPE + """[[customizations.sshkey]] SSHKEY = BASE_RECIPE + """[[customizations.sshkey]]
user = "root" user = "root"
key = "ROOT SSH KEY" key = "ROOT SSH KEY"
@ -204,6 +209,22 @@ class CustomizationsTestCase(unittest.TestCase):
self.assertCustomization(ROOT_PLAIN_KEY, 'sshkey --user root "A SSH KEY FOR THE USER"') self.assertCustomization(ROOT_PLAIN_KEY, 'sshkey --user root "A SSH KEY FOR THE USER"')
self.assertNotCustomization(ROOT_PLAIN_KEY, "rootpw --lock") self.assertNotCustomization(ROOT_PLAIN_KEY, "rootpw --lock")
def test_get_kernel_append(self):
"""Test get_kernel_append function"""
blueprint_data = """name = "test-kernel"
description = "test recipe"
version = "0.0.1"
"""
blueprint2_data = blueprint_data + """
[customizations.kernel]
append="nosmt=force"
"""
recipe = recipe_from_toml(blueprint_data)
self.assertEqual(get_kernel_append(recipe), "")
recipe = recipe_from_toml(blueprint2_data)
self.assertEqual(get_kernel_append(recipe), "nosmt=force")
def test_bootloader_append(self): def test_bootloader_append(self):
"""Test bootloader_append function""" """Test bootloader_append function"""
@ -219,19 +240,148 @@ class CustomizationsTestCase(unittest.TestCase):
self.assertEqual(bootloader_append('bootloader --append="console=tty1" --location=mbr --password="BADPASSWORD"', "nosmt=force"), self.assertEqual(bootloader_append('bootloader --append="console=tty1" --location=mbr --password="BADPASSWORD"', "nosmt=force"),
'bootloader --append="console=tty1 nosmt=force" --location=mbr --password="BADPASSWORD"') 'bootloader --append="console=tty1 nosmt=force" --location=mbr --password="BADPASSWORD"')
def _checkBootloader(self, result, append_str): def test_get_timezone_settings(self):
"""Find the bootloader line and make sure append_str is in it""" """Test get_timezone_settings function"""
blueprint_data = """name = "test-kernel"
description = "test recipe"
version = "0.0.1"
"""
blueprint2_data = blueprint_data + """
[customizations.timezone]
timezone = "US/Samoa"
"""
blueprint3_data = blueprint_data + """
[customizations.timezone]
ntpservers = ["0.north-america.pool.ntp.org", "1.north-america.pool.ntp.org"]
"""
blueprint4_data = blueprint_data + """
[customizations.timezone]
timezone = "US/Samoa"
ntpservers = ["0.north-america.pool.ntp.org", "1.north-america.pool.ntp.org"]
"""
recipe = recipe_from_toml(blueprint_data)
self.assertEqual(get_timezone_settings(recipe), {})
recipe = recipe_from_toml(blueprint2_data)
self.assertEqual(get_timezone_settings(recipe), {"timezone": "US/Samoa"})
recipe = recipe_from_toml(blueprint3_data)
self.assertEqual(get_timezone_settings(recipe),
{"ntpservers": ["0.north-america.pool.ntp.org", "1.north-america.pool.ntp.org"]})
recipe = recipe_from_toml(blueprint4_data)
self.assertEqual(get_timezone_settings(recipe),
{"timezone": "US/Samoa",
"ntpservers": ["0.north-america.pool.ntp.org", "1.north-america.pool.ntp.org"]})
def test_timezone_cmd(self):
"""Test timezone_cmd function"""
self.assertEqual(timezone_cmd("timezone UTC", {}), 'timezone UTC')
self.assertEqual(timezone_cmd("timezone FOO", {"timezone": "US/Samoa"}),
'timezone US/Samoa')
self.assertEqual(timezone_cmd("timezone FOO",
{"timezone": "US/Samoa", "ntpservers": ["0.ntp.org", "1.ntp.org"]}),
'timezone US/Samoa --ntpservers=0.ntp.org,1.ntp.org')
self.assertEqual(timezone_cmd("timezone --ntpservers=a,b,c FOO",
{"timezone": "US/Samoa", "ntpservers": ["0.pool.ntp.org", "1.pool.ntp.org"]}),
'timezone US/Samoa --ntpservers=0.pool.ntp.org,1.pool.ntp.org')
def _checkBootloader(self, result, append_str, line_limit=0):
"""Find the bootloader line and make sure append_str is in it"""
# Optionally check to make sure the change is at the top of the template
line_num = 0
for line in result.splitlines(): for line in result.splitlines():
if line.startswith("bootloader") and append_str in line: if line.startswith("bootloader") and append_str in line:
if line_limit == 0 or line_num < line_limit:
return True return True
else:
print("FAILED: bootloader not in the first %d lines of the output" % line_limit)
return False
line_num += 1
return False return False
def _checkTemplates(self, recipe): def _checkTimezone(self, result, settings, line_limit=0):
"""Apply the recipe to all the templates""" """Find the timezone line and make sure it is as expected"""
# Optionally check to make sure the change is at the top of the template
line_num = 0
for line in result.splitlines():
if line.startswith("timezone"):
if settings["timezone"] in line and all([True for n in settings["ntpservers"] if n in line]):
if line_limit == 0 or line_num < line_limit:
return True
else:
print("FAILED: timezone not in the first %d lines of the output" % line_limit)
return False
else:
print("FAILED: %s not matching %s" % (settings, line))
line_num += 1
return False
def test_template_defaults(self):
"""Test that customize_ks_template includes defaults correctly"""
blueprint_data = """name = "test-kernel"
description = "test recipe"
version = "0.0.1"
[[packages]]
name = "lorax"
version = "*"
"""
recipe = recipe_from_toml(blueprint_data)
# Make sure that a kickstart with no bootloader and no timezone has them added
result = customize_ks_template("firewall --enabled\n", recipe)
print(result)
self.assertEqual(sum([1 for l in result.splitlines() if l.startswith("bootloader")]), 1)
self.assertTrue(self._checkBootloader(result, "none", line_limit=2))
self.assertEqual(sum([1 for l in result.splitlines() if l.startswith("timezone")]), 1)
self.assertTrue(self._checkTimezone(result, {"timezone": "UTC", "ntpservers": []}, line_limit=2))
# Make sure that a kickstart with a bootloader, and no timezone has timezone added to the top
result = customize_ks_template("firewall --enabled\nbootloader --location=mbr\n", recipe)
print(result)
self.assertEqual(sum([1 for l in result.splitlines() if l.startswith("bootloader")]), 1)
self.assertTrue(self._checkBootloader(result, "mbr"))
self.assertEqual(sum([1 for l in result.splitlines() if l.startswith("timezone")]), 1)
self.assertTrue(self._checkTimezone(result, {"timezone": "UTC", "ntpservers": []}, line_limit=1))
# Make sure that a kickstart with a bootloader and timezone has neither added
result = customize_ks_template("firewall --enabled\nbootloader --location=mbr\ntimezone US/Samoa\n", recipe)
print(result)
self.assertEqual(sum([1 for l in result.splitlines() if l.startswith("bootloader")]), 1)
self.assertTrue(self._checkBootloader(result, "mbr"))
self.assertEqual(sum([1 for l in result.splitlines() if l.startswith("timezone")]), 1)
self.assertTrue(self._checkTimezone(result, {"timezone": "US/Samoa", "ntpservers": []}))
def test_customize_ks_template(self):
"""Test that customize_ks_template works correctly"""
blueprint_data = """name = "test-kernel"
description = "test recipe"
version = "0.0.1"
[customizations.kernel]
append="nosmt=force"
[customizations.timezone]
timezone = "US/Samoa"
ntpservers = ["0.north-america.pool.ntp.org", "1.north-america.pool.ntp.org"]
"""
tz_dict = {"timezone": "US/Samoa", "ntpservers": ["0.north-america.pool.ntp.org", "1.north-america.pool.ntp.org"]}
recipe = recipe_from_toml(blueprint_data)
# Test against a kickstart without bootloader # Test against a kickstart without bootloader
result = customize_ks_template("firewall --enabled\n", recipe) result = customize_ks_template("firewall --enabled\n", recipe)
self.assertTrue(self._checkBootloader(result, "nosmt=force", line_limit=2))
self.assertEqual(sum([1 for l in result.splitlines() if l.startswith("bootloader")]), 1)
self.assertTrue(self._checkTimezone(result, tz_dict, line_limit=2))
self.assertEqual(sum([1 for l in result.splitlines() if l.startswith("timezone")]), 1)
# Test against a kickstart with a bootloader line
result = customize_ks_template("firewall --enabled\nbootloader --location=mbr\n", recipe)
self.assertTrue(self._checkBootloader(result, "nosmt=force")) self.assertTrue(self._checkBootloader(result, "nosmt=force"))
self.assertTrue(self._checkTimezone(result, tz_dict, line_limit=2))
# Test against all of the available templates # Test against all of the available templates
share_dir = "./share/" share_dir = "./share/"
@ -242,37 +392,23 @@ class CustomizationsTestCase(unittest.TestCase):
ks_template = open(ks_template_path, "r").read() ks_template = open(ks_template_path, "r").read()
result = customize_ks_template(ks_template, recipe) result = customize_ks_template(ks_template, recipe)
if not self._checkBootloader(result, "nosmt=force"): if not self._checkBootloader(result, "nosmt=force"):
errors.append(("compose_type %s failed" % compose_type, result)) errors.append(("bootloader for compose_type %s failed" % compose_type, result))
if sum([1 for l in result.splitlines() if l.startswith("bootloader")]) != 1:
errors.append(("bootloader for compose_type %s failed: More than 1 entry" % compose_type, result))
# google images should retain their timezone settings
if compose_type == "google":
if self._checkTimezone(result, tz_dict):
errors.append(("timezone for compose_type %s failed" % compose_type, result))
elif not self._checkTimezone(result, tz_dict, line_limit=2):
# None of the templates have a timezone to modify, it should be placed at the top
errors.append(("timezone for compose_type %s failed" % compose_type, result))
if sum([1 for l in result.splitlines() if l.startswith("timezone")]) != 1:
errors.append(("timezone for compose_type %s failed: More than 1 entry" % compose_type, result))
# Print the bad results # Print the bad results
for e, r in errors: for e, r in errors:
print("%s:\n%s\n\n" % (e, r)) print("%s:\n%s\n\n" % (e, r))
self.assertEqual(errors, []) self.assertEqual(errors, [])
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)
self._checkTemplates(recipe)
def test_customize_list(self):
"""Test that [customizations.kernel] works correctly with [[customizations]] error"""
blueprint_data = """name = "test-kernel"
description = "test recipe"
version = "0.0.1"
[[customizations]]
hostname = "testing"
[customizations.kernel]
append="nosmt=force"
"""
recipe = recipe_from_toml(blueprint_data)
self._checkTemplates(recipe)

View File

@ -22,7 +22,7 @@ import tempfile
import unittest import unittest
import pylorax.api.recipes as recipes import pylorax.api.recipes as recipes
from pylorax.api.compose import add_customizations from pylorax.api.compose import add_customizations, customize_ks_template
from pylorax.sysutils import joinpaths from pylorax.sysutils import joinpaths
from pykickstart.parser import KickstartParser from pykickstart.parser import KickstartParser
@ -364,6 +364,7 @@ class CustomizationsTests(unittest.TestCase):
# write out the customization data, and parse the resulting kickstart # write out the customization data, and parse the resulting kickstart
with tempfile.NamedTemporaryFile(prefix="lorax.test.customizations", mode="w") as f: with tempfile.NamedTemporaryFile(prefix="lorax.test.customizations", mode="w") as f:
f.write(customize_ks_template("", recipe_obj))
add_customizations(f, recipe_obj) add_customizations(f, recipe_obj)
f.flush() f.flush()
ks.readKickstart(f.name) ks.readKickstart(f.name)
@ -415,6 +416,30 @@ hostname = "testy.example.com"
ks = self._blueprint_to_ks(blueprint_data) ks = self._blueprint_to_ks(blueprint_data)
self.assertEqual(ks.handler.network.hostname, "testy.example.com") self.assertEqual(ks.handler.network.hostname, "testy.example.com")
def test_timezone(self):
blueprint_data = """name = "test-timezone"
description = "test recipe"
version = "0.0.1"
[customizations.timezone]
timezone = "US/Samoa"
"""
ks = self._blueprint_to_ks(blueprint_data)
self.assertEqual(ks.handler.timezone.timezone, "US/Samoa")
def test_timezone_ntpservers(self):
blueprint_data = """name = "test-ntpservers"
description = "test recipe"
version = "0.0.1"
[customizations.timezone]
timezone = "US/Samoa"
ntpservers = ["1.north-america.pool.ntp.org"]
"""
ks = self._blueprint_to_ks(blueprint_data)
self.assertEqual(ks.handler.timezone.timezone, "US/Samoa")
self.assertEqual(ks.handler.timezone.ntpservers, ["1.north-america.pool.ntp.org"])
def test_user(self): def test_user(self):
blueprint_data = """name = "test-user" blueprint_data = """name = "test-user"
description = "test recipe" description = "test recipe"
@ -525,6 +550,10 @@ name = "widget"
[[customizations.group]] [[customizations.group]]
name = "students" name = "students"
[customizations.timezone]
timezone = "US/Samoa"
ntpservers = ["0.north-america.pool.ntp.org", "1.north-america.pool.ntp.org"]
""" """
ks = self._blueprint_to_ks(blueprint_data) ks = self._blueprint_to_ks(blueprint_data)
@ -564,3 +593,6 @@ name = "students"
studentsGroup = self._find_group(ks, "students") studentsGroup = self._find_group(ks, "students")
self.assertIsNotNone(studentsGroup) self.assertIsNotNone(studentsGroup)
self.assertEqual(studentsGroup.name, "students") self.assertEqual(studentsGroup.name, "students")
self.assertEqual(ks.handler.timezone.timezone, "US/Samoa")
self.assertEqual(ks.handler.timezone.ntpservers, ["0.north-america.pool.ntp.org", "1.north-america.pool.ntp.org"])