diff --git a/cloudinit/config/cc_scripts_per_boot.py b/cloudinit/config/cc_scripts_per_boot.py index b7bfb7aa309..aa311d595a5 100644 --- a/cloudinit/config/cc_scripts_per_boot.py +++ b/cloudinit/config/cc_scripts_per_boot.py @@ -5,29 +5,34 @@ # Author: Juerg Haefliger # # This file is part of cloud-init. See LICENSE file for license information. +"""Scripts Per Boot: Run per boot scripts""" -""" -Scripts Per Boot ----------------- -**Summary:** run per boot scripts +import os +from cloudinit import subp +from cloudinit.config.schema import MetaSchema, get_meta_doc +from cloudinit.distros import ALL_DISTROS +from cloudinit.settings import PER_ALWAYS + +frequency = PER_ALWAYS +MODULE_DESCRIPTION = """\ Any scripts in the ``scripts/per-boot`` directory on the datasource will be run every time the system boots. Scripts will be run in alphabetical order. This module does not accept any config keys. - -**Internal name:** ``cc_scripts_per_boot`` - -**Module frequency:** always - -**Supported distros:** all """ -import os -from cloudinit import subp -from cloudinit.settings import PER_ALWAYS +meta: MetaSchema = { + "id": "cc_scripts_per_boot", + "name": "Scripts Per Boot", + "title": "Run per boot scripts", + "description": MODULE_DESCRIPTION, + "distros": [ALL_DISTROS], + "frequency": frequency, + "examples": [], +} -frequency = PER_ALWAYS +__doc__ = get_meta_doc(meta) SCRIPT_SUBDIR = "per-boot" diff --git a/cloudinit/config/cc_scripts_per_instance.py b/cloudinit/config/cc_scripts_per_instance.py index ef102b1ce44..1fb407175f7 100644 --- a/cloudinit/config/cc_scripts_per_instance.py +++ b/cloudinit/config/cc_scripts_per_instance.py @@ -5,32 +5,36 @@ # Author: Juerg Haefliger # # This file is part of cloud-init. See LICENSE file for license information. +"""Scripts Per Instance: Run per instance scripts""" -""" -Scripts Per Instance --------------------- -**Summary:** run per instance scripts +import os + +from cloudinit import subp +from cloudinit.config.schema import MetaSchema, get_meta_doc +from cloudinit.distros import ALL_DISTROS +from cloudinit.settings import PER_INSTANCE +MODULE_DESCRIPTION = """\ Any scripts in the ``scripts/per-instance`` directory on the datasource will be run when a new instance is first booted. Scripts will be run in alphabetical order. This module does not accept any config keys. Some cloud platforms change instance-id if a significant change was made to the system. As a result per-instance scripts will run again. - -**Internal name:** ``cc_scripts_per_instance`` - -**Module frequency:** per instance - -**Supported distros:** all """ -import os +meta: MetaSchema = { + "id": "cc_scripts_per_instance", + "name": "Scripts Per Instance", + "title": "Run per instance scripts", + "description": MODULE_DESCRIPTION, + "distros": [ALL_DISTROS], + "frequency": PER_INSTANCE, + "examples": [], +} -from cloudinit import subp -from cloudinit.settings import PER_INSTANCE +__doc__ = get_meta_doc(meta) -frequency = PER_INSTANCE SCRIPT_SUBDIR = "per-instance" diff --git a/cloudinit/config/cc_scripts_per_once.py b/cloudinit/config/cc_scripts_per_once.py index bf4231e790b..d9f406b7d53 100644 --- a/cloudinit/config/cc_scripts_per_once.py +++ b/cloudinit/config/cc_scripts_per_once.py @@ -5,30 +5,34 @@ # Author: Juerg Haefliger # # This file is part of cloud-init. See LICENSE file for license information. +"""Scripts Per Once: Run one time scripts""" -""" -Scripts Per Once ----------------- -**Summary:** run one time scripts +import os + +from cloudinit import subp +from cloudinit.config.schema import MetaSchema, get_meta_doc +from cloudinit.distros import ALL_DISTROS +from cloudinit.settings import PER_ONCE +frequency = PER_ONCE +MODULE_DESCRIPTION = """\ Any scripts in the ``scripts/per-once`` directory on the datasource will be run only once. Changes to the instance will not force a re-run. The only way to re-run these scripts is to run the clean subcommand and reboot. Scripts will be run in alphabetical order. This module does not accept any config keys. - -**Internal name:** ``cc_scripts_per_once`` - -**Module frequency:** per once - -**Supported distros:** all """ -import os - -from cloudinit import subp -from cloudinit.settings import PER_ONCE - -frequency = PER_ONCE +meta: MetaSchema = { + "id": "cc_scripts_per_once", + "name": "Scripts Per Once", + "title": "Run one time scripts", + "description": MODULE_DESCRIPTION, + "distros": [ALL_DISTROS], + "frequency": frequency, + "examples": [], +} + +__doc__ = get_meta_doc(meta) SCRIPT_SUBDIR = "per-once" diff --git a/cloudinit/config/cc_scripts_user.py b/cloudinit/config/cc_scripts_user.py index e0d6c56021b..85375dac375 100644 --- a/cloudinit/config/cc_scripts_user.py +++ b/cloudinit/config/cc_scripts_user.py @@ -5,32 +5,36 @@ # Author: Juerg Haefliger # # This file is part of cloud-init. See LICENSE file for license information. +"""Scripts User: Run user scripts""" -""" -Scripts User ------------- -**Summary:** run user scripts +import os + +from cloudinit import subp +from cloudinit.config.schema import MetaSchema, get_meta_doc +from cloudinit.distros import ALL_DISTROS +from cloudinit.settings import PER_INSTANCE +MODULE_DESCRIPTION = """\ This module runs all user scripts. User scripts are not specified in the ``scripts`` directory in the datasource, but rather are present in the ``scripts`` dir in the instance configuration. Any cloud-config parts with a ``#!`` will be treated as a script and run. Scripts specified as cloud-config parts will be run in the order they are specified in the configuration. This module does not accept any config keys. - -**Internal name:** ``cc_scripts_user`` - -**Module frequency:** per instance - -**Supported distros:** all """ -import os +meta: MetaSchema = { + "id": "cc_scripts_user", + "name": "Scripts User", + "title": "Run user scripts", + "description": MODULE_DESCRIPTION, + "distros": [ALL_DISTROS], + "frequency": PER_INSTANCE, + "examples": [], +} -from cloudinit import subp -from cloudinit.settings import PER_INSTANCE +__doc__ = get_meta_doc(meta) -frequency = PER_INSTANCE SCRIPT_SUBDIR = "scripts" diff --git a/cloudinit/config/cc_scripts_vendor.py b/cloudinit/config/cc_scripts_vendor.py index 1b30fa1b3de..894404f8eef 100644 --- a/cloudinit/config/cc_scripts_vendor.py +++ b/cloudinit/config/cc_scripts_vendor.py @@ -3,35 +3,59 @@ # Author: Ben Howard # # This file is part of cloud-init. See LICENSE file for license information. - -""" -Scripts Vendor --------------- -**Summary:** run vendor scripts - -Any scripts in the ``scripts/vendor`` directory in the datasource will be run -when a new instance is first booted. Scripts will be run in alphabetical order. -Vendor scripts can be run with an optional prefix specified in the ``prefix`` -entry under the ``vendor_data`` config key. - -**Internal name:** ``cc_scripts_vendor`` - -**Module frequency:** per instance - -**Supported distros:** all - -**Config keys**:: - - vendor_data: - prefix: -""" +"""Scripts Vendor: Run vendor scripts""" import os +from textwrap import dedent from cloudinit import subp, util +from cloudinit.config.schema import MetaSchema, get_meta_doc +from cloudinit.distros import ALL_DISTROS from cloudinit.settings import PER_INSTANCE -frequency = PER_INSTANCE +MODULE_DESCRIPTION = """\ +On select Datasources, vendors have a channel for the consumption +of all supported user data types via a special channel called +vendor data. Any scripts in the ``scripts/vendor`` directory in the datasource +will be run when a new instance is first booted. Scripts will be run in +alphabetical order. This module allows control over the execution of +vendor data. +""" + +meta: MetaSchema = { + "id": "cc_scripts_vendor", + "name": "Scripts Vendor", + "title": "Run vendor scripts", + "description": MODULE_DESCRIPTION, + "distros": [ALL_DISTROS], + "frequency": PER_INSTANCE, + "examples": [ + dedent( + """\ + vendor_data: + enabled: true + prefix: /usr/bin/ltrace + """ + ), + dedent( + """\ + vendor_data: + enabled: true + prefix: [timeout, 30] + """ + ), + dedent( + """\ + # Vendor data will not be processed + vendor_data: + enabled: false + """ + ), + ], +} + +__doc__ = get_meta_doc(meta) + SCRIPT_SUBDIR = "vendor" diff --git a/cloudinit/config/cc_seed_random.py b/cloudinit/config/cc_seed_random.py index 67ba8ef5079..b0ffdd1573a 100644 --- a/cloudinit/config/cc_seed_random.py +++ b/cloudinit/config/cc_seed_random.py @@ -6,73 +6,72 @@ # Author: Scott Moser # # This file is part of cloud-init. See LICENSE file for license information. +"""Seed Random: Provide random seed data""" -""" -Seed Random ------------ -**Summary:** provide random seed data +import base64 +import os +from io import BytesIO +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.distros import ALL_DISTROS +from cloudinit.settings import PER_INSTANCE + +LOG = logging.getLogger(__name__) -Since all cloud instances started from the same image will produce very similar -data when they are first booted, as they are all starting with the same seed +MODULE_DESCRIPTION = """\ +All cloud instances started from the same image will produce very similar +data when they are first booted as they are all starting with the same seed for the kernel's entropy keyring. To avoid this, random seed data can be provided to the instance either as a string or by specifying a command to run to generate the data. -Configuration for this module is under the ``random_seed`` config key. The -``file`` key specifies the path to write the data to, defaulting to -``/dev/urandom``. Data can be passed in directly with ``data``, and may -optionally be specified in encoded form, with the encoding specified in -``encoding``. - -If the cloud provides its own random seed data, it will be appended to ``data`` +Configuration for this module is under the ``random_seed`` config key. If +the cloud provides its own random seed data, it will be appended to ``data`` before it is written to ``file``. -.. note:: - when using a multiline value for ``data`` or specifying binary data, be - sure to follow yaml syntax and use the ``|`` and ``!binary`` yaml format - specifiers when appropriate - If the ``command`` key is specified, the given command will be executed. This will happen after ``file`` has been populated. That command's environment will contain the value of the ``file`` key as ``RANDOM_SEED_FILE``. If a command is specified that cannot be run, no error will be reported unless ``command_required`` is set to true. - -For example, to use ``pollinate`` to gather data from a -remote entropy server and write it to ``/dev/urandom``, the following could be -used:: - - random_seed: - file: /dev/urandom - command: ["pollinate", "--server=http://local.polinate.server"] - command_required: true - -**Internal name:** ``cc_seed_random`` - -**Module frequency:** per instance - -**Supported distros:** all - -**Config keys**:: - - random_seed: - file: - data: - encoding: - command: [, , ...] - command_required: """ -import base64 -import os -from io import BytesIO - -from cloudinit import log as logging -from cloudinit import subp, util -from cloudinit.settings import PER_INSTANCE - -frequency = PER_INSTANCE -LOG = logging.getLogger(__name__) +meta: MetaSchema = { + "id": "cc_seed_random", + "name": "Seed Random", + "title": "Provide random seed data", + "description": MODULE_DESCRIPTION, + "distros": [ALL_DISTROS], + "frequency": PER_INSTANCE, + "examples": [ + dedent( + """\ + random_seed: + file: /dev/urandom + data: my random string + encoding: raw + command: ['sh', '-c', 'dd if=/dev/urandom of=$RANDOM_SEED_FILE'] + command_required: true + """ + ), + dedent( + """\ + # To use 'pollinate' to gather data from a remote entropy + # server and write it to '/dev/urandom', the following + # could be used: + random_seed: + file: /dev/urandom + command: ["pollinate", "--server=http://local.polinate.server"] + command_required: true + """ + ), + ], +} + +__doc__ = get_meta_doc(meta) def _decode(data, encoding=None): diff --git a/cloudinit/config/cc_set_hostname.py b/cloudinit/config/cc_set_hostname.py index eb0ca32852d..f78df7b8bf8 100644 --- a/cloudinit/config/cc_set_hostname.py +++ b/cloudinit/config/cc_set_hostname.py @@ -5,24 +5,36 @@ # Author: Juerg Haefliger # # This file is part of cloud-init. See LICENSE file for license information. +"""Set Hostname: Set hostname and FQDN""" -""" -Set Hostname ------------- -**Summary:** set hostname and fqdn - -This module handles setting the system hostname and fqdn. If -``preserve_hostname`` is set, then the hostname will not be altered. +import os +from textwrap import dedent -A hostname and fqdn can be provided by specifying a full domain name under the -``fqdn`` key. Alternatively, a hostname can be specified using the ``hostname`` -key, and the fqdn of the cloud wil be used. If a fqdn specified with the +from cloudinit import util +from cloudinit.atomic_helper import write_json +from cloudinit.config.schema import MetaSchema, get_meta_doc +from cloudinit.distros import ALL_DISTROS +from cloudinit.settings import PER_ALWAYS + +frequency = PER_ALWAYS +MODULE_DESCRIPTION = """\ +This module handles setting the system hostname and fully qualified domain +name (FQDN). If ``preserve_hostname`` is set, then the hostname will not be +altered. + +A hostname and FQDN can be provided by specifying a full domain name under the +``FQDN`` key. Alternatively, a hostname can be specified using the ``hostname`` +key, and the FQDN of the cloud will be used. If a FQDN specified with the ``hostname`` key, it will be handled properly, although it is better to use the ``fqdn`` config key. If both ``fqdn`` and ``hostname`` are set, -it is distro dependent whether ``hostname`` or ``fqdn`` is used, -unless the ``prefer_fqdn_over_hostname`` option is true and fqdn is set -it will force the use of FQDN in all distros, and if false then it will -force the hostname use. +the ``prefer_fqdn_over_hostname`` will force the use of FQDN in all distros +when true, and when false it will force the short hostname. Otherwise, the +hostname to use is distro-dependent. + +.. note:: + cloud-init performs no hostname input validation before sending the + hostname to distro-specific tools, and most tools will not accept a + trailing dot on the FQDN. This module will run in the init-local stage before networking is configured if the hostname is set by metadata or user data on the local system. @@ -31,25 +43,28 @@ data are available locally. This ensures that the desired hostname is applied before any DHCP requests are preformed on these platforms where dynamic DNS is based on initial hostname. - -**Internal name:** ``cc_set_hostname`` - -**Module frequency:** always - -**Supported distros:** all - -**Config keys**:: - - preserve_hostname: - prefer_fqdn_over_hostname: - fqdn: - hostname: """ -import os - -from cloudinit import util -from cloudinit.atomic_helper import write_json +meta: MetaSchema = { + "id": "cc_set_hostname", + "name": "Set Hostname", + "title": "Set hostname and FQDN", + "description": MODULE_DESCRIPTION, + "distros": [ALL_DISTROS], + "frequency": frequency, + "examples": [ + "perserve_hostname: true", + dedent( + """\ + hostname: myhost + fqdn: myhost.example.com + prefer_fqdn_over_hostname: true + """ + ), + ], +} + +__doc__ = get_meta_doc(meta) class SetHostnameError(Exception): diff --git a/cloudinit/config/cc_set_passwords.py b/cloudinit/config/cc_set_passwords.py index d8df8e23a5f..939e773bab8 100755 --- a/cloudinit/config/cc_set_passwords.py +++ b/cloudinit/config/cc_set_passwords.py @@ -5,85 +5,73 @@ # Author: Juerg Haefliger # # This file is part of cloud-init. See LICENSE file for license information. +"""Set Passwords: Set user passwords and enable/disable SSH password auth""" -""" -Set Passwords -------------- -**Summary:** Set user passwords and enable/disable SSH password authentication +import re +from string import ascii_letters, digits +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.distros import ALL_DISTROS, ug_util +from cloudinit.settings import PER_INSTANCE +from cloudinit.ssh_util import update_ssh_config +MODULE_DESCRIPTION = """\ This module consumes three top-level config keys: ``ssh_pwauth``, ``chpasswd`` and ``password``. The ``ssh_pwauth`` config key determines whether or not sshd will be configured -to accept password authentication. True values will enable password auth, -false values will disable password auth, and the literal string ``unchanged`` -will leave it unchanged. Setting no value will also leave the current setting -on-disk unchanged. +to accept password authentication. The ``chpasswd`` config key accepts a dictionary containing either or both of -``expire`` and ``list``. - -If the ``list`` key is provided, it should contain a list of -``username:password`` pairs. This can be either a YAML list (of strings), or a -multi-line string with one pair per line. Each user will have the -corresponding password set. A password can be randomly generated by specifying -``RANDOM`` or ``R`` as a user's password. A hashed password, created by a tool -like ``mkpasswd``, can be specified; a regex -(``r'\\$(1|2a|2y|5|6)(\\$.+){2}'``) is used to determine if a password value -should be treated as a hash. - -.. note:: - The users specified must already exist on the system. Users will have been - created by the ``cc_users_groups`` module at this point. - -By default, all users on the system will have their passwords expired (meaning -that they will have to be reset the next time the user logs in). To disable -this behaviour, set ``expire`` under ``chpasswd`` to a false value. - -If a ``list`` of user/password pairs is not specified under ``chpasswd``, then -the value of the ``password`` config key will be used to set the default user's -password. - -**Internal name:** ``cc_set_passwords`` - -**Module frequency:** per instance - -**Supported distros:** all - -**Config keys**:: - - ssh_pwauth: - - password: password1 - chpasswd: - expire: - - chpasswd: - list: | - user1:password1 - user2:RANDOM - user3:password3 - user4:R - - ## - # or as yaml list - ## - chpasswd: - list: - - user1:password1 - - user2:RANDOM - - user3:password3 - - user4:R - - user4:$6$rL..$ej... -""" +``list`` and ``expire``. The ``list`` key is used to assign a password to a +to a corresponding pre-existing user. The ``expire`` key is used to set +whether to expire all user passwords such that a password will need to be reset +on the user's next login. -import re -from string import ascii_letters, digits +``password`` config key is used to set the default user's password. It is +ignored if the ``chpasswd`` ``list`` is used. +""" -from cloudinit import log as logging -from cloudinit import subp, util -from cloudinit.distros import ug_util -from cloudinit.ssh_util import update_ssh_config +meta: MetaSchema = { + "id": "cc_set_passwords", + "name": "Set Passwords", + "title": "Set user passwords and enable/disable SSH password auth", + "description": MODULE_DESCRIPTION, + "distros": [ALL_DISTROS], + "frequency": PER_INSTANCE, + "examples": [ + dedent( + """\ + # Set a default password that would need to be changed + # at first login + ssh_pwauth: true + password: password1 + """ + ), + dedent( + """\ + # Disable ssh password authentication + # Don't require users to change their passwords on next login + # Set the password for user1 to be 'password1' (OS does hashing) + # Set the password for user2 to be a randomly generated password, + # which will be written to the system console + # Set the password for user3 to a pre-hashed password + ssh_pwauth: false + chpasswd: + expire: false + list: + - user1:password1 + - user2:RANDOM + - user3:$6$rounds=4096$5DJ8a9WMTEzIo5J4$Yms6imfeBvf3Yfu84mQBerh18l7OR1Wm1BJXZqFSpJ6BVas0AYJqIjP7czkOaAZHZi1kxQ5Y1IhgWN8K9NgxR1 + """ # noqa + ), + ], +} + +__doc__ = get_meta_doc(meta) LOG = logging.getLogger(__name__) @@ -101,6 +89,12 @@ def handle_ssh_pwauth(pw_auth, distro): @return: None""" cfg_name = "PasswordAuthentication" + if isinstance(pw_auth, str): + LOG.warning( + "DEPRECATION: The 'ssh_pwauth' config key should be set to " + "a boolean value. The string format is deprecated and will be " + "removed in a future version of cloud-init." + ) if util.is_true(pw_auth): cfg_val = "yes" elif util.is_false(pw_auth): @@ -141,6 +135,11 @@ def handle(_name, cfg, cloud, log, args): log.debug("Handling input for chpasswd as list.") plist = util.get_cfg_option_list(chfg, "list", plist) else: + log.warning( + "DEPRECATION: The chpasswd multiline string format is " + "deprecated and will be removed from a future version of " + "cloud-init. Use the list format instead." + ) log.debug("Handling input for chpasswd as multiline string.") plist = util.get_cfg_option_str(chfg, "list", plist) if plist: diff --git a/cloudinit/config/cc_snap.py b/cloudinit/config/cc_snap.py index 9f343df0f75..fddca899c50 100644 --- a/cloudinit/config/cc_snap.py +++ b/cloudinit/config/cc_snap.py @@ -9,11 +9,7 @@ from cloudinit import log as logging from cloudinit import subp, 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 from cloudinit.subp import prepend_base_command @@ -54,10 +50,6 @@ best to create a snap seed directory and seed.yaml manifest in **/var/lib/snapd/seed/** which snapd automatically installs on startup. - - **Development only**: The ``squashfuse_in_container`` boolean can be - set true to install squashfuse package when in a container to enable - snap installs. Default is false. """ ), "distros": distros, @@ -74,16 +66,6 @@ 00: snap create-user --sudoer --known @mydomain.com 01: snap install canonical-livepatch 02: canonical-livepatch enable - """ - ), - dedent( - """\ - # LXC-based containers require squashfuse before snaps can be installed - snap: - commands: - 00: apt-get install squashfuse -y - 11: snap install emoj - """ ), dedent( @@ -92,80 +74,40 @@ # as a list and 'snap' will automatically be prepended. # The following commands are equivalent: snap: - commands: - 00: ['install', 'vlc'] - 01: ['snap', 'install', 'vlc'] - 02: snap install vlc - 03: 'snap install vlc' + commands: + 00: ['install', 'vlc'] + 01: ['snap', 'install', 'vlc'] + 02: snap install vlc + 03: 'snap install vlc' """ ), dedent( """\ # You can use a list of commands snap: - commands: - - ['install', 'vlc'] - - ['snap', 'install', 'vlc'] - - snap install vlc - - 'snap install vlc' + commands: + - ['install', 'vlc'] + - ['snap', 'install', 'vlc'] + - snap install vlc + - 'snap install vlc' """ ), dedent( """\ # You can use a list of assertions snap: - assertions: - - signed_assertion_blob_here - - | - signed_assertion_blob_here + assertions: + - signed_assertion_blob_here + - | + signed_assertion_blob_here """ ), ], "frequency": PER_INSTANCE, } -schema = { - "type": "object", - "properties": { - "snap": { - "type": "object", - "properties": { - "assertions": { - "type": ["object", "array"], # Array of strings or dict - "items": {"type": "string"}, - "additionalItems": False, # Reject items non-string - "minItems": 1, - "minProperties": 1, - "uniqueItems": True, - "additionalProperties": {"type": "string"}, - }, - "commands": { - "type": ["object", "array"], # Array of strings or dict - "items": { - "oneOf": [ - {"type": "array", "items": {"type": "string"}}, - {"type": "string"}, - ] - }, - "additionalItems": False, # Reject non-string & non-list - "minItems": 1, - "minProperties": 1, - "additionalProperties": { - "oneOf": [ - {"type": "string"}, - {"type": "array", "items": {"type": "string"}}, - ], - }, - }, - "squashfuse_in_container": {"type": "boolean"}, - }, - "additionalProperties": False, # Reject keys not in schema - "minProperties": 1, - } - }, -} -__doc__ = get_meta_doc(meta, schema) # Supplement python help() +__doc__ = get_meta_doc(meta) SNAP_CMD = "snap" ASSERTIONS_FILE = "/var/lib/cloud/instance/snapd.assertions" @@ -265,7 +207,6 @@ def handle(name, cfg, cloud, log, args): ) return - validate_cloudconfig_schema(cfg, schema) if util.is_true(cfgin.get("squashfuse_in_container", False)): maybe_install_squashfuse(cloud) add_assertions(cfgin.get("assertions", [])) diff --git a/cloudinit/config/cc_spacewalk.py b/cloudinit/config/cc_spacewalk.py index 3fa6c388c25..d319efef010 100644 --- a/cloudinit/config/cc_spacewalk.py +++ b/cloudinit/config/cc_spacewalk.py @@ -1,10 +1,14 @@ # This file is part of cloud-init. See LICENSE file for license information. +"""Spacewalk: Install and configure spacewalk""" -""" -Spacewalk ---------- -**Summary:** install and configure spacewalk +from textwrap import dedent + +from cloudinit import subp +from cloudinit.config.schema import MetaSchema, get_meta_doc +from cloudinit.settings import PER_INSTANCE +distros = ["redhat", "fedora"] +MODULE_DESCRIPTION = """\ This module installs spacewalk and applies basic configuration. If the ``spacewalk`` config key is present spacewalk will be installed. The server to connect to after installation must be provided in the ``server`` in spacewalk @@ -12,22 +16,29 @@ be specified. For more information about spacewalk see: https://fedorahosted.org/spacewalk/ +""" -**Internal name:** ``cc_spacewalk`` - -**Module frequency:** per instance - -**Supported distros:** redhat, fedora +meta: MetaSchema = { + "id": "cc_spacewalk", + "name": "Spacewalk", + "title": "Install and configure spacewalk", + "description": MODULE_DESCRIPTION, + "distros": distros, + "frequency": PER_INSTANCE, + "examples": [ + dedent( + """\ + spacewalk: + server: + proxy: + activation_key: + """ + ) + ], +} -**Config keys**:: +__doc__ = get_meta_doc(meta) - spacewalk: - server: - proxy: - activation_key: -""" - -from cloudinit import subp distros = ["redhat", "fedora"] required_packages = ["rhn-setup"] diff --git a/cloudinit/config/cc_ssh.py b/cloudinit/config/cc_ssh.py index 64486b9c1e4..ac5640b709d 100755 --- a/cloudinit/config/cc_ssh.py +++ b/cloudinit/config/cc_ssh.py @@ -5,12 +5,19 @@ # Author: Juerg Haefliger # # This file is part of cloud-init. See LICENSE file for license information. +"""SSH: Configure SSH and SSH keys""" -""" -SSH ---- -**Summary:** configure SSH and SSH keys (host and authorized) +import glob +import os +import sys +from textwrap import dedent +from cloudinit import ssh_util, subp, util +from cloudinit.config.schema import MetaSchema, get_meta_doc +from cloudinit.distros import ALL_DISTROS, ug_util +from cloudinit.settings import PER_INSTANCE + +MODULE_DESCRIPTION = """\ This module handles most configuration for SSH and both host and authorized SSH keys. @@ -28,12 +35,7 @@ password authentication Root login can be enabled/disabled using the ``disable_root`` config key. Root -login options can be manually specified with ``disable_root_opts``. If -``disable_root_opts`` is specified and contains the string ``$USER``, -it will be replaced with the username of the default user. By default, -root login is disabled, and root login opts are set to:: - - no-port-forwarding,no-agent-forwarding,no-X11-forwarding +login options can be manually specified with ``disable_root_opts``. Supported public key types for the ``ssh_authorized_keys`` are: @@ -75,32 +77,18 @@ ^^^^^^^^^ Host keys are for authenticating a specific instance. Many images have default -host SSH keys, which can be removed using ``ssh_deletekeys``. This prevents -re-use of a private host key from an image on multiple machines. Since -removing default host keys is usually the desired behavior this option is -enabled by default. - -Host keys can be added using the ``ssh_keys`` configuration key. The argument -to this config key should be a dictionary entries for the public and private -keys of each desired key type. Entries in the ``ssh_keys`` config dict should -have keys in the format ``_private``, ``_public``, and, -optionally, ``_certificate``, e.g. ``rsa_private: ``, -``rsa_public: ``, and ``rsa_certificate: ``. See below for supported -key types. Not all key types have to be specified, ones left unspecified will -not be used. If this config option is used, then no keys will be generated. +host SSH keys, which can be removed using ``ssh_deletekeys``. + +Host keys can be added using the ``ssh_keys`` configuration key. When host keys are generated the output of the ssh-keygen command(s) can be displayed on the console using the ``ssh_quiet_keygen`` configuration key. -This settings defaults to False which displays the keygen output. .. note:: when specifying private host keys in cloud-config, care should be taken to ensure that the communication between the data source and the instance is secure -.. note:: - to specify multiline private host keys and certificates, use yaml - multiline syntax If no host keys are specified using ``ssh_keys``, then keys will be generated using ``ssh-keygen``. By default one public/private pair of each supported @@ -113,57 +101,59 @@ Supported host key types for the ``ssh_keys`` and the ``ssh_genkeytypes`` config flags are: - - rsa - dsa - ecdsa + - ecdsa-sk - ed25519 - -**Internal name:** ``cc_ssh`` - -**Module frequency:** per instance - -**Supported distros:** all - -**Config keys**:: - - ssh_deletekeys: - ssh_keys: - rsa_private: | - -----BEGIN RSA PRIVATE KEY----- - MIIBxwIBAAJhAKD0YSHy73nUgysO13XsJmd4fHiFyQ+00R7VVu2iV9Qco - ... - -----END RSA PRIVATE KEY----- - rsa_public: ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAGEAoPRhIfLvedSDKw7Xd ... - rsa_certificate: | - ssh-rsa-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQt ... - dsa_private: | - -----BEGIN DSA PRIVATE KEY----- - MIIBxwIBAAJhAKD0YSHy73nUgysO13XsJmd4fHiFyQ+00R7VVu2iV9Qco - ... - -----END DSA PRIVATE KEY----- - dsa_public: ssh-dsa AAAAB3NzaC1yc2EAAAABIwAAAGEAoPRhIfLvedSDKw7Xd ... - dsa_certificate: | - ssh-dsa-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQt ... - - ssh_genkeytypes: - disable_root: - disable_root_opts: - ssh_authorized_keys: - - ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAGEA3FSyQwBI6Z+nCSjUU ... - - ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA3I7VUf2l5gSn5uavROsc5HRDpZ ... - allow_public_ssh_keys: - ssh_publish_hostkeys: - enabled: (Defaults to true) - blacklist: (Defaults to [dsa]) - ssh_quiet_keygen: + - ed25519-sk + - rsa """ -import glob -import os -import sys +meta: MetaSchema = { + "id": "cc_ssh", + "name": "SSH", + "title": "Configure SSH and SSH keys", + "description": MODULE_DESCRIPTION, + "distros": [ALL_DISTROS], + "frequency": PER_INSTANCE, + "examples": [ + dedent( + """\ + ssh_keys: + rsa_private: | + -----BEGIN RSA PRIVATE KEY----- + MIIBxwIBAAJhAKD0YSHy73nUgysO13XsJmd4fHiFyQ+00R7VVu2iV9Qco + ... + -----END RSA PRIVATE KEY----- + rsa_public: ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAGEAoPRhIfLvedSDKw7Xd ... + rsa_certificate: | + ssh-rsa-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQt ... + dsa_private: | + -----BEGIN DSA PRIVATE KEY----- + MIIBxwIBAAJhAKD0YSHy73nUgysO13XsJmd4fHiFyQ+00R7VVu2iV9Qco + ... + -----END DSA PRIVATE KEY----- + dsa_public: ssh-dsa AAAAB3NzaC1yc2EAAAABIwAAAGEAoPRhIfLvedSDKw7Xd ... + dsa_certificate: | + ssh-dsa-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQt ... + ssh_authorized_keys: + - ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAGEA3FSyQwBI6Z+nCSjUU ... + - ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA3I7VUf2l5gSn5uavROsc5HRDpZ ... + ssh_deletekeys: true + ssh_genkeytypes: [rsa, dsa, ecdsa, ed25519] + disable_root: true + disable_root_opts: no-port-forwarding,no-agent-forwarding,no-X11-forwarding + allow_public_ssh_keys: true + ssh_quiet_keygen: true + ssh_publish_hostkeys: + enabled: true + blacklist: [dsa] + """ # noqa: E501 + ) + ], +} -from cloudinit import ssh_util, subp, util -from cloudinit.distros import ug_util +__doc__ = get_meta_doc(meta) GENERATE_KEY_NAMES = ["rsa", "dsa", "ecdsa", "ed25519"] KEY_FILE_TPL = "/etc/ssh/ssh_host_%s_key" diff --git a/cloudinit/config/cc_ssh_authkey_fingerprints.py b/cloudinit/config/cc_ssh_authkey_fingerprints.py index e4a3640a073..c2d3b0d1c55 100755 --- a/cloudinit/config/cc_ssh_authkey_fingerprints.py +++ b/cloudinit/config/cc_ssh_authkey_fingerprints.py @@ -3,34 +3,37 @@ # Author: Joshua Harlow # # This file is part of cloud-init. See LICENSE file for license information. +"""SSH AuthKey Fingerprints: Log fingerprints of user SSH keys""" -""" -SSH Authkey Fingerprints ------------------------- -**Summary:** log fingerprints of user SSH keys +import base64 +import hashlib + +from cloudinit import ssh_util, util +from cloudinit.config.schema import MetaSchema, get_meta_doc +from cloudinit.distros import ALL_DISTROS, ug_util +from cloudinit.settings import PER_INSTANCE +from cloudinit.simpletable import SimpleTable +MODULE_DESCRIPTION = """\ Write fingerprints of authorized keys for each user to log. This is enabled by default, but can be disabled using ``no_ssh_fingerprints``. The hash type for the keys can be specified, but defaults to ``sha256``. - -**Internal name:** ``cc_ssh_authkey_fingerprints`` - -**Module frequency:** per instance - -**Supported distros:** all - -**Config keys**:: - - no_ssh_fingerprints: - authkey_hash: """ -import base64 -import hashlib - -from cloudinit import ssh_util, util -from cloudinit.distros import ug_util -from cloudinit.simpletable import SimpleTable +meta: MetaSchema = { + "id": "cc_ssh_authkey_fingerprints", + "name": "SSH AuthKey Fingerprints", + "title": "Log fingerprints of user SSH keys", + "description": MODULE_DESCRIPTION, + "distros": [ALL_DISTROS], + "frequency": PER_INSTANCE, + "examples": [ + "no_ssh_fingerprints: true", + "authkey_hash: sha512", + ], +} + +__doc__ = get_meta_doc(meta) def _split_hash(bin_hash): diff --git a/cloudinit/config/cc_ssh_import_id.py b/cloudinit/config/cc_ssh_import_id.py index a54e8e47182..e5864878387 100755 --- a/cloudinit/config/cc_ssh_import_id.py +++ b/cloudinit/config/cc_ssh_import_id.py @@ -5,39 +5,47 @@ # Author: Juerg Haefliger # # This file is part of cloud-init. See LICENSE file for license information. - -""" -SSH Import Id -------------- -**Summary:** import SSH id - -This module imports SSH keys from either a public keyserver, usually launchpad -or github using ``ssh-import-id``. Keys are referenced by the username they are -associated with on the keyserver. The keyserver can be specified by prepending -either ``lp:`` for launchpad or ``gh:`` for github to the username. - -**Internal name:** ``cc_ssh_import_id`` - -**Module frequency:** per instance - -**Supported distros:** ubuntu, debian - -**Config keys**:: - - ssh_import_id: - - user - - gh:user - - lp:user -""" +"""SSH Import ID: Import SSH id""" import pwd +from textwrap import dedent from cloudinit import subp, util +from cloudinit.config.schema import MetaSchema, get_meta_doc from cloudinit.distros import ug_util +from cloudinit.settings import PER_INSTANCE # https://launchpad.net/ssh-import-id distros = ["ubuntu", "debian"] +MODULE_DESCRIPTION = """\ +This module imports SSH keys from either a public keyserver, usually launchpad +or github using ``ssh-import-id``. Keys are referenced by the username they are +associated with on the keyserver. The keyserver can be specified by prepending +either ``lp:`` for launchpad or ``gh:`` for github to the username. +""" + +meta: MetaSchema = { + "id": "cc_ssh_import_id", + "name": "SSH Import ID", + "title": "Import SSH id", + "description": MODULE_DESCRIPTION, + "distros": distros, + "frequency": PER_INSTANCE, + "examples": [ + dedent( + """\ + ssh_import_id: + - user + - gh:user + - lp:user + """ + ) + ], +} + +__doc__ = get_meta_doc(meta) + def handle(_name, cfg, cloud, log, args): diff --git a/cloudinit/config/cc_timezone.py b/cloudinit/config/cc_timezone.py index 24e6099eedc..47da2d06549 100644 --- a/cloudinit/config/cc_timezone.py +++ b/cloudinit/config/cc_timezone.py @@ -5,31 +5,30 @@ # Author: Juerg Haefliger # # This file is part of cloud-init. See LICENSE file for license information. - -""" -Timezone --------- -**Summary:** set system timezone - -Set the system timezone. If any args are passed to the module then the first -will be used for the timezone. Otherwise, the module will attempt to retrieve -the timezone from cloud config. - -**Internal name:** ``cc_timezone`` - -**Module frequency:** per instance - -**Supported distros:** all - -**Config keys**:: - - timezone: -""" +"""Timezone: Set the system timezone""" from cloudinit import util +from cloudinit.config.schema import MetaSchema, get_meta_doc +from cloudinit.distros import ALL_DISTROS from cloudinit.settings import PER_INSTANCE -frequency = PER_INSTANCE +MODULE_DESCRIPTION = """\ +Sets the system timezone based on the value provided. +""" + +meta: MetaSchema = { + "id": "cc_timezone", + "name": "Timezone", + "title": "Set the system timezone", + "description": MODULE_DESCRIPTION, + "distros": [ALL_DISTROS], + "frequency": PER_INSTANCE, + "examples": [ + "timezone: US/Eastern", + ], +} + +__doc__ = get_meta_doc(meta) def handle(name, cfg, cloud, log, args): diff --git a/cloudinit/config/cloud-init-schema.json b/cloudinit/config/cloud-init-schema.json index 75d97bc064a..88cabd1d476 100644 --- a/cloudinit/config/cloud-init-schema.json +++ b/cloudinit/config/cloud-init-schema.json @@ -1462,6 +1462,306 @@ } } } + }, + "cc_scripts_vendor": { + "type": "object", + "properties": { + "vendor_data": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": ["boolean", "string"], + "description": "Whether vendor data is enabled or not. Use of string for this value is DEPRECATED. Default: ``true``" + }, + "prefix": { + "type": ["array", "string"], + "items": {"type": ["string", "integer"]}, + "description": "The command to run before any vendor scripts. Its primary use case is for profiling a script, not to prevent its run" + } + } + } + } + }, + "cc_seed_random": { + "type": "object", + "properties": { + "seed_random": { + "type": "object", + "additionalProperties": false, + "properties": { + "file": { + "type": "string", + "default": "/dev/urandom", + "description": "File to write random data to. Default: ``/dev/urandom``" + }, + "data": { + "type": "string", + "description": "This data will be written to ``file`` before data from the datasource. When using a multiline value or specifying binary data, be sure to follow yaml syntax and use the ``|`` and ``!binary`` yaml format specifiers when appropriate" + }, + "encoding": { + "type": "string", + "default": "raw", + "enum": ["raw", "base64", "b64", "gzip", "gz"], + "description": "Used to decode ``data`` provided. Allowed values are ``raw``, ``base64``, ``b64``, ``gzip``, or ``gz``. Default: ``raw``" + }, + "command": { + "type": "array", + "items": {"type": "string"}, + "description": "Execute this command to seed random. The command will have RANDOM_SEED_FILE in its environment set to the value of ``file`` above." + }, + "command_required": { + "type": "boolean", + "default": false, + "description": "If true, and ``command`` is not available to be run then an exception is raised and cloud-init will record failure. Otherwise, only debug error is mentioned. Default: ``false``" + } + } + } + } + }, + "cc_set_hostname": { + "type": "object", + "properties": { + "preserve_hostname": { + "type": "boolean", + "default": false, + "description": "If true, the hostname will not be changed. Default: ``false``" + }, + "hostname": { + "type": "string", + "description": "The hostname to set" + }, + "fqdn": { + "type": "string", + "description": "The fully qualified domain name to set" + }, + "prefer_fqdn_over_hostname": { + "type": "boolean", + "description": "If true, the fqdn will be used if it is set. If false, the hostname will be used. If unset, the result is distro-dependent" + } + } + }, + "cc_set_passwords": { + "type": "object", + "properties": { + "ssh_pwauth": { + "oneOf": [ + {"type": "boolean"}, + {"type": "string"} + ], + "description": "Sets whether or not to accept password authentication. ``true`` will enable password auth. ``false`` will disable. Default is to leave the value unchanged. Use of non-boolean values for this field is DEPRECATED and will result in an error in a future version of cloud-init." + }, + "chpasswd": { + "type": "object", + "additionalProperties": false, + "properties": { + "expire": { + "type": "boolean", + "default": true, + "description": "Whether to expire all user passwords such that a password will need to be reset on the user's next login. Default: ``true``" + }, + "list": { + "oneOf": [ + {"type": "string"}, + { + "type": "array", + "items": { + "type": "string", + "pattern": "^.+:.+$" + }} + ], + "minItems": 1, + "description": "List of ``username:password`` pairs. Each user will have the corresponding password set. A password can be randomly generated by specifying ``RANDOM`` or ``R`` as a user's password. A hashed password, created by a tool like ``mkpasswd``, can be specified. A regex (``r'\\$(1|2a|2y|5|6)(\\$.+){2}'``) is used to determine if a password value should be treated as a hash.\n\nUse of a multiline string for this field is DEPRECATED and will result in an error in a future version of cloud-init." + } + } + }, + "password": { + "type": "string", + "description": "Set the default user's password. Ignored if ``chpasswd`` ``list`` is used" + } + } + }, + "cc_snap": { + "type": "object", + "properties": { + "snap": { + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "assertions": { + "type": ["object", "array"], + "description": "Properly-signed snap assertions which will run before and snap ``commands``.", + "items": {"type": "string"}, + "additionalItems": false, + "minItems": 1, + "minProperties": 1, + "uniqueItems": true, + "additionalProperties": {"type": "string"} + }, + "commands": { + "type": ["object", "array"], + "description": "Snap commands to run on the target system", + "items": { + "oneOf": [ + {"type": "string"}, + {"type": "array", "items": {"type": "string"}} + ] + }, + "additionalItems": false, + "minItems": 1, + "minProperties": 1, + "additionalProperties": { + "oneOf": [ + {"type": "string"}, + {"type": "array", "items": {"type": "string"}} + ] + } + }, + "squashfuse_in_container": { + "type": "boolean", + "default": false, + "description": "DEPRECATED: This key is no longer needed and will be removed in a future version of cloud-init." + } + } + } + } + }, + "cc_spacewalk": { + "type": "object", + "properties": { + "spacewalk": { + "type": "object", + "additionalProperties": false, + "properties": { + "server": { + "type": "string", + "description": "The Spacewalk server to use" + }, + "proxy": { + "type": "string", + "description": "The proxy to use when connecting to Spacewalk" + }, + "activation_key": { + "type": "string", + "description": "The activation key to use when registering with Spacewalk" + } + } + } + } + }, + "cc_ssh_authkey_fingerprints": { + "type": "object", + "properties": { + "no_ssh_fingerprints": { + "type": "boolean", + "default": false, + "description": "If true, SSH fingerprints will not be written. Default: ``false``" + }, + "authkey_hash": { + "type": "string", + "default": "sha256", + "description": "The hash type to use when generating SSH fingerprints. Default: ``sha256``" + } + } + }, + "cc_ssh_import_id": { + "type": "object", + "properties": { + "ssh_import_id": { + "type": "array", + "items": { + "type": "string", + "description": "The SSH public key to import" + } + } + } + }, + "cc_ssh": { + "type": "object", + "properties": { + "ssh_keys": { + "type": "object", + "description": "A dictionary entries for the public and private host keys of each desired key type. Entries in the ``ssh_keys`` config dict should have keys in the format ``_private``, ``_public``, and, optionally, ``_certificate``, e.g. ``rsa_private: ``, ``rsa_public: ``, and ``rsa_certificate: ``. Not all key types have to be specified, ones left unspecified will not be used. If this config option is used, then separate keys will not be automatically generated. In order to specify multiline private host keys and certificates, use yaml multiline syntax.", + "patternProperties": { + "^(dsa|ecdsa|ecdsa-sk|ed25519|ed25519-sk|rsa)_(public|private|certificate)$": { + "label": "", + "type": "string" + } + }, + "additionalProperties": false + }, + "ssh_authorized_keys": { + "type": "array", + "minItems": 1, + "description": "The SSH public keys to add ``.ssh/authorized_keys`` in the default user's home directory", + "items": { + "type": "string" + } + }, + "ssh_deletekeys" : { + "type": "boolean", + "default": true, + "description": "Remove host SSH keys. This prevents re-use of a private host key from an image with default host SSH keys. Default: ``true``" + }, + "ssh_genkeytypes": { + "type": "array", + "description": "The SSH key types to generate. Default: ``[rsa, dsa, ecdsa, ed25519]``", + "default": ["dsa", "ecdsa", "ecdsa-sk", "ed25519", "ed25519-sk", "rsa"], + "minItems": 1, + "items": { + "type": "string", + "enum": ["dsa", "ecdsa", "ecdsa-sk", "ed25519", "ed25519-sk", "rsa"] + } + }, + "disable_root": { + "type": "boolean", + "default": true, + "description": "Disable root login. Default: ``true``" + }, + "disable_root_opts": { + "type": "string", + "default": "``no-port-forwarding,no-agent-forwarding,no-X11-forwarding,command=\"echo 'Please login as the user \\\"$USER\\\" rather than the user \\\"$DISABLE_USER\\\".';echo;sleep 10;exit 142\"``", + "description": "Disable root login options. If ``disable_root_opts`` is specified and contains the string ``$USER``, it will be replaced with the username of the default user. Default: ``no-port-forwarding,no-agent-forwarding,no-X11-forwarding,command=\"echo 'Please login as the user \\\"$USER\\\" rather than the user \\\"$DISABLE_USER\\\".';echo;sleep 10;exit 142\"``" + }, + "allow_public_ssh_keys": { + "type": "boolean", + "default": true, + "description": "If ``true``, will import the public SSH keys from the datasource's metadata to the user's ``.ssh/authorized_keys`` file. Default: ``true``" + }, + "ssh_quiet_keygen": { + "type": "boolean", + "default": false, + "description": "If ``true``, will suppress the output of key generation to the console. Default: ``false``" + }, + "ssh_publish_hostkeys": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "If true, will read host keys from /etc/ssh/*.pub and publish them to the datasource (if supported). Default: ``true``" + }, + "blacklist": { + "type": "array", + "description": "The SSH key types to ignore when publishing. Default: ``[dsa]``", + "items": { + "type": "string" + } + } + } + } + } + }, + "cc_timezone": { + "type": "object", + "properties": { + "timezone": { + "type": "string", + "description": "The timezone to use as represented in /usr/share/zoneinfo" + } + } } }, "allOf": [ @@ -1497,6 +1797,16 @@ { "$ref": "#/$defs/cc_rh_subscription"}, { "$ref": "#/$defs/cc_rsyslog"}, { "$ref": "#/$defs/cc_runcmd"}, - { "$ref": "#/$defs/cc_salt_minion"} + { "$ref": "#/$defs/cc_salt_minion"}, + { "$ref": "#/$defs/cc_scripts_vendor"}, + { "$ref": "#/$defs/cc_seed_random"}, + { "$ref": "#/$defs/cc_set_hostname"}, + { "$ref": "#/$defs/cc_set_passwords"}, + { "$ref": "#/$defs/cc_snap"}, + { "$ref": "#/$defs/cc_spacewalk"}, + { "$ref": "#/$defs/cc_ssh_authkey_fingerprints"}, + { "$ref": "#/$defs/cc_ssh_import_id"}, + { "$ref": "#/$defs/cc_ssh"}, + { "$ref": "#/$defs/cc_timezone"} ] } diff --git a/cloudinit/config/schema.py b/cloudinit/config/schema.py index 1ccfe41d701..40647f9f693 100644 --- a/cloudinit/config/schema.py +++ b/cloudinit/config/schema.py @@ -36,15 +36,17 @@ **Supported distros:** {distros} -**Config schema**: +{property_header} {property_doc} + {examples} """ +SCHEMA_PROPERTY_HEADER = "**Config schema**:" SCHEMA_PROPERTY_TMPL = "{prefix}**{prop_name}:** ({prop_type}) {description}" SCHEMA_LIST_ITEM_TMPL = ( "{prefix}Each item in **{prop_name}** list supports the following keys:" ) -SCHEMA_EXAMPLES_HEADER = "\n**Examples**::\n\n" +SCHEMA_EXAMPLES_HEADER = "**Examples**::\n\n" SCHEMA_EXAMPLES_SPACER_TEMPLATE = "\n # --- Example{0} ---" @@ -604,6 +606,7 @@ def get_meta_doc(meta: MetaSchema, schema: dict = None) -> str: # cast away type annotation meta_copy = dict(deepcopy(meta)) + meta_copy["property_header"] = "" defs = schema.get("$defs", {}) if defs.get(meta["id"]): schema = defs.get(meta["id"]) @@ -612,6 +615,8 @@ def get_meta_doc(meta: MetaSchema, schema: dict = None) -> str: except AttributeError: LOG.warning("Unable to render property_doc due to invalid schema") meta_copy["property_doc"] = "" + if meta_copy["property_doc"]: + meta_copy["property_header"] = SCHEMA_PROPERTY_HEADER meta_copy["examples"] = _get_examples(meta) meta_copy["distros"] = ", ".join(meta["distros"]) # Need an underbar of the same length as the name diff --git a/cloudinit/stages.py b/cloudinit/stages.py index fc8406a4f65..e3f8b3ef781 100644 --- a/cloudinit/stages.py +++ b/cloudinit/stages.py @@ -757,6 +757,13 @@ def _consume_vendordata(self, vendor_source, frequency=PER_INSTANCE): LOG.debug("%s consumption is disabled.", vendor_source) return + if isinstance(enabled, str): + LOG.debug( + "Use of string '%s' for 'vendor_data:enabled' field " + "is deprecated. Use boolean value instead", + enabled, + ) + LOG.debug( "%s will be consumed. disabled_handlers=%s", vendor_source, diff --git a/doc/examples/cloud-config-seed-random.txt b/doc/examples/cloud-config-seed-random.txt deleted file mode 100644 index 142b10cd813..00000000000 --- a/doc/examples/cloud-config-seed-random.txt +++ /dev/null @@ -1,32 +0,0 @@ -#cloud-config -# -# random_seed is a dictionary. -# -# The config module will write seed data from the datasource -# to 'file' described below. -# -# Entries in this dictionary are: -# file: the file to write random data to (default is /dev/urandom) -# data: this data will be written to 'file' before data from -# the datasource -# encoding: this will be used to decode 'data' provided. -# allowed values are 'encoding', 'raw', 'base64', 'b64' -# 'gzip', or 'gz'. Default is 'raw' -# -# command: execute this command to seed random. -# the command will have RANDOM_SEED_FILE in its environment -# set to the value of 'file' above. -# command_required: default False -# if true, and 'command' is not available to be run -# then exception is raised and cloud-init will record failure. -# Otherwise, only debug error is mentioned. -# -# Note: command could be ['pollinate', -# '--server=http://local.pollinate.server'] -# which would have pollinate populate /dev/urandom from provided server -random_seed: - file: '/dev/urandom' - data: 'my random string' - encoding: 'raw' - command: ['sh', '-c', 'dd if=/dev/urandom of=$RANDOM_SEED_FILE'] - command_required: True diff --git a/doc/examples/cloud-config-vendor-data.txt b/doc/examples/cloud-config-vendor-data.txt deleted file mode 100644 index 920d12e80f4..00000000000 --- a/doc/examples/cloud-config-vendor-data.txt +++ /dev/null @@ -1,16 +0,0 @@ -#cloud-config -# -# This explains how to control vendordata via a cloud-config -# -# On select Datasources, vendors have a channel for the consumptions -# of all support user-data types via a special channel called -# vendordata. Users of the end system are given ultimate control. -# -vendor_data: - enabled: True - prefix: /usr/bin/ltrace - -# enabled: whether it is enabled or not -# prefix: the command to run before any vendor scripts. -# Note: this is a fairly weak method of containment. It should -# be used to profile a script, not to prevent its run diff --git a/tests/unittests/config/test_cc_scripts_vendor.py b/tests/unittests/config/test_cc_scripts_vendor.py new file mode 100644 index 00000000000..a8cbfb4f4bd --- /dev/null +++ b/tests/unittests/config/test_cc_scripts_vendor.py @@ -0,0 +1,28 @@ +import pytest + +from cloudinit.config.schema import ( + SchemaValidationError, + get_schema, + validate_cloudconfig_schema, +) +from tests.unittests.helpers import skipUnlessJsonSchema + + +class TestScriptsVendorSchema: + @pytest.mark.parametrize( + "config, error_msg", + ( + ({"vendor_data": {"enabled": True}}, None), + ({"vendor_data": {"enabled": "yes"}}, None), + ), + ) + @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) diff --git a/tests/unittests/config/test_cc_seed_random.py b/tests/unittests/config/test_cc_seed_random.py index 8b2fdcdd296..05340d082be 100644 --- a/tests/unittests/config/test_cc_seed_random.py +++ b/tests/unittests/config/test_cc_seed_random.py @@ -12,15 +12,22 @@ import tempfile from io import BytesIO +import pytest + from cloudinit import subp, util from cloudinit.config import cc_seed_random -from tests.unittests import helpers as t_help +from cloudinit.config.schema import ( + SchemaValidationError, + get_schema, + validate_cloudconfig_schema, +) +from tests.unittests.helpers import TestCase, skipUnlessJsonSchema from tests.unittests.util import get_cloud LOG = logging.getLogger(__name__) -class TestRandomSeed(t_help.TestCase): +class TestRandomSeed(TestCase): def setUp(self): super(TestRandomSeed, self).setUp() self._seed_file = tempfile.mktemp() @@ -218,4 +225,36 @@ def apply_patches(patches): return ret +class TestSeedRandomSchema: + @pytest.mark.parametrize( + "config, error_msg", + [ + ( + {"seed_random": {"encoding": "bad"}}, + "'bad' is not one of " + r"\['raw', 'base64', 'b64', 'gzip', 'gz'\]", + ), + ( + {"seed_random": {"command": "foo"}}, + "'foo' is not of type 'array'", + ), + ( + {"seed_random": {"command_required": "true"}}, + "'true' is not of type 'boolean'", + ), + ( + {"seed_random": {"bad": "key"}}, + "Additional properties are not allowed", + ), + ], + ) + @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) + + # vi: ts=4 expandtab diff --git a/tests/unittests/config/test_cc_set_passwords.py b/tests/unittests/config/test_cc_set_passwords.py index bc81214b495..94c1e8cc3a8 100644 --- a/tests/unittests/config/test_cc_set_passwords.py +++ b/tests/unittests/config/test_cc_set_passwords.py @@ -2,9 +2,16 @@ from unittest import mock +import pytest + from cloudinit import util from cloudinit.config import cc_set_passwords as setpass -from tests.unittests.helpers import CiTestCase +from cloudinit.config.schema import ( + SchemaValidationError, + get_schema, + validate_cloudconfig_schema, +) +from tests.unittests.helpers import CiTestCase, skipUnlessJsonSchema MODPATH = "cloudinit.config.cc_set_passwords." @@ -174,4 +181,35 @@ def test_handle_on_chpasswd_list_creates_random_passwords( self.fail("Password not emitted to console") +class TestSetPasswordsSchema: + @pytest.mark.parametrize( + "config, error_msg", + [ + # Test both formats still work + ({"ssh_pwauth": True}, None), + ({"ssh_pwauth": "yes"}, None), + ({"ssh_pwauth": "unchanged"}, None), + ({"chpasswd": {"list": "blah"}}, None), + # Test regex + ({"chpasswd": {"list": ["user:pass"]}}, None), + # Test valid + ({"password": "pass"}, None), + # Test invalid values + ( + {"chpasswd": {"expire": "yes"}}, + "'yes' is not of type 'boolean'", + ), + ({"chpasswd": {"list": ["user"]}}, ""), + ({"chpasswd": {"list": []}}, r"\[\] is too short"), + ], + ) + @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) + + # vi: ts=4 expandtab diff --git a/tests/unittests/config/test_cc_snap.py b/tests/unittests/config/test_cc_snap.py index 1632676dbb0..1eb7e634030 100644 --- a/tests/unittests/config/test_cc_snap.py +++ b/tests/unittests/config/test_cc_snap.py @@ -3,6 +3,8 @@ import re from io import StringIO +import pytest + from cloudinit import util from cloudinit.config.cc_snap import ( ASSERTIONS_FILE, @@ -10,12 +12,14 @@ handle, maybe_install_squashfuse, run_commands, - schema, ) -from cloudinit.config.schema import validate_cloudconfig_schema +from cloudinit.config.schema import ( + SchemaValidationError, + get_schema, + validate_cloudconfig_schema, +) from tests.unittests.helpers import ( CiTestCase, - SchemaTestCaseMixin, mock, skipUnlessJsonSchema, wrap_and_call, @@ -275,184 +279,65 @@ def test_run_command_dict_sorted_as_command_script(self): @skipUnlessJsonSchema() -class TestSchema(CiTestCase, SchemaTestCaseMixin): - - with_logs = True - schema = schema - - def test_schema_warns_on_snap_not_as_dict(self): - """If the snap configuration is not a dict, emit a warning.""" - validate_cloudconfig_schema({"snap": "wrong type"}, schema) - self.assertEqual( - "WARNING: Invalid cloud-config provided:\nsnap: 'wrong type'" - " is not of type 'object'\n", - self.logs.getvalue(), - ) - - @mock.patch("cloudinit.config.cc_snap.run_commands") - def test_schema_disallows_unknown_keys(self, _): - """Unknown keys in the snap configuration emit warnings.""" - validate_cloudconfig_schema( - {"snap": {"commands": ["ls"], "invalid-key": ""}}, schema - ) - self.assertIn( - "WARNING: Invalid cloud-config provided:\nsnap: Additional" - " properties are not allowed ('invalid-key' was unexpected)", - self.logs.getvalue(), - ) - - def test_warn_schema_requires_either_commands_or_assertions(self): - """Warn when snap configuration lacks both commands and assertions.""" - validate_cloudconfig_schema({"snap": {}}, schema) - self.assertIn( - "WARNING: Invalid cloud-config provided:\nsnap: {} does not" - " have enough properties", - self.logs.getvalue(), - ) - - @mock.patch("cloudinit.config.cc_snap.run_commands") - def test_warn_schema_commands_is_not_list_or_dict(self, _): - """Warn when snap:commands config is not a list or dict.""" - validate_cloudconfig_schema({"snap": {"commands": "broken"}}, schema) - self.assertEqual( - "WARNING: Invalid cloud-config provided:\nsnap.commands: 'broken'" - " is not of type 'object', 'array'\n", - self.logs.getvalue(), - ) - - @mock.patch("cloudinit.config.cc_snap.run_commands") - def test_warn_schema_when_commands_is_empty(self, _): - """Emit warnings when snap:commands is an empty list or dict.""" - validate_cloudconfig_schema({"snap": {"commands": []}}, schema) - validate_cloudconfig_schema({"snap": {"commands": {}}}, schema) - self.assertEqual( - "WARNING: Invalid cloud-config provided:\nsnap.commands: [] is" - " too short\nWARNING: Invalid cloud-config provided:\n" - "snap.commands: {} does not have enough properties\n", - self.logs.getvalue(), - ) - - @mock.patch("cloudinit.config.cc_snap.run_commands") - def test_schema_when_commands_are_list_or_dict(self, _): - """No warnings when snap:commands are either a list or dict.""" - validate_cloudconfig_schema({"snap": {"commands": ["valid"]}}, schema) - validate_cloudconfig_schema( - {"snap": {"commands": {"01": "also valid"}}}, schema - ) - self.assertEqual("", self.logs.getvalue()) - - @mock.patch("cloudinit.config.cc_snap.run_commands") - def test_schema_when_commands_values_are_invalid_type(self, _): - """Warnings when snap:commands values are invalid type (e.g. int)""" - validate_cloudconfig_schema({"snap": {"commands": [123]}}, schema) - validate_cloudconfig_schema( - {"snap": {"commands": {"01": 123}}}, schema - ) - self.assertEqual( - "WARNING: Invalid cloud-config provided:\n" - "snap.commands.0: 123 is not valid under any of the given" - " schemas\n" - "WARNING: Invalid cloud-config provided:\n" - "snap.commands.01: 123 is not valid under any of the given" - " schemas\n", - self.logs.getvalue(), - ) - - @mock.patch("cloudinit.config.cc_snap.run_commands") - def test_schema_when_commands_list_values_are_invalid_type(self, _): - """Warnings when snap:commands list values are wrong type (e.g. int)""" - validate_cloudconfig_schema( - {"snap": {"commands": [["snap", "install", 123]]}}, schema - ) - validate_cloudconfig_schema( - {"snap": {"commands": {"01": ["snap", "install", 123]}}}, schema - ) - self.assertEqual( - "WARNING: Invalid cloud-config provided:\n" - "snap.commands.0: ['snap', 'install', 123] is not valid under any" - " of the given schemas\n", - "WARNING: Invalid cloud-config provided:\n" - "snap.commands.0: ['snap', 'install', 123] is not valid under any" - " of the given schemas\n", - self.logs.getvalue(), - ) - - @mock.patch("cloudinit.config.cc_snap.run_commands") - def test_schema_when_assertions_values_are_invalid_type(self, _): - """Warnings when snap:assertions values are invalid type (e.g. int)""" - validate_cloudconfig_schema({"snap": {"assertions": [123]}}, schema) - validate_cloudconfig_schema( - {"snap": {"assertions": {"01": 123}}}, schema - ) - self.assertEqual( - "WARNING: Invalid cloud-config provided:\n" - "snap.assertions.0: 123 is not of type 'string'\n" - "WARNING: Invalid cloud-config provided:\n" - "snap.assertions.01: 123 is not of type 'string'\n", - self.logs.getvalue(), - ) - - @mock.patch("cloudinit.config.cc_snap.add_assertions") - def test_warn_schema_assertions_is_not_list_or_dict(self, _): - """Warn when snap:assertions config is not a list or dict.""" - validate_cloudconfig_schema({"snap": {"assertions": "broken"}}, schema) - self.assertEqual( - "WARNING: Invalid cloud-config provided:\nsnap.assertions:" - " 'broken' is not of type 'object', 'array'\n", - self.logs.getvalue(), - ) - - @mock.patch("cloudinit.config.cc_snap.add_assertions") - def test_warn_schema_when_assertions_is_empty(self, _): - """Emit warnings when snap:assertions is an empty list or dict.""" - validate_cloudconfig_schema({"snap": {"assertions": []}}, schema) - validate_cloudconfig_schema({"snap": {"assertions": {}}}, schema) - self.assertEqual( - "WARNING: Invalid cloud-config provided:\nsnap.assertions: []" - " is too short\n" - "WARNING: Invalid cloud-config provided:\nsnap.assertions: {}" - " does not have enough properties\n", - self.logs.getvalue(), - ) - - @mock.patch("cloudinit.config.cc_snap.add_assertions") - def test_schema_when_assertions_are_list_or_dict(self, _): - """No warnings when snap:assertions are a list or dict.""" - validate_cloudconfig_schema( - {"snap": {"assertions": ["valid"]}}, schema - ) - validate_cloudconfig_schema( - {"snap": {"assertions": {"01": "also valid"}}}, schema - ) - self.assertEqual("", self.logs.getvalue()) - - def test_duplicates_are_fine_array_array(self): - """Duplicated commands array/array entries are allowed.""" - self.assertSchemaValid( - {"commands": [["echo", "bye"], ["echo", "bye"]]}, - "command entries can be duplicate.", - ) - - def test_duplicates_are_fine_array_string(self): - """Duplicated commands array/string entries are allowed.""" - self.assertSchemaValid( - {"commands": ["echo bye", "echo bye"]}, - "command entries can be duplicate.", - ) - - def test_duplicates_are_fine_dict_array(self): - """Duplicated commands dict/array entries are allowed.""" - self.assertSchemaValid( - {"commands": {"00": ["echo", "bye"], "01": ["echo", "bye"]}}, - "command entries can be duplicate.", - ) - - def test_duplicates_are_fine_dict_string(self): - """Duplicated commands dict/string entries are allowed.""" - self.assertSchemaValid( - {"commands": {"00": "echo bye", "01": "echo bye"}}, - "command entries can be duplicate.", - ) +class TestSnapSchema: + @pytest.mark.parametrize( + "config, error_msg", + [ + # Valid + ({"snap": {"commands": ["valid"]}}, None), + ({"snap": {"commands": {"01": "also valid"}}}, None), + ({"snap": {"assertions": ["valid"]}}, None), + ({"snap": {"assertions": {"01": "also valid"}}}, None), + ({"commands": [["echo", "bye"], ["echo", "bye"]]}, None), + ({"commands": ["echo bye", "echo bye"]}, None), + ( + {"commands": {"00": ["echo", "bye"], "01": ["echo", "bye"]}}, + None, + ), + ({"commands": {"00": "echo bye", "01": "echo bye"}}, None), + # Invalid + ({"snap": "wrong type"}, "'wrong type' is not of type 'object'"), + ( + {"snap": {"commands": ["ls"], "invalid-key": ""}}, + "Additional properties are not allowed", + ), + ({"snap": {}}, "{} does not have enough properties"), + ( + {"snap": {"commands": "broken"}}, + "'broken' is not of type 'object', 'array'", + ), + ({"snap": {"commands": []}}, r"snap.commands: \[\] is too short"), + ( + {"snap": {"commands": {}}}, + r"snap.commands: {} does not have enough properties", + ), + ({"snap": {"commands": [123]}}, ""), + ({"snap": {"commands": {"01": 123}}}, ""), + ({"snap": {"commands": [["snap", "install", 123]]}}, ""), + ({"snap": {"commands": {"01": ["snap", "install", 123]}}}, ""), + ({"snap": {"assertions": [123]}}, "123 is not of type 'string'"), + ( + {"snap": {"assertions": {"01": 123}}}, + "123 is not of type 'string'", + ), + ( + {"snap": {"assertions": "broken"}}, + "'broken' is not of type 'object', 'array'", + ), + ({"snap": {"assertions": []}}, r"\[\] is too short"), + ( + {"snap": {"assertions": {}}}, + r"\{} does not have enough properties", + ), + ], + ) + @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) class TestHandle(CiTestCase): @@ -463,21 +348,6 @@ def setUp(self): super(TestHandle, self).setUp() self.tmp = self.tmp_dir() - @mock.patch("cloudinit.config.cc_snap.run_commands") - @mock.patch("cloudinit.config.cc_snap.add_assertions") - @mock.patch("cloudinit.config.cc_snap.validate_cloudconfig_schema") - def test_handle_no_config(self, m_schema, m_add, m_run): - """When no snap-related configuration is provided, nothing happens.""" - cfg = {} - handle("snap", cfg=cfg, cloud=None, log=self.logger, args=None) - self.assertIn( - "DEBUG: Skipping module named snap, no 'snap' key in config", - self.logs.getvalue(), - ) - m_schema.assert_not_called() - m_add.assert_not_called() - m_run.assert_not_called() - @mock.patch("cloudinit.config.cc_snap.run_commands") @mock.patch("cloudinit.config.cc_snap.add_assertions") @mock.patch("cloudinit.config.cc_snap.maybe_install_squashfuse") @@ -558,28 +428,6 @@ def test_handle_adds_assertions(self, m_subp): util.load_file(compare_file), util.load_file(assert_file) ) - @mock.patch("cloudinit.config.cc_snap.subp.subp") - @skipUnlessJsonSchema() - def test_handle_validates_schema(self, m_subp): - """Any provided configuration is runs validate_cloudconfig_schema.""" - assert_file = self.tmp_path("snapd.assertions", dir=self.tmp) - cfg = {"snap": {"invalid": ""}} # Generates schema warning - wrap_and_call( - "cloudinit.config.cc_snap", - {"ASSERTIONS_FILE": {"new": assert_file}}, - handle, - "snap", - cfg=cfg, - cloud=None, - log=self.logger, - args=None, - ) - self.assertEqual( - "WARNING: Invalid cloud-config provided:\nsnap: Additional" - " properties are not allowed ('invalid' was unexpected)\n", - self.logs.getvalue(), - ) - class TestMaybeInstallSquashFuse(CiTestCase): diff --git a/tests/unittests/config/test_cc_ssh.py b/tests/unittests/config/test_cc_ssh.py index d66cc4cba55..5889e1805af 100644 --- a/tests/unittests/config/test_cc_ssh.py +++ b/tests/unittests/config/test_cc_ssh.py @@ -3,9 +3,16 @@ import logging import os.path +import pytest + from cloudinit import ssh_util from cloudinit.config import cc_ssh -from tests.unittests.helpers import CiTestCase, mock +from cloudinit.config.schema import ( + SchemaValidationError, + get_schema, + validate_cloudconfig_schema, +) +from tests.unittests.helpers import CiTestCase, mock, skipUnlessJsonSchema LOG = logging.getLogger(__name__) @@ -465,3 +472,44 @@ def test_handle_ssh_keys_in_cfg(self, m_write_file, m_nug, m_setup_keys): # Check that all expected output has been done. for call_ in expected_calls: self.assertIn(call_, m_write_file.call_args_list) + + +class TestSshSchema: + @pytest.mark.parametrize( + "config, error_msg", + ( + ({"ssh_authorized_keys": ["key1", "key2"]}, None), + ( + {"ssh_keys": {"dsa_private": "key1", "rsa_public": "key2"}}, + None, + ), + ( + {"ssh_keys": {"rsa_a": "key"}}, + "'rsa_a' does not match any of the regexes", + ), + ( + {"ssh_keys": {"a_public": "key"}}, + "'a_public' does not match any of the regexes", + ), + ( + {"ssh_authorized_keys": "ssh-rsa blah"}, + "'ssh-rsa blah' is not of type 'array'", + ), + ({"ssh_genkeytypes": ["bad"]}, "'bad' is not one of"), + ( + {"disable_root_opts": ["no-port-forwarding"]}, + r"\['no-port-forwarding'\] is not of type 'string'", + ), + ( + {"ssh_publish_hostkeys": {"key": "value"}}, + "Additional properties are not allowed", + ), + ), + ) + @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_schema.py b/tests/unittests/config/test_schema.py index 849046afb33..304231b3a2f 100644 --- a/tests/unittests/config/test_schema.py +++ b/tests/unittests/config/test_schema.py @@ -130,7 +130,20 @@ def test_get_schema_coalesces_known_schema(self): "cc_rsyslog", "cc_runcmd", "cc_salt_minion", + "cc_scripts_per_boot", + "cc_scripts_per_instance", + "cc_scripts_per_once", + "cc_scripts_user", + "cc_scripts_vendor", + "cc_seed_random", + "cc_set_hostname", + "cc_set_passwords", "cc_snap", + "cc_spacewalk", + "cc_ssh_authkey_fingerprints", + "cc_ssh_import_id", + "cc_ssh", + "cc_timezone", "cc_ubuntu_advantage", "cc_ubuntu_drivers", "cc_write_files", @@ -176,6 +189,16 @@ def test_get_schema_coalesces_known_schema(self): {"$ref": "#/$defs/cc_rsyslog"}, {"$ref": "#/$defs/cc_runcmd"}, {"$ref": "#/$defs/cc_salt_minion"}, + {"$ref": "#/$defs/cc_scripts_vendor"}, + {"$ref": "#/$defs/cc_seed_random"}, + {"$ref": "#/$defs/cc_set_hostname"}, + {"$ref": "#/$defs/cc_set_passwords"}, + {"$ref": "#/$defs/cc_snap"}, + {"$ref": "#/$defs/cc_spacewalk"}, + {"$ref": "#/$defs/cc_ssh_authkey_fingerprints"}, + {"$ref": "#/$defs/cc_ssh_import_id"}, + {"$ref": "#/$defs/cc_ssh"}, + {"$ref": "#/$defs/cc_timezone"}, ] found_subschema_defs = [] legacy_schema_keys = [] @@ -190,7 +213,6 @@ def test_get_schema_coalesces_known_schema(self): assert [ "drivers", "ntp", - "snap", "ubuntu_advantage", "write_files", "write_files", diff --git a/tests/unittests/test_data.py b/tests/unittests/test_data.py index a5018a427e4..010ac7416d3 100644 --- a/tests/unittests/test_data.py +++ b/tests/unittests/test_data.py @@ -276,7 +276,7 @@ def test_vendor_user_yaml_cloud_config(self): #cloud-config a: c vendor_data: - enabled: True + enabled: true prefix: /bin/true name: user run: @@ -319,7 +319,7 @@ def test_vendordata_script(self): user_blob = """ #cloud-config vendor_data: - enabled: True + enabled: true prefix: /bin/true """ new_root = self.reRoot()