Merge ~mwhudson/curtin:more-grub-config-object into curtin:master

Proposed by Michael Hudson-Doyle
Status: Merged
Approved by: Michael Hudson-Doyle
Approved revision: 09f5220f29e530d687efcf3675e578a150dc70d3
Merge reported by: Server Team CI bot
Merged at revision: not available
Proposed branch: ~mwhudson/curtin:more-grub-config-object
Merge into: curtin:master
Diff against target: 774 lines (+284/-95)
6 files modified
curtin/commands/curthooks.py (+24/-20)
curtin/commands/install_grub.py (+21/-14)
curtin/config.py (+130/-9)
tests/unittests/test_commands_install_grub.py (+34/-35)
tests/unittests/test_config.py (+57/-0)
tests/unittests/test_curthooks.py (+18/-17)
Reviewer Review Type Date Requested Status
Server Team CI bot continuous-integration Approve
Dan Bungert Approve
Review via email: mp+444608@code.launchpad.net

Commit message

extend grub config object to handle all config under the 'grub' key

Pinched a hacked down version of my deserialization code from
subiquity...

To post a comment you must log in.
Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Dan Bungert (dbungert) wrote :

LGTM but I didn't read the serialization parts too closely. Let's think about not having that duplicated in the future.

review: Approve
Revision history for this message
Michael Hudson-Doyle (mwhudson) :
09f5220... by Michael Hudson-Doyle

