diff options
Diffstat (limited to 'cloudinit')
-rw-r--r-- | cloudinit/config/cc_power_state_change.py | 9 | ||||
-rw-r--r-- | cloudinit/config/cc_ubuntu_init_switch.py | 164 | ||||
-rw-r--r-- | cloudinit/distros/__init__.py | 9 | ||||
-rw-r--r-- | cloudinit/distros/arch.py | 13 | ||||
-rw-r--r-- | cloudinit/distros/debian.py | 13 | ||||
-rw-r--r-- | cloudinit/distros/gentoo.py | 13 | ||||
-rw-r--r-- | cloudinit/importer.py | 4 | ||||
-rw-r--r-- | cloudinit/mergers/__init__.py | 5 | ||||
-rw-r--r-- | cloudinit/netinfo.py | 4 | ||||
-rw-r--r-- | cloudinit/sources/DataSourceCloudStack.py | 3 | ||||
-rw-r--r-- | cloudinit/stages.py | 8 | ||||
-rw-r--r-- | cloudinit/templater.py | 113 | ||||
-rw-r--r-- | cloudinit/util.py | 34 |
13 files changed, 318 insertions, 74 deletions
diff --git a/cloudinit/config/cc_power_state_change.py b/cloudinit/config/cc_power_state_change.py index 8f99e887..638daef8 100644 --- a/cloudinit/config/cc_power_state_change.py +++ b/cloudinit/config/cc_power_state_change.py @@ -89,8 +89,9 @@ def load_power_state(cfg): mode = pstate.get("mode") if mode not in opt_map: - raise TypeError("power_state[mode] required, must be one of: %s." % - ','.join(opt_map.keys())) + raise TypeError( + "power_state[mode] required, must be one of: %s. found: '%s'." % + (','.join(opt_map.keys()), mode)) delay = pstate.get("delay", "now") # convert integer 30 or string '30' to '+30' @@ -100,7 +101,9 @@ def load_power_state(cfg): pass if delay != "now" and not re.match(r"\+[0-9]+", delay): - raise TypeError("power_state[delay] must be 'now' or '+m' (minutes).") + raise TypeError( + "power_state[delay] must be 'now' or '+m' (minutes)." + " found '%s'." % delay) args = ["shutdown", opt_map[mode], delay] if pstate.get("message"): diff --git a/cloudinit/config/cc_ubuntu_init_switch.py b/cloudinit/config/cc_ubuntu_init_switch.py new file mode 100644 index 00000000..6f994bff --- /dev/null +++ b/cloudinit/config/cc_ubuntu_init_switch.py @@ -0,0 +1,164 @@ +# vi: ts=4 expandtab +# +# Copyright (C) 2014 Canonical Ltd. +# +# Author: Scott Moser <scott.moser@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/>. + +""" +ubuntu_init_switch: reboot system into another init + +This provides a way for the user to boot with systemd even if the +image is set to boot with upstart. It should be run as one of the first +cloud_init_modules, and will switch the init system and then issue a reboot. +The next boot will come up in the target init system and no action will +be taken. + +This should be inert on non-ubuntu systems, and also exit quickly. + +config is comes under the top level 'init_switch' dictionary. + +#cloud-config +init_switch: + target: systemd + reboot: true + +'target' can be 'systemd' or 'upstart'. Best effort is made, but its possible +this system will break, and probably won't interact well with any other +mechanism you've used to switch the init system. + +'reboot': [default=true]. + true: reboot if a change was made. + false: do not reboot. +""" + +from cloudinit.settings import PER_INSTANCE +from cloudinit import log as logging +from cloudinit import util +from cloudinit.distros import ubuntu + +import os +import time + +frequency = PER_INSTANCE +REBOOT_CMD = ["/sbin/reboot", "--force"] + +DEFAULT_CONFIG = { + 'init_switch': {'target': None, 'reboot': True} +} + +SWITCH_INIT = """ +#!/bin/sh +# switch_init: [upstart | systemd] + +is_systemd() { + [ "$(dpkg-divert --listpackage /sbin/init)" = "systemd-sysv" ] +} +debug() { echo "$@" 1>&2; } +fail() { echo "$@" 1>&2; exit 1; } + +if [ "$1" = "systemd" ]; then + if is_systemd; then + debug "already systemd, nothing to do" + else + [ -f /lib/systemd/systemd ] || fail "no systemd available"; + dpkg-divert --package systemd-sysv --divert /sbin/init.diverted \\ + --rename /sbin/init + fi + [ -f /sbin/init ] || ln /lib/systemd/systemd /sbin/init +elif [ "$1" = "upstart" ]; then + if is_systemd; then + rm -f /sbin/init + dpkg-divert --package systemd-sysv --rename --remove /sbin/init + else + debug "already upstart, nothing to do." + fi +else + fail "Error. expect 'upstart' or 'systemd'" +fi +""" + + +def handle(name, cfg, cloud, log, args): + + if not isinstance(cloud.distro, ubuntu.Distro): + log.debug("%s: distro is '%s', not ubuntu. returning", + name, cloud.distro.__class__) + return + + cfg = util.mergemanydict([cfg, DEFAULT_CONFIG]) + target = cfg['init_switch']['target'] + reboot = cfg['init_switch']['reboot'] + + if len(args) != 0: + target = args[0] + if len(args) > 1: + reboot = util.is_true(args[1]) + + if not target: + log.debug("%s: target=%s. nothing to do", name, target) + return + + if not util.which('dpkg'): + log.warn("%s: 'dpkg' not available. Assuming not ubuntu", name) + return + + supported = ('upstart', 'systemd') + if target not in supported: + log.warn("%s: target set to %s, expected one of: %s", + name, target, str(supported)) + + if os.path.exists("/run/systemd/system"): + current = "systemd" + else: + current = "upstart" + + if current == target: + log.debug("%s: current = target = %s. nothing to do", name, target) + return + + try: + util.subp(['sh', '-s', target], data=SWITCH_INIT) + except util.ProcessExecutionError as e: + log.warn("%s: Failed to switch to init '%s'. %s", name, target, e) + return + + if util.is_false(reboot): + log.info("%s: switched '%s' to '%s'. reboot=false, not rebooting.", + name, current, target) + return + + try: + log.warn("%s: switched '%s' to '%s'. rebooting.", + name, current, target) + logging.flushLoggers(log) + _fire_reboot(log, wait_attempts=4, initial_sleep=4) + except Exception as e: + util.logexc(log, "Requested reboot did not happen!") + raise + + +def _fire_reboot(log, wait_attempts=6, initial_sleep=1, backoff=2): + util.subp(REBOOT_CMD) + start = time.time() + wait_time = initial_sleep + for _i in range(0, wait_attempts): + time.sleep(wait_time) + wait_time *= backoff + elapsed = time.time() - start + log.debug("Rebooted, but still running after %s seconds", int(elapsed)) + # If we got here, not good + elapsed = time.time() - start + raise RuntimeError(("Reboot did not happen" + " after %s seconds!") % (int(elapsed))) diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py index 55d6bcbc..1a56dfb3 100644 --- a/cloudinit/distros/__init__.py +++ b/cloudinit/distros/__init__.py @@ -856,3 +856,12 @@ def fetch(name): mod = importer.import_module(locs[0]) cls = getattr(mod, 'Distro') return cls + + +def set_etc_timezone(tz, tz_file=None, tz_conf="/etc/timezone", + tz_local="/etc/localtime"): + util.write_file(tz_conf, str(tz).rstrip() + "\n") + # This ensures that the correct tz will be used for the system + if tz_local and tz_file: + util.copy(tz_file, self.tz_local_fn) + return diff --git a/cloudinit/distros/arch.py b/cloudinit/distros/arch.py index 310c3dff..9f11b89c 100644 --- a/cloudinit/distros/arch.py +++ b/cloudinit/distros/arch.py @@ -32,8 +32,6 @@ LOG = logging.getLogger(__name__) class Distro(distros.Distro): locale_conf_fn = "/etc/locale.gen" network_conf_dir = "/etc/netctl" - tz_conf_fn = "/etc/timezone" - tz_local_fn = "/etc/localtime" resolve_conf_fn = "/etc/resolv.conf" init_cmd = ['systemctl'] # init scripts @@ -161,16 +159,7 @@ class Distro(distros.Distro): return hostname def set_timezone(self, tz): - tz_file = self._find_tz_file(tz) - # Note: "" provides trailing newline during join - tz_lines = [ - util.make_header(), - str(tz), - "", - ] - util.write_file(self.tz_conf_fn, "\n".join(tz_lines)) - # This ensures that the correct tz will be used for the system - util.copy(tz_file, self.tz_local_fn) + set_etc_timezone(tz=tz, tz_file=self._find_tz_file(tz)) def package_command(self, command, args=None, pkgs=None): if pkgs is None: diff --git a/cloudinit/distros/debian.py b/cloudinit/distros/debian.py index 1ae232fd..7cf4a9ef 100644 --- a/cloudinit/distros/debian.py +++ b/cloudinit/distros/debian.py @@ -46,8 +46,6 @@ class Distro(distros.Distro): hostname_conf_fn = "/etc/hostname" locale_conf_fn = "/etc/default/locale" network_conf_fn = "/etc/network/interfaces" - tz_conf_fn = "/etc/timezone" - tz_local_fn = "/etc/localtime" def __init__(self, name, cfg, paths): distros.Distro.__init__(self, name, cfg, paths) @@ -133,16 +131,7 @@ class Distro(distros.Distro): return "127.0.1.1" def set_timezone(self, tz): - tz_file = self._find_tz_file(tz) - # Note: "" provides trailing newline during join - tz_lines = [ - util.make_header(), - str(tz), - "", - ] - util.write_file(self.tz_conf_fn, "\n".join(tz_lines)) - # This ensures that the correct tz will be used for the system - util.copy(tz_file, self.tz_local_fn) + set_etc_timezone(tz=tz, tz_file=self._find_tz_file(tz)) def package_command(self, command, args=None, pkgs=None): if pkgs is None: diff --git a/cloudinit/distros/gentoo.py b/cloudinit/distros/gentoo.py index 09f8d8ea..c4b02de1 100644 --- a/cloudinit/distros/gentoo.py +++ b/cloudinit/distros/gentoo.py @@ -31,8 +31,6 @@ LOG = logging.getLogger(__name__) class Distro(distros.Distro): locale_conf_fn = "/etc/locale.gen" network_conf_fn = "/etc/conf.d/net" - tz_conf_fn = "/etc/timezone" - tz_local_fn = "/etc/localtime" init_cmd = [''] # init scripts def __init__(self, name, cfg, paths): @@ -140,16 +138,7 @@ class Distro(distros.Distro): return hostname def set_timezone(self, tz): - tz_file = self._find_tz_file(tz) - # Note: "" provides trailing newline during join - tz_lines = [ - util.make_header(), - str(tz), - "", - ] - util.write_file(self.tz_conf_fn, "\n".join(tz_lines)) - # This ensures that the correct tz will be used for the system - util.copy(tz_file, self.tz_local_fn) + set_etc_timezone(tz=tz, tz_file=self._find_tz_file(tz)) def package_command(self, command, args=None, pkgs=None): if pkgs is None: diff --git a/cloudinit/importer.py b/cloudinit/importer.py index a094141a..a1929137 100644 --- a/cloudinit/importer.py +++ b/cloudinit/importer.py @@ -45,8 +45,6 @@ def find_module(base_name, search_paths, required_attrs=None): real_path.append(base_name) full_path = '.'.join(real_path) real_paths.append(full_path) - LOG.debug("Looking for modules %s that have attributes %s", - real_paths, required_attrs) for full_path in real_paths: mod = None try: @@ -62,6 +60,4 @@ def find_module(base_name, search_paths, required_attrs=None): found_attrs += 1 if found_attrs == len(required_attrs): found_places.append(full_path) - LOG.debug("Found %s with attributes %s in %s", base_name, - required_attrs, found_places) return found_places diff --git a/cloudinit/mergers/__init__.py b/cloudinit/mergers/__init__.py index 0978b2c6..650b42a9 100644 --- a/cloudinit/mergers/__init__.py +++ b/cloudinit/mergers/__init__.py @@ -55,9 +55,6 @@ class UnknownMerger(object): if not meth: meth = self._handle_unknown args.insert(0, method_name) - LOG.debug("Merging '%s' into '%s' using method '%s' of '%s'", - type_name, type_utils.obj_name(merge_with), - meth.__name__, self) return meth(*args) @@ -84,8 +81,6 @@ class LookupMerger(UnknownMerger): # First one that has that method/attr gets to be # the one that will be called meth = getattr(merger, meth_wanted) - LOG.debug(("Merging using located merger '%s'" - " since it had method '%s'"), merger, meth_wanted) break if not meth: return UnknownMerger._handle_unknown(self, meth_wanted, diff --git a/cloudinit/netinfo.py b/cloudinit/netinfo.py index 30b6f3b3..1bdca9f7 100644 --- a/cloudinit/netinfo.py +++ b/cloudinit/netinfo.py @@ -56,8 +56,8 @@ def netdev_info(empty=""): # newer (freebsd and fedora) show 'inet xx.yy' # just skip this 'inet' entry. (LP: #1285185) try: - if (toks[i] in ("inet", "inet6") and - toks[i + 1].startswith("addr:")): + if ((toks[i] in ("inet", "inet6") and + toks[i + 1].startswith("addr:"))): continue except IndexError: pass diff --git a/cloudinit/sources/DataSourceCloudStack.py b/cloudinit/sources/DataSourceCloudStack.py index 08f661e4..1bbeca59 100644 --- a/cloudinit/sources/DataSourceCloudStack.py +++ b/cloudinit/sources/DataSourceCloudStack.py @@ -78,7 +78,8 @@ class DataSourceCloudStack(sources.DataSource): (max_wait, timeout) = self._get_url_settings() - urls = [self.metadata_address + "/latest/meta-data/instance-id"] + urls = [uhelp.combine_url(self.metadata_address, + 'latest/meta-data/instance-id')] start_time = time.time() url = uhelp.wait_for_url(urls=urls, max_wait=max_wait, timeout=timeout, status_cb=LOG.warn) diff --git a/cloudinit/stages.py b/cloudinit/stages.py index 58349ffc..9e071fc4 100644 --- a/cloudinit/stages.py +++ b/cloudinit/stages.py @@ -397,8 +397,8 @@ class Init(object): mod = handlers.fixup_handler(mod) types = c_handlers.register(mod) if types: - LOG.debug("Added custom handler for %s from %s", - types, fname) + LOG.debug("Added custom handler for %s [%s] from %s", + types, mod, fname) except Exception: util.logexc(LOG, "Failed to register handler from %s", fname) @@ -644,6 +644,8 @@ class Modules(object): freq = mod.frequency if not freq in FREQUENCIES: freq = PER_INSTANCE + LOG.debug("Running module %s (%s) with frequency %s", + name, mod, freq) # Use the configs logger and not our own # TODO(harlowja): possibly check the module @@ -657,7 +659,7 @@ class Modules(object): run_name = "config-%s" % (name) cc.run(run_name, mod.handle, func_args, freq=freq) except Exception as e: - util.logexc(LOG, "Running %s (%s) failed", name, mod) + util.logexc(LOG, "Running module %s (%s) failed", name, mod) failures.append((name, e)) return (which_ran, failures) diff --git a/cloudinit/templater.py b/cloudinit/templater.py index 77af1270..02f6261d 100644 --- a/cloudinit/templater.py +++ b/cloudinit/templater.py @@ -20,13 +20,119 @@ # 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 Cheetah.Template import Template +import collections +import re +try: + from Cheetah.Template import Template as CTemplate + CHEETAH_AVAILABLE = True +except (ImportError, AttributeError): + CHEETAH_AVAILABLE = False + +try: + import jinja2 + from jinja2 import Template as JTemplate + JINJA_AVAILABLE = True +except (ImportError, AttributeError): + JINJA_AVAILABLE = False + +from cloudinit import log as logging +from cloudinit import type_utils as tu from cloudinit import util +LOG = logging.getLogger(__name__) +TYPE_MATCHER = re.compile(r"##\s*template:(.*)", re.I) +BASIC_MATCHER = re.compile(r'\$\{([A-Za-z0-9_.]+)\}|\$([A-Za-z0-9_.]+)') + + +def basic_render(content, params): + """This does simple replacement of bash variable like templates. + + It identifies patterns like ${a} or $a and can also identify patterns like + ${a.b} or $a.b which will look for a key 'b' in the dictionary rooted + by key 'a'. + """ + + def replacer(match): + # Only 1 of the 2 groups will actually have a valid entry. + name = match.group(1) + if name is None: + name = match.group(2) + if name is None: + raise RuntimeError("Match encountered but no valid group present") + path = collections.deque(name.split(".")) + selected_params = params + while len(path) > 1: + key = path.popleft() + if not isinstance(selected_params, dict): + raise TypeError("Can not traverse into" + " non-dictionary '%s' of type %s while" + " looking for subkey '%s'" + % (selected_params, + tu.obj_name(selected_params), + key)) + selected_params = selected_params[key] + key = path.popleft() + if not isinstance(selected_params, dict): + raise TypeError("Can not extract key '%s' from non-dictionary" + " '%s' of type %s" + % (key, selected_params, + tu.obj_name(selected_params))) + return str(selected_params[key]) + + return BASIC_MATCHER.sub(replacer, content) + + +def detect_template(text): + + def cheetah_render(content, params): + return CTemplate(content, searchList=[params]).respond() + + def jinja_render(content, params): + return JTemplate(content, + undefined=jinja2.StrictUndefined, + trim_blocks=True).render(**params) + + if text.find("\n") != -1: + ident, rest = text.split("\n", 1) + else: + ident = text + rest = '' + type_match = TYPE_MATCHER.match(ident) + if not type_match: + if not CHEETAH_AVAILABLE: + LOG.warn("Cheetah not available as the default renderer for" + " unknown template, reverting to the basic renderer.") + return ('basic', basic_render, text) + else: + return ('cheetah', cheetah_render, text) + else: + template_type = type_match.group(1).lower().strip() + if template_type not in ('jinja', 'cheetah', 'basic'): + raise ValueError("Unknown template rendering type '%s' requested" + % template_type) + if template_type == 'jinja' and not JINJA_AVAILABLE: + LOG.warn("Jinja not available as the selected renderer for" + " desired template, reverting to the basic renderer.") + return ('basic', basic_render, rest) + elif template_type == 'jinja' and JINJA_AVAILABLE: + return ('jinja', jinja_render, rest) + if template_type == 'cheetah' and not CHEETAH_AVAILABLE: + LOG.warn("Cheetah not available as the selected renderer for" + " desired template, reverting to the basic renderer.") + return ('basic', basic_render, rest) + elif template_type == 'cheetah' and CHEETAH_AVAILABLE: + return ('cheetah', cheetah_render, rest) + # Only thing left over is the basic renderer (it is always available). + return ('basic', basic_render, rest) + def render_from_file(fn, params): - return render_string(util.load_file(fn), params) + if not params: + params = {} + template_type, renderer, content = detect_template(util.load_file(fn)) + LOG.debug("Rendering content of '%s' using renderer %s", fn, template_type) + return renderer(content, params) def render_to_file(fn, outfn, params, mode=0644): @@ -37,4 +143,5 @@ def render_to_file(fn, outfn, params, mode=0644): def render_string(content, params): if not params: params = {} - return Template(content, searchList=[params]).respond() + template_type, renderer, content = detect_template(content) + return renderer(content, params) diff --git a/cloudinit/util.py b/cloudinit/util.py index 06039ee2..bc681f4a 100644 --- a/cloudinit/util.py +++ b/cloudinit/util.py @@ -146,23 +146,23 @@ class SeLinuxGuard(object): return False def __exit__(self, excp_type, excp_value, excp_traceback): - if self.selinux and self.selinux.is_selinux_enabled(): - path = os.path.realpath(os.path.expanduser(self.path)) - # path should be a string, not unicode - path = str(path) - do_restore = False - try: - # See if even worth restoring?? - stats = os.lstat(path) - if stat.ST_MODE in stats: - self.selinux.matchpathcon(path, stats[stat.ST_MODE]) - do_restore = True - except OSError: - pass - if do_restore: - LOG.debug("Restoring selinux mode for %s (recursive=%s)", - path, self.recursive) - self.selinux.restorecon(path, recursive=self.recursive) + if not self.selinux or not self.selinux.is_selinux_enabled(): + return + if not os.path.lexists(self.path): + return + + path = os.path.realpath(self.path) + # path should be a string, not unicode + path = str(path) + try: + stats = os.lstat(path) + self.selinux.matchpathcon(path, stats[stat.ST_MODE]) + except OSError: + return + + LOG.debug("Restoring selinux mode for %s (recursive=%s)", + path, self.recursive) + self.selinux.restorecon(path, recursive=self.recursive) class MountFailedError(Exception): |