Merge ~mwhudson/curtin:more-grub-config-object into curtin:master
- Git
- lp:~mwhudson/curtin
- more-grub-config-object
- Merge into master
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) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Server Team CI bot | continuous-integration | Approve | |
Dan Bungert | Approve | ||
Review via email:
|
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...
Description of the change
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Server Team CI bot (server-team-bot) wrote : | # |
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:386058f38cd
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
Click here to trigger a rebuild:
https:/
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Michael Hudson-Doyle (mwhudson) : | # |
- 09f5220... by Michael Hudson-Doyle
-
be more careful to preserve semantics
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:09f5220f29e
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
Click here to trigger a rebuild:
https:/
Preview Diff
1 | diff --git a/curtin/commands/curthooks.py b/curtin/commands/curthooks.py |
2 | index 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) |
112 | diff --git a/curtin/commands/install_grub.py b/curtin/commands/install_grub.py |
113 | index 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"): |
203 | diff --git a/curtin/config.py b/curtin/config.py |
204 | index 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 |
368 | diff --git a/tests/unittests/test_commands_install_grub.py b/tests/unittests/test_commands_install_grub.py |
369 | index 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, |
585 | diff --git a/tests/unittests/test_config.py b/tests/unittests/test_config.py |
586 | index 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 |
658 | diff --git a/tests/unittests/test_curthooks.py b/tests/unittests/test_curthooks.py |
659 | index 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='', |
PASSED: Continuous integration, rev:214202fba0a 75c35ef95c5fd16 3daddf3ba2845a /jenkins. canonical. com/server- team/job/ curtin- ci/148/ /jenkins. canonical. com/server- team/job/ curtin- ci/nodes= metal-amd64/ 148/ /jenkins. canonical. com/server- team/job/ curtin- ci/nodes= metal-arm64/ 148/ /jenkins. canonical. com/server- team/job/ curtin- ci/nodes= metal-ppc64el/ 148/ /jenkins. canonical. com/server- team/job/ curtin- ci/nodes= metal-s390x/ 148/
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
Click here to trigger a rebuild: /jenkins. canonical. com/server- team/job/ curtin- ci/148/ /rebuild
https:/