summaryrefslogtreecommitdiff
path: root/cloudinit/config
diff options
context:
space:
mode:
Diffstat (limited to 'cloudinit/config')
-rw-r--r--cloudinit/config/__init__.py56
-rw-r--r--cloudinit/config/cc_apt_pipelining.py59
-rw-r--r--cloudinit/config/cc_apt_update_upgrade.py272
-rw-r--r--cloudinit/config/cc_bootcmd.py55
-rw-r--r--cloudinit/config/cc_byobu.py71
-rw-r--r--cloudinit/config/cc_ca_certs.py99
-rw-r--r--cloudinit/config/cc_chef.py129
-rw-r--r--cloudinit/config/cc_disable_ec2_metadata.py36
-rw-r--r--cloudinit/config/cc_final_message.py68
-rw-r--r--cloudinit/config/cc_foo.py52
-rw-r--r--cloudinit/config/cc_grub_dpkg.py67
-rw-r--r--cloudinit/config/cc_keys_to_console.py53
-rw-r--r--cloudinit/config/cc_landscape.py95
-rw-r--r--cloudinit/config/cc_locale.py37
-rw-r--r--cloudinit/config/cc_mcollective.py91
-rw-r--r--cloudinit/config/cc_mounts.py200
-rw-r--r--cloudinit/config/cc_phone_home.py118
-rw-r--r--cloudinit/config/cc_puppet.py113
-rw-r--r--cloudinit/config/cc_resizefs.py140
-rw-r--r--cloudinit/config/cc_rightscale_userdata.py102
-rw-r--r--cloudinit/config/cc_rsyslog.py102
-rw-r--r--cloudinit/config/cc_runcmd.py38
-rw-r--r--cloudinit/config/cc_salt_minion.py60
-rw-r--r--cloudinit/config/cc_scripts_per_boot.py41
-rw-r--r--cloudinit/config/cc_scripts_per_instance.py41
-rw-r--r--cloudinit/config/cc_scripts_per_once.py41
-rw-r--r--cloudinit/config/cc_scripts_user.py42
-rw-r--r--cloudinit/config/cc_set_hostname.py35
-rw-r--r--cloudinit/config/cc_set_passwords.py146
-rw-r--r--cloudinit/config/cc_ssh.py132
-rw-r--r--cloudinit/config/cc_ssh_import_id.py53
-rw-r--r--cloudinit/config/cc_timezone.py39
-rw-r--r--cloudinit/config/cc_update_etc_hosts.py60
-rw-r--r--cloudinit/config/cc_update_hostname.py41
34 files changed, 2784 insertions, 0 deletions
diff --git a/cloudinit/config/__init__.py b/cloudinit/config/__init__.py
new file mode 100644
index 00000000..69a8cc68
--- /dev/null
+++ b/cloudinit/config/__init__.py
@@ -0,0 +1,56 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2008-2010 Canonical Ltd.
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+# Author: Chuck Short <chuck.short@canonical.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.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, FREQUENCIES)
+
+from cloudinit import log as logging
+
+LOG = logging.getLogger(__name__)
+
+# This prefix is used to make it less
+# of a chance that when importing
+# we will not find something else with the same
+# name in the lookup path...
+MOD_PREFIX = "cc_"
+
+
+def form_module_name(name):
+ canon_name = name.replace("-", "_")
+ if canon_name.lower().endswith(".py"):
+ canon_name = canon_name[0:(len(canon_name) - 3)]
+ canon_name = canon_name.strip()
+ if not canon_name:
+ return None
+ if not canon_name.startswith(MOD_PREFIX):
+ canon_name = '%s%s' % (MOD_PREFIX, canon_name)
+ return canon_name
+
+
+def fixup_module(mod, def_freq=PER_INSTANCE):
+ if not hasattr(mod, 'frequency'):
+ setattr(mod, 'frequency', def_freq)
+ else:
+ freq = mod.frequency
+ if freq and freq not in FREQUENCIES:
+ LOG.warn("Module %s has an unknown frequency %s", mod, freq)
+ if not hasattr(mod, 'distros'):
+ setattr(mod, 'distros', None)
+ return mod
diff --git a/cloudinit/config/cc_apt_pipelining.py b/cloudinit/config/cc_apt_pipelining.py
new file mode 100644
index 00000000..3426099e
--- /dev/null
+++ b/cloudinit/config/cc_apt_pipelining.py
@@ -0,0 +1,59 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2011 Canonical Ltd.
+#
+# 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 import util
+from cloudinit.settings import PER_INSTANCE
+
+frequency = PER_INSTANCE
+
+distros = ['ubuntu', 'debian']
+
+DEFAULT_FILE = "/etc/apt/apt.conf.d/90cloud-init-pipelining"
+
+APT_PIPE_TPL = ("//Written by cloud-init per 'apt_pipelining'\n"
+ 'Acquire::http::Pipeline-Depth "%s";\n')
+
+# Acquire::http::Pipeline-Depth can be a value
+# from 0 to 5 indicating how many outstanding requests APT should send.
+# A value of zero MUST be specified if the remote host does not properly linger
+# on TCP connections - otherwise data corruption will occur.
+
+
+def handle(_name, cfg, cloud, log, _args):
+
+ apt_pipe_value = util.get_cfg_option_str(cfg, "apt_pipelining", False)
+ apt_pipe_value_s = str(apt_pipe_value).lower().strip()
+
+ if apt_pipe_value_s == "false":
+ write_apt_snippet(cloud, "0", log, DEFAULT_FILE)
+ elif apt_pipe_value_s in ("none", "unchanged", "os"):
+ return
+ elif apt_pipe_value_s in [str(b) for b in xrange(0, 6)]:
+ write_apt_snippet(cloud, apt_pipe_value_s, log, DEFAULT_FILE)
+ else:
+ log.warn("Invalid option for apt_pipeling: %s", apt_pipe_value)
+
+
+def write_apt_snippet(cloud, setting, log, f_name):
+ """ Writes f_name with apt pipeline depth 'setting' """
+
+ file_contents = APT_PIPE_TPL % (setting)
+
+ util.write_file(cloud.paths.join(False, f_name), file_contents)
+
+ log.debug("Wrote %s with apt pipeline depth setting %s", f_name, setting)
diff --git a/cloudinit/config/cc_apt_update_upgrade.py b/cloudinit/config/cc_apt_update_upgrade.py
new file mode 100644
index 00000000..5c5e510c
--- /dev/null
+++ b/cloudinit/config/cc_apt_update_upgrade.py
@@ -0,0 +1,272 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2009-2010 Canonical Ltd.
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.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 glob
+import os
+
+from cloudinit import templater
+from cloudinit import util
+
+distros = ['ubuntu', 'debian']
+
+PROXY_TPL = "Acquire::HTTP::Proxy \"%s\";\n"
+PROXY_FN = "/etc/apt/apt.conf.d/95cloud-init-proxy"
+
+# A temporary shell program to get a given gpg key
+# from a given keyserver
+EXPORT_GPG_KEYID = """
+ k=${1} ks=${2};
+ exec 2>/dev/null
+ [ -n "$k" ] || exit 1;
+ armour=$(gpg --list-keys --armour "${k}")
+ if [ -z "${armour}" ]; then
+ gpg --keyserver ${ks} --recv $k >/dev/null &&
+ armour=$(gpg --export --armour "${k}") &&
+ gpg --batch --yes --delete-keys "${k}"
+ fi
+ [ -n "${armour}" ] && echo "${armour}"
+"""
+
+
+def handle(name, cfg, cloud, log, _args):
+ update = util.get_cfg_option_bool(cfg, 'apt_update', False)
+ upgrade = util.get_cfg_option_bool(cfg, 'apt_upgrade', False)
+
+ release = get_release()
+ mirror = find_apt_mirror(cloud, cfg)
+ if not mirror:
+ log.debug(("Skipping module named %s,"
+ " no package 'mirror' located"), name)
+ return
+
+ log.debug("Selected mirror at: %s" % mirror)
+
+ if not util.get_cfg_option_bool(cfg,
+ 'apt_preserve_sources_list', False):
+ generate_sources_list(release, mirror, cloud, log)
+ old_mir = util.get_cfg_option_str(cfg, 'apt_old_mirror',
+ "archive.ubuntu.com/ubuntu")
+ rename_apt_lists(old_mir, mirror)
+
+ # Set up any apt proxy
+ proxy = cfg.get("apt_proxy", None)
+ proxy_filename = PROXY_FN
+ if proxy:
+ try:
+ # See man 'apt.conf'
+ contents = PROXY_TPL % (proxy)
+ util.write_file(cloud.paths.join(False, proxy_filename),
+ contents)
+ except Exception as e:
+ util.logexc(log, "Failed to write proxy to %s", proxy_filename)
+ elif os.path.isfile(proxy_filename):
+ util.del_file(proxy_filename)
+
+ # Process 'apt_sources'
+ if 'apt_sources' in cfg:
+ errors = add_sources(cloud, cfg['apt_sources'],
+ {'MIRROR': mirror, 'RELEASE': release})
+ for e in errors:
+ log.warn("Source Error: %s", ':'.join(e))
+
+ dconf_sel = util.get_cfg_option_str(cfg, 'debconf_selections', False)
+ if dconf_sel:
+ log.debug("setting debconf selections per cloud config")
+ try:
+ util.subp(('debconf-set-selections', '-'), dconf_sel)
+ except:
+ util.logexc(log, "Failed to run debconf-set-selections")
+
+ pkglist = util.get_cfg_option_list(cfg, 'packages', [])
+
+ errors = []
+ if update or len(pkglist) or upgrade:
+ try:
+ cloud.distro.update_package_sources()
+ except Exception as e:
+ util.logexc(log, "Package update failed")
+ errors.append(e)
+
+ if upgrade:
+ try:
+ cloud.distro.package_command("upgrade")
+ except Exception as e:
+ util.logexc(log, "Package upgrade failed")
+ errors.append(e)
+
+ if len(pkglist):
+ try:
+ cloud.distro.install_packages(pkglist)
+ except Exception as e:
+ util.logexc(log, "Failed to install packages: %s ", pkglist)
+ errors.append(e)
+
+ if len(errors):
+ log.warn("%s failed with exceptions, re-raising the last one",
+ len(errors))
+ raise errors[-1]
+
+
+# get gpg keyid from keyserver
+def getkeybyid(keyid, keyserver):
+ with util.ExtendedTemporaryFile(suffix='.sh') as fh:
+ fh.write(EXPORT_GPG_KEYID)
+ fh.flush()
+ cmd = ['/bin/sh', fh.name, keyid, keyserver]
+ (stdout, _stderr) = util.subp(cmd)
+ return stdout.strip()
+
+
+def mirror2lists_fileprefix(mirror):
+ string = mirror
+ # take off http:// or ftp://
+ if string.endswith("/"):
+ string = string[0:-1]
+ pos = string.find("://")
+ if pos >= 0:
+ string = string[pos + 3:]
+ string = string.replace("/", "_")
+ return string
+
+
+def rename_apt_lists(omirror, new_mirror, lists_d="/var/lib/apt/lists"):
+ oprefix = os.path.join(lists_d, mirror2lists_fileprefix(omirror))
+ nprefix = os.path.join(lists_d, mirror2lists_fileprefix(new_mirror))
+ if oprefix == nprefix:
+ return
+ olen = len(oprefix)
+ for filename in glob.glob("%s_*" % oprefix):
+ # TODO use the cloud.paths.join...
+ util.rename(filename, "%s%s" % (nprefix, filename[olen:]))
+
+
+def get_release():
+ (stdout, _stderr) = util.subp(['lsb_release', '-cs'])
+ return stdout.strip()
+
+
+def generate_sources_list(codename, mirror, cloud, log):
+ template_fn = cloud.get_template_filename('sources.list')
+ if template_fn:
+ params = {'mirror': mirror, 'codename': codename}
+ out_fn = cloud.paths.join(False, '/etc/apt/sources.list')
+ templater.render_to_file(template_fn, out_fn, params)
+ else:
+ log.warn("No template found, not rendering /etc/apt/sources.list")
+
+
+def add_sources(cloud, srclist, template_params=None):
+ """
+ add entries in /etc/apt/sources.list.d for each abbreviated
+ sources.list entry in 'srclist'. When rendering template, also
+ include the values in dictionary searchList
+ """
+ if template_params is None:
+ template_params = {}
+
+ errorlist = []
+ for ent in srclist:
+ if 'source' not in ent:
+ errorlist.append(["", "missing source"])
+ continue
+
+ source = ent['source']
+ if source.startswith("ppa:"):
+ try:
+ util.subp(["add-apt-repository", source])
+ except:
+ errorlist.append([source, "add-apt-repository failed"])
+ continue
+
+ source = templater.render_string(source, template_params)
+
+ if 'filename' not in ent:
+ ent['filename'] = 'cloud_config_sources.list'
+
+ if not ent['filename'].startswith("/"):
+ ent['filename'] = os.path.join("/etc/apt/sources.list.d/",
+ ent['filename'])
+
+ if ('keyid' in ent and 'key' not in ent):
+ ks = "keyserver.ubuntu.com"
+ if 'keyserver' in ent:
+ ks = ent['keyserver']
+ try:
+ ent['key'] = getkeybyid(ent['keyid'], ks)
+ except:
+ errorlist.append([source, "failed to get key from %s" % ks])
+ continue
+
+ if 'key' in ent:
+ try:
+ util.subp(('apt-key', 'add', '-'), ent['key'])
+ except:
+ errorlist.append([source, "failed add key"])
+
+ try:
+ contents = "%s\n" % (source)
+ util.write_file(cloud.paths.join(False, ent['filename']),
+ contents, omode="ab")
+ except:
+ errorlist.append([source,
+ "failed write to file %s" % ent['filename']])
+
+ return errorlist
+
+
+def find_apt_mirror(cloud, cfg):
+ """ find an apt_mirror given the cloud and cfg provided """
+
+ mirror = None
+
+ cfg_mirror = cfg.get("apt_mirror", None)
+ if cfg_mirror:
+ mirror = cfg["apt_mirror"]
+ elif "apt_mirror_search" in cfg:
+ mirror = util.search_for_mirror(cfg['apt_mirror_search'])
+ else:
+ mirror = cloud.get_local_mirror()
+
+ mydom = ""
+
+ doms = []
+
+ if not mirror:
+ # if we have a fqdn, then search its domain portion first
+ (_hostname, fqdn) = util.get_hostname_fqdn(cfg, cloud)
+ mydom = ".".join(fqdn.split(".")[1:])
+ if mydom:
+ doms.append(".%s" % mydom)
+
+ if not mirror:
+ doms.extend((".localdomain", "",))
+
+ mirror_list = []
+ distro = cloud.distro.name
+ mirrorfmt = "http://%s-mirror%s/%s" % (distro, "%s", distro)
+ for post in doms:
+ mirror_list.append(mirrorfmt % (post))
+
+ mirror = util.search_for_mirror(mirror_list)
+
+ if not mirror:
+ mirror = cloud.distro.get_package_mirror()
+
+ return mirror
diff --git a/cloudinit/config/cc_bootcmd.py b/cloudinit/config/cc_bootcmd.py
new file mode 100644
index 00000000..bae1ea54
--- /dev/null
+++ b/cloudinit/config/cc_bootcmd.py
@@ -0,0 +1,55 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2009-2011 Canonical Ltd.
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.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 os
+
+from cloudinit import util
+from cloudinit.settings import PER_ALWAYS
+
+frequency = PER_ALWAYS
+
+
+def handle(name, cfg, cloud, log, _args):
+
+ if "bootcmd" not in cfg:
+ log.debug(("Skipping module named %s,"
+ " no 'bootcmd' key in configuration"), name)
+ return
+
+ with util.ExtendedTemporaryFile(suffix=".sh") as tmpf:
+ try:
+ content = util.shellify(cfg["bootcmd"])
+ tmpf.write(content)
+ tmpf.flush()
+ except:
+ util.logexc(log, "Failed to shellify bootcmd")
+ raise
+
+ try:
+ env = os.environ.copy()
+ iid = cloud.get_instance_id()
+ if iid:
+ env['INSTANCE_ID'] = str(iid)
+ cmd = ['/bin/sh', tmpf.name]
+ util.subp(cmd, env=env, capture=False)
+ except:
+ util.logexc(log,
+ ("Failed to run bootcmd module %s"), name)
+ raise
diff --git a/cloudinit/config/cc_byobu.py b/cloudinit/config/cc_byobu.py
new file mode 100644
index 00000000..4e2e06bb
--- /dev/null
+++ b/cloudinit/config/cc_byobu.py
@@ -0,0 +1,71 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2009-2010 Canonical Ltd.
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.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 import util
+
+distros = ['ubuntu', 'debian']
+
+
+def handle(name, cfg, _cloud, log, args):
+ if len(args) != 0:
+ value = args[0]
+ else:
+ value = util.get_cfg_option_str(cfg, "byobu_by_default", "")
+
+ if not value:
+ log.debug("Skipping module named %s, no 'byobu' values found", name)
+ return
+
+ if value == "user" or value == "system":
+ value = "enable-%s" % value
+
+ valid = ("enable-user", "enable-system", "enable",
+ "disable-user", "disable-system", "disable")
+ if not value in valid:
+ log.warn("Unknown value %s for byobu_by_default", value)
+
+ mod_user = value.endswith("-user")
+ mod_sys = value.endswith("-system")
+ if value.startswith("enable"):
+ bl_inst = "install"
+ dc_val = "byobu byobu/launch-by-default boolean true"
+ mod_sys = True
+ else:
+ if value == "disable":
+ mod_user = True
+ mod_sys = True
+ bl_inst = "uninstall"
+ dc_val = "byobu byobu/launch-by-default boolean false"
+
+ shcmd = ""
+ if mod_user:
+ user = util.get_cfg_option_str(cfg, "user", "ubuntu")
+ shcmd += " sudo -Hu \"%s\" byobu-launcher-%s" % (user, bl_inst)
+ shcmd += " || X=$(($X+1)); "
+ if mod_sys:
+ shcmd += "echo \"%s\" | debconf-set-selections" % dc_val
+ shcmd += " && dpkg-reconfigure byobu --frontend=noninteractive"
+ shcmd += " || X=$(($X+1)); "
+
+ cmd = ["/bin/sh", "-c", "%s %s %s" % ("X=0;", shcmd, "exit $X")]
+
+ log.debug("Setting byobu to %s", value)
+
+ util.subp(cmd, capture=False)
diff --git a/cloudinit/config/cc_ca_certs.py b/cloudinit/config/cc_ca_certs.py
new file mode 100644
index 00000000..dc046bda
--- /dev/null
+++ b/cloudinit/config/cc_ca_certs.py
@@ -0,0 +1,99 @@
+# vi: ts=4 expandtab
+#
+# Author: Mike Milner <mike.milner@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/>.
+
+import os
+
+from cloudinit import util
+
+CA_CERT_PATH = "/usr/share/ca-certificates/"
+CA_CERT_FILENAME = "cloud-init-ca-certs.crt"
+CA_CERT_CONFIG = "/etc/ca-certificates.conf"
+CA_CERT_SYSTEM_PATH = "/etc/ssl/certs/"
+
+distros = ['ubuntu', 'debian']
+
+
+def update_ca_certs():
+ """
+ Updates the CA certificate cache on the current machine.
+ """
+ util.subp(["update-ca-certificates"], capture=False)
+
+
+def add_ca_certs(paths, certs):
+ """
+ Adds certificates to the system. To actually apply the new certificates
+ you must also call L{update_ca_certs}.
+
+ @param certs: A list of certificate strings.
+ """
+ if certs:
+ # First ensure they are strings...
+ cert_file_contents = "\n".join([str(c) for c in certs])
+ cert_file_fullpath = os.path.join(CA_CERT_PATH, CA_CERT_FILENAME)
+ cert_file_fullpath = paths.join(False, cert_file_fullpath)
+ util.write_file(cert_file_fullpath, cert_file_contents, mode=0644)
+ # Append cert filename to CA_CERT_CONFIG file.
+ util.write_file(paths.join(False, CA_CERT_CONFIG),
+ "\n%s" % CA_CERT_FILENAME, omode="ab")
+
+
+def remove_default_ca_certs(paths):
+ """
+ Removes all default trusted CA certificates from the system. To actually
+ apply the change you must also call L{update_ca_certs}.
+ """
+ util.delete_dir_contents(paths.join(False, CA_CERT_PATH))
+ util.delete_dir_contents(paths.join(False, CA_CERT_SYSTEM_PATH))
+ util.write_file(paths.join(False, CA_CERT_CONFIG), "", mode=0644)
+ debconf_sel = "ca-certificates ca-certificates/trust_new_crts select no"
+ util.subp(('debconf-set-selections', '-'), debconf_sel)
+
+
+def handle(name, cfg, cloud, log, _args):
+ """
+ Call to handle ca-cert sections in cloud-config file.
+
+ @param name: The module name "ca-cert" from cloud.cfg
+ @param cfg: A nested dict containing the entire cloud config contents.
+ @param cloud: The L{CloudInit} object in use.
+ @param log: Pre-initialized Python logger object to use for logging.
+ @param args: Any module arguments from cloud.cfg
+ """
+ # If there isn't a ca-certs section in the configuration don't do anything
+ if "ca-certs" not in cfg:
+ log.debug(("Skipping module named %s,"
+ " no 'ca-certs' key in configuration"), name)
+ return
+
+ ca_cert_cfg = cfg['ca-certs']
+
+ # If there is a remove-defaults option set to true, remove the system
+ # default trusted CA certs first.
+ if ca_cert_cfg.get("remove-defaults", False):
+ log.debug("Removing default certificates")
+ remove_default_ca_certs(cloud.paths)
+
+ # If we are given any new trusted CA certs to add, add them.
+ if "trusted" in ca_cert_cfg:
+ trusted_certs = util.get_cfg_option_list(ca_cert_cfg, "trusted")
+ if trusted_certs:
+ log.debug("Adding %d certificates" % len(trusted_certs))
+ add_ca_certs(cloud.paths, trusted_certs)
+
+ # Update the system with the new cert configuration.
+ log.debug("Updating certificates")
+ update_ca_certs()
diff --git a/cloudinit/config/cc_chef.py b/cloudinit/config/cc_chef.py
new file mode 100644
index 00000000..6f568261
--- /dev/null
+++ b/cloudinit/config/cc_chef.py
@@ -0,0 +1,129 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+# Author: Avishai Ish-Shalom <avishai@fewbytes.com>
+# Author: Mike Moulton <mike@meltmedia.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.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 json
+import os
+
+from cloudinit import templater
+from cloudinit import util
+
+RUBY_VERSION_DEFAULT = "1.8"
+
+
+def handle(name, cfg, cloud, log, _args):
+
+ # If there isn't a chef key in the configuration don't do anything
+ if 'chef' not in cfg:
+ log.debug(("Skipping module named %s,"
+ " no 'chef' key in configuration"), name)
+ return
+ chef_cfg = cfg['chef']
+
+ # Ensure the chef directories we use exist
+ c_dirs = [
+ '/etc/chef',
+ '/var/log/chef',
+ '/var/lib/chef',
+ '/var/cache/chef',
+ '/var/backups/chef',
+ '/var/run/chef',
+ ]
+ for d in c_dirs:
+ util.ensure_dir(cloud.paths.join(False, d))
+
+ # Set the validation key based on the presence of either 'validation_key'
+ # or 'validation_cert'. In the case where both exist, 'validation_key'
+ # takes precedence
+ for key in ('validation_key', 'validation_cert'):
+ if key in chef_cfg and chef_cfg[key]:
+ v_fn = cloud.paths.join(False, '/etc/chef/validation.pem')
+ util.write_file(v_fn, chef_cfg[key])
+ break
+
+ # Create the chef config from template
+ template_fn = cloud.get_template_filename('chef_client.rb')
+ if template_fn:
+ iid = str(cloud.datasource.get_instance_id())
+ params = {
+ 'server_url': chef_cfg['server_url'],
+ 'node_name': util.get_cfg_option_str(chef_cfg, 'node_name', iid),
+ 'environment': util.get_cfg_option_str(chef_cfg, 'environment',
+ '_default'),
+ 'validation_name': chef_cfg['validation_name']
+ }
+ out_fn = cloud.paths.join(False, '/etc/chef/client.rb')
+ templater.render_to_file(template_fn, out_fn, params)
+ else:
+ log.warn("No template found, not rendering to /etc/chef/client.rb")
+
+ # set the firstboot json
+ initial_json = {}
+ if 'run_list' in chef_cfg:
+ initial_json['run_list'] = chef_cfg['run_list']
+ if 'initial_attributes' in chef_cfg:
+ initial_attributes = chef_cfg['initial_attributes']
+ for k in list(initial_attributes.keys()):
+ initial_json[k] = initial_attributes[k]
+ firstboot_fn = cloud.paths.join(False, '/etc/chef/firstboot.json')
+ util.write_file(firstboot_fn, json.dumps(initial_json))
+
+ # If chef is not installed, we install chef based on 'install_type'
+ if not os.path.isfile('/usr/bin/chef-client'):
+ install_type = util.get_cfg_option_str(chef_cfg, 'install_type',
+ 'packages')
+ if install_type == "gems":
+ # this will install and run the chef-client from gems
+ chef_version = util.get_cfg_option_str(chef_cfg, 'version', None)
+ ruby_version = util.get_cfg_option_str(chef_cfg, 'ruby_version',
+ RUBY_VERSION_DEFAULT)
+ install_chef_from_gems(cloud.distro, ruby_version, chef_version)
+ # and finally, run chef-client
+ log.debug('Running chef-client')
+ util.subp(['/usr/bin/chef-client',
+ '-d', '-i', '1800', '-s', '20'], capture=False)
+ elif install_type == 'packages':
+ # this will install and run the chef-client from packages
+ cloud.distro.install_packages(('chef',))
+ else:
+ log.warn("Unknown chef install type %s", install_type)
+
+
+def get_ruby_packages(version):
+ # return a list of packages needed to install ruby at version
+ pkgs = ['ruby%s' % version, 'ruby%s-dev' % version]
+ if version == "1.8":
+ pkgs.extend(('libopenssl-ruby1.8', 'rubygems1.8'))
+ return pkgs
+
+
+def install_chef_from_gems(ruby_version, chef_version, distro):
+ distro.install_packages(get_ruby_packages(ruby_version))
+ if not os.path.exists('/usr/bin/gem'):
+ util.sym_link('/usr/bin/gem%s' % ruby_version, '/usr/bin/gem')
+ if not os.path.exists('/usr/bin/ruby'):
+ util.sym_link('/usr/bin/ruby%s' % ruby_version, '/usr/bin/ruby')
+ if chef_version:
+ util.subp(['/usr/bin/gem', 'install', 'chef',
+ '-v %s' % chef_version, '--no-ri',
+ '--no-rdoc', '--bindir', '/usr/bin', '-q'], capture=False)
+ else:
+ util.subp(['/usr/bin/gem', 'install', 'chef',
+ '--no-ri', '--no-rdoc', '--bindir',
+ '/usr/bin', '-q'], capture=False)
diff --git a/cloudinit/config/cc_disable_ec2_metadata.py b/cloudinit/config/cc_disable_ec2_metadata.py
new file mode 100644
index 00000000..3fd2c20f
--- /dev/null
+++ b/cloudinit/config/cc_disable_ec2_metadata.py
@@ -0,0 +1,36 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2009-2010 Canonical Ltd.
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.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 import util
+
+from cloudinit.settings import PER_ALWAYS
+
+frequency = PER_ALWAYS
+
+REJECT_CMD = ['route', 'add', '-host', '169.254.169.254', 'reject']
+
+
+def handle(name, cfg, _cloud, log, _args):
+ disabled = util.get_cfg_option_bool(cfg, "disable_ec2_metadata", False)
+ if disabled:
+ util.subp(REJECT_CMD, capture=False)
+ else:
+ log.debug(("Skipping module named %s,"
+ " disabling the ec2 route not enabled"), name)
diff --git a/cloudinit/config/cc_final_message.py b/cloudinit/config/cc_final_message.py
new file mode 100644
index 00000000..b1caca47
--- /dev/null
+++ b/cloudinit/config/cc_final_message.py
@@ -0,0 +1,68 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2011 Canonical Ltd.
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.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 import templater
+from cloudinit import util
+from cloudinit import version
+
+from cloudinit.settings import PER_ALWAYS
+
+frequency = PER_ALWAYS
+
+FINAL_MESSAGE_DEF = ("Cloud-init v. {{version}} finished at {{timestamp}}."
+ " Up {{uptime}} seconds.")
+
+
+def handle(_name, cfg, cloud, log, args):
+
+ msg_in = None
+ if len(args) != 0:
+ msg_in = args[0]
+ else:
+ msg_in = util.get_cfg_option_str(cfg, "final_message")
+
+ if not msg_in:
+ template_fn = cloud.get_template_filename('final_message')
+ if template_fn:
+ msg_in = util.load_file(template_fn)
+
+ if not msg_in:
+ msg_in = FINAL_MESSAGE_DEF
+
+ uptime = util.uptime()
+ ts = util.time_rfc2822()
+ cver = version.version_string()
+ try:
+ subs = {
+ 'uptime': uptime,
+ 'timestamp': ts,
+ 'version': cver,
+ }
+ util.multi_log("%s\n" % (templater.render_string(msg_in, subs)),
+ console=False, stderr=True)
+ except Exception:
+ util.logexc(log, "Failed to render final message template")
+
+ boot_fin_fn = cloud.paths.boot_finished
+ try:
+ contents = "%s - %s - v. %s\n" % (uptime, ts, cver)
+ util.write_file(boot_fin_fn, contents)
+ except:
+ util.logexc(log, "Failed to write boot finished file %s", boot_fin_fn)
diff --git a/cloudinit/config/cc_foo.py b/cloudinit/config/cc_foo.py
new file mode 100644
index 00000000..95aab4dd
--- /dev/null
+++ b/cloudinit/config/cc_foo.py
@@ -0,0 +1,52 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2009-2010 Canonical Ltd.
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.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
+
+# Modules are expected to have the following attributes.
+# 1. A required 'handle' method which takes the following params.
+# a) The name will not be this files name, but instead
+# the name specified in configuration (which is the name
+# which will be used to find this module).
+# b) A configuration object that is the result of the merging
+# of cloud configs configuration with legacy configuration
+# as well as any datasource provided configuration
+# c) A cloud object that can be used to access various
+# datasource and paths for the given distro and data provided
+# by the various datasource instance types.
+# d) A argument list that may or may not be empty to this module.
+# Typically those are from module configuration where the module
+# is defined with some extra configuration that will eventually
+# be translated from yaml into arguments to this module.
+# 2. A optional 'frequency' that defines how often this module should be ran.
+# Typically one of PER_INSTANCE, PER_ALWAYS, PER_ONCE. If not
+# provided PER_INSTANCE will be assumed.
+# See settings.py for these constants.
+# 3. A optional 'distros' array/set/tuple that defines the known distros
+# this module will work with (if not all of them). This is used to write
+# a warning out if a module is being ran on a untested distribution for
+# informational purposes. If non existent all distros are assumed and
+# no warning occurs.
+
+frequency = PER_INSTANCE
+
+
+def handle(name, _cfg, _cloud, log, _args):
+ log.debug("Hi from module %s", name)
diff --git a/cloudinit/config/cc_grub_dpkg.py b/cloudinit/config/cc_grub_dpkg.py
new file mode 100644
index 00000000..b3ce6fb6
--- /dev/null
+++ b/cloudinit/config/cc_grub_dpkg.py
@@ -0,0 +1,67 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2009-2010 Canonical Ltd.
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.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 os
+
+from cloudinit import util
+
+distros = ['ubuntu', 'debian']
+
+
+def handle(_name, cfg, _cloud, log, _args):
+ idevs = None
+ idevs_empty = None
+
+ if "grub-dpkg" in cfg:
+ idevs = util.get_cfg_option_str(cfg["grub-dpkg"],
+ "grub-pc/install_devices", None)
+ idevs_empty = util.get_cfg_option_str(cfg["grub-dpkg"],
+ "grub-pc/install_devices_empty", None)
+
+ if ((os.path.exists("/dev/sda1") and not os.path.exists("/dev/sda")) or
+ (os.path.exists("/dev/xvda1") and not os.path.exists("/dev/xvda"))):
+ if idevs is None:
+ idevs = ""
+ if idevs_empty is None:
+ idevs_empty = "true"
+ else:
+ if idevs_empty is None:
+ idevs_empty = "false"
+ if idevs is None:
+ idevs = "/dev/sda"
+ for dev in ("/dev/sda", "/dev/vda", "/dev/sda1", "/dev/vda1"):
+ if os.path.exists(dev):
+ idevs = dev
+ break
+
+ # now idevs and idevs_empty are set to determined values
+ # or, those set by user
+
+ dconf_sel = (("grub-pc grub-pc/install_devices string %s\n"
+ "grub-pc grub-pc/install_devices_empty boolean %s\n") %
+ (idevs, idevs_empty))
+
+ log.debug("Setting grub debconf-set-selections with '%s','%s'" %
+ (idevs, idevs_empty))
+
+ try:
+ util.subp(['debconf-set-selections'], dconf_sel)
+ except:
+ util.logexc(log, "Failed to run debconf-set-selections for grub-dpkg")
diff --git a/cloudinit/config/cc_keys_to_console.py b/cloudinit/config/cc_keys_to_console.py
new file mode 100644
index 00000000..ed7af690
--- /dev/null
+++ b/cloudinit/config/cc_keys_to_console.py
@@ -0,0 +1,53 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2011 Canonical Ltd.
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.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 os
+
+from cloudinit.settings import PER_INSTANCE
+from cloudinit import util
+
+frequency = PER_INSTANCE
+
+# This is a tool that cloud init provides
+HELPER_TOOL = '/usr/lib/cloud-init/write-ssh-key-fingerprints'
+
+
+def handle(name, cfg, _cloud, log, _args):
+ if not os.path.exists(HELPER_TOOL):
+ log.warn(("Unable to activate module %s,"
+ " helper tool not found at %s"), name, HELPER_TOOL)
+ return
+
+ fp_blacklist = util.get_cfg_option_list(cfg,
+ "ssh_fp_console_blacklist", [])
+ key_blacklist = util.get_cfg_option_list(cfg,
+ "ssh_key_console_blacklist",
+ ["ssh-dss"])
+
+ try:
+ cmd = [HELPER_TOOL]
+ cmd.append(','.join(fp_blacklist))
+ cmd.append(','.join(key_blacklist))
+ (stdout, _stderr) = util.subp(cmd)
+ util.multi_log("%s\n" % (stdout.strip()),
+ stderr=False, console=True)
+ except:
+ log.warn("Writing keys to the system console failed!")
+ raise
diff --git a/cloudinit/config/cc_landscape.py b/cloudinit/config/cc_landscape.py
new file mode 100644
index 00000000..906a6ff7
--- /dev/null
+++ b/cloudinit/config/cc_landscape.py
@@ -0,0 +1,95 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2011 Canonical Ltd.
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.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 os
+
+from StringIO import StringIO
+
+from configobj import ConfigObj
+
+from cloudinit import util
+
+from cloudinit.settings import PER_INSTANCE
+
+frequency = PER_INSTANCE
+
+LSC_CLIENT_CFG_FILE = "/etc/landscape/client.conf"
+
+distros = ['ubuntu']
+
+# defaults taken from stock client.conf in landscape-client 11.07.1.1-0ubuntu2
+LSC_BUILTIN_CFG = {
+ 'client': {
+ 'log_level': "info",
+ 'url': "https://landscape.canonical.com/message-system",
+ 'ping_url': "http://landscape.canonical.com/ping",
+ 'data_path': "/var/lib/landscape/client",
+ }
+}
+
+
+def handle(_name, cfg, cloud, log, _args):
+ """
+ Basically turn a top level 'landscape' entry with a 'client' dict
+ and render it to ConfigObj format under '[client]' section in
+ /etc/landscape/client.conf
+ """
+
+ ls_cloudcfg = cfg.get("landscape", {})
+
+ if not isinstance(ls_cloudcfg, (dict)):
+ raise RuntimeError(("'landscape' key existed in config,"
+ " but not a dictionary type,"
+ " is a %s instead"), util.obj_name(ls_cloudcfg))
+
+ merge_data = [
+ LSC_BUILTIN_CFG,
+ cloud.paths.join(True, LSC_CLIENT_CFG_FILE),
+ ls_cloudcfg,
+ ]
+ merged = merge_together(merge_data)
+
+ lsc_client_fn = cloud.paths.join(False, LSC_CLIENT_CFG_FILE)
+ lsc_dir = cloud.paths.join(False, os.path.dirname(lsc_client_fn))
+ if not os.path.isdir(lsc_dir):
+ util.ensure_dir(lsc_dir)
+
+ contents = StringIO()
+ merged.write(contents)
+ contents.flush()
+
+ util.write_file(lsc_client_fn, contents.getvalue())
+ log.debug("Wrote landscape config file to %s", lsc_client_fn)
+
+
+def merge_together(objs):
+ """
+ merge together ConfigObj objects or things that ConfigObj() will take in
+ later entries override earlier
+ """
+ cfg = ConfigObj({})
+ for obj in objs:
+ if not obj:
+ continue
+ if isinstance(obj, ConfigObj):
+ cfg.merge(obj)
+ else:
+ cfg.merge(ConfigObj(obj))
+ return cfg
diff --git a/cloudinit/config/cc_locale.py b/cloudinit/config/cc_locale.py
new file mode 100644
index 00000000..6feaae9d
--- /dev/null
+++ b/cloudinit/config/cc_locale.py
@@ -0,0 +1,37 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2011 Canonical Ltd.
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.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 import util
+
+
+def handle(name, cfg, cloud, log, args):
+ if len(args) != 0:
+ locale = args[0]
+ else:
+ locale = util.get_cfg_option_str(cfg, "locale", cloud.get_locale())
+
+ if not locale:
+ log.debug(("Skipping module named %s, "
+ "no 'locale' configuration found"), name)
+ return
+
+ log.debug("Setting locale to %s", locale)
+ locale_cfgfile = util.get_cfg_option_str(cfg, "locale_configfile")
+ cloud.distro.apply_locale(locale, locale_cfgfile)
diff --git a/cloudinit/config/cc_mcollective.py b/cloudinit/config/cc_mcollective.py
new file mode 100644
index 00000000..2acdbc6f
--- /dev/null
+++ b/cloudinit/config/cc_mcollective.py
@@ -0,0 +1,91 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2009-2011 Canonical Ltd.
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+# Author: Marc Cluet <marc.cluet@canonical.com>
+# Based on code by Scott Moser <scott.moser@canonical.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.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 StringIO import StringIO
+
+# Used since this can maintain comments
+# and doesn't need a top level section
+from configobj import ConfigObj
+
+from cloudinit import util
+
+PUBCERT_FILE = "/etc/mcollective/ssl/server-public.pem"
+PRICERT_FILE = "/etc/mcollective/ssl/server-private.pem"
+
+
+def handle(name, cfg, cloud, log, _args):
+
+ # If there isn't a mcollective key in the configuration don't do anything
+ if 'mcollective' not in cfg:
+ log.debug(("Skipping module named %s, "
+ "no 'mcollective' key in configuration"), name)
+ return
+
+ mcollective_cfg = cfg['mcollective']
+
+ # Start by installing the mcollective package ...
+ cloud.distro.install_packages(("mcollective",))
+
+ # ... and then update the mcollective configuration
+ if 'conf' in mcollective_cfg:
+ # Read server.cfg values from the
+ # original file in order to be able to mix the rest up
+ server_cfg_fn = cloud.paths.join(True, '/etc/mcollective/server.cfg')
+ mcollective_config = ConfigObj(server_cfg_fn)
+ # See: http://tiny.cc/jh9agw
+ for (cfg_name, cfg) in mcollective_cfg['conf'].iteritems():
+ if cfg_name == 'public-cert':
+ pubcert_fn = cloud.paths.join(True, PUBCERT_FILE)
+ util.write_file(pubcert_fn, cfg, mode=0644)
+ mcollective_config['plugin.ssl_server_public'] = pubcert_fn
+ mcollective_config['securityprovider'] = 'ssl'
+ elif cfg_name == 'private-cert':
+ pricert_fn = cloud.paths.join(True, PRICERT_FILE)
+ util.write_file(pricert_fn, cfg, mode=0600)
+ mcollective_config['plugin.ssl_server_private'] = pricert_fn
+ mcollective_config['securityprovider'] = 'ssl'
+ else:
+ if isinstance(cfg, (basestring, str)):
+ # Just set it in the 'main' section
+ mcollective_config[cfg_name] = cfg
+ elif isinstance(cfg, (dict)):
+ # Iterate throug the config items, create a section
+ # if it is needed and then add/or create items as needed
+ if cfg_name not in mcollective_config.sections:
+ mcollective_config[cfg_name] = {}
+ for (o, v) in cfg.iteritems():
+ mcollective_config[cfg_name][o] = v
+ else:
+ # Otherwise just try to convert it to a string
+ mcollective_config[cfg_name] = str(cfg)
+ # We got all our config as wanted we'll rename
+ # the previous server.cfg and create our new one
+ old_fn = cloud.paths.join(False, '/etc/mcollective/server.cfg.old')
+ util.rename(server_cfg_fn, old_fn)
+ # Now we got the whole file, write to disk...
+ contents = StringIO()
+ mcollective_config.write(contents)
+ contents = contents.getvalue()
+ server_cfg_rw = cloud.paths.join(False, '/etc/mcollective/server.cfg')
+ util.write_file(server_cfg_rw, contents, mode=0644)
+
+ # Start mcollective
+ util.subp(['service', 'mcollective', 'start'], capture=False)
diff --git a/cloudinit/config/cc_mounts.py b/cloudinit/config/cc_mounts.py
new file mode 100644
index 00000000..d3dcf7af
--- /dev/null
+++ b/cloudinit/config/cc_mounts.py
@@ -0,0 +1,200 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2009-2010 Canonical Ltd.
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.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 string import whitespace # pylint: disable=W0402
+
+import re
+
+from cloudinit import util
+
+# Shortname matches 'sda', 'sda1', 'xvda', 'hda', 'sdb', xvdb, vda, vdd1
+SHORTNAME_FILTER = r"^[x]{0,1}[shv]d[a-z][0-9]*$"
+SHORTNAME = re.compile(SHORTNAME_FILTER)
+WS = re.compile("[%s]+" % (whitespace))
+
+
+def is_mdname(name):
+ # return true if this is a metadata service name
+ if name in ["ami", "root", "swap"]:
+ return True
+ # names 'ephemeral0' or 'ephemeral1'
+ # 'ebs[0-9]' appears when '--block-device-mapping sdf=snap-d4d90bbc'
+ for enumname in ("ephemeral", "ebs"):
+ if name.startswith(enumname) and name.find(":") == -1:
+ return True
+ return False
+
+
+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"]
+ defvals = cfg.get("mount_default_fields", defvals)
+
+ # these are our default set of mounts
+ defmnts = [["ephemeral0", "/mnt", "auto", defvals[3], "0", "2"],
+ ["swap", "none", "swap", "sw", "0", "0"]]
+
+ cfgmnt = []
+ if "mounts" in cfg:
+ cfgmnt = cfg["mounts"]
+
+ for i in range(len(cfgmnt)):
+ # skip something that wasn't a list
+ if not isinstance(cfgmnt[i], list):
+ log.warn("Mount option %s not a list, got a %s instead",
+ (i + 1), util.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
+
+ # in case the user did not quote a field (likely fs-freq, fs_passno)
+ # but do not convert None to 'None' (LP: #898365)
+ for j in range(len(cfgmnt[i])):
+ if j is None:
+ continue
+ else:
+ cfgmnt[i][j] = str(cfgmnt[i][j])
+
+ for i in range(len(cfgmnt)):
+ # fill in values with defaults from defvals above
+ for j in range(len(defvals)):
+ if len(cfgmnt[i]) <= j:
+ cfgmnt[i].append(defvals[j])
+ elif cfgmnt[i][j] is None:
+ cfgmnt[i][j] = defvals[j]
+
+ # if the second entry in the list is 'None' this
+ # clears all previous entries of that same 'fs_spec'
+ # (fs_spec is the first field in /etc/fstab, ie, that device)
+ if cfgmnt[i][1] is None:
+ for j in range(i):
+ if cfgmnt[j][0] == cfgmnt[i][0]:
+ cfgmnt[j][1] = None
+
+ # 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)
+ continue
+ if devname.startswith("/"):
+ defmnt[0] = devname
+ else:
+ defmnt[0] = "/dev/%s" % devname
+
+ log.debug("Mapped default device %s to %s", startname, defmnt[0])
+
+ cfgmnt_has = False
+ for cfgm in cfgmnt:
+ if cfgm[0] == defmnt[0]:
+ cfgmnt_has = True
+ break
+
+ if cfgmnt_has:
+ log.debug(("Not including %s, already"
+ " previously included"), startname)
+ continue
+ cfgmnt.append(defmnt)
+
+ # now, each entry in the cfgmnt list has all fstab values
+ # if the second field is None (not the string, the value) we skip it
+ actlist = []
+ for x in cfgmnt:
+ if x[1] is None:
+ log.debug("Skipping non-existent device named %s", x[0])
+ else:
+ actlist.append(x)
+
+ if len(actlist) == 0:
+ log.debug("No modifications to fstab needed.")
+ return
+
+ comment = "comment=cloudconfig"
+ cc_lines = []
+ needswap = False
+ dirs = []
+ for line in actlist:
+ # write 'comment' in the fs_mntops, entry, claiming this
+ line[3] = "%s,%s" % (line[3], comment)
+ if line[2] == "swap":
+ needswap = True
+ if line[1].startswith("/"):
+ dirs.append(line[1])
+ cc_lines.append('\t'.join(line))
+
+ fstab_lines = []
+ fstab = util.load_file(cloud.paths.join(True, "/etc/fstab"))
+ for line in fstab.splitlines():
+ try:
+ toks = WS.split(line)
+ if toks[3].find(comment) != -1:
+ continue
+ except:
+ pass
+ fstab_lines.append(line)
+
+ fstab_lines.extend(cc_lines)
+ contents = "%s\n" % ('\n'.join(fstab_lines))
+ util.write_file(cloud.paths.join(False, "/etc/fstab"), contents)
+
+ if needswap:
+ try:
+ util.subp(("swapon", "-a"))
+ except:
+ util.logexc(log, "Activating swap via 'swapon -a' failed")
+
+ for d in dirs:
+ real_dir = cloud.paths.join(False, d)
+ try:
+ util.ensure_dir(real_dir)
+ except:
+ util.logexc(log, "Failed to make '%s' config-mount", d)
+
+ try:
+ util.subp(("mount", "-a"))
+ except:
+ util.logexc(log, "Activating mounts via 'mount -a' failed")
diff --git a/cloudinit/config/cc_phone_home.py b/cloudinit/config/cc_phone_home.py
new file mode 100644
index 00000000..ae1349eb
--- /dev/null
+++ b/cloudinit/config/cc_phone_home.py
@@ -0,0 +1,118 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2011 Canonical Ltd.
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.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 import templater
+from cloudinit import url_helper as uhelp
+from cloudinit import util
+
+from cloudinit.settings import PER_INSTANCE
+
+frequency = PER_INSTANCE
+
+POST_LIST_ALL = [
+ 'pub_key_dsa',
+ 'pub_key_rsa',
+ 'pub_key_ecdsa',
+ 'instance_id',
+ 'hostname'
+]
+
+
+# phone_home:
+# url: http://my.foo.bar/$INSTANCE/
+# post: all
+# tries: 10
+#
+# phone_home:
+# url: http://my.foo.bar/$INSTANCE_ID/
+# post: [ pub_key_dsa, pub_key_rsa, pub_key_ecdsa, instance_id
+#
+def handle(name, cfg, cloud, log, args):
+ if len(args) != 0:
+ ph_cfg = util.read_conf(args[0])
+ else:
+ if not 'phone_home' in cfg:
+ log.debug(("Skipping module named %s, "
+ "no 'phone_home' configuration found"), name)
+ return
+ ph_cfg = cfg['phone_home']
+
+ if 'url' not in ph_cfg:
+ log.warn(("Skipping module named %s, "
+ "no 'url' found in 'phone_home' configuration"), name)
+ return
+
+ url = ph_cfg['url']
+ post_list = ph_cfg.get('post', 'all')
+ tries = ph_cfg.get('tries')
+ try:
+ tries = int(tries)
+ except:
+ tries = 10
+ util.logexc(log, ("Configuration entry 'tries'"
+ " is not an integer, using %s instead"), tries)
+
+ if post_list == "all":
+ post_list = POST_LIST_ALL
+
+ all_keys = {}
+ all_keys['instance_id'] = cloud.get_instance_id()
+ all_keys['hostname'] = cloud.get_hostname()
+
+ pubkeys = {
+ 'pub_key_dsa': '/etc/ssh/ssh_host_dsa_key.pub',
+ 'pub_key_rsa': '/etc/ssh/ssh_host_rsa_key.pub',
+ 'pub_key_ecdsa': '/etc/ssh/ssh_host_ecdsa_key.pub',
+ }
+
+ for (n, path) in pubkeys.iteritems():
+ try:
+ all_keys[n] = util.load_file(cloud.paths.join(True, path))
+ except:
+ util.logexc(log, ("%s: failed to open, can not"
+ " phone home that data"), path)
+
+ submit_keys = {}
+ for k in post_list:
+ if k in all_keys:
+ submit_keys[k] = all_keys[k]
+ else:
+ submit_keys[k] = None
+ log.warn(("Requested key %s from 'post'"
+ " configuration list not available"), k)
+
+ # Get them read to be posted
+ real_submit_keys = {}
+ for (k, v) in submit_keys.iteritems():
+ if v is None:
+ real_submit_keys[k] = 'N/A'
+ else:
+ real_submit_keys[k] = str(v)
+
+ # Incase the url is parameterized
+ url_params = {
+ 'INSTANCE_ID': all_keys['instance_id'],
+ }
+ url = templater.render_string(url, url_params)
+ try:
+ uhelp.readurl(url, data=real_submit_keys, retries=tries, sec_between=3)
+ except:
+ util.logexc(log, ("Failed to post phone home data to"
+ " %s in %s tries"), url, tries)
diff --git a/cloudinit/config/cc_puppet.py b/cloudinit/config/cc_puppet.py
new file mode 100644
index 00000000..467c1496
--- /dev/null
+++ b/cloudinit/config/cc_puppet.py
@@ -0,0 +1,113 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2009-2010 Canonical Ltd.
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.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 StringIO import StringIO
+
+import os
+import pwd
+import socket
+
+from cloudinit import helpers
+from cloudinit import util
+
+
+def handle(name, cfg, cloud, log, _args):
+ # If there isn't a puppet key in the configuration don't do anything
+ if 'puppet' not in cfg:
+ log.debug(("Skipping module named %s,"
+ " no 'puppet' configuration found"), name)
+ return
+
+ puppet_cfg = cfg['puppet']
+
+ # Start by installing the puppet package ...
+ cloud.distro.install_packages(["puppet"])
+
+ # ... and then update the puppet configuration
+ if 'conf' in puppet_cfg:
+ # Add all sections from the conf object to puppet.conf
+ puppet_conf_fn = cloud.paths.join(True, '/etc/puppet/puppet.conf')
+ contents = util.load_file(puppet_conf_fn)
+ # Create object for reading puppet.conf values
+ puppet_config = helpers.DefaultingConfigParser()
+ # Read puppet.conf values from original file in order to be able to
+ # mix the rest up. First clean them up (TODO is this really needed??)
+ cleaned_lines = [i.lstrip() for i in contents.splitlines()]
+ cleaned_contents = '\n'.join(cleaned_lines)
+ puppet_config.readfp(StringIO(cleaned_contents),
+ filename=puppet_conf_fn)
+ for (cfg_name, cfg) in puppet_cfg['conf'].iteritems():
+ # Cert configuration is a special case
+ # Dump the puppet master ca certificate in the correct place
+ if cfg_name == 'ca_cert':
+ # Puppet ssl sub-directory isn't created yet
+ # Create it with the proper permissions and ownership
+ pp_ssl_dir = cloud.paths.join(False, '/var/lib/puppet/ssl')
+ util.ensure_dir(pp_ssl_dir, 0771)
+ util.chownbyid(pp_ssl_dir,
+ pwd.getpwnam('puppet').pw_uid, 0)
+ pp_ssl_certs = cloud.paths.join(False,
+ '/var/lib/puppet/ssl/certs/')
+ util.ensure_dir(pp_ssl_certs)
+ util.chownbyid(pp_ssl_certs,
+ pwd.getpwnam('puppet').pw_uid, 0)
+ pp_ssl_ca_certs = cloud.paths.join(False,
+ ('/var/lib/puppet/'
+ 'ssl/certs/ca.pem'))
+ util.write_file(pp_ssl_ca_certs, cfg)
+ util.chownbyid(pp_ssl_ca_certs,
+ pwd.getpwnam('puppet').pw_uid, 0)
+ else:
+ # Iterate throug the config items, we'll use ConfigParser.set
+ # to overwrite or create new items as needed
+ for (o, v) in cfg.iteritems():
+ if o == 'certname':
+ # Expand %f as the fqdn
+ # TODO should this use the cloud fqdn??
+ v = v.replace("%f", socket.getfqdn())
+ # Expand %i as the instance id
+ v = v.replace("%i", cloud.get_instance_id())
+ # certname needs to be downcased
+ v = v.lower()
+ puppet_config.set(cfg_name, o, v)
+ # We got all our config as wanted we'll rename
+ # the previous puppet.conf and create our new one
+ conf_old_fn = cloud.paths.join(False,
+ '/etc/puppet/puppet.conf.old')
+ util.rename(puppet_conf_fn, conf_old_fn)
+ puppet_conf_rw = cloud.paths.join(False, '/etc/puppet/puppet.conf')
+ util.write_file(puppet_conf_rw, puppet_config.stringify())
+
+ # Set puppet to automatically start
+ if os.path.exists('/etc/default/puppet'):
+ util.subp(['sed', '-i',
+ '-e', 's/^START=.*/START=yes/',
+ '/etc/default/puppet'], capture=False)
+ elif os.path.exists('/bin/systemctl'):
+ util.subp(['/bin/systemctl', 'enable', 'puppet.service'],
+ capture=False)
+ elif os.path.exists('/sbin/chkconfig'):
+ util.subp(['/sbin/chkconfig', 'puppet', 'on'], capture=False)
+ else:
+ log.warn(("Sorry we do not know how to enable"
+ " puppet services on this system"))
+
+ # Start puppetd
+ util.subp(['service', 'puppet', 'start'], capture=False)
diff --git a/cloudinit/config/cc_resizefs.py b/cloudinit/config/cc_resizefs.py
new file mode 100644
index 00000000..69cd8872
--- /dev/null
+++ b/cloudinit/config/cc_resizefs.py
@@ -0,0 +1,140 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2011 Canonical Ltd.
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.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 os
+import stat
+import time
+
+from cloudinit import util
+from cloudinit.settings import PER_ALWAYS
+
+frequency = PER_ALWAYS
+
+RESIZE_FS_PREFIXES_CMDS = [
+ ('ext', 'resize2fs'),
+ ('xfs', 'xfs_growfs'),
+]
+
+
+def nodeify_path(devpth, where, log):
+ try:
+ st_dev = os.stat(where).st_dev
+ dev = os.makedev(os.major(st_dev), os.minor(st_dev))
+ os.mknod(devpth, 0400 | stat.S_IFBLK, dev)
+ return st_dev
+ except:
+ if util.is_container():
+ log.debug("Inside container, ignoring mknod failure in resizefs")
+ return
+ log.warn("Failed to make device node to resize %s at %s",
+ where, devpth)
+ raise
+
+
+def get_fs_type(st_dev, path, log):
+ try:
+ dev_entries = util.find_devs_with(tag='TYPE', oformat='value',
+ no_cache=True, path=path)
+ if not dev_entries:
+ return None
+ return dev_entries[0].strip()
+ except util.ProcessExecutionError:
+ util.logexc(log, ("Failed to get filesystem type"
+ " of maj=%s, min=%s for path %s"),
+ os.major(st_dev), os.minor(st_dev), path)
+ raise
+
+
+def handle(name, cfg, cloud, log, args):
+ if len(args) != 0:
+ resize_root = args[0]
+ else:
+ resize_root = util.get_cfg_option_str(cfg, "resize_rootfs", True)
+
+ if not util.translate_bool(resize_root):
+ log.debug("Skipping module named %s, resizing disabled", name)
+ return
+
+ # TODO is the directory ok to be used??
+ resize_root_d = util.get_cfg_option_str(cfg, "resize_rootfs_tmp", "/run")
+ resize_root_d = cloud.paths.join(False, resize_root_d)
+ util.ensure_dir(resize_root_d)
+
+ # TODO: allow what is to be resized to be configurable??
+ resize_what = cloud.paths.join(False, "/")
+ with util.ExtendedTemporaryFile(prefix="cloudinit.resizefs.",
+ dir=resize_root_d, delete=True) as tfh:
+ devpth = tfh.name
+
+ # Delete the file so that mknod will work
+ # but don't change the file handle to know that its
+ # removed so that when a later call that recreates
+ # occurs this temporary file will still benefit from
+ # auto deletion
+ tfh.unlink_now()
+
+ st_dev = nodeify_path(devpth, resize_what, log)
+ fs_type = get_fs_type(st_dev, devpth, log)
+ if not fs_type:
+ log.warn("Could not determine filesystem type of %s", resize_what)
+ return
+
+ resizer = None
+ fstype_lc = fs_type.lower()
+ for (pfix, root_cmd) in RESIZE_FS_PREFIXES_CMDS:
+ if fstype_lc.startswith(pfix):
+ resizer = root_cmd
+ break
+
+ if not resizer:
+ log.warn("Not resizing unknown filesystem type %s for %s",
+ fs_type, resize_what)
+ return
+
+ log.debug("Resizing %s (%s) using %s", resize_what, fs_type, resizer)
+ resize_cmd = [resizer, devpth]
+
+ if resize_root == "noblock":
+ # Fork to a child that will run
+ # the resize command
+ util.fork_cb(do_resize, resize_cmd, log)
+ # Don't delete the file now in the parent
+ tfh.delete = False
+ else:
+ do_resize(resize_cmd, log)
+
+ action = 'Resized'
+ if resize_root == "noblock":
+ action = 'Resizing (via forking)'
+ log.debug("%s root filesystem (type=%s, maj=%i, min=%i, val=%s)",
+ action, fs_type, os.major(st_dev), os.minor(st_dev), resize_root)
+
+
+def do_resize(resize_cmd, log):
+ start = time.time()
+ try:
+ util.subp(resize_cmd)
+ except util.ProcessExecutionError:
+ util.logexc(log, "Failed to resize filesystem (cmd=%s)", resize_cmd)
+ raise
+ tot_time = int(time.time() - start)
+ log.debug("Resizing took %s seconds", tot_time)
+ # TODO: Should we add a fsck check after this to make
+ # sure we didn't corrupt anything?
diff --git a/cloudinit/config/cc_rightscale_userdata.py b/cloudinit/config/cc_rightscale_userdata.py
new file mode 100644
index 00000000..7a134569
--- /dev/null
+++ b/cloudinit/config/cc_rightscale_userdata.py
@@ -0,0 +1,102 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2011 Canonical Ltd.
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.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/>.
+
+##
+## The purpose of this script is to allow cloud-init to consume
+## rightscale style userdata. rightscale user data is key-value pairs
+## in a url-query-string like format.
+##
+## for cloud-init support, there will be a key named
+## 'CLOUD_INIT_REMOTE_HOOK'.
+##
+## This cloud-config module will
+## - read the blob of data from raw user data, and parse it as key/value
+## - for each key that is found, download the content to
+## the local instance/scripts directory and set them executable.
+## - the files in that directory will be run by the user-scripts module
+## Therefore, this must run before that.
+##
+##
+
+import os
+
+from cloudinit import url_helper as uhelp
+from cloudinit import util
+from cloudinit.settings import PER_INSTANCE
+
+from urlparse import parse_qs
+
+frequency = PER_INSTANCE
+
+MY_NAME = "cc_rightscale_userdata"
+MY_HOOKNAME = 'CLOUD_INIT_REMOTE_HOOK'
+
+
+def handle(name, _cfg, cloud, log, _args):
+ try:
+ ud = cloud.get_userdata_raw()
+ except:
+ log.warn("Failed to get raw userdata in module %s", name)
+ return
+
+ try:
+ mdict = parse_qs(ud)
+ if not mdict or not MY_HOOKNAME in mdict:
+ log.debug(("Skipping module %s, "
+ "did not find %s in parsed"
+ " raw userdata"), name, MY_HOOKNAME)
+ return
+ except:
+ util.logexc(log, ("Failed to parse query string %s"
+ " into a dictionary"), ud)
+ raise
+
+ wrote_fns = []
+ captured_excps = []
+
+ # These will eventually be then ran by the cc_scripts_user
+ # TODO: maybe this should just be a new user data handler??
+ # Instead of a late module that acts like a user data handler?
+ scripts_d = cloud.get_ipath_cur('scripts')
+ urls = mdict[MY_HOOKNAME]
+ for (i, url) in enumerate(urls):
+ fname = os.path.join(scripts_d, "rightscale-%02i" % (i))
+ try:
+ resp = uhelp.readurl(url)
+ # Ensure its a valid http response (and something gotten)
+ if resp.ok() and resp.contents:
+ util.write_file(fname, str(resp), mode=0700)
+ wrote_fns.append(fname)
+ except Exception as e:
+ captured_excps.append(e)
+ util.logexc(log, "%s failed to read %s and write %s",
+ MY_NAME, url, fname)
+
+ if wrote_fns:
+ log.debug("Wrote out rightscale userdata to %s files", len(wrote_fns))
+
+ if len(wrote_fns) != len(urls):
+ skipped = len(urls) - len(wrote_fns)
+ log.debug("%s urls were skipped or failed", skipped)
+
+ if captured_excps:
+ log.warn("%s failed with exceptions, re-raising the last one",
+ len(captured_excps))
+ raise captured_excps[-1]
diff --git a/cloudinit/config/cc_rsyslog.py b/cloudinit/config/cc_rsyslog.py
new file mode 100644
index 00000000..78327526
--- /dev/null
+++ b/cloudinit/config/cc_rsyslog.py
@@ -0,0 +1,102 @@
+# vi: ts=4 expandtab syntax=python
+#
+# Copyright (C) 2009-2010 Canonical Ltd.
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.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 os
+
+from cloudinit import util
+
+DEF_FILENAME = "20-cloud-config.conf"
+DEF_DIR = "/etc/rsyslog.d"
+
+
+def handle(name, cfg, cloud, log, _args):
+ # rsyslog:
+ # - "*.* @@192.158.1.1"
+ # - content: "*.* @@192.0.2.1:10514"
+ # - filename: 01-examplecom.conf
+ # content: |
+ # *.* @@syslogd.example.com
+
+ # process 'rsyslog'
+ if not 'rsyslog' in cfg:
+ log.debug(("Skipping module named %s,"
+ " no 'rsyslog' key in configuration"), name)
+ return
+
+ def_dir = cfg.get('rsyslog_dir', DEF_DIR)
+ def_fname = cfg.get('rsyslog_filename', DEF_FILENAME)
+
+ files = []
+ for i, ent in enumerate(cfg['rsyslog']):
+ if isinstance(ent, dict):
+ if not "content" in ent:
+ log.warn("No 'content' entry in config entry %s", i + 1)
+ continue
+ content = ent['content']
+ filename = ent.get("filename", def_fname)
+ else:
+ content = ent
+ filename = def_fname
+
+ filename = filename.strip()
+ if not filename:
+ log.warn("Entry %s has an empty filename", i + 1)
+ continue
+
+ if not filename.startswith("/"):
+ filename = os.path.join(def_dir, filename)
+
+ # Truncate filename first time you see it
+ omode = "ab"
+ if filename not in files:
+ omode = "wb"
+ files.append(filename)
+
+ try:
+ contents = "%s\n" % (content)
+ util.write_file(cloud.paths.join(False, filename),
+ contents, omode=omode)
+ except Exception:
+ util.logexc(log, "Failed to write to %s", filename)
+
+ # Attempt to restart syslogd
+ restarted = False
+ try:
+ # If this config module is running at cloud-init time
+ # (before rsyslog is running) we don't actually have to
+ # restart syslog.
+ #
+ # Upstart actually does what we want here, in that it doesn't
+ # start a service that wasn't running already on 'restart'
+ # it will also return failure on the attempt, so 'restarted'
+ # won't get set.
+ log.debug("Restarting rsyslog")
+ util.subp(['service', 'rsyslog', 'restart'])
+ restarted = True
+ except Exception:
+ util.logexc(log, "Failed restarting rsyslog")
+
+ if restarted:
+ # This only needs to run if we *actually* restarted
+ # syslog above.
+ cloud.cycle_logging()
+ # This should now use rsyslog if
+ # the logging was setup to use it...
+ log.debug("%s configured %s files", name, files)
diff --git a/cloudinit/config/cc_runcmd.py b/cloudinit/config/cc_runcmd.py
new file mode 100644
index 00000000..65064cfb
--- /dev/null
+++ b/cloudinit/config/cc_runcmd.py
@@ -0,0 +1,38 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2009-2010 Canonical Ltd.
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.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 os
+
+from cloudinit import util
+
+
+def handle(name, cfg, cloud, log, _args):
+ if "runcmd" not in cfg:
+ log.debug(("Skipping module named %s,"
+ " no 'runcmd' key in configuration"), name)
+ return
+
+ out_fn = os.path.join(cloud.get_ipath('scripts'), "runcmd")
+ cmd = cfg["runcmd"]
+ try:
+ content = util.shellify(cmd)
+ util.write_file(cloud.paths.join(False, out_fn), content, 0700)
+ except:
+ util.logexc(log, "Failed to shellify %s into file %s", cmd, out_fn)
diff --git a/cloudinit/config/cc_salt_minion.py b/cloudinit/config/cc_salt_minion.py
new file mode 100644
index 00000000..79ed8807
--- /dev/null
+++ b/cloudinit/config/cc_salt_minion.py
@@ -0,0 +1,60 @@
+# vi: ts=4 expandtab
+#
+# Author: Jeff Bauer <jbauer@rubic.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 os
+
+from cloudinit import util
+
+# Note: see http://saltstack.org/topics/installation/
+
+
+def handle(name, cfg, cloud, log, _args):
+ # If there isn't a salt key in the configuration don't do anything
+ if 'salt_minion' not in cfg:
+ log.debug(("Skipping module named %s,"
+ " no 'salt_minion' key in configuration"), name)
+ return
+
+ salt_cfg = cfg['salt_minion']
+
+ # Start by installing the salt package ...
+ cloud.distro.install_packages(["salt-minion"])
+
+ # Ensure we can configure files at the right dir
+ config_dir = cloud.paths.join(False, salt_cfg.get("config_dir",
+ '/etc/salt'))
+ util.ensure_dir(config_dir)
+
+ # ... and then update the salt configuration
+ if 'conf' in salt_cfg:
+ # Add all sections from the conf object to /etc/salt/minion
+ minion_config = os.path.join(config_dir, 'minion')
+ minion_data = util.yaml_dumps(salt_cfg.get('conf'))
+ util.write_file(minion_config, minion_data)
+
+ # ... copy the key pair if specified
+ if 'public_key' in salt_cfg and 'private_key' in salt_cfg:
+ pki_dir = cloud.paths.join(False, salt_cfg.get('pki_dir',
+ '/etc/salt/pki'))
+ with util.umask(077):
+ util.ensure_dir(pki_dir)
+ pub_name = os.path.join(pki_dir, 'minion.pub')
+ pem_name = os.path.join(pki_dir, 'minion.pem')
+ util.write_file(pub_name, salt_cfg['public_key'])
+ util.write_file(pem_name, salt_cfg['private_key'])
+
+ # Start salt-minion
+ util.subp(['service', 'salt-minion', 'start'], capture=False)
diff --git a/cloudinit/config/cc_scripts_per_boot.py b/cloudinit/config/cc_scripts_per_boot.py
new file mode 100644
index 00000000..42b987eb
--- /dev/null
+++ b/cloudinit/config/cc_scripts_per_boot.py
@@ -0,0 +1,41 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2011 Canonical Ltd.
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.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 os
+
+from cloudinit import util
+
+from cloudinit.settings import PER_ALWAYS
+
+frequency = PER_ALWAYS
+
+SCRIPT_SUBDIR = 'per-boot'
+
+
+def handle(name, _cfg, cloud, log, _args):
+ # Comes from the following:
+ # https://forums.aws.amazon.com/thread.jspa?threadID=96918
+ runparts_path = os.path.join(cloud.get_cpath(), 'scripts', SCRIPT_SUBDIR)
+ try:
+ util.runparts(runparts_path)
+ except:
+ log.warn("Failed to run module %s (%s in %s)",
+ name, SCRIPT_SUBDIR, runparts_path)
+ raise
diff --git a/cloudinit/config/cc_scripts_per_instance.py b/cloudinit/config/cc_scripts_per_instance.py
new file mode 100644
index 00000000..b5d71c13
--- /dev/null
+++ b/cloudinit/config/cc_scripts_per_instance.py
@@ -0,0 +1,41 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2011 Canonical Ltd.
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.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 os
+
+from cloudinit import util
+
+from cloudinit.settings import PER_INSTANCE
+
+frequency = PER_INSTANCE
+
+SCRIPT_SUBDIR = 'per-instance'
+
+
+def handle(name, _cfg, cloud, log, _args):
+ # Comes from the following:
+ # https://forums.aws.amazon.com/thread.jspa?threadID=96918
+ runparts_path = os.path.join(cloud.get_cpath(), 'scripts', SCRIPT_SUBDIR)
+ try:
+ util.runparts(runparts_path)
+ except:
+ log.warn("Failed to run module %s (%s in %s)",
+ name, SCRIPT_SUBDIR, runparts_path)
+ raise
diff --git a/cloudinit/config/cc_scripts_per_once.py b/cloudinit/config/cc_scripts_per_once.py
new file mode 100644
index 00000000..d77d36d5
--- /dev/null
+++ b/cloudinit/config/cc_scripts_per_once.py
@@ -0,0 +1,41 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2011 Canonical Ltd.
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.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 os
+
+from cloudinit import util
+
+from cloudinit.settings import PER_ONCE
+
+frequency = PER_ONCE
+
+SCRIPT_SUBDIR = 'per-once'
+
+
+def handle(name, _cfg, cloud, log, _args):
+ # Comes from the following:
+ # https://forums.aws.amazon.com/thread.jspa?threadID=96918
+ runparts_path = os.path.join(cloud.get_cpath(), 'scripts', SCRIPT_SUBDIR)
+ try:
+ util.runparts(runparts_path)
+ except:
+ log.warn("Failed to run module %s (%s in %s)",
+ name, SCRIPT_SUBDIR, runparts_path)
+ raise
diff --git a/cloudinit/config/cc_scripts_user.py b/cloudinit/config/cc_scripts_user.py
new file mode 100644
index 00000000..5c53014f
--- /dev/null
+++ b/cloudinit/config/cc_scripts_user.py
@@ -0,0 +1,42 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2011 Canonical Ltd.
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.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 os
+
+from cloudinit import util
+
+from cloudinit.settings import PER_INSTANCE
+
+frequency = PER_INSTANCE
+
+SCRIPT_SUBDIR = 'scripts'
+
+
+def handle(name, _cfg, cloud, log, _args):
+ # This is written to by the user data handlers
+ # Ie, any custom shell scripts that come down
+ # go here...
+ runparts_path = os.path.join(cloud.get_ipath_cur(), SCRIPT_SUBDIR)
+ try:
+ util.runparts(runparts_path)
+ except:
+ log.warn("Failed to run module %s (%s in %s)",
+ name, SCRIPT_SUBDIR, runparts_path)
+ raise
diff --git a/cloudinit/config/cc_set_hostname.py b/cloudinit/config/cc_set_hostname.py
new file mode 100644
index 00000000..b0f27ebf
--- /dev/null
+++ b/cloudinit/config/cc_set_hostname.py
@@ -0,0 +1,35 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2011 Canonical Ltd.
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.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 import util
+
+
+def handle(name, cfg, cloud, log, _args):
+ if util.get_cfg_option_bool(cfg, "preserve_hostname", False):
+ log.debug(("Configuration option 'preserve_hostname' is set,"
+ " not setting the hostname in module %s"), name)
+ return
+
+ (hostname, _fqdn) = util.get_hostname_fqdn(cfg, cloud)
+ try:
+ log.debug("Setting hostname to %s", hostname)
+ cloud.distro.set_hostname(hostname)
+ except Exception:
+ util.logexc(log, "Failed to set hostname to %s", hostname)
diff --git a/cloudinit/config/cc_set_passwords.py b/cloudinit/config/cc_set_passwords.py
new file mode 100644
index 00000000..ab266741
--- /dev/null
+++ b/cloudinit/config/cc_set_passwords.py
@@ -0,0 +1,146 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2009-2010 Canonical Ltd.
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.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 sys
+
+from cloudinit import ssh_util
+from cloudinit import util
+
+from string import letters, digits # pylint: disable=W0402
+
+# We are removing certain 'painful' letters/numbers
+PW_SET = (letters.translate(None, 'loLOI') +
+ digits.translate(None, '01'))
+
+
+def handle(_name, cfg, cloud, log, args):
+ if len(args) != 0:
+ # if run from command line, and give args, wipe the chpasswd['list']
+ password = args[0]
+ if 'chpasswd' in cfg and 'list' in cfg['chpasswd']:
+ del cfg['chpasswd']['list']
+ else:
+ password = util.get_cfg_option_str(cfg, "password", None)
+
+ expire = True
+ pw_auth = "no"
+ change_pwauth = False
+ plist = None
+
+ if 'chpasswd' in cfg:
+ chfg = cfg['chpasswd']
+ plist = util.get_cfg_option_str(chfg, 'list', plist)
+ expire = util.get_cfg_option_bool(chfg, 'expire', expire)
+
+ if not plist and password:
+ user = util.get_cfg_option_str(cfg, "user", "ubuntu")
+ plist = "%s:%s" % (user, password)
+
+ errors = []
+ if plist:
+ plist_in = []
+ randlist = []
+ users = []
+ for line in plist.splitlines():
+ u, p = line.split(':', 1)
+ if p == "R" or p == "RANDOM":
+ p = rand_user_password()
+ randlist.append("%s:%s" % (u, p))
+ plist_in.append("%s:%s" % (u, p))
+ users.append(u)
+
+ ch_in = '\n'.join(plist_in)
+ try:
+ log.debug("Changing password for %s:", users)
+ util.subp(['chpasswd'], ch_in)
+ except Exception as e:
+ errors.append(e)
+ util.logexc(log,
+ "Failed to set passwords with chpasswd for %s", users)
+
+ if len(randlist):
+ blurb = ("Set the following 'random' passwords\n",
+ '\n'.join(randlist))
+ sys.stderr.write("%s\n%s\n" % blurb)
+
+ if expire:
+ expired_users = []
+ for u in users:
+ try:
+ util.subp(['passwd', '--expire', u])
+ expired_users.append(u)
+ except Exception as e:
+ errors.append(e)
+ util.logexc(log, "Failed to set 'expire' for %s", u)
+ if expired_users:
+ log.debug("Expired passwords for: %s users", expired_users)
+
+ change_pwauth = False
+ pw_auth = None
+ if 'ssh_pwauth' in cfg:
+ change_pwauth = True
+ if util.is_true(cfg['ssh_pwauth']):
+ pw_auth = 'yes'
+ if util.is_false(cfg['ssh_pwauth']):
+ pw_auth = 'no'
+
+ if change_pwauth:
+ replaced_auth = False
+
+ # See: man sshd_config
+ conf_fn = cloud.paths.join(True, ssh_util.DEF_SSHD_CFG)
+ old_lines = ssh_util.parse_ssh_config(conf_fn)
+ new_lines = []
+ i = 0
+ for (i, line) in enumerate(old_lines):
+ # Keywords are case-insensitive and arguments are case-sensitive
+ if line.key == 'passwordauthentication':
+ log.debug("Replacing auth line %s with %s", i + 1, pw_auth)
+ replaced_auth = True
+ line.value = pw_auth
+ new_lines.append(line)
+
+ if not replaced_auth:
+ log.debug("Adding new auth line %s", i + 1)
+ replaced_auth = True
+ new_lines.append(ssh_util.SshdConfigLine('',
+ 'PasswordAuthentication',
+ pw_auth))
+
+ lines = [str(e) for e in new_lines]
+ ssh_rw_fn = cloud.paths.join(False, ssh_util.DEF_SSHD_CFG)
+ util.write_file(ssh_rw_fn, "\n".join(lines))
+
+ try:
+ cmd = ['service']
+ cmd.append(cloud.distro.get_option('ssh_svcname', 'ssh'))
+ cmd.append('restart')
+ util.subp(cmd)
+ log.debug("Restarted the ssh daemon")
+ except:
+ util.logexc(log, "Restarting of the ssh daemon failed")
+
+ if len(errors):
+ log.debug("%s errors occured, re-raising the last one", len(errors))
+ raise errors[-1]
+
+
+def rand_user_password(pwlen=9):
+ return util.rand_str(pwlen, select_from=PW_SET)
diff --git a/cloudinit/config/cc_ssh.py b/cloudinit/config/cc_ssh.py
new file mode 100644
index 00000000..4019ae90
--- /dev/null
+++ b/cloudinit/config/cc_ssh.py
@@ -0,0 +1,132 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2009-2010 Canonical Ltd.
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.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 os
+import glob
+
+from cloudinit import util
+from cloudinit import ssh_util
+
+DISABLE_ROOT_OPTS = ("no-port-forwarding,no-agent-forwarding,"
+"no-X11-forwarding,command=\"echo \'Please login as the user \\\"$USER\\\" "
+"rather than the user \\\"root\\\".\';echo;sleep 10\"")
+
+KEY_2_FILE = {
+ "rsa_private": ("/etc/ssh/ssh_host_rsa_key", 0600),
+ "rsa_public": ("/etc/ssh/ssh_host_rsa_key.pub", 0644),
+ "dsa_private": ("/etc/ssh/ssh_host_dsa_key", 0600),
+ "dsa_public": ("/etc/ssh/ssh_host_dsa_key.pub", 0644),
+ "ecdsa_private": ("/etc/ssh/ssh_host_ecdsa_key", 0600),
+ "ecdsa_public": ("/etc/ssh/ssh_host_ecdsa_key.pub", 0644),
+}
+
+PRIV_2_PUB = {
+ 'rsa_private': 'rsa_public',
+ 'dsa_private': 'dsa_public',
+ 'ecdsa_private': 'ecdsa_public',
+}
+
+KEY_GEN_TPL = 'o=$(ssh-keygen -yf "%s") && echo "$o" root@localhost > "%s"'
+
+GENERATE_KEY_NAMES = ['rsa', 'dsa', 'ecdsa']
+
+KEY_FILE_TPL = '/etc/ssh/ssh_host_%s_key'
+
+
+def handle(_name, cfg, cloud, log, _args):
+
+ # remove the static keys from the pristine image
+ if cfg.get("ssh_deletekeys", True):
+ key_pth = cloud.paths.join(False, "/etc/ssh/", "ssh_host_*key*")
+ for f in glob.glob(key_pth):
+ try:
+ util.del_file(f)
+ except:
+ 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"].iteritems():
+ if key in KEY_2_FILE:
+ tgt_fn = KEY_2_FILE[key][0]
+ tgt_perms = KEY_2_FILE[key][1]
+ util.write_file(cloud.paths.join(False, tgt_fn),
+ val, tgt_perms)
+
+ for (priv, pub) in PRIV_2_PUB.iteritems():
+ if pub in cfg['ssh_keys'] or not priv in cfg['ssh_keys']:
+ continue
+ pair = (KEY_2_FILE[priv][0], KEY_2_FILE[pub][0])
+ cmd = ['sh', '-xc', KEY_GEN_TPL % pair]
+ try:
+ # TODO: 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:
+ 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)
+ for keytype in genkeys:
+ keyfile = cloud.paths.join(False, KEY_FILE_TPL % (keytype))
+ util.ensure_dir(os.path.dirname(keyfile))
+ if not os.path.exists(keyfile):
+ cmd = ['ssh-keygen', '-t', keytype, '-N', '', '-f', keyfile]
+ try:
+ # TODO: Is this guard needed?
+ with util.SeLinuxGuard("/etc/ssh", recursive=True):
+ util.subp(cmd, capture=False)
+ except:
+ util.logexc(log, ("Failed generating key type"
+ " %s to file %s"), keytype, keyfile)
+
+ try:
+ user = util.get_cfg_option_str(cfg, 'user')
+ disable_root = util.get_cfg_option_bool(cfg, "disable_root", True)
+ disable_root_opts = util.get_cfg_option_str(cfg, "disable_root_opts",
+ DISABLE_ROOT_OPTS)
+
+ keys = cloud.get_public_ssh_keys() or []
+ if "ssh_authorized_keys" in cfg:
+ cfgkeys = cfg["ssh_authorized_keys"]
+ keys.extend(cfgkeys)
+
+ apply_credentials(keys, user, cloud.paths,
+ disable_root, disable_root_opts)
+ except:
+ util.logexc(log, "Applying ssh credentials failed!")
+
+
+def apply_credentials(keys, user, paths, disable_root, disable_root_opts):
+
+ keys = set(keys)
+ if user:
+ ssh_util.setup_user_keys(keys, user, '', paths)
+
+ if disable_root and user:
+ key_prefix = disable_root_opts.replace('$USER', user)
+ else:
+ key_prefix = ''
+
+ ssh_util.setup_user_keys(keys, 'root', key_prefix, paths)
diff --git a/cloudinit/config/cc_ssh_import_id.py b/cloudinit/config/cc_ssh_import_id.py
new file mode 100644
index 00000000..c58b28ec
--- /dev/null
+++ b/cloudinit/config/cc_ssh_import_id.py
@@ -0,0 +1,53 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2009-2010 Canonical Ltd.
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.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 import util
+
+# The ssh-import-id only seems to exist on ubuntu (for now)
+# https://launchpad.net/ssh-import-id
+distros = ['ubuntu']
+
+
+def handle(name, cfg, _cloud, log, args):
+ if len(args) != 0:
+ user = args[0]
+ ids = []
+ if len(args) > 1:
+ ids = args[1:]
+ else:
+ user = util.get_cfg_option_str(cfg, "user", "ubuntu")
+ ids = util.get_cfg_option_list(cfg, "ssh_import_id", [])
+
+ if len(ids) == 0:
+ log.debug("Skipping module named %s, no ids found to import", name)
+ return
+
+ if not user:
+ log.debug("Skipping module named %s, no user found to import", name)
+ return
+
+ cmd = ["sudo", "-Hu", user, "ssh-import-id"] + ids
+ log.debug("Importing ssh ids for user %s.", user)
+
+ try:
+ util.subp(cmd, capture=False)
+ except util.ProcessExecutionError as e:
+ util.logexc(log, "Failed to run command to import %s ssh ids", user)
+ raise e
diff --git a/cloudinit/config/cc_timezone.py b/cloudinit/config/cc_timezone.py
new file mode 100644
index 00000000..b9eb85b2
--- /dev/null
+++ b/cloudinit/config/cc_timezone.py
@@ -0,0 +1,39 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2009-2010 Canonical Ltd.
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.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 import util
+
+from cloudinit.settings import PER_INSTANCE
+
+frequency = PER_INSTANCE
+
+
+def handle(name, cfg, cloud, log, args):
+ if len(args) != 0:
+ timezone = args[0]
+ else:
+ timezone = util.get_cfg_option_str(cfg, "timezone", False)
+
+ if not timezone:
+ log.debug("Skipping module named %s, no 'timezone' specified", name)
+ return
+
+ # Let the distro handle settings its timezone
+ cloud.distro.set_timezone(timezone)
diff --git a/cloudinit/config/cc_update_etc_hosts.py b/cloudinit/config/cc_update_etc_hosts.py
new file mode 100644
index 00000000..c148b12e
--- /dev/null
+++ b/cloudinit/config/cc_update_etc_hosts.py
@@ -0,0 +1,60 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2011 Canonical Ltd.
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.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 import util
+from cloudinit import templater
+
+from cloudinit.settings import PER_ALWAYS
+
+frequency = PER_ALWAYS
+
+
+def handle(name, cfg, cloud, log, _args):
+ manage_hosts = util.get_cfg_option_str(cfg, "manage_etc_hosts", False)
+ if util.translate_bool(manage_hosts, addons=['template']):
+ (hostname, fqdn) = util.get_hostname_fqdn(cfg, cloud)
+ if not hostname:
+ log.warn(("Option 'manage_etc_hosts' was set,"
+ " but no hostname was found"))
+ return
+
+ # Render from a template file
+ distro_n = cloud.distro.name
+ tpl_fn_name = cloud.get_template_filename("hosts.%s" % (distro_n))
+ if not tpl_fn_name:
+ raise RuntimeError(("No hosts template could be"
+ " found for distro %s") % (distro_n))
+
+ out_fn = cloud.paths.join(False, '/etc/hosts')
+ templater.render_to_file(tpl_fn_name, out_fn,
+ {'hostname': hostname, 'fqdn': fqdn})
+
+ elif manage_hosts == "localhost":
+ (hostname, fqdn) = util.get_hostname_fqdn(cfg, cloud)
+ if not hostname:
+ log.warn(("Option 'manage_etc_hosts' was set,"
+ " but no hostname was found"))
+ return
+
+ log.debug("Managing localhost in /etc/hosts")
+ cloud.distro.update_etc_hosts(hostname, fqdn)
+ else:
+ log.debug(("Configuration option 'manage_etc_hosts' is not set,"
+ " not managing /etc/hosts in module %s"), name)
diff --git a/cloudinit/config/cc_update_hostname.py b/cloudinit/config/cc_update_hostname.py
new file mode 100644
index 00000000..b84a1a06
--- /dev/null
+++ b/cloudinit/config/cc_update_hostname.py
@@ -0,0 +1,41 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2011 Canonical Ltd.
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.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 os
+
+from cloudinit import util
+from cloudinit.settings import PER_ALWAYS
+
+frequency = PER_ALWAYS
+
+
+def handle(name, cfg, cloud, log, _args):
+ if util.get_cfg_option_bool(cfg, "preserve_hostname", False):
+ log.debug(("Configuration option 'preserve_hostname' is set,"
+ " not updating the hostname in module %s"), name)
+ return
+
+ (hostname, _fqdn) = util.get_hostname_fqdn(cfg, cloud)
+ try:
+ prev_fn = os.path.join(cloud.get_cpath('data'), "previous-hostname")
+ cloud.distro.update_hostname(hostname, prev_fn)
+ except Exception:
+ util.logexc(log, "Failed to set the hostname to %s", hostname)
+ raise