diff options
Diffstat (limited to 'cloudinit/distros')
-rw-r--r-- | cloudinit/distros/__init__.py | 163 | ||||
-rw-r--r-- | cloudinit/distros/debian.py | 149 | ||||
-rw-r--r-- | cloudinit/distros/fedora.py | 31 | ||||
-rw-r--r-- | cloudinit/distros/rhel.py | 337 | ||||
-rw-r--r-- | cloudinit/distros/ubuntu.py | 31 |
5 files changed, 711 insertions, 0 deletions
diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py new file mode 100644 index 00000000..da4d0180 --- /dev/null +++ b/cloudinit/distros/__init__.py @@ -0,0 +1,163 @@ +# vi: ts=4 expandtab +# +# Copyright (C) 2012 Canonical Ltd. +# Copyright (C) 2012 Hewlett-Packard Development Company, L.P. +# Copyright (C) 2012 Yahoo! Inc. +# +# Author: Scott Moser <scott.moser@canonical.com> +# Author: Juerg Haefliger <juerg.haefliger@hp.com> +# Author: Joshua Harlow <harlowja@yahoo-inc.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 abc + +from cloudinit import importer +from cloudinit import log as logging +from cloudinit import util + +# TODO: Make this via config?? +IFACE_ACTIONS = { + 'up': ['ifup', '--all'], + 'down': ['ifdown', '--all'], +} + +LOG = logging.getLogger(__name__) + + +class Distro(object): + + __metaclass__ = abc.ABCMeta + + def __init__(self, name, cfg, paths): + self._paths = paths + self._cfg = cfg + self.name = name + + @abc.abstractmethod + def install_packages(self, pkglist): + raise NotImplementedError() + + @abc.abstractmethod + def _write_network(self, settings): + # In the future use the http://fedorahosted.org/netcf/ + # to write this blob out in a distro format + raise NotImplementedError() + + def get_option(self, opt_name, default=None): + return self._cfg.get(opt_name, default) + + @abc.abstractmethod + def set_hostname(self, hostname): + raise NotImplementedError() + + @abc.abstractmethod + def update_hostname(self, hostname, prev_hostname_fn): + raise NotImplementedError() + + @abc.abstractmethod + def package_command(self, cmd, args=None): + raise NotImplementedError() + + @abc.abstractmethod + def update_package_sources(self): + raise NotImplementedError() + + def get_package_mirror(self): + return self.get_option('package_mirror') + + def apply_network(self, settings, bring_up=True): + # Write it out + self._write_network(settings) + # Now try to bring them up + if bring_up: + return self._interface_action('up') + return False + + @abc.abstractmethod + def apply_locale(self, locale, out_fn=None): + raise NotImplementedError() + + @abc.abstractmethod + def set_timezone(self, tz): + raise NotImplementedError() + + def _get_localhost_ip(self): + return "127.0.0.1" + + def update_etc_hosts(self, hostname, fqdn): + # Format defined at + # http://unixhelp.ed.ac.uk/CGI/man-cgi?hosts + header = "# Added by cloud-init" + real_header = "%s on %s" % (header, util.time_rfc2822()) + local_ip = self._get_localhost_ip() + hosts_line = "%s\t%s %s" % (local_ip, fqdn, hostname) + new_etchosts = StringIO() + need_write = False + need_change = True + hosts_ro_fn = self._paths.join(True, "/etc/hosts") + for line in util.load_file(hosts_ro_fn).splitlines(): + if line.strip().startswith(header): + continue + if not line.strip() or line.strip().startswith("#"): + new_etchosts.write("%s\n" % (line)) + continue + split_line = [s.strip() for s in line.split()] + if len(split_line) < 2: + new_etchosts.write("%s\n" % (line)) + continue + (ip, hosts) = split_line[0], split_line[1:] + if ip == local_ip: + if sorted([hostname, fqdn]) == sorted(hosts): + need_change = False + if need_change: + line = "%s\n%s" % (real_header, hosts_line) + need_change = False + need_write = True + new_etchosts.write("%s\n" % (line)) + if need_change: + new_etchosts.write("%s\n%s\n" % (real_header, hosts_line)) + need_write = True + if need_write: + contents = new_etchosts.getvalue() + util.write_file(self._paths.join(False, "/etc/hosts"), + contents, mode=0644) + + def _interface_action(self, action): + if action not in IFACE_ACTIONS: + raise NotImplementedError("Unknown interface action %s" % (action)) + cmd = IFACE_ACTIONS[action] + try: + LOG.debug("Attempting to run %s interface action using command %s", + action, cmd) + (_out, err) = util.subp(cmd) + if len(err): + LOG.warn("Running %s resulted in stderr output: %s", cmd, err) + return True + except util.ProcessExecutionError: + util.logexc(LOG, "Running interface command %s failed", cmd) + return False + + +def fetch(name): + locs = importer.find_module(name, + ['', __name__], + ['Distro']) + if not locs: + raise ImportError("No distribution found for distro %s" + % (name)) + mod = importer.import_module(locs[0]) + cls = getattr(mod, 'Distro') + return cls diff --git a/cloudinit/distros/debian.py b/cloudinit/distros/debian.py new file mode 100644 index 00000000..3247d7ce --- /dev/null +++ b/cloudinit/distros/debian.py @@ -0,0 +1,149 @@ +# vi: ts=4 expandtab +# +# Copyright (C) 2012 Canonical Ltd. +# Copyright (C) 2012 Hewlett-Packard Development Company, L.P. +# Copyright (C) 2012 Yahoo! Inc. +# +# Author: Scott Moser <scott.moser@canonical.com> +# Author: Juerg Haefliger <juerg.haefliger@hp.com> +# Author: Joshua Harlow <harlowja@yahoo-inc.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 distros +from cloudinit import helpers +from cloudinit import log as logging +from cloudinit import util + +from cloudinit.settings import PER_INSTANCE + +LOG = logging.getLogger(__name__) + + +class Distro(distros.Distro): + + def __init__(self, name, cfg, paths): + distros.Distro.__init__(self, name, cfg, paths) + # This will be used to restrict certain + # calls from repeatly happening (when they + # should only happen say once per instance...) + self._runner = helpers.Runners(paths) + + def apply_locale(self, locale, out_fn=None): + if not out_fn: + out_fn = self._paths.join(False, '/etc/default/locale') + util.subp(['locale-gen', locale], capture=False) + util.subp(['update-locale', locale], capture=False) + contents = [ + "# Created by cloud-init", + 'LANG="%s"' % (locale), + ] + util.write_file(out_fn, "\n".join(contents)) + + def install_packages(self, pkglist): + self.update_package_sources() + self.package_command('install', pkglist) + + def _write_network(self, settings): + net_fn = self._paths.join(False, "/etc/network/interfaces") + util.write_file(net_fn, settings) + + def set_hostname(self, hostname): + out_fn = self._paths.join(False, "/etc/hostname") + self._write_hostname(hostname, out_fn) + if out_fn == '/etc/hostname': + # Only do this if we are running in non-adjusted root mode + LOG.debug("Setting hostname to %s", hostname) + util.subp(['hostname', hostname]) + + def _write_hostname(self, hostname, out_fn): + lines = [] + lines.append("# Created by cloud-init") + lines.append(str(hostname)) + contents = "\n".join(lines) + util.write_file(out_fn, contents, 0644) + + def update_hostname(self, hostname, prev_fn): + hostname_prev = self._read_hostname(prev_fn) + read_fn = self._paths.join(True, "/etc/hostname") + hostname_in_etc = self._read_hostname(read_fn) + update_files = [] + if not hostname_prev or hostname_prev != hostname: + update_files.append(prev_fn) + if (not hostname_in_etc or + (hostname_in_etc == hostname_prev and + hostname_in_etc != hostname)): + write_fn = self._paths.join(False, "/etc/hostname") + update_files.append(write_fn) + for fn in update_files: + try: + self._write_hostname(hostname, fn) + except: + util.logexc(LOG, "Failed to write hostname %s to %s", + hostname, fn) + if (hostname_in_etc and hostname_prev and + hostname_in_etc != hostname_prev): + LOG.debug(("%s differs from /etc/hostname." + " Assuming user maintained hostname."), prev_fn) + if "/etc/hostname" in update_files: + # Only do this if we are running in non-adjusted root mode + LOG.debug("Setting hostname to %s", hostname) + util.subp(['hostname', hostname]) + + def _read_hostname(self, filename, default=None): + contents = util.load_file(filename, quiet=True) + for line in contents.splitlines(): + c_pos = line.find("#") + # Handle inline comments + if c_pos != -1: + line = line[0:c_pos] + line_c = line.strip() + if line_c: + return line_c + return default + + def _get_localhost_ip(self): + # Note: http://www.leonardoborda.com/blog/127-0-1-1-ubuntu-debian/ + return "127.0.1.1" + + def set_timezone(self, tz): + tz_file = os.path.join("/usr/share/zoneinfo", tz) + if not os.path.isfile(tz_file): + raise RuntimeError(("Invalid timezone %s," + " no file found at %s") % (tz, tz_file)) + tz_lines = [ + "# Created by cloud-init", + str(tz), + ] + tz_contents = "\n".join(tz_lines) + tz_fn = self._paths.join(False, "/etc/timezone") + util.write_file(tz_fn, tz_contents) + util.copy(tz_file, self._paths.join(False, "/etc/localtime")) + + def package_command(self, command, args=None): + e = os.environ.copy() + # See: http://tiny.cc/kg91fw + # Or: http://tiny.cc/mh91fw + e['DEBIAN_FRONTEND'] = 'noninteractive' + cmd = ['apt-get', '--option', 'Dpkg::Options::=--force-confold', + '--assume-yes', '--quiet', command] + if args: + cmd.extend(args) + # Allow the output of this to flow outwards (ie not be captured) + util.subp(cmd, env=e, capture=False) + + def update_package_sources(self): + self._runner.run("update-sources", self.package_command, + ["update"], freq=PER_INSTANCE) diff --git a/cloudinit/distros/fedora.py b/cloudinit/distros/fedora.py new file mode 100644 index 00000000..c777845d --- /dev/null +++ b/cloudinit/distros/fedora.py @@ -0,0 +1,31 @@ +# vi: ts=4 expandtab +# +# Copyright (C) 2012 Canonical Ltd. +# Copyright (C) 2012 Hewlett-Packard Development Company, L.P. +# Copyright (C) 2012 Yahoo! Inc. +# +# Author: Scott Moser <scott.moser@canonical.com> +# Author: Juerg Haefliger <juerg.haefliger@hp.com> +# Author: Joshua Harlow <harlowja@yahoo-inc.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.distros import rhel + +from cloudinit import log as logging + +LOG = logging.getLogger(__name__) + + +class Distro(rhel.Distro): + pass diff --git a/cloudinit/distros/rhel.py b/cloudinit/distros/rhel.py new file mode 100644 index 00000000..7fa69f03 --- /dev/null +++ b/cloudinit/distros/rhel.py @@ -0,0 +1,337 @@ +# vi: ts=4 expandtab +# +# Copyright (C) 2012 Canonical Ltd. +# Copyright (C) 2012 Hewlett-Packard Development Company, L.P. +# Copyright (C) 2012 Yahoo! Inc. +# +# Author: Scott Moser <scott.moser@canonical.com> +# Author: Juerg Haefliger <juerg.haefliger@hp.com> +# Author: Joshua Harlow <harlowja@yahoo-inc.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 distros +from cloudinit import helpers +from cloudinit import log as logging +from cloudinit import util + +from cloudinit.settings import PER_INSTANCE + +LOG = logging.getLogger(__name__) + +NETWORK_FN_TPL = '/etc/sysconfig/network-scripts/ifcfg-%s' + +# See: http://tiny.cc/6r99fw +# For what alot of these files that are being written +# are and the format of them + +# This library is used to parse/write +# out the various sysconfig files edited +# +# It has to be slightly modified though +# to ensure that all values are quoted +# since these configs are usually sourced into +# bash scripts... +from configobj import ConfigObj + +# See: http://tiny.cc/oezbgw +D_QUOTE_CHARS = { + "\"": "\\\"", + "(": "\\(", + ")": "\\)", + "$": '\$', + '`': '\`', +} + + +class Distro(distros.Distro): + + def __init__(self, name, cfg, paths): + distros.Distro.__init__(self, name, cfg, paths) + # This will be used to restrict certain + # calls from repeatly happening (when they + # should only happen say once per instance...) + self._runner = helpers.Runners(paths) + + def install_packages(self, pkglist): + self.package_command('install', pkglist) + + def _write_network(self, settings): + # TODO fix this... since this is the ubuntu format + entries = translate_network(settings) + LOG.debug("Translated ubuntu style network settings %s into %s", + settings, entries) + # Make the intermediate format as the rhel format... + for (dev, info) in entries.iteritems(): + net_fn = NETWORK_FN_TPL % (dev) + net_ro_fn = self._paths.join(True, net_fn) + (prev_exist, net_cfg) = self._read_conf(net_ro_fn) + net_cfg['DEVICE'] = dev + boot_proto = info.get('bootproto') + if boot_proto: + net_cfg['BOOTPROTO'] = boot_proto + net_mask = info.get('netmask') + if net_mask: + net_cfg["NETMASK"] = net_mask + addr = info.get('address') + if addr: + net_cfg["IPADDR"] = addr + if info.get('auto'): + net_cfg['ONBOOT'] = 'yes' + else: + net_cfg['ONBOOT'] = 'no' + gtway = info.get('gateway') + if gtway: + net_cfg["GATEWAY"] = gtway + bcast = info.get('broadcast') + if bcast: + net_cfg["BROADCAST"] = bcast + mac_addr = info.get('hwaddress') + if mac_addr: + net_cfg["MACADDR"] = mac_addr + lines = net_cfg.write() + if not prev_exist: + lines.insert(0, '# Created by cloud-init') + w_contents = "\n".join(lines) + net_rw_fn = self._paths.join(False, net_fn) + util.write_file(net_rw_fn, w_contents, 0644) + + def set_hostname(self, hostname): + out_fn = self._paths.join(False, '/etc/sysconfig/network') + self._write_hostname(hostname, out_fn) + if out_fn == '/etc/sysconfig/network': + # Only do this if we are running in non-adjusted root mode + LOG.debug("Setting hostname to %s", hostname) + util.subp(['hostname', hostname]) + + def apply_locale(self, locale, out_fn=None): + if not out_fn: + out_fn = self._paths.join(False, '/etc/sysconfig/i18n') + ro_fn = self._paths.join(True, '/etc/sysconfig/i18n') + (_exists, contents) = self._read_conf(ro_fn) + contents['LANG'] = locale + w_contents = "\n".join(contents.write()) + util.write_file(out_fn, w_contents, 0644) + + def _write_hostname(self, hostname, out_fn): + (_exists, contents) = self._read_conf(out_fn) + contents['HOSTNAME'] = hostname + w_contents = "\n".join(contents.write()) + util.write_file(out_fn, w_contents, 0644) + + def update_hostname(self, hostname, prev_file): + hostname_prev = self._read_hostname(prev_file) + read_fn = self._paths.join(True, "/etc/sysconfig/network") + hostname_in_sys = self._read_hostname(read_fn) + update_files = [] + if not hostname_prev or hostname_prev != hostname: + update_files.append(prev_file) + if (not hostname_in_sys or + (hostname_in_sys == hostname_prev + and hostname_in_sys != hostname)): + write_fn = self._paths.join(False, "/etc/sysconfig/network") + update_files.append(write_fn) + for fn in update_files: + try: + self._write_hostname(hostname, fn) + except: + util.logexc(LOG, "Failed to write hostname %s to %s", + hostname, fn) + if (hostname_in_sys and hostname_prev and + hostname_in_sys != hostname_prev): + LOG.debug(("%s differs from /etc/sysconfig/network." + " Assuming user maintained hostname."), prev_file) + if "/etc/sysconfig/network" in update_files: + # Only do this if we are running in non-adjusted root mode + LOG.debug("Setting hostname to %s", hostname) + util.subp(['hostname', hostname]) + + def _read_hostname(self, filename, default=None): + (_exists, contents) = self._read_conf(filename) + if 'HOSTNAME' in contents: + return contents['HOSTNAME'] + else: + return default + + def _read_conf(self, fn): + exists = False + if os.path.isfile(fn): + contents = util.load_file(fn).splitlines() + exists = True + else: + contents = [] + return (exists, QuotingConfigObj(contents)) + + def set_timezone(self, tz): + tz_file = os.path.join("/usr/share/zoneinfo", tz) + if not os.path.isfile(tz_file): + raise RuntimeError(("Invalid timezone %s," + " no file found at %s") % (tz, tz_file)) + # Adjust the sysconfig clock zone setting + read_fn = self._paths.join(True, "/etc/sysconfig/clock") + (_exists, contents) = self._read_conf(read_fn) + contents['ZONE'] = tz + tz_contents = "\n".join(contents.write()) + write_fn = self._paths.join(False, "/etc/sysconfig/clock") + util.write_file(write_fn, tz_contents) + # This ensures that the correct tz will be used for the system + util.copy(tz_file, self._paths.join(False, "/etc/localtime")) + + def package_command(self, command, args=None): + cmd = ['yum'] + # If enabled, then yum will be tolerant of errors on the command line + # with regard to packages. + # For example: if you request to install foo, bar and baz and baz is + # installed; yum won't error out complaining that baz is already + # installed. + cmd.append("-t") + # Determines whether or not yum prompts for confirmation + # of critical actions. We don't want to prompt... + cmd.append("-y") + cmd.append(command) + if args: + cmd.extend(args) + # Allow the output of this to flow outwards (ie not be captured) + util.subp(cmd, capture=False) + + def update_package_sources(self): + self._runner.run("update-sources", self.package_command, + ["update"], freq=PER_INSTANCE) + + +# This class helps adjust the configobj +# writing to ensure that when writing a k/v +# on a line, that they are properly quoted +# and have no spaces between the '=' sign. +# - This is mainly due to the fact that +# the sysconfig scripts are often sourced +# directly into bash/shell scripts so ensure +# that it works for those types of use cases. +class QuotingConfigObj(ConfigObj): + def __init__(self, lines): + ConfigObj.__init__(self, lines, + interpolation=False, + write_empty_values=True) + + def _quote_posix(self, text): + if not text: + return '' + for (k, v) in D_QUOTE_CHARS.iteritems(): + text = text.replace(k, v) + return '"%s"' % (text) + + def _quote_special(self, text): + if text.lower() in ['yes', 'no', 'true', 'false']: + return text + else: + return self._quote_posix(text) + + def _write_line(self, indent_string, entry, this_entry, comment): + # Ensure it is formatted fine for + # how these sysconfig scripts are used + val = self._decode_element(self._quote(this_entry)) + # Single quoted strings should + # always work. + if not val.startswith("'"): + # Perform any special quoting + val = self._quote_special(val) + key = self._decode_element(self._quote(entry, multiline=False)) + cmnt = self._decode_element(comment) + return '%s%s%s%s%s' % (indent_string, + key, + "=", + val, + cmnt) + + +# This is a util function to translate a ubuntu /etc/network/interfaces 'blob' +# to a rhel equiv. that can then be written to /etc/sysconfig/network-scripts/ +# TODO remove when we have python-netcf active... +def translate_network(settings): + # Get the standard cmd, args from the ubuntu format + entries = [] + for line in settings.splitlines(): + line = line.strip() + if not line or line.startswith("#"): + continue + split_up = line.split(None, 1) + if len(split_up) <= 1: + continue + entries.append(split_up) + # Figure out where each iface section is + ifaces = [] + consume = {} + for (cmd, args) in entries: + if cmd == 'iface': + if consume: + ifaces.append(consume) + consume = {} + consume[cmd] = args + else: + consume[cmd] = args + # Check if anything left over to consume + absorb = False + for (cmd, args) in consume.iteritems(): + if cmd == 'iface': + absorb = True + if absorb: + ifaces.append(consume) + # Now translate + real_ifaces = {} + for info in ifaces: + if 'iface' not in info: + continue + iface_details = info['iface'].split(None) + dev_name = None + if len(iface_details) >= 1: + dev = iface_details[0].strip().lower() + if dev: + dev_name = dev + if not dev_name: + continue + iface_info = {} + if len(iface_details) >= 3: + proto_type = iface_details[2].strip().lower() + # Seems like this can be 'loopback' which we don't + # really care about + if proto_type in ['dhcp', 'static']: + iface_info['bootproto'] = proto_type + # These can just be copied over + for k in ['netmask', 'address', 'gateway', 'broadcast']: + if k in info: + val = info[k].strip().lower() + if val: + iface_info[k] = val + # Is any mac address spoofing going on?? + if 'hwaddress' in info: + hw_info = info['hwaddress'].lower().strip() + hw_split = hw_info.split(None, 1) + if len(hw_split) == 2 and hw_split[0].startswith('ether'): + hw_addr = hw_split[1] + if hw_addr: + iface_info['hwaddress'] = hw_addr + real_ifaces[dev_name] = iface_info + # Check for those that should be started on boot via 'auto' + for (cmd, args) in entries: + if cmd == 'auto': + # Seems like auto can be like 'auto eth0 eth0:1' so just get the + # first part out as the device name + args = args.split(None) + if not args: + continue + dev_name = args[0].strip().lower() + if dev_name in real_ifaces: + real_ifaces[dev_name]['auto'] = True + return real_ifaces diff --git a/cloudinit/distros/ubuntu.py b/cloudinit/distros/ubuntu.py new file mode 100644 index 00000000..77c2aff4 --- /dev/null +++ b/cloudinit/distros/ubuntu.py @@ -0,0 +1,31 @@ +# vi: ts=4 expandtab +# +# Copyright (C) 2012 Canonical Ltd. +# Copyright (C) 2012 Hewlett-Packard Development Company, L.P. +# Copyright (C) 2012 Yahoo! Inc. +# +# Author: Scott Moser <scott.moser@canonical.com> +# Author: Juerg Haefliger <juerg.haefliger@hp.com> +# Author: Joshua Harlow <harlowja@yahoo-inc.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.distros import debian + +from cloudinit import log as logging + +LOG = logging.getLogger(__name__) + + +class Distro(debian.Distro): + pass |