diff options
author | Chad Smith <chad.smith@canonical.com> | 2022-10-23 22:35:50 -0600 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-10-23 23:35:50 -0500 |
commit | 96a803ac2aa1829ec70b04132ed2a4c3117f96d2 (patch) | |
tree | 31b38f179135b53ba98b9cfe6405595eeeb4b5b0 | |
parent | e77c0bf84c867a05ce7947fc15548a2c8b261a24 (diff) | |
download | cloud-init-git-96a803ac2aa1829ec70b04132ed2a4c3117f96d2.tar.gz |
lxd: add support for lxd preseed config(#1789)
User-data now supports opaque lxd.preseed YAML
which can be provided directly to the command:
lxd init --preseed
Complex LXD host, network, profile and storage setup
and configuration can be provided via this config entry.
For details on available configuration options and examples:
https://linuxcontainers.org/lxd/docs/master/preseed/
- or -
lxd init --dump
-rw-r--r-- | cloudinit/config/cc_lxd.py | 183 | ||||
-rw-r--r-- | cloudinit/config/schemas/schema-cloud-config-v1.json | 6 | ||||
-rw-r--r-- | tests/integration_tests/modules/test_lxd.py | 203 | ||||
-rw-r--r-- | tests/unittests/config/test_cc_lxd.py | 168 |
4 files changed, 497 insertions, 63 deletions
diff --git a/cloudinit/config/cc_lxd.py b/cloudinit/config/cc_lxd.py index b58d719d..e692fbd5 100644 --- a/cloudinit/config/cc_lxd.py +++ b/cloudinit/config/cc_lxd.py @@ -12,7 +12,7 @@ from textwrap import dedent from typing import List, Tuple from cloudinit import log as logging -from cloudinit import subp, util +from cloudinit import safeyaml, subp, util from cloudinit.cloud import Cloud from cloudinit.config import Config from cloudinit.config.schema import MetaSchema, get_meta_doc @@ -50,6 +50,7 @@ meta: MetaSchema = { ), dedent( """\ + # LXD init showcasing cloud-init's LXD config options lxd: init: network_address: 0.0.0.0 @@ -73,6 +74,84 @@ meta: MetaSchema = { domain: lxd """ ), + dedent( + """\ + # For more complex non-iteractive LXD configuration of networks, + # storage_pools, profiles, projects, clusters and core config, + # `lxd:preseed` config will be passed as stdin to the command: + # lxd init --preseed + # See https://linuxcontainers.org/lxd/docs/master/preseed/ or + # run: lxd init --dump to see viable preseed YAML allowed. + # + # Preseed settings configuring the LXD daemon for HTTPS connections + # on 192.168.1.1 port 9999, a nested profile which allows for + # LXD nesting on containers and a limited project allowing for + # RBAC approach when defining behavior for sub projects. + lxd: + preseed: | + config: + core.https_address: 192.168.1.1:9999 + networks: + - config: + ipv4.address: 10.42.42.1/24 + ipv4.nat: true + ipv6.address: fd42:4242:4242:4242::1/64 + ipv6.nat: true + description: "" + name: lxdbr0 + type: bridge + project: default + storage_pools: + - config: + size: 5GiB + source: /var/snap/lxd/common/lxd/disks/default.img + description: "" + name: default + driver: zfs + profiles: + - config: {} + description: Default LXD profile + devices: + eth0: + name: eth0 + network: lxdbr0 + type: nic + root: + path: / + pool: default + type: disk + name: default + - config: {} + security.nesting: true + devices: + eth0: + name: eth0 + network: lxdbr0 + type: nic + root: + path: / + pool: default + type: disk + name: nested + projects: + - config: + features.images: true + features.networks: true + features.profiles: true + features.storage.volumes: true + description: Default LXD project + name: default + - config: + features.images: false + features.networks: true + features.profiles: false + features.storage.volumes: false + description: Limited Access LXD project + name: limited + + + """ + ), ], "frequency": PER_INSTANCE, "activate_by_schema_keys": ["lxd"], @@ -81,6 +160,43 @@ meta: MetaSchema = { __doc__ = get_meta_doc(meta) +def supplemental_schema_validation( + init_cfg: dict, bridge_cfg: dict, preseed_str: str +): + """Validate user-provided lxd network and bridge config option values. + + @raises: ValueError describing invalid values provided. + """ + errors = [] + if not isinstance(init_cfg, dict): + errors.append( + f"lxd.init config must be a dictionary. found a" + f" '{type(init_cfg).__name__}'", + ) + + if not isinstance(bridge_cfg, dict): + errors.append( + f"lxd.bridge config must be a dictionary. found a" + f" '{type(bridge_cfg).__name__}'", + ) + + if not isinstance(preseed_str, str): + errors.append( + f"lxd.preseed config must be a string. found a" + f" '{type(preseed_str).__name__}'", + ) + if preseed_str and (init_cfg or bridge_cfg): + incompat_cfg = ["lxd.init"] if init_cfg else [] + incompat_cfg += ["lxd.bridge"] if bridge_cfg else [] + + errors.append( + "Unable to configure LXD. lxd.preseed config can not be provided" + f" with key(s): {', '.join(incompat_cfg)}" + ) + if errors: + raise ValueError(". ".join(errors)) + + def handle( name: str, cfg: Config, cloud: Cloud, log: Logger, args: list ) -> None: @@ -92,28 +208,18 @@ def handle( ) return if not isinstance(lxd_cfg, dict): - log.warning( - "lxd config must be a dictionary. found a '%s'", type(lxd_cfg) + raise ValueError( + f"lxd config must be a dictionary. found a" + f" '{type(lxd_cfg).__name__}'" ) - return # Grab the configuration - init_cfg = lxd_cfg.get("init") - if not isinstance(init_cfg, dict): - log.warning( - "lxd/init config must be a dictionary. found a '%s'", - type(init_cfg), - ) - init_cfg = {} - + init_cfg = lxd_cfg.get("init", {}) + preseed_str = lxd_cfg.get("preseed", "") bridge_cfg = lxd_cfg.get("bridge", {}) - if not isinstance(bridge_cfg, dict): - log.warning( - "lxd/bridge config must be a dictionary. found a '%s'", - type(bridge_cfg), - ) - bridge_cfg = {} - packages = get_required_packages(init_cfg) + supplemental_schema_validation(init_cfg, bridge_cfg, preseed_str) + + packages = get_required_packages(init_cfg, preseed_str) if len(packages): try: cloud.distro.install_packages(packages) @@ -121,6 +227,10 @@ def handle( log.warning("failed to install packages %s: %s", packages, exc) return + subp.subp(["lxd", "waitready", "--timeout=300"]) + if preseed_str: + subp.subp(["lxd", "init", "--preseed"], data=preseed_str) + return # Set up lxd if init config is given if init_cfg: @@ -136,8 +246,6 @@ def handle( "trust_password", ) - subp.subp(["lxd", "waitready", "--timeout=300"]) - # Bug https://bugs.launchpad.net/ubuntu/+source/linux-kvm/+bug/1982780 kernel = util.system_info()["uname"][2] if init_cfg["storage_backend"] == "lvm" and not os.path.exists( @@ -388,7 +496,7 @@ def maybe_cleanup_default( LOG.debug(msg, nic_name, profile, fail_assume_enoent) -def get_required_packages(cfg: dict) -> List[str]: +def get_required_packages(init_cfg: dict, preseed_str: str) -> List[str]: """identify required packages for install""" packages = [] if not subp.which("lxd"): @@ -396,12 +504,27 @@ def get_required_packages(cfg: dict) -> List[str]: # binary for pool creation must be available for the requested backend: # zfs, lvcreate, mkfs.btrfs - storage: str = cfg.get("storage_backend", "") - if storage: - if storage == "zfs" and not subp.which("zfs"): - packages.append("zfsutils-linux") - if storage == "lvm" and not subp.which("lvcreate"): - packages.append("lvm2") - if storage == "btrfs" and not subp.which("mkfs.btrfs"): - packages.append("btrfs-progs") + storage_drivers: List[str] = [] + preseed_cfg: dict = {} + if "storage_backend" in init_cfg: + storage_drivers.append(init_cfg["storage_backend"]) + if preseed_str and "storage_pools" in preseed_str: + # Assume correct YAML preseed format + try: + preseed_cfg = safeyaml.load(preseed_str) + except (safeyaml.YAMLError, TypeError, ValueError): + LOG.warning( + "lxd.preseed string value is not YAML. " + " Unable to determine required storage driver packages to" + " support storage_pools config." + ) + for storage_pool in preseed_cfg.get("storage_pools", []): + if storage_pool.get("driver"): + storage_drivers.append(storage_pool["driver"]) + if "zfs" in storage_drivers and not subp.which("zfs"): + packages.append("zfsutils-linux") + if "lvm" in storage_drivers and not subp.which("lvcreate"): + packages.append("lvm2") + if "btrfs" in storage_drivers and not subp.which("mkfs.btrfs"): + packages.append("btrfs-progs") return packages diff --git a/cloudinit/config/schemas/schema-cloud-config-v1.json b/cloudinit/config/schemas/schema-cloud-config-v1.json index 737decbb..48656938 100644 --- a/cloudinit/config/schemas/schema-cloud-config-v1.json +++ b/cloudinit/config/schemas/schema-cloud-config-v1.json @@ -1259,6 +1259,7 @@ "init": { "type": "object", "additionalProperties": false, + "description": "LXD init configuration values to provide to `lxd init --auto` command. Can not be combined with ``lxd.preseed``.", "properties": { "network_address": { "type": "string", @@ -1296,6 +1297,7 @@ "type": "object", "required": ["mode"], "additionalProperties": false, + "description": "LXD bridge configuration provided to setup the host lxd bridge. Can not be combined with ``lxd.preseed``.", "properties": { "mode": { "type": "string", @@ -1356,6 +1358,10 @@ "description": "Domain to advertise to DHCP clients and use for DNS resolution." } } + }, + "preseed": { + "type": "string", + "description": "Opaque LXD preseed YAML config passed via stdin to the command: lxd init --preseed. See: https://linuxcontainers.org/lxd/docs/master/preseed/ or lxd init --dump for viable config. Can not be combined with either ``lxd.init`` or ``lxd.bridge``." } } } diff --git a/tests/integration_tests/modules/test_lxd.py b/tests/integration_tests/modules/test_lxd.py index 3443b74a..777c123d 100644 --- a/tests/integration_tests/modules/test_lxd.py +++ b/tests/integration_tests/modules/test_lxd.py @@ -8,6 +8,7 @@ import warnings import pytest import yaml +from tests.integration_tests.clouds import ImageSpecification, IntegrationCloud from tests.integration_tests.util import verify_clean_log BRIDGE_USER_DATA = """\ @@ -34,6 +35,119 @@ lxd: storage_backend: {} """ +PRESEED_USER_DATA = """\ +#cloud-config +lxd: + preseed: | + config: {} + networks: + - config: + ipv4.address: auto + ipv6.address: auto + description: "" + managed: false + name: lxdbr0 + type: "" + storage_pools: + - config: + source: /var/snap/lxd/common/lxd/storage-pools/default + description: "" + name: default + driver: dir + profiles: + - config: {} + description: "" + devices: + eth0: + name: eth0 + nictype: bridged + parent: lxdbr0 + type: nic + root: + path: / + pool: default + type: disk + name: default + cluster: null +""" + + +STORAGE_PRESEED_USER_DATA = """\ +#cloud-config +lxd: + preseed: | + networks: + - config: + ipv4.address: 10.42.42.1/24 + ipv4.nat: "true" + ipv6.address: fd42:4242:4242:4242::1/64 + ipv6.nat: "true" + description: "" + name: lxdbr0 + type: bridge + project: default + storage_pools: + - config: + size: 5GiB + source: /var/snap/lxd/common/lxd/disks/default.img + description: "" + name: default + driver: {driver} + profiles: + - config: {{ }} + description: Default LXD profile + devices: + eth0: + {nictype} + {parent} + {network} + type: nic + root: + path: / + pool: default + type: disk + name: default + - config: + user.vendor-data: | + #cloud-config + write_files: + - path: /var/lib/cloud/scripts/per-once/setup-lxc.sh + encoding: b64 + permissions: '0755' + owner: root:root + content: | + IyEvYmluL2Jhc2gKZWNobyBZRVAgPj4gL3Zhci9sb2cvY2xvdWQtaW5pdC5sb2cK + devices: + config: + source: cloud-init:config + type: disk + eth0: + name: eth0 + network: lxdbr0 + type: nic + root: + path: / + pool: default + type: disk + description: Pycloudlib LXD profile for bionic VMs + name: bionic-vm-lxc-setup + projects: + - config: + features.images: "true" + features.networks: "true" + features.profiles: "true" + features.storage.volumes: "true" + description: Default LXD project + name: default + - config: + features.images: "false" + features.networks: "true" + features.profiles: "false" + features.storage.volumes: "true" + description: Limited project + name: limited +""" + @pytest.mark.no_container @pytest.mark.user_data(BRIDGE_USER_DATA) @@ -62,6 +176,38 @@ def validate_storage(validate_client, pkg_name, command): return log +def validate_preseed_profiles(client, preseed_cfg): + for src_profile in preseed_cfg["profiles"]: + profile = yaml.safe_load( + client.execute(f"lxc profile show {src_profile['name']}") + ) + assert src_profile["config"] == profile["config"] + + +def validate_preseed_storage_pools(client, preseed_cfg): + for src_storage in preseed_cfg["storage_pools"]: + storage_pool = yaml.safe_load( + client.execute(f"lxc storage show {src_storage['name']}") + ) + if "volatile.initial_source" in storage_pool["config"]: + assert storage_pool["config"]["source"] == storage_pool[ + "config" + ].pop("volatile.initial_source") + if storage_pool["driver"] == "zfs": + "default" == storage_pool["config"].pop("zfs.pool_name") + assert storage_pool["config"] == src_storage["config"] + assert storage_pool["driver"] == src_storage["driver"] + + +def validate_preseed_projects(client, preseed_cfg): + for src_project in preseed_cfg.get("projects", []): + project = yaml.safe_load( + client.execute(f"lxc project show {src_project['name']}") + ) + project.pop("used_by", None) + assert project == src_project + + @pytest.mark.no_container @pytest.mark.user_data(STORAGE_USER_DATA.format("btrfs")) def test_storage_btrfs(client): @@ -69,6 +215,30 @@ def test_storage_btrfs(client): @pytest.mark.no_container +@pytest.mark.not_bionic +def test_storage_preseed_btrfs(setup_image, session_cloud: IntegrationCloud): + cfg_image_spec = ImageSpecification.from_os_image() + if cfg_image_spec.release in ("bionic",): + nictype = "nictype: bridged" + parent = "parent: lxdbr0" + network = "" + else: + nictype = "" + parent = "" + network = "network: lxdbr0" + user_data = STORAGE_PRESEED_USER_DATA.format( + driver="btrfs", nictype=nictype, parent=parent, network=network + ) + with session_cloud.launch(user_data=user_data) as client: + validate_storage(client, "btrfs-progs", "mkfs.btrfs") + src_cfg = yaml.safe_load(user_data) + preseed_cfg = yaml.safe_load(src_cfg["lxd"]["preseed"]) + validate_preseed_profiles(client, preseed_cfg) + validate_preseed_storage_pools(client, preseed_cfg) + validate_preseed_projects(client, preseed_cfg) + + +@pytest.mark.no_container @pytest.mark.user_data(STORAGE_USER_DATA.format("lvm")) def test_storage_lvm(client): log = client.read_from_file("/var/log/cloud-init.log") @@ -82,7 +252,40 @@ def test_storage_lvm(client): validate_storage(client, "lvm2", "lvcreate") +@pytest.mark.user_data(PRESEED_USER_DATA) +def test_basic_preseed(client): + preseed_cfg = yaml.safe_load(PRESEED_USER_DATA)["lxd"]["preseed"] + preseed_cfg = yaml.safe_load(preseed_cfg) + validate_preseed_profiles(client, preseed_cfg) + validate_preseed_storage_pools(client, preseed_cfg) + validate_preseed_projects(client, preseed_cfg) + + @pytest.mark.no_container @pytest.mark.user_data(STORAGE_USER_DATA.format("zfs")) def test_storage_zfs(client): validate_storage(client, "zfsutils-linux", "zpool") + + +@pytest.mark.no_container +@pytest.mark.not_bionic +def test_storage_preseed_zfs(setup_image, session_cloud: IntegrationCloud): + cfg_image_spec = ImageSpecification.from_os_image() + if cfg_image_spec.release in ("bionic",): + nictype = "nictype: bridged" + parent = "parent: lxdbr0" + network = "" + else: + nictype = "" + parent = "" + network = "network: lxdbr0" + user_data = STORAGE_PRESEED_USER_DATA.format( + driver="zfs", nictype=nictype, parent=parent, network=network + ) + with session_cloud.launch(user_data=user_data) as client: + validate_storage(client, "zfsutils-linux", "zpool") + src_cfg = yaml.safe_load(user_data) + preseed_cfg = yaml.safe_load(src_cfg["lxd"]["preseed"]) + validate_preseed_profiles(client, preseed_cfg) + validate_preseed_storage_pools(client, preseed_cfg) + validate_preseed_projects(client, preseed_cfg) diff --git a/tests/unittests/config/test_cc_lxd.py b/tests/unittests/config/test_cc_lxd.py index 8b75a1f7..184b586e 100644 --- a/tests/unittests/config/test_cc_lxd.py +++ b/tests/unittests/config/test_cc_lxd.py @@ -14,27 +14,27 @@ from cloudinit.config.schema import ( from tests.unittests import helpers as t_help from tests.unittests.util import get_cloud +BACKEND_DEF = ( + ("zfs", "zfs", "zfsutils-linux"), + ("btrfs", "mkfs.btrfs", "btrfs-progs"), + ("lvm", "lvcreate", "lvm2"), + ("dir", None, None), +) +LXD_INIT_CFG = { + "lxd": { + "init": { + "network_address": "0.0.0.0", + "storage_backend": "zfs", + "storage_pool": "poolname", + } + } +} + class TestLxd(t_help.CiTestCase): with_logs = True - lxd_cfg = { - "lxd": { - "init": { - "network_address": "0.0.0.0", - "storage_backend": "zfs", - "storage_pool": "poolname", - } - } - } - backend_def = ( - ("zfs", "zfs", "zfsutils-linux"), - ("btrfs", "mkfs.btrfs", "btrfs-progs"), - ("lvm", "lvcreate", "lvm2"), - ("dir", None, None), - ) - @mock.patch("cloudinit.config.cc_lxd.util.system_info") @mock.patch("cloudinit.config.cc_lxd.os.path.exists", return_value=True) @mock.patch("cloudinit.config.cc_lxd.subp.subp", return_value=True) @@ -47,8 +47,8 @@ class TestLxd(t_help.CiTestCase): cc = get_cloud(mocked_distro=True) install = cc.distro.install_packages - for backend, cmd, package in self.backend_def: - lxd_cfg = deepcopy(self.lxd_cfg) + for backend, cmd, package in BACKEND_DEF: + lxd_cfg = deepcopy(LXD_INIT_CFG) lxd_cfg["lxd"]["init"]["storage_backend"] = backend subp.call_args_list = [] install.call_args_list = [] @@ -94,27 +94,16 @@ class TestLxd(t_help.CiTestCase): else: self.assertEqual([], exists.call_args_list) - @mock.patch("cloudinit.config.cc_lxd.subp.which", return_value=False) - def test_lxd_package_install(self, m_which): - for backend, _, package in self.backend_def: - lxd_cfg = deepcopy(self.lxd_cfg) - lxd_cfg["lxd"]["init"]["storage_backend"] = backend - - packages = cc_lxd.get_required_packages(lxd_cfg["lxd"]["init"]) - assert "lxd" in packages - if package: - assert package in packages - @mock.patch("cloudinit.config.cc_lxd.maybe_cleanup_default") @mock.patch("cloudinit.config.cc_lxd.subp") def test_lxd_install(self, mock_subp, m_maybe_clean): cc = get_cloud() cc.distro = mock.MagicMock() mock_subp.which.return_value = None - cc_lxd.handle("cc_lxd", self.lxd_cfg, cc, self.logger, []) + cc_lxd.handle("cc_lxd", LXD_INIT_CFG, cc, self.logger, []) self.assertNotIn("WARN", self.logs.getvalue()) self.assertTrue(cc.distro.install_packages.called) - cc_lxd.handle("cc_lxd", self.lxd_cfg, cc, self.logger, []) + cc_lxd.handle("cc_lxd", LXD_INIT_CFG, cc, self.logger, []) self.assertFalse(m_maybe_clean.called) install_pkg = cc.distro.install_packages.call_args_list[0][0][0] self.assertEqual(sorted(install_pkg), ["lxd", "zfsutils-linux"]) @@ -139,6 +128,25 @@ class TestLxd(t_help.CiTestCase): self.assertFalse(mock_subp.subp.called) self.assertFalse(m_maybe_clean.called) + @mock.patch("cloudinit.config.cc_lxd.subp") + def test_lxd_preseed(self, mock_subp): + cc = get_cloud() + cc.distro = mock.MagicMock() + cc_lxd.handle( + "cc_lxd", + {"lxd": {"preseed": '{"chad": True}'}}, + cc, + self.logger, + [], + ) + self.assertEqual( + [ + mock.call(["lxd", "waitready", "--timeout=300"]), + mock.call(["lxd", "init", "--preseed"], data='{"chad": True}'), + ], + mock_subp.subp.call_args_list, + ) + def test_lxd_debconf_new_full(self): data = { "mode": "new", @@ -330,11 +338,42 @@ class TestLxdMaybeCleanupDefault(t_help.CiTestCase): ) +class TestGetRequiredPackages: + @pytest.mark.parametrize( + "storage_type, cmd, preseed, package", + ( + ("zfs", "zfs", "", "zfsutils-linux"), + ("btrfs", "mkfs.btrfs", "", "btrfs-progs"), + ("lvm", "lvcreate", "", "lvm2"), + ("lvm", "lvcreate", "storage_pools: [{driver: lvm}]", "lvm2"), + ("dir", None, "", None), + ), + ) + @mock.patch("cloudinit.config.cc_lxd.subp.which", return_value=False) + def test_lxd_package_install( + self, m_which, storage_type, cmd, preseed, package + ): + if preseed: # preseed & lxd.init mutually exclusive + init_cfg = {} + else: + lxd_cfg = deepcopy(LXD_INIT_CFG) + lxd_cfg["lxd"]["init"]["storage_backend"] = storage_type + init_cfg = lxd_cfg["lxd"]["init"] + + packages = cc_lxd.get_required_packages(init_cfg, preseed) + assert "lxd" in packages + which_calls = [mock.call("lxd")] + if package: + which_calls.append(mock.call(cmd)) + assert package in packages + assert which_calls == m_which.call_args_list + + class TestLXDSchema: @pytest.mark.parametrize( "config, error_msg", [ - # Only allow init and bridge keys + # Only allow init, bridge and preseed keys ({"lxd": {"bridgeo": 1}}, "Additional properties are not allowed"), # Only allow init.storage_backend values zfs and dir ( @@ -347,7 +386,11 @@ class TestLXDSchema: # Require bridge.mode ({"lxd": {"bridge": {}}}, "bridge: 'mode' is a required property"), # Require init or bridge keys - ({"lxd": {}}, "does not have enough properties"), + ({"lxd": {}}, "lxd: {} does not have enough properties"), + # Require some non-empty preseed config of type string + ({"lxd": {"preseed": {}}}, "not of type 'string'"), + ({"lxd": {"preseed": ""}}, None), + ({"lxd": {"preseed": "this is {} opaque"}}, None), # Require bridge.mode ({"lxd": {"bridge": {"mode": "new", "mtu": 9000}}}, None), # LXD's default value @@ -371,5 +414,64 @@ class TestLXDSchema: else: validate_cloudconfig_schema(config, get_schema(), strict=True) + @pytest.mark.parametrize( + "init_cfg, bridge_cfg, preseed_str, error_expectation", + ( + pytest.param( + {}, {}, "", t_help.does_not_raise(), id="empty_cfgs_no_errors" + ), + pytest.param( + {"init-cfg": 1}, + {"bridge-cfg": 2}, + "", + t_help.does_not_raise(), + id="cfg_init_and_bridge_allowed", + ), + pytest.param( + {}, + {}, + "profiles: []", + t_help.does_not_raise(), + id="cfg_preseed_allowed_without_bridge_or_init", + ), + pytest.param( + {"init-cfg": 1}, + {"bridge-cfg": 2}, + "profiles: []", + pytest.raises( + ValueError, + match=re.escape( + "Unable to configure LXD. lxd.preseed config can not" + " be provided with key(s): lxd.init, lxd.bridge" + ), + ), + ), + pytest.param( + "nope", + {}, + "", + pytest.raises( + ValueError, + match=re.escape( + "lxd.init config must be a dictionary. found a 'str'" + ), + ), + ), + ), + ) + def test_supplemental_schema_validation_raises_value_error( + self, init_cfg, bridge_cfg, preseed_str, error_expectation + ): + """LXD is strict on invalid user-data raising conspicuous ValueErrors + cc_lxd.supplemental_schema_validation + + Hard errors result is faster triage/awareness of config problems than + warnings do. + """ + with error_expectation: + cc_lxd.supplemental_schema_validation( + init_cfg, bridge_cfg, preseed_str + ) + # vi: ts=4 expandtab |