summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CONTRIBUTING.rst2
-rw-r--r--cloudinit/config/cc_keyboard.py69
-rw-r--r--cloudinit/config/cc_keys_to_console.py80
-rw-r--r--cloudinit/config/cc_landscape.py110
-rw-r--r--cloudinit/config/cc_locale.py33
-rw-r--r--cloudinit/config/cc_lxd.py106
-rw-r--r--cloudinit/config/cloud-init-schema.json243
-rw-r--r--doc/examples/cloud-config-landscape.txt23
-rw-r--r--tests/unittests/config/test_cc_keyboard.py77
-rw-r--r--tests/unittests/config/test_cc_keys_to_console.py81
-rw-r--r--tests/unittests/config/test_cc_landscape.py34
-rw-r--r--tests/unittests/config/test_cc_locale.py38
-rw-r--r--tests/unittests/config/test_cc_lxd.py31
-rw-r--r--tests/unittests/config/test_schema.py18
14 files changed, 699 insertions, 246 deletions
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index 73122d79..2c14ebc7 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -136,7 +136,7 @@ Do these things for each feature or bug
* Apply black and isort formatting rules with `tox`_::
- tox -e format
+ tox -e do_format
* Run unit tests and lint/formatting checks with `tox`_::
diff --git a/cloudinit/config/cc_keyboard.py b/cloudinit/config/cc_keyboard.py
index 98ef326a..211cb015 100644
--- a/cloudinit/config/cc_keyboard.py
+++ b/cloudinit/config/cc_keyboard.py
@@ -10,31 +10,21 @@ from textwrap import dedent
from cloudinit import distros
from cloudinit import log as logging
-from cloudinit.config.schema import (
- MetaSchema,
- get_meta_doc,
- validate_cloudconfig_schema,
-)
+from cloudinit.config.schema import MetaSchema, get_meta_doc
from cloudinit.settings import PER_INSTANCE
-frequency = PER_INSTANCE
-
# FIXME: setting keyboard layout should be supported by all OSes.
# But currently only implemented for Linux distributions that use systemd.
-osfamilies = ["arch", "debian", "redhat", "suse"]
-distros = distros.Distro.expand_osfamily(osfamilies)
DEFAULT_KEYBOARD_MODEL = "pc105"
+distros = distros.Distro.expand_osfamily(["arch", "debian", "redhat", "suse"])
+
meta: MetaSchema = {
"id": "cc_keyboard",
"name": "Keyboard",
"title": "Set keyboard layout",
- "description": dedent(
- """\
- Handle keyboard configuration.
- """
- ),
+ "description": "Handle keyboard configuration.",
"distros": distros,
"examples": [
dedent(
@@ -55,57 +45,11 @@ meta: MetaSchema = {
"""
),
],
- "frequency": frequency,
+ "frequency": PER_INSTANCE,
}
-schema = {
- "type": "object",
- "properties": {
- "keyboard": {
- "type": "object",
- "properties": {
- "layout": {
- "type": "string",
- "description": dedent(
- """\
- Required. Keyboard layout. Corresponds to XKBLAYOUT.
- """
- ),
- },
- "model": {
- "type": "string",
- "default": DEFAULT_KEYBOARD_MODEL,
- "description": dedent(
- """\
- Optional. Keyboard model. Corresponds to XKBMODEL.
- """
- ),
- },
- "variant": {
- "type": "string",
- "description": dedent(
- """\
- Optional. Keyboard variant. Corresponds to XKBVARIANT.
- """
- ),
- },
- "options": {
- "type": "string",
- "description": dedent(
- """\
- Optional. Keyboard options. Corresponds to XKBOPTIONS.
- """
- ),
- },
- },
- "required": ["layout"],
- "additionalProperties": False,
- }
- },
-}
-
-__doc__ = get_meta_doc(meta, schema)
+__doc__ = get_meta_doc(meta)
LOG = logging.getLogger(__name__)
@@ -116,7 +60,6 @@ def handle(name, cfg, cloud, log, args):
"Skipping module named %s, no 'keyboard' section found", name
)
return
- validate_cloudconfig_schema(cfg, schema)
kb_cfg = cfg["keyboard"]
layout = kb_cfg["layout"]
model = kb_cfg.get("model", DEFAULT_KEYBOARD_MODEL)
diff --git a/cloudinit/config/cc_keys_to_console.py b/cloudinit/config/cc_keys_to_console.py
index ab35e136..dd8b92fe 100644
--- a/cloudinit/config/cc_keys_to_console.py
+++ b/cloudinit/config/cc_keys_to_console.py
@@ -6,46 +6,64 @@
#
# This file is part of cloud-init. See LICENSE file for license information.
-"""
-Keys to Console
----------------
-**Summary:** control which SSH host keys may be written to console
-
-For security reasons it may be desirable not to write SSH host keys and their
-fingerprints to the console. To avoid either being written to the console the
-``emit_keys_to_console`` config key under the main ``ssh`` config key can be
-used. To avoid the fingerprint of types of SSH host keys being written to
-console the ``ssh_fp_console_blacklist`` config key can be used. By default
-all types of keys will have their fingerprints written to console. To avoid
-host keys of a key type being written to console the
-``ssh_key_console_blacklist`` config key can be used. By default ``ssh-dss``
-host keys are not written to console.
-
-**Internal name:** ``cc_keys_to_console``
-
-**Module frequency:** per instance
-
-**Supported distros:** all
-
-**Config keys**::
-
- ssh:
- emit_keys_to_console: false
-
- ssh_fp_console_blacklist: <list of key types>
- ssh_key_console_blacklist: <list of key types>
-"""
+"""Keys to Console: Control which SSH host keys may be written to console"""
import os
+from textwrap import dedent
from cloudinit import subp, util
+from cloudinit.config.schema import MetaSchema, get_meta_doc
from cloudinit.settings import PER_INSTANCE
-frequency = PER_INSTANCE
-
# This is a tool that cloud init provides
HELPER_TOOL_TPL = "%s/cloud-init/write-ssh-key-fingerprints"
+distros = ["all"]
+
+meta: MetaSchema = {
+ "id": "cc_keys_to_console",
+ "name": "Keys to Console",
+ "title": "Control which SSH host keys may be written to console",
+ "description": (
+ "For security reasons it may be desirable not to write SSH host keys"
+ " and their fingerprints to the console. To avoid either being written"
+ " to the console the ``emit_keys_to_console`` config key under the"
+ " main ``ssh`` config key can be used. To avoid the fingerprint of"
+ " types of SSH host keys being written to console the"
+ " ``ssh_fp_console_blacklist`` config key can be used. By default,"
+ " all types of keys will have their fingerprints written to console."
+ " To avoid host keys of a key type being written to console the"
+ "``ssh_key_console_blacklist`` config key can be used. By default,"
+ " ``ssh-dss`` host keys are not written to console."
+ ),
+ "distros": distros,
+ "examples": [
+ dedent(
+ """\
+ # Do not print any SSH keys to system console
+ ssh:
+ emit_keys_to_console: false
+ """
+ ),
+ dedent(
+ """\
+ # Do not print certain ssh key types to console
+ ssh_key_console_blacklist: [dsa, ssh-dss]
+ """
+ ),
+ dedent(
+ """\
+ # Do not print specific ssh key fingerprints to console
+ ssh_fp_console_blacklist:
+ - E25451E0221B5773DEBFF178ECDACB160995AA89
+ - FE76292D55E8B28EE6DB2B34B2D8A784F8C0AAB0
+ """
+ ),
+ ],
+ "frequency": PER_INSTANCE,
+}
+__doc__ = get_meta_doc(meta)
+
def _get_helper_tool_path(distro):
try:
diff --git a/cloudinit/config/cc_landscape.py b/cloudinit/config/cc_landscape.py
index 03ebf411..ede09bd9 100644
--- a/cloudinit/config/cc_landscape.py
+++ b/cloudinit/config/cc_landscape.py
@@ -6,17 +6,38 @@
#
# This file is part of cloud-init. See LICENSE file for license information.
-"""
-Landscape
----------
-**Summary:** install and configure landscape client
+"""install and configure landscape client"""
+
+import os
+from io import BytesIO
+from textwrap import dedent
+
+from configobj import ConfigObj
+
+from cloudinit import subp, type_utils, util
+from cloudinit.config.schema import MetaSchema, get_meta_doc
+from cloudinit.settings import PER_INSTANCE
+LSC_CLIENT_CFG_FILE = "/etc/landscape/client.conf"
+LS_DEFAULT_FILE = "/etc/default/landscape-client"
+
+# defaults taken from stock client.conf in landscape-client 11.07.1.1-0ubuntu2
+LSC_BUILTIN_CFG = {
+ "client": {
+ "log_level": "info",
+ "url": "https://landscape.canonical.com/message-system",
+ "ping_url": "http://landscape.canonical.com/ping",
+ "data_path": "/var/lib/landscape/client",
+ }
+}
+
+MODULE_DESCRIPTION = """\
This module installs and configures ``landscape-client``. The landscape client
will only be installed if the key ``landscape`` is present in config. Landscape
client configuration is given under the ``client`` key under the main
``landscape`` config key. The config parameters are not interpreted by
cloud-init, but rather are converted into a ConfigObj formatted file and
-written out to ``/etc/landscape/client.conf``.
+written out to the `[client]` section in ``/etc/landscape/client.conf``.
The following default client config is provided, but can be overridden::
@@ -33,53 +54,47 @@ The following default client config is provided, but can be overridden::
.. note::
if ``tags`` is defined, its contents should be a string delimited with
``,`` rather than a list
-
-**Internal name:** ``cc_landscape``
-
-**Module frequency:** per instance
-
-**Supported distros:** ubuntu
-
-**Config keys**::
-
- landscape:
- client:
- url: "https://landscape.canonical.com/message-system"
- ping_url: "http://landscape.canonical.com/ping"
- data_path: "/var/lib/landscape/client"
- http_proxy: "http://my.proxy.com/foobar"
- https_proxy: "https://my.proxy.com/foobar"
- tags: "server,cloud"
- computer_title: "footitle"
- registration_key: "fookey"
- account_name: "fooaccount"
"""
-
-import os
-from io import BytesIO
-
-from configobj import ConfigObj
-
-from cloudinit import subp, type_utils, util
-from cloudinit.settings import PER_INSTANCE
-
-frequency = PER_INSTANCE
-
-LSC_CLIENT_CFG_FILE = "/etc/landscape/client.conf"
-LS_DEFAULT_FILE = "/etc/default/landscape-client"
-
distros = ["ubuntu"]
-# defaults taken from stock client.conf in landscape-client 11.07.1.1-0ubuntu2
-LSC_BUILTIN_CFG = {
- "client": {
- "log_level": "info",
- "url": "https://landscape.canonical.com/message-system",
- "ping_url": "http://landscape.canonical.com/ping",
- "data_path": "/var/lib/landscape/client",
- }
+meta: MetaSchema = {
+ "id": "cc_landscape",
+ "name": "Landscape",
+ "title": "Install and configure landscape client",
+ "description": MODULE_DESCRIPTION,
+ "distros": distros,
+ "examples": [
+ dedent(
+ """\
+ # To discover additional supported client keys, run
+ # man landscape-config.
+ landscape:
+ client:
+ url: "https://landscape.canonical.com/message-system"
+ ping_url: "http://landscape.canonical.com/ping"
+ data_path: "/var/lib/landscape/client"
+ http_proxy: "http://my.proxy.com/foobar"
+ https_proxy: "https://my.proxy.com/foobar"
+ tags: "server,cloud"
+ computer_title: "footitle"
+ registration_key: "fookey"
+ account_name: "fooaccount"
+ """
+ ),
+ dedent(
+ """\
+ # Any keys below `client` are optional and the default values will
+ # be used.
+ landscape:
+ client: {}
+ """
+ ),
+ ],
+ "frequency": PER_INSTANCE,
}
+__doc__ = get_meta_doc(meta)
+
def handle(_name, cfg, cloud, log, _args):
"""
@@ -102,6 +117,7 @@ def handle(_name, cfg, cloud, log, _args):
cloud.distro.install_packages(("landscape-client",))
+ # Later order config values override earlier values
merge_data = [
LSC_BUILTIN_CFG,
LSC_CLIENT_CFG_FILE,
diff --git a/cloudinit/config/cc_locale.py b/cloudinit/config/cc_locale.py
index 29f6a9b6..6a31933e 100644
--- a/cloudinit/config/cc_locale.py
+++ b/cloudinit/config/cc_locale.py
@@ -11,15 +11,11 @@
from textwrap import dedent
from cloudinit import util
-from cloudinit.config.schema import (
- MetaSchema,
- get_meta_doc,
- validate_cloudconfig_schema,
-)
+from cloudinit.config.schema import MetaSchema, get_meta_doc
from cloudinit.settings import PER_INSTANCE
-frequency = PER_INSTANCE
distros = ["all"]
+
meta: MetaSchema = {
"id": "cc_locale",
"name": "Locale",
@@ -45,29 +41,10 @@ meta: MetaSchema = {
"""
),
],
- "frequency": frequency,
-}
-
-schema = {
- "type": "object",
- "properties": {
- "locale": {
- "type": "string",
- "description": (
- "The locale to set as the system's locale (e.g. ar_PS)"
- ),
- },
- "locale_configfile": {
- "type": "string",
- "description": (
- "The file in which to write the locale configuration (defaults"
- " to the distro's default location)"
- ),
- },
- },
+ "frequency": PER_INSTANCE,
}
-__doc__ = get_meta_doc(meta, schema) # Supplement python help()
+__doc__ = get_meta_doc(meta)
def handle(name, cfg, cloud, log, args):
@@ -82,8 +59,6 @@ def handle(name, cfg, cloud, log, args):
)
return
- validate_cloudconfig_schema(cfg, schema)
-
log.debug("Setting locale to %s", locale)
locale_cfgfile = util.get_cfg_option_str(cfg, "locale_configfile")
cloud.distro.apply_locale(locale, locale_cfgfile)
diff --git a/cloudinit/config/cc_lxd.py b/cloudinit/config/cc_lxd.py
index 13ddcbe9..847a7c3c 100644
--- a/cloudinit/config/cc_lxd.py
+++ b/cloudinit/config/cc_lxd.py
@@ -4,59 +4,75 @@
#
# This file is part of cloud-init. See LICENSE file for license information.
-"""
-LXD
----
-**Summary:** configure lxd with ``lxd init`` and optionally lxd-bridge
+"""LXD: configure lxd with ``lxd init`` and optionally lxd-bridge"""
+
+import os
+from textwrap import dedent
+
+from cloudinit import log as logging
+from cloudinit import subp, util
+from cloudinit.config.schema import MetaSchema, get_meta_doc
+from cloudinit.settings import PER_INSTANCE
+
+LOG = logging.getLogger(__name__)
+
+_DEFAULT_NETWORK_NAME = "lxdbr0"
+
+MODULE_DESCRIPTION = """\
This module configures lxd with user specified options using ``lxd init``.
If lxd is not present on the system but lxd configuration is provided, then
lxd will be installed. If the selected storage backend is zfs, then zfs will
be installed if missing. If network bridge configuration is provided, then
lxd-bridge will be configured accordingly.
-
-**Internal name:** ``cc_lxd``
-
-**Module frequency:** per instance
-
-**Supported distros:** ubuntu
-
-**Config keys**::
-
- lxd:
- init:
- network_address: <ip addr>
- network_port: <port>
- storage_backend: <zfs/dir>
- storage_create_device: <dev>
- storage_create_loop: <size>
- storage_pool: <name>
- trust_password: <password>
- bridge:
- mode: <new, existing or none>
- name: <name>
- ipv4_address: <ip addr>
- ipv4_netmask: <cidr>
- ipv4_dhcp_first: <ip addr>
- ipv4_dhcp_last: <ip addr>
- ipv4_dhcp_leases: <size>
- ipv4_nat: <bool>
- ipv6_address: <ip addr>
- ipv6_netmask: <cidr>
- ipv6_nat: <bool>
- domain: <domain>
"""
-import os
-
-from cloudinit import log as logging
-from cloudinit import subp, util
-
distros = ["ubuntu"]
-LOG = logging.getLogger(__name__)
-
-_DEFAULT_NETWORK_NAME = "lxdbr0"
+meta: MetaSchema = {
+ "id": "cc_lxd",
+ "name": "LXD",
+ "title": "Configure LXD with ``lxd init`` and optionally lxd-bridge",
+ "description": MODULE_DESCRIPTION,
+ "distros": distros,
+ "examples": [
+ dedent(
+ """\
+ # Simplest working directory backed LXD configuration
+ lxd:
+ init:
+ storage_backend: dir
+ """
+ ),
+ dedent(
+ """\
+ lxd:
+ init:
+ network_address: 0.0.0.0
+ network_port: 8443
+ storage_backend: zfs
+ storage_pool: datapool
+ storage_create_loop: 10
+ bridge:
+ mode: new
+ name: lxdbr0
+ ipv4_address: 10.0.8.1
+ ipv4_netmask: 24
+ ipv4_dhcp_first: 10.0.8.2
+ ipv4_dhcp_last: 10.0.8.3
+ ipv4_dhcp_leases: 250
+ ipv4_nat: true
+ ipv6_address: fd98:9e0:3744::1
+ ipv6_netmask: 64
+ ipv6_nat: true
+ domain: lxd
+ """
+ ),
+ ],
+ "frequency": PER_INSTANCE,
+}
+
+__doc__ = get_meta_doc(meta)
def handle(name, cfg, cloud, log, args):
@@ -300,8 +316,8 @@ def maybe_cleanup_default(
"""Newer versions of lxc (3.0.1+) create a lxdbr0 network when
'lxd init --auto' is run. Older versions did not.
- By removing ay that lxd-init created, we simply leave the add/attach
- code in-tact.
+ By removing any that lxd-init created, we simply leave the add/attach
+ code intact.
https://github.com/lxc/lxd/issues/4649"""
if net_name != _DEFAULT_NETWORK_NAME or not did_init:
diff --git a/cloudinit/config/cloud-init-schema.json b/cloudinit/config/cloud-init-schema.json
index f7771434..23156c11 100644
--- a/cloudinit/config/cloud-init-schema.json
+++ b/cloudinit/config/cloud-init-schema.json
@@ -545,6 +545,244 @@
}
}
},
+ "cc_keyboard": {
+ "type": "object",
+ "properties": {
+ "keyboard": {
+ "type": "object",
+ "properties": {
+ "layout": {
+ "type": "string",
+ "description": "Required. Keyboard layout. Corresponds to XKBLAYOUT."
+ },
+ "model": {
+ "type": "string",
+ "default": "pc105",
+ "description": "Optional. Keyboard model. Corresponds to XKBMODEL. Default: ``pc105``."
+ },
+ "variant": {
+ "type": "string",
+ "description": "Optional. Keyboard variant. Corresponds to XKBVARIANT."
+ },
+ "options": {
+ "type": "string",
+ "description": "Optional. Keyboard options. Corresponds to XKBOPTIONS."
+ }
+ },
+ "required": ["layout"],
+ "additionalProperties": false
+ }
+ }
+ },
+ "cc_keys_to_console": {
+ "type": "object",
+ "properties": {
+ "ssh": {
+ "type": "object",
+ "properties": {
+ "emit_keys_to_console": {
+ "type": "boolean",
+ "default": true,
+ "description": "Set false to avoid printing SSH keys to system console. Default: ``true``."
+ }
+ },
+ "additionalProperties": false,
+ "required": ["emit_keys_to_console"]
+ },
+ "ssh_key_console_blacklist": {
+ "type": "array",
+ "default": ["ssh-dss"],
+ "description": "Avoid printing matching SSH key types to the system console.",
+ "items": {"type": "string"},
+ "uniqueItems": true
+ },
+ "ssh_fp_console_blacklist": {
+ "type": "array",
+ "description": "Avoid printing matching SSH fingerprints to the system console.",
+ "items": {"type": "string"},
+ "uniqueItems": true
+ }
+ }
+ },
+ "cc_landscape": {
+ "type": "object",
+ "properties": {
+ "landscape": {
+ "type": "object",
+ "required": ["client"],
+ "properties": {
+ "client": {
+ "type": "object",
+ "properties": {
+ "url": {
+ "type": "string",
+ "default": "https://landscape.canonical.com/message-system",
+ "description": "The Landscape server URL to connect to. Default: ``https://landscape.canonical.com/message-system``."
+ },
+ "ping_url": {
+ "type": "string",
+ "default": "https://landscape.canonical.com/ping",
+ "description": "The URL to perform lightweight exchange initiation with. Default: ``https://landscape.canonical.com/ping``."
+ },
+ "data_path": {
+ "type": "string",
+ "default": "/var/lib/landscape/client",
+ "description": "The directory to store data files in. Default: ``/var/lib/landā€scape/client/``."
+ },
+ "log_level": {
+ "type": "string",
+ "default": "info",
+ "enum": ["debug", "info", "warning", "error", "critical"],
+ "description": "The log level for the client. Default: ``info``."
+ },
+ "computer_tite": {
+ "type": "string",
+ "description": "The title of this computer."
+ },
+ "account_name": {
+ "type": "string",
+ "description": "The account this computer belongs to."
+ },
+ "registration_key": {
+ "type": "string",
+ "description": "The account-wide key used for registering clients."
+ },
+ "tags": {
+ "type": "string",
+ "pattern": "^[-_0-9a-zA-Z]+(,[-_0-9a-zA-Z]+)*$",
+ "description": "Comma separated list of tag names to be sent to the server."
+ },
+ "http_proxy": {
+ "type": "string",
+ "description": "The URL of the HTTP proxy, if one is needed."
+ },
+ "https_proxy": {
+ "type": "string",
+ "description": "The URL of the HTTPS proxy, if one is needed."
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "cc_locale": {
+ "properties": {
+ "locale": {
+ "type": "string",
+ "description": "The locale to set as the system's locale (e.g. ar_PS)"
+ },
+ "locale_configfile": {
+ "type": "string",
+ "description": "The file in which to write the locale configuration (defaults to the distro's default location)"
+ }
+ }
+ },
+ "cc_lxd": {
+ "type": "object",
+ "properties": {
+ "lxd": {
+ "type": "object",
+ "minProperties": 1,
+ "properties": {
+ "init": {
+ "type": "object",
+ "properties": {
+ "network_address": {
+ "type": "string",
+ "description": "IP address for LXD to listen on"
+ },
+ "network_port": {
+ "type": "integer",
+ "description": "Network port to bind LXD to."
+ },
+ "storage_backend": {
+ "type": "string",
+ "enum": ["zfs", "dir"],
+ "default": "dir",
+ "description": "Storage backend to use. Default: ``dir``."
+ },
+ "storage_create_device": {
+ "type": "string",
+ "description": "Setup device based storage using DEVICE"
+ },
+ "storage_create_loop": {
+ "type": "integer",
+ "description": "Setup loop based storage with SIZE in GB"
+ },
+ "storage_pool": {
+ "type": "string",
+ "description": "Name of storage pool to use or create"
+ },
+ "trust_password": {
+ "type": "string",
+ "description": "The password required to add new clients"
+ }
+ }
+ },
+ "bridge": {
+ "type": "object",
+ "required": ["mode"],
+ "properties": {
+ "mode": {
+ "type": "string",
+ "description": "Whether to setup LXD bridge, use an existing bridge by ``name`` or create a new bridge. `none` will avoid bridge setup, `existing` will configure lxd to use the bring matching ``name`` and `new` will create a new bridge.",
+ "enum": ["none", "existing", "new"]
+ },
+ "name": {
+ "type": "string",
+ "description": "Name of the LXD network bridge to attach or create. Default: ``lxdbr0``.",
+ "default": "lxdbr0"
+ },
+ "ipv4_address": {
+ "type": "string",
+ "description": "IPv4 address for the bridge. If set, ``ipv4_netmask`` key required."
+ },
+ "ipv4_netmask": {
+ "type": "integer",
+ "description": "Prefix length for the ``ipv4_address`` key. Required when ``ipv4_address`` is set."
+ },
+ "ipv4_dhcp_first": {
+ "type": "string",
+ "description": "First IPv4 address of the DHCP range for the network created. This value will combined with ``ipv4_dhcp_last`` key to set LXC ``ipv4.dhcp.ranges``."
+ },
+ "ipv4_dhcp_last": {
+ "type": "string",
+ "description": "Last IPv4 address of the DHCP range for the network created. This value will combined with ``ipv4_dhcp_first`` key to set LXC ``ipv4.dhcp.ranges``."
+ },
+ "ipv4_dhcp_leases": {
+ "type": "integer",
+ "description": "Number of DHCP leases to allocate within the range. Automatically calculated based on `ipv4_dhcp_first` and `ipv4_dchp_last` when unset."
+ },
+ "ipv4_nat": {
+ "type": "boolean",
+ "default": false,
+ "description": "Set ``true`` to NAT the IPv4 traffic allowing for a routed IPv4 network. Default: ``false``."
+ },
+ "ipv6_address": {
+ "type": "string",
+ "description": "IPv6 address for the bridge (CIDR notation). When set, ``ipv6_netmask`` key is required. When absent, no IPv6 will be configured."
+ },
+ "ipv6_netmask": {
+ "type": "integer",
+ "description": "Prefix length for ``ipv6_address`` provided. Required when ``ipv6_address`` is set."
+ },
+ "ipv6_nat": {
+ "type": "boolean",
+ "default": false,
+ "description": "Whether to NAT. Default: ``false``."
+ },
+ "domain": {
+ "type": "string",
+ "description": "Domain to advertise to DHCP clients and use for DNS resolution."
+ }
+ }
+ }
+ },
+ "additionalProperties": false
+ }
+ }
+ },
"cc_package_update_upgrade_install": {
"type": "object",
"properties": {
@@ -1020,6 +1258,11 @@
{ "$ref": "#/$defs/cc_debug" },
{ "$ref": "#/$defs/cc_disable_ec2_metadata" },
{ "$ref": "#/$defs/cc_disk_setup" },
+ { "$ref": "#/$defs/cc_keyboard" },
+ { "$ref": "#/$defs/cc_keys_to_console" },
+ { "$ref": "#/$defs/cc_landscape" },
+ { "$ref": "#/$defs/cc_locale" },
+ { "$ref": "#/$defs/cc_lxd" },
{ "$ref": "#/$defs/cc_package_update_upgrade_install" },
{ "$ref": "#/$defs/cc_phone_home" },
{ "$ref": "#/$defs/cc_power_state_change"},
diff --git a/doc/examples/cloud-config-landscape.txt b/doc/examples/cloud-config-landscape.txt
deleted file mode 100644
index b76bf028..00000000
--- a/doc/examples/cloud-config-landscape.txt
+++ /dev/null
@@ -1,23 +0,0 @@
-#cloud-config
-# Landscape-client configuration
-#
-# Anything under the top 'landscape: client' entry
-# will be basically rendered into a ConfigObj formatted file
-# under the '[client]' section of /etc/landscape/client.conf
-#
-# Note: 'tags' should be specified as a comma delimited string
-# rather than a list.
-#
-# You can get example key/values by running 'landscape-config',
-# answer question, then look at /etc/landscape/client.config
-landscape:
- client:
- url: "https://landscape.canonical.com/message-system"
- ping_url: "http://landscape.canonical.com/ping"
- data_path: "/var/lib/landscape/client"
- http_proxy: "http://my.proxy.com/foobar"
- tags: "server,cloud"
- computer_title: footitle
- https_proxy: fooproxy
- registration_key: fookey
- account_name: fooaccount
diff --git a/tests/unittests/config/test_cc_keyboard.py b/tests/unittests/config/test_cc_keyboard.py
new file mode 100644
index 00000000..00fad9ff
--- /dev/null
+++ b/tests/unittests/config/test_cc_keyboard.py
@@ -0,0 +1,77 @@
+# This file is part of cloud-init. See LICENSE file for license information.
+
+"""Tests cc_keyboard module"""
+
+import re
+
+import pytest
+
+from cloudinit.config.schema import (
+ SchemaValidationError,
+ get_schema,
+ validate_cloudconfig_schema,
+)
+from tests.unittests.helpers import skipUnlessJsonSchema
+
+
+class TestKeyboard:
+ @pytest.mark.parametrize(
+ "config, error_msg",
+ (
+ # Valid schemas
+ ({"keyboard": {"layout": "somestring"}}, None),
+ # Invalid schemas
+ (
+ {"keyboard": {}},
+ "Cloud config schema errors: keyboard: 'layout' is a"
+ " required property",
+ ),
+ (
+ {"keyboard": "bogus"},
+ "Cloud config schema errors: keyboard: 'bogus' is not"
+ " of type 'object'",
+ ),
+ (
+ {"keyboard": {"layout": 1}},
+ "Cloud config schema errors: keyboard.layout: 1 is not"
+ " of type 'string'",
+ ),
+ (
+ {"keyboard": {"layout": "somestr", "model": None}},
+ "Cloud config schema errors: keyboard.model: None is not"
+ " of type 'string'",
+ ),
+ (
+ {"keyboard": {"layout": "somestr", "variant": [1]}},
+ re.escape(
+ "Cloud config schema errors: keyboard.variant: [1] is"
+ " not of type 'string'"
+ ),
+ ),
+ (
+ {"keyboard": {"layout": "somestr", "options": {}}},
+ "Cloud config schema errors: keyboard.options: {} is not"
+ " of type 'string'",
+ ),
+ (
+ {"keyboard": {"layout": "somestr", "extraprop": "somestr"}},
+ re.escape(
+ "Cloud config schema errors: keyboard: Additional"
+ " properties are not allowed ('extraprop' was unexpected)"
+ ),
+ ),
+ ),
+ )
+ @skipUnlessJsonSchema()
+ def test_schema_validation(self, config, error_msg):
+ """Assert expected schema validation and error messages."""
+ # New-style schema $defs exist in config/cloud-init-schema*.json
+ schema = get_schema()
+ if error_msg is None:
+ validate_cloudconfig_schema(config, schema, strict=True)
+ else:
+ with pytest.raises(SchemaValidationError, match=error_msg):
+ validate_cloudconfig_schema(config, schema, strict=True)
+
+
+# vi: ts=4 expandtab
diff --git a/tests/unittests/config/test_cc_keys_to_console.py b/tests/unittests/config/test_cc_keys_to_console.py
index 9efc2b48..61f62e96 100644
--- a/tests/unittests/config/test_cc_keys_to_console.py
+++ b/tests/unittests/config/test_cc_keys_to_console.py
@@ -1,9 +1,16 @@
"""Tests for cc_keys_to_console."""
-from unittest import mock
+
+import re
import pytest
from cloudinit.config import cc_keys_to_console
+from cloudinit.config.schema import (
+ SchemaValidationError,
+ get_schema,
+ validate_cloudconfig_schema,
+)
+from tests.unittests.helpers import mock, skipUnlessJsonSchema
class TestHandle:
@@ -38,3 +45,75 @@ class TestHandle:
cc_keys_to_console.handle("name", cfg, mock.Mock(), mock.Mock(), ())
assert subp_called == (m_subp.call_count == 1)
+
+
+class TestKeysToConsoleSchema:
+ @pytest.mark.parametrize(
+ "config, error_msg",
+ (
+ # Valid schemas are covered by meta examples tests in test_schema
+ # Invalid schemas
+ (
+ {"ssh": {}},
+ "Cloud config schema errors: ssh: 'emit_keys_to_console' is"
+ " a required property",
+ ),
+ ( # Avoid common failure giving a string 'false' instead of false
+ {"ssh": {"emit_keys_to_console": "false"}},
+ "Cloud config schema errors: ssh.emit_keys_to_console: 'false'"
+ " is not of type 'boolean'",
+ ),
+ (
+ {"ssh": {"noextraprop": False, "emit_keys_to_console": False}},
+ re.escape(
+ "Cloud config schema errors: ssh: Additional properties"
+ " are not allowed ('noextraprop' was unexpected)"
+ ),
+ ),
+ ( # Avoid common failure giving a string 'false' instead of false
+ {"ssh": {"emit_keys_to_console": "false"}},
+ "Cloud config schema errors: ssh.emit_keys_to_console: 'false'"
+ " is not of type 'boolean'",
+ ),
+ ( # Avoid common failure giving a string 'false' instead of false
+ {"ssh_key_console_blacklist": False},
+ "Cloud config schema errors: ssh_key_console_blacklist: False"
+ " is not of type 'array'",
+ ),
+ ( # Avoid common failure giving a string 'false' instead of false
+ {"ssh_key_console_blacklist": [1]},
+ "Cloud config schema errors: ssh_key_console_blacklist.0: 1 is"
+ " not of type 'string'",
+ ),
+ ( # Avoid common failure giving a string 'false' instead of false
+ {"ssh_key_console_blacklist": [1]},
+ "Cloud config schema errors: ssh_key_console_blacklist.0: 1 is"
+ " not of type 'string'",
+ ),
+ ( # Avoid common failure giving a string 'false' instead of false
+ {"ssh_fp_console_blacklist": None},
+ "Cloud config schema errors: ssh_fp_console_blacklist: None"
+ " is not of type 'array'",
+ ),
+ ( # Avoid common failure giving a string 'false' instead of false
+ {"ssh_fp_console_blacklist": [1]},
+ "Cloud config schema errors: ssh_fp_console_blacklist.0: 1 is"
+ " not of type 'string'",
+ ),
+ ( # Avoid common failure giving a string 'false' instead of false
+ {"ssh_fp_console_blacklist": [1]},
+ "Cloud config schema errors: ssh_fp_console_blacklist.0: 1 is"
+ " not of type 'string'",
+ ),
+ ),
+ )
+ @skipUnlessJsonSchema()
+ def test_schema_validation(self, config, error_msg):
+ """Assert expected schema validation and error messages."""
+ # New-style schema $defs exist in config/cloud-init-schema*.json
+ schema = get_schema()
+ with pytest.raises(SchemaValidationError, match=error_msg):
+ validate_cloudconfig_schema(config, schema, strict=True)
+
+
+# vi: ts=4 expandtab
diff --git a/tests/unittests/config/test_cc_landscape.py b/tests/unittests/config/test_cc_landscape.py
index efddc1b6..79ea6b0a 100644
--- a/tests/unittests/config/test_cc_landscape.py
+++ b/tests/unittests/config/test_cc_landscape.py
@@ -1,13 +1,20 @@
# This file is part of cloud-init. See LICENSE file for license information.
import logging
+import pytest
from configobj import ConfigObj
from cloudinit import util
from cloudinit.config import cc_landscape
+from cloudinit.config.schema import (
+ SchemaValidationError,
+ get_schema,
+ validate_cloudconfig_schema,
+)
from tests.unittests.helpers import (
FilesystemMockingTestCase,
mock,
+ skipUnlessJsonSchema,
wrap_and_call,
)
from tests.unittests.util import get_cloud
@@ -168,3 +175,30 @@ class TestLandscape(FilesystemMockingTestCase):
"Wrote landscape config file to {0}".format(self.conf),
self.logs.getvalue(),
)
+
+
+class TestLandscapeSchema:
+ @pytest.mark.parametrize(
+ "config, error_msg",
+ [
+ # Allow undocumented keys client keys without error
+ ({"landscape": {"client": {"allow_additional_keys": 1}}}, None),
+ # tags are comma-delimited
+ ({"landscape": {"client": {"tags": "1,2,3"}}}, None),
+ ({"landscape": {"client": {"tags": "1"}}}, None),
+ # Require client key
+ ({"landscape": {}}, "'client' is a required property"),
+ # tags are not whitespace-delimited
+ (
+ {"landscape": {"client": {"tags": "1, 2,3"}}},
+ "'1, 2,3' does not match",
+ ),
+ ],
+ )
+ @skipUnlessJsonSchema()
+ def test_schema_validation(self, config, error_msg):
+ if error_msg is None:
+ validate_cloudconfig_schema(config, get_schema(), strict=True)
+ else:
+ with pytest.raises(SchemaValidationError, match=error_msg):
+ validate_cloudconfig_schema(config, get_schema(), strict=True)
diff --git a/tests/unittests/config/test_cc_locale.py b/tests/unittests/config/test_cc_locale.py
index 7190bc68..d64610b6 100644
--- a/tests/unittests/config/test_cc_locale.py
+++ b/tests/unittests/config/test_cc_locale.py
@@ -8,19 +8,28 @@ import os
import shutil
import tempfile
from io import BytesIO
-from unittest import mock
+import pytest
from configobj import ConfigObj
from cloudinit import util
from cloudinit.config import cc_locale
-from tests.unittests import helpers as t_help
+from cloudinit.config.schema import (
+ SchemaValidationError,
+ get_schema,
+ validate_cloudconfig_schema,
+)
+from tests.unittests.helpers import (
+ FilesystemMockingTestCase,
+ mock,
+ skipUnlessJsonSchema,
+)
from tests.unittests.util import get_cloud
LOG = logging.getLogger(__name__)
-class TestLocale(t_help.FilesystemMockingTestCase):
+class TestLocale(FilesystemMockingTestCase):
def setUp(self):
super(TestLocale, self).setUp()
self.new_root = tempfile.mkdtemp()
@@ -120,4 +129,27 @@ class TestLocale(t_help.FilesystemMockingTestCase):
)
+class TestLocaleSchema:
+ @pytest.mark.parametrize(
+ "config, error_msg",
+ (
+ # Valid schemas tested via meta['examples'] in test_schema.py
+ # Invalid schemas
+ ({"locale": 1}, "locale: 1 is not of type 'string'"),
+ (
+ {"locale_configfile": 1},
+ "locale_configfile: 1 is not of type 'string'",
+ ),
+ ),
+ )
+ @skipUnlessJsonSchema()
+ def test_schema_validation(self, config, error_msg):
+ schema = get_schema()
+ if error_msg is None:
+ validate_cloudconfig_schema(config, schema, strict=True)
+ else:
+ with pytest.raises(SchemaValidationError, match=error_msg):
+ validate_cloudconfig_schema(config, schema, strict=True)
+
+
# vi: ts=4 expandtab
diff --git a/tests/unittests/config/test_cc_lxd.py b/tests/unittests/config/test_cc_lxd.py
index 720274d6..3b444127 100644
--- a/tests/unittests/config/test_cc_lxd.py
+++ b/tests/unittests/config/test_cc_lxd.py
@@ -1,7 +1,15 @@
# This file is part of cloud-init. See LICENSE file for license information.
+import re
from unittest import mock
+import pytest
+
from cloudinit.config import cc_lxd
+from cloudinit.config.schema import (
+ SchemaValidationError,
+ get_schema,
+ validate_cloudconfig_schema,
+)
from tests.unittests import helpers as t_help
from tests.unittests.util import get_cloud
@@ -269,4 +277,27 @@ class TestLxdMaybeCleanupDefault(t_help.CiTestCase):
)
+class TestLXDSchema:
+ @pytest.mark.parametrize(
+ "config, error_msg",
+ [
+ # Only allow init and bridge keys
+ ({"lxd": {"bridgeo": 1}}, "Additional properties are not allowed"),
+ # Only allow init.storage_backend values zfs and dir
+ (
+ {"lxd": {"init": {"storage_backend": "1zfs"}}},
+ re.escape("not one of ['zfs', 'dir']"),
+ ),
+ # Require bridge.mode
+ ({"lxd": {"bridge": {}}}, "bridge: 'mode' is a required property"),
+ # Require init or bridge keys
+ ({"lxd": {}}, "does not have enough properties"),
+ ],
+ )
+ @t_help.skipUnlessJsonSchema()
+ def test_schema_validation(self, config, error_msg):
+ with pytest.raises(SchemaValidationError, match=error_msg):
+ validate_cloudconfig_schema(config, get_schema(), strict=True)
+
+
# vi: ts=4 expandtab
diff --git a/tests/unittests/config/test_schema.py b/tests/unittests/config/test_schema.py
index a8cd276a..ee00a88c 100644
--- a/tests/unittests/config/test_schema.py
+++ b/tests/unittests/config/test_schema.py
@@ -82,6 +82,13 @@ def get_module_variable(var_name) -> dict:
class TestGetSchema:
+ def test_static_schema_file_is_valid(self, caplog):
+ with caplog.at_level(logging.WARNING):
+ get_schema()
+ # Assert no warnings parsing our packaged schema file
+ warnings = [msg for (_, _, msg) in caplog.record_tuples]
+ assert [] == warnings
+
def test_get_schema_coalesces_known_schema(self):
"""Every cloudconfig module with schema is listed in allOf keyword."""
schema = get_schema()
@@ -99,7 +106,10 @@ class TestGetSchema:
"cc_disk_setup",
"cc_install_hotplug",
"cc_keyboard",
+ "cc_keys_to_console",
+ "cc_landscape",
"cc_locale",
+ "cc_lxd",
"cc_ntp",
"cc_package_update_upgrade_install",
"cc_phone_home",
@@ -135,6 +145,11 @@ class TestGetSchema:
{"$ref": "#/$defs/cc_debug"},
{"$ref": "#/$defs/cc_disable_ec2_metadata"},
{"$ref": "#/$defs/cc_disk_setup"},
+ {"$ref": "#/$defs/cc_keyboard"},
+ {"$ref": "#/$defs/cc_keys_to_console"},
+ {"$ref": "#/$defs/cc_landscape"},
+ {"$ref": "#/$defs/cc_locale"},
+ {"$ref": "#/$defs/cc_lxd"},
{"$ref": "#/$defs/cc_package_update_upgrade_install"},
{"$ref": "#/$defs/cc_phone_home"},
{"$ref": "#/$defs/cc_power_state_change"},
@@ -158,9 +173,6 @@ class TestGetSchema:
# This list will dwindle as we move legacy schema to new $defs
assert [
"drivers",
- "keyboard",
- "locale",
- "locale_configfile",
"ntp",
"snap",
"ubuntu_advantage",