diff --git a/cloudinit/config/cc_ubuntu_advantage.py b/cloudinit/config/cc_ubuntu_advantage.py index e469bb22d9b..7f4263e68ec 100644 --- a/cloudinit/config/cc_ubuntu_advantage.py +++ b/cloudinit/config/cc_ubuntu_advantage.py @@ -6,11 +6,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 UA_URL = "https://ubuntu.com/advantage" @@ -77,29 +73,7 @@ "frequency": PER_INSTANCE, } -schema = { - "type": "object", - "properties": { - "ubuntu_advantage": { - "type": "object", - "properties": { - "enable": { - "type": "array", - "items": {"type": "string"}, - }, - "token": { - "type": "string", - "description": "A contract token obtained from %s." - % UA_URL, - }, - }, - "required": ["token"], - "additionalProperties": False, - } - }, -} - -__doc__ = get_meta_doc(meta, schema) # Supplement python help() +__doc__ = get_meta_doc(meta) LOG = logging.getLogger(__name__) @@ -194,7 +168,6 @@ def handle(name, cfg, cloud, log, args): name, ) return - validate_cloudconfig_schema(cfg, schema) if "commands" in ua_section: msg = ( 'Deprecated configuration "ubuntu-advantage: commands" provided.' diff --git a/cloudinit/config/cc_ubuntu_drivers.py b/cloudinit/config/cc_ubuntu_drivers.py index 44a3bdb4b15..15f621a79c9 100644 --- a/cloudinit/config/cc_ubuntu_drivers.py +++ b/cloudinit/config/cc_ubuntu_drivers.py @@ -7,17 +7,13 @@ from cloudinit import log as logging from cloudinit import subp, temp_utils, type_utils, 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 LOG = logging.getLogger(__name__) -frequency = PER_INSTANCE distros = ["ubuntu"] + meta: MetaSchema = { "id": "cc_ubuntu_drivers", "name": "Ubuntu Drivers", @@ -37,47 +33,15 @@ """ ) ], - "frequency": frequency, + "frequency": PER_INSTANCE, } -schema = { - "type": "object", - "properties": { - "drivers": { - "type": "object", - "additionalProperties": False, - "properties": { - "nvidia": { - "type": "object", - "additionalProperties": False, - "required": ["license-accepted"], - "properties": { - "license-accepted": { - "type": "boolean", - "description": ( - "Do you accept the NVIDIA driver license?" - ), - }, - "version": { - "type": "string", - "description": ( - "The version of the driver to install (e.g." - ' "390", "410"). Defaults to the latest' - " version." - ), - }, - }, - }, - }, - }, - }, -} +__doc__ = get_meta_doc(meta) + OLD_UBUNTU_DRIVERS_STDERR_NEEDLE = ( "ubuntu-drivers: error: argument : invalid choice: 'install'" ) -__doc__ = get_meta_doc(meta, schema) # Supplement python help() - # Use a debconf template to configure a global debconf variable # (linux/nvidia/latelink) setting this to "true" allows the @@ -180,5 +144,4 @@ def handle(name, cfg, cloud, log, _args): log.debug("Skipping module named %s, no 'drivers' key in config", name) return - validate_cloudconfig_schema(cfg, schema) install_drivers(cfg["drivers"], cloud.distro.install_packages) diff --git a/cloudinit/config/cc_update_etc_hosts.py b/cloudinit/config/cc_update_etc_hosts.py index f0aa9b0f6e4..5334f453f3a 100644 --- a/cloudinit/config/cc_update_etc_hosts.py +++ b/cloudinit/config/cc_update_etc_hosts.py @@ -6,18 +6,22 @@ # # This file is part of cloud-init. See LICENSE file for license information. -""" -Update Etc Hosts ----------------- -**Summary:** update the hosts file (usually ``/etc/hosts``) +"""Update Etc Hosts: Update the hosts file (usually ``/etc/hosts``)""" + +from textwrap import dedent + +from cloudinit import templater, util +from cloudinit.config.schema import MetaSchema, get_meta_doc +from cloudinit.settings import PER_ALWAYS +MODULE_DESCRIPTION = """\ This module will update the contents of the local hosts database (hosts file; usually ``/etc/hosts``) based on the hostname/fqdn specified in config. Management of the hosts file is controlled using ``manage_etc_hosts``. If this is set to false, cloud-init will not manage the hosts file at all. This is the default behavior. -If set to ``true`` or ``template``, cloud-init will generate the hosts file +If set to ``true``, cloud-init will generate the hosts file using the template located in ``/etc/cloud/templates/hosts.tmpl``. In the ``/etc/cloud/templates/hosts.tmpl`` template, the strings ``$hostname`` and ``$fqdn`` will be replaced with the hostname and fqdn respectively. @@ -36,24 +40,57 @@ .. note:: for instructions on specifying hostname and fqdn, see documentation for ``cc_set_hostname`` - -**Internal name:** ``cc_update_etc_hosts`` - -**Module frequency:** always - -**Supported distros:** all - -**Config keys**:: - - manage_etc_hosts: - fqdn: - hostname: """ -from cloudinit import templater, util -from cloudinit.settings import PER_ALWAYS - -frequency = PER_ALWAYS +distros = ["all"] + +meta: MetaSchema = { + "id": "cc_update_etc_hosts", + "name": "Update Etc Hosts", + "title": "Update the hosts file (usually ``/etc/hosts``)", + "description": MODULE_DESCRIPTION, + "distros": distros, + "examples": [ + dedent( + """\ + # Do not update or manage /etc/hosts at all. This is the default behavior. + # + # Whatever is present at instance boot time will be present after boot. + # User changes will not be overwritten. + manage_etc_hosts: false + """ + ), + dedent( + """\ + # Manage /etc/hosts with cloud-init. + # On every boot, /etc/hosts will be re-written from + # ``/etc/cloud/templates/hosts.tmpl``. + # + # The strings '$hostname' and '$fqdn' are replaced in the template + # with the appropriate values either from the config-config ``fqdn`` or + # ``hostname`` if provided. When absent, the cloud metadata will be + # checked for ``local-hostname` which can be split into .. + # + # To make modifications persistent across a reboot, you must modify + # ``/etc/cloud/templates/hosts.tmpl``. + manage_etc_hosts: true + """ + ), + dedent( + """\ + # Update /etc/hosts every boot providing a "localhost" 127.0.1.1 entry + # with the latest hostname and fqdn as provided by either IMDS or + # cloud-config. + # All other entries will be left as is. + # 'ping `hostname`' will ping 127.0.1.1 + manage_etc_hosts: localhost + """ + ), + ], + "frequency": PER_ALWAYS, +} + +__doc__ = get_meta_doc(meta) def handle(name, cfg, cloud, log, _args): @@ -62,6 +99,11 @@ def handle(name, cfg, cloud, log, _args): hosts_fn = cloud.distro.hosts_fn if util.translate_bool(manage_hosts, addons=["template"]): + if manage_hosts == "template": + log.warning( + "DEPRECATED: please use manage_etc_hosts: true instead of" + " 'template'" + ) (hostname, fqdn) = util.get_hostname_fqdn(cfg, cloud) if not hostname: log.warning( diff --git a/cloudinit/config/cc_update_hostname.py b/cloudinit/config/cc_update_hostname.py index 09f6f6daa7c..50c657cd4d0 100644 --- a/cloudinit/config/cc_update_hostname.py +++ b/cloudinit/config/cc_update_hostname.py @@ -6,38 +6,76 @@ # # This file is part of cloud-init. See LICENSE file for license information. -""" -Update Hostname ---------------- -**Summary:** update hostname and fqdn +"""Update Hostname: Update hostname and fqdn""" + +import os +from textwrap import dedent + +from cloudinit import util +from cloudinit.config.schema import MetaSchema, get_meta_doc +from cloudinit.settings import PER_ALWAYS +MODULE_DESCRIPTION = """\ This module will update the system hostname and fqdn. If ``preserve_hostname`` -is set, then the hostname will not be altered. +is set ``true``, then the hostname will not be altered. .. note:: for instructions on specifying hostname and fqdn, see documentation for ``cc_set_hostname`` - -**Internal name:** ``cc_update_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.settings import PER_ALWAYS - -frequency = PER_ALWAYS +distros = ["all"] + +meta: MetaSchema = { + "id": "cc_update_hostname", + "name": "Update Hostname", + "title": "Update hostname and fqdn", + "description": MODULE_DESCRIPTION, + "distros": distros, + "examples": [ + dedent( + """\ + # By default: when ``preserve_hostname`` is not specified cloud-init + # updates ``/etc/hostname`` per-boot based on the cloud provided + # ``local-hostname`` setting. If you manually change ``/etc/hostname`` + # after boot cloud-init will no longer modify it. + # + # This default cloud-init behavior is equivalent to this cloud-config: + preserve_hostname: false + """ + ), + dedent( + """\ + # Prevent cloud-init from updating the system hostname. + preseve_hostname: true + """ + ), + dedent( + """\ + # Prevent cloud-init from updating ``/etc/hostname`` + preseve_hostname: true + """ + ), + dedent( + """\ + # Set hostname to "external.fqdn.me" instead of "myhost" + fqdn: external.fqdn.me + hostname: myhost + prefer_fqdn_over_hostname: true + """ + ), + dedent( + """\ + # Set hostname to "external" instead of "external.fqdn.me" when + # cloud metadata provides the ``local-hostname``: "external.fqdn.me". + prefer_fqdn_over_hostname: false + """ + ), + ], + "frequency": PER_ALWAYS, +} + +__doc__ = get_meta_doc(meta) def handle(name, cfg, cloud, log, _args): diff --git a/cloudinit/config/cc_write_files.py b/cloudinit/config/cc_write_files.py index 37dae3923ce..e07bc2c7189 100644 --- a/cloudinit/config/cc_write_files.py +++ b/cloudinit/config/cc_write_files.py @@ -12,15 +12,9 @@ from cloudinit import log as logging 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 - DEFAULT_OWNER = "root:root" DEFAULT_PERMS = 0o644 DEFAULT_DEFER = False @@ -28,25 +22,6 @@ LOG = logging.getLogger(__name__) -distros = ["all"] - -# The schema definition for each cloud-config module is a strict contract for -# describing supported configuration parameters for each cloud-config section. -# It allows cloud-config to validate and alert users to invalid or ignored -# configuration options before actually attempting to deploy with said -# configuration. - -supported_encoding_types = [ - "gz", - "gzip", - "gz+base64", - "gzip+base64", - "gz+b64", - "gzip+b64", - "b64", - "base64", -] - meta: MetaSchema = { "id": "cc_write_files", "name": "Write Files", @@ -70,7 +45,7 @@ the early boot process. Use /run/somedir instead to avoid race LP:1707222.""" ), - "distros": distros, + "distros": ["all"], "examples": [ dedent( """\ @@ -132,113 +107,13 @@ """ ), ], - "frequency": frequency, -} - -schema = { - "type": "object", - "properties": { - "write_files": { - "type": "array", - "items": { - "type": "object", - "properties": { - "path": { - "type": "string", - "description": dedent( - """\ - Path of the file to which ``content`` is decoded - and written - """ - ), - }, - "content": { - "type": "string", - "default": "", - "description": dedent( - """\ - Optional content to write to the provided ``path``. - When content is present and encoding is not '%s', - decode the content prior to writing. Default: - **''** - """ - % UNKNOWN_ENC - ), - }, - "owner": { - "type": "string", - "default": DEFAULT_OWNER, - "description": dedent( - """\ - Optional owner:group to chown on the file. Default: - **{owner}** - """.format( - owner=DEFAULT_OWNER - ) - ), - }, - "permissions": { - "type": "string", - "default": oct(DEFAULT_PERMS).replace("o", ""), - "description": dedent( - """\ - Optional file permissions to set on ``path`` - represented as an octal string '0###'. Default: - **'{perms}'** - """.format( - perms=oct(DEFAULT_PERMS).replace("o", "") - ) - ), - }, - "encoding": { - "type": "string", - "default": UNKNOWN_ENC, - "enum": supported_encoding_types, - "description": dedent( - """\ - Optional encoding type of the content. Default is - **text/plain** and no content decoding is - performed. Supported encoding types are: - %s.""" - % ", ".join(supported_encoding_types) - ), - }, - "append": { - "type": "boolean", - "default": False, - "description": dedent( - """\ - Whether to append ``content`` to existing file if - ``path`` exists. Default: **false**. - """ - ), - }, - "defer": { - "type": "boolean", - "default": DEFAULT_DEFER, - "description": dedent( - """\ - Defer writing the file until 'final' stage, after - users were created, and packages were installed. - Default: **{defer}**. - """.format( - defer=DEFAULT_DEFER - ) - ), - }, - }, - "required": ["path"], - "additionalProperties": False, - }, - } - }, + "frequency": PER_INSTANCE, } -__doc__ = get_meta_doc(meta, schema) # Supplement python help() +__doc__ = get_meta_doc(meta) def handle(name, cfg, _cloud, log, _args): - validate_cloudconfig_schema(cfg, schema) file_list = cfg.get("write_files", []) filtered_files = [ f diff --git a/cloudinit/config/cc_write_files_deferred.py b/cloudinit/config/cc_write_files_deferred.py index 1294628cd58..c189950bfe8 100644 --- a/cloudinit/config/cc_write_files_deferred.py +++ b/cloudinit/config/cc_write_files_deferred.py @@ -5,10 +5,7 @@ """Defer writing certain files""" from cloudinit import util -from cloudinit.config.cc_write_files import DEFAULT_DEFER -from cloudinit.config.cc_write_files import schema as write_files_schema -from cloudinit.config.cc_write_files import write_files -from cloudinit.config.schema import validate_cloudconfig_schema +from cloudinit.config.cc_write_files import DEFAULT_DEFER, write_files # meta is not used in this module, but it remains as code documentation # @@ -28,15 +25,11 @@ # *Please note that his module is not exposed to the user through # its own dedicated top-level directive.* -schema = write_files_schema - - # Not exposed, because related modules should document this behaviour __doc__ = None def handle(name, cfg, _cloud, log, _args): - validate_cloudconfig_schema(cfg, schema) file_list = cfg.get("write_files", []) filtered_files = [ f diff --git a/cloudinit/config/cc_yum_add_repo.py b/cloudinit/config/cc_yum_add_repo.py index 7a232689d27..f73571926a9 100644 --- a/cloudinit/config/cc_yum_add_repo.py +++ b/cloudinit/config/cc_yum_add_repo.py @@ -4,38 +4,23 @@ # # This file is part of cloud-init. See LICENSE file for license information. -""" -Yum Add Repo ------------- -**Summary:** add yum repository configuration to the system - -Add yum repository configuration to ``/etc/yum.repos.d``. Configuration files -are named based on the dictionary key under the ``yum_repos`` they are -specified with. If a config file already exists with the same name as a config -entry, the config entry will be skipped. - -**Internal name:** ``cc_yum_add_repo`` - -**Module frequency:** always - -**Supported distros:** almalinux, centos, cloudlinux, eurolinux, fedora, - miraclelinux, openEuler, photon, rhel, rocky, virtuozzo - -**Config keys**:: - - yum_repos: - : - baseurl: - name: - enabled: - # any repository configuration options (see man yum.conf) -""" +"Yum Add Repo: Add yum repository configuration to the system" import io import os from configparser import ConfigParser +from textwrap import dedent from cloudinit import util +from cloudinit.config.schema import MetaSchema, get_meta_doc +from cloudinit.settings import PER_INSTANCE + +MODULE_DESCRIPTION = """\ +Add yum repository configuration to ``/etc/yum.repos.d``. Configuration files +are named based on the opaque dictionary key under the ``yum_repos`` they are +specified with. If a config file already exists with the same name as a config +entry, the config entry will be skipped. +""" distros = [ "almalinux", @@ -50,6 +35,87 @@ "virtuozzo", ] +COPR_BASEURL = ( + "https://download.copr.fedorainfracloud.org/results/@cloud-init/" + "cloud-init-dev/epel-8-$basearch/" +) +COPR_GPG_URL = ( + "https://download.copr.fedorainfracloud.org/results/@cloud-init/" + "cloud-init-dev/pubkey.gpg" +) +EPEL_TESTING_BASEURL = ( + "https://download.copr.fedorainfracloud.org/results/@cloud-init/" + "cloud-init-dev/pubkey.gpg" +) + +meta: MetaSchema = { + "id": "cc_yum_add_repo", + "name": "Yum Add Repo", + "title": "Add yum repository configuration to the system", + "description": MODULE_DESCRIPTION, + "distros": distros, + "examples": [ + dedent( + """\ + yum_repos: + my_repo: + baseurl: http://blah.org/pub/epel/testing/5/$basearch/ + yum_repo_dir: /store/custom/yum.repos.d + """ + ), + dedent( + f"""\ + # Enable cloud-init upstream's daily testing repo for EPEL 8 to + # install latest cloud-init from tip of `main` for testing. + yum_repos: + cloud-init-daily: + name: Copr repo for cloud-init-dev owned by @cloud-init + baseurl: {COPR_BASEURL} + type: rpm-md + skip_if_unavailable: true + gpgcheck: true + gpgkey: {COPR_GPG_URL} + enabled_metadata: 1 + """ + ), + dedent( + f"""\ + # Add the file /etc/yum.repos.d/epel_testing.repo which can then + # subsequently be used by yum for later operations. + yum_repos: + # The name of the repository + epel-testing: + baseurl: {EPEL_TESTING_BASEURL} + enabled: false + failovermethod: priority + gpgcheck: true + gpgkey: file:///etc/pki/rpm-gpg/RPM-GPG-KEY-EPEL + name: Extra Packages for Enterprise Linux 5 - Testing + """ + ), + dedent( + """\ + # Any yum repo configuration can be passed directly into + # the repository file created. See: man yum.conf for supported + # config keys. + # + # Write /etc/yum.conf.d/my_package_stream.repo with gpgkey checks + # on the repo data of the repositoy enabled. + yum_repos: + my package stream: + baseurl: http://blah.org/pub/epel/testing/5/$basearch/ + mirrorlist: http://some-url-to-list-of-baseurls + repo_gpgcheck: 1 + enable_gpgcheck: true + gpgkey: https://url.to.ascii-armored-gpg-key + """ + ), + ], + "frequency": PER_INSTANCE, +} + +__doc__ = get_meta_doc(meta) + def _canonicalize_id(repo_id): repo_id = repo_id.lower().replace("-", "_") diff --git a/cloudinit/config/cc_zypper_add_repo.py b/cloudinit/config/cc_zypper_add_repo.py index be444cce083..9b682bc6495 100644 --- a/cloudinit/config/cc_zypper_add_repo.py +++ b/cloudinit/config/cc_zypper_add_repo.py @@ -3,7 +3,7 @@ # # This file is part of cloud-init. See LICENSE file for license information. -"""zypper_add_repo: Add zyper repositories to the system""" +"""zypper_add_repo: Add zypper repositories to the system""" import os from textwrap import dedent @@ -16,22 +16,25 @@ from cloudinit.settings import PER_ALWAYS distros = ["opensuse", "sles"] - +MODULE_DESCRIPTION = """\ +Zypper behavior can be configured using the ``config`` key, which will modify +``/etc/zypp/zypp.conf``. The configuration writer will only append the +provided configuration options to the configuration file. Any duplicate +options will be resolved by the way the zypp.conf INI file is parsed. + +.. note:: + Setting ``configdir`` is not supported and will be skipped. + +The ``repos`` key may be used to add repositories to the system. Beyond the +required ``id`` and ``baseurl`` attributions, no validation is performed +on the ``repos`` entries. It is assumed the user is familiar with the +zypper repository file format. +""" meta: MetaSchema = { "id": "cc_zypper_add_repo", - "name": "ZypperAddRepo", + "name": "Zypper Add Repo", "title": "Configure zypper behavior and add zypper repositories", - "description": dedent( - """\ - Configure zypper behavior by modifying /etc/zypp/zypp.conf. The - configuration writer is "dumb" and will simply append the provided - configuration options to the configuration file. Option settings - that may be duplicate will be resolved by the way the zypp.conf file - is parsed. The file is in INI format. - Add repositories to the system. No validation is performed on the - repository file entries, it is assumed the user is familiar with - the zypper repository file format.""" - ), + "description": MODULE_DESCRIPTION, "distros": distros, "examples": [ dedent( @@ -60,53 +63,7 @@ "frequency": PER_ALWAYS, } -schema = { - "type": "object", - "properties": { - "zypper": { - "type": "object", - "properties": { - "repos": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": dedent( - """\ - The unique id of the repo, used when - writing - /etc/zypp/repos.d/.repo.""" - ), - }, - "baseurl": { - "type": "string", - "format": "uri", # built-in format type - "description": "The base repositoy URL", - }, - }, - "required": ["id", "baseurl"], - "additionalProperties": True, - }, - "minItems": 1, - }, - "config": { - "type": "object", - "description": dedent( - """\ - Any supported zypo.conf key is written to - /etc/zypp/zypp.conf'""" - ), - }, - }, - "minProperties": 1, # Either config or repo must be provided - "additionalProperties": False, # only repos and config allowed - } - }, -} - -__doc__ = get_meta_doc(meta, schema) # Supplement python help() +__doc__ = get_meta_doc(meta) LOG = logging.getLogger(__name__) diff --git a/cloudinit/config/cloud-init-schema.json b/cloudinit/config/cloud-init-schema.json index 88cabd1d476..02a73c0615b 100644 --- a/cloudinit/config/cloud-init-schema.json +++ b/cloudinit/config/cloud-init-schema.json @@ -1762,6 +1762,228 @@ "description": "The timezone to use as represented in /usr/share/zoneinfo" } } + }, + "cc_ubuntu_advantage": { + "type": "object", + "properties": { + "ubuntu_advantage": { + "type": "object", + "properties": { + "enable": { + "type": "array", + "items": {"type": "string"}, + "description": "Optional list of ubuntu-advantage services to enable. Any of: cc-eal, cis, esm-infra, fips, fips-updates, livepatch. By default, a given contract token will automatically enable a number of services, use this list to supplement which services should additionally be enabled. Any service unavailable on a given Ubuntu release or unentitled in a given contract will remain disabled." + }, + "token": { + "type": "string", + "description": "Required contract token obtained from https://ubuntu.com/advantage to attach." + } + }, + "required": ["token"], + "additionalProperties": false + } + } + }, + "cc_ubuntu_drivers": { + "type": "object", + "properties": { + "drivers": { + "type": "object", + "additionalProperties": false, + "properties": { + "nvidia": { + "type": "object", + "additionalProperties": false, + "required": [ + "license-accepted" + ], + "properties": { + "license-accepted": { + "type": "boolean", + "description": "Do you accept the NVIDIA driver license?" + }, + "version": { + "type": "string", + "description": "The version of the driver to install (e.g. \"390\", \"410\"). Defaults to the latest version." + } + } + } + } + } + } + }, + "cc_update_etc_hosts": { + "type": "object", + "properties": { + "manage_etc_hosts": { + "default": false, + "description": "Whether to manage ``/etc/hosts`` on the system. If ``true``, render the hosts file using ``/etc/cloud/templates/hosts.tmpl`` replacing ``$hostname`` and ``$fdqn``. If ``localhost``, append a ``127.0.1.1`` entry that resolves from FQDN and hostname every boot. Default: ``false``. DEPRECATED value ``template`` will be dropped, use ``true`` instead.", + "enum": [true, false, "template", "localhost"] + }, + "fqdn": { + "type": "string", + "description": "Optional fully qualified domain name to use when updating ``/etc/hosts``. Preferred over ``hostname`` if both are provided. In absence of ``hostname`` and ``fqdn`` in cloud-config, the ``local-hostname`` value will be used from datasource metadata." + }, + "hostname": { + "type": "string", + "description": "Hostname to set when rendering ``/etc/hosts``. If ``fqdn`` is set, the hostname extracted from ``fqdn`` overrides ``hostname``." + } + } + }, + "cc_update_hostname": { + "type": "object", + "properties": { + "preserve_hostname": { + "type": "boolean", + "default": false, + "description": "Do not update system hostname when ``true``. Default: ``false``." + }, + "prefer_fqdn_over_hostname": { + "type": "boolean", + "default": null, + "description": "By default, it is distro-dependent whether cloud-init uses the short hostname or fully qualified domain name when both ``local-hostname` and ``fqdn`` are both present in instance metadata. When set ``true``, use fully qualified domain name if present as hostname instead of short hostname. When set ``false``, use ``hostname`` config value if present, otherwise fallback to ``fqdn``." + } + } + }, + "cc_write_files": { + "type": "object", + "properties": { + "write_files": { + "type": "array", + "items": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "Path of the file to which ``content`` is decoded and written" + }, + "content": { + "type": "string", + "default": "", + "description": "Optional content to write to the provided ``path``. When content is present and encoding is not 'text/plain', decode the content prior to writing. Default: ``''``" + }, + "owner": { + "type": "string", + "default": "root:root", + "description": "Optional owner:group to chown on the file. Default: ``root:root``" + }, + "permissions": { + "type": "string", + "default": "0o644", + "description": "Optional file permissions to set on ``path`` represented as an octal string '0###'. Default: ``0o644``" + }, + "encoding": { + "type": "string", + "default": "text/plain", + "enum": ["gz", "gzip", "gz+base64", "gzip+base64", "gz+b64", "gzip+b64", "b64", "base64"], + "description": "Optional encoding type of the content. Default is ``text/plain`` and no content decoding is performed. Supported encoding types are: gz, gzip, gz+base64, gzip+base64, gz+b64, gzip+b64, b64, base64" + }, + "append": { + "type": "boolean", + "default": false, + "description": "Whether to append ``content`` to existing file if ``path`` exists. Default: ``false``." + }, + "defer": { + "type": "boolean", + "default": false, + "description": "Defer writing the file until 'final' stage, after users were created, and packages were installed. Default: ``false``." + } + }, + "required": ["path"], + "additionalProperties": false + }, + "minItems": 1 + } + } + }, + "cc_yum_add_repo": { + "type": "object", + "properties": { + "yum_repo_dir": { + "type": "string", + "default": "/etc/yum.repos.d", + "description": "The repo parts directory where individual yum repo config files will be written. Default: ``/etc/yum.repos.d``" + }, + "yum_repos": { + "type": "object", + "minProperties": 1, + "patternProperties": { + "^[0-9a-zA-Z -_]+$": { + "label": "", + "type": "object", + "description": "Object keyed on unique yum repo IDs. The key used will be used to write yum repo config files in ``yum_repo_dir``/.repo.", + "properties": { + "baseurl": { + "type": "string", + "format": "uri", + "description": "URL to the directory where the yum repository's 'repodata' directory lives" + }, + "name": { + "type": "string", + "description": "Optional human-readable name of the yum repo." + }, + "enabled": { + "type": "boolean", + "default": true, + "description": "Whether to enable the repo. Default: ``true``." + } + }, + "patternProperties": { + "^[0-9a-zA-Z_]+$": { + "label": "", + "oneOf": [ + {"type": "integer"}, + {"type": "boolean"}, + {"type": "string"} + ], + "description": "Any supported yum repository configuration options will be written to the yum repo config file. See: man yum.conf" + } + }, + "required": ["baseurl"] + } + }, + "additionalProperties": false + } + } + }, + "cc_zypper_add_repo": { + "type": "object", + "properties": { + "zypper": { + "type": "object", + "properties": { + "repos": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The unique id of the repo, used when writing /etc/zypp/repos.d/.repo." + }, + "baseurl": { + "type": "string", + "format": "uri", + "description": "The base repositoy URL" + } + }, + "required": [ + "id", + "baseurl" + ], + "additionalProperties": true + }, + "minItems": 1 + }, + "config": { + "type": "object", + "description": "Any supported zypo.conf key is written to ``/etc/zypp/zypp.conf``" + } + }, + "minProperties": 1, + "additionalProperties": false + } + } } }, "allOf": [ @@ -1807,6 +2029,13 @@ { "$ref": "#/$defs/cc_ssh_authkey_fingerprints"}, { "$ref": "#/$defs/cc_ssh_import_id"}, { "$ref": "#/$defs/cc_ssh"}, - { "$ref": "#/$defs/cc_timezone"} + { "$ref": "#/$defs/cc_timezone"}, + { "$ref": "#/$defs/cc_ubuntu_advantage"}, + { "$ref": "#/$defs/cc_ubuntu_drivers"}, + { "$ref": "#/$defs/cc_update_etc_hosts"}, + { "$ref": "#/$defs/cc_update_hostname"}, + { "$ref": "#/$defs/cc_write_files"}, + { "$ref": "#/$defs/cc_yum_add_repo"}, + { "$ref": "#/$defs/cc_zypper_add_repo"} ] } diff --git a/doc/examples/cloud-config.txt b/doc/examples/cloud-config.txt index 42c26f21867..ec9a51d8766 100644 --- a/doc/examples/cloud-config.txt +++ b/doc/examples/cloud-config.txt @@ -315,7 +315,7 @@ resize_rootfs: True # * Whatever is present at instance boot time will be present after boot. # * User changes will not be overwritten # -# true or 'template': +# true: # on every boot, /etc/hosts will be re-written from # /etc/cloud/templates/hosts.tmpl. # The strings '$hostname' and '$fqdn' are replaced in the template diff --git a/doc/rtd/topics/modules.rst b/doc/rtd/topics/modules.rst index 355d4b9ac2e..80a136bedc7 100644 --- a/doc/rtd/topics/modules.rst +++ b/doc/rtd/topics/modules.rst @@ -63,4 +63,5 @@ Modules .. automodule:: cloudinit.config.cc_users_groups .. automodule:: cloudinit.config.cc_write_files .. automodule:: cloudinit.config.cc_yum_add_repo +.. automodule:: cloudinit.config.cc_zypper_add_repo .. vi: textwidth=79 diff --git a/tests/unittests/config/test_cc_ubuntu_advantage.py b/tests/unittests/config/test_cc_ubuntu_advantage.py index 2037c5ed86b..0c5544e1301 100644 --- a/tests/unittests/config/test_cc_ubuntu_advantage.py +++ b/tests/unittests/config/test_cc_ubuntu_advantage.py @@ -1,19 +1,20 @@ # This file is part of cloud-init. See LICENSE file for license information. +import re + +import pytest from cloudinit import subp from cloudinit.config.cc_ubuntu_advantage import ( configure_ua, handle, maybe_install_ua_tools, - schema, ) -from cloudinit.config.schema import validate_cloudconfig_schema -from tests.unittests.helpers import ( - CiTestCase, - SchemaTestCaseMixin, - mock, - skipUnlessJsonSchema, +from cloudinit.config.schema import ( + SchemaValidationError, + get_schema, + validate_cloudconfig_schema, ) +from tests.unittests.helpers import CiTestCase, mock, skipUnlessJsonSchema # Module path used in mocks MPATH = "cloudinit.config.cc_ubuntu_advantage" @@ -172,64 +173,28 @@ def test_configure_ua_attach_with_weird_services(self, m_subp): ) -@skipUnlessJsonSchema() -class TestSchema(CiTestCase, SchemaTestCaseMixin): - - with_logs = True - schema = schema - - @mock.patch("%s.maybe_install_ua_tools" % MPATH) - @mock.patch("%s.configure_ua" % MPATH) - def test_schema_warns_on_ubuntu_advantage_not_dict(self, _cfg, _): - """If ubuntu_advantage configuration is not a dict, emit a warning.""" - validate_cloudconfig_schema({"ubuntu_advantage": "wrong type"}, schema) - self.assertEqual( - "WARNING: Invalid cloud-config provided:\nubuntu_advantage:" - " 'wrong type' is not of type 'object'\n", - self.logs.getvalue(), - ) - - @mock.patch("%s.maybe_install_ua_tools" % MPATH) - @mock.patch("%s.configure_ua" % MPATH) - def test_schema_disallows_unknown_keys(self, _cfg, _): - """Unknown keys in ubuntu_advantage configuration emit warnings.""" - validate_cloudconfig_schema( - {"ubuntu_advantage": {"token": "winner", "invalid-key": ""}}, - schema, - ) - self.assertIn( - "WARNING: Invalid cloud-config provided:\nubuntu_advantage:" - " Additional properties are not allowed ('invalid-key' was" - " unexpected)", - self.logs.getvalue(), - ) - - @mock.patch("%s.maybe_install_ua_tools" % MPATH) - @mock.patch("%s.configure_ua" % MPATH) - def test_warn_schema_requires_token(self, _cfg, _): - """Warn if ubuntu_advantage configuration lacks token.""" - validate_cloudconfig_schema( - {"ubuntu_advantage": {"enable": ["esm"]}}, schema - ) - self.assertEqual( - "WARNING: Invalid cloud-config provided:\nubuntu_advantage:" - " 'token' is a required property\n", - self.logs.getvalue(), - ) - - @mock.patch("%s.maybe_install_ua_tools" % MPATH) - @mock.patch("%s.configure_ua" % MPATH) - def test_warn_schema_services_is_not_list_or_dict(self, _cfg, _): - """Warn when ubuntu_advantage:enable config is not a list.""" - validate_cloudconfig_schema( - {"ubuntu_advantage": {"enable": "needslist"}}, schema - ) - self.assertEqual( - "WARNING: Invalid cloud-config provided:\nubuntu_advantage:" - " 'token' is a required property\nubuntu_advantage.enable:" - " 'needslist' is not of type 'array'\n", - self.logs.getvalue(), - ) +class TestUbuntuAdvantageSchema: + @pytest.mark.parametrize( + "config, error_msg", + [ + ({"ubuntu_advantage": {}}, "'token' is a required property"), + # Strict keys + ( + {"ubuntu_advantage": {"token": "win", "invalidkey": ""}}, + re.escape( + "ubuntu_advantage: Additional properties are not allowed" + " ('invalidkey" + ), + ), + ], + ) + @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): @@ -240,8 +205,8 @@ def setUp(self): super(TestHandle, self).setUp() self.tmp = self.tmp_dir() - @mock.patch("%s.validate_cloudconfig_schema" % MPATH) - def test_handle_no_config(self, m_schema): + @mock.patch("%s.maybe_install_ua_tools" % MPATH) + def test_handle_no_config(self, m_maybe_install_ua_tools): """When no ua-related configuration is provided, nothing happens.""" cfg = {} handle("ua-test", cfg=cfg, cloud=None, log=self.logger, args=None) @@ -250,7 +215,7 @@ def test_handle_no_config(self, m_schema): " configuration found", self.logs.getvalue(), ) - m_schema.assert_not_called() + self.assertEqual(m_maybe_install_ua_tools.call_count, 0) @mock.patch("%s.configure_ua" % MPATH) @mock.patch("%s.maybe_install_ua_tools" % MPATH) diff --git a/tests/unittests/config/test_cc_ubuntu_drivers.py b/tests/unittests/config/test_cc_ubuntu_drivers.py index 4987492d842..3cbde8b2075 100644 --- a/tests/unittests/config/test_cc_ubuntu_drivers.py +++ b/tests/unittests/config/test_cc_ubuntu_drivers.py @@ -2,10 +2,14 @@ import copy import os +import re + +import pytest from cloudinit.config import cc_ubuntu_drivers as drivers from cloudinit.config.schema import ( SchemaValidationError, + get_schema, validate_cloudconfig_schema, ) from cloudinit.subp import ProcessExecutionError @@ -47,17 +51,6 @@ class TestUbuntuDrivers(CiTestCase): with_logs = True - @skipUnlessJsonSchema() - def test_schema_requires_boolean_for_license_accepted(self): - with self.assertRaisesRegex( - SchemaValidationError, ".*license-accepted.*TRUE.*boolean" - ): - validate_cloudconfig_schema( - {"drivers": {"nvidia": {"license-accepted": "TRUE"}}}, - schema=drivers.schema, - strict=True, - ) - @mock.patch(M_TMP_PATH) @mock.patch(MPATH + "subp.subp", return_value=("", "")) @mock.patch(MPATH + "subp.which", return_value=False) @@ -290,4 +283,39 @@ def test_specifying_a_version_doesnt_override_license_acceptance(self): ) +class TestUbuntuAdvantageSchema: + @pytest.mark.parametrize( + "config, error_msg", + [ + # Strict boolean license-accepted + ( + {"drivers": {"nvidia": {"license-accepted": "TRUE"}}}, + "drivers.nvidia.license-accepted: 'TRUE' is not of type" + " 'boolean'", + ), + # Additional properties disallowed + ( + {"drivers": {"bogus": {"license-accepted": True}}}, + re.escape( + "drivers: Additional properties are not allowed ('bogus'" + ), + ), + ( + {"drivers": {"nvidia": {"bogus": True}}}, + re.escape( + "drivers.nvidia: Additional properties are not allowed" + " ('bogus' " + ), + ), + ], + ) + @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_update_etc_hosts.py b/tests/unittests/config/test_cc_update_etc_hosts.py index 2bbc16f439e..f7aafe4623f 100644 --- a/tests/unittests/config/test_cc_update_etc_hosts.py +++ b/tests/unittests/config/test_cc_update_etc_hosts.py @@ -2,10 +2,18 @@ import logging import os +import re import shutil +import pytest + from cloudinit import cloud, distros, helpers, util from cloudinit.config import cc_update_etc_hosts +from cloudinit.config.schema import ( + SchemaValidationError, + get_schema, + validate_cloudconfig_schema, +) from tests.unittests import helpers as t_help LOG = logging.getLogger(__name__) @@ -66,3 +74,25 @@ def test_write_etc_hosts_suse_template(self): self.assertIsNone("No entry for 127.0.1.1 in etc/hosts") if "::1 cloud-init.test.us cloud-init" not in contents: self.assertIsNone("No entry for 127.0.0.1 in etc/hosts") + + +class TestUpdateEtcHosts: + @pytest.mark.parametrize( + "config, error_msg", + [ + ( + {"manage_etc_hosts": "templatey"}, + re.escape( + "manage_etc_hosts: 'templatey' is not one of" + " [True, False, 'template', 'localhost']" + ), + ), + ], + ) + @t_help.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_write_files.py b/tests/unittests/config/test_cc_write_files.py index faea5885aa2..956e4327579 100644 --- a/tests/unittests/config/test_cc_write_files.py +++ b/tests/unittests/config/test_cc_write_files.py @@ -1,19 +1,25 @@ # This file is part of cloud-init. See LICENSE file for license information. import base64 -import copy import gzip import io +import re import shutil import tempfile +import pytest + from cloudinit import log as logging from cloudinit import util from cloudinit.config.cc_write_files import decode_perms, handle, write_files +from cloudinit.config.schema import ( + SchemaValidationError, + get_schema, + validate_cloudconfig_schema, +) from tests.unittests.helpers import ( CiTestCase, FilesystemMockingTestCase, - mock, skipUnlessJsonSchema, ) @@ -55,74 +61,6 @@ ] } -INVALID_SCHEMA = { # Dropped required path key - "write_files": [ - { - "append": False, - "content": "a", - "encoding": "gzip", - "owner": "jeff", - "permissions": "0777", - } - ] -} - - -@skipUnlessJsonSchema() -@mock.patch("cloudinit.config.cc_write_files.write_files") -class TestWriteFilesSchema(CiTestCase): - - with_logs = True - - def test_schema_validation_warns_missing_path(self, m_write_files): - """The only required file item property is 'path'.""" - cc = self.tmp_cloud("ubuntu") - valid_config = {"write_files": [{"path": "/some/path"}]} - handle("cc_write_file", valid_config, cc, LOG, []) - self.assertNotIn( - "Invalid cloud-config provided:", self.logs.getvalue() - ) - handle("cc_write_file", INVALID_SCHEMA, cc, LOG, []) - self.assertIn("Invalid cloud-config provided:", self.logs.getvalue()) - self.assertIn("'path' is a required property", self.logs.getvalue()) - - def test_schema_validation_warns_non_string_type_for_files( - self, m_write_files - ): - """Schema validation warns of non-string values for each file item.""" - cc = self.tmp_cloud("ubuntu") - for key in VALID_SCHEMA["write_files"][0].keys(): - if key == "append": - key_type = "boolean" - else: - key_type = "string" - invalid_config = copy.deepcopy(VALID_SCHEMA) - invalid_config["write_files"][0][key] = 1 - handle("cc_write_file", invalid_config, cc, LOG, []) - self.assertIn( - mock.call("cc_write_file", invalid_config["write_files"]), - m_write_files.call_args_list, - ) - self.assertIn( - "write_files.0.%s: 1 is not of type '%s'" % (key, key_type), - self.logs.getvalue(), - ) - self.assertIn("Invalid cloud-config provided:", self.logs.getvalue()) - - def test_schema_validation_warns_on_additional_undefined_propertes( - self, m_write_files - ): - """Schema validation warns on additional undefined file properties.""" - cc = self.tmp_cloud("ubuntu") - invalid_config = copy.deepcopy(VALID_SCHEMA) - invalid_config["write_files"][0]["bogus"] = "value" - handle("cc_write_file", invalid_config, cc, LOG, []) - self.assertIn( - "Invalid cloud-config provided:\nwrite_files.0: Additional" - " properties are not allowed ('bogus' was unexpected)", - self.logs.getvalue(), - ) - class TestWriteFiles(FilesystemMockingTestCase): @@ -133,19 +71,6 @@ def setUp(self): self.tmp = tempfile.mkdtemp() self.addCleanup(shutil.rmtree, self.tmp) - @skipUnlessJsonSchema() - def test_handler_schema_validation_warns_non_array_type(self): - """Schema validation warns of non-array value.""" - invalid_config = {"write_files": 1} - cc = self.tmp_cloud("ubuntu") - with self.assertRaises(TypeError): - handle("cc_write_file", invalid_config, cc, LOG, []) - self.assertIn( - "Invalid cloud-config provided:\nwrite_files: 1 is not of type" - " 'array'", - self.logs.getvalue(), - ) - def test_simple(self): self.patchUtils(self.tmp) expected = "hello world\n" @@ -264,4 +189,36 @@ def _gzip_bytes(data): fp.close() +class TestWriteFilesSchema: + @pytest.mark.parametrize( + "config, error_msg", + [ + # Top-level write_files type validation + ({"write_files": 1}, "write_files: 1 is not of type 'array'"), + ({"write_files": []}, re.escape("write_files: [] is too short")), + ( + {"write_files": [{}]}, + "write_files.0: 'path' is a required property", + ), + ( + {"write_files": [{"path": "/some", "bogus": True}]}, + re.escape( + "write_files.0: Additional properties are not allowed" + " ('bogus'" + ), + ), + ( # Strict encoding choices + {"write_files": [{"path": "/some", "encoding": "g"}]}, + re.escape( + "write_files.0.encoding: 'g' is not one of ['gz', 'gzip'," + ), + ), + ], + ) + @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_cc_write_files_deferred.py b/tests/unittests/config/test_cc_write_files_deferred.py index 172032334bd..ed2056bb623 100644 --- a/tests/unittests/config/test_cc_write_files_deferred.py +++ b/tests/unittests/config/test_cc_write_files_deferred.py @@ -3,56 +3,24 @@ import shutil import tempfile +import pytest + from cloudinit import log as logging from cloudinit import util from cloudinit.config.cc_write_files_deferred import handle +from cloudinit.config.schema import ( + SchemaValidationError, + get_schema, + validate_cloudconfig_schema, +) from tests.unittests.helpers import ( - CiTestCase, FilesystemMockingTestCase, - mock, skipUnlessJsonSchema, ) -from .test_cc_write_files import VALID_SCHEMA - LOG = logging.getLogger(__name__) -@skipUnlessJsonSchema() -@mock.patch("cloudinit.config.cc_write_files_deferred.write_files") -class TestWriteFilesDeferredSchema(CiTestCase): - - with_logs = True - - def test_schema_validation_warns_invalid_value( - self, m_write_files_deferred - ): - """If 'defer' is defined, it must be of type 'bool'.""" - - valid_config = { - "write_files": [ - {**VALID_SCHEMA.get("write_files")[0], "defer": True} - ] - } - - invalid_config = { - "write_files": [ - {**VALID_SCHEMA.get("write_files")[0], "defer": str("no")} - ] - } - - cc = self.tmp_cloud("ubuntu") - handle("cc_write_files_deferred", valid_config, cc, LOG, []) - self.assertNotIn( - "Invalid cloud-config provided:", self.logs.getvalue() - ) - handle("cc_write_files_deferred", invalid_config, cc, LOG, []) - self.assertIn("Invalid cloud-config provided:", self.logs.getvalue()) - self.assertIn( - "defer: 'no' is not of type 'boolean'", self.logs.getvalue() - ) - - class TestWriteFilesDeferred(FilesystemMockingTestCase): with_logs = True @@ -82,4 +50,21 @@ def test_filtering_deferred_files(self): util.load_file("/tmp/not_deferred.file") +class TestWriteFilesDeferredSchema: + @pytest.mark.parametrize( + "config, error_msg", + [ + # Allow undocumented keys client keys without error + ( + {"write_files": [{"defer": "no"}]}, + "write_files.0.defer: 'no' is not of type 'boolean'", + ), + ], + ) + @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_cc_yum_add_repo.py b/tests/unittests/config/test_cc_yum_add_repo.py index 550b0af245f..d6de2ec2155 100644 --- a/tests/unittests/config/test_cc_yum_add_repo.py +++ b/tests/unittests/config/test_cc_yum_add_repo.py @@ -2,11 +2,19 @@ import configparser import logging +import re import shutil import tempfile +import pytest + from cloudinit import util from cloudinit.config import cc_yum_add_repo +from cloudinit.config.schema import ( + SchemaValidationError, + get_schema, + validate_cloudconfig_schema, +) from tests.unittests import helpers LOG = logging.getLogger(__name__) @@ -117,4 +125,40 @@ def test_write_config_array(self): self.assertEqual(parser.get(section, k), v) +class TestAddYumRepoSchema: + @pytest.mark.parametrize( + "config, error_msg", + [ + # Happy path case + ({"yum_repos": {"My-Repo 123": {"baseurl": "http://doit"}}}, None), + # yum_repo_dir is a string + ( + {"yum_repo_dir": True}, + "yum_repo_dir: True is not of type 'string'", + ), + ( + {"yum_repos": {}}, + re.escape("yum_repos: {} does not have enough properties"), + ), + # baseurl required + ( + {"yum_repos": {"My-Repo": {}}}, + "yum_repos.My-Repo: 'baseurl' is a required", + ), + # patternProperties don't override type of explicit property names + ( + {"yum_repos": {"My Repo": {"enabled": "nope"}}}, + "yum_repos.My Repo.enabled: 'nope' is not of type 'boolean'", + ), + ], + ) + @helpers.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_schema.py b/tests/unittests/config/test_schema.py index 304231b3a2f..7630290a852 100644 --- a/tests/unittests/config/test_schema.py +++ b/tests/unittests/config/test_schema.py @@ -146,7 +146,10 @@ def test_get_schema_coalesces_known_schema(self): "cc_timezone", "cc_ubuntu_advantage", "cc_ubuntu_drivers", + "cc_update_etc_hosts", + "cc_update_hostname", "cc_write_files", + "cc_yum_add_repo", "cc_zypper_add_repo", ] ) == sorted( @@ -199,6 +202,13 @@ def test_get_schema_coalesces_known_schema(self): {"$ref": "#/$defs/cc_ssh_import_id"}, {"$ref": "#/$defs/cc_ssh"}, {"$ref": "#/$defs/cc_timezone"}, + {"$ref": "#/$defs/cc_ubuntu_advantage"}, + {"$ref": "#/$defs/cc_ubuntu_drivers"}, + {"$ref": "#/$defs/cc_update_etc_hosts"}, + {"$ref": "#/$defs/cc_update_hostname"}, + {"$ref": "#/$defs/cc_write_files"}, + {"$ref": "#/$defs/cc_yum_add_repo"}, + {"$ref": "#/$defs/cc_zypper_add_repo"}, ] found_subschema_defs = [] legacy_schema_keys = [] @@ -211,12 +221,7 @@ def test_get_schema_coalesces_known_schema(self): assert expected_subschema_defs == found_subschema_defs # This list will dwindle as we move legacy schema to new $defs assert [ - "drivers", "ntp", - "ubuntu_advantage", - "write_files", - "write_files", - "zypper", ] == sorted(legacy_schema_keys) @@ -229,7 +234,7 @@ class TestLoadDoc: "module_name", ( "cc_apt_pipelining", # new style composite schema file - "cc_zypper_add_repo", # legacy sub-schema defined in module + "cc_install_hotplug", # legacy sub-schema defined in module ), ) def test_report_docs_for_legacy_and_consolidated_schema(self, module_name):