summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChad Smith <chad.smith@canonical.com>2022-10-23 22:35:50 -0600
committerGitHub <noreply@github.com>2022-10-23 23:35:50 -0500
commit96a803ac2aa1829ec70b04132ed2a4c3117f96d2 (patch)
tree31b38f179135b53ba98b9cfe6405595eeeb4b5b0
parente77c0bf84c867a05ce7947fc15548a2c8b261a24 (diff)
downloadcloud-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.py183
-rw-r--r--cloudinit/config/schemas/schema-cloud-config-v1.json6
-rw-r--r--tests/integration_tests/modules/test_lxd.py203
-rw-r--r--tests/unittests/config/test_cc_lxd.py168
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