summaryrefslogtreecommitdiff
path: root/cloudinit
diff options
context:
space:
mode:
Diffstat (limited to 'cloudinit')
-rw-r--r--cloudinit/config/cc_power_state_change.py9
-rw-r--r--cloudinit/config/cc_ubuntu_init_switch.py164
-rw-r--r--cloudinit/distros/__init__.py9
-rw-r--r--cloudinit/distros/arch.py13
-rw-r--r--cloudinit/distros/debian.py13
-rw-r--r--cloudinit/distros/gentoo.py13
-rw-r--r--cloudinit/importer.py4
-rw-r--r--cloudinit/mergers/__init__.py5
-rw-r--r--cloudinit/netinfo.py4
-rw-r--r--cloudinit/sources/DataSourceCloudStack.py3
-rw-r--r--cloudinit/stages.py8
-rw-r--r--cloudinit/templater.py113
-rw-r--r--cloudinit/util.py34
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):