diff options
Diffstat (limited to 'cloudinit/config')
-rw-r--r-- | cloudinit/config/cc_apt_configure.py | 30 | ||||
-rw-r--r-- | cloudinit/config/cc_disk_setup.py | 790 | ||||
-rw-r--r-- | cloudinit/config/cc_final_message.py | 2 | ||||
-rw-r--r-- | cloudinit/config/cc_growpart.py | 7 | ||||
-rw-r--r-- | cloudinit/config/cc_mounts.py | 131 | ||||
-rw-r--r-- | cloudinit/config/cc_power_state_change.py | 6 | ||||
-rw-r--r-- | cloudinit/config/cc_seed_random.py | 61 | ||||
-rw-r--r-- | cloudinit/config/cc_ssh_authkey_fingerprints.py | 2 |
8 files changed, 983 insertions, 46 deletions
diff --git a/cloudinit/config/cc_apt_configure.py b/cloudinit/config/cc_apt_configure.py index 5a407016..29c13a3d 100644 --- a/cloudinit/config/cc_apt_configure.py +++ b/cloudinit/config/cc_apt_configure.py @@ -20,6 +20,7 @@ import glob import os +import re from cloudinit import templater from cloudinit import util @@ -30,6 +31,9 @@ PROXY_TPL = "Acquire::HTTP::Proxy \"%s\";\n" APT_CONFIG_FN = "/etc/apt/apt.conf.d/94cloud-init-config" APT_PROXY_FN = "/etc/apt/apt.conf.d/95cloud-init-proxy" +# this will match 'XXX:YYY' (ie, 'cloud-archive:foo' or 'ppa:bar') +ADD_APT_REPO_MATCH = r"^[\w-]+:\w" + # A temporary shell program to get a given gpg key # from a given keyserver EXPORT_GPG_KEYID = """ @@ -78,7 +82,15 @@ def handle(name, cfg, cloud, log, _args): params = mirrors params['RELEASE'] = release params['MIRROR'] = mirror - errors = add_sources(cfg['apt_sources'], params) + + matchcfg = cfg.get('add_apt_repo_match', ADD_APT_REPO_MATCH) + if matchcfg: + matcher = re.compile(matchcfg).search + else: + matcher = lambda f: False + + errors = add_sources(cfg['apt_sources'], params, + aa_repo_match=matcher) for e in errors: log.warn("Add source error: %s", ':'.join(e)) @@ -147,7 +159,7 @@ def generate_sources_list(codename, mirrors, cloud, log): templater.render_to_file(template_fn, '/etc/apt/sources.list', params) -def add_sources(srclist, template_params=None): +def add_sources(srclist, template_params=None, aa_repo_match=None): """ add entries in /etc/apt/sources.list.d for each abbreviated sources.list entry in 'srclist'. When rendering template, also @@ -156,6 +168,9 @@ def add_sources(srclist, template_params=None): if template_params is None: template_params = {} + if aa_repo_match is None: + aa_repo_match = lambda f: False + errorlist = [] for ent in srclist: if 'source' not in ent: @@ -163,15 +178,16 @@ def add_sources(srclist, template_params=None): continue source = ent['source'] - if source.startswith("ppa:"): + source = templater.render_string(source, template_params) + + if aa_repo_match(source): try: util.subp(["add-apt-repository", source]) - except: - errorlist.append([source, "add-apt-repository failed"]) + except util.ProcessExecutionError as e: + errorlist.append([source, + ("add-apt-repository failed. " + str(e))]) continue - source = templater.render_string(source, template_params) - if 'filename' not in ent: ent['filename'] = 'cloud_config_sources.list' diff --git a/cloudinit/config/cc_disk_setup.py b/cloudinit/config/cc_disk_setup.py new file mode 100644 index 00000000..0b970e4e --- /dev/null +++ b/cloudinit/config/cc_disk_setup.py @@ -0,0 +1,790 @@ +# vi: ts=4 expandtab +# +# Copyright (C) 2009-2010 Canonical Ltd. +# Copyright (C) 2012 Hewlett-Packard Development Company, L.P. +# +# Author: Ben Howard <ben.howard@canonical.com> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3, as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +from cloudinit.settings import PER_INSTANCE +from cloudinit import util +import logging +import os +import shlex + +frequency = PER_INSTANCE + +# Define the commands to use +UDEVADM_CMD = util.which('udevadm') +SFDISK_CMD = util.which("sfdisk") +LSBLK_CMD = util.which("lsblk") +BLKID_CMD = util.which("blkid") +BLKDEV_CMD = util.which("blockdev") +WIPEFS_CMD = util.which("wipefs") + +LOG = logging.getLogger(__name__) + + +def handle(_name, cfg, cloud, log, _args): + """ + See doc/examples/cloud-config_disk-setup.txt for documentation on the + format. + """ + disk_setup = cfg.get("disk_setup") + if isinstance(disk_setup, dict): + update_disk_setup_devices(disk_setup, cloud.device_name_to_device) + log.debug("Partitioning disks: %s", str(disk_setup)) + for disk, definition in disk_setup.items(): + if not isinstance(definition, dict): + log.warn("Invalid disk definition for %s" % disk) + continue + + try: + log.debug("Creating new partition table/disk") + util.log_time(logfunc=LOG.debug, + msg="Creating partition on %s" % disk, + func=mkpart, args=(disk, definition)) + except Exception as e: + util.logexc(LOG, "Failed partitioning operation\n%s" % e) + + fs_setup = cfg.get("fs_setup") + if isinstance(fs_setup, list): + log.debug("setting up filesystems: %s", str(fs_setup)) + update_fs_setup_devices(fs_setup, cloud.device_name_to_device) + for definition in fs_setup: + if not isinstance(definition, dict): + log.warn("Invalid file system definition: %s" % definition) + continue + + try: + log.debug("Creating new filesystem.") + device = definition.get('device') + util.log_time(logfunc=LOG.debug, + msg="Creating fs for %s" % device, + func=mkfs, args=(definition,)) + except Exception as e: + util.logexc(LOG, "Failed during filesystem operation\n%s" % e) + + +def update_disk_setup_devices(disk_setup, tformer): + # update 'disk_setup' dictionary anywhere were a device may occur + # update it with the response from 'tformer' + for origname in disk_setup.keys(): + transformed = tformer(origname) + if transformed is None or transformed == origname: + continue + if transformed in disk_setup: + LOG.info("Replacing %s in disk_setup for translation of %s", + origname, transformed) + del disk_setup[transformed] + + disk_setup[transformed] = disk_setup[origname] + disk_setup[transformed]['_origname'] = origname + del disk_setup[origname] + LOG.debug("updated disk_setup device entry '%s' to '%s'", + origname, transformed) + + +def update_fs_setup_devices(disk_setup, tformer): + # update 'fs_setup' dictionary anywhere were a device may occur + # update it with the response from 'tformer' + for definition in disk_setup: + if not isinstance(definition, dict): + LOG.warn("entry in disk_setup not a dict: %s", definition) + continue + + origname = definition.get('device') + + if origname is None: + continue + + (dev, part) = util.expand_dotted_devname(origname) + + tformed = tformer(dev) + if tformed is not None: + dev = tformed + LOG.debug("%s is mapped to disk=%s part=%s", + origname, tformed, part) + definition['_origname'] = origname + definition['device'] = tformed + + if part and 'partition' in definition: + definition['_partition'] = definition['partition'] + definition['partition'] = part + + +def value_splitter(values, start=None): + """ + Returns the key/value pairs of output sent as string + like: FOO='BAR' HOME='127.0.0.1' + """ + _values = shlex.split(values) + if start: + _values = _values[start:] + + for key, value in [x.split('=') for x in _values]: + yield key, value + + +def enumerate_disk(device, nodeps=False): + """ + Enumerate the elements of a child device. + + Parameters: + device: the kernel device name + nodeps <BOOL>: don't enumerate children devices + + Return a dict describing the disk: + type: the entry type, i.e disk or part + fstype: the filesystem type, if it exists + label: file system label, if it exists + name: the device name, i.e. sda + """ + + lsblk_cmd = [LSBLK_CMD, '--pairs', '--out', 'NAME,TYPE,FSTYPE,LABEL', + device] + + if nodeps: + lsblk_cmd.append('--nodeps') + + info = None + try: + info, _err = util.subp(lsblk_cmd) + except Exception as e: + raise Exception("Failed during disk check for %s\n%s" % (device, e)) + + parts = [x for x in (info.strip()).splitlines() if len(x.split()) > 0] + + for part in parts: + d = {'name': None, + 'type': None, + 'fstype': None, + 'label': None, + } + + for key, value in value_splitter(part): + d[key.lower()] = value + + yield d + + +def device_type(device): + """ + Return the device type of the device by calling lsblk. + """ + + for d in enumerate_disk(device, nodeps=True): + if "type" in d: + return d["type"].lower() + return None + + +def is_device_valid(name, partition=False): + """ + Check if the device is a valid device. + """ + d_type = "" + try: + d_type = device_type(name) + except: + LOG.warn("Query against device %s failed" % name) + return False + + if partition and d_type == 'part': + return True + elif not partition and d_type == 'disk': + return True + return False + + +def check_fs(device): + """ + Check if the device has a filesystem on it + + Output of blkid is generally something like: + /dev/sda: LABEL="Backup500G" UUID="..." TYPE="ext4" + + Return values are device, label, type, uuid + """ + out, label, fs_type, uuid = None, None, None, None + + blkid_cmd = [BLKID_CMD, '-c', '/dev/null', device] + try: + out, _err = util.subp(blkid_cmd, rcs=[0, 2]) + except Exception as e: + raise Exception("Failed during disk check for %s\n%s" % (device, e)) + + if out: + if len(out.splitlines()) == 1: + for key, value in value_splitter(out, start=1): + if key.lower() == 'label': + label = value + elif key.lower() == 'type': + fs_type = value + elif key.lower() == 'uuid': + uuid = value + + return label, fs_type, uuid + + +def is_filesystem(device): + """ + Returns true if the device has a file system. + """ + _, fs_type, _ = check_fs(device) + return fs_type + + +def find_device_node(device, fs_type=None, label=None, valid_targets=None, + label_match=True, replace_fs=None): + """ + Find a device that is either matches the spec, or the first + + The return is value is (<device>, <bool>) where the device is the + device to use and the bool is whether the device matches the + fs_type and label. + + Note: This works with GPT partition tables! + """ + # label of None is same as no label + if label is None: + label = "" + + if not valid_targets: + valid_targets = ['disk', 'part'] + + raw_device_used = False + for d in enumerate_disk(device): + + if d['fstype'] == replace_fs and label_match is False: + # We found a device where we want to replace the FS + return ('/dev/%s' % d['name'], False) + + if (d['fstype'] == fs_type and + ((label_match and d['label'] == label) or not label_match)): + # If we find a matching device, we return that + return ('/dev/%s' % d['name'], True) + + if d['type'] in valid_targets: + + if d['type'] != 'disk' or d['fstype']: + raw_device_used = True + + if d['type'] == 'disk': + # Skip the raw disk, its the default + pass + + elif not d['fstype']: + return ('/dev/%s' % d['name'], False) + + if not raw_device_used: + return (device, False) + + LOG.warn("Failed to find device during available device search.") + return (None, False) + + +def is_disk_used(device): + """ + Check if the device is currently used. Returns true if the device + has either a file system or a partition entry + is no filesystem found on the disk. + """ + + # If the child count is higher 1, then there are child nodes + # such as partition or device mapper nodes + use_count = [x for x in enumerate_disk(device)] + if len(use_count.splitlines()) > 1: + return True + + # If we see a file system, then its used + _, check_fstype, _ = check_fs(device) + if check_fstype: + return True + + return False + + +def get_hdd_size(device): + """ + Returns the hard disk size. + This works with any disk type, including GPT. + """ + + size_cmd = [SFDISK_CMD, '--show-size', device] + size = None + try: + size, _err = util.subp(size_cmd) + except Exception as e: + raise Exception("Failed to get %s size\n%s" % (device, e)) + + return int(size.strip()) + + +def get_dyn_func(*args): + """ + Call the appropriate function. + + The first value is the template for function name + The second value is the template replacement + The remain values are passed to the function + + For example: get_dyn_func("foo_%s", 'bar', 1, 2, 3,) + would call "foo_bar" with args of 1, 2, 3 + """ + if len(args) < 2: + raise Exception("Unable to determine dynamic funcation name") + + func_name = (args[0] % args[1]) + func_args = args[2:] + + try: + if func_args: + return globals()[func_name](*func_args) + else: + return globals()[func_name] + + except KeyError: + raise Exception("No such function %s to call!" % func_name) + + +def check_partition_mbr_layout(device, layout): + """ + Returns true if the partition layout matches the one on the disk + + Layout should be a list of values. At this time, this only + verifies that the number of partitions and their labels is correct. + """ + + read_parttbl(device) + prt_cmd = [SFDISK_CMD, "-l", device] + try: + out, _err = util.subp(prt_cmd, data="%s\n" % layout) + except Exception as e: + raise Exception("Error running partition command on %s\n%s" % ( + device, e)) + + found_layout = [] + for line in out.splitlines(): + _line = line.split() + if len(_line) == 0: + continue + + if device in _line[0]: + # We don't understand extended partitions yet + if _line[-1].lower() in ['extended', 'empty']: + continue + + # Find the partition types + type_label = None + for x in sorted(range(1, len(_line)), reverse=True): + if _line[x].isdigit() and _line[x] != '/': + type_label = _line[x] + break + + found_layout.append(type_label) + + if isinstance(layout, bool): + # if we are using auto partitioning, or "True" be happy + # if a single partition exists. + if layout and len(found_layout) >= 1: + return True + return False + + else: + if len(found_layout) != len(layout): + return False + else: + # This just makes sure that the number of requested + # partitions and the type labels are right + for x in range(1, len(layout) + 1): + if isinstance(layout[x - 1], tuple): + _, part_type = layout[x] + if int(found_layout[x]) != int(part_type): + return False + return True + + return False + + +def check_partition_layout(table_type, device, layout): + """ + See if the partition lay out matches. + + This is future a future proofing function. In order + to add support for other disk layout schemes, add a + function called check_partition_%s_layout + """ + return get_dyn_func("check_partition_%s_layout", table_type, device, + layout) + + +def get_partition_mbr_layout(size, layout): + """ + Calculate the layout of the partition table. Partition sizes + are defined as percentage values or a tuple of percentage and + partition type. + + For example: + [ 33, [66: 82] ] + + Defines the first partition to be a size of 1/3 the disk, + while the remaining 2/3's will be of type Linux Swap. + """ + + if not isinstance(layout, list) and isinstance(layout, bool): + # Create a single partition + return "0," + + if ((len(layout) == 0 and isinstance(layout, list)) or + not isinstance(layout, list)): + raise Exception("Partition layout is invalid") + + last_part_num = len(layout) + if last_part_num > 4: + raise Exception("Only simply partitioning is allowed.") + + part_definition = [] + part_num = 0 + for part in layout: + part_type = 83 # Default to Linux + percent = part + part_num += 1 + + if isinstance(part, list): + if len(part) != 2: + raise Exception("Partition was incorrectly defined: %s" % part) + percent, part_type = part + + part_size = int((float(size) * (float(percent) / 100)) / 1024) + + if part_num == last_part_num: + part_definition.append(",,%s" % part_type) + else: + part_definition.append(",%s,%s" % (part_size, part_type)) + + sfdisk_definition = "\n".join(part_definition) + if len(part_definition) > 4: + raise Exception("Calculated partition definition is too big\n%s" % + sfdisk_definition) + + return sfdisk_definition + + +def purge_disk_ptable(device): + # wipe the first and last megabyte of a disk (or file) + # gpt stores partition table both at front and at end. + null = '\0' # pylint: disable=W1401 + start_len = 1024 * 1024 + end_len = 1024 * 1024 + with open(device, "rb+") as fp: + fp.write(null * (start_len)) + fp.seek(-end_len, os.SEEK_END) + fp.write(null * end_len) + fp.flush() + + read_parttbl(device) + + +def purge_disk(device): + """ + Remove parition table entries + """ + + # wipe any file systems first + for d in enumerate_disk(device): + if d['type'] not in ["disk", "crypt"]: + wipefs_cmd = [WIPEFS_CMD, "--all", "/dev/%s" % d['name']] + try: + LOG.info("Purging filesystem on /dev/%s" % d['name']) + util.subp(wipefs_cmd) + except Exception: + raise Exception("Failed FS purge of /dev/%s" % d['name']) + + purge_disk_ptable(device) + + +def get_partition_layout(table_type, size, layout): + """ + Call the appropriate function for creating the table + definition. Returns the table definition + + This is a future proofing function. To add support for + other layouts, simply add a "get_partition_%s_layout" + function. + """ + return get_dyn_func("get_partition_%s_layout", table_type, size, layout) + + +def read_parttbl(device): + """ + Use partprobe instead of 'udevadm'. Partprobe is the only + reliable way to probe the partition table. + """ + blkdev_cmd = [BLKDEV_CMD, '--rereadpt', device] + udev_cmd = [UDEVADM_CMD, 'settle'] + try: + util.subp(udev_cmd) + util.subp(blkdev_cmd) + util.subp(udev_cmd) + except Exception as e: + util.logexc(LOG, "Failed reading the partition table %s" % e) + + +def exec_mkpart_mbr(device, layout): + """ + Break out of mbr partition to allow for future partition + types, i.e. gpt + """ + # Create the partitions + prt_cmd = [SFDISK_CMD, "--Linux", "-uM", device] + try: + util.subp(prt_cmd, data="%s\n" % layout) + except Exception as e: + raise Exception("Failed to partition device %s\n%s" % (device, e)) + + read_parttbl(device) + + +def exec_mkpart(table_type, device, layout): + """ + Fetches the function for creating the table type. + This allows to dynamically find which function to call. + + Paramaters: + table_type: type of partition table to use + device: the device to work on + layout: layout definition specific to partition table + """ + return get_dyn_func("exec_mkpart_%s", table_type, device, layout) + + +def mkpart(device, definition): + """ + Creates the partition table. + + Parameters: + definition: dictionary describing how to create the partition. + + The following are supported values in the dict: + overwrite: Should the partition table be created regardless + of any pre-exisiting data? + layout: the layout of the partition table + table_type: Which partition table to use, defaults to MBR + device: the device to work on. + """ + + LOG.debug("Checking values for %s definition" % device) + overwrite = definition.get('overwrite', False) + layout = definition.get('layout', False) + table_type = definition.get('table_type', 'mbr') + + # Check if the default device is a partition or not + LOG.debug("Checking against default devices") + + if (isinstance(layout, bool) and not layout) or not layout: + LOG.debug("Device is not to be partitioned, skipping") + return # Device is not to be partitioned + + # This prevents you from overwriting the device + LOG.debug("Checking if device %s is a valid device", device) + if not is_device_valid(device): + raise Exception("Device %s is not a disk device!", device) + + # Remove the partition table entries + if isinstance(layout, str) and layout.lower() == "remove": + LOG.debug("Instructed to remove partition table entries") + purge_disk(device) + return + + LOG.debug("Checking if device layout matches") + if check_partition_layout(table_type, device, layout): + LOG.debug("Device partitioning layout matches") + return True + + LOG.debug("Checking if device is safe to partition") + if not overwrite and (is_disk_used(device) or is_filesystem(device)): + LOG.debug("Skipping partitioning on configured device %s" % device) + return + + LOG.debug("Checking for device size") + device_size = get_hdd_size(device) + + LOG.debug("Calculating partition layout") + part_definition = get_partition_layout(table_type, device_size, layout) + LOG.debug(" Layout is: %s" % part_definition) + + LOG.debug("Creating partition table on %s", device) + exec_mkpart(table_type, device, part_definition) + + LOG.debug("Partition table created for %s", device) + + +def lookup_force_flag(fs): + """ + A force flag might be -F or -F, this look it up + """ + flags = {'ext': '-F', + 'btrfs': '-f', + 'xfs': '-f', + 'reiserfs': '-f', + } + + if 'ext' in fs.lower(): + fs = 'ext' + + if fs.lower() in flags: + return flags[fs] + + LOG.warn("Force flag for %s is unknown." % fs) + return '' + + +def mkfs(fs_cfg): + """ + Create a file system on the device. + + label: defines the label to use on the device + fs_cfg: defines how the filesystem is to look + The following values are required generally: + device: which device or cloud defined default_device + filesystem: which file system type + overwrite: indiscriminately create the file system + partition: when device does not define a partition, + setting this to a number will mean + device + partition. When set to 'auto', the + first free device or the first device which + matches both label and type will be used. + + 'any' means the first filesystem that matches + on the device. + + When 'cmd' is provided then no other parameter is required. + """ + label = fs_cfg.get('label') + device = fs_cfg.get('device') + partition = str(fs_cfg.get('partition', 'any')) + fs_type = fs_cfg.get('filesystem') + fs_cmd = fs_cfg.get('cmd', []) + fs_opts = fs_cfg.get('extra_opts', []) + fs_replace = fs_cfg.get('replace_fs', False) + overwrite = fs_cfg.get('overwrite', False) + + # This allows you to define the default ephemeral or swap + LOG.debug("Checking %s against default devices", device) + + if not partition or partition.isdigit(): + # Handle manual definition of partition + if partition.isdigit(): + device = "%s%s" % (device, partition) + LOG.debug("Manual request of partition %s for %s", + partition, device) + + # Check to see if the fs already exists + LOG.debug("Checking device %s", device) + check_label, check_fstype, _ = check_fs(device) + LOG.debug("Device %s has %s %s", device, check_label, check_fstype) + + if check_label == label and check_fstype == fs_type: + LOG.debug("Existing file system found at %s", device) + + if not overwrite: + LOG.debug("Device %s has required file system", device) + return + else: + LOG.warn("Destroying filesystem on %s", device) + + else: + LOG.debug("Device %s is cleared for formating", device) + + elif partition and str(partition).lower() in ('auto', 'any'): + # For auto devices, we match if the filesystem does exist + odevice = device + LOG.debug("Identifying device to create %s filesytem on", label) + + # any mean pick the first match on the device with matching fs_type + label_match = True + if partition.lower() == 'any': + label_match = False + + device, reuse = find_device_node(device, fs_type=fs_type, label=label, + label_match=label_match, + replace_fs=fs_replace) + LOG.debug("Automatic device for %s identified as %s", odevice, device) + + if reuse: + LOG.debug("Found filesystem match, skipping formating.") + return + + if not reuse and fs_replace and device: + LOG.debug("Replacing file system on %s as instructed." % device) + + if not device: + LOG.debug("No device aviable that matches request. " + "Skipping fs creation for %s", fs_cfg) + return + elif not partition or str(partition).lower() == 'none': + LOG.debug("Using the raw device to place filesystem %s on" % label) + + else: + LOG.debug("Error in device identification handling.") + return + + LOG.debug("File system %s will be created on %s", label, device) + + # Make sure the device is defined + if not device: + LOG.warn("Device is not known: %s", device) + return + + # Check that we can create the FS + if not (fs_type or fs_cmd): + raise Exception("No way to create filesystem '%s'. fs_type or fs_cmd " + "must be set.", label) + + # Create the commands + if fs_cmd: + fs_cmd = fs_cfg['cmd'] % {'label': label, + 'filesystem': fs_type, + 'device': device, + } + else: + # Find the mkfs command + mkfs_cmd = util.which("mkfs.%s" % fs_type) + if not mkfs_cmd: + mkfs_cmd = util.which("mk%s" % fs_type) + + if not mkfs_cmd: + LOG.warn("Cannot create fstype '%s'. No mkfs.%s command", fs_type, + fs_type) + return + + fs_cmd = [mkfs_cmd, device] + + if label: + fs_cmd.extend(["-L", label]) + + # File systems that support the -F flag + if not fs_cmd and (overwrite or device_type(device) == "disk"): + fs_cmd.append(lookup_force_flag(fs_type)) + + # Add the extends FS options + if fs_opts: + fs_cmd.extend(fs_opts) + + LOG.debug("Creating file system %s on %s", label, device) + LOG.debug(" Using cmd: %s", " ".join(fs_cmd)) + try: + util.subp(fs_cmd) + except Exception as e: + raise Exception("Failed to exec of '%s':\n%s" % (fs_cmd, e)) diff --git a/cloudinit/config/cc_final_message.py b/cloudinit/config/cc_final_message.py index 6b864fda..e92cba4a 100644 --- a/cloudinit/config/cc_final_message.py +++ b/cloudinit/config/cc_final_message.py @@ -54,7 +54,7 @@ def handle(_name, cfg, cloud, log, args): 'datasource': str(cloud.datasource), } util.multi_log("%s\n" % (templater.render_string(msg_in, subs)), - console=False, stderr=True) + console=False, stderr=True, log=log) except Exception: util.logexc(log, "Failed to render final message template") diff --git a/cloudinit/config/cc_growpart.py b/cloudinit/config/cc_growpart.py index 2d54aabf..0dd92a46 100644 --- a/cloudinit/config/cc_growpart.py +++ b/cloudinit/config/cc_growpart.py @@ -32,6 +32,7 @@ frequency = PER_ALWAYS DEFAULT_CONFIG = { 'mode': 'auto', 'devices': ['/'], + 'ignore_growroot_disabled': False, } @@ -251,6 +252,12 @@ def handle(_name, cfg, _cloud, log, _args): log.debug("growpart disabled: mode=%s" % mode) return + if util.is_false(mycfg.get('ignore_growroot_disabled', False)): + if os.path.isfile("/etc/growroot-disabled"): + log.debug("growpart disabled: /etc/growroot-disabled exists") + log.debug("use ignore_growroot_disabled to ignore") + return + devices = util.get_cfg_option_list(cfg, "devices", ["/"]) if not len(devices): log.debug("growpart: empty device list") diff --git a/cloudinit/config/cc_mounts.py b/cloudinit/config/cc_mounts.py index 390ba711..80590118 100644 --- a/cloudinit/config/cc_mounts.py +++ b/cloudinit/config/cc_mounts.py @@ -20,6 +20,8 @@ from string import whitespace # pylint: disable=W0402 +import logging +import os.path import re from cloudinit import type_utils @@ -31,6 +33,8 @@ SHORTNAME = re.compile(SHORTNAME_FILTER) WS = re.compile("[%s]+" % (whitespace)) FSTAB_PATH = "/etc/fstab" +LOG = logging.getLogger(__name__) + def is_mdname(name): # return true if this is a metadata service name @@ -44,6 +48,33 @@ def is_mdname(name): return False +def sanitize_devname(startname, transformer, log): + log.debug("Attempting to determine the real name of %s", startname) + + # workaround, allow user to specify 'ephemeral' + # rather than more ec2 correct 'ephemeral0' + devname = startname + if devname == "ephemeral": + devname = "ephemeral0" + log.debug("Adjusted mount option from ephemeral to ephemeral0") + + (blockdev, part) = util.expand_dotted_devname(devname) + + if is_mdname(blockdev): + orig = blockdev + blockdev = transformer(blockdev) + if not blockdev: + return None + if not blockdev.startswith("/"): + blockdev = "/dev/%s" % blockdev + log.debug("Mapped metadata name %s to %s", orig, blockdev) + else: + if SHORTNAME.match(startname): + blockdev = "/dev/%s" % blockdev + + return devnode_for_dev_part(blockdev, part) + + def handle(_name, cfg, cloud, log, _args): # fs_spec, fs_file, fs_vfstype, fs_mntops, fs-freq, fs_passno defvals = [None, None, "auto", "defaults,nobootwait", "0", "2"] @@ -64,32 +95,15 @@ def handle(_name, cfg, cloud, log, _args): (i + 1), type_utils.obj_name(cfgmnt[i])) continue - startname = str(cfgmnt[i][0]) - log.debug("Attempting to determine the real name of %s", startname) - - # workaround, allow user to specify 'ephemeral' - # rather than more ec2 correct 'ephemeral0' - if startname == "ephemeral": - cfgmnt[i][0] = "ephemeral0" - log.debug(("Adjusted mount option %s " - "name from ephemeral to ephemeral0"), (i + 1)) - - if is_mdname(startname): - newname = cloud.device_name_to_device(startname) - if not newname: - log.debug("Ignoring nonexistant named mount %s", startname) - cfgmnt[i][1] = None - else: - renamed = newname - if not newname.startswith("/"): - renamed = "/dev/%s" % newname - cfgmnt[i][0] = renamed - log.debug("Mapped metadata name %s to %s", startname, renamed) - else: - if SHORTNAME.match(startname): - renamed = "/dev/%s" % startname - log.debug("Mapped shortname name %s to %s", startname, renamed) - cfgmnt[i][0] = renamed + start = str(cfgmnt[i][0]) + sanitized = sanitize_devname(start, cloud.device_name_to_device, log) + if sanitized is None: + log.debug("Ignorming nonexistant named mount %s", start) + 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) # but do not convert None to 'None' (LP: #898365) @@ -118,17 +132,14 @@ def handle(_name, cfg, cloud, log, _args): # for each of the "default" mounts, add them only if no other # entry has the same device name for defmnt in defmnts: - startname = defmnt[0] - devname = cloud.device_name_to_device(startname) - if devname is None: - log.debug("Ignoring nonexistant named default mount %s", startname) + 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 devname.startswith("/"): - defmnt[0] = devname - else: - defmnt[0] = "/dev/%s" % devname - - log.debug("Mapped default device %s to %s", startname, defmnt[0]) + if sanitized != start: + log.debug("changed default device %s => %s" % (start, sanitized)) + defmnt[0] = sanitized cfgmnt_has = False for cfgm in cfgmnt: @@ -138,7 +149,7 @@ def handle(_name, cfg, cloud, log, _args): if cfgmnt_has: log.debug(("Not including %s, already" - " previously included"), startname) + " previously included"), start) continue cfgmnt.append(defmnt) @@ -198,3 +209,49 @@ def handle(_name, cfg, cloud, log, _args): util.subp(("mount", "-a")) except: util.logexc(log, "Activating mounts via 'mount -a' failed") + + +def devnode_for_dev_part(device, partition): + """ + Find the name of the partition. While this might seem rather + straight forward, its not since some devices are '<device><partition>' + while others are '<device>p<partition>'. For example, /dev/xvda3 on EC2 + will present as /dev/xvda3p1 for the first partition since /dev/xvda3 is + a block device. + """ + if not os.path.exists(device): + return None + + short_name = os.path.basename(device) + sys_path = "/sys/block/%s" % short_name + + if not os.path.exists(sys_path): + LOG.debug("did not find entry for %s in /sys/block", short_name) + return None + + sys_long_path = sys_path + "/" + short_name + + if partition is not None: + partition = str(partition) + + if partition is None: + valid_mappings = [sys_long_path + "1", sys_long_path + "p1"] + elif partition != "0": + valid_mappings = [sys_long_path + "%s" % partition, + sys_long_path + "p%s" % partition] + else: + valid_mappings = [] + + for cdisk in valid_mappings: + if not os.path.exists(cdisk): + continue + + dev_path = "/dev/%s" % os.path.basename(cdisk) + if os.path.exists(dev_path): + return dev_path + + if partition is None or partition == "0": + return device + + LOG.debug("Did not fine partition %s for device %s", partition, device) + return None diff --git a/cloudinit/config/cc_power_state_change.py b/cloudinit/config/cc_power_state_change.py index 188047e5..e3150808 100644 --- a/cloudinit/config/cc_power_state_change.py +++ b/cloudinit/config/cc_power_state_change.py @@ -75,6 +75,12 @@ def load_power_state(cfg): ','.join(opt_map.keys())) delay = pstate.get("delay", "now") + # convert integer 30 or string '30' to '+30' + try: + delay = "+%s" % int(delay) + except ValueError: + pass + if delay != "now" and not re.match(r"\+[0-9]+", delay): raise TypeError("power_state[delay] must be 'now' or '+m' (minutes).") diff --git a/cloudinit/config/cc_seed_random.py b/cloudinit/config/cc_seed_random.py new file mode 100644 index 00000000..22a31f29 --- /dev/null +++ b/cloudinit/config/cc_seed_random.py @@ -0,0 +1,61 @@ +# vi: ts=4 expandtab +# +# Copyright (C) 2013 Yahoo! Inc. +# +# Author: Joshua Harlow <harlowja@yahoo-inc.com> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3, as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import base64 +from StringIO import StringIO + +from cloudinit.settings import PER_INSTANCE +from cloudinit import util + +frequency = PER_INSTANCE + + +def _decode(data, encoding=None): + if not data: + return '' + if not encoding or encoding.lower() in ['raw']: + return data + elif encoding.lower() in ['base64', 'b64']: + return base64.b64decode(data) + elif encoding.lower() in ['gzip', 'gz']: + return util.decomp_gzip(data, quiet=False) + else: + raise IOError("Unknown random_seed encoding: %s" % (encoding)) + + +def handle(name, cfg, cloud, log, _args): + if not cfg or "random_seed" not in cfg: + log.debug(("Skipping module named %s, " + "no 'random_seed' configuration found"), name) + return + + my_cfg = cfg['random_seed'] + seed_path = my_cfg.get('file', '/dev/urandom') + seed_buf = StringIO() + seed_buf.write(_decode(my_cfg.get('data', ''), + encoding=my_cfg.get('encoding'))) + + metadata = cloud.datasource.metadata + if metadata and 'random_seed' in metadata: + seed_buf.write(metadata['random_seed']) + + seed_data = seed_buf.getvalue() + if len(seed_data): + log.debug("%s: adding %s bytes of random seed entrophy to %s", name, + len(seed_data), seed_path) + util.append_file(seed_path, seed_data) diff --git a/cloudinit/config/cc_ssh_authkey_fingerprints.py b/cloudinit/config/cc_ssh_authkey_fingerprints.py index c38bcea2..be8083db 100644 --- a/cloudinit/config/cc_ssh_authkey_fingerprints.py +++ b/cloudinit/config/cc_ssh_authkey_fingerprints.py @@ -63,7 +63,7 @@ def _is_printable_key(entry): def _pprint_key_entries(user, key_fn, key_entries, hash_meth='md5', prefix='ci-info: '): if not key_entries: - message = ("%sno authorized ssh keys fingerprints found for user %s." + message = ("%sno authorized ssh keys fingerprints found for user %s.\n" % (prefix, user)) util.multi_log(message) return |