diff --git a/cloudinit/config/cc_resizefs.py b/cloudinit/config/cc_resizefs.py index 19b923a8a90..39da1b5a8aa 100644 --- a/cloudinit/config/cc_resizefs.py +++ b/cloudinit/config/cc_resizefs.py @@ -14,18 +14,12 @@ from textwrap import dedent 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.distros import ALL_DISTROS from cloudinit.settings import PER_ALWAYS NOBLOCK = "noblock" -frequency = PER_ALWAYS -distros = ["all"] - meta: MetaSchema = { "id": "cc_resizefs", "name": "Resizefs", @@ -39,30 +33,18 @@ partition and will block the boot process while the resize command is running. Optionally, the resize operation can be performed in the background while cloud-init continues running modules. This can be - enabled by setting ``resize_rootfs`` to ``true``. This module can be + enabled by setting ``resize_rootfs`` to ``noblock``. This module can be disabled altogether by setting ``resize_rootfs`` to ``false``.""" ), - "distros": distros, + "distros": [ALL_DISTROS], "examples": [ - "resize_rootfs: false # disable root filesystem resize operation" + "resize_rootfs: false # disable root filesystem resize operation", + "resize_rootfs: noblock # runs resize operation in the background", ], "frequency": PER_ALWAYS, } -schema = { - "type": "object", - "properties": { - "resize_rootfs": { - "enum": [True, False, NOBLOCK], - "description": dedent( - """\ - Whether to resize the root partition. Default: 'true'""" - ), - } - }, -} - -__doc__ = get_meta_doc(meta, schema) # Supplement python help() +__doc__ = get_meta_doc(meta) def _resize_btrfs(mount_point, devpth): @@ -229,7 +211,6 @@ def handle(name, cfg, _cloud, log, args): resize_root = args[0] else: resize_root = util.get_cfg_option_str(cfg, "resize_rootfs", True) - validate_cloudconfig_schema(cfg, schema) if not util.translate_bool(resize_root, addons=[NOBLOCK]): log.debug("Skipping module named %s, resizing disabled", name) return diff --git a/cloudinit/config/cc_resolv_conf.py b/cloudinit/config/cc_resolv_conf.py index b2970d516c3..bbf68079260 100644 --- a/cloudinit/config/cc_resolv_conf.py +++ b/cloudinit/config/cc_resolv_conf.py @@ -6,18 +6,38 @@ # # This file is part of cloud-init. See LICENSE file for license information. -""" -Resolv Conf ------------ -**Summary:** configure resolv.conf +"""Resolv Conf: configure resolv.conf""" + +from textwrap import dedent +from cloudinit import log as logging +from cloudinit import templater, util +from cloudinit.config.schema import MetaSchema, get_meta_doc +from cloudinit.settings import PER_INSTANCE + +LOG = logging.getLogger(__name__) + +RESOLVE_CONFIG_TEMPLATE_MAP = { + "/etc/resolv.conf": "resolv.conf", + "/etc/systemd/resolved.conf": "systemd.resolved.conf", +} + +MODULE_DESCRIPTION = """\ This module is intended to manage resolv.conf in environments where early configuration of resolv.conf is necessary for further bootstrapping and/or -where configuration management such as puppet or chef own dns configuration. +where configuration management such as puppet or chef own DNS configuration. As Debian/Ubuntu will, by default, utilize resolvconf, and similarly Red Hat will use sysconfig, this module is likely to be of little use unless those are configured correctly. +When using a :ref:`datasource_config_drive` and a RHEL-like system, +resolv.conf will also be managed automatically due to the available +information provided for DNS servers in the :ref:`network_config_v2` format. +For those that with to have different settings, use this module. + +In order for the ``resolv_conf`` section to be applied, ``manage_resolv_conf`` +must be set ``true``. + .. note:: For Red Hat with sysconfig, be sure to set PEERDNS=no for all DHCP enabled NICs. @@ -25,42 +45,40 @@ .. note:: And, in Ubuntu/Debian it is recommended that DNS be configured via the standard /etc/network/interfaces configuration file. - -**Internal name:** ``cc_resolv_conf`` - -**Module frequency:** per instance - -**Supported distros:** alpine, fedora, photon, rhel, sles - -**Config keys**:: - - manage_resolv_conf: - resolv_conf: - nameservers: ['8.8.4.4', '8.8.8.8'] - searchdomains: - - foo.example.com - - bar.example.com - domain: example.com - options: - rotate: - timeout: 1 """ -from cloudinit import log as logging -from cloudinit import templater, util -from cloudinit.settings import PER_INSTANCE - -LOG = logging.getLogger(__name__) - -frequency = PER_INSTANCE - -distros = ["alpine", "fedora", "opensuse", "photon", "rhel", "sles"] - -RESOLVE_CONFIG_TEMPLATE_MAP = { - "/etc/resolv.conf": "resolv.conf", - "/etc/systemd/resolved.conf": "systemd.resolved.conf", +meta: MetaSchema = { + "id": "cc_resolv_conf", + "name": "Resolv Conf", + "title": "Configure resolv.conf", + "description": MODULE_DESCRIPTION, + "distros": ["alpine", "fedora", "opensuse", "photon", "rhel", "sles"], + "frequency": PER_INSTANCE, + "examples": [ + dedent( + """\ + manage_resolv_conf: true + resolv_conf: + nameservers: + - 8.8.8.8 + - 8.8.4.4 + searchdomains: + - foo.example.com + - bar.example.com + domain: example.com + sortlist: + - 10.0.0.1/255 + - 10.0.0.2 + options: + rotate: true + timeout: 1 + """ + ) + ], } +__doc__ = get_meta_doc(meta) + def generate_resolv_conf(template_fn, params, target_fname): flags = [] diff --git a/cloudinit/config/cc_rh_subscription.py b/cloudinit/config/cc_rh_subscription.py index b81a7a9b4de..992bce01eaf 100644 --- a/cloudinit/config/cc_rh_subscription.py +++ b/cloudinit/config/cc_rh_subscription.py @@ -3,47 +3,76 @@ # Author: Brent Baude # # This file is part of cloud-init. See LICENSE file for license information. +"""Red Hat Subscription: Register Red Hat Enterprise Linux based system""" -""" -Red Hat Subscription --------------------- -**Summary:** register red hat enterprise linux based system - -Register a Red Hat system either by username and password *or* activation and -org. Following a sucessful registration, you can auto-attach subscriptions, set -the service level, add subscriptions based on pool id, enable/disable yum -repositories based on repo id, and alter the rhsm_baseurl and server-hostname -in ``/etc/rhsm/rhs.conf``. For more details, see the ``Register Red Hat -Subscription`` example config. - -**Internal name:** ``cc_rh_subscription`` - -**Module frequency:** per instance - -**Supported distros:** rhel, fedora - -**Config keys**:: - - rh_subscription: - username: - password: - activation-key: - org: - auto-attach: - service-level: - add-pool: - enable-repo: - disable-repo: - rhsm-baseurl: - server-hostname: -""" +from textwrap import dedent from cloudinit import log as logging from cloudinit import subp, util +from cloudinit.config.schema import MetaSchema, get_meta_doc +from cloudinit.settings import PER_INSTANCE LOG = logging.getLogger(__name__) -distros = ["fedora", "rhel"] +MODULE_DESCRIPTION = """\ +Register a Red Hat system either by username and password *or* activation and +org. Following a sucessful registration, you can: + - auto-attach subscriptions + - set the service level + - add subscriptions based on pool id + - enable/disable yum repositories based on repo id + - alter the rhsm_baseurl and server-hostname in ``/etc/rhsm/rhs.conf``. +""" + +meta: MetaSchema = { + "id": "cc_rh_subscription", + "name": "Red Hat Subscription", + "title": "Register Red Hat Enterprise Linux based system", + "description": MODULE_DESCRIPTION, + "distros": ["fedora", "rhel"], + "frequency": PER_INSTANCE, + "examples": [ + dedent( + """\ + rh_subscription: + username: joe@foo.bar + ## Quote your password if it has symbols to be safe + password: '1234abcd' + """ + ), + dedent( + """\ + rh_subscription: + activation-key: foobar + org: 12345 + """ + ), + dedent( + """\ + rh_subscription: + activation-key: foobar + org: 12345 + auto-attach: true + service-level: self-support + add-pool: + - 1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a + - 2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b + enable-repo: + - repo-id-to-enable + - other-repo-id-to-enable + disable-repo: + - repo-id-to-disable + - other-repo-id-to-disable + # Alter the baseurl in /etc/rhsm/rhsm.conf + rhsm-baseurl: http://url + # Alter the server hostname in /etc/rhsm/rhsm.conf + server-hostname: foo.bar.com + """ + ), + ], +} + +__doc__ = get_meta_doc(meta) def handle(name, cfg, _cloud, log, _args): diff --git a/cloudinit/config/cc_rightscale_userdata.py b/cloudinit/config/cc_rightscale_userdata.py index 36a009a2fae..c1b0f8bd3b4 100644 --- a/cloudinit/config/cc_rightscale_userdata.py +++ b/cloudinit/config/cc_rightscale_userdata.py @@ -6,13 +6,23 @@ # # This file is part of cloud-init. See LICENSE file for license information. -""" -Rightscale Userdata -------------------- -**Summary:** support rightscale configuration hooks +import os +from urllib.parse import parse_qs + +from cloudinit import url_helper as uhelp +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 +MY_NAME = "cc_rightscale_userdata" +MY_HOOKNAME = "CLOUD_INIT_REMOTE_HOOK" + +"""Rightscale Userdata: Support rightscale configuration hooks""" + +MODULE_DESCRIPTION = """\ This module adds support for RightScale configuration hooks to cloud-init. -RightScale adds a entry in the format ``CLOUD_INIT_REMOTE_HOOK=http://...`` to +RightScale adds an entry in the format ``CLOUD_INIT_REMOTE_HOOK=http://...`` to ec2 user-data. This module checks for this line in the raw userdata and retrieves any scripts linked by the RightScale user data and places them in the user scripts configuration directory, to be run later by ``cc_scripts_user``. @@ -21,17 +31,23 @@ the ``CLOUD_INIT_REMOTE_HOOK`` config variable is present in the raw ec2 user data only, not in any cloud-config parts -**Internal name:** ``cc_rightscale_userdata`` - -**Module frequency:** per instance - -**Supported distros:** all - -**Config keys**:: +**Raw user data schema**:: CLOUD_INIT_REMOTE_HOOK= """ +meta: MetaSchema = { + "id": "cc_rightscale_userdata", + "name": "RightScale Userdata", + "title": "Support rightscale configuration hooks", + "description": MODULE_DESCRIPTION, + "distros": [ALL_DISTROS], + "frequency": PER_INSTANCE, + "examples": [], +} + +__doc__ = get_meta_doc(meta) + # # The purpose of this script is to allow cloud-init to consume # rightscale style userdata. rightscale user data is key-value pairs @@ -49,18 +65,6 @@ # # -import os -from urllib.parse import parse_qs - -from cloudinit import url_helper as uhelp -from cloudinit import util -from cloudinit.settings import PER_INSTANCE - -frequency = PER_INSTANCE - -MY_NAME = "cc_rightscale_userdata" -MY_HOOKNAME = "CLOUD_INIT_REMOTE_HOOK" - def handle(name, _cfg, cloud, log, _args): try: diff --git a/cloudinit/config/cc_rsyslog.py b/cloudinit/config/cc_rsyslog.py index db2a3c79496..3b351222d5f 100644 --- a/cloudinit/config/cc_rsyslog.py +++ b/cloudinit/config/cc_rsyslog.py @@ -6,183 +6,63 @@ # # This file is part of cloud-init. See LICENSE file for license information. -""" -.. _cc_rsyslog: +"""Rsyslog: Configure system logging via rsyslog""" -Rsyslog -------- -**Summary:** configure system logging via rsyslog +import os +import re +from textwrap import dedent -This module configures remote system logging using rsyslog. +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 -The rsyslog config file to write to can be specified in ``config_filename``, -which defaults to ``20-cloud-config.conf``. The rsyslog config directory to -write config files to may be specified in ``config_dir``, which defaults to -``/etc/rsyslog.d``. - -A list of configurations for rsyslog can be specified under the ``configs`` key -in the ``rsyslog`` config. Each entry in ``configs`` is either a string or a -dictionary. Each config entry contains a configuration string and a file to -write it to. For config entries that are a dictionary, ``filename`` sets the -target filename and ``content`` specifies the config string to write. For -config entries that are only a string, the string is used as the config string -to write. If the filename to write the config to is not specified, the value of -the ``config_filename`` key is used. A file with the selected filename will be -written inside the directory specified by ``config_dir``. - -The command to use to reload the rsyslog service after the config has been -updated can be specified in ``service_reload_command``. If this is set to -``auto``, then an appropriate command for the distro will be used. This is the -default behavior. To manually set the command, use a list of command args (e.g. -``[systemctl, restart, rsyslog]``). +MODULE_DESCRIPTION = """\ +This module configures remote system logging using rsyslog. Configuration for remote servers can be specified in ``configs``, but for -convenience it can be specified as key value pairs in ``remotes``. Each key -is the name for an rsyslog remote entry. Each value holds the contents of the -remote config for rsyslog. The config consists of the following parts: - - - filter for log messages (defaults to ``*.*``) - - optional leading ``@`` or ``@@``, indicating udp and tcp respectively - (defaults to ``@``, for udp) - - ipv4 or ipv6 hostname or address. ipv6 addresses must be in ``[::1]`` - format, (e.g. ``@[fd00::1]:514``) - - optional port number (defaults to ``514``) - -This module will provide sane defaults for any part of the remote entry that is -not specified, so in most cases remote hosts can be specified just using -``:
``. - -For backwards compatibility, this module still supports legacy names for the -config entries. Legacy to new mappings are as follows: - - - ``rsyslog`` -> ``rsyslog/configs`` - - ``rsyslog_filename`` -> ``rsyslog/config_filename`` - - ``rsyslog_dir`` -> ``rsyslog/config_dir`` - -.. note:: - The legacy config format does not support specifying - ``service_reload_command``. - -**Internal name:** ``cc_rsyslog`` - -**Module frequency:** per instance - -**Supported distros:** all - -**Config keys**:: - - rsyslog: - config_dir: config_dir - config_filename: config_filename - configs: - - "*.* @@192.158.1.1" - - content: "*.* @@192.0.2.1:10514" - filename: 01-example.conf - - content: | - *.* @@syslogd.example.com - remotes: - maas: "192.168.1.1" - juju: "10.0.4.1" - service_reload_command: [your, syslog, restart, command] - -**Legacy config keys**:: - - rsyslog: - - "*.* @@192.158.1.1" - rsyslog_dir: /etc/rsyslog-config.d/ - rsyslog_filename: 99-local.conf +convenience it can be specified as key value pairs in ``remotes``. """ -# Old rsyslog documentation, kept for reference: -# -# rsyslog module allows configuration of syslog logging via rsyslog -# Configuration is done under the cloud-config top level 'rsyslog'. -# -# Under 'rsyslog' you can define: -# - configs: [default=[]] -# this is a list. entries in it are a string or a dictionary. -# each entry has 2 parts: -# * content -# * filename -# if the entry is a string, then it is assigned to 'content'. -# for each entry, content is written to the provided filename. -# if filename is not provided, its default is read from 'config_filename' -# -# Content here can be any valid rsyslog configuration. No format -# specific format is enforced. -# -# For simply logging to an existing remote syslog server, via udp: -# configs: ["*.* @192.168.1.1"] -# -# - remotes: [default={}] -# This is a dictionary of name / value pairs. -# In comparison to 'config's, it is more focused in that it only supports -# remote syslog configuration. It is not rsyslog specific, and could -# convert to other syslog implementations. -# -# Each entry in remotes is a 'name' and a 'value'. -# * name: an string identifying the entry. good practice would indicate -# using a consistent and identifiable string for the producer. -# For example, the MAAS service could use 'maas' as the key. -# * value consists of the following parts: -# * optional filter for log messages -# default if not present: *.* -# * optional leading '@' or '@@' (indicates udp or tcp respectively). -# default if not present (udp): @ -# This is rsyslog format for that. if not present, is '@'. -# * ipv4 or ipv6 or hostname -# ipv6 addresses must be in [::1] format. (@[fd00::1]:514) -# * optional port -# port defaults to 514 -# -# - config_filename: [default=20-cloud-config.conf] -# this is the file name to use if none is provided in a config entry. -# -# - config_dir: [default=/etc/rsyslog.d] -# this directory is used for filenames that are not absolute paths. -# -# - service_reload_command: [default="auto"] -# this command is executed if files have been written and thus the syslog -# daemon needs to be told. -# -# Note, since cloud-init 0.5 a legacy version of rsyslog config has been -# present and is still supported. See below for the mappings between old -# value and new value: -# old value -> new value -# 'rsyslog' -> rsyslog/configs -# 'rsyslog_filename' -> rsyslog/config_filename -# 'rsyslog_dir' -> rsyslog/config_dir -# -# the legacy config does not support 'service_reload_command'. -# -# Example config: -# #cloud-config -# rsyslog: -# configs: -# - "*.* @@192.158.1.1" -# - content: "*.* @@192.0.2.1:10514" -# filename: 01-example.conf -# - content: | -# *.* @@syslogd.example.com -# remotes: -# maas: "192.168.1.1" -# juju: "10.0.4.1" -# config_dir: config_dir -# config_filename: config_filename -# service_reload_command: [your, syslog, restart, command] -# -# Example Legacy config: -# #cloud-config -# rsyslog: -# - "*.* @@192.158.1.1" -# rsyslog_dir: /etc/rsyslog-config.d/ -# rsyslog_filename: 99-local.conf - -import os -import re - -from cloudinit import log as logging -from cloudinit import subp, util +meta: MetaSchema = { + "id": "cc_rsyslog", + "name": "Rsyslog", + "title": "Configure system logging via rsyslog", + "description": MODULE_DESCRIPTION, + "distros": [ALL_DISTROS], + "frequency": PER_INSTANCE, + "examples": [ + dedent( + """\ + rsyslog: + remotes: + maas: 192.168.1.1 + juju: 10.0.4.1 + service_reload_command: auto + """ + ), + dedent( + """\ + rsyslog: + config_dir: /opt/etc/rsyslog.d + config_filename: 99-late-cloud-config.conf + configs: + - "*.* @@192.158.1.1" + - content: "*.* @@192.0.2.1:10514" + filename: 01-example.conf + - content: | + *.* @@syslogd.example.com + remotes: + maas: 192.168.1.1 + juju: 10.0.4.1 + service_reload_command: [your, syslog, restart, command] + """ + ), + ], +} + +__doc__ = get_meta_doc(meta) DEF_FILENAME = "20-cloud-config.conf" DEF_DIR = "/etc/rsyslog.d" @@ -220,6 +100,10 @@ def load_config(cfg): mycfg = cfg.get("rsyslog", {}) if isinstance(cfg.get("rsyslog"), list): + LOG.warning( + "DEPRECATION: This rsyslog list format is deprecated and will be " + "removed in a future version of cloud-init. Use documented keys." + ) mycfg = {KEYNAME_CONFIGS: cfg.get("rsyslog")} if KEYNAME_LEGACY_FILENAME in cfg: mycfg[KEYNAME_FILENAME] = cfg[KEYNAME_LEGACY_FILENAME] diff --git a/cloudinit/config/cc_runcmd.py b/cloudinit/config/cc_runcmd.py index c520600384b..fe56b1ed8fb 100644 --- a/cloudinit/config/cc_runcmd.py +++ b/cloudinit/config/cc_runcmd.py @@ -12,11 +12,7 @@ from textwrap import dedent from cloudinit import util -from cloudinit.config.schema import ( - MetaSchema, - get_meta_doc, - validate_cloudconfig_schema, -) +from cloudinit.config.schema import MetaSchema, get_meta_doc from cloudinit.distros import ALL_DISTROS from cloudinit.settings import PER_INSTANCE @@ -26,36 +22,36 @@ # configuration options before actually attempting to deploy with said # configuration. -distros = [ALL_DISTROS] -meta: MetaSchema = { - "id": "cc_runcmd", - "name": "Runcmd", - "title": "Run arbitrary commands", - "description": dedent( - """\ - Run arbitrary commands at a rc.local like level with output to the - console. Each item can be either a list or a string. If the item is a - list, it will be properly quoted. Each item is written to - ``/var/lib/cloud/instance/runcmd`` to be later interpreted using - ``sh``. +MODULE_DESCRIPTION = """\ +Run arbitrary commands at a rc.local like level with output to the +console. Each item can be either a list or a string. If the item is a +list, it will be properly quoted. Each item is written to +``/var/lib/cloud/instance/runcmd`` to be later interpreted using +``sh``. - Note that the ``runcmd`` module only writes the script to be run - later. The module that actually runs the script is ``scripts-user`` - in the :ref:`Final` boot stage. +Note that the ``runcmd`` module only writes the script to be run +later. The module that actually runs the script is ``scripts-user`` +in the :ref:`Final` boot stage. - .. note:: +.. note:: - all commands must be proper yaml, so you have to quote any characters - yaml would eat (':' can be problematic) + all commands must be proper yaml, so you have to quote any characters + yaml would eat (':' can be problematic) - .. note:: +.. note:: - when writing files, do not use /tmp dir as it races with - systemd-tmpfiles-clean LP: #1707222. Use /run/somedir instead. - """ - ), - "distros": distros, + when writing files, do not use /tmp dir as it races with + systemd-tmpfiles-clean LP: #1707222. Use /run/somedir instead. +""" + +meta: MetaSchema = { + "id": "cc_runcmd", + "name": "Runcmd", + "title": "Run arbitrary commands", + "description": MODULE_DESCRIPTION, + "distros": [ALL_DISTROS], + "frequency": PER_INSTANCE, "examples": [ dedent( """\ @@ -68,29 +64,9 @@ """ ) ], - "frequency": PER_INSTANCE, -} - -schema = { - "type": "object", - "properties": { - "runcmd": { - "type": "array", - "items": { - "oneOf": [ - {"type": "array", "items": {"type": "string"}}, - {"type": "string"}, - {"type": "null"}, - ] - }, - "additionalItems": False, # Reject items of non-string non-list - "additionalProperties": False, - "minItems": 1, - } - }, } -__doc__ = get_meta_doc(meta, schema) # Supplement python help() +__doc__ = get_meta_doc(meta) def handle(name, cfg, cloud, log, _args): @@ -100,7 +76,6 @@ def handle(name, cfg, cloud, log, _args): ) return - validate_cloudconfig_schema(cfg, schema) out_fn = os.path.join(cloud.get_ipath("scripts"), "runcmd") cmd = cfg["runcmd"] try: diff --git a/cloudinit/config/cc_salt_minion.py b/cloudinit/config/cc_salt_minion.py index 0eb466644cd..df9d4205b03 100644 --- a/cloudinit/config/cc_salt_minion.py +++ b/cloudinit/config/cc_salt_minion.py @@ -2,11 +2,17 @@ # # This file is part of cloud-init. See LICENSE file for license information. -""" -Salt Minion ------------ -**Summary:** set up and run salt minion +"""Salt Minion: Setup and run salt minion""" + +import os +from textwrap import dedent + +from cloudinit import safeyaml, subp, util +from cloudinit.config.schema import MetaSchema, get_meta_doc +from cloudinit.distros import ALL_DISTROS, bsd_utils +from cloudinit.settings import PER_INSTANCE +MODULE_DESCRIPTION = """\ This module installs, configures and starts salt minion. If the ``salt_minion`` key is present in the config parts, then salt minion will be installed and started. Configuration for salt minion can be specified in the ``conf`` key @@ -16,37 +22,45 @@ you have a custom package name, service name or config directory you can specify them with ``pkg_name``, ``service_name`` and ``config_dir``. -**Internal name:** ``cc_salt_minion`` - -**Module frequency:** per instance - -**Supported distros:** all - -**Config keys**:: - - salt_minion: - pkg_name: 'salt-minion' - service_name: 'salt-minion' - config_dir: '/etc/salt' - conf: - master: salt.example.com - grains: - role: - - web - public_key: | - ------BEGIN PUBLIC KEY------- - - ------END PUBLIC KEY------- - private_key: | - ------BEGIN PRIVATE KEY------ - - ------END PRIVATE KEY------- +Salt keys can be manually generated by: ``salt-key --gen-keys=GEN_KEYS``, +where ``GEN_KEYS`` is the name of the keypair, e.g. 'minion'. The keypair +will be copied to ``/etc/salt/pki`` on the minion instance. """ -import os +meta: MetaSchema = { + "id": "cc_salt_minion", + "name": "Salt Minion", + "title": "Setup and run salt minion", + "description": MODULE_DESCRIPTION, + "distros": [ALL_DISTROS], + "frequency": PER_INSTANCE, + "examples": [ + dedent( + """\ + salt_minion: + pkg_name: salt-minion + service_name: salt-minion + config_dir: /etc/salt + conf: + master: salt.example.com + grains: + role: + - web + public_key: | + ------BEGIN PUBLIC KEY------- + + ------END PUBLIC KEY------- + private_key: | + ------BEGIN PRIVATE KEY------ + + ------END PRIVATE KEY------- + pki_dir: /etc/salt/pki/minion + """ + ) + ], +} -from cloudinit import safeyaml, subp, util -from cloudinit.distros import bsd_utils +__doc__ = get_meta_doc(meta) # Note: see https://docs.saltstack.com/en/latest/topics/installation/ # Note: see https://docs.saltstack.com/en/latest/ref/configuration/ diff --git a/cloudinit/config/cloud-init-schema.json b/cloudinit/config/cloud-init-schema.json index 6ed755ed48a..f777143473d 100644 --- a/cloudinit/config/cloud-init-schema.json +++ b/cloudinit/config/cloud-init-schema.json @@ -787,6 +787,226 @@ } } } + }, + "cc_resizefs": { + "type": "object", + "properties": { + "resize_rootfs": { + "enum": [true, false, "noblock"], + "description": "Whether to resize the root partition. ``noblock`` will resize in the background. Default: ``true``" + } + } + }, + "cc_resolv_conf": { + "type": "object", + "properties": { + "manage_resolv_conf": { + "type": "boolean", + "default": false, + "description": "Whether to manage the resolv.conf file. ``resolv_conf`` block will be ignored unless this is set to ``true``. Default: ``false``" + }, + "resolv_conf": { + "type": "object", + "additionalProperties": false, + "properties": { + "nameservers": { + "type": "array", + "description": "A list of nameservers to use to be added as ``nameserver`` lines" + }, + "searchdomains": { + "type": "array", + "description": "A list of domains to be added ``search`` line" + }, + "domain": { + "type": "string", + "description": "The domain to be added as ``domain`` line" + }, + "sortlist": { + "type": "array", + "description": "A list of IP addresses to be added to ``sortlist`` line" + }, + "options": { + "type": "object", + "description": "Key/value pairs of options to go under ``options`` heading. A unary option should be specified as ``true``" + } + } + } + } + }, + "cc_rh_subscription": { + "type": "object", + "properties": { + "rh_subscription": { + "type": "object", + "additionalProperties": false, + "properties": { + "username": { + "type": "string", + "description": "The username to use. Must be used with password. Should not be used with ``activation-key`` or ``org``" + }, + "password": { + "type": "string", + "description": "The password to use. Must be used with username. Should not be used with ``activation-key`` or ``org``" + }, + "activation-key": { + "type": "string", + "description": "The activation key to use. Must be used with ``org``. Should not be used with ``username`` or ``password``" + }, + "org": { + "type": "integer", + "description": "The organization number to use. Must be used with ``activation-key``. Should not be used with ``username`` or ``password``" + }, + "auto-attach": { + "type": "boolean", + "description": "Whether to attach subscriptions automatically" + }, + "service-level": { + "type": "string", + "description": "The service level to use when subscribing to RH repositories. ``auto-attach`` must be true for this to be used" + }, + "add-pool": { + "type": "array", + "description": "A list of pools ids add to the subscription", + "items": { + "type": "string" + } + }, + "enable-repo": { + "type": "array", + "description": "A list of repositories to enable", + "items": { + "type": "string" + } + }, + "disable-repo": { + "type": "array", + "description": "A list of repositories to disable", + "items": { + "type": "string" + } + }, + "rhsm-baseurl": { + "type": "string", + "description": "Sets the baseurl in ``/etc/rhsm/rhsm.conf``" + }, + "server-hostname": { + "type": "string", + "description": "Sets the serverurl in ``/etc/rhsm/rhsm.conf``" + } + } + } + } + }, + "cc_rsyslog": { + "type": "object", + "properties": { + "rsyslog": { + "type": "object", + "additionalProperties": false, + "properties": { + "config_dir": { + "type": "string", + "description": "The directory where rsyslog configuration files will be written. Default: ``/etc/rsyslog.d``" + }, + "config_filename": { + "type": "string", + "description": "The name of the rsyslog configuration file. Default: ``20-cloud-config.conf``" + }, + "configs": { + "type": "array", + "description": "Each entry in ``configs`` is either a string or an object. Each config entry contains a configuration string and a file to write it to. For config entries that are an object, ``filename`` sets the target filename and ``content`` specifies the config string to write. For config entries that are only a string, the string is used as the config string to write. If the filename to write the config to is not specified, the value of the ``config_filename`` key is used. A file with the selected filename will be written inside the directory specified by ``config_dir``.", + "items": { + "oneOf": [ + {"type": "string"}, + { + "type": "object", + "additionalProperties": false, + "required": ["content"], + "properties": { + "filename": { + "type": "string" + }, + "content": { + "type": "string" + } + } + } + ] + } + }, + "remotes": { + "type": "object", + "description": "Each key is the name for an rsyslog remote entry. Each value holds the contents of the remote config for rsyslog. The config consists of the following parts:\n\n- filter for log messages (defaults to ``*.*``)\n\n- optional leading ``@`` or ``@@``, indicating udp and tcp respectively (defaults to ``@``, for udp)\n\n- ipv4 or ipv6 hostname or address. ipv6 addresses must be in ``[::1]`` format, (e.g. ``@[fd00::1]:514``)\n\n- optional port number (defaults to ``514``)\n\nThis module will provide sane defaults for any part of the remote entry that is not specified, so in most cases remote hosts can be specified just using ``:
``." + }, + "service_reload_command": { + "description": "The command to use to reload the rsyslog service after the config has been updated. If this is set to ``auto``, then an appropriate command for the distro will be used. This is the default behavior. To manually set the command, use a list of command args (e.g. ``[systemctl, restart, rsyslog]``).", + "oneOf": [ + {"enum": ["auto"]}, + {"type": "array", "items": {"type": "string"}} + ] + } + } + } + } + }, + "cc_runcmd": { + "type": "object", + "properties": { + "runcmd": { + "type": "array", + "items": { + "oneOf": [ + {"type": "array", "items": {"type": "string"}}, + {"type": "string"}, + {"type": "null"} + ] + }, + "minItems": 1 + } + } + }, + "cc_salt_minion": { + "type": "object", + "properties": { + "salt_minion": { + "type": "object", + "additionalProperties": false, + "properties": { + "pkg_name": { + "type": "string", + "description": "Package name to install. Default: ``salt-minion``" + }, + "service_name": { + "type": "string", + "description": "Service name to enable. Default: ``salt-minion``" + }, + "config_dir": { + "type": "string", + "description": "Directory to write config files to. Default: ``/etc/salt``" + }, + "conf": { + "type": "object", + "description": "Configuration to be written to `config_dir`/minion" + }, + "grains": { + "type": "object", + "description": "Configuration to be written to `config_dir`/grains" + }, + "public_key": { + "type": "string", + "description": "Public key to be used by the salt minion" + }, + "private_key": { + "type": "string", + "description": "Private key to be used by salt minion" + }, + "pki_dir": { + "type": "string", + "description": "Directory to write key files. Default: `config_dir`/pki/minion" + } + } + } + } } }, "allOf": [ @@ -803,6 +1023,12 @@ { "$ref": "#/$defs/cc_package_update_upgrade_install" }, { "$ref": "#/$defs/cc_phone_home" }, { "$ref": "#/$defs/cc_power_state_change"}, - { "$ref": "#/$defs/cc_puppet"} + { "$ref": "#/$defs/cc_puppet"}, + { "$ref": "#/$defs/cc_resizefs"}, + { "$ref": "#/$defs/cc_resolv_conf"}, + { "$ref": "#/$defs/cc_rh_subscription"}, + { "$ref": "#/$defs/cc_rsyslog"}, + { "$ref": "#/$defs/cc_runcmd"}, + { "$ref": "#/$defs/cc_salt_minion"} ] } diff --git a/doc/examples/cloud-config-resolv-conf.txt b/doc/examples/cloud-config-resolv-conf.txt deleted file mode 100644 index c4843f5455d..00000000000 --- a/doc/examples/cloud-config-resolv-conf.txt +++ /dev/null @@ -1,20 +0,0 @@ -#cloud-config -# -# This is an example file to automatically configure resolv.conf when the -# instance boots for the first time. -# -# Ensure that your yaml is valid and pass this as user-data when starting -# the instance. Also be sure that your cloud.cfg file includes this -# configuration module in the appropriate section. -# -manage_resolv_conf: true - -resolv_conf: - nameservers: ['8.8.4.4', '8.8.8.8'] - searchdomains: - - foo.example.com - - bar.example.com - domain: example.com - options: - rotate: true - timeout: 1 diff --git a/doc/examples/cloud-config-rh_subscription.txt b/doc/examples/cloud-config-rh_subscription.txt deleted file mode 100644 index 5cc903a2d48..00000000000 --- a/doc/examples/cloud-config-rh_subscription.txt +++ /dev/null @@ -1,49 +0,0 @@ -#cloud-config - -# register your Red Hat Enterprise Linux based operating system -# -# this cloud-init plugin is capable of registering by username -# and password *or* activation and org. Following a successfully -# registration you can: -# - auto-attach subscriptions -# - set the service level -# - add subscriptions based on its pool ID -# - enable yum repositories based on its repo id -# - disable yum repositories based on its repo id -# - alter the rhsm_baseurl and server-hostname in the -# /etc/rhsm/rhs.conf file - -rh_subscription: - username: joe@foo.bar - - ## Quote your password if it has symbols to be safe - password: '1234abcd' - - ## If you prefer, you can use the activation key and - ## org instead of username and password. Be sure to - ## comment out username and password - - #activation-key: foobar - #org: 12345 - - ## Uncomment to auto-attach subscriptions to your system - #auto-attach: True - - ## Uncomment to set the service level for your - ## subscriptions - #service-level: self-support - - ## Uncomment to add pools (needs to be a list of IDs) - #add-pool: [] - - ## Uncomment to add or remove yum repos - ## (needs to be a list of repo IDs) - #enable-repo: [] - #disable-repo: [] - - ## Uncomment to alter the baseurl in /etc/rhsm/rhsm.conf - #rhsm-baseurl: http://url - - ## Uncomment to alter the server hostname in - ## /etc/rhsm/rhsm.conf - #server-hostname: foo.bar.com diff --git a/doc/examples/cloud-config-rsyslog.txt b/doc/examples/cloud-config-rsyslog.txt deleted file mode 100644 index d28dd38ee4c..00000000000 --- a/doc/examples/cloud-config-rsyslog.txt +++ /dev/null @@ -1,47 +0,0 @@ -#cloud-config -## the rsyslog module allows you to configure the systems syslog. -## configuration of syslog is under the top level cloud-config -## entry 'rsyslog'. -## -## Example: -#cloud-config -rsyslog: - remotes: - # udp to host 'maas.mydomain' port 514 - maashost: maas.mydomain - # udp to ipv4 host on port 514 - maas: "@[10.5.1.56]:514" - # tcp to host ipv6 host on port 555 - maasipv6: "*.* @@[FE80::0202:B3FF:FE1E:8329]:555" - configs: - - "*.* @@192.158.1.1" - - content: "*.* @@192.0.2.1:10514" - filename: 01-example.conf - - content: | - *.* @@syslogd.example.com - config_dir: /etc/rsyslog.d - config_filename: 20-cloud-config.conf - service_reload_command: [your, syslog, reload, command] - -## Additionally the following legacy format is supported -## it is converted into the format above before use. -## rsyslog_filename -> rsyslog/config_filename -## rsyslog_dir -> rsyslog/config_dir -## rsyslog -> rsyslog/configs -# rsyslog: -# - "*.* @@192.158.1.1" -# - content: "*.* @@192.0.2.1:10514" -# filename: 01-example.conf -# - content: | -# *.* @@syslogd.example.com -# rsyslog_filename: 20-cloud-config.conf -# rsyslog_dir: /etc/rsyslog.d - -## to configure rsyslog to accept remote logging on Ubuntu -## write the following into /etc/rsyslog.d/20-remote-udp.conf -## $ModLoad imudp -## $UDPServerRun 514 -## $template LogRemote,"/var/log/maas/rsyslog/%HOSTNAME%/messages" -## :fromhost-ip, !isequal, "127.0.0.1" ?LogRemote -## then: -## sudo service rsyslog restart diff --git a/doc/examples/cloud-config-salt-minion.txt b/doc/examples/cloud-config-salt-minion.txt deleted file mode 100644 index 939fdc8bc1b..00000000000 --- a/doc/examples/cloud-config-salt-minion.txt +++ /dev/null @@ -1,53 +0,0 @@ -#cloud-config -# -# This is an example file to automatically setup and run a salt -# minion when the instance boots for the first time. -# Make sure that this file is valid yaml before starting instances. -# It should be passed as user-data when starting the instance. - -salt_minion: - # conf contains all the directives to be assigned in /etc/salt/minion. - - conf: - # Set the location of the salt master server, if the master server cannot be - # resolved, then the minion will fail to start. - - master: salt.example.com - - # Salt keys are manually generated by: salt-key --gen-keys=GEN_KEYS, - # where GEN_KEYS is the name of the keypair, e.g. 'minion'. The keypair - # will be copied to /etc/salt/pki on the minion instance. - - public_key: | - -----BEGIN PUBLIC KEY----- - MIIBIDANBgkqhkiG9w0BAQEFAAOCAQ0AMIIBCAKCAQEAwI4yqk1Y12zVmu9Ejlua - h2FD6kjrt+N9XfGqZUUVNeRb7CA0Sj5Q6NtgoaiXuIrSea2sLda6ivqAGmtxMMrP - zpf3FwsYWxBUNF7D4YeLmYjvcTbfr3bCOIRnPNXZ+4isuvvEiM02u2cO0okZSgeb - dofNa1NbTLYAQr9jZZb7GPKrTO4CKy0xzBih/A+sl6dL9PNDmqXQEjyJS6PXG1Vj - PvD5jpSrxuIl5Ms/+2Ro3ALgvC8dgoY/3m3csnd06afumGKv5YOGtf+bnWLhc0bf - 6Sk8Q6i5t0Bl+HAULSPr+B9x/I0rN76ZnPvTj1+hJ0zTof4d0hOLx/K5OQyt7AKo - 4wIBAQ== - -----END PUBLIC KEY----- - - private_key: | - -----BEGIN RSA PRIVATE KEY----- - Proc-Type: 4,ENCRYPTED - DEK-Info: AES-128-CBC,ECE30DBBA56E2DF06B7BC415F8870994 - - YQOE5HIsghqjRsxPQqiWMH/VHmyFH6xIpBcmzxzispEHwBojlvLXviwvR66YhgNw - 7smwE10Ik4/cwwiHTZqCk++jPATPygBiqQkUijCWzcT9kfaxmqdP4PL+hu9g7kGC - KrD2Bm8/oO08s957aThuHC1sABRcJ1V3FRzJT6Za4fwweyvHVYRnmgaDA6zH0qV8 - NqBSB2hnNXKEdh6UFz9QGcrQxnRjfdIaW64zoEX7jT7gYYL7FkGXBa3XdMOA4fnl - adRwLFMs0jfilisZv8oUbPdZ6J6x3o8p8LVecCF8tdZt1zkcLSIXKnoDFpHSISGs - BD9aqD+E4ejynM/tPaVFq4IHzT8viN6h6WcH8fbpClFZ66Iyy9XL3/CjAY7Jzhh9 - fnbc4Iq28cdbmO/vkR7JyVOgEMWe1BcSqtro70XoUNRY8uDJUPqohrhm/9AigFRA - Pwyf3LqojxRnwXjHsZtGltUtEAPZzgh3fKJnx9MyRR7DPXBRig7TAHU7n2BFRhHA - TYThy29bK6NkIc/cKc2kEQVo98Cr04PO8jVxZM332FlhiVlP0kpAp+tFj7aMzPTG - sJumb9kPbMsgpEuTCONm3yyoufGEBFMrIJ+Po48M2RlYOh50VkO09pI+Eu7FPtVB - H4gKzoJIpZZ/7vYXQ3djM8s9hc5gD5CVExTZV4drbsXt6ITiwHuxZ6CNHRBPL5AY - wmF8QZz4oivv1afdSe6E6OGC3uVmX3Psn5CVq2pE8VlRDKFy1WqfU2enRAijSS2B - rtJs263fOJ8ZntDzMVMPgiAlzzfA285KUletpAeUmz+peR1gNzkE0eKSG6THOCi0 - rfmR8SeEzyNvin0wQ3qgYiiHjHbbFhJIMAQxoX+0hDSooM7Wo5wkLREULpGuesTg - A6Fe3CiOivMDraNGA7H6Yg== - -----END RSA PRIVATE KEY----- - diff --git a/doc/examples/cloud-config.txt b/doc/examples/cloud-config.txt index c759cb29236..42c26f21867 100644 --- a/doc/examples/cloud-config.txt +++ b/doc/examples/cloud-config.txt @@ -253,21 +253,6 @@ locale: en_US.UTF-8 # render template default-locale.tmpl to locale_configfile locale_configfile: /etc/default/locale -# add entries to rsyslog configuration -# The first occurrence of a given filename will truncate. -# subsequent entries will append. -# if value is a scalar, its content is assumed to be 'content', and the -# default filename is used. -# if filename is not provided, it will default to 'rsylog_filename' -# if filename does not start with a '/', it will be put in 'rsyslog_dir' -# rsyslog_dir default: /etc/rsyslog.d -# rsyslog_filename default: 20-cloud-config.conf -rsyslog: - - ':syslogtag, isequal, "[CLOUDINIT]" /var/log/cloud-foo.log' - - content: "*.* @@192.0.2.1:10514" - - filename: 01-examplecom.conf - content: "*.* @@syslogd.example.com" - # resize_rootfs should the / filesystem be resized on first boot # this allows you to launch an instance with a larger disk / partition # and have the instance automatically grow / to accomoddate it diff --git a/doc/rtd/topics/examples.rst b/doc/rtd/topics/examples.rst index 8c7071e5bca..a07269a9286 100644 --- a/doc/rtd/topics/examples.rst +++ b/doc/rtd/topics/examples.rst @@ -34,18 +34,6 @@ Configure an instances trusted CA certificates :language: yaml :linenos: -Configure an instances resolv.conf -================================== - -*Note:* when using a config drive and a RHEL like system resolv.conf -will also be managed 'automatically' due to the available information -provided for dns servers in the config drive network format. For those -that wish to have different settings use this module. - -.. literalinclude:: ../../examples/cloud-config-resolv-conf.txt - :language: yaml - :linenos: - Install and run `chef`_ recipes =============================== @@ -149,13 +137,6 @@ Disk setup :language: yaml :linenos: -Register Red Hat Subscription -============================= - -.. literalinclude:: ../../examples/cloud-config-rh_subscription.txt - :language: yaml - :linenos: - Configure data sources ====================== diff --git a/tests/unittests/config/test_cc_resizefs.py b/tests/unittests/config/test_cc_resizefs.py index 9981dcea2d4..44659f7d18c 100644 --- a/tests/unittests/config/test_cc_resizefs.py +++ b/tests/unittests/config/test_cc_resizefs.py @@ -3,6 +3,8 @@ import logging from collections import namedtuple +import pytest + from cloudinit.config.cc_resizefs import ( _resize_btrfs, _resize_ext, @@ -13,6 +15,11 @@ handle, maybe_get_writable_device_path, ) +from cloudinit.config.schema import ( + SchemaValidationError, + get_schema, + validate_cloudconfig_schema, +) from cloudinit.subp import ProcessExecutionError from tests.unittests.helpers import ( CiTestCase, @@ -82,25 +89,6 @@ def test_handle_noops_on_disabled(self): self.logs.getvalue(), ) - @skipUnlessJsonSchema() - def test_handle_schema_validation_logs_invalid_resize_rootfs_value(self): - """The handle reports json schema violations as a warning. - - Invalid values for resize_rootfs result in disabling the module. - """ - cfg = {"resize_rootfs": "junk"} - handle("cc_resizefs", cfg, _cloud=None, log=LOG, args=[]) - logs = self.logs.getvalue() - self.assertIn( - "WARNING: Invalid cloud-config provided:\nresize_rootfs: 'junk' is" - " not one of [True, False, 'noblock']", - logs, - ) - self.assertIn( - "DEBUG: Skipping module named cc_resizefs, resizing disabled\n", - logs, - ) - @mock.patch("cloudinit.config.cc_resizefs.util.get_mount_info") def test_handle_warns_on_unknown_mount_info(self, m_get_mount_info): """handle warns when get_mount_info sees unknown filesystem for /.""" @@ -487,4 +475,24 @@ def test_maybe_get_writable_device_path_zfs_freebsd( self.assertEqual("gpt/system", devpth) +class TestResizefsSchema: + @pytest.mark.parametrize( + "config, error_msg", + [ + ({"resize_rootfs": True}, None), + ( + {"resize_rootfs": "wrong"}, + r"'wrong' is not one of \[True, False, 'noblock'\]", + ), + ], + ) + @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_resolv_conf.py b/tests/unittests/config/test_cc_resolv_conf.py index 8896a4e8d3e..4ae9b3f3a74 100644 --- a/tests/unittests/config/test_cc_resolv_conf.py +++ b/tests/unittests/config/test_cc_resolv_conf.py @@ -12,7 +12,16 @@ from cloudinit import cloud, distros, helpers, util from cloudinit.config import cc_resolv_conf from cloudinit.config.cc_resolv_conf import generate_resolv_conf -from tests.unittests import helpers as t_help +from cloudinit.config.schema import ( + SchemaValidationError, + get_schema, + validate_cloudconfig_schema, +) +from tests.unittests.helpers import ( + FilesystemMockingTestCase, + cloud_init_project_dir, + skipUnlessJsonSchema, +) from tests.unittests.util import MockDistro LOG = logging.getLogger(__name__) @@ -24,7 +33,7 @@ #\n\n""" -class TestResolvConf(t_help.FilesystemMockingTestCase): +class TestResolvConf(FilesystemMockingTestCase): with_logs = True cfg = {"manage_resolv_conf": True, "resolv_conf": {}} @@ -117,7 +126,7 @@ def test_resolv_conf_invalid_resolve_conf_fn(self, m_render_to_file): class TestGenerateResolvConf: dist = MockDistro() - tmpl_fn = t_help.cloud_init_project_dir("templates/resolv.conf.tmpl") + tmpl_fn = cloud_init_project_dir("templates/resolv.conf.tmpl") @mock.patch("cloudinit.config.cc_resolv_conf.templater.render_to_file") def test_dist_resolv_conf_fn(self, m_render_to_file): @@ -194,4 +203,64 @@ def test_flags_and_options( ] == m_write_file.call_args_list +class TestResolvConfSchema: + @pytest.mark.parametrize( + "config, error_msg", + [ + # Valid + ({"manage_resolv_conf": False}, None), + ({"resolv_conf": {"options": {"any": "thing"}}}, None), + # Invalid + ( + {"manage_resolv_conf": "asdf"}, + "'asdf' is not of type 'boolean'", + ), + # What may be some common misunderstandings of the template + ( + {"resolv_conf": {"nameserver": ["1.1.1.1"]}}, + "Additional properties are not allowed", + ), + ( + {"resolv_conf": {"nameservers": "1.1.1.1"}}, + "'1.1.1.1' is not of type 'array'", + ), + ( + {"resolv_conf": {"search": ["foo.com"]}}, + "Additional properties are not allowed", + ), + ( + {"resolv_conf": {"searchdomains": "foo.com"}}, + "'foo.com' is not of type 'array'", + ), + ( + {"resolv_conf": {"domain": ["foo.com"]}}, + r"\['foo.com'\] is not of type 'string'", + ), + ( + {"resolv_conf": {"sortlist": "1.2.3.4"}}, + "'1.2.3.4' is not of type 'array'", + ), + ( + {"resolv_conf": {"options": "timeout: 1"}}, + "'timeout: 1' is not of type 'object'", + ), + ( + {"resolv_conf": {"options": "rotate"}}, + "'rotate' is not of type 'object'", + ), + ( + {"resolv_conf": {"options": ["rotate"]}}, + r"\['rotate'\] is not of type 'object'", + ), + ], + ) + @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_rh_subscription.py b/tests/unittests/config/test_cc_rh_subscription.py index fcc7db34767..57313361d6a 100644 --- a/tests/unittests/config/test_cc_rh_subscription.py +++ b/tests/unittests/config/test_cc_rh_subscription.py @@ -5,9 +5,16 @@ import copy import logging +import pytest + from cloudinit import subp from cloudinit.config import cc_rh_subscription -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 SUBMGR = cc_rh_subscription.SubscriptionManager SUB_MAN_CLI = "cloudinit.config.cc_rh_subscription._sub_man_cli" @@ -317,4 +324,35 @@ def test_bad_key_value(self, m_sman_cli): ) +class TestRhSubscriptionSchema: + @pytest.mark.parametrize( + "config, error_msg", + [ + ( + {"rh_subscription": {"bad": "input"}}, + "Additional properties are not allowed", + ), + ( + {"rh_subscription": {"add-pool": [1]}}, + "1 is not of type 'string'", + ), + ( + {"rh_subscription": {"enable-repo": "name"}}, + "'name' is not of type 'array'", + ), + ( + {"rh_subscription": {"disable-repo": "name"}}, + "'name' is not of type 'array'", + ), + ], + ) + @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_rsyslog.py b/tests/unittests/config/test_cc_rsyslog.py index e5d06ca2267..578a30fd570 100644 --- a/tests/unittests/config/test_cc_rsyslog.py +++ b/tests/unittests/config/test_cc_rsyslog.py @@ -4,6 +4,8 @@ import shutil import tempfile +import pytest + from cloudinit import util from cloudinit.config.cc_rsyslog import ( DEF_DIR, @@ -14,10 +16,15 @@ parse_remotes_line, remotes_to_rsyslog_cfg, ) -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 -class TestLoadConfig(t_help.TestCase): +class TestLoadConfig(TestCase): def setUp(self): super(TestLoadConfig, self).setUp() self.basecfg = { @@ -63,7 +70,7 @@ def test_new_configs(self): ) -class TestApplyChanges(t_help.TestCase): +class TestApplyChanges(TestCase): def setUp(self): self.tmp = tempfile.mkdtemp() self.addCleanup(shutil.rmtree, self.tmp) @@ -136,7 +143,7 @@ def test_multiline_content(self): self.assertEqual(expected_content, found_content) -class TestParseRemotesLine(t_help.TestCase): +class TestParseRemotesLine(TestCase): def test_valid_port(self): r = parse_remotes_line("foo:9") self.assertEqual(9, r.port) @@ -164,7 +171,7 @@ def test_name_in_string(self): self.assertEqual("*.* @syslog.host # foobar", str(r)) -class TestRemotesToSyslog(t_help.TestCase): +class TestRemotesToSyslog(TestCase): def test_simple(self): # str rendered line must appear in remotes_to_ryslog_cfg return mycfg = "*.* myhost" @@ -195,4 +202,41 @@ def test_with_empty_or_null(self): self.assertTrue(myline in r.splitlines()) +class TestRsyslogSchema: + @pytest.mark.parametrize( + "config, error_msg", + [ + ({"rsyslog": {"remotes": {"any": "string"}}}, None), + ( + {"rsyslog": {"unknown": "a"}}, + "Additional properties are not allowed", + ), + ({"rsyslog": {"configs": [{"filename": "a"}]}}, ""), + ( + { + "rsyslog": { + "configs": [ + {"filename": "a", "content": "a", "a": "a"} + ] + } + }, + "", + ), + ( + {"rsyslog": {"remotes": ["a"]}}, + r"\['a'\] is not of type 'object'", + ), + ({"rsyslog": {"remotes": "a"}}, "'a' is not of type 'object"), + ({"rsyslog": {"service_reload_command": "a"}}, ""), + ], + ) + @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_runcmd.py b/tests/unittests/config/test_cc_runcmd.py index 59490d67377..ab5733a7d9a 100644 --- a/tests/unittests/config/test_cc_runcmd.py +++ b/tests/unittests/config/test_cc_runcmd.py @@ -4,12 +4,17 @@ import stat from unittest.mock import patch +import pytest + from cloudinit import helpers, subp, util -from cloudinit.config.cc_runcmd import handle, schema +from cloudinit.config.cc_runcmd import handle +from cloudinit.config.schema import ( + SchemaValidationError, + get_schema, + validate_cloudconfig_schema, +) from tests.unittests.helpers import ( - CiTestCase, FilesystemMockingTestCase, - SchemaTestCaseMixin, skipUnlessJsonSchema, ) from tests.unittests.util import get_cloud @@ -61,45 +66,6 @@ def test_handler_invalid_command_set(self): str(cm.exception), ) - @skipUnlessJsonSchema() - def test_handler_schema_validation_warns_non_array_type(self): - """Schema validation warns of non-array type for runcmd key. - - Schema validation is not strict, so runcmd attempts to shellify the - invalid content. - """ - invalid_config = {"runcmd": 1} - cc = get_cloud(paths=self.paths) - with self.assertRaises(TypeError) as cm: - handle("cc_runcmd", invalid_config, cc, LOG, []) - self.assertIn( - "Invalid cloud-config provided:\nruncmd: 1 is not of type 'array'", - self.logs.getvalue(), - ) - self.assertIn("Failed to shellify", str(cm.exception)) - - @skipUnlessJsonSchema() - def test_handler_schema_validation_warns_non_array_item_type(self): - """Schema validation warns of non-array or string runcmd items. - - Schema validation is not strict, so runcmd attempts to shellify the - invalid content. - """ - invalid_config = { - "runcmd": ["ls /", 20, ["wget", "http://stuff/blah"], {"a": "n"}] - } - cc = get_cloud(paths=self.paths) - with self.assertRaises(TypeError) as cm: - handle("cc_runcmd", invalid_config, cc, LOG, []) - expected_warnings = [ - "runcmd.1: 20 is not valid under any of the given schemas", - "runcmd.3: {'a': 'n'} is not valid under any of the given schema", - ] - logs = self.logs.getvalue() - for warning in expected_warnings: - self.assertIn(warning, logs) - self.assertIn("Failed to shellify", str(cm.exception)) - def test_handler_write_valid_runcmd_schema_to_file(self): """Valid runcmd schema is written to a runcmd shell script.""" valid_config = {"runcmd": [["ls", "/"]]} @@ -115,23 +81,36 @@ def test_handler_write_valid_runcmd_schema_to_file(self): @skipUnlessJsonSchema() -class TestSchema(CiTestCase, SchemaTestCaseMixin): - """Directly test schema rather than through handle.""" - - schema = schema - - def test_duplicates_are_fine_array_array(self): - """Duplicated commands array/array entries are allowed.""" - self.assertSchemaValid( - [["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( - ["echo bye", "echo bye"], "command entries can be duplicate." - ) +class TestRunCmdSchema: + @pytest.mark.parametrize( + "config, error_msg", + ( + # Ensure duplicate commands are valid + ({"runcmd": [["echo", "bye"], ["echo", "bye"]]}, None), + ({"runcmd": ["echo bye", "echo bye"]}, None), + # Invalid schemas + ({"runcmd": 1}, "1 is not of type 'array'"), + ({"runcmd": []}, r"runcmd: \[\] is too short"), + ( + { + "runcmd": [ + "ls /", + 20, + ["wget", "http://stuff/blah"], + {"a": "n"}, + ] + }, + "", + ), + ), + ) + @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_salt_minion.py b/tests/unittests/config/test_salt_minion.py new file mode 100644 index 00000000000..b16034b4989 --- /dev/null +++ b/tests/unittests/config/test_salt_minion.py @@ -0,0 +1,33 @@ +# This file is part of cloud-init. See LICENSE file for license information. +import pytest + +from cloudinit.config.schema import ( + SchemaValidationError, + get_schema, + validate_cloudconfig_schema, +) +from tests.unittests.helpers import skipUnlessJsonSchema + + +@skipUnlessJsonSchema() +class TestSaltMinionSchema: + @pytest.mark.parametrize( + "config, error_msg", + ( + ({"salt_minion": {"conf": {"any": "thing"}}}, None), + ({"salt_minion": {"grains": {"any": "thing"}}}, None), + ( + {"salt_minion": {"invalid": "key"}}, + "Additional properties are not allowed", + ), + ({"salt_minion": {"conf": "a"}}, "'a' is not of type 'object'"), + ({"salt_minion": {"grains": "a"}}, "'a' is not of type 'object'"), + ), + ) + @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 abc8aaee7d1..a8cd276a97b 100644 --- a/tests/unittests/config/test_schema.py +++ b/tests/unittests/config/test_schema.py @@ -106,7 +106,12 @@ def test_get_schema_coalesces_known_schema(self): "cc_power_state_change", "cc_puppet", "cc_resizefs", + "cc_resolv_conf", + "cc_rightscale_userdata", + "cc_rh_subscription", + "cc_rsyslog", "cc_runcmd", + "cc_salt_minion", "cc_snap", "cc_ubuntu_advantage", "cc_ubuntu_drivers", @@ -134,6 +139,12 @@ def test_get_schema_coalesces_known_schema(self): {"$ref": "#/$defs/cc_phone_home"}, {"$ref": "#/$defs/cc_power_state_change"}, {"$ref": "#/$defs/cc_puppet"}, + {"$ref": "#/$defs/cc_resizefs"}, + {"$ref": "#/$defs/cc_resolv_conf"}, + {"$ref": "#/$defs/cc_rh_subscription"}, + {"$ref": "#/$defs/cc_rsyslog"}, + {"$ref": "#/$defs/cc_runcmd"}, + {"$ref": "#/$defs/cc_salt_minion"}, ] found_subschema_defs = [] legacy_schema_keys = [] @@ -151,8 +162,6 @@ def test_get_schema_coalesces_known_schema(self): "locale", "locale_configfile", "ntp", - "resize_rootfs", - "runcmd", "snap", "ubuntu_advantage", "updates",