diff options
Diffstat (limited to 'cloudinit/config')
34 files changed, 2780 insertions, 0 deletions
diff --git a/cloudinit/config/__init__.py b/cloudinit/config/__init__.py new file mode 100644 index 00000000..5cd08575 --- /dev/null +++ b/cloudinit/config/__init__.py @@ -0,0 +1,57 @@ +# 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__) + +# TODO remove this from being a prefix?? +TRANSFORM_PREFIX = '' # "cc_" + + +def form_transform_name(name, mod=__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(TRANSFORM_PREFIX): + canon_name = '%s%s' % (TRANSFORM_PREFIX, canon_name) + return ".".join([str(mod), str(canon_name)]) + + +def fixup_transform(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("Transform %s has an unknown frequency %s", mod, freq) + if not hasattr(mod, 'handle'): + def empty_handle(_name, _cfg, _cloud, _log, _args): + pass + setattr(mod, 'handle', empty_handle) + if not hasattr(mod, 'distros'): + setattr(mod, 'distros', None) + return mod diff --git a/cloudinit/config/apt_pipelining.py b/cloudinit/config/apt_pipelining.py new file mode 100644 index 00000000..f460becb --- /dev/null +++ b/cloudinit/config/apt_pipelining.py @@ -0,0 +1,57 @@ +# 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" + +# 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 = ("//Written by cloud-init per 'apt_pipelining'\n" + 'Acquire::http::Pipeline-Depth "%s";\n') % (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/apt_update_upgrade.py b/cloudinit/config/apt_update_upgrade.py new file mode 100644 index 00000000..f5b4b58f --- /dev/null +++ b/cloudinit/config/apt_update_upgrade.py @@ -0,0 +1,243 @@ +# 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" + + +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) + + 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 = "/etc/apt/apt.conf.d/95cloud-init-proxy" + 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] + + +def mirror2lists_fileprefix(mirror): + string = mirror + # take of 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 = "%s/%s" % (lists_d, mirror2lists_fileprefix(omirror)) + nprefix = "%s/%s" % (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'] = util.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/bootcmd.py b/cloudinit/config/bootcmd.py new file mode 100644 index 00000000..635e3a1f --- /dev/null +++ b/cloudinit/config/bootcmd.py @@ -0,0 +1,56 @@ +# 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 +import tempfile + +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 transform named %s," + " no 'bootcmd' key in configuration"), name) + return + + with tempfile.NamedTemporaryFile(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 transform %s"), name) + raise diff --git a/cloudinit/config/byobu.py b/cloudinit/config/byobu.py new file mode 100644 index 00000000..741aa934 --- /dev/null +++ b/cloudinit/config/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 transform 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) diff --git a/cloudinit/config/ca_certs.py b/cloudinit/config/ca_certs.py new file mode 100644 index 00000000..56c41561 --- /dev/null +++ b/cloudinit/config/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'] + + +def update_ca_certs(): + """ + Updates the CA certificate cache on the current machine. + """ + util.subp(["update-ca-certificates"]) + + +def add_ca_certs(cloud, 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 = cloud.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(cloud.paths.join(False, CA_CERT_CONFIG), + "\n%s" % CA_CERT_FILENAME, omode="ab") + + +def remove_default_ca_certs(cloud): + """ + 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(cloud.paths.join(False, CA_CERT_PATH)) + util.delete_dir_contents(cloud.paths.join(False, CA_CERT_SYSTEM_PATH)) + util.write_file(cloud.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 transform 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) + + # 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, trusted_certs) + + # Update the system with the new cert configuration. + log.debug("Updating certificates") + update_ca_certs() diff --git a/cloudinit/config/cc_mcollective.py b/cloudinit/config/cc_mcollective.py new file mode 100644 index 00000000..4cec6494 --- /dev/null +++ b/cloudinit/config/cc_mcollective.py @@ -0,0 +1,97 @@ +# 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 + +from cloudinit import helpers +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 transform 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: + # Create object for reading server.cfg values + mcollective_config = helpers.DefaultingConfigParser() + # Read server.cfg values from original file in order to be able to mix + # the rest up + server_cfg_fn = cloud.paths.join(True, '/etc/mcollective/server.cfg') + old_contents = util.load_file(server_cfg_fn) + # It doesn't contain any sections so just add one temporarily + # Use a hash id based off the contents, + # just incase of conflicts... (try to not have any...) + # This is so that an error won't occur when reading (and no + # sections exist in the file) + section_tpl = "[nullsection_%s]" + attempts = 0 + section_head = section_tpl % (attempts) + while old_contents.find(section_head) != -1: + attempts += 1 + section_head = section_tpl % (attempts) + sectioned_contents = "%s\n%s" % (section_head, old_contents) + mcollective_config.readfp(StringIO(sectioned_contents), + filename=server_cfg_fn) + 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.set(cfg_name, + 'plugin.ssl_server_public', pubcert_fn) + mcollective_config.set(cfg_name, '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.set(cfg_name, + 'plugin.ssl_server_private', pricert_fn) + mcollective_config.set(cfg_name, 'securityprovider', 'ssl') + 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(): + mcollective_config.set(cfg_name, o, v) + # We got all our config as wanted we'll rename + # the previous server.cfg and create our new one + old_fn = "%s.old" % (server_cfg_fn) + util.rename(server_cfg_fn, old_fn) + # Now we got the whole file, write to disk except the section + # we added so that config parser won't error out when trying to read. + # Note below, that we've just used ConfigParser because it generally + # works. Below, we remove the initial 'nullsection' header. + contents = mcollective_config.stringify() + contents = contents.replace("%s\n" % (section_head), "") + util.write_file(server_cfg_fn, contents, mode=0644) + + # Start mcollective + util.subp(['service', 'mcollective', 'start'], capture=False) diff --git a/cloudinit/config/cc_puppet.py b/cloudinit/config/cc_puppet.py new file mode 100644 index 00000000..5fb88bf2 --- /dev/null +++ b/cloudinit/config/cc_puppet.py @@ -0,0 +1,111 @@ +# 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 transform 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(False, '/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 + puppet_conf_old_fn = "%s.old" % (puppet_conf_fn) + util.rename(puppet_conf_fn, puppet_conf_old_fn) + util.write_file(puppet_conf_fn, 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/chef.py b/cloudinit/config/chef.py new file mode 100644 index 00000000..4e8ef346 --- /dev/null +++ b/cloudinit/config/chef.py @@ -0,0 +1,128 @@ +# 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 transform 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']) + 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']) + else: + util.subp(['/usr/bin/gem', 'install', 'chef', + '--no-ri', '--no-rdoc', '--bindir', + '/usr/bin', '-q']) diff --git a/cloudinit/config/disable_ec2_metadata.py b/cloudinit/config/disable_ec2_metadata.py new file mode 100644 index 00000000..c7d26029 --- /dev/null +++ b/cloudinit/config/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) + else: + log.debug(("Skipping transform named %s," + " disabling the ec2 route not enabled"), name) diff --git a/cloudinit/config/final_message.py b/cloudinit/config/final_message.py new file mode 100644 index 00000000..c257b6d0 --- /dev/null +++ b/cloudinit/config/final_message.py @@ -0,0 +1,71 @@ +# 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 sys + +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, + } + # Use stdout, stderr or the logger?? + content = templater.render_string(msg_in, subs) + sys.stderr.write("%s\n" % (content)) + 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/foo.py b/cloudinit/config/foo.py new file mode 100644 index 00000000..99135704 --- /dev/null +++ b/cloudinit/config/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 transform %s", name) diff --git a/cloudinit/config/grub_dpkg.py b/cloudinit/config/grub_dpkg.py new file mode 100644 index 00000000..02f05ce3 --- /dev/null +++ b/cloudinit/config/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 == None: + idevs = "" + if idevs_empty == None: + idevs_empty = "true" + else: + if idevs_empty == None: + idevs_empty = "false" + if idevs == 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/keys_to_console.py b/cloudinit/config/keys_to_console.py new file mode 100644 index 00000000..40758198 --- /dev/null +++ b/cloudinit/config/keys_to_console.py @@ -0,0 +1,52 @@ +# 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 transform %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.write_file(cloud.paths.join(False, '/dev/console'), stdout) + except: + log.warn("Writing keys to /dev/console failed!") + raise diff --git a/cloudinit/config/landscape.py b/cloudinit/config/landscape.py new file mode 100644 index 00000000..29ce41b9 --- /dev/null +++ b/cloudinit/config/landscape.py @@ -0,0 +1,97 @@ +# 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 + +try: + from configobj import ConfigObj +except ImportError: + ConfigObj = None + +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 + """ + if not ConfigObj: + log.warn(("'ConfigObj' support not available," + " running transform %s disabled"), name) + return + + ls_cloudcfg = cfg.get("landscape", {}) + + if not isinstance(ls_cloudcfg, dict): + raise Exception(("'landscape' key existed in config," + " but not a dictionary type," + " is a %s instead"), util.obj_name(ls_cloudcfg)) + + lsc_client_fn = cloud.paths.join(True, LSC_CLIENT_CFG_FILE) + merged = merge_together([LSC_BUILTIN_CFG, lsc_client_fn, ls_cloudcfg]) + + 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/locale.py b/cloudinit/config/locale.py new file mode 100644 index 00000000..7f273123 --- /dev/null +++ b/cloudinit/config/locale.py @@ -0,0 +1,58 @@ +# 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 templater +from cloudinit import util + + +def apply_locale(locale, cfgfile, cloud, log): + # TODO this command might not work on RH... + if os.path.exists('/usr/sbin/locale-gen'): + util.subp(['locale-gen', locale], capture=False) + if os.path.exists('/usr/sbin/update-locale'): + util.subp(['update-locale', locale], capture=False) + if not cfgfile: + return + template_fn = cloud.get_template_filename('default-locale') + if not template_fn: + log.warn("No template filename found to write to %s", cfgfile) + else: + templater.render_to_file(template_fn, cfgfile, {'locale': locale}) + + +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()) + + locale_cfgfile = util.get_cfg_option_str(cfg, "locale_configfile", + "/etc/default/locale") + + if not locale: + log.debug(("Skipping transform named %s, " + "no 'locale' configuration found"), name) + return + + log.debug("Setting locale to %s", locale) + + apply_locale(locale, locale_cfgfile, cloud, log) diff --git a/cloudinit/config/mounts.py b/cloudinit/config/mounts.py new file mode 100644 index 00000000..700fbc44 --- /dev/null +++ b/cloudinit/config/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: + try: + util.ensure_dir(cloud.paths.join(False, d)) + 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/phone_home.py b/cloudinit/config/phone_home.py new file mode 100644 index 00000000..a8752527 --- /dev/null +++ b/cloudinit/config/phone_home.py @@ -0,0 +1,111 @@ +# 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: + return + ph_cfg = cfg['phone_home'] + + if 'url' not in ph_cfg: + log.warn(("Skipping transform 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', 10) + 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/resizefs.py b/cloudinit/config/resizefs.py new file mode 100644 index 00000000..1690094a --- /dev/null +++ b/cloudinit/config/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 transform 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.SilentTemporaryFile(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/rightscale_userdata.py b/cloudinit/config/rightscale_userdata.py new file mode 100644 index 00000000..8385e281 --- /dev/null +++ b/cloudinit/config/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 transform %s", name) + return + + try: + mdict = parse_qs(ud) + if not mdict or not MY_HOOKNAME in mdict: + log.debug(("Skipping transform %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 transform 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/rsyslog.py b/cloudinit/config/rsyslog.py new file mode 100644 index 00000000..f2c1de1e --- /dev/null +++ b/cloudinit/config/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 transform 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/runcmd.py b/cloudinit/config/runcmd.py new file mode 100644 index 00000000..f121484b --- /dev/null +++ b/cloudinit/config/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 transform 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/salt_minion.py b/cloudinit/config/salt_minion.py new file mode 100644 index 00000000..16f5286d --- /dev/null +++ b/cloudinit/config/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 transform 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"]) + + # Ensure we can configure files at the right dir + config_dir = salt_cfg.get("config_dir", '/etc/salt') + config_dir = cloud.paths.join(False, config_dir) + 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 = salt_cfg.get('pki_dir', '/etc/salt/pki') + pki_dir = cloud.paths.join(pki_dir) + 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/scripts_per_boot.py b/cloudinit/config/scripts_per_boot.py new file mode 100644 index 00000000..364e1d02 --- /dev/null +++ b/cloudinit/config/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 transform %s (%s in %s)", + name, script_subdir, runparts_path) + raise diff --git a/cloudinit/config/scripts_per_instance.py b/cloudinit/config/scripts_per_instance.py new file mode 100644 index 00000000..d75ab47d --- /dev/null +++ b/cloudinit/config/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 transform %s (%s in %s)", + name, script_subdir, runparts_path) + raise diff --git a/cloudinit/config/scripts_per_once.py b/cloudinit/config/scripts_per_once.py new file mode 100644 index 00000000..80f8c325 --- /dev/null +++ b/cloudinit/config/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 transform %s (%s in %s)", + name, script_subdir, runparts_path) + raise diff --git a/cloudinit/config/scripts_user.py b/cloudinit/config/scripts_user.py new file mode 100644 index 00000000..f4fe3a2a --- /dev/null +++ b/cloudinit/config/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 transform %s (%s in %s)", + name, script_subdir, runparts_path) + raise diff --git a/cloudinit/config/set_hostname.py b/cloudinit/config/set_hostname.py new file mode 100644 index 00000000..3ac8a8fa --- /dev/null +++ b/cloudinit/config/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 transform %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/set_passwords.py b/cloudinit/config/set_passwords.py new file mode 100644 index 00000000..e7049f22 --- /dev/null +++ b/cloudinit/config/set_passwords.py @@ -0,0 +1,151 @@ +# 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 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_str(cfg['ssh_pwauth']): + pw_auth = 'yes' + if util.is_false_str(cfg['ssh_pwauth']): + pw_auth = 'no' + + if change_pwauth: + new_lines = [] + replaced_auth = False + replacement = "PasswordAuthentication %s" % (pw_auth) + + # See http://linux.die.net/man/5/sshd_config + old_lines = util.load_file('/etc/ssh/sshd_config').splitlines() + for i, line in enumerate(old_lines): + if not line.strip() or line.startswith("#"): + new_lines.append(line) + continue + splitup = line.split(None, 1) + if len(splitup) <= 1: + new_lines.append(line) + continue + (cmd, args) = splitup + # Keywords are case-insensitive and arguments are case-sensitive + cmd = cmd.lower().strip() + if cmd == 'passwordauthentication': + log.debug("Replacing auth line %s with %s", i + 1, replacement) + replaced_auth = True + new_lines.append(replacement) + else: + new_lines.append(line) + + if not replaced_auth: + log.debug("Adding new auth line %s", replacement) + replaced_auth = True + new_lines.append(replacement) + + util.write_file(cloud.paths.join(False, '/etc/ssh/sshd_config'), + "\n".join(new_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/ssh.py b/cloudinit/config/ssh.py new file mode 100644 index 00000000..e5e99560 --- /dev/null +++ b/cloudinit/config/ssh.py @@ -0,0 +1,131 @@ +# 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\"") + +key2file = { + "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), +} + +priv2pub = { + '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_keys = ['rsa', 'dsa', 'ecdsa'] + + +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 key2file: + tgt_fn = key2file[key][0] + tgt_perms = key2file[key][1] + util.write_file(cloud.paths.join(False, tgt_fn), + val, tgt_perms) + + for (priv, pub) in priv2pub.iteritems(): + if pub in cfg['ssh_keys'] or not priv in cfg['ssh_keys']: + continue + pair = (key2file[priv][0], key2file[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_keys) + for keytype in genkeys: + keyfile = '/etc/ssh/ssh_host_%s_key' % (keytype) + keyfile = cloud.paths.join(False, keyfile) + 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/ssh_import_id.py b/cloudinit/config/ssh_import_id.py new file mode 100644 index 00000000..d57e4665 --- /dev/null +++ b/cloudinit/config/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 transform named %s, no ids found to import", name) + return + + if not user: + log.debug("Skipping transform 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/timezone.py b/cloudinit/config/timezone.py new file mode 100644 index 00000000..747c436c --- /dev/null +++ b/cloudinit/config/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 transform named %s, no 'timezone' specified", name) + return + + # Let the distro handle settings its timezone + cloud.distro.set_timezone(timezone) diff --git a/cloudinit/config/update_etc_hosts.py b/cloudinit/config/update_etc_hosts.py new file mode 100644 index 00000000..75615db1 --- /dev/null +++ b/cloudinit/config/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 Exception(("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 transform %s"), name) diff --git a/cloudinit/config/update_hostname.py b/cloudinit/config/update_hostname.py new file mode 100644 index 00000000..58444fab --- /dev/null +++ b/cloudinit/config/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 transform %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 |