summaryrefslogtreecommitdiff
path: root/cloudinit/config
diff options
context:
space:
mode:
Diffstat (limited to 'cloudinit/config')
-rw-r--r--cloudinit/config/cc_apt_configure.py4
-rw-r--r--cloudinit/config/cc_bootcmd.py1
-rw-r--r--cloudinit/config/cc_disable_ec2_metadata.py14
-rw-r--r--cloudinit/config/cc_disk_setup.py12
-rw-r--r--cloudinit/config/cc_emit_upstart.py2
-rw-r--r--cloudinit/config/cc_lxd.py64
-rw-r--r--cloudinit/config/cc_mounts.py75
-rw-r--r--cloudinit/config/cc_ntp.py485
-rw-r--r--cloudinit/config/cc_phone_home.py7
-rw-r--r--cloudinit/config/cc_power_state_change.py2
-rw-r--r--cloudinit/config/cc_resizefs.py10
-rw-r--r--cloudinit/config/cc_rh_subscription.py18
-rw-r--r--cloudinit/config/cc_rsyslog.py4
-rw-r--r--cloudinit/config/cc_runcmd.py1
-rwxr-xr-xcloudinit/config/cc_set_passwords.py105
-rw-r--r--cloudinit/config/cc_snap.py5
-rw-r--r--cloudinit/config/cc_snappy.py4
-rw-r--r--cloudinit/config/cc_ubuntu_advantage.py5
-rw-r--r--cloudinit/config/cc_users_groups.py8
-rw-r--r--cloudinit/config/schema.py68
-rw-r--r--cloudinit/config/tests/test_disable_ec2_metadata.py50
-rw-r--r--cloudinit/config/tests/test_set_passwords.py71
-rw-r--r--cloudinit/config/tests/test_snap.py29
-rw-r--r--cloudinit/config/tests/test_ubuntu_advantage.py30
24 files changed, 827 insertions, 247 deletions
diff --git a/cloudinit/config/cc_apt_configure.py b/cloudinit/config/cc_apt_configure.py
index 5b9cbca0..e18944ec 100644
--- a/cloudinit/config/cc_apt_configure.py
+++ b/cloudinit/config/cc_apt_configure.py
@@ -121,7 +121,7 @@ and https protocols respectively. The ``proxy`` key also exists as an alias for
All source entries in ``apt-sources`` that match regex in
``add_apt_repo_match`` will be added to the system using
``add-apt-repository``. If ``add_apt_repo_match`` is not specified, it defaults
-to ``^[\w-]+:\w``
+to ``^[\\w-]+:\\w``
**Add source list entries:**
@@ -378,7 +378,7 @@ def apply_debconf_selections(cfg, target=None):
# get a complete list of packages listed in input
pkgs_cfgd = set()
- for key, content in selsets.items():
+ for _key, content in selsets.items():
for line in content.splitlines():
if line.startswith("#"):
continue
diff --git a/cloudinit/config/cc_bootcmd.py b/cloudinit/config/cc_bootcmd.py
index 233da1ef..db64f0a6 100644
--- a/cloudinit/config/cc_bootcmd.py
+++ b/cloudinit/config/cc_bootcmd.py
@@ -63,7 +63,6 @@ schema = {
'additionalProperties': False,
'minItems': 1,
'required': [],
- 'uniqueItems': True
}
}
}
diff --git a/cloudinit/config/cc_disable_ec2_metadata.py b/cloudinit/config/cc_disable_ec2_metadata.py
index c56319b5..885b3138 100644
--- a/cloudinit/config/cc_disable_ec2_metadata.py
+++ b/cloudinit/config/cc_disable_ec2_metadata.py
@@ -32,13 +32,23 @@ from cloudinit.settings import PER_ALWAYS
frequency = PER_ALWAYS
-REJECT_CMD = ['route', 'add', '-host', '169.254.169.254', 'reject']
+REJECT_CMD_IF = ['route', 'add', '-host', '169.254.169.254', 'reject']
+REJECT_CMD_IP = ['ip', 'route', 'add', 'prohibit', '169.254.169.254']
def handle(name, cfg, _cloud, log, _args):
disabled = util.get_cfg_option_bool(cfg, "disable_ec2_metadata", False)
if disabled:
- util.subp(REJECT_CMD, capture=False)
+ reject_cmd = None
+ if util.which('ip'):
+ reject_cmd = REJECT_CMD_IP
+ elif util.which('ifconfig'):
+ reject_cmd = REJECT_CMD_IF
+ else:
+ log.error(('Neither "route" nor "ip" command found, unable to '
+ 'manipulate routing table'))
+ return
+ util.subp(reject_cmd, capture=False)
else:
log.debug(("Skipping module named %s,"
" disabling the ec2 route not enabled"), name)
diff --git a/cloudinit/config/cc_disk_setup.py b/cloudinit/config/cc_disk_setup.py
index c3e8c484..943089e0 100644
--- a/cloudinit/config/cc_disk_setup.py
+++ b/cloudinit/config/cc_disk_setup.py
@@ -680,13 +680,13 @@ def read_parttbl(device):
reliable way to probe the partition table.
"""
blkdev_cmd = [BLKDEV_CMD, '--rereadpt', device]
- udevadm_settle()
+ util.udevadm_settle()
try:
util.subp(blkdev_cmd)
except Exception as e:
util.logexc(LOG, "Failed reading the partition table %s" % e)
- udevadm_settle()
+ util.udevadm_settle()
def exec_mkpart_mbr(device, layout):
@@ -737,14 +737,10 @@ def exec_mkpart(table_type, device, layout):
return get_dyn_func("exec_mkpart_%s", table_type, device, layout)
-def udevadm_settle():
- util.subp(['udevadm', 'settle'])
-
-
def assert_and_settle_device(device):
"""Assert that device exists and settle so it is fully recognized."""
if not os.path.exists(device):
- udevadm_settle()
+ util.udevadm_settle()
if not os.path.exists(device):
raise RuntimeError("Device %s did not exist and was not created "
"with a udevamd settle." % device)
@@ -752,7 +748,7 @@ def assert_and_settle_device(device):
# Whether or not the device existed above, it is possible that udev
# events that would populate udev database (for reading by lsdname) have
# not yet finished. So settle again.
- udevadm_settle()
+ util.udevadm_settle()
def mkpart(device, definition):
diff --git a/cloudinit/config/cc_emit_upstart.py b/cloudinit/config/cc_emit_upstart.py
index 69dc2d5e..eb9fbe66 100644
--- a/cloudinit/config/cc_emit_upstart.py
+++ b/cloudinit/config/cc_emit_upstart.py
@@ -43,7 +43,7 @@ def is_upstart_system():
del myenv['UPSTART_SESSION']
check_cmd = ['initctl', 'version']
try:
- (out, err) = util.subp(check_cmd, env=myenv)
+ (out, _err) = util.subp(check_cmd, env=myenv)
return 'upstart' in out
except util.ProcessExecutionError as e:
LOG.debug("'%s' returned '%s', not using upstart",
diff --git a/cloudinit/config/cc_lxd.py b/cloudinit/config/cc_lxd.py
index 09374d2e..ac72ac4a 100644
--- a/cloudinit/config/cc_lxd.py
+++ b/cloudinit/config/cc_lxd.py
@@ -47,11 +47,16 @@ lxd-bridge will be configured accordingly.
domain: <domain>
"""
+from cloudinit import log as logging
from cloudinit import util
import os
distros = ['ubuntu']
+LOG = logging.getLogger(__name__)
+
+_DEFAULT_NETWORK_NAME = "lxdbr0"
+
def handle(name, cfg, cloud, log, args):
# Get config
@@ -109,6 +114,7 @@ def handle(name, cfg, cloud, log, args):
# Set up lxd-bridge if bridge config is given
dconf_comm = "debconf-communicate"
if bridge_cfg:
+ net_name = bridge_cfg.get("name", _DEFAULT_NETWORK_NAME)
if os.path.exists("/etc/default/lxd-bridge") \
and util.which(dconf_comm):
# Bridge configured through packaging
@@ -135,15 +141,18 @@ def handle(name, cfg, cloud, log, args):
else:
# Built-in LXD bridge support
cmd_create, cmd_attach = bridge_to_cmd(bridge_cfg)
+ maybe_cleanup_default(
+ net_name=net_name, did_init=bool(init_cfg),
+ create=bool(cmd_create), attach=bool(cmd_attach))
if cmd_create:
log.debug("Creating lxd bridge: %s" %
" ".join(cmd_create))
- util.subp(cmd_create)
+ _lxc(cmd_create)
if cmd_attach:
log.debug("Setting up default lxd bridge: %s" %
" ".join(cmd_create))
- util.subp(cmd_attach)
+ _lxc(cmd_attach)
elif bridge_cfg:
raise RuntimeError(
@@ -204,10 +213,10 @@ def bridge_to_cmd(bridge_cfg):
if bridge_cfg.get("mode") == "none":
return None, None
- bridge_name = bridge_cfg.get("name", "lxdbr0")
+ bridge_name = bridge_cfg.get("name", _DEFAULT_NETWORK_NAME)
cmd_create = []
- cmd_attach = ["lxc", "network", "attach-profile", bridge_name,
- "default", "eth0", "--force-local"]
+ cmd_attach = ["network", "attach-profile", bridge_name,
+ "default", "eth0"]
if bridge_cfg.get("mode") == "existing":
return None, cmd_attach
@@ -215,7 +224,7 @@ def bridge_to_cmd(bridge_cfg):
if bridge_cfg.get("mode") != "new":
raise Exception("invalid bridge mode \"%s\"" % bridge_cfg.get("mode"))
- cmd_create = ["lxc", "network", "create", bridge_name]
+ cmd_create = ["network", "create", bridge_name]
if bridge_cfg.get("ipv4_address") and bridge_cfg.get("ipv4_netmask"):
cmd_create.append("ipv4.address=%s/%s" %
@@ -247,8 +256,47 @@ def bridge_to_cmd(bridge_cfg):
if bridge_cfg.get("domain"):
cmd_create.append("dns.domain=%s" % bridge_cfg.get("domain"))
- cmd_create.append("--force-local")
-
return cmd_create, cmd_attach
+
+def _lxc(cmd):
+ env = {'LC_ALL': 'C'}
+ util.subp(['lxc'] + list(cmd) + ["--force-local"], update_env=env)
+
+
+def maybe_cleanup_default(net_name, did_init, create, attach,
+ profile="default", nic_name="eth0"):
+ """Newer versions of lxc (3.0.1+) create a lxdbr0 network when
+ 'lxd init --auto' is run. Older versions did not.
+
+ By removing ay that lxd-init created, we simply leave the add/attach
+ code in-tact.
+
+ https://github.com/lxc/lxd/issues/4649"""
+ if net_name != _DEFAULT_NETWORK_NAME or not did_init:
+ return
+
+ fail_assume_enoent = " failed. Assuming it did not exist."
+ succeeded = " succeeded."
+ if create:
+ msg = "Deletion of lxd network '%s'" % net_name
+ try:
+ _lxc(["network", "delete", net_name])
+ LOG.debug(msg + succeeded)
+ except util.ProcessExecutionError as e:
+ if e.exit_code != 1:
+ raise e
+ LOG.debug(msg + fail_assume_enoent)
+
+ if attach:
+ msg = "Removal of device '%s' from profile '%s'" % (nic_name, profile)
+ try:
+ _lxc(["profile", "device", "remove", profile, nic_name])
+ LOG.debug(msg + succeeded)
+ except util.ProcessExecutionError as e:
+ if e.exit_code != 1:
+ raise e
+ LOG.debug(msg + fail_assume_enoent)
+
+
# vi: ts=4 expandtab
diff --git a/cloudinit/config/cc_mounts.py b/cloudinit/config/cc_mounts.py
index f14a4fc5..339baba9 100644
--- a/cloudinit/config/cc_mounts.py
+++ b/cloudinit/config/cc_mounts.py
@@ -76,6 +76,7 @@ DEVICE_NAME_FILTER = r"^([x]{0,1}[shv]d[a-z][0-9]*|sr[0-9]+)$"
DEVICE_NAME_RE = re.compile(DEVICE_NAME_FILTER)
WS = re.compile("[%s]+" % (whitespace))
FSTAB_PATH = "/etc/fstab"
+MNT_COMMENT = "comment=cloudconfig"
LOG = logging.getLogger(__name__)
@@ -232,8 +233,8 @@ def setup_swapfile(fname, size=None, maxsize=None):
if str(size).lower() == "auto":
try:
memsize = util.read_meminfo()['total']
- except IOError as e:
- LOG.debug("Not creating swap. failed to read meminfo")
+ except IOError:
+ LOG.debug("Not creating swap: failed to read meminfo")
return
util.ensure_dir(tdir)
@@ -280,17 +281,17 @@ def handle_swapcfg(swapcfg):
if os.path.exists(fname):
if not os.path.exists("/proc/swaps"):
- LOG.debug("swap file %s existed. no /proc/swaps. Being safe.",
- fname)
+ LOG.debug("swap file %s exists, but no /proc/swaps exists, "
+ "being safe", fname)
return fname
try:
for line in util.load_file("/proc/swaps").splitlines():
if line.startswith(fname + " "):
- LOG.debug("swap file %s already in use.", fname)
+ LOG.debug("swap file %s already in use", fname)
return fname
- LOG.debug("swap file %s existed, but not in /proc/swaps", fname)
+ LOG.debug("swap file %s exists, but not in /proc/swaps", fname)
except Exception:
- LOG.warning("swap file %s existed. Error reading /proc/swaps",
+ LOG.warning("swap file %s exists. Error reading /proc/swaps",
fname)
return fname
@@ -327,6 +328,22 @@ def handle(_name, cfg, cloud, log, _args):
LOG.debug("mounts configuration is %s", cfgmnt)
+ fstab_lines = []
+ fstab_devs = {}
+ fstab_removed = []
+
+ for line in util.load_file(FSTAB_PATH).splitlines():
+ if MNT_COMMENT in line:
+ fstab_removed.append(line)
+ continue
+
+ try:
+ toks = WS.split(line)
+ except Exception:
+ pass
+ fstab_devs[toks[0]] = line
+ fstab_lines.append(line)
+
for i in range(len(cfgmnt)):
# skip something that wasn't a list
if not isinstance(cfgmnt[i], list):
@@ -336,12 +353,17 @@ def handle(_name, cfg, cloud, log, _args):
start = str(cfgmnt[i][0])
sanitized = sanitize_devname(start, cloud.device_name_to_device, log)
+ if sanitized != start:
+ log.debug("changed %s => %s" % (start, sanitized))
+
if sanitized is None:
- log.debug("Ignorming nonexistant named mount %s", start)
+ log.debug("Ignoring nonexistent named mount %s", start)
+ continue
+ elif sanitized in fstab_devs:
+ log.info("Device %s already defined in fstab: %s",
+ sanitized, fstab_devs[sanitized])
continue
- if sanitized != start:
- log.debug("changed %s => %s" % (start, sanitized))
cfgmnt[i][0] = sanitized
# in case the user did not quote a field (likely fs-freq, fs_passno)
@@ -373,11 +395,17 @@ def handle(_name, cfg, cloud, log, _args):
for defmnt in defmnts:
start = defmnt[0]
sanitized = sanitize_devname(start, cloud.device_name_to_device, log)
- if sanitized is None:
- log.debug("Ignoring nonexistant default named mount %s", start)
- continue
if sanitized != start:
log.debug("changed default device %s => %s" % (start, sanitized))
+
+ if sanitized is None:
+ log.debug("Ignoring nonexistent default named mount %s", start)
+ continue
+ elif sanitized in fstab_devs:
+ log.debug("Device %s already defined in fstab: %s",
+ sanitized, fstab_devs[sanitized])
+ continue
+
defmnt[0] = sanitized
cfgmnt_has = False
@@ -397,7 +425,7 @@ def handle(_name, cfg, cloud, log, _args):
actlist = []
for x in cfgmnt:
if x[1] is None:
- log.debug("Skipping non-existent device named %s", x[0])
+ log.debug("Skipping nonexistent device named %s", x[0])
else:
actlist.append(x)
@@ -406,34 +434,21 @@ def handle(_name, cfg, cloud, log, _args):
actlist.append([swapret, "none", "swap", "sw", "0", "0"])
if len(actlist) == 0:
- log.debug("No modifications to fstab needed.")
+ log.debug("No modifications to fstab needed")
return
- comment = "comment=cloudconfig"
cc_lines = []
needswap = False
dirs = []
for line in actlist:
# write 'comment' in the fs_mntops, entry, claiming this
- line[3] = "%s,%s" % (line[3], comment)
+ line[3] = "%s,%s" % (line[3], MNT_COMMENT)
if line[2] == "swap":
needswap = True
if line[1].startswith("/"):
dirs.append(line[1])
cc_lines.append('\t'.join(line))
- fstab_lines = []
- removed = []
- for line in util.load_file(FSTAB_PATH).splitlines():
- try:
- toks = WS.split(line)
- if toks[3].find(comment) != -1:
- removed.append(line)
- continue
- except Exception:
- pass
- fstab_lines.append(line)
-
for d in dirs:
try:
util.ensure_dir(d)
@@ -441,7 +456,7 @@ def handle(_name, cfg, cloud, log, _args):
util.logexc(log, "Failed to make '%s' config-mount", d)
sadds = [WS.sub(" ", n) for n in cc_lines]
- sdrops = [WS.sub(" ", n) for n in removed]
+ sdrops = [WS.sub(" ", n) for n in fstab_removed]
sops = (["- " + drop for drop in sdrops if drop not in sadds] +
["+ " + add for add in sadds if add not in sdrops])
diff --git a/cloudinit/config/cc_ntp.py b/cloudinit/config/cc_ntp.py
index cbd0237d..9e074bda 100644
--- a/cloudinit/config/cc_ntp.py
+++ b/cloudinit/config/cc_ntp.py
@@ -10,20 +10,95 @@ from cloudinit.config.schema import (
get_schema_doc, validate_cloudconfig_schema)
from cloudinit import log as logging
from cloudinit.settings import PER_INSTANCE
+from cloudinit import temp_utils
from cloudinit import templater
from cloudinit import type_utils
from cloudinit import util
+import copy
import os
+import six
from textwrap import dedent
LOG = logging.getLogger(__name__)
frequency = PER_INSTANCE
NTP_CONF = '/etc/ntp.conf'
-TIMESYNCD_CONF = '/etc/systemd/timesyncd.conf.d/cloud-init.conf'
NR_POOL_SERVERS = 4
-distros = ['centos', 'debian', 'fedora', 'opensuse', 'sles', 'ubuntu']
+distros = ['centos', 'debian', 'fedora', 'opensuse', 'rhel', 'sles', 'ubuntu']
+
+NTP_CLIENT_CONFIG = {
+ 'chrony': {
+ 'check_exe': 'chronyd',
+ 'confpath': '/etc/chrony.conf',
+ 'packages': ['chrony'],
+ 'service_name': 'chrony',
+ 'template_name': 'chrony.conf.{distro}',
+ 'template': None,
+ },
+ 'ntp': {
+ 'check_exe': 'ntpd',
+ 'confpath': NTP_CONF,
+ 'packages': ['ntp'],
+ 'service_name': 'ntp',
+ 'template_name': 'ntp.conf.{distro}',
+ 'template': None,
+ },
+ 'ntpdate': {
+ 'check_exe': 'ntpdate',
+ 'confpath': NTP_CONF,
+ 'packages': ['ntpdate'],
+ 'service_name': 'ntpdate',
+ 'template_name': 'ntp.conf.{distro}',
+ 'template': None,
+ },
+ 'systemd-timesyncd': {
+ 'check_exe': '/lib/systemd/systemd-timesyncd',
+ 'confpath': '/etc/systemd/timesyncd.conf.d/cloud-init.conf',
+ 'packages': [],
+ 'service_name': 'systemd-timesyncd',
+ 'template_name': 'timesyncd.conf',
+ 'template': None,
+ },
+}
+
+# This is Distro-specific configuration overrides of the base config
+DISTRO_CLIENT_CONFIG = {
+ 'debian': {
+ 'chrony': {
+ 'confpath': '/etc/chrony/chrony.conf',
+ },
+ },
+ 'opensuse': {
+ 'chrony': {
+ 'service_name': 'chronyd',
+ },
+ 'ntp': {
+ 'confpath': '/etc/ntp.conf',
+ 'service_name': 'ntpd',
+ },
+ 'systemd-timesyncd': {
+ 'check_exe': '/usr/lib/systemd/systemd-timesyncd',
+ },
+ },
+ 'sles': {
+ 'chrony': {
+ 'service_name': 'chronyd',
+ },
+ 'ntp': {
+ 'confpath': '/etc/ntp.conf',
+ 'service_name': 'ntpd',
+ },
+ 'systemd-timesyncd': {
+ 'check_exe': '/usr/lib/systemd/systemd-timesyncd',
+ },
+ },
+ 'ubuntu': {
+ 'chrony': {
+ 'confpath': '/etc/chrony/chrony.conf',
+ },
+ },
+}
# The schema definition for each cloud-config module is a strict contract for
@@ -48,7 +123,34 @@ schema = {
'distros': distros,
'examples': [
dedent("""\
+ # Override ntp with chrony configuration on Ubuntu
+ ntp:
+ enabled: true
+ ntp_client: chrony # Uses cloud-init default chrony configuration
+ """),
+ dedent("""\
+ # Provide a custom ntp client configuration
ntp:
+ enabled: true
+ ntp_client: myntpclient
+ config:
+ confpath: /etc/myntpclient/myntpclient.conf
+ check_exe: myntpclientd
+ packages:
+ - myntpclient
+ service_name: myntpclient
+ template: |
+ ## template:jinja
+ # My NTP Client config
+ {% if pools -%}# pools{% endif %}
+ {% for pool in pools -%}
+ pool {{pool}} iburst
+ {% endfor %}
+ {%- if servers %}# servers
+ {% endif %}
+ {% for server in servers -%}
+ server {{server}} iburst
+ {% endfor %}
pools: [0.int.pool.ntp.org, 1.int.pool.ntp.org, ntp.myorg.org]
servers:
- ntp.server.local
@@ -83,79 +185,159 @@ schema = {
List of ntp servers. If both pools and servers are
empty, 4 default pool servers will be provided with
the format ``{0-3}.{distro}.pool.ntp.org``.""")
- }
+ },
+ 'ntp_client': {
+ 'type': 'string',
+ 'default': 'auto',
+ 'description': dedent("""\
+ Name of an NTP client to use to configure system NTP.
+ When unprovided or 'auto' the default client preferred
+ by the distribution will be used. The following
+ built-in client names can be used to override existing
+ configuration defaults: chrony, ntp, ntpdate,
+ systemd-timesyncd."""),
+ },
+ 'enabled': {
+ 'type': 'boolean',
+ 'default': True,
+ 'description': dedent("""\
+ Attempt to enable ntp clients if set to True. If set
+ to False, ntp client will not be configured or
+ installed"""),
+ },
+ 'config': {
+ 'description': dedent("""\
+ Configuration settings or overrides for the
+ ``ntp_client`` specified."""),
+ 'type': ['object'],
+ 'properties': {
+ 'confpath': {
+ 'type': 'string',
+ 'description': dedent("""\
+ The path to where the ``ntp_client``
+ configuration is written."""),
+ },
+ 'check_exe': {
+ 'type': 'string',
+ 'description': dedent("""\
+ The executable name for the ``ntp_client``.
+ For example, ntp service ``check_exe`` is
+ 'ntpd' because it runs the ntpd binary."""),
+ },
+ 'packages': {
+ 'type': 'array',
+ 'items': {
+ 'type': 'string',
+ },
+ 'uniqueItems': True,
+ 'description': dedent("""\
+ List of packages needed to be installed for the
+ selected ``ntp_client``."""),
+ },
+ 'service_name': {
+ 'type': 'string',
+ 'description': dedent("""\
+ The systemd or sysvinit service name used to
+ start and stop the ``ntp_client``
+ service."""),
+ },
+ 'template': {
+ 'type': 'string',
+ 'description': dedent("""\
+ Inline template allowing users to define their
+ own ``ntp_client`` configuration template.
+ The value must start with '## template:jinja'
+ to enable use of templating support.
+ """),
+ },
+ },
+ # Don't use REQUIRED_NTP_CONFIG_KEYS to allow for override
+ # of builtin client values.
+ 'required': [],
+ 'minProperties': 1, # If we have config, define something
+ 'additionalProperties': False
+ },
},
'required': [],
'additionalProperties': False
}
}
}
-
-__doc__ = get_schema_doc(schema) # Supplement python help()
+REQUIRED_NTP_CONFIG_KEYS = frozenset([
+ 'check_exe', 'confpath', 'packages', 'service_name'])
-def handle(name, cfg, cloud, log, _args):
- """Enable and configure ntp."""
- if 'ntp' not in cfg:
- LOG.debug(
- "Skipping module named %s, not present or disabled by cfg", name)
- return
- ntp_cfg = cfg['ntp']
- if ntp_cfg is None:
- ntp_cfg = {} # Allow empty config which will install the package
+__doc__ = get_schema_doc(schema) # Supplement python help()
- # TODO drop this when validate_cloudconfig_schema is strict=True
- if not isinstance(ntp_cfg, (dict)):
- raise RuntimeError(
- "'ntp' key existed in config, but not a dictionary type,"
- " is a {_type} instead".format(_type=type_utils.obj_name(ntp_cfg)))
- validate_cloudconfig_schema(cfg, schema)
- if ntp_installable():
- service_name = 'ntp'
- confpath = NTP_CONF
- template_name = None
- packages = ['ntp']
- check_exe = 'ntpd'
- else:
- service_name = 'systemd-timesyncd'
- confpath = TIMESYNCD_CONF
- template_name = 'timesyncd.conf'
- packages = []
- check_exe = '/lib/systemd/systemd-timesyncd'
-
- rename_ntp_conf()
- # ensure when ntp is installed it has a configuration file
- # to use instead of starting up with packaged defaults
- write_ntp_config_template(ntp_cfg, cloud, confpath, template=template_name)
- install_ntp(cloud.distro.install_packages, packages=packages,
- check_exe=check_exe)
+def distro_ntp_client_configs(distro):
+ """Construct a distro-specific ntp client config dictionary by merging
+ distro specific changes into base config.
- try:
- reload_ntp(service_name, systemd=cloud.distro.uses_systemd())
- except util.ProcessExecutionError as e:
- LOG.exception("Failed to reload/start ntp service: %s", e)
- raise
+ @param distro: String providing the distro class name.
+ @returns: Dict of distro configurations for ntp clients.
+ """
+ dcfg = DISTRO_CLIENT_CONFIG
+ cfg = copy.copy(NTP_CLIENT_CONFIG)
+ if distro in dcfg:
+ cfg = util.mergemanydict([cfg, dcfg[distro]], reverse=True)
+ return cfg
-def ntp_installable():
- """Check if we can install ntp package
+def select_ntp_client(ntp_client, distro):
+ """Determine which ntp client is to be used, consulting the distro
+ for its preference.
- Ubuntu-Core systems do not have an ntp package available, so
- we always return False. Other systems require package managers to install
- the ntp package If we fail to find one of the package managers, then we
- cannot install ntp.
+ @param ntp_client: String name of the ntp client to use.
+ @param distro: Distro class instance.
+ @returns: Dict of the selected ntp client or {} if none selected.
"""
- if util.system_is_snappy():
- return False
- if any(map(util.which, ['apt-get', 'dnf', 'yum', 'zypper'])):
- return True
+ # construct distro-specific ntp_client_config dict
+ distro_cfg = distro_ntp_client_configs(distro.name)
+
+ # user specified client, return its config
+ if ntp_client and ntp_client != 'auto':
+ LOG.debug('Selected NTP client "%s" via user-data configuration',
+ ntp_client)
+ return distro_cfg.get(ntp_client, {})
+
+ # default to auto if unset in distro
+ distro_ntp_client = distro.get_option('ntp_client', 'auto')
+
+ clientcfg = {}
+ if distro_ntp_client == "auto":
+ for client in distro.preferred_ntp_clients:
+ cfg = distro_cfg.get(client)
+ if util.which(cfg.get('check_exe')):
+ LOG.debug('Selected NTP client "%s", already installed',
+ client)
+ clientcfg = cfg
+ break
+
+ if not clientcfg:
+ client = distro.preferred_ntp_clients[0]
+ LOG.debug(
+ 'Selected distro preferred NTP client "%s", not yet installed',
+ client)
+ clientcfg = distro_cfg.get(client)
+ else:
+ LOG.debug('Selected NTP client "%s" via distro system config',
+ distro_ntp_client)
+ clientcfg = distro_cfg.get(distro_ntp_client, {})
+
+ return clientcfg
- return False
+def install_ntp_client(install_func, packages=None, check_exe="ntpd"):
+ """Install ntp client package if not already installed.
-def install_ntp(install_func, packages=None, check_exe="ntpd"):
+ @param install_func: function. This parameter is invoked with the contents
+ of the packages parameter.
+ @param packages: list. This parameter defaults to ['ntp'].
+ @param check_exe: string. The name of a binary that indicates the package
+ the specified package is already installed.
+ """
if util.which(check_exe):
return
if packages is None:
@@ -164,15 +346,23 @@ def install_ntp(install_func, packages=None, check_exe="ntpd"):
install_func(packages)
-def rename_ntp_conf(config=None):
- """Rename any existing ntp.conf file"""
- if config is None: # For testing
- config = NTP_CONF
- if os.path.exists(config):
- util.rename(config, config + ".dist")
+def rename_ntp_conf(confpath=None):
+ """Rename any existing ntp client config file
+
+ @param confpath: string. Specify a path to an existing ntp client
+ configuration file.
+ """
+ if os.path.exists(confpath):
+ util.rename(confpath, confpath + ".dist")
def generate_server_names(distro):
+ """Generate a list of server names to populate an ntp client configuration
+ file.
+
+ @param distro: string. Specify the distro name
+ @returns: list: A list of strings representing ntp servers for this distro.
+ """
names = []
pool_distro = distro
# For legal reasons x.pool.sles.ntp.org does not exist,
@@ -185,34 +375,60 @@ def generate_server_names(distro):
return names
-def write_ntp_config_template(cfg, cloud, path, template=None):
- servers = cfg.get('servers', [])
- pools = cfg.get('pools', [])
+def write_ntp_config_template(distro_name, servers=None, pools=None,
+ path=None, template_fn=None, template=None):
+ """Render a ntp client configuration for the specified client.
+
+ @param distro_name: string. The distro class name.
+ @param servers: A list of strings specifying ntp servers. Defaults to empty
+ list.
+ @param pools: A list of strings specifying ntp pools. Defaults to empty
+ list.
+ @param path: A string to specify where to write the rendered template.
+ @param template_fn: A string to specify the template source file.
+ @param template: A string specifying the contents of the template. This
+ content will be written to a temporary file before being used to render
+ the configuration file.
+
+ @raises: ValueError when path is None.
+ @raises: ValueError when template_fn is None and template is None.
+ """
+ if not servers:
+ servers = []
+ if not pools:
+ pools = []
if len(servers) == 0 and len(pools) == 0:
- pools = generate_server_names(cloud.distro.name)
+ pools = generate_server_names(distro_name)
LOG.debug(
'Adding distro default ntp pool servers: %s', ','.join(pools))
- params = {
- 'servers': servers,
- 'pools': pools,
- }
+ if not path:
+ raise ValueError('Invalid value for path parameter')
- if template is None:
- template = 'ntp.conf.%s' % cloud.distro.name
+ if not template_fn and not template:
+ raise ValueError('Not template_fn or template provided')
- template_fn = cloud.get_template_filename(template)
- if not template_fn:
- template_fn = cloud.get_template_filename('ntp.conf')
- if not template_fn:
- raise RuntimeError(
- 'No template found, not rendering {path}'.format(path=path))
+ params = {'servers': servers, 'pools': pools}
+ if template:
+ tfile = temp_utils.mkstemp(prefix='template_name-', suffix=".tmpl")
+ template_fn = tfile[1] # filepath is second item in tuple
+ util.write_file(template_fn, content=template)
templater.render_to_file(template_fn, path, params)
+ # clean up temporary template
+ if template:
+ util.del_file(template_fn)
def reload_ntp(service, systemd=False):
+ """Restart or reload an ntp system service.
+
+ @param service: A string specifying the name of the service to be affected.
+ @param systemd: A boolean indicating if the distro uses systemd, defaults
+ to False.
+ @returns: A tuple of stdout, stderr results from executing the action.
+ """
if systemd:
cmd = ['systemctl', 'reload-or-restart', service]
else:
@@ -220,4 +436,117 @@ def reload_ntp(service, systemd=False):
util.subp(cmd, capture=True)
+def supplemental_schema_validation(ntp_config):
+ """Validate user-provided ntp:config option values.
+
+ This function supplements flexible jsonschema validation with specific
+ value checks to aid in triage of invalid user-provided configuration.
+
+ @param ntp_config: Dictionary of configuration value under 'ntp'.
+
+ @raises: ValueError describing invalid values provided.
+ """
+ errors = []
+ missing = REQUIRED_NTP_CONFIG_KEYS.difference(set(ntp_config.keys()))
+ if missing:
+ keys = ', '.join(sorted(missing))
+ errors.append(
+ 'Missing required ntp:config keys: {keys}'.format(keys=keys))
+ elif not any([ntp_config.get('template'),
+ ntp_config.get('template_name')]):
+ errors.append(
+ 'Either ntp:config:template or ntp:config:template_name values'
+ ' are required')
+ for key, value in sorted(ntp_config.items()):
+ keypath = 'ntp:config:' + key
+ if key == 'confpath':
+ if not all([value, isinstance(value, six.string_types)]):
+ errors.append(
+ 'Expected a config file path {keypath}.'
+ ' Found ({value})'.format(keypath=keypath, value=value))
+ elif key == 'packages':
+ if not isinstance(value, list):
+ errors.append(
+ 'Expected a list of required package names for {keypath}.'
+ ' Found ({value})'.format(keypath=keypath, value=value))
+ elif key in ('template', 'template_name'):
+ if value is None: # Either template or template_name can be none
+ continue
+ if not isinstance(value, six.string_types):
+ errors.append(
+ 'Expected a string type for {keypath}.'
+ ' Found ({value})'.format(keypath=keypath, value=value))
+ elif not isinstance(value, six.string_types):
+ errors.append(
+ 'Expected a string type for {keypath}.'
+ ' Found ({value})'.format(keypath=keypath, value=value))
+
+ if errors:
+ raise ValueError(r'Invalid ntp configuration:\n{errors}'.format(
+ errors='\n'.join(errors)))
+
+
+def handle(name, cfg, cloud, log, _args):
+ """Enable and configure ntp."""
+ if 'ntp' not in cfg:
+ LOG.debug(
+ "Skipping module named %s, not present or disabled by cfg", name)
+ return
+ ntp_cfg = cfg['ntp']
+ if ntp_cfg is None:
+ ntp_cfg = {} # Allow empty config which will install the package
+
+ # TODO drop this when validate_cloudconfig_schema is strict=True
+ if not isinstance(ntp_cfg, (dict)):
+ raise RuntimeError(
+ "'ntp' key existed in config, but not a dictionary type,"
+ " is a {_type} instead".format(_type=type_utils.obj_name(ntp_cfg)))
+
+ validate_cloudconfig_schema(cfg, schema)
+
+ # Allow users to explicitly enable/disable
+ enabled = ntp_cfg.get('enabled', True)
+ if util.is_false(enabled):
+ LOG.debug("Skipping module named %s, disabled by cfg", name)
+ return
+
+ # Select which client is going to be used and get the configuration
+ ntp_client_config = select_ntp_client(ntp_cfg.get('ntp_client'),
+ cloud.distro)
+
+ # Allow user ntp config to override distro configurations
+ ntp_client_config = util.mergemanydict(
+ [ntp_client_config, ntp_cfg.get('config', {})], reverse=True)
+
+ supplemental_schema_validation(ntp_client_config)
+ rename_ntp_conf(confpath=ntp_client_config.get('confpath'))
+
+ template_fn = None
+ if not ntp_client_config.get('template'):
+ template_name = (
+ ntp_client_config.get('template_name').replace('{distro}',
+ cloud.distro.name))
+ template_fn = cloud.get_template_filename(template_name)
+ if not template_fn:
+ msg = ('No template found, not rendering %s' %
+ ntp_client_config.get('template_name'))
+ raise RuntimeError(msg)
+
+ write_ntp_config_template(cloud.distro.name,
+ servers=ntp_cfg.get('servers', []),
+ pools=ntp_cfg.get('pools', []),
+ path=ntp_client_config.get('confpath'),
+ template_fn=template_fn,
+ template=ntp_client_config.get('template'))
+
+ install_ntp_client(cloud.distro.install_packages,
+ packages=ntp_client_config['packages'],
+ check_exe=ntp_client_config['check_exe'])
+ try:
+ reload_ntp(ntp_client_config['service_name'],
+ systemd=cloud.distro.uses_systemd())
+ except util.ProcessExecutionError as e:
+ LOG.exception("Failed to reload/start ntp service: %s", e)
+ raise
+
# vi: ts=4 expandtab
diff --git a/cloudinit/config/cc_phone_home.py b/cloudinit/config/cc_phone_home.py
index 878069b7..3be0d1c1 100644
--- a/cloudinit/config/cc_phone_home.py
+++ b/cloudinit/config/cc_phone_home.py
@@ -41,6 +41,7 @@ keys to post. Available keys are:
"""
from cloudinit import templater
+from cloudinit import url_helper
from cloudinit import util
from cloudinit.settings import PER_INSTANCE
@@ -136,9 +137,9 @@ def handle(name, cfg, cloud, log, args):
}
url = templater.render_string(url, url_params)
try:
- util.read_file_or_url(url, data=real_submit_keys,
- retries=tries, sec_between=3,
- ssl_details=util.fetch_ssl_details(cloud.paths))
+ url_helper.read_file_or_url(
+ url, data=real_submit_keys, retries=tries, sec_between=3,
+ ssl_details=util.fetch_ssl_details(cloud.paths))
except Exception:
util.logexc(log, "Failed to post phone home data to %s in %s tries",
url, tries)
diff --git a/cloudinit/config/cc_power_state_change.py b/cloudinit/config/cc_power_state_change.py
index 4da3a588..50b37470 100644
--- a/cloudinit/config/cc_power_state_change.py
+++ b/cloudinit/config/cc_power_state_change.py
@@ -74,7 +74,7 @@ def givecmdline(pid):
if util.is_FreeBSD():
(output, _err) = util.subp(['procstat', '-c', str(pid)])
line = output.splitlines()[1]
- m = re.search('\d+ (\w|\.|-)+\s+(/\w.+)', line)
+ m = re.search(r'\d+ (\w|\.|-)+\s+(/\w.+)', line)
return m.group(2)
else:
return util.load_file("/proc/%s/cmdline" % pid)
diff --git a/cloudinit/config/cc_resizefs.py b/cloudinit/config/cc_resizefs.py
index 013e69b5..2edddd0c 100644
--- a/cloudinit/config/cc_resizefs.py
+++ b/cloudinit/config/cc_resizefs.py
@@ -81,7 +81,7 @@ def _resize_xfs(mount_point, devpth):
def _resize_ufs(mount_point, devpth):
- return ('growfs', devpth)
+ return ('growfs', '-y', devpth)
def _resize_zfs(mount_point, devpth):
@@ -89,13 +89,11 @@ def _resize_zfs(mount_point, devpth):
def _get_dumpfs_output(mount_point):
- dumpfs_res, err = util.subp(['dumpfs', '-m', mount_point])
- return dumpfs_res
+ return util.subp(['dumpfs', '-m', mount_point])[0]
def _get_gpart_output(part):
- gpart_res, err = util.subp(['gpart', 'show', part])
- return gpart_res
+ return util.subp(['gpart', 'show', part])[0]
def _can_skip_resize_ufs(mount_point, devpth):
@@ -113,7 +111,7 @@ def _can_skip_resize_ufs(mount_point, devpth):
if not line.startswith('#'):
newfs_cmd = shlex.split(line)
opt_value = 'O:Ua:s:b:d:e:f:g:h:i:jk:m:o:'
- optlist, args = getopt.getopt(newfs_cmd[1:], opt_value)
+ optlist, _args = getopt.getopt(newfs_cmd[1:], opt_value)
for o, a in optlist:
if o == "-s":
cur_fs_sz = int(a)
diff --git a/cloudinit/config/cc_rh_subscription.py b/cloudinit/config/cc_rh_subscription.py
index 530808ce..1c679430 100644
--- a/cloudinit/config/cc_rh_subscription.py
+++ b/cloudinit/config/cc_rh_subscription.py
@@ -209,8 +209,7 @@ class SubscriptionManager(object):
cmd.append("--serverurl={0}".format(self.server_hostname))
try:
- return_out, return_err = self._sub_man_cli(cmd,
- logstring_val=True)
+ return_out = self._sub_man_cli(cmd, logstring_val=True)[0]
except util.ProcessExecutionError as e:
if e.stdout == "":
self.log_warn("Registration failed due "
@@ -233,8 +232,7 @@ class SubscriptionManager(object):
# Attempting to register the system only
try:
- return_out, return_err = self._sub_man_cli(cmd,
- logstring_val=True)
+ return_out = self._sub_man_cli(cmd, logstring_val=True)[0]
except util.ProcessExecutionError as e:
if e.stdout == "":
self.log_warn("Registration failed due "
@@ -257,7 +255,7 @@ class SubscriptionManager(object):
.format(self.servicelevel)]
try:
- return_out, return_err = self._sub_man_cli(cmd)
+ return_out = self._sub_man_cli(cmd)[0]
except util.ProcessExecutionError as e:
if e.stdout.rstrip() != '':
for line in e.stdout.split("\n"):
@@ -275,7 +273,7 @@ class SubscriptionManager(object):
def _set_auto_attach(self):
cmd = ['attach', '--auto']
try:
- return_out, return_err = self._sub_man_cli(cmd)
+ return_out = self._sub_man_cli(cmd)[0]
except util.ProcessExecutionError as e:
self.log_warn("Auto-attach failed with: {0}".format(e))
return False
@@ -294,12 +292,12 @@ class SubscriptionManager(object):
# Get all available pools
cmd = ['list', '--available', '--pool-only']
- results, errors = self._sub_man_cli(cmd)
+ results = self._sub_man_cli(cmd)[0]
available = (results.rstrip()).split("\n")
# Get all consumed pools
cmd = ['list', '--consumed', '--pool-only']
- results, errors = self._sub_man_cli(cmd)
+ results = self._sub_man_cli(cmd)[0]
consumed = (results.rstrip()).split("\n")
return available, consumed
@@ -311,14 +309,14 @@ class SubscriptionManager(object):
'''
cmd = ['repos', '--list-enabled']
- return_out, return_err = self._sub_man_cli(cmd)
+ return_out = self._sub_man_cli(cmd)[0]
active_repos = []
for repo in return_out.split("\n"):
if "Repo ID:" in repo:
active_repos.append((repo.split(':')[1]).strip())
cmd = ['repos', '--list-disabled']
- return_out, return_err = self._sub_man_cli(cmd)
+ return_out = self._sub_man_cli(cmd)[0]
inactive_repos = []
for repo in return_out.split("\n"):
diff --git a/cloudinit/config/cc_rsyslog.py b/cloudinit/config/cc_rsyslog.py
index af08788c..27d2366c 100644
--- a/cloudinit/config/cc_rsyslog.py
+++ b/cloudinit/config/cc_rsyslog.py
@@ -203,8 +203,8 @@ LOG = logging.getLogger(__name__)
COMMENT_RE = re.compile(r'[ ]*[#]+[ ]*')
HOST_PORT_RE = re.compile(
r'^(?P<proto>[@]{0,2})'
- '(([[](?P<bracket_addr>[^\]]*)[\]])|(?P<addr>[^:]*))'
- '([:](?P<port>[0-9]+))?$')
+ r'(([[](?P<bracket_addr>[^\]]*)[\]])|(?P<addr>[^:]*))'
+ r'([:](?P<port>[0-9]+))?$')
def reload_syslog(command=DEF_RELOAD, systemd=False):
diff --git a/cloudinit/config/cc_runcmd.py b/cloudinit/config/cc_runcmd.py
index 539cbd5d..b6f6c807 100644
--- a/cloudinit/config/cc_runcmd.py
+++ b/cloudinit/config/cc_runcmd.py
@@ -66,7 +66,6 @@ schema = {
'additionalProperties': False,
'minItems': 1,
'required': [],
- 'uniqueItems': True
}
}
}
diff --git a/cloudinit/config/cc_set_passwords.py b/cloudinit/config/cc_set_passwords.py
index bb24d57f..5ef97376 100755
--- a/cloudinit/config/cc_set_passwords.py
+++ b/cloudinit/config/cc_set_passwords.py
@@ -68,16 +68,57 @@ import re
import sys
from cloudinit.distros import ug_util
-from cloudinit import ssh_util
+from cloudinit import log as logging
+from cloudinit.ssh_util import update_ssh_config
from cloudinit import util
from string import ascii_letters, digits
+LOG = logging.getLogger(__name__)
+
# We are removing certain 'painful' letters/numbers
PW_SET = (''.join([x for x in ascii_letters + digits
if x not in 'loLOI01']))
+def handle_ssh_pwauth(pw_auth, service_cmd=None, service_name="ssh"):
+ """Apply sshd PasswordAuthentication changes.
+
+ @param pw_auth: config setting from 'pw_auth'.
+ Best given as True, False, or "unchanged".
+ @param service_cmd: The service command list (['service'])
+ @param service_name: The name of the sshd service for the system.
+
+ @return: None"""
+ cfg_name = "PasswordAuthentication"
+ if service_cmd is None:
+ service_cmd = ["service"]
+
+ if util.is_true(pw_auth):
+ cfg_val = 'yes'
+ elif util.is_false(pw_auth):
+ cfg_val = 'no'
+ else:
+ bmsg = "Leaving ssh config '%s' unchanged." % cfg_name
+ if pw_auth is None or pw_auth.lower() == 'unchanged':
+ LOG.debug("%s ssh_pwauth=%s", bmsg, pw_auth)
+ else:
+ LOG.warning("%s Unrecognized value: ssh_pwauth=%s", bmsg, pw_auth)
+ return
+
+ updated = update_ssh_config({cfg_name: cfg_val})
+ if not updated:
+ LOG.debug("No need to restart ssh service, %s not updated.", cfg_name)
+ return
+
+ if 'systemctl' in service_cmd:
+ cmd = list(service_cmd) + ["restart", service_name]
+ else:
+ cmd = list(service_cmd) + [service_name, "restart"]
+ util.subp(cmd)
+ LOG.debug("Restarted the ssh daemon.")
+
+
def handle(_name, cfg, cloud, log, args):
if len(args) != 0:
# if run from command line, and give args, wipe the chpasswd['list']
@@ -170,65 +211,9 @@ def handle(_name, cfg, cloud, log, args):
if expired_users:
log.debug("Expired passwords for: %s users", expired_users)
- change_pwauth = False
- pw_auth = None
- if 'ssh_pwauth' in cfg:
- if util.is_true(cfg['ssh_pwauth']):
- change_pwauth = True
- pw_auth = 'yes'
- elif util.is_false(cfg['ssh_pwauth']):
- change_pwauth = True
- pw_auth = 'no'
- elif str(cfg['ssh_pwauth']).lower() == 'unchanged':
- log.debug('Leaving auth line unchanged')
- change_pwauth = False
- elif not str(cfg['ssh_pwauth']).strip():
- log.debug('Leaving auth line unchanged')
- change_pwauth = False
- elif not cfg['ssh_pwauth']:
- log.debug('Leaving auth line unchanged')
- change_pwauth = False
- else:
- msg = 'Unrecognized value %s for ssh_pwauth' % cfg['ssh_pwauth']
- util.logexc(log, msg)
-
- if change_pwauth:
- replaced_auth = False
-
- # See: man sshd_config
- old_lines = ssh_util.parse_ssh_config(ssh_util.DEF_SSHD_CFG)
- new_lines = []
- i = 0
- for (i, line) in enumerate(old_lines):
- # Keywords are case-insensitive and arguments are case-sensitive
- if line.key == 'passwordauthentication':
- log.debug("Replacing auth line %s with %s", i + 1, pw_auth)
- replaced_auth = True
- line.value = pw_auth
- new_lines.append(line)
-
- if not replaced_auth:
- log.debug("Adding new auth line %s", i + 1)
- replaced_auth = True
- new_lines.append(ssh_util.SshdConfigLine('',
- 'PasswordAuthentication',
- pw_auth))
-
- lines = [str(l) for l in new_lines]
- util.write_file(ssh_util.DEF_SSHD_CFG, "\n".join(lines),
- copy_mode=True)
-
- try:
- cmd = cloud.distro.init_cmd # Default service
- cmd.append(cloud.distro.get_option('ssh_svcname', 'ssh'))
- cmd.append('restart')
- if 'systemctl' in cmd: # Switch action ordering
- cmd[1], cmd[2] = cmd[2], cmd[1]
- cmd = filter(None, cmd) # Remove empty arguments
- util.subp(cmd)
- log.debug("Restarted the ssh daemon")
- except Exception:
- util.logexc(log, "Restarting of the ssh daemon failed")
+ handle_ssh_pwauth(
+ cfg.get('ssh_pwauth'), service_cmd=cloud.distro.init_cmd,
+ service_name=cloud.distro.get_option('ssh_svcname', 'ssh'))
if len(errors):
log.debug("%s errors occured, re-raising the last one", len(errors))
diff --git a/cloudinit/config/cc_snap.py b/cloudinit/config/cc_snap.py
index 34a53fd4..90724b81 100644
--- a/cloudinit/config/cc_snap.py
+++ b/cloudinit/config/cc_snap.py
@@ -110,7 +110,6 @@ schema = {
'additionalItems': False, # Reject non-string & non-list
'minItems': 1,
'minProperties': 1,
- 'uniqueItems': True
},
'squashfuse_in_container': {
'type': 'boolean'
@@ -204,12 +203,12 @@ def maybe_install_squashfuse(cloud):
return
try:
cloud.distro.update_package_sources()
- except Exception as e:
+ except Exception:
util.logexc(LOG, "Package update failed")
raise
try:
cloud.distro.install_packages(['squashfuse'])
- except Exception as e:
+ except Exception:
util.logexc(LOG, "Failed to install squashfuse")
raise
diff --git a/cloudinit/config/cc_snappy.py b/cloudinit/config/cc_snappy.py
index bab80bbe..15bee2d3 100644
--- a/cloudinit/config/cc_snappy.py
+++ b/cloudinit/config/cc_snappy.py
@@ -213,7 +213,7 @@ def render_snap_op(op, name, path=None, cfgfile=None, config=None):
def read_installed_packages():
ret = []
- for (name, date, version, dev) in read_pkg_data():
+ for (name, _date, _version, dev) in read_pkg_data():
if dev:
ret.append(NAMESPACE_DELIM.join([name, dev]))
else:
@@ -222,7 +222,7 @@ def read_installed_packages():
def read_pkg_data():
- out, err = util.subp([SNAPPY_CMD, "list"])
+ out, _err = util.subp([SNAPPY_CMD, "list"])
pkg_data = []
for line in out.splitlines()[1:]:
toks = line.split(sep=None, maxsplit=3)
diff --git a/cloudinit/config/cc_ubuntu_advantage.py b/cloudinit/config/cc_ubuntu_advantage.py
index 16b1868b..5e082bd6 100644
--- a/cloudinit/config/cc_ubuntu_advantage.py
+++ b/cloudinit/config/cc_ubuntu_advantage.py
@@ -87,7 +87,6 @@ schema = {
'additionalItems': False, # Reject non-string & non-list
'minItems': 1,
'minProperties': 1,
- 'uniqueItems': True
}
},
'additionalProperties': False, # Reject keys not in schema
@@ -149,12 +148,12 @@ def maybe_install_ua_tools(cloud):
return
try:
cloud.distro.update_package_sources()
- except Exception as e:
+ except Exception:
util.logexc(LOG, "Package update failed")
raise
try:
cloud.distro.install_packages(['ubuntu-advantage-tools'])
- except Exception as e:
+ except Exception:
util.logexc(LOG, "Failed to install ubuntu-advantage-tools")
raise
diff --git a/cloudinit/config/cc_users_groups.py b/cloudinit/config/cc_users_groups.py
index b215e95a..c95bdaad 100644
--- a/cloudinit/config/cc_users_groups.py
+++ b/cloudinit/config/cc_users_groups.py
@@ -54,8 +54,9 @@ config keys for an entry in ``users`` are as follows:
- ``ssh_authorized_keys``: Optional. List of ssh keys to add to user's
authkeys file. Default: none
- ``ssh_import_id``: Optional. SSH id to import for user. Default: none
- - ``sudo``: Optional. Sudo rule to use, or list of sudo rules to use.
- Default: none.
+ - ``sudo``: Optional. Sudo rule to use, list of sudo rules to use or False.
+ Default: none. An absence of sudo key, or a value of none or false
+ will result in no sudo rules being written for the user.
- ``system``: Optional. Create user as system user with no home directory.
Default: false
- ``uid``: Optional. The user's ID. Default: The next available value.
@@ -82,6 +83,9 @@ config keys for an entry in ``users`` are as follows:
users:
- default
+ # User explicitly omitted from sudo permission; also default behavior.
+ - name: <some_restricted_user>
+ sudo: false
- name: <username>
expiredate: <date>
gecos: <comment>
diff --git a/cloudinit/config/schema.py b/cloudinit/config/schema.py
index ca7d0d5b..080a6d06 100644
--- a/cloudinit/config/schema.py
+++ b/cloudinit/config/schema.py
@@ -4,7 +4,7 @@
from __future__ import print_function
from cloudinit import importer
-from cloudinit.util import find_modules, read_file_or_url
+from cloudinit.util import find_modules, load_file
import argparse
from collections import defaultdict
@@ -93,20 +93,33 @@ def validate_cloudconfig_schema(config, schema, strict=False):
def annotated_cloudconfig_file(cloudconfig, original_content, schema_errors):
"""Return contents of the cloud-config file annotated with schema errors.
- @param cloudconfig: YAML-loaded object from the original_content.
+ @param cloudconfig: YAML-loaded dict from the original_content or empty
+ dict if unparseable.
@param original_content: The contents of a cloud-config file
@param schema_errors: List of tuples from a JSONSchemaValidationError. The
tuples consist of (schemapath, error_message).
"""
if not schema_errors:
return original_content
- schemapaths = _schemapath_for_cloudconfig(cloudconfig, original_content)
+ schemapaths = {}
+ if cloudconfig:
+ schemapaths = _schemapath_for_cloudconfig(
+ cloudconfig, original_content)
errors_by_line = defaultdict(list)
error_count = 1
error_footer = []
annotated_content = []
for path, msg in schema_errors:
- errors_by_line[schemapaths[path]].append(msg)
+ match = re.match(r'format-l(?P<line>\d+)\.c(?P<col>\d+).*', path)
+ if match:
+ line, col = match.groups()
+ errors_by_line[int(line)].append(msg)
+ else:
+ col = None
+ errors_by_line[schemapaths[path]].append(msg)
+ if col is not None:
+ msg = 'Line {line} column {col}: {msg}'.format(
+ line=line, col=col, msg=msg)
error_footer.append('# E{0}: {1}'.format(error_count, msg))
error_count += 1
lines = original_content.decode().split('\n')
@@ -139,21 +152,34 @@ def validate_cloudconfig_file(config_path, schema, annotate=False):
"""
if not os.path.exists(config_path):
raise RuntimeError('Configfile {0} does not exist'.format(config_path))
- content = read_file_or_url('file://{0}'.format(config_path)).contents
+ content = load_file(config_path, decode=False)
if not content.startswith(CLOUD_CONFIG_HEADER):
errors = (
- ('header', 'File {0} needs to begin with "{1}"'.format(
+ ('format-l1.c1', 'File {0} needs to begin with "{1}"'.format(
config_path, CLOUD_CONFIG_HEADER.decode())),)
- raise SchemaValidationError(errors)
-
+ error = SchemaValidationError(errors)
+ if annotate:
+ print(annotated_cloudconfig_file({}, content, error.schema_errors))
+ raise error
try:
cloudconfig = yaml.safe_load(content)
- except yaml.parser.ParserError as e:
- errors = (
- ('format', 'File {0} is not valid yaml. {1}'.format(
- config_path, str(e))),)
- raise SchemaValidationError(errors)
-
+ except (yaml.YAMLError) as e:
+ line = column = 1
+ mark = None
+ if hasattr(e, 'context_mark') and getattr(e, 'context_mark'):
+ mark = getattr(e, 'context_mark')
+ elif hasattr(e, 'problem_mark') and getattr(e, 'problem_mark'):
+ mark = getattr(e, 'problem_mark')
+ if mark:
+ line = mark.line + 1
+ column = mark.column + 1
+ errors = (('format-l{line}.c{col}'.format(line=line, col=column),
+ 'File {0} is not valid yaml. {1}'.format(
+ config_path, str(e))),)
+ error = SchemaValidationError(errors)
+ if annotate:
+ print(annotated_cloudconfig_file({}, content, error.schema_errors))
+ raise error
try:
validate_cloudconfig_schema(
cloudconfig, schema, strict=True)
@@ -176,7 +202,7 @@ def _schemapath_for_cloudconfig(config, original_content):
list_index = 0
RE_YAML_INDENT = r'^(\s*)'
scopes = []
- for line_number, line in enumerate(content_lines):
+ for line_number, line in enumerate(content_lines, 1):
indent_depth = len(re.match(RE_YAML_INDENT, line).groups()[0])
line = line.strip()
if not line or line.startswith('#'):
@@ -208,8 +234,8 @@ def _schemapath_for_cloudconfig(config, original_content):
scopes.append((indent_depth + 2, key + '.0'))
for inner_list_index in range(0, len(yaml.safe_load(value))):
list_key = key + '.' + str(inner_list_index)
- schema_line_numbers[list_key] = line_number + 1
- schema_line_numbers[key] = line_number + 1
+ schema_line_numbers[list_key] = line_number
+ schema_line_numbers[key] = line_number
return schema_line_numbers
@@ -297,8 +323,8 @@ def get_schema():
configs_dir = os.path.dirname(os.path.abspath(__file__))
potential_handlers = find_modules(configs_dir)
- for (fname, mod_name) in potential_handlers.items():
- mod_locs, looked_locs = importer.find_module(
+ for (_fname, mod_name) in potential_handlers.items():
+ mod_locs, _looked_locs = importer.find_module(
mod_name, ['cloudinit.config'], ['schema'])
if mod_locs:
mod = importer.import_module(mod_locs[0])
@@ -337,9 +363,11 @@ def handle_schema_args(name, args):
try:
validate_cloudconfig_file(
args.config_file, full_schema, args.annotate)
- except (SchemaValidationError, RuntimeError) as e:
+ except SchemaValidationError as e:
if not args.annotate:
error(str(e))
+ except RuntimeError as e:
+ error(str(e))
else:
print("Valid cloud-config file {0}".format(args.config_file))
if args.doc:
diff --git a/cloudinit/config/tests/test_disable_ec2_metadata.py b/cloudinit/config/tests/test_disable_ec2_metadata.py
new file mode 100644
index 00000000..67646b03
--- /dev/null
+++ b/cloudinit/config/tests/test_disable_ec2_metadata.py
@@ -0,0 +1,50 @@
+# This file is part of cloud-init. See LICENSE file for license information.
+
+"""Tests cc_disable_ec2_metadata handler"""
+
+import cloudinit.config.cc_disable_ec2_metadata as ec2_meta
+
+from cloudinit.tests.helpers import CiTestCase, mock
+
+import logging
+
+LOG = logging.getLogger(__name__)
+
+DISABLE_CFG = {'disable_ec2_metadata': 'true'}
+
+
+class TestEC2MetadataRoute(CiTestCase):
+
+ with_logs = True
+
+ @mock.patch('cloudinit.config.cc_disable_ec2_metadata.util.which')
+ @mock.patch('cloudinit.config.cc_disable_ec2_metadata.util.subp')
+ def test_disable_ifconfig(self, m_subp, m_which):
+ """Set the route if ifconfig command is available"""
+ m_which.side_effect = lambda x: x if x == 'ifconfig' else None
+ ec2_meta.handle('foo', DISABLE_CFG, None, LOG, None)
+ m_subp.assert_called_with(
+ ['route', 'add', '-host', '169.254.169.254', 'reject'],
+ capture=False)
+
+ @mock.patch('cloudinit.config.cc_disable_ec2_metadata.util.which')
+ @mock.patch('cloudinit.config.cc_disable_ec2_metadata.util.subp')
+ def test_disable_ip(self, m_subp, m_which):
+ """Set the route if ip command is available"""
+ m_which.side_effect = lambda x: x if x == 'ip' else None
+ ec2_meta.handle('foo', DISABLE_CFG, None, LOG, None)
+ m_subp.assert_called_with(
+ ['ip', 'route', 'add', 'prohibit', '169.254.169.254'],
+ capture=False)
+
+ @mock.patch('cloudinit.config.cc_disable_ec2_metadata.util.which')
+ @mock.patch('cloudinit.config.cc_disable_ec2_metadata.util.subp')
+ def test_disable_no_tool(self, m_subp, m_which):
+ """Log error when neither route nor ip commands are available"""
+ m_which.return_value = None # Find neither ifconfig nor ip
+ ec2_meta.handle('foo', DISABLE_CFG, None, LOG, None)
+ self.assertEqual(
+ [mock.call('ip'), mock.call('ifconfig')], m_which.call_args_list)
+ m_subp.assert_not_called()
+
+# vi: ts=4 expandtab
diff --git a/cloudinit/config/tests/test_set_passwords.py b/cloudinit/config/tests/test_set_passwords.py
new file mode 100644
index 00000000..b051ec82
--- /dev/null
+++ b/cloudinit/config/tests/test_set_passwords.py
@@ -0,0 +1,71 @@
+# This file is part of cloud-init. See LICENSE file for license information.
+
+import mock
+
+from cloudinit.config import cc_set_passwords as setpass
+from cloudinit.tests.helpers import CiTestCase
+from cloudinit import util
+
+MODPATH = "cloudinit.config.cc_set_passwords."
+
+
+class TestHandleSshPwauth(CiTestCase):
+ """Test cc_set_passwords handling of ssh_pwauth in handle_ssh_pwauth."""
+
+ with_logs = True
+
+ @mock.patch(MODPATH + "util.subp")
+ def test_unknown_value_logs_warning(self, m_subp):
+ setpass.handle_ssh_pwauth("floo")
+ self.assertIn("Unrecognized value: ssh_pwauth=floo",
+ self.logs.getvalue())
+ m_subp.assert_not_called()
+
+ @mock.patch(MODPATH + "update_ssh_config", return_value=True)
+ @mock.patch(MODPATH + "util.subp")
+ def test_systemctl_as_service_cmd(self, m_subp, m_update_ssh_config):
+ """If systemctl in service cmd: systemctl restart name."""
+ setpass.handle_ssh_pwauth(
+ True, service_cmd=["systemctl"], service_name="myssh")
+ self.assertEqual(mock.call(["systemctl", "restart", "myssh"]),
+ m_subp.call_args)
+
+ @mock.patch(MODPATH + "update_ssh_config", return_value=True)
+ @mock.patch(MODPATH + "util.subp")
+ def test_service_as_service_cmd(self, m_subp, m_update_ssh_config):
+ """If systemctl in service cmd: systemctl restart name."""
+ setpass.handle_ssh_pwauth(
+ True, service_cmd=["service"], service_name="myssh")
+ self.assertEqual(mock.call(["service", "myssh", "restart"]),
+ m_subp.call_args)
+
+ @mock.patch(MODPATH + "update_ssh_config", return_value=False)
+ @mock.patch(MODPATH + "util.subp")
+ def test_not_restarted_if_not_updated(self, m_subp, m_update_ssh_config):
+ """If config is not updated, then no system restart should be done."""
+ setpass.handle_ssh_pwauth(True)
+ m_subp.assert_not_called()
+ self.assertIn("No need to restart ssh", self.logs.getvalue())
+
+ @mock.patch(MODPATH + "update_ssh_config", return_value=True)
+ @mock.patch(MODPATH + "util.subp")
+ def test_unchanged_does_nothing(self, m_subp, m_update_ssh_config):
+ """If 'unchanged', then no updates to config and no restart."""
+ setpass.handle_ssh_pwauth(
+ "unchanged", service_cmd=["systemctl"], service_name="myssh")
+ m_update_ssh_config.assert_not_called()
+ m_subp.assert_not_called()
+
+ @mock.patch(MODPATH + "util.subp")
+ def test_valid_change_values(self, m_subp):
+ """If value is a valid changen value, then update should be called."""
+ upname = MODPATH + "update_ssh_config"
+ optname = "PasswordAuthentication"
+ for value in util.FALSE_STRINGS + util.TRUE_STRINGS:
+ optval = "yes" if value in util.TRUE_STRINGS else "no"
+ with mock.patch(upname, return_value=False) as m_update:
+ setpass.handle_ssh_pwauth(value)
+ m_update.assert_called_with({optname: optval})
+ m_subp.assert_not_called()
+
+# vi: ts=4 expandtab
diff --git a/cloudinit/config/tests/test_snap.py b/cloudinit/config/tests/test_snap.py
index c5b4a9de..34c80f1e 100644
--- a/cloudinit/config/tests/test_snap.py
+++ b/cloudinit/config/tests/test_snap.py
@@ -9,7 +9,7 @@ from cloudinit.config.cc_snap import (
from cloudinit.config.schema import validate_cloudconfig_schema
from cloudinit import util
from cloudinit.tests.helpers import (
- CiTestCase, mock, wrap_and_call, skipUnlessJsonSchema)
+ CiTestCase, SchemaTestCaseMixin, mock, wrap_and_call, skipUnlessJsonSchema)
SYSTEM_USER_ASSERTION = """\
@@ -245,9 +245,10 @@ class TestRunCommands(CiTestCase):
@skipUnlessJsonSchema()
-class TestSchema(CiTestCase):
+class TestSchema(CiTestCase, SchemaTestCaseMixin):
with_logs = True
+ schema = schema
def test_schema_warns_on_snap_not_as_dict(self):
"""If the snap configuration is not a dict, emit a warning."""
@@ -340,6 +341,30 @@ class TestSchema(CiTestCase):
{'snap': {'assertions': {'01': 'also valid'}}}, schema)
self.assertEqual('', self.logs.getvalue())
+ def test_duplicates_are_fine_array_array(self):
+ """Duplicated commands array/array entries are allowed."""
+ self.assertSchemaValid(
+ {'commands': [["echo", "bye"], ["echo" "bye"]]},
+ "command entries can be duplicate.")
+
+ def test_duplicates_are_fine_array_string(self):
+ """Duplicated commands array/string entries are allowed."""
+ self.assertSchemaValid(
+ {'commands': ["echo bye", "echo bye"]},
+ "command entries can be duplicate.")
+
+ def test_duplicates_are_fine_dict_array(self):
+ """Duplicated commands dict/array entries are allowed."""
+ self.assertSchemaValid(
+ {'commands': {'00': ["echo", "bye"], '01': ["echo", "bye"]}},
+ "command entries can be duplicate.")
+
+ def test_duplicates_are_fine_dict_string(self):
+ """Duplicated commands dict/string entries are allowed."""
+ self.assertSchemaValid(
+ {'commands': {'00': "echo bye", '01': "echo bye"}},
+ "command entries can be duplicate.")
+
class TestHandle(CiTestCase):
diff --git a/cloudinit/config/tests/test_ubuntu_advantage.py b/cloudinit/config/tests/test_ubuntu_advantage.py
index f2a59faf..f1beeff8 100644
--- a/cloudinit/config/tests/test_ubuntu_advantage.py
+++ b/cloudinit/config/tests/test_ubuntu_advantage.py
@@ -7,7 +7,8 @@ from cloudinit.config.cc_ubuntu_advantage import (
handle, maybe_install_ua_tools, run_commands, schema)
from cloudinit.config.schema import validate_cloudconfig_schema
from cloudinit import util
-from cloudinit.tests.helpers import CiTestCase, mock, skipUnlessJsonSchema
+from cloudinit.tests.helpers import (
+ CiTestCase, mock, SchemaTestCaseMixin, skipUnlessJsonSchema)
# Module path used in mocks
@@ -105,9 +106,10 @@ class TestRunCommands(CiTestCase):
@skipUnlessJsonSchema()
-class TestSchema(CiTestCase):
+class TestSchema(CiTestCase, SchemaTestCaseMixin):
with_logs = True
+ schema = schema
def test_schema_warns_on_ubuntu_advantage_not_as_dict(self):
"""If ubuntu-advantage configuration is not a dict, emit a warning."""
@@ -169,6 +171,30 @@ class TestSchema(CiTestCase):
{'ubuntu-advantage': {'commands': {'01': 'also valid'}}}, schema)
self.assertEqual('', self.logs.getvalue())
+ def test_duplicates_are_fine_array_array(self):
+ """Duplicated commands array/array entries are allowed."""
+ self.assertSchemaValid(
+ {'commands': [["echo", "bye"], ["echo" "bye"]]},
+ "command entries can be duplicate.")
+
+ def test_duplicates_are_fine_array_string(self):
+ """Duplicated commands array/string entries are allowed."""
+ self.assertSchemaValid(
+ {'commands': ["echo bye", "echo bye"]},
+ "command entries can be duplicate.")
+
+ def test_duplicates_are_fine_dict_array(self):
+ """Duplicated commands dict/array entries are allowed."""
+ self.assertSchemaValid(
+ {'commands': {'00': ["echo", "bye"], '01': ["echo", "bye"]}},
+ "command entries can be duplicate.")
+
+ def test_duplicates_are_fine_dict_string(self):
+ """Duplicated commands dict/string entries are allowed."""
+ self.assertSchemaValid(
+ {'commands': {'00': "echo bye", '01': "echo bye"}},
+ "command entries can be duplicate.")
+
class TestHandle(CiTestCase):