summaryrefslogtreecommitdiff
path: root/cloudinit/config
diff options
context:
space:
mode:
authorScott Moser <smoser@ubuntu.com>2012-07-06 17:19:37 -0400
committerScott Moser <smoser@ubuntu.com>2012-07-06 17:19:37 -0400
commitb2a21ed1dc682a262d55a4202c6b9606496d211f (patch)
tree37f1c4acd3ea891c1e7a60bcd9dd8aa8c7ca1e0c /cloudinit/config
parent646384ccd6f1707b2712d9bcd683ae877f1903bd (diff)
parente7095a1b19e849c530650d2d71edf8b28d30f1d1 (diff)
downloadvyos-cloud-init-b2a21ed1dc682a262d55a4202c6b9606496d211f.tar.gz
vyos-cloud-init-b2a21ed1dc682a262d55a4202c6b9606496d211f.zip
Merge rework branch in [Joshua Harlow]
- unified binary that activates the various stages - Now using argparse + subcommands to specify the various CLI options - a stage module that clearly separates the stages of the different components (also described how they are used and in what order in the new unified binary) - user_data is now a module that just does user data processing while the actual activation and 'handling' of the processed user data is done via a separate set of files (and modules) with the main 'init' stage being the controller of this - creation of boot_hook, cloud_config, shell_script, upstart_job version 2 modules (with classes that perform there functionality) instead of those having functionality that is attached to the cloudinit object (which reduces reuse and limits future functionality, and makes testing harder) - removal of global config that defined paths, shared config, now this is via objects making unit testing testing and global side-effects a non issue - creation of a 'helpers.py' - this contains an abstraction for the 'lock' like objects that the various module/handler running stages use to avoid re-running a given module/handler for a given frequency. this makes it separated from the actual usage of that object (thus helpful for testing and clear lines usage and how the actual job is accomplished) - a common 'runner' class is the main entrypoint using these locks to run function objects passed in (along with there arguments) and there frequency - add in a 'paths' object that provides access to the previously global and/or config based paths (thus providing a single entrypoint object/type that provides path information) - this also adds in the ability to change the path when constructing that path 'object' and adding in additional config that can be used to alter the root paths of 'joins' (useful for testing or possibly useful in chroots?) - config options now avaiable that can alter the 'write_root' and the 'read_root' when backing code uses the paths join() function - add a config parser subclass that will automatically add unknown sections and return default values (instead of throwing exceptions for these cases) - a new config merging class that will be the central object that knows how to do the common configuration merging from the various configuration sources. The order is the following: - cli config files override environment config files which override instance configs which override datasource configs which override base configuration which overrides default configuration. - remove the passing around of the 'cloudinit' object as a 'cloud' variable and instead pass around an 'interface' object that can be given to modules and handlers as there cloud access layer while the backing of that object can be varied (good for abstraction and testing) - use a single set of functions to do importing of modules - add a function in which will search for a given set of module names with a given set of attributes and return those which are found - refactor logging so that instead of using a single top level 'log' that instead each component/module can use its own logger (if desired), this should be backwards compatible with handlers and config modules that used the passed in logger (its still passed in) - ensure that all places where exception are caught and where applicable that the util logexc() is called, so that no exceptions that may occur are dropped without first being logged (where it makes sense for this to happen) - add a 'requires' file that lists cloud-init dependencies - applying it in package creation (bdeb and brpm) as well as using it in the modified setup.py to ensure dependencies are installed when using that method of packaging - add a 'version.py' that lists the active version (in code) so that code inside cloud-init can report the version in messaging and other config files - cleanup of subprocess usage so that all subprocess calls go through the subp() utility method, which now has an exception type that will provide detailed information on python 2.6 and 2.7 - forced all code loading, moving, chmod, writing files and other system level actions to go through standard set of util functions, this greatly helps in debugging and determining exactly which system actions cloud-init is performing - switching out the templating engine cheetah for tempita since tempita has no external dependencies (minus python) while cheetah has many dependencies which makes it more difficult to adopt cloud-init in distros that may not have those dependencies - adjust url fetching and url trying to go through a single function that reads urls in the new 'url helper' file, this helps in tracing, debugging and knowing which urls are being called and/or posted to from with-in cloud-init code - add in the sending of a 'User-Agent' header for all urls fetched that do not provide there own header mapping, derive this user-agent from the following template, 'Cloud-Init/{version}' where the version is the cloud-init version number - using prettytable for netinfo 'debug' printing since it provides a standard and defined output that should be easier to parse than a custom format - add a set of distro specific classes, that handle distro specific actions that modules and or handler code can use as needed, this is organized into a base abstract class with child classes that implement the shared functionality. config determines exactly which subclass to load, so it can be easily extended as needed. - current functionality - network interface config file writing - hostname setting/updating - locale/timezone/ setting - updating of /etc/hosts (with templates or generically) - package commands (ie installing, removing)/mirror finding - interface up/down activating - implemented a debian + ubuntu subclass - implemented a redhat + fedora subclass - adjust the root 'cloud.cfg' file to now have distrobution/path specific configuration values in it. these special configs are merged as the normal config is, but the system level config is not passed into modules/handlers - modules/handlers must go through the path and distro object instead - have the cloudstack datasource test the url before calling into boto to avoid the long wait for boto to finish retrying and finally fail when the gateway meta-data address is unavailable - add a simple mock ec2 meta-data python based http server that can serve a very simple set of ec2 meta-data back to callers - useful for testing or for understanding what the ec2 meta-data service can provide in terms of data or functionality - for ssh key and authorized key file parsing add in classes and util functions that maintain the state of individual lines, allowing for a clearer separation of parsing and modification (useful for testing and tracing) - add a set of 'base' init.d scripts that can be used on systems that do not have full upstart or systemd support (or support that does not match the standard fedora/ubuntu implementation) - currently these are being tested on RHEL 6.2 - separate the datasources into there own subdirectory (instead of being a top-level item), this matches how config 'modules' and user-data 'handlers' are also in there own subdirectory (thus helping new developers and others understand the code layout in a quicker manner) - add the building of rpms based off a new cli tool and template 'spec' file that will templatize and perform the necessary commands to create a source and binary package to be used with a cloud-init install on a 'rpm' supporting system - uses the new standard set of requires and converts those pypi requirements into a local set of package requirments (that are known to exist on RHEL systems but should also exist on fedora systems) - adjust the bdeb builder to be a python script (instead of a shell script) and make its 'control' file a template that takes in the standard set of pypi dependencies and uses a local mapping (known to work on ubuntu) to create the packages set of dependencies (that should also work on ubuntu-like systems) - pythonify a large set of various pieces of code - remove wrapping return statements with () when it has no effect - upper case all constants used - correctly 'case' class and method names (where applicable) - use os.path.join (and similar commands) instead of custom path creation - use 'is None' instead of the frowned upon '== None' which picks up a large set of 'true' cases than is typically desired (ie for objects that have there own equality) - use context managers on locks, tempdir, chdir, file, selinux, umask, unmounting commands so that these actions do not have to be closed and/or cleaned up manually in finally blocks, which is typically not done and will eventually be a bug in the future - use the 'abc' module for abstract classes base where possible - applied in the datasource root class, the distro root class, and the user-data v2 root class - when loading yaml, check that the 'root' type matches a predefined set of valid types (typically just 'dict') and throw a type error if a mismatch occurs, this seems to be a good idea to do when loading user config files - when forking a long running task (ie resizing a filesytem) use a new util function that will fork and then call a callback, instead of having to implement all that code in a non-shared location (thus allowing it to be used by others in the future) - when writing out filenames, go through a util function that will attempt to ensure that the given filename is 'filesystem' safe by replacing '/' with '_' and removing characters which do not match a given whitelist of allowed filename characters - for the varying usages of the 'blkid' command make a function in the util module that can be used as the single point of entry for interaction with that command (and its results) instead of having X separate implementations - place the rfc 8222 time formatting and uptime repeated pieces of code in the util module as a set of function with the name 'time_rfc2822'/'uptime' - separate the pylint+pep8 calling from one tool into two indivudal tools so that they can be called independently, add make file sections that can be used to call these independently - remove the support for the old style config that was previously located in '/etc/ec2-init/ec2-config.cfg', no longer supported! - instead of using a altered config parser that added its own 'dummy' section on in the 'mcollective' module, use configobj which handles the parsing of config without sections better (and it also maintains comments instead of removing them) - use the new defaulting config parser (that will not raise errors on sections that do not exist or return errors when values are fetched that do not exist) in the 'puppet' module - for config 'modules' add in the ability for the module to provide a list of distro names which it is known to work with, if when ran and the distro being used name does not match one of those in this list, a warning will be written out saying that this module may not work correctly on this distrobution - for all dynamically imported modules ensure that they are fixed up before they are used by ensuring that they have certain attributes, if they do not have those attributes they will be set to a sensible set of defaults instead - adjust all 'config' modules and handlers to use the adjusted util functions and the new distro objects where applicable so that those pieces of code can benefit from the unified and enhanced functionality being provided in that util module - fix a potential bug whereby when a #includeonce was encountered it would enable checking of urls against a cache, if later a #include was encountered it would continue checking against that cache, instead of refetching (which would likely be the expected case) - add a openstack/nova based pep8 extension utility ('hacking.py') that allows for custom checks (along with the standard pep8 checks) to occur when running 'make pep8' and its derivatives
Diffstat (limited to 'cloudinit/config')
-rw-r--r--cloudinit/config/__init__.py56
-rw-r--r--cloudinit/config/cc_apt_pipelining.py59
-rw-r--r--cloudinit/config/cc_apt_update_upgrade.py272
-rw-r--r--cloudinit/config/cc_bootcmd.py55
-rw-r--r--cloudinit/config/cc_byobu.py71
-rw-r--r--cloudinit/config/cc_ca_certs.py99
-rw-r--r--cloudinit/config/cc_chef.py129
-rw-r--r--cloudinit/config/cc_disable_ec2_metadata.py36
-rw-r--r--cloudinit/config/cc_final_message.py68
-rw-r--r--cloudinit/config/cc_foo.py52
-rw-r--r--cloudinit/config/cc_grub_dpkg.py67
-rw-r--r--cloudinit/config/cc_keys_to_console.py53
-rw-r--r--cloudinit/config/cc_landscape.py95
-rw-r--r--cloudinit/config/cc_locale.py37
-rw-r--r--cloudinit/config/cc_mcollective.py91
-rw-r--r--cloudinit/config/cc_mounts.py200
-rw-r--r--cloudinit/config/cc_phone_home.py118
-rw-r--r--cloudinit/config/cc_puppet.py113
-rw-r--r--cloudinit/config/cc_resizefs.py140
-rw-r--r--cloudinit/config/cc_rightscale_userdata.py102
-rw-r--r--cloudinit/config/cc_rsyslog.py102
-rw-r--r--cloudinit/config/cc_runcmd.py38
-rw-r--r--cloudinit/config/cc_salt_minion.py60
-rw-r--r--cloudinit/config/cc_scripts_per_boot.py41
-rw-r--r--cloudinit/config/cc_scripts_per_instance.py41
-rw-r--r--cloudinit/config/cc_scripts_per_once.py41
-rw-r--r--cloudinit/config/cc_scripts_user.py42
-rw-r--r--cloudinit/config/cc_set_hostname.py35
-rw-r--r--cloudinit/config/cc_set_passwords.py146
-rw-r--r--cloudinit/config/cc_ssh.py132
-rw-r--r--cloudinit/config/cc_ssh_import_id.py53
-rw-r--r--cloudinit/config/cc_timezone.py39
-rw-r--r--cloudinit/config/cc_update_etc_hosts.py60
-rw-r--r--cloudinit/config/cc_update_hostname.py41
34 files changed, 2784 insertions, 0 deletions
diff --git a/cloudinit/config/__init__.py b/cloudinit/config/__init__.py
new file mode 100644
index 00000000..69a8cc68
--- /dev/null
+++ b/cloudinit/config/__init__.py
@@ -0,0 +1,56 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2008-2010 Canonical Ltd.
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+# Author: Chuck Short <chuck.short@canonical.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3, as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+from cloudinit.settings import (PER_INSTANCE, FREQUENCIES)
+
+from cloudinit import log as logging
+
+LOG = logging.getLogger(__name__)
+
+# This prefix is used to make it less
+# of a chance that when importing
+# we will not find something else with the same
+# name in the lookup path...
+MOD_PREFIX = "cc_"
+
+
+def form_module_name(name):
+ canon_name = name.replace("-", "_")
+ if canon_name.lower().endswith(".py"):
+ canon_name = canon_name[0:(len(canon_name) - 3)]
+ canon_name = canon_name.strip()
+ if not canon_name:
+ return None
+ if not canon_name.startswith(MOD_PREFIX):
+ canon_name = '%s%s' % (MOD_PREFIX, canon_name)
+ return canon_name
+
+
+def fixup_module(mod, def_freq=PER_INSTANCE):
+ if not hasattr(mod, 'frequency'):
+ setattr(mod, 'frequency', def_freq)
+ else:
+ freq = mod.frequency
+ if freq and freq not in FREQUENCIES:
+ LOG.warn("Module %s has an unknown frequency %s", mod, freq)
+ if not hasattr(mod, 'distros'):
+ setattr(mod, 'distros', None)
+ return mod
diff --git a/cloudinit/config/cc_apt_pipelining.py b/cloudinit/config/cc_apt_pipelining.py
new file mode 100644
index 00000000..3426099e
--- /dev/null
+++ b/cloudinit/config/cc_apt_pipelining.py
@@ -0,0 +1,59 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2011 Canonical Ltd.
+#
+# Author: Ben Howard <ben.howard@canonical.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3, as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from cloudinit import util
+from cloudinit.settings import PER_INSTANCE
+
+frequency = PER_INSTANCE
+
+distros = ['ubuntu', 'debian']
+
+DEFAULT_FILE = "/etc/apt/apt.conf.d/90cloud-init-pipelining"
+
+APT_PIPE_TPL = ("//Written by cloud-init per 'apt_pipelining'\n"
+ 'Acquire::http::Pipeline-Depth "%s";\n')
+
+# Acquire::http::Pipeline-Depth can be a value
+# from 0 to 5 indicating how many outstanding requests APT should send.
+# A value of zero MUST be specified if the remote host does not properly linger
+# on TCP connections - otherwise data corruption will occur.
+
+
+def handle(_name, cfg, cloud, log, _args):
+
+ apt_pipe_value = util.get_cfg_option_str(cfg, "apt_pipelining", False)
+ apt_pipe_value_s = str(apt_pipe_value).lower().strip()
+
+ if apt_pipe_value_s == "false":
+ write_apt_snippet(cloud, "0", log, DEFAULT_FILE)
+ elif apt_pipe_value_s in ("none", "unchanged", "os"):
+ return
+ elif apt_pipe_value_s in [str(b) for b in xrange(0, 6)]:
+ write_apt_snippet(cloud, apt_pipe_value_s, log, DEFAULT_FILE)
+ else:
+ log.warn("Invalid option for apt_pipeling: %s", apt_pipe_value)
+
+
+def write_apt_snippet(cloud, setting, log, f_name):
+ """ Writes f_name with apt pipeline depth 'setting' """
+
+ file_contents = APT_PIPE_TPL % (setting)
+
+ util.write_file(cloud.paths.join(False, f_name), file_contents)
+
+ log.debug("Wrote %s with apt pipeline depth setting %s", f_name, setting)
diff --git a/cloudinit/config/cc_apt_update_upgrade.py b/cloudinit/config/cc_apt_update_upgrade.py
new file mode 100644
index 00000000..5c5e510c
--- /dev/null
+++ b/cloudinit/config/cc_apt_update_upgrade.py
@@ -0,0 +1,272 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2009-2010 Canonical Ltd.
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3, as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import glob
+import os
+
+from cloudinit import templater
+from cloudinit import util
+
+distros = ['ubuntu', 'debian']
+
+PROXY_TPL = "Acquire::HTTP::Proxy \"%s\";\n"
+PROXY_FN = "/etc/apt/apt.conf.d/95cloud-init-proxy"
+
+# A temporary shell program to get a given gpg key
+# from a given keyserver
+EXPORT_GPG_KEYID = """
+ k=${1} ks=${2};
+ exec 2>/dev/null
+ [ -n "$k" ] || exit 1;
+ armour=$(gpg --list-keys --armour "${k}")
+ if [ -z "${armour}" ]; then
+ gpg --keyserver ${ks} --recv $k >/dev/null &&
+ armour=$(gpg --export --armour "${k}") &&
+ gpg --batch --yes --delete-keys "${k}"
+ fi
+ [ -n "${armour}" ] && echo "${armour}"
+"""
+
+
+def handle(name, cfg, cloud, log, _args):
+ update = util.get_cfg_option_bool(cfg, 'apt_update', False)
+ upgrade = util.get_cfg_option_bool(cfg, 'apt_upgrade', False)
+
+ release = get_release()
+ mirror = find_apt_mirror(cloud, cfg)
+ if not mirror:
+ log.debug(("Skipping module named %s,"
+ " no package 'mirror' located"), name)
+ return
+
+ log.debug("Selected mirror at: %s" % mirror)
+
+ if not util.get_cfg_option_bool(cfg,
+ 'apt_preserve_sources_list', False):
+ generate_sources_list(release, mirror, cloud, log)
+ old_mir = util.get_cfg_option_str(cfg, 'apt_old_mirror',
+ "archive.ubuntu.com/ubuntu")
+ rename_apt_lists(old_mir, mirror)
+
+ # Set up any apt proxy
+ proxy = cfg.get("apt_proxy", None)
+ proxy_filename = PROXY_FN
+ if proxy:
+ try:
+ # See man 'apt.conf'
+ contents = PROXY_TPL % (proxy)
+ util.write_file(cloud.paths.join(False, proxy_filename),
+ contents)
+ except Exception as e:
+ util.logexc(log, "Failed to write proxy to %s", proxy_filename)
+ elif os.path.isfile(proxy_filename):
+ util.del_file(proxy_filename)
+
+ # Process 'apt_sources'
+ if 'apt_sources' in cfg:
+ errors = add_sources(cloud, cfg['apt_sources'],
+ {'MIRROR': mirror, 'RELEASE': release})
+ for e in errors:
+ log.warn("Source Error: %s", ':'.join(e))
+
+ dconf_sel = util.get_cfg_option_str(cfg, 'debconf_selections', False)
+ if dconf_sel:
+ log.debug("setting debconf selections per cloud config")
+ try:
+ util.subp(('debconf-set-selections', '-'), dconf_sel)
+ except:
+ util.logexc(log, "Failed to run debconf-set-selections")
+
+ pkglist = util.get_cfg_option_list(cfg, 'packages', [])
+
+ errors = []
+ if update or len(pkglist) or upgrade:
+ try:
+ cloud.distro.update_package_sources()
+ except Exception as e:
+ util.logexc(log, "Package update failed")
+ errors.append(e)
+
+ if upgrade:
+ try:
+ cloud.distro.package_command("upgrade")
+ except Exception as e:
+ util.logexc(log, "Package upgrade failed")
+ errors.append(e)
+
+ if len(pkglist):
+ try:
+ cloud.distro.install_packages(pkglist)
+ except Exception as e:
+ util.logexc(log, "Failed to install packages: %s ", pkglist)
+ errors.append(e)
+
+ if len(errors):
+ log.warn("%s failed with exceptions, re-raising the last one",
+ len(errors))
+ raise errors[-1]
+
+
+# get gpg keyid from keyserver
+def getkeybyid(keyid, keyserver):
+ with util.ExtendedTemporaryFile(suffix='.sh') as fh:
+ fh.write(EXPORT_GPG_KEYID)
+ fh.flush()
+ cmd = ['/bin/sh', fh.name, keyid, keyserver]
+ (stdout, _stderr) = util.subp(cmd)
+ return stdout.strip()
+
+
+def mirror2lists_fileprefix(mirror):
+ string = mirror
+ # take off http:// or ftp://
+ if string.endswith("/"):
+ string = string[0:-1]
+ pos = string.find("://")
+ if pos >= 0:
+ string = string[pos + 3:]
+ string = string.replace("/", "_")
+ return string
+
+
+def rename_apt_lists(omirror, new_mirror, lists_d="/var/lib/apt/lists"):
+ oprefix = os.path.join(lists_d, mirror2lists_fileprefix(omirror))
+ nprefix = os.path.join(lists_d, mirror2lists_fileprefix(new_mirror))
+ if oprefix == nprefix:
+ return
+ olen = len(oprefix)
+ for filename in glob.glob("%s_*" % oprefix):
+ # TODO use the cloud.paths.join...
+ util.rename(filename, "%s%s" % (nprefix, filename[olen:]))
+
+
+def get_release():
+ (stdout, _stderr) = util.subp(['lsb_release', '-cs'])
+ return stdout.strip()
+
+
+def generate_sources_list(codename, mirror, cloud, log):
+ template_fn = cloud.get_template_filename('sources.list')
+ if template_fn:
+ params = {'mirror': mirror, 'codename': codename}
+ out_fn = cloud.paths.join(False, '/etc/apt/sources.list')
+ templater.render_to_file(template_fn, out_fn, params)
+ else:
+ log.warn("No template found, not rendering /etc/apt/sources.list")
+
+
+def add_sources(cloud, srclist, template_params=None):
+ """
+ add entries in /etc/apt/sources.list.d for each abbreviated
+ sources.list entry in 'srclist'. When rendering template, also
+ include the values in dictionary searchList
+ """
+ if template_params is None:
+ template_params = {}
+
+ errorlist = []
+ for ent in srclist:
+ if 'source' not in ent:
+ errorlist.append(["", "missing source"])
+ continue
+
+ source = ent['source']
+ if source.startswith("ppa:"):
+ try:
+ util.subp(["add-apt-repository", source])
+ except:
+ errorlist.append([source, "add-apt-repository failed"])
+ continue
+
+ source = templater.render_string(source, template_params)
+
+ if 'filename' not in ent:
+ ent['filename'] = 'cloud_config_sources.list'
+
+ if not ent['filename'].startswith("/"):
+ ent['filename'] = os.path.join("/etc/apt/sources.list.d/",
+ ent['filename'])
+
+ if ('keyid' in ent and 'key' not in ent):
+ ks = "keyserver.ubuntu.com"
+ if 'keyserver' in ent:
+ ks = ent['keyserver']
+ try:
+ ent['key'] = getkeybyid(ent['keyid'], ks)
+ except:
+ errorlist.append([source, "failed to get key from %s" % ks])
+ continue
+
+ if 'key' in ent:
+ try:
+ util.subp(('apt-key', 'add', '-'), ent['key'])
+ except:
+ errorlist.append([source, "failed add key"])
+
+ try:
+ contents = "%s\n" % (source)
+ util.write_file(cloud.paths.join(False, ent['filename']),
+ contents, omode="ab")
+ except:
+ errorlist.append([source,
+ "failed write to file %s" % ent['filename']])
+
+ return errorlist
+
+
+def find_apt_mirror(cloud, cfg):
+ """ find an apt_mirror given the cloud and cfg provided """
+
+ mirror = None
+
+ cfg_mirror = cfg.get("apt_mirror", None)
+ if cfg_mirror:
+ mirror = cfg["apt_mirror"]
+ elif "apt_mirror_search" in cfg:
+ mirror = util.search_for_mirror(cfg['apt_mirror_search'])
+ else:
+ mirror = cloud.get_local_mirror()
+
+ mydom = ""
+
+ doms = []
+
+ if not mirror:
+ # if we have a fqdn, then search its domain portion first
+ (_hostname, fqdn) = util.get_hostname_fqdn(cfg, cloud)
+ mydom = ".".join(fqdn.split(".")[1:])
+ if mydom:
+ doms.append(".%s" % mydom)
+
+ if not mirror:
+ doms.extend((".localdomain", "",))
+
+ mirror_list = []
+ distro = cloud.distro.name
+ mirrorfmt = "http://%s-mirror%s/%s" % (distro, "%s", distro)
+ for post in doms:
+ mirror_list.append(mirrorfmt % (post))
+
+ mirror = util.search_for_mirror(mirror_list)
+
+ if not mirror:
+ mirror = cloud.distro.get_package_mirror()
+
+ return mirror
diff --git a/cloudinit/config/cc_bootcmd.py b/cloudinit/config/cc_bootcmd.py
new file mode 100644
index 00000000..bae1ea54
--- /dev/null
+++ b/cloudinit/config/cc_bootcmd.py
@@ -0,0 +1,55 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2009-2011 Canonical Ltd.
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3, as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+
+from cloudinit import util
+from cloudinit.settings import PER_ALWAYS
+
+frequency = PER_ALWAYS
+
+
+def handle(name, cfg, cloud, log, _args):
+
+ if "bootcmd" not in cfg:
+ log.debug(("Skipping module named %s,"
+ " no 'bootcmd' key in configuration"), name)
+ return
+
+ with util.ExtendedTemporaryFile(suffix=".sh") as tmpf:
+ try:
+ content = util.shellify(cfg["bootcmd"])
+ tmpf.write(content)
+ tmpf.flush()
+ except:
+ util.logexc(log, "Failed to shellify bootcmd")
+ raise
+
+ try:
+ env = os.environ.copy()
+ iid = cloud.get_instance_id()
+ if iid:
+ env['INSTANCE_ID'] = str(iid)
+ cmd = ['/bin/sh', tmpf.name]
+ util.subp(cmd, env=env, capture=False)
+ except:
+ util.logexc(log,
+ ("Failed to run bootcmd module %s"), name)
+ raise
diff --git a/cloudinit/config/cc_byobu.py b/cloudinit/config/cc_byobu.py
new file mode 100644
index 00000000..4e2e06bb
--- /dev/null
+++ b/cloudinit/config/cc_byobu.py
@@ -0,0 +1,71 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2009-2010 Canonical Ltd.
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3, as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from cloudinit import util
+
+distros = ['ubuntu', 'debian']
+
+
+def handle(name, cfg, _cloud, log, args):
+ if len(args) != 0:
+ value = args[0]
+ else:
+ value = util.get_cfg_option_str(cfg, "byobu_by_default", "")
+
+ if not value:
+ log.debug("Skipping module named %s, no 'byobu' values found", name)
+ return
+
+ if value == "user" or value == "system":
+ value = "enable-%s" % value
+
+ valid = ("enable-user", "enable-system", "enable",
+ "disable-user", "disable-system", "disable")
+ if not value in valid:
+ log.warn("Unknown value %s for byobu_by_default", value)
+
+ mod_user = value.endswith("-user")
+ mod_sys = value.endswith("-system")
+ if value.startswith("enable"):
+ bl_inst = "install"
+ dc_val = "byobu byobu/launch-by-default boolean true"
+ mod_sys = True
+ else:
+ if value == "disable":
+ mod_user = True
+ mod_sys = True
+ bl_inst = "uninstall"
+ dc_val = "byobu byobu/launch-by-default boolean false"
+
+ shcmd = ""
+ if mod_user:
+ user = util.get_cfg_option_str(cfg, "user", "ubuntu")
+ shcmd += " sudo -Hu \"%s\" byobu-launcher-%s" % (user, bl_inst)
+ shcmd += " || X=$(($X+1)); "
+ if mod_sys:
+ shcmd += "echo \"%s\" | debconf-set-selections" % dc_val
+ shcmd += " && dpkg-reconfigure byobu --frontend=noninteractive"
+ shcmd += " || X=$(($X+1)); "
+
+ cmd = ["/bin/sh", "-c", "%s %s %s" % ("X=0;", shcmd, "exit $X")]
+
+ log.debug("Setting byobu to %s", value)
+
+ util.subp(cmd, capture=False)
diff --git a/cloudinit/config/cc_ca_certs.py b/cloudinit/config/cc_ca_certs.py
new file mode 100644
index 00000000..dc046bda
--- /dev/null
+++ b/cloudinit/config/cc_ca_certs.py
@@ -0,0 +1,99 @@
+# vi: ts=4 expandtab
+#
+# Author: Mike Milner <mike.milner@canonical.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3, as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+
+from cloudinit import util
+
+CA_CERT_PATH = "/usr/share/ca-certificates/"
+CA_CERT_FILENAME = "cloud-init-ca-certs.crt"
+CA_CERT_CONFIG = "/etc/ca-certificates.conf"
+CA_CERT_SYSTEM_PATH = "/etc/ssl/certs/"
+
+distros = ['ubuntu', 'debian']
+
+
+def update_ca_certs():
+ """
+ Updates the CA certificate cache on the current machine.
+ """
+ util.subp(["update-ca-certificates"], capture=False)
+
+
+def add_ca_certs(paths, certs):
+ """
+ Adds certificates to the system. To actually apply the new certificates
+ you must also call L{update_ca_certs}.
+
+ @param certs: A list of certificate strings.
+ """
+ if certs:
+ # First ensure they are strings...
+ cert_file_contents = "\n".join([str(c) for c in certs])
+ cert_file_fullpath = os.path.join(CA_CERT_PATH, CA_CERT_FILENAME)
+ cert_file_fullpath = paths.join(False, cert_file_fullpath)
+ util.write_file(cert_file_fullpath, cert_file_contents, mode=0644)
+ # Append cert filename to CA_CERT_CONFIG file.
+ util.write_file(paths.join(False, CA_CERT_CONFIG),
+ "\n%s" % CA_CERT_FILENAME, omode="ab")
+
+
+def remove_default_ca_certs(paths):
+ """
+ Removes all default trusted CA certificates from the system. To actually
+ apply the change you must also call L{update_ca_certs}.
+ """
+ util.delete_dir_contents(paths.join(False, CA_CERT_PATH))
+ util.delete_dir_contents(paths.join(False, CA_CERT_SYSTEM_PATH))
+ util.write_file(paths.join(False, CA_CERT_CONFIG), "", mode=0644)
+ debconf_sel = "ca-certificates ca-certificates/trust_new_crts select no"
+ util.subp(('debconf-set-selections', '-'), debconf_sel)
+
+
+def handle(name, cfg, cloud, log, _args):
+ """
+ Call to handle ca-cert sections in cloud-config file.
+
+ @param name: The module name "ca-cert" from cloud.cfg
+ @param cfg: A nested dict containing the entire cloud config contents.
+ @param cloud: The L{CloudInit} object in use.
+ @param log: Pre-initialized Python logger object to use for logging.
+ @param args: Any module arguments from cloud.cfg
+ """
+ # If there isn't a ca-certs section in the configuration don't do anything
+ if "ca-certs" not in cfg:
+ log.debug(("Skipping module named %s,"
+ " no 'ca-certs' key in configuration"), name)
+ return
+
+ ca_cert_cfg = cfg['ca-certs']
+
+ # If there is a remove-defaults option set to true, remove the system
+ # default trusted CA certs first.
+ if ca_cert_cfg.get("remove-defaults", False):
+ log.debug("Removing default certificates")
+ remove_default_ca_certs(cloud.paths)
+
+ # If we are given any new trusted CA certs to add, add them.
+ if "trusted" in ca_cert_cfg:
+ trusted_certs = util.get_cfg_option_list(ca_cert_cfg, "trusted")
+ if trusted_certs:
+ log.debug("Adding %d certificates" % len(trusted_certs))
+ add_ca_certs(cloud.paths, trusted_certs)
+
+ # Update the system with the new cert configuration.
+ log.debug("Updating certificates")
+ update_ca_certs()
diff --git a/cloudinit/config/cc_chef.py b/cloudinit/config/cc_chef.py
new file mode 100644
index 00000000..6f568261
--- /dev/null
+++ b/cloudinit/config/cc_chef.py
@@ -0,0 +1,129 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+# Author: Avishai Ish-Shalom <avishai@fewbytes.com>
+# Author: Mike Moulton <mike@meltmedia.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3, as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import json
+import os
+
+from cloudinit import templater
+from cloudinit import util
+
+RUBY_VERSION_DEFAULT = "1.8"
+
+
+def handle(name, cfg, cloud, log, _args):
+
+ # If there isn't a chef key in the configuration don't do anything
+ if 'chef' not in cfg:
+ log.debug(("Skipping module named %s,"
+ " no 'chef' key in configuration"), name)
+ return
+ chef_cfg = cfg['chef']
+
+ # Ensure the chef directories we use exist
+ c_dirs = [
+ '/etc/chef',
+ '/var/log/chef',
+ '/var/lib/chef',
+ '/var/cache/chef',
+ '/var/backups/chef',
+ '/var/run/chef',
+ ]
+ for d in c_dirs:
+ util.ensure_dir(cloud.paths.join(False, d))
+
+ # Set the validation key based on the presence of either 'validation_key'
+ # or 'validation_cert'. In the case where both exist, 'validation_key'
+ # takes precedence
+ for key in ('validation_key', 'validation_cert'):
+ if key in chef_cfg and chef_cfg[key]:
+ v_fn = cloud.paths.join(False, '/etc/chef/validation.pem')
+ util.write_file(v_fn, chef_cfg[key])
+ break
+
+ # Create the chef config from template
+ template_fn = cloud.get_template_filename('chef_client.rb')
+ if template_fn:
+ iid = str(cloud.datasource.get_instance_id())
+ params = {
+ 'server_url': chef_cfg['server_url'],
+ 'node_name': util.get_cfg_option_str(chef_cfg, 'node_name', iid),
+ 'environment': util.get_cfg_option_str(chef_cfg, 'environment',
+ '_default'),
+ 'validation_name': chef_cfg['validation_name']
+ }
+ out_fn = cloud.paths.join(False, '/etc/chef/client.rb')
+ templater.render_to_file(template_fn, out_fn, params)
+ else:
+ log.warn("No template found, not rendering to /etc/chef/client.rb")
+
+ # set the firstboot json
+ initial_json = {}
+ if 'run_list' in chef_cfg:
+ initial_json['run_list'] = chef_cfg['run_list']
+ if 'initial_attributes' in chef_cfg:
+ initial_attributes = chef_cfg['initial_attributes']
+ for k in list(initial_attributes.keys()):
+ initial_json[k] = initial_attributes[k]
+ firstboot_fn = cloud.paths.join(False, '/etc/chef/firstboot.json')
+ util.write_file(firstboot_fn, json.dumps(initial_json))
+
+ # If chef is not installed, we install chef based on 'install_type'
+ if not os.path.isfile('/usr/bin/chef-client'):
+ install_type = util.get_cfg_option_str(chef_cfg, 'install_type',
+ 'packages')
+ if install_type == "gems":
+ # this will install and run the chef-client from gems
+ chef_version = util.get_cfg_option_str(chef_cfg, 'version', None)
+ ruby_version = util.get_cfg_option_str(chef_cfg, 'ruby_version',
+ RUBY_VERSION_DEFAULT)
+ install_chef_from_gems(cloud.distro, ruby_version, chef_version)
+ # and finally, run chef-client
+ log.debug('Running chef-client')
+ util.subp(['/usr/bin/chef-client',
+ '-d', '-i', '1800', '-s', '20'], capture=False)
+ elif install_type == 'packages':
+ # this will install and run the chef-client from packages
+ cloud.distro.install_packages(('chef',))
+ else:
+ log.warn("Unknown chef install type %s", install_type)
+
+
+def get_ruby_packages(version):
+ # return a list of packages needed to install ruby at version
+ pkgs = ['ruby%s' % version, 'ruby%s-dev' % version]
+ if version == "1.8":
+ pkgs.extend(('libopenssl-ruby1.8', 'rubygems1.8'))
+ return pkgs
+
+
+def install_chef_from_gems(ruby_version, chef_version, distro):
+ distro.install_packages(get_ruby_packages(ruby_version))
+ if not os.path.exists('/usr/bin/gem'):
+ util.sym_link('/usr/bin/gem%s' % ruby_version, '/usr/bin/gem')
+ if not os.path.exists('/usr/bin/ruby'):
+ util.sym_link('/usr/bin/ruby%s' % ruby_version, '/usr/bin/ruby')
+ if chef_version:
+ util.subp(['/usr/bin/gem', 'install', 'chef',
+ '-v %s' % chef_version, '--no-ri',
+ '--no-rdoc', '--bindir', '/usr/bin', '-q'], capture=False)
+ else:
+ util.subp(['/usr/bin/gem', 'install', 'chef',
+ '--no-ri', '--no-rdoc', '--bindir',
+ '/usr/bin', '-q'], capture=False)
diff --git a/cloudinit/config/cc_disable_ec2_metadata.py b/cloudinit/config/cc_disable_ec2_metadata.py
new file mode 100644
index 00000000..3fd2c20f
--- /dev/null
+++ b/cloudinit/config/cc_disable_ec2_metadata.py
@@ -0,0 +1,36 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2009-2010 Canonical Ltd.
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3, as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from cloudinit import util
+
+from cloudinit.settings import PER_ALWAYS
+
+frequency = PER_ALWAYS
+
+REJECT_CMD = ['route', 'add', '-host', '169.254.169.254', 'reject']
+
+
+def handle(name, cfg, _cloud, log, _args):
+ disabled = util.get_cfg_option_bool(cfg, "disable_ec2_metadata", False)
+ if disabled:
+ util.subp(REJECT_CMD, capture=False)
+ else:
+ log.debug(("Skipping module named %s,"
+ " disabling the ec2 route not enabled"), name)
diff --git a/cloudinit/config/cc_final_message.py b/cloudinit/config/cc_final_message.py
new file mode 100644
index 00000000..b1caca47
--- /dev/null
+++ b/cloudinit/config/cc_final_message.py
@@ -0,0 +1,68 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2011 Canonical Ltd.
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3, as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from cloudinit import templater
+from cloudinit import util
+from cloudinit import version
+
+from cloudinit.settings import PER_ALWAYS
+
+frequency = PER_ALWAYS
+
+FINAL_MESSAGE_DEF = ("Cloud-init v. {{version}} finished at {{timestamp}}."
+ " Up {{uptime}} seconds.")
+
+
+def handle(_name, cfg, cloud, log, args):
+
+ msg_in = None
+ if len(args) != 0:
+ msg_in = args[0]
+ else:
+ msg_in = util.get_cfg_option_str(cfg, "final_message")
+
+ if not msg_in:
+ template_fn = cloud.get_template_filename('final_message')
+ if template_fn:
+ msg_in = util.load_file(template_fn)
+
+ if not msg_in:
+ msg_in = FINAL_MESSAGE_DEF
+
+ uptime = util.uptime()
+ ts = util.time_rfc2822()
+ cver = version.version_string()
+ try:
+ subs = {
+ 'uptime': uptime,
+ 'timestamp': ts,
+ 'version': cver,
+ }
+ util.multi_log("%s\n" % (templater.render_string(msg_in, subs)),
+ console=False, stderr=True)
+ except Exception:
+ util.logexc(log, "Failed to render final message template")
+
+ boot_fin_fn = cloud.paths.boot_finished
+ try:
+ contents = "%s - %s - v. %s\n" % (uptime, ts, cver)
+ util.write_file(boot_fin_fn, contents)
+ except:
+ util.logexc(log, "Failed to write boot finished file %s", boot_fin_fn)
diff --git a/cloudinit/config/cc_foo.py b/cloudinit/config/cc_foo.py
new file mode 100644
index 00000000..95aab4dd
--- /dev/null
+++ b/cloudinit/config/cc_foo.py
@@ -0,0 +1,52 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2009-2010 Canonical Ltd.
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3, as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from cloudinit.settings import PER_INSTANCE
+
+# Modules are expected to have the following attributes.
+# 1. A required 'handle' method which takes the following params.
+# a) The name will not be this files name, but instead
+# the name specified in configuration (which is the name
+# which will be used to find this module).
+# b) A configuration object that is the result of the merging
+# of cloud configs configuration with legacy configuration
+# as well as any datasource provided configuration
+# c) A cloud object that can be used to access various
+# datasource and paths for the given distro and data provided
+# by the various datasource instance types.
+# d) A argument list that may or may not be empty to this module.
+# Typically those are from module configuration where the module
+# is defined with some extra configuration that will eventually
+# be translated from yaml into arguments to this module.
+# 2. A optional 'frequency' that defines how often this module should be ran.
+# Typically one of PER_INSTANCE, PER_ALWAYS, PER_ONCE. If not
+# provided PER_INSTANCE will be assumed.
+# See settings.py for these constants.
+# 3. A optional 'distros' array/set/tuple that defines the known distros
+# this module will work with (if not all of them). This is used to write
+# a warning out if a module is being ran on a untested distribution for
+# informational purposes. If non existent all distros are assumed and
+# no warning occurs.
+
+frequency = PER_INSTANCE
+
+
+def handle(name, _cfg, _cloud, log, _args):
+ log.debug("Hi from module %s", name)
diff --git a/cloudinit/config/cc_grub_dpkg.py b/cloudinit/config/cc_grub_dpkg.py
new file mode 100644
index 00000000..b3ce6fb6
--- /dev/null
+++ b/cloudinit/config/cc_grub_dpkg.py
@@ -0,0 +1,67 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2009-2010 Canonical Ltd.
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3, as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+
+from cloudinit import util
+
+distros = ['ubuntu', 'debian']
+
+
+def handle(_name, cfg, _cloud, log, _args):
+ idevs = None
+ idevs_empty = None
+
+ if "grub-dpkg" in cfg:
+ idevs = util.get_cfg_option_str(cfg["grub-dpkg"],
+ "grub-pc/install_devices", None)
+ idevs_empty = util.get_cfg_option_str(cfg["grub-dpkg"],
+ "grub-pc/install_devices_empty", None)
+
+ if ((os.path.exists("/dev/sda1") and not os.path.exists("/dev/sda")) or
+ (os.path.exists("/dev/xvda1") and not os.path.exists("/dev/xvda"))):
+ if idevs is None:
+ idevs = ""
+ if idevs_empty is None:
+ idevs_empty = "true"
+ else:
+ if idevs_empty is None:
+ idevs_empty = "false"
+ if idevs is None:
+ idevs = "/dev/sda"
+ for dev in ("/dev/sda", "/dev/vda", "/dev/sda1", "/dev/vda1"):
+ if os.path.exists(dev):
+ idevs = dev
+ break
+
+ # now idevs and idevs_empty are set to determined values
+ # or, those set by user
+
+ dconf_sel = (("grub-pc grub-pc/install_devices string %s\n"
+ "grub-pc grub-pc/install_devices_empty boolean %s\n") %
+ (idevs, idevs_empty))
+
+ log.debug("Setting grub debconf-set-selections with '%s','%s'" %
+ (idevs, idevs_empty))
+
+ try:
+ util.subp(['debconf-set-selections'], dconf_sel)
+ except:
+ util.logexc(log, "Failed to run debconf-set-selections for grub-dpkg")
diff --git a/cloudinit/config/cc_keys_to_console.py b/cloudinit/config/cc_keys_to_console.py
new file mode 100644
index 00000000..ed7af690
--- /dev/null
+++ b/cloudinit/config/cc_keys_to_console.py
@@ -0,0 +1,53 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2011 Canonical Ltd.
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3, as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+
+from cloudinit.settings import PER_INSTANCE
+from cloudinit import util
+
+frequency = PER_INSTANCE
+
+# This is a tool that cloud init provides
+HELPER_TOOL = '/usr/lib/cloud-init/write-ssh-key-fingerprints'
+
+
+def handle(name, cfg, _cloud, log, _args):
+ if not os.path.exists(HELPER_TOOL):
+ log.warn(("Unable to activate module %s,"
+ " helper tool not found at %s"), name, HELPER_TOOL)
+ return
+
+ fp_blacklist = util.get_cfg_option_list(cfg,
+ "ssh_fp_console_blacklist", [])
+ key_blacklist = util.get_cfg_option_list(cfg,
+ "ssh_key_console_blacklist",
+ ["ssh-dss"])
+
+ try:
+ cmd = [HELPER_TOOL]
+ cmd.append(','.join(fp_blacklist))
+ cmd.append(','.join(key_blacklist))
+ (stdout, _stderr) = util.subp(cmd)
+ util.multi_log("%s\n" % (stdout.strip()),
+ stderr=False, console=True)
+ except:
+ log.warn("Writing keys to the system console failed!")
+ raise
diff --git a/cloudinit/config/cc_landscape.py b/cloudinit/config/cc_landscape.py
new file mode 100644
index 00000000..906a6ff7
--- /dev/null
+++ b/cloudinit/config/cc_landscape.py
@@ -0,0 +1,95 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2011 Canonical Ltd.
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3, as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+
+from StringIO import StringIO
+
+from configobj import ConfigObj
+
+from cloudinit import util
+
+from cloudinit.settings import PER_INSTANCE
+
+frequency = PER_INSTANCE
+
+LSC_CLIENT_CFG_FILE = "/etc/landscape/client.conf"
+
+distros = ['ubuntu']
+
+# defaults taken from stock client.conf in landscape-client 11.07.1.1-0ubuntu2
+LSC_BUILTIN_CFG = {
+ 'client': {
+ 'log_level': "info",
+ 'url': "https://landscape.canonical.com/message-system",
+ 'ping_url': "http://landscape.canonical.com/ping",
+ 'data_path': "/var/lib/landscape/client",
+ }
+}
+
+
+def handle(_name, cfg, cloud, log, _args):
+ """
+ Basically turn a top level 'landscape' entry with a 'client' dict
+ and render it to ConfigObj format under '[client]' section in
+ /etc/landscape/client.conf
+ """
+
+ ls_cloudcfg = cfg.get("landscape", {})
+
+ if not isinstance(ls_cloudcfg, (dict)):
+ raise RuntimeError(("'landscape' key existed in config,"
+ " but not a dictionary type,"
+ " is a %s instead"), util.obj_name(ls_cloudcfg))
+
+ merge_data = [
+ LSC_BUILTIN_CFG,
+ cloud.paths.join(True, LSC_CLIENT_CFG_FILE),
+ ls_cloudcfg,
+ ]
+ merged = merge_together(merge_data)
+
+ lsc_client_fn = cloud.paths.join(False, LSC_CLIENT_CFG_FILE)
+ lsc_dir = cloud.paths.join(False, os.path.dirname(lsc_client_fn))
+ if not os.path.isdir(lsc_dir):
+ util.ensure_dir(lsc_dir)
+
+ contents = StringIO()
+ merged.write(contents)
+ contents.flush()
+
+ util.write_file(lsc_client_fn, contents.getvalue())
+ log.debug("Wrote landscape config file to %s", lsc_client_fn)
+
+
+def merge_together(objs):
+ """
+ merge together ConfigObj objects or things that ConfigObj() will take in
+ later entries override earlier
+ """
+ cfg = ConfigObj({})
+ for obj in objs:
+ if not obj:
+ continue
+ if isinstance(obj, ConfigObj):
+ cfg.merge(obj)
+ else:
+ cfg.merge(ConfigObj(obj))
+ return cfg
diff --git a/cloudinit/config/cc_locale.py b/cloudinit/config/cc_locale.py
new file mode 100644
index 00000000..6feaae9d
--- /dev/null
+++ b/cloudinit/config/cc_locale.py
@@ -0,0 +1,37 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2011 Canonical Ltd.
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3, as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from cloudinit import util
+
+
+def handle(name, cfg, cloud, log, args):
+ if len(args) != 0:
+ locale = args[0]
+ else:
+ locale = util.get_cfg_option_str(cfg, "locale", cloud.get_locale())
+
+ if not locale:
+ log.debug(("Skipping module named %s, "
+ "no 'locale' configuration found"), name)
+ return
+
+ log.debug("Setting locale to %s", locale)
+ locale_cfgfile = util.get_cfg_option_str(cfg, "locale_configfile")
+ cloud.distro.apply_locale(locale, locale_cfgfile)
diff --git a/cloudinit/config/cc_mcollective.py b/cloudinit/config/cc_mcollective.py
new file mode 100644
index 00000000..2acdbc6f
--- /dev/null
+++ b/cloudinit/config/cc_mcollective.py
@@ -0,0 +1,91 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2009-2011 Canonical Ltd.
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+# Author: Marc Cluet <marc.cluet@canonical.com>
+# Based on code by Scott Moser <scott.moser@canonical.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3, as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from StringIO import StringIO
+
+# Used since this can maintain comments
+# and doesn't need a top level section
+from configobj import ConfigObj
+
+from cloudinit import util
+
+PUBCERT_FILE = "/etc/mcollective/ssl/server-public.pem"
+PRICERT_FILE = "/etc/mcollective/ssl/server-private.pem"
+
+
+def handle(name, cfg, cloud, log, _args):
+
+ # If there isn't a mcollective key in the configuration don't do anything
+ if 'mcollective' not in cfg:
+ log.debug(("Skipping module named %s, "
+ "no 'mcollective' key in configuration"), name)
+ return
+
+ mcollective_cfg = cfg['mcollective']
+
+ # Start by installing the mcollective package ...
+ cloud.distro.install_packages(("mcollective",))
+
+ # ... and then update the mcollective configuration
+ if 'conf' in mcollective_cfg:
+ # Read server.cfg values from the
+ # original file in order to be able to mix the rest up
+ server_cfg_fn = cloud.paths.join(True, '/etc/mcollective/server.cfg')
+ mcollective_config = ConfigObj(server_cfg_fn)
+ # See: http://tiny.cc/jh9agw
+ for (cfg_name, cfg) in mcollective_cfg['conf'].iteritems():
+ if cfg_name == 'public-cert':
+ pubcert_fn = cloud.paths.join(True, PUBCERT_FILE)
+ util.write_file(pubcert_fn, cfg, mode=0644)
+ mcollective_config['plugin.ssl_server_public'] = pubcert_fn
+ mcollective_config['securityprovider'] = 'ssl'
+ elif cfg_name == 'private-cert':
+ pricert_fn = cloud.paths.join(True, PRICERT_FILE)
+ util.write_file(pricert_fn, cfg, mode=0600)
+ mcollective_config['plugin.ssl_server_private'] = pricert_fn
+ mcollective_config['securityprovider'] = 'ssl'
+ else:
+ if isinstance(cfg, (basestring, str)):
+ # Just set it in the 'main' section
+ mcollective_config[cfg_name] = cfg
+ elif isinstance(cfg, (dict)):
+ # Iterate throug the config items, create a section
+ # if it is needed and then add/or create items as needed
+ if cfg_name not in mcollective_config.sections:
+ mcollective_config[cfg_name] = {}
+ for (o, v) in cfg.iteritems():
+ mcollective_config[cfg_name][o] = v
+ else:
+ # Otherwise just try to convert it to a string
+ mcollective_config[cfg_name] = str(cfg)
+ # We got all our config as wanted we'll rename
+ # the previous server.cfg and create our new one
+ old_fn = cloud.paths.join(False, '/etc/mcollective/server.cfg.old')
+ util.rename(server_cfg_fn, old_fn)
+ # Now we got the whole file, write to disk...
+ contents = StringIO()
+ mcollective_config.write(contents)
+ contents = contents.getvalue()
+ server_cfg_rw = cloud.paths.join(False, '/etc/mcollective/server.cfg')
+ util.write_file(server_cfg_rw, contents, mode=0644)
+
+ # Start mcollective
+ util.subp(['service', 'mcollective', 'start'], capture=False)
diff --git a/cloudinit/config/cc_mounts.py b/cloudinit/config/cc_mounts.py
new file mode 100644
index 00000000..d3dcf7af
--- /dev/null
+++ b/cloudinit/config/cc_mounts.py
@@ -0,0 +1,200 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2009-2010 Canonical Ltd.
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3, as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from string import whitespace # pylint: disable=W0402
+
+import re
+
+from cloudinit import util
+
+# Shortname matches 'sda', 'sda1', 'xvda', 'hda', 'sdb', xvdb, vda, vdd1
+SHORTNAME_FILTER = r"^[x]{0,1}[shv]d[a-z][0-9]*$"
+SHORTNAME = re.compile(SHORTNAME_FILTER)
+WS = re.compile("[%s]+" % (whitespace))
+
+
+def is_mdname(name):
+ # return true if this is a metadata service name
+ if name in ["ami", "root", "swap"]:
+ return True
+ # names 'ephemeral0' or 'ephemeral1'
+ # 'ebs[0-9]' appears when '--block-device-mapping sdf=snap-d4d90bbc'
+ for enumname in ("ephemeral", "ebs"):
+ if name.startswith(enumname) and name.find(":") == -1:
+ return True
+ return False
+
+
+def handle(_name, cfg, cloud, log, _args):
+ # fs_spec, fs_file, fs_vfstype, fs_mntops, fs-freq, fs_passno
+ defvals = [None, None, "auto", "defaults,nobootwait", "0", "2"]
+ defvals = cfg.get("mount_default_fields", defvals)
+
+ # these are our default set of mounts
+ defmnts = [["ephemeral0", "/mnt", "auto", defvals[3], "0", "2"],
+ ["swap", "none", "swap", "sw", "0", "0"]]
+
+ cfgmnt = []
+ if "mounts" in cfg:
+ cfgmnt = cfg["mounts"]
+
+ for i in range(len(cfgmnt)):
+ # skip something that wasn't a list
+ if not isinstance(cfgmnt[i], list):
+ log.warn("Mount option %s not a list, got a %s instead",
+ (i + 1), util.obj_name(cfgmnt[i]))
+ continue
+
+ startname = str(cfgmnt[i][0])
+ log.debug("Attempting to determine the real name of %s", startname)
+
+ # workaround, allow user to specify 'ephemeral'
+ # rather than more ec2 correct 'ephemeral0'
+ if startname == "ephemeral":
+ cfgmnt[i][0] = "ephemeral0"
+ log.debug(("Adjusted mount option %s "
+ "name from ephemeral to ephemeral0"), (i + 1))
+
+ if is_mdname(startname):
+ newname = cloud.device_name_to_device(startname)
+ if not newname:
+ log.debug("Ignoring nonexistant named mount %s", startname)
+ cfgmnt[i][1] = None
+ else:
+ renamed = newname
+ if not newname.startswith("/"):
+ renamed = "/dev/%s" % newname
+ cfgmnt[i][0] = renamed
+ log.debug("Mapped metadata name %s to %s", startname, renamed)
+ else:
+ if SHORTNAME.match(startname):
+ renamed = "/dev/%s" % startname
+ log.debug("Mapped shortname name %s to %s", startname, renamed)
+ cfgmnt[i][0] = renamed
+
+ # in case the user did not quote a field (likely fs-freq, fs_passno)
+ # but do not convert None to 'None' (LP: #898365)
+ for j in range(len(cfgmnt[i])):
+ if j is None:
+ continue
+ else:
+ cfgmnt[i][j] = str(cfgmnt[i][j])
+
+ for i in range(len(cfgmnt)):
+ # fill in values with defaults from defvals above
+ for j in range(len(defvals)):
+ if len(cfgmnt[i]) <= j:
+ cfgmnt[i].append(defvals[j])
+ elif cfgmnt[i][j] is None:
+ cfgmnt[i][j] = defvals[j]
+
+ # if the second entry in the list is 'None' this
+ # clears all previous entries of that same 'fs_spec'
+ # (fs_spec is the first field in /etc/fstab, ie, that device)
+ if cfgmnt[i][1] is None:
+ for j in range(i):
+ if cfgmnt[j][0] == cfgmnt[i][0]:
+ cfgmnt[j][1] = None
+
+ # for each of the "default" mounts, add them only if no other
+ # entry has the same device name
+ for defmnt in defmnts:
+ startname = defmnt[0]
+ devname = cloud.device_name_to_device(startname)
+ if devname is None:
+ log.debug("Ignoring nonexistant named default mount %s", startname)
+ continue
+ if devname.startswith("/"):
+ defmnt[0] = devname
+ else:
+ defmnt[0] = "/dev/%s" % devname
+
+ log.debug("Mapped default device %s to %s", startname, defmnt[0])
+
+ cfgmnt_has = False
+ for cfgm in cfgmnt:
+ if cfgm[0] == defmnt[0]:
+ cfgmnt_has = True
+ break
+
+ if cfgmnt_has:
+ log.debug(("Not including %s, already"
+ " previously included"), startname)
+ continue
+ cfgmnt.append(defmnt)
+
+ # now, each entry in the cfgmnt list has all fstab values
+ # if the second field is None (not the string, the value) we skip it
+ actlist = []
+ for x in cfgmnt:
+ if x[1] is None:
+ log.debug("Skipping non-existent device named %s", x[0])
+ else:
+ actlist.append(x)
+
+ if len(actlist) == 0:
+ log.debug("No modifications to fstab needed.")
+ return
+
+ comment = "comment=cloudconfig"
+ cc_lines = []
+ needswap = False
+ dirs = []
+ for line in actlist:
+ # write 'comment' in the fs_mntops, entry, claiming this
+ line[3] = "%s,%s" % (line[3], comment)
+ if line[2] == "swap":
+ needswap = True
+ if line[1].startswith("/"):
+ dirs.append(line[1])
+ cc_lines.append('\t'.join(line))
+
+ fstab_lines = []
+ fstab = util.load_file(cloud.paths.join(True, "/etc/fstab"))
+ for line in fstab.splitlines():
+ try:
+ toks = WS.split(line)
+ if toks[3].find(comment) != -1:
+ continue
+ except:
+ pass
+ fstab_lines.append(line)
+
+ fstab_lines.extend(cc_lines)
+ contents = "%s\n" % ('\n'.join(fstab_lines))
+ util.write_file(cloud.paths.join(False, "/etc/fstab"), contents)
+
+ if needswap:
+ try:
+ util.subp(("swapon", "-a"))
+ except:
+ util.logexc(log, "Activating swap via 'swapon -a' failed")
+
+ for d in dirs:
+ real_dir = cloud.paths.join(False, d)
+ try:
+ util.ensure_dir(real_dir)
+ except:
+ util.logexc(log, "Failed to make '%s' config-mount", d)
+
+ try:
+ util.subp(("mount", "-a"))
+ except:
+ util.logexc(log, "Activating mounts via 'mount -a' failed")
diff --git a/cloudinit/config/cc_phone_home.py b/cloudinit/config/cc_phone_home.py
new file mode 100644
index 00000000..ae1349eb
--- /dev/null
+++ b/cloudinit/config/cc_phone_home.py
@@ -0,0 +1,118 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2011 Canonical Ltd.
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3, as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from cloudinit import templater
+from cloudinit import url_helper as uhelp
+from cloudinit import util
+
+from cloudinit.settings import PER_INSTANCE
+
+frequency = PER_INSTANCE
+
+POST_LIST_ALL = [
+ 'pub_key_dsa',
+ 'pub_key_rsa',
+ 'pub_key_ecdsa',
+ 'instance_id',
+ 'hostname'
+]
+
+
+# phone_home:
+# url: http://my.foo.bar/$INSTANCE/
+# post: all
+# tries: 10
+#
+# phone_home:
+# url: http://my.foo.bar/$INSTANCE_ID/
+# post: [ pub_key_dsa, pub_key_rsa, pub_key_ecdsa, instance_id
+#
+def handle(name, cfg, cloud, log, args):
+ if len(args) != 0:
+ ph_cfg = util.read_conf(args[0])
+ else:
+ if not 'phone_home' in cfg:
+ log.debug(("Skipping module named %s, "
+ "no 'phone_home' configuration found"), name)
+ return
+ ph_cfg = cfg['phone_home']
+
+ if 'url' not in ph_cfg:
+ log.warn(("Skipping module named %s, "
+ "no 'url' found in 'phone_home' configuration"), name)
+ return
+
+ url = ph_cfg['url']
+ post_list = ph_cfg.get('post', 'all')
+ tries = ph_cfg.get('tries')
+ try:
+ tries = int(tries)
+ except:
+ tries = 10
+ util.logexc(log, ("Configuration entry 'tries'"
+ " is not an integer, using %s instead"), tries)
+
+ if post_list == "all":
+ post_list = POST_LIST_ALL
+
+ all_keys = {}
+ all_keys['instance_id'] = cloud.get_instance_id()
+ all_keys['hostname'] = cloud.get_hostname()
+
+ pubkeys = {
+ 'pub_key_dsa': '/etc/ssh/ssh_host_dsa_key.pub',
+ 'pub_key_rsa': '/etc/ssh/ssh_host_rsa_key.pub',
+ 'pub_key_ecdsa': '/etc/ssh/ssh_host_ecdsa_key.pub',
+ }
+
+ for (n, path) in pubkeys.iteritems():
+ try:
+ all_keys[n] = util.load_file(cloud.paths.join(True, path))
+ except:
+ util.logexc(log, ("%s: failed to open, can not"
+ " phone home that data"), path)
+
+ submit_keys = {}
+ for k in post_list:
+ if k in all_keys:
+ submit_keys[k] = all_keys[k]
+ else:
+ submit_keys[k] = None
+ log.warn(("Requested key %s from 'post'"
+ " configuration list not available"), k)
+
+ # Get them read to be posted
+ real_submit_keys = {}
+ for (k, v) in submit_keys.iteritems():
+ if v is None:
+ real_submit_keys[k] = 'N/A'
+ else:
+ real_submit_keys[k] = str(v)
+
+ # Incase the url is parameterized
+ url_params = {
+ 'INSTANCE_ID': all_keys['instance_id'],
+ }
+ url = templater.render_string(url, url_params)
+ try:
+ uhelp.readurl(url, data=real_submit_keys, retries=tries, sec_between=3)
+ except:
+ util.logexc(log, ("Failed to post phone home data to"
+ " %s in %s tries"), url, tries)
diff --git a/cloudinit/config/cc_puppet.py b/cloudinit/config/cc_puppet.py
new file mode 100644
index 00000000..467c1496
--- /dev/null
+++ b/cloudinit/config/cc_puppet.py
@@ -0,0 +1,113 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2009-2010 Canonical Ltd.
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3, as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from StringIO import StringIO
+
+import os
+import pwd
+import socket
+
+from cloudinit import helpers
+from cloudinit import util
+
+
+def handle(name, cfg, cloud, log, _args):
+ # If there isn't a puppet key in the configuration don't do anything
+ if 'puppet' not in cfg:
+ log.debug(("Skipping module named %s,"
+ " no 'puppet' configuration found"), name)
+ return
+
+ puppet_cfg = cfg['puppet']
+
+ # Start by installing the puppet package ...
+ cloud.distro.install_packages(["puppet"])
+
+ # ... and then update the puppet configuration
+ if 'conf' in puppet_cfg:
+ # Add all sections from the conf object to puppet.conf
+ puppet_conf_fn = cloud.paths.join(True, '/etc/puppet/puppet.conf')
+ contents = util.load_file(puppet_conf_fn)
+ # Create object for reading puppet.conf values
+ puppet_config = helpers.DefaultingConfigParser()
+ # Read puppet.conf values from original file in order to be able to
+ # mix the rest up. First clean them up (TODO is this really needed??)
+ cleaned_lines = [i.lstrip() for i in contents.splitlines()]
+ cleaned_contents = '\n'.join(cleaned_lines)
+ puppet_config.readfp(StringIO(cleaned_contents),
+ filename=puppet_conf_fn)
+ for (cfg_name, cfg) in puppet_cfg['conf'].iteritems():
+ # Cert configuration is a special case
+ # Dump the puppet master ca certificate in the correct place
+ if cfg_name == 'ca_cert':
+ # Puppet ssl sub-directory isn't created yet
+ # Create it with the proper permissions and ownership
+ pp_ssl_dir = cloud.paths.join(False, '/var/lib/puppet/ssl')
+ util.ensure_dir(pp_ssl_dir, 0771)
+ util.chownbyid(pp_ssl_dir,
+ pwd.getpwnam('puppet').pw_uid, 0)
+ pp_ssl_certs = cloud.paths.join(False,
+ '/var/lib/puppet/ssl/certs/')
+ util.ensure_dir(pp_ssl_certs)
+ util.chownbyid(pp_ssl_certs,
+ pwd.getpwnam('puppet').pw_uid, 0)
+ pp_ssl_ca_certs = cloud.paths.join(False,
+ ('/var/lib/puppet/'
+ 'ssl/certs/ca.pem'))
+ util.write_file(pp_ssl_ca_certs, cfg)
+ util.chownbyid(pp_ssl_ca_certs,
+ pwd.getpwnam('puppet').pw_uid, 0)
+ else:
+ # Iterate throug the config items, we'll use ConfigParser.set
+ # to overwrite or create new items as needed
+ for (o, v) in cfg.iteritems():
+ if o == 'certname':
+ # Expand %f as the fqdn
+ # TODO should this use the cloud fqdn??
+ v = v.replace("%f", socket.getfqdn())
+ # Expand %i as the instance id
+ v = v.replace("%i", cloud.get_instance_id())
+ # certname needs to be downcased
+ v = v.lower()
+ puppet_config.set(cfg_name, o, v)
+ # We got all our config as wanted we'll rename
+ # the previous puppet.conf and create our new one
+ conf_old_fn = cloud.paths.join(False,
+ '/etc/puppet/puppet.conf.old')
+ util.rename(puppet_conf_fn, conf_old_fn)
+ puppet_conf_rw = cloud.paths.join(False, '/etc/puppet/puppet.conf')
+ util.write_file(puppet_conf_rw, puppet_config.stringify())
+
+ # Set puppet to automatically start
+ if os.path.exists('/etc/default/puppet'):
+ util.subp(['sed', '-i',
+ '-e', 's/^START=.*/START=yes/',
+ '/etc/default/puppet'], capture=False)
+ elif os.path.exists('/bin/systemctl'):
+ util.subp(['/bin/systemctl', 'enable', 'puppet.service'],
+ capture=False)
+ elif os.path.exists('/sbin/chkconfig'):
+ util.subp(['/sbin/chkconfig', 'puppet', 'on'], capture=False)
+ else:
+ log.warn(("Sorry we do not know how to enable"
+ " puppet services on this system"))
+
+ # Start puppetd
+ util.subp(['service', 'puppet', 'start'], capture=False)
diff --git a/cloudinit/config/cc_resizefs.py b/cloudinit/config/cc_resizefs.py
new file mode 100644
index 00000000..69cd8872
--- /dev/null
+++ b/cloudinit/config/cc_resizefs.py
@@ -0,0 +1,140 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2011 Canonical Ltd.
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3, as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import stat
+import time
+
+from cloudinit import util
+from cloudinit.settings import PER_ALWAYS
+
+frequency = PER_ALWAYS
+
+RESIZE_FS_PREFIXES_CMDS = [
+ ('ext', 'resize2fs'),
+ ('xfs', 'xfs_growfs'),
+]
+
+
+def nodeify_path(devpth, where, log):
+ try:
+ st_dev = os.stat(where).st_dev
+ dev = os.makedev(os.major(st_dev), os.minor(st_dev))
+ os.mknod(devpth, 0400 | stat.S_IFBLK, dev)
+ return st_dev
+ except:
+ if util.is_container():
+ log.debug("Inside container, ignoring mknod failure in resizefs")
+ return
+ log.warn("Failed to make device node to resize %s at %s",
+ where, devpth)
+ raise
+
+
+def get_fs_type(st_dev, path, log):
+ try:
+ dev_entries = util.find_devs_with(tag='TYPE', oformat='value',
+ no_cache=True, path=path)
+ if not dev_entries:
+ return None
+ return dev_entries[0].strip()
+ except util.ProcessExecutionError:
+ util.logexc(log, ("Failed to get filesystem type"
+ " of maj=%s, min=%s for path %s"),
+ os.major(st_dev), os.minor(st_dev), path)
+ raise
+
+
+def handle(name, cfg, cloud, log, args):
+ if len(args) != 0:
+ resize_root = args[0]
+ else:
+ resize_root = util.get_cfg_option_str(cfg, "resize_rootfs", True)
+
+ if not util.translate_bool(resize_root):
+ log.debug("Skipping module named %s, resizing disabled", name)
+ return
+
+ # TODO is the directory ok to be used??
+ resize_root_d = util.get_cfg_option_str(cfg, "resize_rootfs_tmp", "/run")
+ resize_root_d = cloud.paths.join(False, resize_root_d)
+ util.ensure_dir(resize_root_d)
+
+ # TODO: allow what is to be resized to be configurable??
+ resize_what = cloud.paths.join(False, "/")
+ with util.ExtendedTemporaryFile(prefix="cloudinit.resizefs.",
+ dir=resize_root_d, delete=True) as tfh:
+ devpth = tfh.name
+
+ # Delete the file so that mknod will work
+ # but don't change the file handle to know that its
+ # removed so that when a later call that recreates
+ # occurs this temporary file will still benefit from
+ # auto deletion
+ tfh.unlink_now()
+
+ st_dev = nodeify_path(devpth, resize_what, log)
+ fs_type = get_fs_type(st_dev, devpth, log)
+ if not fs_type:
+ log.warn("Could not determine filesystem type of %s", resize_what)
+ return
+
+ resizer = None
+ fstype_lc = fs_type.lower()
+ for (pfix, root_cmd) in RESIZE_FS_PREFIXES_CMDS:
+ if fstype_lc.startswith(pfix):
+ resizer = root_cmd
+ break
+
+ if not resizer:
+ log.warn("Not resizing unknown filesystem type %s for %s",
+ fs_type, resize_what)
+ return
+
+ log.debug("Resizing %s (%s) using %s", resize_what, fs_type, resizer)
+ resize_cmd = [resizer, devpth]
+
+ if resize_root == "noblock":
+ # Fork to a child that will run
+ # the resize command
+ util.fork_cb(do_resize, resize_cmd, log)
+ # Don't delete the file now in the parent
+ tfh.delete = False
+ else:
+ do_resize(resize_cmd, log)
+
+ action = 'Resized'
+ if resize_root == "noblock":
+ action = 'Resizing (via forking)'
+ log.debug("%s root filesystem (type=%s, maj=%i, min=%i, val=%s)",
+ action, fs_type, os.major(st_dev), os.minor(st_dev), resize_root)
+
+
+def do_resize(resize_cmd, log):
+ start = time.time()
+ try:
+ util.subp(resize_cmd)
+ except util.ProcessExecutionError:
+ util.logexc(log, "Failed to resize filesystem (cmd=%s)", resize_cmd)
+ raise
+ tot_time = int(time.time() - start)
+ log.debug("Resizing took %s seconds", tot_time)
+ # TODO: Should we add a fsck check after this to make
+ # sure we didn't corrupt anything?
diff --git a/cloudinit/config/cc_rightscale_userdata.py b/cloudinit/config/cc_rightscale_userdata.py
new file mode 100644
index 00000000..7a134569
--- /dev/null
+++ b/cloudinit/config/cc_rightscale_userdata.py
@@ -0,0 +1,102 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2011 Canonical Ltd.
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3, as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+##
+## The purpose of this script is to allow cloud-init to consume
+## rightscale style userdata. rightscale user data is key-value pairs
+## in a url-query-string like format.
+##
+## for cloud-init support, there will be a key named
+## 'CLOUD_INIT_REMOTE_HOOK'.
+##
+## This cloud-config module will
+## - read the blob of data from raw user data, and parse it as key/value
+## - for each key that is found, download the content to
+## the local instance/scripts directory and set them executable.
+## - the files in that directory will be run by the user-scripts module
+## Therefore, this must run before that.
+##
+##
+
+import os
+
+from cloudinit import url_helper as uhelp
+from cloudinit import util
+from cloudinit.settings import PER_INSTANCE
+
+from urlparse import parse_qs
+
+frequency = PER_INSTANCE
+
+MY_NAME = "cc_rightscale_userdata"
+MY_HOOKNAME = 'CLOUD_INIT_REMOTE_HOOK'
+
+
+def handle(name, _cfg, cloud, log, _args):
+ try:
+ ud = cloud.get_userdata_raw()
+ except:
+ log.warn("Failed to get raw userdata in module %s", name)
+ return
+
+ try:
+ mdict = parse_qs(ud)
+ if not mdict or not MY_HOOKNAME in mdict:
+ log.debug(("Skipping module %s, "
+ "did not find %s in parsed"
+ " raw userdata"), name, MY_HOOKNAME)
+ return
+ except:
+ util.logexc(log, ("Failed to parse query string %s"
+ " into a dictionary"), ud)
+ raise
+
+ wrote_fns = []
+ captured_excps = []
+
+ # These will eventually be then ran by the cc_scripts_user
+ # TODO: maybe this should just be a new user data handler??
+ # Instead of a late module that acts like a user data handler?
+ scripts_d = cloud.get_ipath_cur('scripts')
+ urls = mdict[MY_HOOKNAME]
+ for (i, url) in enumerate(urls):
+ fname = os.path.join(scripts_d, "rightscale-%02i" % (i))
+ try:
+ resp = uhelp.readurl(url)
+ # Ensure its a valid http response (and something gotten)
+ if resp.ok() and resp.contents:
+ util.write_file(fname, str(resp), mode=0700)
+ wrote_fns.append(fname)
+ except Exception as e:
+ captured_excps.append(e)
+ util.logexc(log, "%s failed to read %s and write %s",
+ MY_NAME, url, fname)
+
+ if wrote_fns:
+ log.debug("Wrote out rightscale userdata to %s files", len(wrote_fns))
+
+ if len(wrote_fns) != len(urls):
+ skipped = len(urls) - len(wrote_fns)
+ log.debug("%s urls were skipped or failed", skipped)
+
+ if captured_excps:
+ log.warn("%s failed with exceptions, re-raising the last one",
+ len(captured_excps))
+ raise captured_excps[-1]
diff --git a/cloudinit/config/cc_rsyslog.py b/cloudinit/config/cc_rsyslog.py
new file mode 100644
index 00000000..78327526
--- /dev/null
+++ b/cloudinit/config/cc_rsyslog.py
@@ -0,0 +1,102 @@
+# vi: ts=4 expandtab syntax=python
+#
+# Copyright (C) 2009-2010 Canonical Ltd.
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3, as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+
+from cloudinit import util
+
+DEF_FILENAME = "20-cloud-config.conf"
+DEF_DIR = "/etc/rsyslog.d"
+
+
+def handle(name, cfg, cloud, log, _args):
+ # rsyslog:
+ # - "*.* @@192.158.1.1"
+ # - content: "*.* @@192.0.2.1:10514"
+ # - filename: 01-examplecom.conf
+ # content: |
+ # *.* @@syslogd.example.com
+
+ # process 'rsyslog'
+ if not 'rsyslog' in cfg:
+ log.debug(("Skipping module named %s,"
+ " no 'rsyslog' key in configuration"), name)
+ return
+
+ def_dir = cfg.get('rsyslog_dir', DEF_DIR)
+ def_fname = cfg.get('rsyslog_filename', DEF_FILENAME)
+
+ files = []
+ for i, ent in enumerate(cfg['rsyslog']):
+ if isinstance(ent, dict):
+ if not "content" in ent:
+ log.warn("No 'content' entry in config entry %s", i + 1)
+ continue
+ content = ent['content']
+ filename = ent.get("filename", def_fname)
+ else:
+ content = ent
+ filename = def_fname
+
+ filename = filename.strip()
+ if not filename:
+ log.warn("Entry %s has an empty filename", i + 1)
+ continue
+
+ if not filename.startswith("/"):
+ filename = os.path.join(def_dir, filename)
+
+ # Truncate filename first time you see it
+ omode = "ab"
+ if filename not in files:
+ omode = "wb"
+ files.append(filename)
+
+ try:
+ contents = "%s\n" % (content)
+ util.write_file(cloud.paths.join(False, filename),
+ contents, omode=omode)
+ except Exception:
+ util.logexc(log, "Failed to write to %s", filename)
+
+ # Attempt to restart syslogd
+ restarted = False
+ try:
+ # If this config module is running at cloud-init time
+ # (before rsyslog is running) we don't actually have to
+ # restart syslog.
+ #
+ # Upstart actually does what we want here, in that it doesn't
+ # start a service that wasn't running already on 'restart'
+ # it will also return failure on the attempt, so 'restarted'
+ # won't get set.
+ log.debug("Restarting rsyslog")
+ util.subp(['service', 'rsyslog', 'restart'])
+ restarted = True
+ except Exception:
+ util.logexc(log, "Failed restarting rsyslog")
+
+ if restarted:
+ # This only needs to run if we *actually* restarted
+ # syslog above.
+ cloud.cycle_logging()
+ # This should now use rsyslog if
+ # the logging was setup to use it...
+ log.debug("%s configured %s files", name, files)
diff --git a/cloudinit/config/cc_runcmd.py b/cloudinit/config/cc_runcmd.py
new file mode 100644
index 00000000..65064cfb
--- /dev/null
+++ b/cloudinit/config/cc_runcmd.py
@@ -0,0 +1,38 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2009-2010 Canonical Ltd.
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3, as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+
+from cloudinit import util
+
+
+def handle(name, cfg, cloud, log, _args):
+ if "runcmd" not in cfg:
+ log.debug(("Skipping module named %s,"
+ " no 'runcmd' key in configuration"), name)
+ return
+
+ out_fn = os.path.join(cloud.get_ipath('scripts'), "runcmd")
+ cmd = cfg["runcmd"]
+ try:
+ content = util.shellify(cmd)
+ util.write_file(cloud.paths.join(False, out_fn), content, 0700)
+ except:
+ util.logexc(log, "Failed to shellify %s into file %s", cmd, out_fn)
diff --git a/cloudinit/config/cc_salt_minion.py b/cloudinit/config/cc_salt_minion.py
new file mode 100644
index 00000000..79ed8807
--- /dev/null
+++ b/cloudinit/config/cc_salt_minion.py
@@ -0,0 +1,60 @@
+# vi: ts=4 expandtab
+#
+# Author: Jeff Bauer <jbauer@rubic.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3, as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+
+from cloudinit import util
+
+# Note: see http://saltstack.org/topics/installation/
+
+
+def handle(name, cfg, cloud, log, _args):
+ # If there isn't a salt key in the configuration don't do anything
+ if 'salt_minion' not in cfg:
+ log.debug(("Skipping module named %s,"
+ " no 'salt_minion' key in configuration"), name)
+ return
+
+ salt_cfg = cfg['salt_minion']
+
+ # Start by installing the salt package ...
+ cloud.distro.install_packages(["salt-minion"])
+
+ # Ensure we can configure files at the right dir
+ config_dir = cloud.paths.join(False, salt_cfg.get("config_dir",
+ '/etc/salt'))
+ util.ensure_dir(config_dir)
+
+ # ... and then update the salt configuration
+ if 'conf' in salt_cfg:
+ # Add all sections from the conf object to /etc/salt/minion
+ minion_config = os.path.join(config_dir, 'minion')
+ minion_data = util.yaml_dumps(salt_cfg.get('conf'))
+ util.write_file(minion_config, minion_data)
+
+ # ... copy the key pair if specified
+ if 'public_key' in salt_cfg and 'private_key' in salt_cfg:
+ pki_dir = cloud.paths.join(False, salt_cfg.get('pki_dir',
+ '/etc/salt/pki'))
+ with util.umask(077):
+ util.ensure_dir(pki_dir)
+ pub_name = os.path.join(pki_dir, 'minion.pub')
+ pem_name = os.path.join(pki_dir, 'minion.pem')
+ util.write_file(pub_name, salt_cfg['public_key'])
+ util.write_file(pem_name, salt_cfg['private_key'])
+
+ # Start salt-minion
+ util.subp(['service', 'salt-minion', 'start'], capture=False)
diff --git a/cloudinit/config/cc_scripts_per_boot.py b/cloudinit/config/cc_scripts_per_boot.py
new file mode 100644
index 00000000..42b987eb
--- /dev/null
+++ b/cloudinit/config/cc_scripts_per_boot.py
@@ -0,0 +1,41 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2011 Canonical Ltd.
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3, as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+
+from cloudinit import util
+
+from cloudinit.settings import PER_ALWAYS
+
+frequency = PER_ALWAYS
+
+SCRIPT_SUBDIR = 'per-boot'
+
+
+def handle(name, _cfg, cloud, log, _args):
+ # Comes from the following:
+ # https://forums.aws.amazon.com/thread.jspa?threadID=96918
+ runparts_path = os.path.join(cloud.get_cpath(), 'scripts', SCRIPT_SUBDIR)
+ try:
+ util.runparts(runparts_path)
+ except:
+ log.warn("Failed to run module %s (%s in %s)",
+ name, SCRIPT_SUBDIR, runparts_path)
+ raise
diff --git a/cloudinit/config/cc_scripts_per_instance.py b/cloudinit/config/cc_scripts_per_instance.py
new file mode 100644
index 00000000..b5d71c13
--- /dev/null
+++ b/cloudinit/config/cc_scripts_per_instance.py
@@ -0,0 +1,41 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2011 Canonical Ltd.
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3, as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+
+from cloudinit import util
+
+from cloudinit.settings import PER_INSTANCE
+
+frequency = PER_INSTANCE
+
+SCRIPT_SUBDIR = 'per-instance'
+
+
+def handle(name, _cfg, cloud, log, _args):
+ # Comes from the following:
+ # https://forums.aws.amazon.com/thread.jspa?threadID=96918
+ runparts_path = os.path.join(cloud.get_cpath(), 'scripts', SCRIPT_SUBDIR)
+ try:
+ util.runparts(runparts_path)
+ except:
+ log.warn("Failed to run module %s (%s in %s)",
+ name, SCRIPT_SUBDIR, runparts_path)
+ raise
diff --git a/cloudinit/config/cc_scripts_per_once.py b/cloudinit/config/cc_scripts_per_once.py
new file mode 100644
index 00000000..d77d36d5
--- /dev/null
+++ b/cloudinit/config/cc_scripts_per_once.py
@@ -0,0 +1,41 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2011 Canonical Ltd.
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3, as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+
+from cloudinit import util
+
+from cloudinit.settings import PER_ONCE
+
+frequency = PER_ONCE
+
+SCRIPT_SUBDIR = 'per-once'
+
+
+def handle(name, _cfg, cloud, log, _args):
+ # Comes from the following:
+ # https://forums.aws.amazon.com/thread.jspa?threadID=96918
+ runparts_path = os.path.join(cloud.get_cpath(), 'scripts', SCRIPT_SUBDIR)
+ try:
+ util.runparts(runparts_path)
+ except:
+ log.warn("Failed to run module %s (%s in %s)",
+ name, SCRIPT_SUBDIR, runparts_path)
+ raise
diff --git a/cloudinit/config/cc_scripts_user.py b/cloudinit/config/cc_scripts_user.py
new file mode 100644
index 00000000..5c53014f
--- /dev/null
+++ b/cloudinit/config/cc_scripts_user.py
@@ -0,0 +1,42 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2011 Canonical Ltd.
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3, as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+
+from cloudinit import util
+
+from cloudinit.settings import PER_INSTANCE
+
+frequency = PER_INSTANCE
+
+SCRIPT_SUBDIR = 'scripts'
+
+
+def handle(name, _cfg, cloud, log, _args):
+ # This is written to by the user data handlers
+ # Ie, any custom shell scripts that come down
+ # go here...
+ runparts_path = os.path.join(cloud.get_ipath_cur(), SCRIPT_SUBDIR)
+ try:
+ util.runparts(runparts_path)
+ except:
+ log.warn("Failed to run module %s (%s in %s)",
+ name, SCRIPT_SUBDIR, runparts_path)
+ raise
diff --git a/cloudinit/config/cc_set_hostname.py b/cloudinit/config/cc_set_hostname.py
new file mode 100644
index 00000000..b0f27ebf
--- /dev/null
+++ b/cloudinit/config/cc_set_hostname.py
@@ -0,0 +1,35 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2011 Canonical Ltd.
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3, as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from cloudinit import util
+
+
+def handle(name, cfg, cloud, log, _args):
+ if util.get_cfg_option_bool(cfg, "preserve_hostname", False):
+ log.debug(("Configuration option 'preserve_hostname' is set,"
+ " not setting the hostname in module %s"), name)
+ return
+
+ (hostname, _fqdn) = util.get_hostname_fqdn(cfg, cloud)
+ try:
+ log.debug("Setting hostname to %s", hostname)
+ cloud.distro.set_hostname(hostname)
+ except Exception:
+ util.logexc(log, "Failed to set hostname to %s", hostname)
diff --git a/cloudinit/config/cc_set_passwords.py b/cloudinit/config/cc_set_passwords.py
new file mode 100644
index 00000000..ab266741
--- /dev/null
+++ b/cloudinit/config/cc_set_passwords.py
@@ -0,0 +1,146 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2009-2010 Canonical Ltd.
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3, as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import sys
+
+from cloudinit import ssh_util
+from cloudinit import util
+
+from string import letters, digits # pylint: disable=W0402
+
+# We are removing certain 'painful' letters/numbers
+PW_SET = (letters.translate(None, 'loLOI') +
+ digits.translate(None, '01'))
+
+
+def handle(_name, cfg, cloud, log, args):
+ if len(args) != 0:
+ # if run from command line, and give args, wipe the chpasswd['list']
+ password = args[0]
+ if 'chpasswd' in cfg and 'list' in cfg['chpasswd']:
+ del cfg['chpasswd']['list']
+ else:
+ password = util.get_cfg_option_str(cfg, "password", None)
+
+ expire = True
+ pw_auth = "no"
+ change_pwauth = False
+ plist = None
+
+ if 'chpasswd' in cfg:
+ chfg = cfg['chpasswd']
+ plist = util.get_cfg_option_str(chfg, 'list', plist)
+ expire = util.get_cfg_option_bool(chfg, 'expire', expire)
+
+ if not plist and password:
+ user = util.get_cfg_option_str(cfg, "user", "ubuntu")
+ plist = "%s:%s" % (user, password)
+
+ errors = []
+ if plist:
+ plist_in = []
+ randlist = []
+ users = []
+ for line in plist.splitlines():
+ u, p = line.split(':', 1)
+ if p == "R" or p == "RANDOM":
+ p = rand_user_password()
+ randlist.append("%s:%s" % (u, p))
+ plist_in.append("%s:%s" % (u, p))
+ users.append(u)
+
+ ch_in = '\n'.join(plist_in)
+ try:
+ log.debug("Changing password for %s:", users)
+ util.subp(['chpasswd'], ch_in)
+ except Exception as e:
+ errors.append(e)
+ util.logexc(log,
+ "Failed to set passwords with chpasswd for %s", users)
+
+ if len(randlist):
+ blurb = ("Set the following 'random' passwords\n",
+ '\n'.join(randlist))
+ sys.stderr.write("%s\n%s\n" % blurb)
+
+ if expire:
+ expired_users = []
+ for u in users:
+ try:
+ util.subp(['passwd', '--expire', u])
+ expired_users.append(u)
+ except Exception as e:
+ errors.append(e)
+ util.logexc(log, "Failed to set 'expire' for %s", u)
+ if expired_users:
+ log.debug("Expired passwords for: %s users", expired_users)
+
+ change_pwauth = False
+ pw_auth = None
+ if 'ssh_pwauth' in cfg:
+ change_pwauth = True
+ if util.is_true(cfg['ssh_pwauth']):
+ pw_auth = 'yes'
+ if util.is_false(cfg['ssh_pwauth']):
+ pw_auth = 'no'
+
+ if change_pwauth:
+ replaced_auth = False
+
+ # See: man sshd_config
+ conf_fn = cloud.paths.join(True, ssh_util.DEF_SSHD_CFG)
+ old_lines = ssh_util.parse_ssh_config(conf_fn)
+ new_lines = []
+ i = 0
+ for (i, line) in enumerate(old_lines):
+ # Keywords are case-insensitive and arguments are case-sensitive
+ if line.key == 'passwordauthentication':
+ log.debug("Replacing auth line %s with %s", i + 1, pw_auth)
+ replaced_auth = True
+ line.value = pw_auth
+ new_lines.append(line)
+
+ if not replaced_auth:
+ log.debug("Adding new auth line %s", i + 1)
+ replaced_auth = True
+ new_lines.append(ssh_util.SshdConfigLine('',
+ 'PasswordAuthentication',
+ pw_auth))
+
+ lines = [str(e) for e in new_lines]
+ ssh_rw_fn = cloud.paths.join(False, ssh_util.DEF_SSHD_CFG)
+ util.write_file(ssh_rw_fn, "\n".join(lines))
+
+ try:
+ cmd = ['service']
+ cmd.append(cloud.distro.get_option('ssh_svcname', 'ssh'))
+ cmd.append('restart')
+ util.subp(cmd)
+ log.debug("Restarted the ssh daemon")
+ except:
+ util.logexc(log, "Restarting of the ssh daemon failed")
+
+ if len(errors):
+ log.debug("%s errors occured, re-raising the last one", len(errors))
+ raise errors[-1]
+
+
+def rand_user_password(pwlen=9):
+ return util.rand_str(pwlen, select_from=PW_SET)
diff --git a/cloudinit/config/cc_ssh.py b/cloudinit/config/cc_ssh.py
new file mode 100644
index 00000000..4019ae90
--- /dev/null
+++ b/cloudinit/config/cc_ssh.py
@@ -0,0 +1,132 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2009-2010 Canonical Ltd.
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3, as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import glob
+
+from cloudinit import util
+from cloudinit import ssh_util
+
+DISABLE_ROOT_OPTS = ("no-port-forwarding,no-agent-forwarding,"
+"no-X11-forwarding,command=\"echo \'Please login as the user \\\"$USER\\\" "
+"rather than the user \\\"root\\\".\';echo;sleep 10\"")
+
+KEY_2_FILE = {
+ "rsa_private": ("/etc/ssh/ssh_host_rsa_key", 0600),
+ "rsa_public": ("/etc/ssh/ssh_host_rsa_key.pub", 0644),
+ "dsa_private": ("/etc/ssh/ssh_host_dsa_key", 0600),
+ "dsa_public": ("/etc/ssh/ssh_host_dsa_key.pub", 0644),
+ "ecdsa_private": ("/etc/ssh/ssh_host_ecdsa_key", 0600),
+ "ecdsa_public": ("/etc/ssh/ssh_host_ecdsa_key.pub", 0644),
+}
+
+PRIV_2_PUB = {
+ 'rsa_private': 'rsa_public',
+ 'dsa_private': 'dsa_public',
+ 'ecdsa_private': 'ecdsa_public',
+}
+
+KEY_GEN_TPL = 'o=$(ssh-keygen -yf "%s") && echo "$o" root@localhost > "%s"'
+
+GENERATE_KEY_NAMES = ['rsa', 'dsa', 'ecdsa']
+
+KEY_FILE_TPL = '/etc/ssh/ssh_host_%s_key'
+
+
+def handle(_name, cfg, cloud, log, _args):
+
+ # remove the static keys from the pristine image
+ if cfg.get("ssh_deletekeys", True):
+ key_pth = cloud.paths.join(False, "/etc/ssh/", "ssh_host_*key*")
+ for f in glob.glob(key_pth):
+ try:
+ util.del_file(f)
+ except:
+ util.logexc(log, "Failed deleting key file %s", f)
+
+ if "ssh_keys" in cfg:
+ # if there are keys in cloud-config, use them
+ for (key, val) in cfg["ssh_keys"].iteritems():
+ if key in KEY_2_FILE:
+ tgt_fn = KEY_2_FILE[key][0]
+ tgt_perms = KEY_2_FILE[key][1]
+ util.write_file(cloud.paths.join(False, tgt_fn),
+ val, tgt_perms)
+
+ for (priv, pub) in PRIV_2_PUB.iteritems():
+ if pub in cfg['ssh_keys'] or not priv in cfg['ssh_keys']:
+ continue
+ pair = (KEY_2_FILE[priv][0], KEY_2_FILE[pub][0])
+ cmd = ['sh', '-xc', KEY_GEN_TPL % pair]
+ try:
+ # TODO: Is this guard needed?
+ with util.SeLinuxGuard("/etc/ssh", recursive=True):
+ util.subp(cmd, capture=False)
+ log.debug("Generated a key for %s from %s", pair[0], pair[1])
+ except:
+ util.logexc(log, ("Failed generated a key"
+ " for %s from %s"), pair[0], pair[1])
+ else:
+ # if not, generate them
+ genkeys = util.get_cfg_option_list(cfg,
+ 'ssh_genkeytypes',
+ GENERATE_KEY_NAMES)
+ for keytype in genkeys:
+ keyfile = cloud.paths.join(False, KEY_FILE_TPL % (keytype))
+ util.ensure_dir(os.path.dirname(keyfile))
+ if not os.path.exists(keyfile):
+ cmd = ['ssh-keygen', '-t', keytype, '-N', '', '-f', keyfile]
+ try:
+ # TODO: Is this guard needed?
+ with util.SeLinuxGuard("/etc/ssh", recursive=True):
+ util.subp(cmd, capture=False)
+ except:
+ util.logexc(log, ("Failed generating key type"
+ " %s to file %s"), keytype, keyfile)
+
+ try:
+ user = util.get_cfg_option_str(cfg, 'user')
+ disable_root = util.get_cfg_option_bool(cfg, "disable_root", True)
+ disable_root_opts = util.get_cfg_option_str(cfg, "disable_root_opts",
+ DISABLE_ROOT_OPTS)
+
+ keys = cloud.get_public_ssh_keys() or []
+ if "ssh_authorized_keys" in cfg:
+ cfgkeys = cfg["ssh_authorized_keys"]
+ keys.extend(cfgkeys)
+
+ apply_credentials(keys, user, cloud.paths,
+ disable_root, disable_root_opts)
+ except:
+ util.logexc(log, "Applying ssh credentials failed!")
+
+
+def apply_credentials(keys, user, paths, disable_root, disable_root_opts):
+
+ keys = set(keys)
+ if user:
+ ssh_util.setup_user_keys(keys, user, '', paths)
+
+ if disable_root and user:
+ key_prefix = disable_root_opts.replace('$USER', user)
+ else:
+ key_prefix = ''
+
+ ssh_util.setup_user_keys(keys, 'root', key_prefix, paths)
diff --git a/cloudinit/config/cc_ssh_import_id.py b/cloudinit/config/cc_ssh_import_id.py
new file mode 100644
index 00000000..c58b28ec
--- /dev/null
+++ b/cloudinit/config/cc_ssh_import_id.py
@@ -0,0 +1,53 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2009-2010 Canonical Ltd.
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3, as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from cloudinit import util
+
+# The ssh-import-id only seems to exist on ubuntu (for now)
+# https://launchpad.net/ssh-import-id
+distros = ['ubuntu']
+
+
+def handle(name, cfg, _cloud, log, args):
+ if len(args) != 0:
+ user = args[0]
+ ids = []
+ if len(args) > 1:
+ ids = args[1:]
+ else:
+ user = util.get_cfg_option_str(cfg, "user", "ubuntu")
+ ids = util.get_cfg_option_list(cfg, "ssh_import_id", [])
+
+ if len(ids) == 0:
+ log.debug("Skipping module named %s, no ids found to import", name)
+ return
+
+ if not user:
+ log.debug("Skipping module named %s, no user found to import", name)
+ return
+
+ cmd = ["sudo", "-Hu", user, "ssh-import-id"] + ids
+ log.debug("Importing ssh ids for user %s.", user)
+
+ try:
+ util.subp(cmd, capture=False)
+ except util.ProcessExecutionError as e:
+ util.logexc(log, "Failed to run command to import %s ssh ids", user)
+ raise e
diff --git a/cloudinit/config/cc_timezone.py b/cloudinit/config/cc_timezone.py
new file mode 100644
index 00000000..b9eb85b2
--- /dev/null
+++ b/cloudinit/config/cc_timezone.py
@@ -0,0 +1,39 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2009-2010 Canonical Ltd.
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3, as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from cloudinit import util
+
+from cloudinit.settings import PER_INSTANCE
+
+frequency = PER_INSTANCE
+
+
+def handle(name, cfg, cloud, log, args):
+ if len(args) != 0:
+ timezone = args[0]
+ else:
+ timezone = util.get_cfg_option_str(cfg, "timezone", False)
+
+ if not timezone:
+ log.debug("Skipping module named %s, no 'timezone' specified", name)
+ return
+
+ # Let the distro handle settings its timezone
+ cloud.distro.set_timezone(timezone)
diff --git a/cloudinit/config/cc_update_etc_hosts.py b/cloudinit/config/cc_update_etc_hosts.py
new file mode 100644
index 00000000..c148b12e
--- /dev/null
+++ b/cloudinit/config/cc_update_etc_hosts.py
@@ -0,0 +1,60 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2011 Canonical Ltd.
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3, as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from cloudinit import util
+from cloudinit import templater
+
+from cloudinit.settings import PER_ALWAYS
+
+frequency = PER_ALWAYS
+
+
+def handle(name, cfg, cloud, log, _args):
+ manage_hosts = util.get_cfg_option_str(cfg, "manage_etc_hosts", False)
+ if util.translate_bool(manage_hosts, addons=['template']):
+ (hostname, fqdn) = util.get_hostname_fqdn(cfg, cloud)
+ if not hostname:
+ log.warn(("Option 'manage_etc_hosts' was set,"
+ " but no hostname was found"))
+ return
+
+ # Render from a template file
+ distro_n = cloud.distro.name
+ tpl_fn_name = cloud.get_template_filename("hosts.%s" % (distro_n))
+ if not tpl_fn_name:
+ raise RuntimeError(("No hosts template could be"
+ " found for distro %s") % (distro_n))
+
+ out_fn = cloud.paths.join(False, '/etc/hosts')
+ templater.render_to_file(tpl_fn_name, out_fn,
+ {'hostname': hostname, 'fqdn': fqdn})
+
+ elif manage_hosts == "localhost":
+ (hostname, fqdn) = util.get_hostname_fqdn(cfg, cloud)
+ if not hostname:
+ log.warn(("Option 'manage_etc_hosts' was set,"
+ " but no hostname was found"))
+ return
+
+ log.debug("Managing localhost in /etc/hosts")
+ cloud.distro.update_etc_hosts(hostname, fqdn)
+ else:
+ log.debug(("Configuration option 'manage_etc_hosts' is not set,"
+ " not managing /etc/hosts in module %s"), name)
diff --git a/cloudinit/config/cc_update_hostname.py b/cloudinit/config/cc_update_hostname.py
new file mode 100644
index 00000000..b84a1a06
--- /dev/null
+++ b/cloudinit/config/cc_update_hostname.py
@@ -0,0 +1,41 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2011 Canonical Ltd.
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3, as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+
+from cloudinit import util
+from cloudinit.settings import PER_ALWAYS
+
+frequency = PER_ALWAYS
+
+
+def handle(name, cfg, cloud, log, _args):
+ if util.get_cfg_option_bool(cfg, "preserve_hostname", False):
+ log.debug(("Configuration option 'preserve_hostname' is set,"
+ " not updating the hostname in module %s"), name)
+ return
+
+ (hostname, _fqdn) = util.get_hostname_fqdn(cfg, cloud)
+ try:
+ prev_fn = os.path.join(cloud.get_cpath('data'), "previous-hostname")
+ cloud.distro.update_hostname(hostname, prev_fn)
+ except Exception:
+ util.logexc(log, "Failed to set the hostname to %s", hostname)
+ raise