cloud-init-dev team mailing list archive
-
cloud-init-dev team
-
Mailing list archive
-
Message #06569
[Merge] ~raharper/cloud-init:feature/bootspeed-v2 into cloud-init:master
Ryan Harper has proposed merging ~raharper/cloud-init:feature/bootspeed-v2 into cloud-init:master.
Commit message:
changes to enable starting ssh service very early.
Requested reviews:
cloud-init commiters (cloud-init-dev)
For more details, see:
https://code.launchpad.net/~raharper/cloud-init/+git/cloud-init/+merge/371908
--
Your team cloud-init commiters is requested to review the proposed merge of ~raharper/cloud-init:feature/bootspeed-v2 into cloud-init:master.
diff --git a/cloudinit/analyze/show.py b/cloudinit/analyze/show.py
index 511b808..28e4941 100644
--- a/cloudinit/analyze/show.py
+++ b/cloudinit/analyze/show.py
@@ -101,6 +101,10 @@ def event_parent(event):
return None
+def event_is_stage(event):
+ return '/' not in event_name(event)
+
+
def event_timestamp(event):
return float(event.get('timestamp'))
@@ -319,7 +323,9 @@ def generate_records(events, blame_sort=False,
next_evt = None
if event_type(event) == 'start':
- if event.get('name') in stages_seen:
+ stage_name = event_parent(event)
+ if stage_name == event_name(event) and stage_name in stages_seen:
+ # new boot record
records.append(total_time_record(total_time))
boot_records.append(records)
records = []
@@ -339,19 +345,26 @@ def generate_records(events, blame_sort=False,
event,
next_evt)))
else:
- # This is a parent event
- records.append("Starting stage: %s" % event.get('name'))
- unprocessed.append(event)
- stages_seen.append(event.get('name'))
- continue
+ if event_is_stage(event):
+ records.append("Starting stage: %s" % event.get('name'))
+ unprocessed.append(event)
+ stages_seen.append(event.get('name'))
+ else:
+ # Start of a substage event
+ records.append(format_record(print_format,
+ event_record(start_time,
+ event,
+ next_evt)))
+
else:
prev_evt = unprocessed.pop()
if event_name(event) == event_name(prev_evt):
- record = event_record(start_time, prev_evt, event)
- records.append(format_record("Finished stage: "
- "(%n) %d seconds ",
- record) + "\n")
- total_time += record.get('delta')
+ if event_is_stage(event):
+ record = event_record(start_time, prev_evt, event)
+ records.append(format_record("Finished stage: "
+ "(%n) %d seconds ",
+ record) + "\n")
+ total_time += record.get('delta')
else:
# not a match, put it back
unprocessed.append(prev_evt)
diff --git a/cloudinit/cmd/main.py b/cloudinit/cmd/main.py
index a5446da..789ca6e 100644
--- a/cloudinit/cmd/main.py
+++ b/cloudinit/cmd/main.py
@@ -320,7 +320,7 @@ def main_init(name, args):
# Stage 5
try:
- init.fetch(existing=existing)
+ fetched_ds = init.fetch(existing=existing)
# if in network mode, and the datasource is local
# then work was done at that stage.
if mode == sources.DSMODE_NETWORK and init.datasource.dsmode != mode:
@@ -362,7 +362,12 @@ def main_init(name, args):
init.apply_network_config(bring_up=bool(mode != sources.DSMODE_LOCAL))
if mode == sources.DSMODE_LOCAL:
- if init.datasource.dsmode != mode:
+ if fetched_ds and hasattr(fetched_ds, 'network_config'):
+ # local mode with ds that returned data and has netcfg
+ # lets try to run some mods early \o/
+ LOG.debug("WARK: datasource %s in local mode but has "
+ " networking, trying to run mods.", init.datasource)
+ elif init.datasource.dsmode != mode:
LOG.debug("[%s] Exiting. datasource %s not in local mode.",
mode, init.datasource)
return (init.datasource, [])
diff --git a/cloudinit/config/cc_bootcmd.py b/cloudinit/config/cc_bootcmd.py
index 6813f53..362e43d 100644
--- a/cloudinit/config/cc_bootcmd.py
+++ b/cloudinit/config/cc_bootcmd.py
@@ -75,6 +75,8 @@ schema = {
__doc__ = get_schema_doc(schema) # Supplement python help()
+CC_KEYS = ['bootcmd']
+
def handle(name, cfg, cloud, log, _args):
diff --git a/cloudinit/config/cc_ca_certs.py b/cloudinit/config/cc_ca_certs.py
index 64bc900..3f4a578 100644
--- a/cloudinit/config/cc_ca_certs.py
+++ b/cloudinit/config/cc_ca_certs.py
@@ -45,6 +45,7 @@ CA_CERT_SYSTEM_PATH = "/etc/ssl/certs/"
CA_CERT_FULL_PATH = os.path.join(CA_CERT_PATH, CA_CERT_FILENAME)
distros = ['ubuntu', 'debian']
+CC_KEYS = ['ca-certs']
def update_ca_certs():
diff --git a/cloudinit/config/cc_disk_setup.py b/cloudinit/config/cc_disk_setup.py
index 29e192e..61490fd 100644
--- a/cloudinit/config/cc_disk_setup.py
+++ b/cloudinit/config/cc_disk_setup.py
@@ -116,6 +116,7 @@ WIPEFS_CMD = util.which("wipefs")
LANG_C_ENV = {'LANG': 'C'}
+CC_KEYS = ['device_aliases', 'disk_setup', 'fs_setup']
LOG = logging.getLogger(__name__)
diff --git a/cloudinit/config/cc_growpart.py b/cloudinit/config/cc_growpart.py
index 564f376..fbfb758 100644
--- a/cloudinit/config/cc_growpart.py
+++ b/cloudinit/config/cc_growpart.py
@@ -80,6 +80,8 @@ DEFAULT_CONFIG = {
'ignore_growroot_disabled': False,
}
+CC_KEYS = ['growpart']
+
class RESIZE(object):
SKIPPED = "SKIPPED"
diff --git a/cloudinit/config/cc_mcollective.py b/cloudinit/config/cc_mcollective.py
index d5f63f5..3af8a47 100644
--- a/cloudinit/config/cc_mcollective.py
+++ b/cloudinit/config/cc_mcollective.py
@@ -72,7 +72,7 @@ def configure(config, server_cfg=SERVER_CFG,
# Read server.cfg (if it exists) values from the
# original file in order to be able to mix the rest up.
try:
- old_contents = util.load_file(server_cfg, quiet=False, decode=False)
+ old_contents = util.load_file(server_cfg, strict=True, decode=False)
mcollective_config = ConfigObj(BytesIO(old_contents))
except IOError as e:
if e.errno != errno.ENOENT:
diff --git a/cloudinit/config/cc_migrator.py b/cloudinit/config/cc_migrator.py
index 3995704..e4134c2 100644
--- a/cloudinit/config/cc_migrator.py
+++ b/cloudinit/config/cc_migrator.py
@@ -34,6 +34,7 @@ from cloudinit import util
from cloudinit.settings import PER_ALWAYS
+CC_KEYS = ['migrate']
frequency = PER_ALWAYS
diff --git a/cloudinit/config/cc_mounts.py b/cloudinit/config/cc_mounts.py
index 123ffb8..d7e3570 100644
--- a/cloudinit/config/cc_mounts.py
+++ b/cloudinit/config/cc_mounts.py
@@ -71,6 +71,8 @@ import re
from cloudinit import type_utils
from cloudinit import util
+CC_KEYS = ['mounts', 'mount_default_fields', 'swap']
+
# Shortname matches 'sda', 'sda1', 'xvda', 'hda', 'sdb', xvdb, vda, vdd1, sr0
DEVICE_NAME_FILTER = r"^([x]{0,1}[shv]d[a-z][0-9]*|sr[0-9]+)$"
DEVICE_NAME_RE = re.compile(DEVICE_NAME_FILTER)
@@ -277,7 +279,7 @@ def handle_swapcfg(swapcfg):
if not (size and fname):
LOG.debug("no need to setup swap")
- return
+ return None
if os.path.exists(fname):
if not os.path.exists("/proc/swaps"):
@@ -319,7 +321,7 @@ def handle(_name, cfg, cloud, log, _args):
defvals = cfg.get("mount_default_fields", defvals)
# these are our default set of mounts
- defmnts = [["ephemeral0", "/mnt", "auto", defvals[3], "0", "2"],
+ defmnts = [["ephemeral0", "/mnt", defvals[2], defvals[3], "0", "2"],
["swap", "none", "swap", "sw", "0", "0"]]
cfgmnt = []
@@ -442,9 +444,11 @@ def handle(_name, cfg, cloud, log, _args):
need_mount_all = False
dirs = []
for line in actlist:
+ print(line)
# write 'comment' in the fs_mntops, entry, claiming this
line[3] = "%s,%s" % (line[3], MNT_COMMENT)
if line[2] == "swap":
+ print('WARK enabled swap')
needswap = True
if line[1].startswith("/"):
dirs.append(line[1])
@@ -461,6 +465,7 @@ def handle(_name, cfg, cloud, log, _args):
# If any of them does not already show up in the list of current
# mount points, we will definitely need to do mount -a.
if not need_mount_all and d not in mount_points:
+ print('enabled mount all: %s not in %s' % (d, mount_points))
need_mount_all = True
sadds = [WS.sub(" ", n) for n in cc_lines]
@@ -474,19 +479,18 @@ def handle(_name, cfg, cloud, log, _args):
util.write_file(FSTAB_PATH, contents)
activate_cmds = []
- if needswap:
- activate_cmds.append(["swapon", "-a"])
-
if len(sops) == 0:
log.debug("No changes to /etc/fstab made.")
else:
log.debug("Changes to fstab: %s", sops)
need_mount_all = True
+ if needswap:
+ activate_cmds.append(["swapon", "-a"])
if need_mount_all:
- activate_cmds.append(["mount", "-a"])
if uses_systemd:
activate_cmds.append(["systemctl", "daemon-reload"])
+ activate_cmds.append(["mount", "-a"])
fmt = "Activating swap and mounts with: %s"
for cmd in activate_cmds:
diff --git a/cloudinit/config/cc_resizefs.py b/cloudinit/config/cc_resizefs.py
index afd2e06..6aa1d5b 100644
--- a/cloudinit/config/cc_resizefs.py
+++ b/cloudinit/config/cc_resizefs.py
@@ -55,6 +55,8 @@ schema = {
}
}
+CC_KEYS = ['resize_rootfs']
+
__doc__ = get_schema_doc(schema) # Supplement python help()
diff --git a/cloudinit/config/cc_rsyslog.py b/cloudinit/config/cc_rsyslog.py
index 22b1753..91f7564 100644
--- a/cloudinit/config/cc_rsyslog.py
+++ b/cloudinit/config/cc_rsyslog.py
@@ -198,6 +198,7 @@ KEYNAME_LEGACY_FILENAME = 'rsyslog_filename'
KEYNAME_LEGACY_DIR = 'rsyslog_dir'
KEYNAME_REMOTES = 'remotes'
+CC_KEYS = ['rsyslog', 'rsyslog_dir', 'rsyslog_filename']
LOG = logging.getLogger(__name__)
COMMENT_RE = re.compile(r'[ ]*[#]+[ ]*')
diff --git a/cloudinit/config/cc_seed_random.py b/cloudinit/config/cc_seed_random.py
index 65f6e77..b462374 100644
--- a/cloudinit/config/cc_seed_random.py
+++ b/cloudinit/config/cc_seed_random.py
@@ -70,6 +70,7 @@ from cloudinit import util
frequency = PER_INSTANCE
LOG = logging.getLogger(__name__)
+CC_KEYS = ['random_seed']
def _decode(data, encoding=None):
diff --git a/cloudinit/config/cc_set_hostname.py b/cloudinit/config/cc_set_hostname.py
index 3d2b2da..8651a32 100644
--- a/cloudinit/config/cc_set_hostname.py
+++ b/cloudinit/config/cc_set_hostname.py
@@ -38,6 +38,8 @@ import os
from cloudinit.atomic_helper import write_json
from cloudinit import util
+CC_KEYS = ['preserve_hostname', 'fqdn', 'hostname']
+
class SetHostnameError(Exception):
"""Raised when the distro runs into an exception when setting hostname.
diff --git a/cloudinit/config/cc_ssh.py b/cloudinit/config/cc_ssh.py
index fdd8f4d..8251898 100755
--- a/cloudinit/config/cc_ssh.py
+++ b/cloudinit/config/cc_ssh.py
@@ -11,41 +11,8 @@ SSH
---
**Summary:** configure ssh and ssh keys
-This module handles most configuration for ssh and ssh keys. Many images have
-default ssh keys, which can be removed using ``ssh_deletekeys``. Since removing
-default keys is usually the desired behavior this option is enabled by default.
-
-Keys can be added using the ``ssh_keys`` configuration key. The argument to
-this config key should be a dictionary entries for the public and private keys
-of each desired key type. Entries in the ``ssh_keys`` config dict should
-have keys in the format ``<key type>_private`` and ``<key type>_public``, e.g.
-``rsa_private: <key>`` and ``rsa_public: <key>``. See below for supported key
-types. Not all key types have to be specified, ones left unspecified will not
-be used. If this config option is used, then no keys will be generated.
-
-.. note::
- when specifying private keys in cloud-config, care should be taken to
- ensure that the communication between the data source and the instance is
- secure
-
-.. note::
- to specify multiline private keys, use yaml multiline syntax
-
-If no keys are specified using ``ssh_keys``, then keys will be generated using
-``ssh-keygen``. By default one public/private pair of each supported key type
-will be generated. The key types to generate can be specified using the
-``ssh_genkeytypes`` config flag, which accepts a list of key types to use. For
-each key type for which this module has been instructed to create a keypair, if
-a key of the same type is already present on the system (i.e. if
-``ssh_deletekeys`` was false), no key will be generated.
-
-Supported key types for the ``ssh_keys`` and the ``ssh_genkeytypes`` config
-flags are:
-
- - rsa
- - dsa
- - ecdsa
- - ed25519
+This module handles publishing ssh host keys, disabling root login and
+importing user public keys into the default user.
Root login can be enabled/disabled using the ``disable_root`` config key. Root
login options can be manually specified with ``disable_root_opts``. If
@@ -59,6 +26,16 @@ Authorized keys for the default user/first user defined in ``users`` can be
specified using `ssh_authorized_keys``. Keys should be specified as a list of
public keys.
+On some platforms, the host ssh keys can be published to the platform to
+allow users to verify host ssh fingerprints when connecting to new instances.
+This operation is a no-op for platforms which do not support publishing
+host keys. The ``ssh_publish_hostkeys`` config controls the behavior.
+Publishing is enabled by default, to disable this, one may emit a config with
+``ssh_publish_hostkeys: {enabled: false}``. Users also may control which
+key types are published. To skip publishing a host key type, add the
+key type (rsa, dsa, etc) to the ``blacklist`` list under
+``ssh_publish_hostkeys`` config.
+
.. note::
see the ``cc_set_passwords`` module documentation to enable/disable ssh
password authentication
@@ -71,21 +48,6 @@ public keys.
**Config keys**::
- ssh_deletekeys: <true/false>
- ssh_keys:
- rsa_private: |
- -----BEGIN RSA PRIVATE KEY-----
- MIIBxwIBAAJhAKD0YSHy73nUgysO13XsJmd4fHiFyQ+00R7VVu2iV9Qco
- ...
- -----END RSA PRIVATE KEY-----
- rsa_public: ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAGEAoPRhIfLvedSDKw7Xd ...
- dsa_private: |
- -----BEGIN DSA PRIVATE KEY-----
- MIIBxwIBAAJhAKD0YSHy73nUgysO13XsJmd4fHiFyQ+00R7VVu2iV9Qco
- ...
- -----END DSA PRIVATE KEY-----
- dsa_public: ssh-dsa AAAAB3NzaC1yc2EAAAABIwAAAGEAoPRhIfLvedSDKw7Xd ...
- ssh_genkeytypes: <key type>
disable_root: <true/false>
disable_root_opts: <disable root options string>
ssh_authorized_keys:
@@ -95,94 +57,20 @@ public keys.
enabled: <true/false> (Defaults to true)
blacklist: <list of key types> (Defaults to [dsa])
"""
-
-import glob
-import os
-import sys
-
from cloudinit.distros import ug_util
from cloudinit import ssh_util
from cloudinit import util
-
-GENERATE_KEY_NAMES = ['rsa', 'dsa', 'ecdsa', 'ed25519']
-KEY_FILE_TPL = '/etc/ssh/ssh_host_%s_key'
+CC_KEYS = ['disable_root', 'disable_root_opts', 'ssh_authorized_keys',
+ 'ssh_publish_hostkeys']
PUBLISH_HOST_KEYS = True
# Don't publish the dsa hostkey by default since OpenSSH recommends not using
# it.
HOST_KEY_PUBLISH_BLACKLIST = ['dsa']
-CONFIG_KEY_TO_FILE = {}
-PRIV_TO_PUB = {}
-for k in GENERATE_KEY_NAMES:
- CONFIG_KEY_TO_FILE.update({"%s_private" % k: (KEY_FILE_TPL % k, 0o600)})
- CONFIG_KEY_TO_FILE.update(
- {"%s_public" % k: (KEY_FILE_TPL % k + ".pub", 0o600)})
- PRIV_TO_PUB["%s_private" % k] = "%s_public" % k
-
-KEY_GEN_TPL = 'o=$(ssh-keygen -yf "%s") && echo "$o" root@localhost > "%s"'
-
def handle(_name, cfg, cloud, log, _args):
- # remove the static keys from the pristine image
- if cfg.get("ssh_deletekeys", True):
- key_pth = os.path.join("/etc/ssh/", "ssh_host_*key*")
- for f in glob.glob(key_pth):
- try:
- util.del_file(f)
- except Exception:
- util.logexc(log, "Failed deleting key file %s", f)
-
- if "ssh_keys" in cfg:
- # if there are keys in cloud-config, use them
- for (key, val) in cfg["ssh_keys"].items():
- if key in CONFIG_KEY_TO_FILE:
- tgt_fn = CONFIG_KEY_TO_FILE[key][0]
- tgt_perms = CONFIG_KEY_TO_FILE[key][1]
- util.write_file(tgt_fn, val, tgt_perms)
-
- for (priv, pub) in PRIV_TO_PUB.items():
- if pub in cfg['ssh_keys'] or priv not in cfg['ssh_keys']:
- continue
- pair = (CONFIG_KEY_TO_FILE[priv][0], CONFIG_KEY_TO_FILE[pub][0])
- cmd = ['sh', '-xc', KEY_GEN_TPL % pair]
- try:
- # TODO(harlowja): Is this guard needed?
- with util.SeLinuxGuard("/etc/ssh", recursive=True):
- util.subp(cmd, capture=False)
- log.debug("Generated a key for %s from %s", pair[0], pair[1])
- except Exception:
- util.logexc(log, "Failed generated a key for %s from %s",
- pair[0], pair[1])
- else:
- # if not, generate them
- genkeys = util.get_cfg_option_list(cfg,
- 'ssh_genkeytypes',
- GENERATE_KEY_NAMES)
- lang_c = os.environ.copy()
- lang_c['LANG'] = 'C'
- for keytype in genkeys:
- keyfile = KEY_FILE_TPL % (keytype)
- if os.path.exists(keyfile):
- continue
- util.ensure_dir(os.path.dirname(keyfile))
- cmd = ['ssh-keygen', '-t', keytype, '-N', '', '-f', keyfile]
-
- # TODO(harlowja): Is this guard needed?
- with util.SeLinuxGuard("/etc/ssh", recursive=True):
- try:
- out, err = util.subp(cmd, capture=True, env=lang_c)
- sys.stdout.write(util.decode_binary(out))
- except util.ProcessExecutionError as e:
- err = util.decode_binary(e.stderr).lower()
- if (e.exit_code == 1 and
- err.lower().startswith("unknown key")):
- log.debug("ssh-keygen: unknown key type '%s'", keytype)
- else:
- util.logexc(log, "Failed generating key type %s to "
- "file %s", keytype, keyfile)
-
if "ssh_publish_hostkeys" in cfg:
host_key_blacklist = util.get_cfg_option_list(
cfg["ssh_publish_hostkeys"], "blacklist",
@@ -241,7 +129,8 @@ def get_public_host_keys(blacklist=None):
@returns: List of keys, each formatted as a two-element tuple.
e.g. [('ssh-rsa', 'AAAAB3Nz...'), ('ssh-ed25519', 'AAAAC3Nx...')]
"""
- public_key_file_tmpl = '%s.pub' % (KEY_FILE_TPL,)
+ import glob
+ public_key_file_tmpl = '%s.pub' % ('/etc/ssh/ssh_host_%s_key',)
key_list = []
blacklist_files = []
if blacklist:
diff --git a/cloudinit/config/cc_ssh_host_keys.py b/cloudinit/config/cc_ssh_host_keys.py
new file mode 100644
index 0000000..ea3b162
--- /dev/null
+++ b/cloudinit/config/cc_ssh_host_keys.py
@@ -0,0 +1,189 @@
+# This file is part of cloud-init. See LICENSE file for license information.
+
+"""
+SSH Host Keys
+---
+**Summary:** configure ssh host key generation
+
+This module handles removing and generating host ssh keys. Many images have
+ssh keys, which can be removed using ``ssh_deletekeys``. Since removing default
+keys is usually the desired behavior this option is enabled by default.
+
+Keys can be added using the ``ssh_keys`` configuration key. The argument to
+this config key should be a dictionary entries for the public and private keys
+of each desired key type. Entries in the ``ssh_keys`` config dict should
+have keys in the format ``<key type>_private`` and ``<key type>_public``, e.g.
+``rsa_private: <key>`` and ``rsa_public: <key>``. See below for supported key
+types. Not all key types have to be specified, ones left unspecified will not
+be used. If this config option is used, then no keys will be generated.
+
+.. note::
+ when specifying private keys in cloud-config, care should be taken to
+ ensure that the communication between the data source and the instance is
+ secure
+
+.. note::
+ to specify multiline private keys, use yaml multiline syntax
+
+If no keys are specified using ``ssh_keys``, then keys will be generated using
+``ssh-keygen``. By default one public/private pair of each supported key type
+will be generated. The key types to generate can be specified using the
+``ssh_genkeytypes`` config flag, which accepts a list of key types to use. For
+each key type for which this module has been instructed to create a keypair, if
+a key of the same type is already present on the system (i.e. if
+``ssh_deletekeys`` was false), no key will be generated.
+
+Supported key types for the ``ssh_keys`` and the ``ssh_genkeytypes`` config
+flags are:
+
+ - rsa
+ - dsa
+ - ecdsa
+ - ed25519
+
+The ``ssh_early_start`` boolean if enabled will activate ssh and additional
+login services as soon as possible. This feature allows users to ssh into
+an instances much sooner than without. This can be disabled by setting
+``ssh_early_start`` to false.
+
+
+**Internal name:** ``cc_ssh_host_keys``
+
+**Module frequency:** per instance
+
+**Supported distros:** all
+
+**Config keys**::
+
+ ssh_keys:
+ rsa_private: |
+ -----BEGIN RSA PRIVATE KEY-----
+ MIIBxwIBAAJhAKD0YSHy73nUgysO13XsJmd4fHiFyQ+00R7VVu2iV9Qco
+ ...
+ -----END RSA PRIVATE KEY-----
+ rsa_public: ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAGEAoPRhIfLvedSDKw7Xd ...
+ dsa_private: |
+ -----BEGIN DSA PRIVATE KEY-----
+ MIIBxwIBAAJhAKD0YSHy73nUgysO13XsJmd4fHiFyQ+00R7VVu2iV9Qco
+ ...
+ -----END DSA PRIVATE KEY-----
+ dsa_public: ssh-dsa AAAAB3NzaC1yc2EAAAABIwAAAGEAoPRhIfLvedSDKw7Xd ...
+ ssh_genkeytypes: <key type>
+ ssh_early_start: <bool>
+
+"""
+
+import glob
+import os
+import sys
+
+from cloudinit import log as logging
+from cloudinit import util
+
+LOG = logging.getLogger(__name__)
+CC_KEYS = ['ssh_keys', 'ssh_genkeytypes', 'ssh_early_start']
+
+GENERATE_KEY_NAMES = ['rsa', 'dsa', 'ecdsa', 'ed25519']
+KEY_FILE_TPL = '/etc/ssh/ssh_host_%s_key'
+CONFIG_KEY_TO_FILE = {}
+PRIV_TO_PUB = {}
+for k in GENERATE_KEY_NAMES:
+ CONFIG_KEY_TO_FILE.update({"%s_private" % k: (KEY_FILE_TPL % k, 0o600)})
+ CONFIG_KEY_TO_FILE.update(
+ {"%s_public" % k: (KEY_FILE_TPL % k + ".pub", 0o600)})
+ PRIV_TO_PUB["%s_private" % k] = "%s_public" % k
+
+KEY_GEN_TPL = 'o=$(ssh-keygen -yf "%s") && echo "$o" root@localhost > "%s"'
+
+
+def handle(_name, cfg, cloud, log, _args):
+
+ # remove the static keys from the pristine image if told to
+ if cfg.get("ssh_deletekeys", True):
+ key_pth = os.path.join("/etc/ssh/", "ssh_host_*key*")
+ for f in glob.glob(key_pth):
+ try:
+ util.del_file(f)
+ except Exception:
+ util.logexc(log, "Failed deleting key file %s", f)
+
+ util.ensure_dir(os.path.dirname(KEY_FILE_TPL))
+ updated_keys = False
+ if "ssh_keys" in cfg:
+ # if there are keys in cloud-config, use them
+ for (key, val) in cfg["ssh_keys"].items():
+ if key in CONFIG_KEY_TO_FILE:
+ tgt_fn = CONFIG_KEY_TO_FILE[key][0]
+ tgt_perms = CONFIG_KEY_TO_FILE[key][1]
+ util.write_file(tgt_fn, val, tgt_perms)
+ updated_keys = False
+
+ for (priv, pub) in PRIV_TO_PUB.items():
+ if pub in cfg['ssh_keys'] or priv not in cfg['ssh_keys']:
+ continue
+ pair = (CONFIG_KEY_TO_FILE[priv][0], CONFIG_KEY_TO_FILE[pub][0])
+ cmd = ['sh', '-xc', KEY_GEN_TPL % pair]
+ try:
+ # TODO(harlowja): Is this guard needed?
+ with util.SeLinuxGuard("/etc/ssh", recursive=True):
+ util.subp(cmd, capture=False)
+ log.debug("Generated a key for %s from %s", pair[0], pair[1])
+ except Exception:
+ util.logexc(log, "Failed generated a key for %s from %s",
+ pair[0], pair[1])
+
+ else:
+ # if not, generate them
+ genkeys = util.get_cfg_option_list(cfg,
+ 'ssh_genkeytypes',
+ GENERATE_KEY_NAMES)
+ lang_c = os.environ.copy()
+ lang_c['LANG'] = 'C'
+ for keytype in genkeys:
+ keyfile = KEY_FILE_TPL % (keytype)
+ if os.path.exists(keyfile):
+ continue
+ cmd = ['ssh-keygen', '-t', keytype, '-N', '', '-f', keyfile]
+ # TODO(harlowja): Is this guard needed?
+ with util.SeLinuxGuard("/etc/ssh", recursive=True):
+ try:
+ out, err = util.subp(cmd, capture=True, env=lang_c)
+ sys.stdout.write(util.decode_binary(out))
+ updated_keys = True
+ except util.ProcessExecutionError as e:
+ err = util.decode_binary(e.stderr).lower()
+ if (e.exit_code == 1 and
+ err.lower().startswith("unknown key")):
+ LOG.debug("ssh-keygen: unknown key type '%s'", keytype)
+ else:
+ util.logexc(LOG, "Failed generating key type %s to "
+ "file %s", keytype, keyfile)
+
+ # SRU Blocker, this shouldn't be enabled by default
+ if cfg.get("ssh_early_start", True):
+ ssh_services = ['ssh', 'systemd-user-sessions', 'systemd-logind']
+
+ # check if ssh has started yet and start ssh if we generated keys
+ # or keys exist but ssh isn't yet running.
+ out, _err = util.subp(['systemctl', 'show', 'ssh.service',
+ '-p', 'ActiveState,SubState'], capture=True)
+ LOG.debug('WARK: ssh.service status: %s', out)
+ if 'ActiveState=active' not in out:
+ LOG.debug('WARK: starting up early ssh access')
+ for service in ssh_services:
+ cmd = ['systemctl', '--job-mode=ignore-dependencies',
+ '--no-block', 'start', '%s.service' % service]
+ util.subp(cmd, capture=True)
+
+ # we may have started ssh early with older host keys, so reload/restart
+ # if this is the case.
+ elif 'running' in out and updated_keys:
+ LOG.debug('WARK: reloading early ssh service')
+ for service in ssh_services:
+ cmd = ['systemctl', '--job-mode=ignore-dependencies',
+ '--no-block', 'reload-or-restart',
+ '%s.service' % service]
+ util.subp(cmd, capture=True)
+
+
+# vi: ts=4 expandtab
diff --git a/cloudinit/config/cc_update_etc_hosts.py b/cloudinit/config/cc_update_etc_hosts.py
index c96eede..13c3a24 100644
--- a/cloudinit/config/cc_update_etc_hosts.py
+++ b/cloudinit/config/cc_update_etc_hosts.py
@@ -54,6 +54,7 @@ from cloudinit import util
from cloudinit.settings import PER_ALWAYS
+CC_KEYS = ['manage_etc_hosts', 'fqdn', 'hostname']
frequency = PER_ALWAYS
diff --git a/cloudinit/config/cc_update_hostname.py b/cloudinit/config/cc_update_hostname.py
index d5f4eb5..a088714 100644
--- a/cloudinit/config/cc_update_hostname.py
+++ b/cloudinit/config/cc_update_hostname.py
@@ -37,6 +37,7 @@ from cloudinit.settings import PER_ALWAYS
from cloudinit import util
frequency = PER_ALWAYS
+CC_KEYS = ['preserve_hostname', 'fqdn', 'hostname']
def handle(name, cfg, cloud, log, _args):
diff --git a/cloudinit/config/cc_users_groups.py b/cloudinit/config/cc_users_groups.py
index c32a743..c4cc398 100644
--- a/cloudinit/config/cc_users_groups.py
+++ b/cloudinit/config/cc_users_groups.py
@@ -128,6 +128,7 @@ from cloudinit import log as logging
from cloudinit.settings import PER_INSTANCE
+CC_KEYS = ['groups', 'users']
LOG = logging.getLogger(__name__)
frequency = PER_INSTANCE
diff --git a/cloudinit/config/cc_write_files.py b/cloudinit/config/cc_write_files.py
index 0b6546e..8d109ac 100644
--- a/cloudinit/config/cc_write_files.py
+++ b/cloudinit/config/cc_write_files.py
@@ -64,6 +64,7 @@ from cloudinit.settings import PER_INSTANCE
from cloudinit import util
+CC_KEYS = ['write_files']
frequency = PER_INSTANCE
DEFAULT_OWNER = "root:root"
diff --git a/cloudinit/config/tests/test_ssh.py b/cloudinit/config/tests/test_ssh.py
index e778984..5767034 100644
--- a/cloudinit/config/tests/test_ssh.py
+++ b/cloudinit/config/tests/test_ssh.py
@@ -75,8 +75,7 @@ class TestHandleSsh(CiTestCase):
@mock.patch(MODPATH + "glob.glob")
@mock.patch(MODPATH + "ug_util.normalize_users_groups")
@mock.patch(MODPATH + "os.path.exists")
- def test_handle_no_cfg(self, m_path_exists, m_nug,
- m_glob, m_setup_keys):
+ def handle_no_cfg(self, m_path_exists, m_nug, m_glob, m_setup_keys):
"""Test handle with no config ignores generating existing keyfiles."""
cfg = {}
keys = ["key1"]
@@ -103,8 +102,8 @@ class TestHandleSsh(CiTestCase):
@mock.patch(MODPATH + "glob.glob")
@mock.patch(MODPATH + "ug_util.normalize_users_groups")
@mock.patch(MODPATH + "os.path.exists")
- def test_handle_no_cfg_and_default_root(self, m_path_exists, m_nug,
- m_glob, m_setup_keys):
+ def handle_no_cfg_and_default_root(self, m_path_exists, m_nug,
+ m_glob, m_setup_keys):
"""Test handle with no config and a default distro user."""
cfg = {}
keys = ["key1"]
@@ -126,8 +125,8 @@ class TestHandleSsh(CiTestCase):
@mock.patch(MODPATH + "glob.glob")
@mock.patch(MODPATH + "ug_util.normalize_users_groups")
@mock.patch(MODPATH + "os.path.exists")
- def test_handle_cfg_with_explicit_disable_root(self, m_path_exists, m_nug,
- m_glob, m_setup_keys):
+ def handle_cfg_with_explicit_disable_root(self, m_path_exists, m_nug,
+ m_glob, m_setup_keys):
"""Test handle with explicit disable_root and a default distro user."""
# This test is identical to test_handle_no_cfg_and_default_root,
# except this uses an explicit cfg value
@@ -151,8 +150,8 @@ class TestHandleSsh(CiTestCase):
@mock.patch(MODPATH + "glob.glob")
@mock.patch(MODPATH + "ug_util.normalize_users_groups")
@mock.patch(MODPATH + "os.path.exists")
- def test_handle_cfg_without_disable_root(self, m_path_exists, m_nug,
- m_glob, m_setup_keys):
+ def handle_cfg_without_disable_root(self, m_path_exists, m_nug,
+ m_glob, m_setup_keys):
"""Test handle with disable_root == False."""
# When disable_root == False, the ssh redirect for root is skipped
cfg = {"disable_root": False}
@@ -174,7 +173,7 @@ class TestHandleSsh(CiTestCase):
@mock.patch(MODPATH + "glob.glob")
@mock.patch(MODPATH + "ug_util.normalize_users_groups")
@mock.patch(MODPATH + "os.path.exists")
- def test_handle_publish_hostkeys_default(
+ def handle_publish_hostkeys_default(
self, m_path_exists, m_nug, m_glob, m_setup_keys):
"""Test handle with various configs for ssh_publish_hostkeys."""
self._publish_hostkey_test_setup()
@@ -203,7 +202,7 @@ class TestHandleSsh(CiTestCase):
@mock.patch(MODPATH + "glob.glob")
@mock.patch(MODPATH + "ug_util.normalize_users_groups")
@mock.patch(MODPATH + "os.path.exists")
- def test_handle_publish_hostkeys_config_enable(
+ def handle_publish_hostkeys_config_enable(
self, m_path_exists, m_nug, m_glob, m_setup_keys):
"""Test handle with various configs for ssh_publish_hostkeys."""
self._publish_hostkey_test_setup()
@@ -232,7 +231,7 @@ class TestHandleSsh(CiTestCase):
@mock.patch(MODPATH + "glob.glob")
@mock.patch(MODPATH + "ug_util.normalize_users_groups")
@mock.patch(MODPATH + "os.path.exists")
- def test_handle_publish_hostkeys_config_disable(
+ def handle_publish_hostkeys_config_disable(
self, m_path_exists, m_nug, m_glob, m_setup_keys):
"""Test handle with various configs for ssh_publish_hostkeys."""
self._publish_hostkey_test_setup()
@@ -259,7 +258,7 @@ class TestHandleSsh(CiTestCase):
@mock.patch(MODPATH + "glob.glob")
@mock.patch(MODPATH + "ug_util.normalize_users_groups")
@mock.patch(MODPATH + "os.path.exists")
- def test_handle_publish_hostkeys_config_blacklist(
+ def handle_publish_hostkeys_config_blacklist(
self, m_path_exists, m_nug, m_glob, m_setup_keys):
"""Test handle with various configs for ssh_publish_hostkeys."""
self._publish_hostkey_test_setup()
@@ -289,7 +288,7 @@ class TestHandleSsh(CiTestCase):
@mock.patch(MODPATH + "glob.glob")
@mock.patch(MODPATH + "ug_util.normalize_users_groups")
@mock.patch(MODPATH + "os.path.exists")
- def test_handle_publish_hostkeys_empty_blacklist(
+ def handle_publish_hostkeys_empty_blacklist(
self, m_path_exists, m_nug, m_glob, m_setup_keys):
"""Test handle with various configs for ssh_publish_hostkeys."""
self._publish_hostkey_test_setup()
diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py
index 00bdee3..1b9829f 100644
--- a/cloudinit/distros/__init__.py
+++ b/cloudinit/distros/__init__.py
@@ -15,7 +15,6 @@ from six import StringIO
import abc
import os
import re
-import stat
from cloudinit import importer
from cloudinit import log as logging
@@ -796,11 +795,6 @@ def set_etc_timezone(tz, tz_file=None, tz_conf="/etc/timezone",
def uses_systemd():
- try:
- res = os.lstat('/run/systemd/system')
- return stat.S_ISDIR(res.st_mode)
- except Exception:
- return False
-
+ return util.uses_systemd()
# vi: ts=4 expandtab
diff --git a/cloudinit/net/__init__.py b/cloudinit/net/__init__.py
index ea707c0..8e2a1c5 100644
--- a/cloudinit/net/__init__.py
+++ b/cloudinit/net/__init__.py
@@ -9,6 +9,7 @@ import errno
import logging
import os
import re
+import time
from functools import partial
from cloudinit.net.network_state import mask_to_net_prefix
@@ -97,6 +98,21 @@ def is_up(devname):
return read_sys_net_safe(devname, "operstate", translate=translate)
+def networking_is_active():
+ if util.uses_systemd():
+ try:
+ out, _err = util.subp(['systemctl', 'show', '-p', 'ActiveState',
+ 'systemd-networkd.service'], capture=True)
+ if out:
+ show = util.load_shell_content(out)
+ if show['ActiveState'] == "active":
+ return True
+ except Exception:
+ pass
+
+ return False
+
+
def is_wireless(devname):
return os.path.exists(sys_dev_path(devname, "wireless"))
@@ -333,6 +349,58 @@ def extract_physdevs(netcfg):
raise RuntimeError('Unknown network config version: %s' % version)
+def wait_online(devices, ignore=None, mode='any', timeout=10.0):
+ """Wait for up to timeout seconds and collect 'is_up' result for each
+ device. If mode is 'any' return True if one or more is up. If mode is
+ 'all' only return True if all devices are up.
+
+ :param: devices: list of network device names, e.g. ens3
+ :param: mode: string describing exit conditions. 'all' requires
+ every device to be up to return success. 'any' will return success
+ if one or more devices are up. Defaults to 'any'.
+ :param: timeout: float: Number of seconds to wait for success.
+ :returns: tuple: (boolean, list of last results, list of devices)
+ :raises: ValueError if mode is not valid, or if timeout is not castable
+ to type float.
+ """
+ if not devices:
+ devices = get_devicelist()
+
+ if ignore is None:
+ ignore = ['lo']
+
+ if mode not in ['any', 'all']:
+ raise ValueError("'mode' must be one of 'all' or 'any': %s" % mode)
+
+ if not isinstance(timeout, float):
+ timeout = float(timeout)
+
+ devs = [dev for dev in devices if dev not in ignore]
+ msg = ('waiting up to %s seconds for %s of %s '
+ 'to be up, ignoring %s' % (timeout, mode, devices, ignore))
+
+ return util.log_time(LOG.debug, msg, _wait_timeout,
+ [is_up, devs, mode, timeout])
+
+
+def _wait_timeout(check_func, data, mode, timeout):
+ start = time.time()
+ while True:
+ results = list(map(check_func, data))
+ if mode == 'any':
+ if any(results):
+ return (True, results, data)
+ if mode == 'all':
+ if all(results):
+ return (True, results, data)
+ time.sleep(0.1)
+ if (time.time() - start) >= timeout:
+ print('Timed out')
+ break
+
+ return (False, results, data)
+
+
def wait_for_physdevs(netcfg, strict=True):
physdevs = extract_physdevs(netcfg)
diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py
index 4984fa8..fd8f546 100755
--- a/cloudinit/sources/DataSourceAzure.py
+++ b/cloudinit/sources/DataSourceAzure.py
@@ -230,6 +230,9 @@ BUILTIN_DS_CONFIG = {
# RELEASE_BLOCKER: Xenial and earlier apply_network_config default is False
BUILTIN_CLOUD_CONFIG = {
+ 'dynamic_module_order': True,
+ 'ssh_early_start': True,
+ 'resize_rootfs': 'noblock',
'disk_setup': {
'ephemeral0': {'table_type': 'gpt',
'layout': [100],
@@ -1257,7 +1260,7 @@ def _get_random_seed(source=PLATFORM_ENTROPY_SOURCE):
# now update ds_cfg to reflect contents pass in config
if source is None:
return None
- seed = util.load_file(source, quiet=True, decode=False)
+ seed = util.load_file(source, strict=False, decode=False)
# The seed generally contains non-Unicode characters. load_file puts
# them into a str (in python 2) or bytes (in python 3). In python 2,
@@ -1381,7 +1384,9 @@ def get_metadata_from_imds(fallback_nic, retries):
kwargs = {'logfunc': LOG.debug,
'msg': 'Crawl of Azure Instance Metadata Service (IMDS)',
'func': _get_metadata_from_imds, 'args': (retries,)}
- if net.is_up(fallback_nic):
+ status = net.wait_online([])
+ if status[0]:
+ LOG.debug('Network is already up, skipping EphemeralDHCP')
return util.log_time(**kwargs)
else:
try:
diff --git a/cloudinit/sources/DataSourceOpenStack.py b/cloudinit/sources/DataSourceOpenStack.py
index 4a01524..672679b 100644
--- a/cloudinit/sources/DataSourceOpenStack.py
+++ b/cloudinit/sources/DataSourceOpenStack.py
@@ -7,6 +7,7 @@
import time
from cloudinit import log as logging
+from cloudinit import net
from cloudinit.net.dhcp import EphemeralDHCPv4, NoDHCPLeaseError
from cloudinit import sources
from cloudinit import url_helper
@@ -127,14 +128,20 @@ class DataSourceOpenStack(openstack.SourceMixin, sources.DataSource):
return False
if self.perform_dhcp_setup: # Setup networking in init-local stage.
- try:
- with EphemeralDHCPv4(self.fallback_interface):
- results = util.log_time(
- logfunc=LOG.debug, msg='Crawl of metadata service',
- func=self._crawl_metadata)
- except (NoDHCPLeaseError, sources.InvalidMetaDataException) as e:
- util.logexc(LOG, str(e))
- return False
+ kwargs = {'logfunc': LOG.debug, 'msg': 'Crawl of metadata service',
+ 'func': self._crawl_metadata}
+ results = None
+ status = net.wait_online([])
+ if status[0]:
+ LOG.debug('Network is already up, skipping EphemeralDHCP')
+ results = util.log_time(**kwargs)
+ if not results:
+ try:
+ with EphemeralDHCPv4(self.fallback_interface):
+ results = util.log_time(**kwargs)
+ except (NoDHCPLeaseError, sources.InvalidMetaDataException):
+ util.logexc(LOG, "Error during EphemeralDHCP imds crawl")
+ return False
else:
try:
results = self._crawl_metadata()
diff --git a/cloudinit/stages.py b/cloudinit/stages.py
index 5012988..bdde0ed 100644
--- a/cloudinit/stages.py
+++ b/cloudinit/stages.py
@@ -817,7 +817,9 @@ class Modules(object):
# and which ones failed + the exception of why it failed
failures = []
which_ran = []
- for (mod, name, freq, args) in mostly_mods:
+ # see if we can reoder these for faster ssh
+ ordered_mods = _earlyssh_module_order(mostly_mods, self.cfg)
+ for (mod, name, freq, args) in ordered_mods:
try:
# Try the modules frequency, otherwise fallback to a known one
if not freq:
@@ -956,4 +958,81 @@ def _pkl_load(fname):
util.logexc(LOG, "Failed loading pickled blob from %s", fname)
return None
+
+def _earlyssh_module_order(modules, cfg):
+ """ This function will reorder the sequence in which
+ modules are run with the goal of enabling ssh
+ as fast as possible without breaking existing behavior.
+
+ For any module that would run before ssh_host_keys,
+ users-groups, and ssh has config, it will continue
+ to run before these modules, except if the module
+ a) doesn't have any config b) doesn't affect /home
+ """
+ # handle empty
+ if not modules:
+ return modules
+
+ earlyssh_order = [
+ 'migrator', 'seed_random', 'ssh_host_keys', 'users-groups', 'ssh',
+ 'bootcmd', 'write-files', 'growpart', 'resizefs', 'disk_setup',
+ 'mounts', 'set_hostname', 'update_hostname', 'update_etc_hosts',
+ 'ca-certs', 'rsyslog']
+
+ # first mod tuple, name element
+ if modules[0][1] != "migrator":
+ LOG.debug('WARK: returning o.g. order, not init_modules')
+ return modules
+
+ name_to_mod = {name: (mod, name, freq, args)
+ for (mod, name, freq, args) in modules}
+ cfg_set = set(cfg.keys())
+
+ # check if mounts has /home, if so keep original order
+ mods_with_cfg = []
+ for (mod, name, _freq, _args) in modules:
+ if hasattr(mod, 'CC_KEYS'):
+ mod_cc_keys = mod.CC_KEYS
+ else:
+ LOG.debug('WARK: module %s has no CC_KEYS attr', name)
+ mod_cc_keys = []
+
+ has_cfg = list(set(mod_cc_keys).intersection(cfg_set))
+ if len(has_cfg) > 0:
+ LOG.debug('WARK: %s module has cloud-config to consume', name)
+ mods_with_cfg.append(name)
+
+ # we always ignore disk_setup as it doesn't mean we have to
+ # keep original order
+ ignored = ['migrator', 'seed_random', 'ssh_host_keys', 'users-groups',
+ 'ssh', 'growpart', 'resizefs', 'disk_setup',
+ 'set_hostname', 'update_hostname', 'update_etc_hosts',
+ 'ca-certs', 'rsyslog']
+ LOG.debug('WARK: checking if we can ignore mods w/cfg: %s', mods_with_cfg)
+ for mod_name in mods_with_cfg:
+ # we can possible ignore these if the mounts do not
+ # include /home.
+ if mod_name in ['mounts']:
+ can_ignore = True
+ for mnt in cfg['mounts']:
+ if '/home' in mnt[1]:
+ can_ignore = False
+ break
+ if can_ignore:
+ ignored.append('mounts')
+
+ # excluding ignores, any modules with config?
+ LOG.debug('WARK: mods with cfg: %s ignoring %s', mods_with_cfg, ignored)
+ delta = set(mods_with_cfg).difference(ignored)
+ if len(delta) == 0:
+ LOG.debug('WARK: enabling earlyssh mod order')
+ early = [name_to_mod[name] for name in earlyssh_order
+ if name in name_to_mod]
+ LOG.debug('WARK: early order-> %s', early)
+ return early
+
+ LOG.debug('WARK: Keeping o.g order, delta=%s', len(delta))
+ return modules
+
+
# vi: ts=4 expandtab
diff --git a/cloudinit/tests/helpers.py b/cloudinit/tests/helpers.py
index 23fddd0..65e82a3 100644
--- a/cloudinit/tests/helpers.py
+++ b/cloudinit/tests/helpers.py
@@ -6,7 +6,9 @@ import functools
import httpretty
import logging
import os
+import random
import shutil
+import string
import sys
import tempfile
import time
@@ -122,6 +124,12 @@ class TestCase(unittest2.TestCase):
parser.readfp(contents)
return parser
+ @classmethod
+ def random_string(cls, length=8):
+ """ return a random lowercase string with default length of 8"""
+ return ''.join(
+ random.choice(string.ascii_lowercase) for _ in range(length))
+
class CiTestCase(TestCase):
"""This is the preferred test case base class unless user
diff --git a/cloudinit/util.py b/cloudinit/util.py
index aa23b3f..2050a79 100644
--- a/cloudinit/util.py
+++ b/cloudinit/util.py
@@ -1352,19 +1352,21 @@ def uniq_list(in_list):
return out_list
-def load_file(fname, read_cb=None, quiet=False, decode=True):
- LOG.debug("Reading from %s (quiet=%s)", fname, quiet)
+def load_file(fname, read_cb=None, verbose=False, strict=True, decode=True):
+ if verbose:
+ LOG.debug("Reading from %s (strict=%s)", fname, strict)
ofh = six.BytesIO()
try:
with open(fname, 'rb') as ifh:
pipe_in_out(ifh, ofh, chunk_cb=read_cb)
except IOError as e:
- if not quiet:
+ if strict:
raise
if e.errno != ENOENT:
raise
contents = ofh.getvalue()
- LOG.debug("Read %s bytes from %s", len(contents), fname)
+ if verbose:
+ LOG.debug("Read %s bytes from %s", len(contents), fname)
if decode:
return decode_binary(contents)
else:
@@ -2565,7 +2567,7 @@ def pathprefix2dict(base, required=None, optional=None, delim=os.path.sep):
ret = {}
for f in required + optional:
try:
- ret[f] = load_file(base + delim + f, quiet=False, decode=False)
+ ret[f] = load_file(base + delim + f, strict=True, decode=False)
except IOError as e:
if e.errno != ENOENT:
raise
@@ -2759,7 +2761,7 @@ def system_is_snappy():
# this is certainly not a perfect test, but good enough for now.
orpath = "/etc/os-release"
try:
- orinfo = load_shell_content(load_file(orpath, quiet=True))
+ orinfo = load_shell_content(load_file(orpath, strict=False))
if orinfo.get('ID', '').lower() == "ubuntu-core":
return True
except ValueError as e:
@@ -2769,7 +2771,7 @@ def system_is_snappy():
if 'snap_core=' in cmdline:
return True
- content = load_file("/etc/system-image/channel.ini", quiet=True)
+ content = load_file("/etc/system-image/channel.ini", strict=False)
if 'ubuntu-core' in content.lower():
return True
if os.path.isdir("/etc/system-image/config.d/"):
@@ -2884,7 +2886,7 @@ def get_proc_ppid(pid):
"""
ppid = 0
try:
- contents = load_file("/proc/%s/stat" % pid, quiet=True)
+ contents = load_file("/proc/%s/stat" % pid, strict=False)
except IOError as e:
LOG.warning('Failed to load /proc/%s/stat. %s', pid, e)
if contents:
@@ -2894,4 +2896,13 @@ def get_proc_ppid(pid):
ppid = int(parts[3])
return ppid
+
+def uses_systemd():
+ try:
+ res = os.lstat('/run/systemd/system')
+ return stat.S_ISDIR(res.st_mode)
+ except Exception:
+ return False
+
+
# vi: ts=4 expandtab
diff --git a/config/cloud.cfg.tmpl b/config/cloud.cfg.tmpl
index 684c747..63c0064 100644
--- a/config/cloud.cfg.tmpl
+++ b/config/cloud.cfg.tmpl
@@ -61,6 +61,7 @@ cloud_init_modules:
- ca-certs
- rsyslog
{% endif %}
+ - ssh_host_keys
- users-groups
- ssh
diff --git a/setup.py b/setup.py
index fcaf26f..9cae049 100755
--- a/setup.py
+++ b/setup.py
@@ -250,6 +250,7 @@ data_files = [
(ETC + '/cloud/templates', glob('templates/*')),
(USR_LIB_EXEC + '/cloud-init', ['tools/ds-identify',
'tools/uncloud-init',
+ 'tools/ds_customizer',
'tools/write-ssh-key-fingerprints']),
(USR + '/share/bash-completion/completions',
['bash_completion/cloud-init']),
diff --git a/systemd/cloud-init-generator.tmpl b/systemd/cloud-init-generator.tmpl
index 45efa24..d72a412 100755
--- a/systemd/cloud-init-generator.tmpl
+++ b/systemd/cloud-init-generator.tmpl
@@ -105,6 +105,22 @@ check_for_datasource() {
return 1
}
+datasource_customizer() {
+ dsfound="/run/cloud-init/dsfound"
+ if [ ! -e "${dsfound}" ]; then
+ debug 1 "datasource_customier not enabled, ${dsfound} not present."
+ return 0
+ fi
+ for ds in $(ls -1 /run/cloud-init/dsfound); do
+ customizer="/usr/lib/cloud-init/ds_customizer"
+ if [ -e "${customizer}" ]; then
+ debug 1 "Running datsource_customizer $ds"
+ ${customizer} $ds
+ fi
+ done
+ return 0
+}
+
main() {
local normal_d="$1" early_d="$2" late_d="$3"
local target_name="multi-user.target" gen_d="$early_d"
@@ -154,6 +170,8 @@ main() {
fi
fi
: > "$RUN_ENABLED_FILE"
+ debug 1 "emitting datasource customization"
+ datasource_customizer
elif [ "$result" = "$DISABLE" ]; then
if [ -f "$link_path" ]; then
if rm -f "$link_path"; then
diff --git a/systemd/cloud-init-local.service.tmpl b/systemd/cloud-init-local.service.tmpl
index ff9c644..9946954 100644
--- a/systemd/cloud-init-local.service.tmpl
+++ b/systemd/cloud-init-local.service.tmpl
@@ -17,6 +17,7 @@ RequiresMountsFor=/var/lib/cloud
[Service]
Type=oneshot
+ExecStartPre=/bin/echo "Starting cloud-init-local"
ExecStart=/usr/bin/cloud-init init --local
ExecStart=/bin/touch /run/cloud-init/network-config-ready
RemainAfterExit=yes
diff --git a/tests/unittests/test_datasource/test_azure.py b/tests/unittests/test_datasource/test_azure.py
index 3547dd9..dd85c85 100644
--- a/tests/unittests/test_datasource/test_azure.py
+++ b/tests/unittests/test_datasource/test_azure.py
@@ -163,18 +163,18 @@ class TestGetMetadataFromIMDS(HttprettyTestCase):
@mock.patch(MOCKPATH + 'readurl')
@mock.patch(MOCKPATH + 'EphemeralDHCPv4')
- @mock.patch(MOCKPATH + 'net.is_up')
+ @mock.patch(MOCKPATH + 'net.wait_online')
def test_get_metadata_does_not_dhcp_if_network_is_up(
self, m_net_is_up, m_dhcp, m_readurl):
"""Do not perform DHCP setup when nic is already up."""
- m_net_is_up.return_value = True
+ m_net_is_up.return_value = (True,)
m_readurl.return_value = url_helper.StringResponse(
json.dumps(NETWORK_METADATA).encode('utf-8'))
self.assertEqual(
NETWORK_METADATA,
dsaz.get_metadata_from_imds('eth9', retries=3))
- m_net_is_up.assert_called_with('eth9')
+ m_net_is_up.assert_called_with([])
m_dhcp.assert_not_called()
self.assertIn(
"Crawl of Azure Instance Metadata Service (IMDS) took", # log_time
@@ -182,11 +182,11 @@ class TestGetMetadataFromIMDS(HttprettyTestCase):
@mock.patch(MOCKPATH + 'readurl')
@mock.patch(MOCKPATH + 'EphemeralDHCPv4WithReporting')
- @mock.patch(MOCKPATH + 'net.is_up')
+ @mock.patch(MOCKPATH + 'net.wait_online')
def test_get_metadata_performs_dhcp_when_network_is_down(
self, m_net_is_up, m_dhcp, m_readurl):
"""Perform DHCP setup when nic is not up."""
- m_net_is_up.return_value = False
+ m_net_is_up.return_value = (False,)
m_readurl.return_value = url_helper.StringResponse(
json.dumps(NETWORK_METADATA).encode('utf-8'))
@@ -194,7 +194,7 @@ class TestGetMetadataFromIMDS(HttprettyTestCase):
NETWORK_METADATA,
dsaz.get_metadata_from_imds('eth9', retries=2))
- m_net_is_up.assert_called_with('eth9')
+ m_net_is_up.assert_called_with([])
m_dhcp.assert_called_with(mock.ANY, 'eth9')
self.assertIn(
"Crawl of Azure Instance Metadata Service (IMDS) took", # log_time
@@ -206,7 +206,7 @@ class TestGetMetadataFromIMDS(HttprettyTestCase):
timeout=dsaz.IMDS_TIMEOUT_IN_SECONDS)
@mock.patch('cloudinit.url_helper.time.sleep')
- @mock.patch(MOCKPATH + 'net.is_up')
+ @mock.patch(MOCKPATH + 'net.wait_online')
def test_get_metadata_from_imds_empty_when_no_imds_present(
self, m_net_is_up, m_sleep):
"""Return empty dict when IMDS network metadata is absent."""
@@ -215,11 +215,11 @@ class TestGetMetadataFromIMDS(HttprettyTestCase):
dsaz.IMDS_URL + 'instance?api-version=2017-12-01',
body={}, status=404)
- m_net_is_up.return_value = True # skips dhcp
+ m_net_is_up.return_value = (True,) # skips dhcp
self.assertEqual({}, dsaz.get_metadata_from_imds('eth9', retries=2))
- m_net_is_up.assert_called_with('eth9')
+ m_net_is_up.assert_called_with([])
self.assertEqual([mock.call(1), mock.call(1)], m_sleep.call_args_list)
self.assertIn(
"Crawl of Azure Instance Metadata Service (IMDS) took", # log_time
@@ -227,7 +227,7 @@ class TestGetMetadataFromIMDS(HttprettyTestCase):
@mock.patch('requests.Session.request')
@mock.patch('cloudinit.url_helper.time.sleep')
- @mock.patch(MOCKPATH + 'net.is_up')
+ @mock.patch(MOCKPATH + 'net.wait_online')
def test_get_metadata_from_imds_retries_on_timeout(
self, m_net_is_up, m_sleep, m_request):
"""Retry IMDS network metadata on timeout errors."""
@@ -244,11 +244,11 @@ class TestGetMetadataFromIMDS(HttprettyTestCase):
dsaz.IMDS_URL + 'instance?api-version=2017-12-01',
body=retry_callback)
- m_net_is_up.return_value = True # skips dhcp
+ m_net_is_up.return_value = (True,) # skips dhcp
self.assertEqual({}, dsaz.get_metadata_from_imds('eth9', retries=3))
- m_net_is_up.assert_called_with('eth9')
+ m_net_is_up.assert_called_with([])
self.assertEqual([mock.call(1)]*3, m_sleep.call_args_list)
self.assertIn(
"Crawl of Azure Instance Metadata Service (IMDS) took", # log_time
diff --git a/tests/unittests/test_datasource/test_openstack.py b/tests/unittests/test_datasource/test_openstack.py
index a731f1e..796e374 100644
--- a/tests/unittests/test_datasource/test_openstack.py
+++ b/tests/unittests/test_datasource/test_openstack.py
@@ -251,9 +251,10 @@ class TestOpenStackDataSource(test_helpers.HttprettyTestCase):
m_dhcp.assert_not_called()
@hp.activate
+ @test_helpers.mock.patch('cloudinit.net.wait_online')
@test_helpers.mock.patch('cloudinit.net.dhcp.EphemeralIPv4Network')
@test_helpers.mock.patch('cloudinit.net.dhcp.maybe_perform_dhcp_discovery')
- def test_local_datasource(self, m_dhcp, m_net):
+ def test_local_datasource(self, m_dhcp, m_net, m_wait_on):
"""OpenStackLocal calls EphemeralDHCPNetwork and gets instance data."""
_register_uris(self.VERSION, EC2_FILES, EC2_META, OS_FILES)
ds_os_local = ds.DataSourceOpenStackLocal(
@@ -263,6 +264,7 @@ class TestOpenStackDataSource(test_helpers.HttprettyTestCase):
'interface': 'eth9', 'fixed-address': '192.168.2.9',
'routers': '192.168.2.1', 'subnet-mask': '255.255.255.0',
'broadcast-address': '192.168.2.255'}]
+ m_wait_on.return_value = (False,)
self.assertIsNone(ds_os_local.version)
mock_path = MOCK_PATH + 'detect_openstack'
@@ -282,6 +284,36 @@ class TestOpenStackDataSource(test_helpers.HttprettyTestCase):
self.assertIsNone(ds_os_local.vendordata_raw)
m_dhcp.assert_called_with('eth9')
+ @hp.activate
+ @test_helpers.mock.patch('cloudinit.net.wait_online')
+ @test_helpers.mock.patch('cloudinit.net.dhcp.EphemeralIPv4Network')
+ @test_helpers.mock.patch('cloudinit.net.dhcp.maybe_perform_dhcp_discovery')
+ def test_local_datasource_net_up(self, m_dhcp, m_net, m_wait_on):
+ """OpenStackLocal skips dhcp when net.is_up and gets instance data."""
+ _register_uris(self.VERSION, EC2_FILES, EC2_META, OS_FILES)
+ ds_os_local = ds.DataSourceOpenStackLocal(
+ settings.CFG_BUILTIN, None, helpers.Paths({'run_dir': self.tmp}))
+ ds_os_local._fallback_interface = 'eth9' # Monkey patch for dhcp
+ m_wait_on.return_value = (True,)
+
+ self.assertIsNone(ds_os_local.version)
+ mock_path = MOCK_PATH + 'detect_openstack'
+ with test_helpers.mock.patch(mock_path) as m_detect_os:
+ m_detect_os.return_value = True
+ found = ds_os_local.get_data()
+ self.assertTrue(found)
+ self.assertEqual(2, ds_os_local.version)
+ md = dict(ds_os_local.metadata)
+ md.pop('instance-id', None)
+ md.pop('local-hostname', None)
+ self.assertEqual(OSTACK_META, md)
+ self.assertEqual(EC2_META, ds_os_local.ec2_metadata)
+ self.assertEqual(USER_DATA, ds_os_local.userdata_raw)
+ self.assertEqual(2, len(ds_os_local.files))
+ self.assertEqual(VENDOR_DATA, ds_os_local.vendordata_pure)
+ self.assertIsNone(ds_os_local.vendordata_raw)
+ self.assertEqual(0, m_dhcp.call_count)
+
def test_bad_datasource_meta(self):
os_files = copy.deepcopy(OS_FILES)
for k in list(os_files.keys()):
diff --git a/tests/unittests/test_handler/test_handler_mounts.py b/tests/unittests/test_handler/test_handler_mounts.py
index 0fb160b..93b360d 100644
--- a/tests/unittests/test_handler/test_handler_mounts.py
+++ b/tests/unittests/test_handler/test_handler_mounts.py
@@ -255,7 +255,8 @@ class TestFstabHandling(test_helpers.FilesystemMockingTestCase):
self.assertEqual(fstab_expected_content, fstab_new_content)
cc_mounts.handle(None, cc, self.mock_cloud, self.mock_log, [])
self.m_util_subp.assert_has_calls([
+ mock.call(['systemctl', 'daemon-reload']),
mock.call(['mount', '-a']),
- mock.call(['systemctl', 'daemon-reload'])])
+ ])
# vi: ts=4 expandtab
diff --git a/tests/unittests/test_runs/test_simple_run.py b/tests/unittests/test_runs/test_simple_run.py
index d67c422..8d6061d 100644
--- a/tests/unittests/test_runs/test_simple_run.py
+++ b/tests/unittests/test_runs/test_simple_run.py
@@ -1,6 +1,7 @@
# This file is part of cloud-init. See LICENSE file for license information.
import copy
+import mock
import os
@@ -178,4 +179,88 @@ class TestSimpleRun(helpers.FilesystemMockingTestCase):
self.assertTrue(len(failures) == 0)
self.assertEqual([], which_ran)
+
+class TestEarlySshRun(helpers.FilesystemMockingTestCase):
+
+ with_logs = True
+ maxDiff = None
+
+ def setUp(self):
+ super(TestEarlySshRun, self).setUp()
+ self.new_root = self.tmp_dir()
+ self.replicateTestRoot('simple_ubuntu', self.new_root)
+ self.cfg = {
+ 'datasource_list': ['None'],
+ 'system_info': {'paths': {'run_dir': self.new_root}},
+ 'bootcmd': ['ls /etc'], # test ALL_DISTROS
+ 'write_files': [
+ {
+ 'path': '/etc/blah.ini',
+ 'content': 'blah',
+ 'permissions': 0o755,
+ },
+ ],
+ 'cloud_init_modules': ['migrator', 'seed_random', 'bootcmd',
+ 'write-files', 'growpart', 'users-groups',
+ 'ssh', 'ssh_host_keys'],
+ }
+ cloud_cfg = util.yaml_dumps(self.cfg)
+ util.ensure_dir(os.path.join(self.new_root, 'etc', 'cloud'))
+ util.write_file(os.path.join(self.new_root, 'etc',
+ 'cloud', 'cloud.cfg'), cloud_cfg)
+ self.patchOS(self.new_root)
+ self.patchUtils(self.new_root)
+
+ @mock.patch('cloudinit.config.cc_bootcmd.handle')
+ def test_modules_keep_original_order_if_cfg_present(self, m_bh):
+ """Allow module reordering if possible. """
+ initer = stages.Init()
+ initer.read_cfg()
+ initer.initialize()
+ initer.fetch()
+ initer.instancify()
+ initer.update()
+ initer.cloudify().run('consume_data', initer.consume_data,
+ args=[PER_INSTANCE], freq=PER_INSTANCE)
+
+ mods = stages.Modules(initer)
+ (which_ran, failures) = mods.run_section('cloud_init_modules')
+ print(failures)
+ print(self.logs.getvalue())
+ self.assertTrue(len(failures) == 0)
+ self.assertEqual(self.cfg['cloud_init_modules'], which_ran)
+
+ def test_modules_reoder_for_early_ssh(self):
+ """Allow module reordering if possible. """
+ # re-write cloud.cfg with different cloud config
+ cfg = copy.deepcopy(self.cfg)
+ del cfg['bootcmd']
+ del cfg['write_files']
+ cfg['ssh_early_start'] = True
+ cloud_cfg = util.yaml_dumps(cfg)
+ util.ensure_dir(os.path.join(self.new_root, 'etc', 'cloud'))
+ util.write_file(os.path.join(self.new_root, 'etc',
+ 'cloud', 'cloud.cfg'), cloud_cfg)
+
+ initer = stages.Init()
+ initer.read_cfg()
+ initer.initialize()
+ initer.fetch()
+ initer.instancify()
+ initer.update()
+ initer.cloudify().run('consume_data', initer.consume_data,
+ args=[PER_INSTANCE], freq=PER_INSTANCE)
+
+ mods = stages.Modules(initer)
+ (which_ran, failures) = mods.run_section('cloud_init_modules')
+ # early ssh pushes some modules up
+ expected_order = [
+ 'migrator', 'seed_random', 'ssh_host_keys', 'users-groups',
+ 'ssh', 'bootcmd', 'write-files', 'growpart']
+ print(failures)
+ print(self.logs.getvalue())
+ self.assertTrue(len(failures) == 0)
+ self.assertEqual(expected_order, which_ran)
+
+
# vi: ts=4 expandtab
diff --git a/tools/ds-identify b/tools/ds-identify
index e0d4865..d5784e5 100755
--- a/tools/ds-identify
+++ b/tools/ds-identify
@@ -1224,11 +1224,18 @@ record_notfound() {
found() {
# found(ds1, [ds2 ...], [-- [extra lines]])
local list="" ds=""
+ local dsmarker="${PATH_RUN_CI}/dsfound"
+ if [ "$DI_MODE" != "report" ]; then
+ mkdir -p "${dsmarker}"
+ fi
while [ $# -ne 0 ]; do
if [ "$1" = "--" ]; then
shift
break
fi
+ if [ "$DI_MODE" != "report" ]; then
+ touch "${dsmarker}/$1"
+ fi
list="${list:+${list}, }$1"
shift
done
diff --git a/tools/ds_customizer b/tools/ds_customizer
new file mode 100755
index 0000000..2c85a94
--- /dev/null
+++ b/tools/ds_customizer
@@ -0,0 +1,93 @@
+#!/bin/sh
+
+early_networking() {
+# dhcp on all ethernets netplan style
+ if command -v netplan; then
+ CI_NETPLAN="/run/netplan/10-cloud-init-dhcp.yaml"
+ mkdir -p /run/netplan
+ cat >"${CI_NETPLAN}" << EOF
+# This file is generated when cloud-init is enable to bring up networking
+# as fast as possible. Cloud-init will render the complete instance
+# network configuration later in boot.
+network:
+ ethernets:
+ en_all:
+ dhcp4: true
+ match:
+ name: e*
+ optional: true
+ version: 2
+EOF
+ netplan generate
+
+ # start networkd early, before cloud-init-local
+ SDNET_CONFD="/run/systemd/generator.early/systemd-networkd.service.d"
+ SDNET_EARLY_NETWORKD="${SDNET_CONFD}/override.conf"
+ mkdir -p $SDNET_CONFD
+ cat >"${SDNET_EARLY_NETWORKD}" << EOF
+# generated by cloud-init-generator - ds_customizer
+# NOTE: this does not work, BUG lp:#####
+[Unit]
+DefaultDependencies=no
+After=
+After=systemd-udevd.service local-fs.target
+Before=
+Before=network-pre.target
+Wants=
+Wants=network-pre.target
+[Service]
+ExecStartPre=/bin/echo "WARK: Starting networkd"
+EOF
+ # modify vendor file with our edited version into generator.early
+ LIB_NETD="/lib/systemd/system/systemd-networkd.service"
+ RUN_NETD="/run/systemd/generator.early/systemd-networkd.service"
+ sed -e 's,After=.*,After=systemd-udevd.service,g' \
+ -e 's,Before=.*,Before=network-pre.target multi-user.target shutdown.target,g' \
+ -e 's,Wants=.*,Wants=network-pre.target,g' ${LIB_NETD} > ${RUN_NETD}
+
+ # Start ssh early, after networkd and right before cloud-init-local
+ CI_CONFD="/run/systemd/generator.early/cloud-init-local.service.d"
+ mkdir -p $CI_CONFD
+ CI_EARLY_NETWORKD="${CI_CONFD}/10-start-networkd.conf"
+ cat >"${CI_EARLY_NETWORKD}" << EOF
+# generated by cloud-init-generator - ds_customizer
+[Unit]
+After=systemd-networkd.service
+EOF
+
+ CI_EARLY_SSH="${CI_CONFD}/20-start-ssh.conf"
+ cat >"${CI_EARLY_SSH}" << EOF
+# generated by cloud-init-generator - ds_customizer
+[Service]
+ExecStartPre=/bin/echo "20-start-ssh.conf"
+ExecStartPre=/bin/sh -c '[ -n "\$(ls /etc/ssh/ssh_host*)" ] && for svc in ssh.service systemd-user-sessions.service systemd-logind.service; do systemctl --job-mode=ignore-dependencies --no-block start \$svc; done; exit 0;'
+EOF
+ fi
+
+ # dhcp on all the e* nics for that old school cool with eni
+ if command -v ifup; then
+ if [ -d /etc/network/interfaces.d ]; then
+ CI_ENI="/etc/network/10-cloud-init-dhcp.cfg"
+ cat >"${CI_ENI}" << EOF
+# This file is generated when cloud-init is enable to bring up networking
+# as fast as possible. Cloud-init will render the complete instance
+# network configuration later in boot.
+auto lo
+iface lo inet loopback
+
+EOF
+ for iface in $(ls -1 /sys/class/net/ | grep ^e); do
+ echo -e "auto $iface\niface $iface inet dhcp\n" >> $CI_ENI
+ done
+ fi
+ fi
+}
+
+DS=${1};
+case $DS in
+ Azure|NoCloud|OpenStack|ConfigDrive)
+ early_networking
+ ;;
+ *) echo "No customizations for Datasource $DS";;
+esac
+exit 0
References