be more careful to preserve semantics

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/curtin/commands/curthooks.py b/curtin/commands/curthooks.py
2index 868fbad..c52d5ab 100644
3--- a/curtin/commands/curthooks.py
4+++ b/curtin/commands/curthooks.py
5@@ -423,9 +423,8 @@ def install_kernel(cfg, target):
6 " System may not boot.", package)
7
8
9-def uefi_remove_old_loaders(grubcfg: dict, target):
10+def uefi_remove_old_loaders(grubcfg: config.GrubConfig, target: str):
11 """Removes the old UEFI loaders from efibootmgr."""
12- grubcfg = config.fromdict(config.GrubConfig, grubcfg)
13 efi_state = util.get_efibootmgr(target)
14
15 LOG.debug('UEFI remove old olders efi state:\n%s', efi_state)
16@@ -517,7 +516,7 @@ def _reorder_new_entry(
17
18
19 def uefi_reorder_loaders(
20- grubcfg: dict,
21+ grubcfg: config.GrubConfig,
22 target: str,
23 efi_orig_state: util.EFIBootState,
24 variant: str,
25@@ -533,8 +532,6 @@ def uefi_reorder_loaders(
26 is installed after the the previous first entry (before we installed grub).
27
28 """
29- grubcfg = config.fromdict(config.GrubConfig, grubcfg)
30-
31 if not grubcfg.reorder_uefi:
32 LOG.debug("Skipped reordering of UEFI boot methods.")
33 LOG.debug("Currently booted UEFI loader might no longer boot.")
34@@ -578,9 +575,10 @@ def uefi_reorder_loaders(
35 in_chroot.subp(['efibootmgr', '-o', new_boot_order])
36
37
38-def uefi_remove_duplicate_entries(grubcfg: dict, target: str) -> None:
39- grubcfg = config.fromdict(config.GrubConfig, grubcfg)
40-
41+def uefi_remove_duplicate_entries(
42+ grubcfg: config.GrubConfig,
43+ target: str,
44+ ) -> None:
45 if not grubcfg.remove_duplicate_entries:
46 LOG.debug("Skipped removing duplicate UEFI boot entries per config.")
47 return
48@@ -725,7 +723,12 @@ def uefi_find_grub_device_ids(sconfig):
49 return grub_device_ids
50
51
52-def setup_grub(cfg, target, osfamily, variant):
53+def setup_grub(
54+ cfg: dict,
55+ target: str,
56+ osfamily: str,
57+ variant: str,
58+ ) -> None:
59 # target is the path to the mounted filesystem
60
61 # FIXME: these methods need moving to curtin.block
62@@ -733,11 +736,13 @@ def setup_grub(cfg, target, osfamily, variant):
63 from curtin.commands.block_meta import (extract_storage_ordered_dict,
64 get_path_to_storage_volume)
65
66- grubcfg = cfg.get('grub', {})
67+ grubcfg_d = cfg.get('grub', {})
68
69 # copy legacy top level name
70- if 'grub_install_devices' in cfg and 'install_devices' not in grubcfg:
71- grubcfg['install_devices'] = cfg['grub_install_devices']
72+ if 'grub_install_devices' in cfg and 'install_devices' not in grubcfg_d:
73+ grubcfg_d['install_devices'] = cfg['grub_install_devices']
74+
75+ grubcfg = config.fromdict(config.GrubConfig, grubcfg_d)
76
77 LOG.debug("setup grub on target %s", target)
78 # if there is storage config, look for devices tagged with 'grub_device'
79@@ -763,17 +768,16 @@ def setup_grub(cfg, target, osfamily, variant):
80 get_path_to_storage_volume(item_id, storage_cfg_odict))
81
82 if len(storage_grub_devices) > 0:
83- if len(grubcfg.get('install_devices', [])):
84+ if grubcfg.install_devices and \
85+ grubcfg.install_devices is not grubcfg.install_devices_default:
86 LOG.warn("Storage Config grub device config takes precedence "
87 "over grub 'install_devices' value, ignoring: %s",
88 grubcfg['install_devices'])
89- grubcfg['install_devices'] = storage_grub_devices
90+ grubcfg.install_devices = storage_grub_devices
91
92- LOG.debug("install_devices: %s", grubcfg.get('install_devices'))
93- if 'install_devices' in grubcfg:
94- instdevs = grubcfg.get('install_devices')
95- if isinstance(instdevs, str):
96- instdevs = [instdevs]
97+ LOG.debug("install_devices: %s", grubcfg.install_devices)
98+ if grubcfg.install_devices is not grubcfg.install_devices_default:
99+ instdevs = grubcfg.install_devices
100 if instdevs is None:
101 LOG.debug("grub installation disabled by config")
102 else:
103@@ -823,7 +827,7 @@ def setup_grub(cfg, target, osfamily, variant):
104 else:
105 instdevs = ["none"]
106
107- update_nvram = grubcfg.get('update_nvram', True)
108+ update_nvram = grubcfg.update_nvram
109 if uefi_bootable and update_nvram:
110 efi_orig_state = util.get_efibootmgr(target)
111 uefi_remove_old_loaders(grubcfg, target)
112diff --git a/curtin/commands/install_grub.py b/curtin/commands/install_grub.py
113index 03b4670..2faa2c6 100644
114--- a/curtin/commands/install_grub.py
115+++ b/curtin/commands/install_grub.py
116@@ -3,6 +3,7 @@ import re
117 import platform
118 import shutil
119 import sys
120+from typing import List, Optional
121
122 from curtin import block
123 from curtin import config
124@@ -137,7 +138,7 @@ def prepare_grub_dir(target, grub_cfg):
125 shutil.move(ci_cfg, ci_cfg + '.disabled')
126
127
128-def get_carryover_params(distroinfo):
129+def get_carryover_params(distroinfo) -> str:
130 # return a string to append to installed systems boot parameters
131 # it may include a '--' after a '---'
132 # see LP: 1402042 for some history here.
133@@ -206,14 +207,17 @@ def replace_grub_cmdline_linux_default(target, new_args):
134 LOG.debug('updated %s to set: %s', target_grubconf, newcontent)
135
136
137-def write_grub_config(target, grubcfg, grub_conf, new_params):
138- replace_default = config.value_as_boolean(
139- grubcfg.get('replace_linux_default', True))
140+def write_grub_config(
141+ target: str,
142+ grubcfg: config.GrubConfig,
143+ grub_conf: str,
144+ new_params: str,
145+ ) -> None:
146+ replace_default = grubcfg.replace_linux_default
147 if replace_default:
148 replace_grub_cmdline_linux_default(target, new_params)
149
150- probe_os = config.value_as_boolean(
151- grubcfg.get('probe_additional_os', False))
152+ probe_os = grubcfg.probe_additional_os
153 if not probe_os:
154 probe_content = [
155 ('# Curtin disable grub os prober that might find other '
156@@ -224,10 +228,7 @@ def write_grub_config(target, grubcfg, grub_conf, new_params):
157 "\n".join(probe_content), omode='a+')
158
159 # if terminal is present in config, but unset, then don't
160- grub_terminal = grubcfg.get('terminal', 'console')
161- if not isinstance(grub_terminal, str):
162- raise ValueError("Unexpected value %s for 'terminal'. "
163- "Value must be a string" % grub_terminal)
164+ grub_terminal = grubcfg.terminal
165 if not grub_terminal.lower() == "unmodified":
166 terminal_content = [
167 '# Curtin configured GRUB_TERMINAL value',
168@@ -394,7 +395,13 @@ def check_target_arch_machine(target, arch=None, machine=None, uefi=None):
169 raise RuntimeError(errmsg)
170
171
172-def install_grub(devices, target, uefi=None, grubcfg=None):
173+def install_grub(
174+ devices: List[str],
175+ target: str,
176+ *,
177+ grubcfg: config.GrubConfig,
178+ uefi: Optional[bool] = None,
179+ ):
180 """Install grub to devices inside target chroot.
181
182 :param: devices: List of block device paths to install grub upon.
183@@ -411,8 +418,8 @@ def install_grub(devices, target, uefi=None, grubcfg=None):
184 raise ValueError("Invalid parameter 'target': %s" % target)
185
186 LOG.debug("installing grub to target=%s devices=%s [replace_defaults=%s]",
187- target, devices, grubcfg.get('replace_default'))
188- update_nvram = config.value_as_boolean(grubcfg.get('update_nvram', True))
189+ target, devices, grubcfg.replace_linux_default)
190+ update_nvram = grubcfg.update_nvram
191 distroinfo = distro.get_distroinfo(target=target)
192 target_arch = distro.get_architecture(target=target)
193 rhel_ver = (distro.rpm_get_dist_id(target)
194@@ -460,7 +467,7 @@ def install_grub_main(args):
195 cfg = config.load_command_config(args, state)
196 stack_prefix = state.get('report_stack_prefix', '')
197 uefi = util.is_uefi_bootable()
198- grubcfg = cfg.get('grub')
199+ grubcfg = config.fromdict(config.GrubConfig, cfg.get('grub'))
200 with events.ReportEventStack(
201 name=stack_prefix, reporting_enabled=True, level="INFO",
202 description="Installing grub to target devices"):
203diff --git a/curtin/config.py b/curtin/config.py
204index b65410b..b30ecc5 100644
205--- a/curtin/config.py
206+++ b/curtin/config.py
207@@ -1,6 +1,7 @@
208 # This file is part of curtin. See LICENSE file for copyright and license info.
209
210 import json
211+import typing
212
213 import attr
214 import yaml
215@@ -129,23 +130,143 @@ def value_as_boolean(value):
216 return value not in false_values
217
218
219+def _convert_install_devices(value):
220+ if isinstance(value, str):
221+ return [value]
222+ return value
223+
224+
225 @attr.s(auto_attribs=True)
226 class GrubConfig:
227- # This is not yet every option that appears under the "grub" config key,
228- # but it is a work in progress.
229+ install_devices_default = object()
230+ install_devices: typing.Optional[typing.List[str]] = attr.ib(
231+ converter=_convert_install_devices, default=install_devices_default)
232+ probe_additional_os: bool = attr.ib(
233+ default=False, converter=value_as_boolean)
234+ remove_duplicate_entries: bool = True
235 remove_old_uefi_loaders: bool = True
236 reorder_uefi: bool = True
237 reorder_uefi_force_fallback: bool = attr.ib(
238 default=False, converter=value_as_boolean)
239- remove_duplicate_entries: bool = True
240+ replace_linux_default: bool = attr.ib(
241+ default=True, converter=value_as_boolean)
242+ terminal: str = "console"
243+ update_nvram: bool = attr.ib(default=True, converter=value_as_boolean)
244
245
246-def fromdict(cls, d):
247- kw = {}
248- for field in attr.fields(cls):
249- if field.name in d:
250- kw[field.name] = d[field.name]
251- return cls(**kw)
252+class SerializationError(Exception):
253+ def __init__(self, obj, path, message):
254+ self.obj = obj
255+ self.path = path
256+ self.message = message
257+
258+ def __str__(self):
259+ p = self.path
260+ if not p:
261+ p = 'top-level'
262+ return f"processing {self.obj}: at {p}, {self.message}"
263+
264+
265+@attr.s(auto_attribs=True)
266+class SerializationContext:
267+ obj: typing.Any
268+ cur: typing.Any
269+ path: str
270+ metadata: typing.Optional[typing.Dict]
271+
272+ @classmethod
273+ def new(cls, obj):
274+ return SerializationContext(obj, obj, '', {})
275+
276+ def child(self, path, cur, metadata=None):
277+ if metadata is None:
278+ metadata = self.metadata
279+ return attr.evolve(
280+ self, path=self.path + path, cur=cur, metadata=metadata)
281+
282+ def error(self, message):
283+ raise SerializationError(self.obj, self.path, message)
284+
285+ def assert_type(self, typ):
286+ if type(self.cur) is not typ:
287+ self.error("{!r} is not a {}".format(self.cur, typ))
288+
289+
290+class Deserializer:
291+
292+ def __init__(self):
293+ self.typing_walkers = {
294+ list: self._walk_List,
295+ typing.List: self._walk_List,
296+ typing.Union: self._walk_Union,
297+ }
298+ self.type_deserializers = {}
299+ for typ in int, str, bool, list, dict, type(None):
300+ self.type_deserializers[typ] = self._scalar
301+
302+ def _scalar(self, annotation, context):
303+ context.assert_type(annotation)
304+ return context.cur
305+
306+ def _walk_List(self, meth, args, context):
307+ return [
308+ meth(args[0], context.child(f'[{i}]', v))
309+ for i, v in enumerate(context.cur)
310+ ]
311+
312+ def _walk_Union(self, meth, args, context):
313+ NoneType = type(None)
314+ if NoneType in args:
315+ args = [a for a in args if a is not NoneType]
316+ if len(args) == 1:
317+ # I.e. Optional[thing]
318+ if context.cur is None:
319+ return context.cur
320+ return meth(args[0], context)
321+ context.error(f"cannot serialize Union[{args}]")
322+
323+ def _deserialize_attr(self, annotation, context):
324+ context.assert_type(dict)
325+ args = {}
326+ fields = {
327+ field.name: field for field in attr.fields(annotation)
328+ }
329+ for key, value in context.cur.items():
330+ if key not in fields:
331+ continue
332+ field = fields[key]
333+ if field.converter:
334+ value = field.converter(value)
335+ args[field.name] = self._deserialize(
336+ field.type,
337+ context.child(f'[{key!r}]', value, field.metadata))
338+ return annotation(**args)
339+
340+ def _deserialize(self, annotation, context):
341+ if annotation is None:
342+ context.assert_type(type(None))
343+ return None
344+ if annotation is typing.Any:
345+ return context.cur
346+ if attr.has(annotation):
347+ return self._deserialize_attr(annotation, context)
348+ origin = getattr(annotation, '__origin__', None)
349+ if origin is not None:
350+ return self.typing_walkers[origin](
351+ self._deserialize, annotation.__args__, context)
352+ return self.type_deserializers[annotation](annotation, context)
353+
354+ def deserialize(self, annotation, value):
355+ context = SerializationContext.new(value)
356+ return self._deserialize(annotation, context)
357+
358+
359+T = typing.TypeVar("T")
360+
361+
362+def fromdict(cls: typing.Type[T], d) -> T:
363+ deserializer = Deserializer()
364+ return deserializer.deserialize(cls, d)
365
366
367 # vi: ts=4 expandtab syntax=python
368diff --git a/tests/unittests/test_commands_install_grub.py b/tests/unittests/test_commands_install_grub.py
369index 00004d7..fc19da3 100644
370--- a/tests/unittests/test_commands_install_grub.py
371+++ b/tests/unittests/test_commands_install_grub.py
372@@ -1,5 +1,6 @@
373 # This file is part of curtin. See LICENSE file for copyright and license info.
374
375+from curtin import config
376 from curtin import distro
377 from curtin import util
378 from curtin import paths
379@@ -456,7 +457,7 @@ class TestWriteGrubConfig(CiTestCase):
380 self.assertEqual(expected, found)
381
382 def test_write_grub_config_defaults(self):
383- grubcfg = {}
384+ grubcfg = config.GrubConfig()
385 new_params = ['foo=bar', 'wark=1']
386 expected_default = "\n".join([
387 'GRUB_CMDLINE_LINUX_DEFAULT="foo=bar wark=1"', ''])
388@@ -473,7 +474,7 @@ class TestWriteGrubConfig(CiTestCase):
389 self._verify_expected(expected_default, expected_curtin)
390
391 def test_write_grub_config_no_replace(self):
392- grubcfg = {'replace_linux_default': False}
393+ grubcfg = config.GrubConfig(replace_linux_default=False)
394 new_params = ['foo=bar', 'wark=1']
395 expected_default = "\n".join([])
396 expected_curtin = "\n".join([
397@@ -489,7 +490,7 @@ class TestWriteGrubConfig(CiTestCase):
398 self._verify_expected(expected_default, expected_curtin)
399
400 def test_write_grub_config_disable_probe(self):
401- grubcfg = {'probe_additional_os': False} # DISABLE_OS_PROBER=1
402+ grubcfg = config.GrubConfig(probe_additional_os=False)
403 new_params = ['foo=bar', 'wark=1']
404 expected_default = "\n".join([
405 'GRUB_CMDLINE_LINUX_DEFAULT="foo=bar wark=1"', ''])
406@@ -506,7 +507,7 @@ class TestWriteGrubConfig(CiTestCase):
407 self._verify_expected(expected_default, expected_curtin)
408
409 def test_write_grub_config_enable_probe(self):
410- grubcfg = {'probe_additional_os': True} # DISABLE_OS_PROBER=0, default
411+ grubcfg = config.GrubConfig(probe_additional_os=True)
412 new_params = ['foo=bar', 'wark=1']
413 expected_default = "\n".join([
414 'GRUB_CMDLINE_LINUX_DEFAULT="foo=bar wark=1"', ''])
415@@ -520,10 +521,9 @@ class TestWriteGrubConfig(CiTestCase):
416 self._verify_expected(expected_default, expected_curtin)
417
418 def test_write_grub_config_no_grub_settings_file(self):
419- grubcfg = {
420- 'probe_additional_os': True,
421- 'terminal': 'unmodified',
422- }
423+ grubcfg = config.GrubConfig(
424+ probe_additional_os=True,
425+ terminal='unmodified')
426 new_params = []
427 install_grub.write_grub_config(
428 self.target, grubcfg, self.grubconf, new_params)
429@@ -531,7 +531,8 @@ class TestWriteGrubConfig(CiTestCase):
430 self.assertFalse(os.path.exists(self.target_grubconf))
431
432 def test_write_grub_config_specify_terminal(self):
433- grubcfg = {'terminal': 'serial'}
434+ grubcfg = config.GrubConfig(
435+ terminal='serial')
436 new_params = ['foo=bar', 'wark=1']
437 expected_default = "\n".join([
438 'GRUB_CMDLINE_LINUX_DEFAULT="foo=bar wark=1"', ''])
439@@ -548,7 +549,7 @@ class TestWriteGrubConfig(CiTestCase):
440 self._verify_expected(expected_default, expected_curtin)
441
442 def test_write_grub_config_terminal_unmodified(self):
443- grubcfg = {'terminal': 'unmodified'}
444+ grubcfg = config.GrubConfig(terminal='unmodified')
445 new_params = ['foo=bar', 'wark=1']
446 expected_default = "\n".join([
447 'GRUB_CMDLINE_LINUX_DEFAULT="foo=bar wark=1"', ''])
448@@ -562,13 +563,6 @@ class TestWriteGrubConfig(CiTestCase):
449
450 self._verify_expected(expected_default, expected_curtin)
451
452- def test_write_grub_config_invalid_terminal(self):
453- grubcfg = {'terminal': ['color-tv']}
454- new_params = ['foo=bar', 'wark=1']
455- with self.assertRaises(ValueError):
456- install_grub.write_grub_config(
457- self.target, grubcfg, self.grubconf, new_params)
458-
459
460 class TestFindEfiLoader(CiTestCase):
461
462@@ -1119,39 +1113,43 @@ class TestInstallGrub(CiTestCase):
463 def test_grub_install_raise_exception_on_no_devices(self):
464 devices = []
465 with self.assertRaises(ValueError):
466- install_grub.install_grub(devices, self.target, False, {})
467+ install_grub.install_grub(
468+ devices, self.target, uefi=False, grubcfg=config.GrubConfig())
469
470 def test_grub_install_raise_exception_on_no_target(self):
471 devices = ['foobar']
472 with self.assertRaises(ValueError):
473- install_grub.install_grub(devices, None, False, {})
474+ install_grub.install_grub(
475+ devices, None, uefi=False, grubcfg=config.GrubConfig())
476
477 def test_grub_install_raise_exception_on_s390x(self):
478 self.m_distro_get_architecture.return_value = 's390x'
479 self.m_platform_machine.return_value = 's390x'
480 devices = ['foobar']
481 with self.assertRaises(RuntimeError):
482- install_grub.install_grub(devices, self.target, False, {})
483+ install_grub.install_grub(
484+ devices, self.target, uefi=False, grubcfg=config.GrubConfig())
485
486 def test_grub_install_raise_exception_on_armv7(self):
487 self.m_distro_get_architecture.return_value = 'armhf'
488 self.m_platform_machine.return_value = 'armv7l'
489 devices = ['foobar']
490 with self.assertRaises(RuntimeError):
491- install_grub.install_grub(devices, self.target, False, {})
492+ install_grub.install_grub(
493+ devices, self.target, uefi=False, grubcfg=config.GrubConfig())
494
495 def test_grub_install_raise_exception_on_arm64_no_uefi(self):
496 self.m_distro_get_architecture.return_value = 'arm64'
497 self.m_platform_machine.return_value = 'aarch64'
498- uefi = False
499 devices = ['foobar']
500 with self.assertRaises(RuntimeError):
501- install_grub.install_grub(devices, self.target, uefi, {})
502+ install_grub.install_grub(
503+ devices, self.target, uefi=False, grubcfg=config.GrubConfig())
504
505 def test_grub_install_ubuntu(self):
506 devices = ['/dev/disk-a-part1']
507 uefi = False
508- grubcfg = {}
509+ grubcfg = config.GrubConfig()
510 grub_conf = self.tmp_path('grubconf')
511 new_params = []
512 self.m_get_grub_package_name.return_value = ('grub-pc', 'i386-pc')
513@@ -1161,7 +1159,8 @@ class TestInstallGrub(CiTestCase):
514 self.m_gen_install_commands.return_value = (
515 [['/bin/true']], [['/bin/false']])
516
517- install_grub.install_grub(devices, self.target, uefi, grubcfg)
518+ install_grub.install_grub(
519+ devices, self.target, uefi=uefi, grubcfg=grubcfg)
520
521 self.m_distro_get_distroinfo.assert_called_with(target=self.target)
522 self.m_distro_get_architecture.assert_called_with(target=self.target)
523@@ -1189,8 +1188,7 @@ class TestInstallGrub(CiTestCase):
524 def test_uefi_grub_install_ubuntu(self):
525 devices = ['/dev/disk-a-part1']
526 uefi = True
527- update_nvram = True
528- grubcfg = {'update_nvram': update_nvram}
529+ grubcfg = config.GrubConfig(update_nvram=True)
530 grub_conf = self.tmp_path('grubconf')
531 new_params = []
532 grub_name = 'grub-efi-amd64'
533@@ -1203,7 +1201,8 @@ class TestInstallGrub(CiTestCase):
534 self.m_gen_uefi_install_commands.return_value = (
535 [['/bin/true']], [['/bin/false']])
536
537- install_grub.install_grub(devices, self.target, uefi, grubcfg)
538+ install_grub.install_grub(
539+ devices, self.target, uefi=uefi, grubcfg=grubcfg)
540
541 self.m_distro_get_distroinfo.assert_called_with(target=self.target)
542 self.m_distro_get_architecture.assert_called_with(target=self.target)
543@@ -1219,8 +1218,8 @@ class TestInstallGrub(CiTestCase):
544 self.m_get_grub_install_command.assert_called_with(
545 uefi, self.distroinfo, self.target)
546 self.m_gen_uefi_install_commands.assert_called_with(
547- grub_name, grub_target, grub_cmd, update_nvram, self.distroinfo,
548- devices, self.target)
549+ grub_name, grub_target, grub_cmd, grubcfg.update_nvram,
550+ self.distroinfo, devices, self.target)
551
552 self.m_subp.assert_has_calls([
553 mock.call(['/bin/true'], env=self.env, capture=True,
554@@ -1232,8 +1231,7 @@ class TestInstallGrub(CiTestCase):
555 def test_uefi_grub_install_ubuntu_multiple_esp(self):
556 devices = ['/dev/disk-a-part1']
557 uefi = True
558- update_nvram = True
559- grubcfg = {'update_nvram': update_nvram}
560+ grubcfg = config.GrubConfig(update_nvram=True)
561 grub_conf = self.tmp_path('grubconf')
562 new_params = []
563 grub_name = 'grub-efi-amd64'
564@@ -1246,7 +1244,8 @@ class TestInstallGrub(CiTestCase):
565 self.m_gen_uefi_install_commands.return_value = (
566 [['/bin/true']], [['/bin/false']])
567
568- install_grub.install_grub(devices, self.target, uefi, grubcfg)
569+ install_grub.install_grub(
570+ devices, self.target, uefi=uefi, grubcfg=grubcfg)
571
572 self.m_distro_get_distroinfo.assert_called_with(target=self.target)
573 self.m_distro_get_architecture.assert_called_with(target=self.target)
574@@ -1262,8 +1261,8 @@ class TestInstallGrub(CiTestCase):
575 self.m_get_grub_install_command.assert_called_with(
576 uefi, self.distroinfo, self.target)
577 self.m_gen_uefi_install_commands.assert_called_with(
578- grub_name, grub_target, grub_cmd, update_nvram, self.distroinfo,
579- devices, self.target)
580+ grub_name, grub_target, grub_cmd, grubcfg.update_nvram,
581+ self.distroinfo, devices, self.target)
582
583 self.m_subp.assert_has_calls([
584 mock.call(['/bin/true'], env=self.env, capture=True,
585diff --git a/tests/unittests/test_config.py b/tests/unittests/test_config.py
586index af7f251..ae51744 100644
587--- a/tests/unittests/test_config.py
588+++ b/tests/unittests/test_config.py
589@@ -3,6 +3,9 @@
590 import copy
591 import json
592 import textwrap
593+import typing
594+
595+import attr
596
597 from curtin import config
598 from .helpers import CiTestCase
599@@ -139,4 +142,58 @@ def _replace_consts(cfgstr):
600 cfgstr = cfgstr.replace(k, v)
601 return cfgstr
602
603+
604+class TestDeserializer(CiTestCase):
605+
606+ def test_scalar(self):
607+ deserializer = config.Deserializer()
608+ self.assertEqual(1, deserializer.deserialize(int, 1))
609+ self.assertEqual("a", deserializer.deserialize(str, "a"))
610+
611+ def test_attr(self):
612+ deserializer = config.Deserializer()
613+
614+ @attr.s(auto_attribs=True)
615+ class Point:
616+ x: int
617+ y: int
618+
619+ self.assertEqual(
620+ Point(x=1, y=2),
621+ deserializer.deserialize(Point, {'x': 1, 'y': 2}))
622+
623+ def test_list(self):
624+ deserializer = config.Deserializer()
625+ self.assertEqual(
626+ [1, 2, 3],
627+ deserializer.deserialize(typing.List[int], [1, 2, 3]))
628+
629+ def test_optional(self):
630+ deserializer = config.Deserializer()
631+ self.assertEqual(
632+ 1,
633+ deserializer.deserialize(typing.Optional[int], 1))
634+ self.assertEqual(
635+ None,
636+ deserializer.deserialize(typing.Optional[int], None))
637+
638+ def test_converter(self):
639+ deserializer = config.Deserializer()
640+
641+ @attr.s(auto_attribs=True)
642+ class WithoutConverter:
643+ val: bool
644+
645+ with self.assertRaises(config.SerializationError):
646+ deserializer.deserialize(WithoutConverter, {"val": "on"})
647+
648+ @attr.s(auto_attribs=True)
649+ class WithConverter:
650+ val: bool = attr.ib(converter=config.value_as_boolean)
651+
652+ self.assertEqual(
653+ WithConverter(val=True),
654+ deserializer.deserialize(WithConverter, {"val": "on"}))
655+
656+
657 # vi: ts=4 expandtab syntax=python
658diff --git a/tests/unittests/test_curthooks.py b/tests/unittests/test_curthooks.py
659index 6067ea4..d6b5445 100644
660--- a/tests/unittests/test_curthooks.py
661+++ b/tests/unittests/test_curthooks.py
662@@ -636,13 +636,11 @@ class TestSetupGrub(CiTestCase):
663 cfg = {
664 'grub_install_devices': ['/dev/vdb']
665 }
666- updated_cfg = {
667- 'install_devices': ['/dev/vdb']
668- }
669 curthooks.setup_grub(cfg, self.target, osfamily=self.distro_family,
670 variant=self.variant)
671 self.m_install_grub.assert_called_with(
672- ['/dev/vdb'], self.target, uefi=False, grubcfg=updated_cfg)
673+ ['/dev/vdb'], self.target, uefi=False,
674+ grubcfg=config.GrubConfig(install_devices=['/dev/vdb']))
675
676 def test_uses_install_devices_in_grubcfg(self):
677 cfg = {
678@@ -654,7 +652,8 @@ class TestSetupGrub(CiTestCase):
679 cfg, self.target,
680 osfamily=self.distro_family, variant=self.variant)
681 self.m_install_grub.assert_called_with(
682- ['/dev/vdb'], self.target, uefi=False, grubcfg=cfg.get('grub'))
683+ ['/dev/vdb'], self.target, uefi=False,
684+ grubcfg=config.fromdict(config.GrubConfig, cfg.get('grub')))
685
686 @patch('curtin.commands.block_meta.multipath')
687 @patch('curtin.commands.curthooks.os.path.exists')
688@@ -678,7 +677,7 @@ class TestSetupGrub(CiTestCase):
689 variant=self.variant)
690 self.m_install_grub.assert_called_with(
691 ['/dev/vdb'], self.target, uefi=False,
692- grubcfg={'install_devices': ['/dev/vdb']})
693+ grubcfg=config.GrubConfig(install_devices=['/dev/vdb']))
694
695 @patch('curtin.commands.block_meta.multipath')
696 @patch('curtin.block.is_valid_device')
697@@ -729,8 +728,9 @@ class TestSetupGrub(CiTestCase):
698 variant='centos')
699 self.m_install_grub.assert_called_with(
700 ['/dev/vdb1'], self.target, uefi=True,
701- grubcfg={'update_nvram': False, 'install_devices': ['/dev/vdb1']}
702- )
703+ grubcfg=config.GrubConfig(
704+ update_nvram=False,
705+ install_devices=['/dev/vdb1']))
706
707 def test_grub_install_installs_to_none_if_install_devices_None(self):
708 cfg = {
709@@ -742,7 +742,7 @@ class TestSetupGrub(CiTestCase):
710 variant=self.variant)
711 self.m_install_grub.assert_called_with(
712 ['none'], self.target, uefi=False,
713- grubcfg={'install_devices': None}
714+ grubcfg=config.GrubConfig(install_devices=None),
715 )
716
717 @patch.object(util.ChrootableTarget, "__enter__", new=lambda a: a)
718@@ -772,7 +772,8 @@ class TestSetupGrub(CiTestCase):
719 curthooks.setup_grub(cfg, self.target, osfamily=self.distro_family,
720 variant=self.variant)
721 self.m_install_grub.assert_called_with(
722- ['/dev/vdb'], self.target, uefi=True, grubcfg=cfg.get('grub')
723+ ['/dev/vdb'], self.target, uefi=True,
724+ grubcfg=config.fromdict(config.GrubConfig, cfg.get('grub'))
725 )
726
727 @patch.object(util.ChrootableTarget, "__enter__", new=lambda a: a)
728@@ -1101,7 +1102,7 @@ class TestUefiRemoveDuplicateEntries(CiTestCase):
729
730 @patch.object(util.ChrootableTarget, "__enter__", new=lambda a: a)
731 def test_uefi_remove_duplicate_entries(self):
732- grubcfg = {}
733+ grubcfg = config.GrubConfig()
734 curthooks.uefi_remove_duplicate_entries(grubcfg, self.target)
735 self.assertEqual([
736 call(['efibootmgr', '--bootnum=0001', '--delete-bootnum'],
737@@ -1112,7 +1113,7 @@ class TestUefiRemoveDuplicateEntries(CiTestCase):
738
739 @patch.object(util.ChrootableTarget, "__enter__", new=lambda a: a)
740 def test_uefi_remove_duplicate_entries_no_bootcurrent(self):
741- grubcfg = {}
742+ grubcfg = config.GrubConfig()
743 efiout = copy_efi_state(self.efibootmgr_output)
744 efiout.current = ''
745 self.m_efibootmgr.return_value = efiout
746@@ -1126,15 +1127,15 @@ class TestUefiRemoveDuplicateEntries(CiTestCase):
747
748 @patch.object(util.ChrootableTarget, "__enter__", new=lambda a: a)
749 def test_uefi_remove_duplicate_entries_disabled(self):
750- grubcfg = {
751- 'remove_duplicate_entries': False,
752- }
753+ grubcfg = config.GrubConfig(
754+ remove_duplicate_entries=False,
755+ )
756 curthooks.uefi_remove_duplicate_entries(grubcfg, self.target)
757 self.assertEquals([], self.m_subp.call_args_list)
758
759 @patch.object(util.ChrootableTarget, "__enter__", new=lambda a: a)
760 def test_uefi_remove_duplicate_entries_skip_bootcurrent(self):
761- grubcfg = {}
762+ grubcfg = config.GrubConfig()
763 efiout = copy_efi_state(self.efibootmgr_output)
764 efiout.current = '0003'
765 self.m_efibootmgr.return_value = efiout
766@@ -1148,7 +1149,7 @@ class TestUefiRemoveDuplicateEntries(CiTestCase):
767
768 @patch.object(util.ChrootableTarget, "__enter__", new=lambda a: a)
769 def test_uefi_remove_duplicate_entries_no_change(self):
770- grubcfg = {}
771+ grubcfg = config.GrubConfig()
772 self.m_efibootmgr.return_value = util.EFIBootState(
773 order=[],
774 timeout='',

Subscribers

People subscribed via source and target branches