diff options
Diffstat (limited to 'cloudinit/distros')
-rw-r--r-- | cloudinit/distros/__init__.py | 322 | ||||
-rw-r--r-- | cloudinit/distros/debian.py | 11 | ||||
-rw-r--r-- | cloudinit/distros/fedora.py | 1 | ||||
-rw-r--r-- | cloudinit/distros/rhel.py | 135 | ||||
-rw-r--r-- | cloudinit/distros/ubuntu.py | 1 |
5 files changed, 342 insertions, 128 deletions
diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py index bf4317f1..a5d41bdb 100644 --- a/cloudinit/distros/__init__.py +++ b/cloudinit/distros/__init__.py @@ -24,9 +24,8 @@ from StringIO import StringIO import abc -import grp +import itertools import os -import pwd import re from cloudinit import importer @@ -36,12 +35,6 @@ from cloudinit import util from cloudinit.distros import helpers -# TODO(harlowja): Make this via config?? -IFACE_ACTIONS = { - 'up': ['ifup', '--all'], - 'down': ['ifdown', '--all'], -} - LOG = logging.getLogger(__name__) @@ -56,34 +49,6 @@ class Distro(object): self._cfg = cfg self.name = name - def add_default_user(self): - # Adds the distro user using the rules: - # - Password is same as username but is locked - # - nopasswd sudo access - - user = self.get_default_user() - groups = self.get_default_user_groups() - - if not user: - raise NotImplementedError("No Default user") - - user_dict = { - 'name': user, - 'plain_text_passwd': user, - 'home': "/home/%s" % user, - 'shell': "/bin/bash", - 'lock_passwd': True, - 'gecos': "%s%s" % (user[0:1].upper(), user[1:]), - 'sudo': "ALL=(ALL) NOPASSWD:ALL", - } - - if groups: - user_dict['groups'] = groups - - self.create_user(**user_dict) - - LOG.info("Added default '%s' user with passwordless sudo", user) - @abc.abstractmethod def install_packages(self, pkglist): raise NotImplementedError() @@ -120,7 +85,7 @@ class Distro(object): return arch def _get_arch_package_mirror_info(self, arch=None): - mirror_info = self.get_option("package_mirrors", None) + mirror_info = self.get_option("package_mirrors", []) if arch == None: arch = self.get_primary_arch() return _get_arch_package_mirror_info(mirror_info, arch) @@ -130,16 +95,15 @@ class Distro(object): # this resolves the package_mirrors config option # down to a single dict of {mirror_name: mirror_url} arch_info = self._get_arch_package_mirror_info(arch) - return _get_package_mirror_info(availability_zone=availability_zone, mirror_info=arch_info) def apply_network(self, settings, bring_up=True): # Write it out - self._write_network(settings) + dev_names = self._write_network(settings) # Now try to bring them up if bring_up: - return self._interface_action('up') + return self._bring_up_interfaces(dev_names) return False @abc.abstractmethod @@ -192,13 +156,11 @@ class Distro(object): contents.write("%s\n" % (eh)) util.write_file("/etc/hosts", contents.getvalue(), mode=0644) - def _interface_action(self, action): - if action not in IFACE_ACTIONS: - raise NotImplementedError("Unknown interface action %s" % (action)) - cmd = IFACE_ACTIONS[action] + def _bring_up_interface(self, device_name): + cmd = ['ifup', device_name] + LOG.debug("Attempting to run bring up interface %s using command %s", + device_name, cmd) 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) @@ -207,18 +169,32 @@ class Distro(object): util.logexc(LOG, "Running interface command %s failed", cmd) return False - def isuser(self, name): - try: - if pwd.getpwnam(name): - return True - except KeyError: - return False + def _bring_up_interfaces(self, device_names): + am_failed = 0 + for d in device_names: + if not self._bring_up_interface(d): + am_failed += 1 + if am_failed == 0: + return True + return False def get_default_user(self): - return self.default_user - - def get_default_user_groups(self): - return self.default_user_groups + if not self.default_user: + return None + user_cfg = { + 'name': self.default_user, + 'plain_text_passwd': self.default_user, + 'home': "/home/%s" % (self.default_user), + 'shell': "/bin/bash", + 'lock_passwd': True, + 'gecos': "%s" % (self.default_user.title()), + 'sudo': "ALL=(ALL) NOPASSWD:ALL", + } + def_groups = self.default_user_groups + if not def_groups: + def_groups = [] + user_cfg['groups'] = util.uniq_merge_sorted(def_groups) + return user_cfg def create_user(self, name, **kwargs): """ @@ -275,7 +251,7 @@ class Distro(object): adduser_cmd.append('-m') # Create the user - if self.isuser(name): + if util.is_user(name): LOG.warn("User %s already exists, skipping." % name) else: LOG.debug("Creating name %s" % name) @@ -342,7 +318,7 @@ class Distro(object): content += "\n" if not os.path.exists(sudo_file): - util.write_file(sudo_file, content, 0644) + util.write_file(sudo_file, content, 0440) else: try: @@ -352,18 +328,11 @@ class Distro(object): util.logexc(LOG, "Failed to write %s" % sudo_file, e) raise e - def isgroup(self, name): - try: - if grp.getgrnam(name): - return True - except: - return False - def create_group(self, name, members): group_add_cmd = ['groupadd', name] # Check if group exists, and then add it doesn't - if self.isgroup(name): + if util.is_group(name): LOG.warn("Skipping creation of existing group '%s'" % name) else: try: @@ -375,7 +344,7 @@ class Distro(object): # Add members to the group, if so defined if len(members) > 0: for member in members: - if not self.isuser(member): + if not util.is_user(member): LOG.warn("Unable to add group member '%s' to group '%s'" "; user does not exist." % (member, name)) continue @@ -389,6 +358,8 @@ def _get_package_mirror_info(mirror_info, availability_zone=None, # given a arch specific 'mirror_info' entry (from package_mirrors) # search through the 'search' entries, and fallback appropriately # return a dict with only {name: mirror} entries. + if not mirror_info: + mirror_info = {} ec2_az_re = ("^[a-z][a-z]-(%s)-[1-9][0-9]*[a-z]$" % "north|northeast|east|southeast|south|southwest|west|northwest") @@ -433,6 +404,223 @@ def _get_arch_package_mirror_info(package_mirrors, arch): return default +# Normalizes a input group configuration +# which can be a comma seperated list of +# group names, or a list of group names +# or a python dictionary of group names +# to a list of members of that group. +# +# The output is a dictionary of group +# names => members of that group which +# is the standard form used in the rest +# of cloud-init +def _normalize_groups(grp_cfg): + if isinstance(grp_cfg, (str, basestring, list)): + c_grp_cfg = {} + for i in util.uniq_merge(grp_cfg): + c_grp_cfg[i] = [] + grp_cfg = c_grp_cfg + + groups = {} + if isinstance(grp_cfg, (dict)): + for (grp_name, grp_members) in grp_cfg.items(): + groups[grp_name] = util.uniq_merge_sorted(grp_members) + else: + raise TypeError(("Group config must be list, dict " + " or string types only and not %s") % + util.obj_name(grp_cfg)) + return groups + + +# Normalizes a input group configuration +# which can be a comma seperated list of +# user names, or a list of string user names +# or a list of dictionaries with components +# that define the user config + 'name' (if +# a 'name' field does not exist then the +# default user is assumed to 'own' that +# configuration. +# +# The output is a dictionary of user +# names => user config which is the standard +# form used in the rest of cloud-init. Note +# the default user will have a special config +# entry 'default' which will be marked as true +# all other users will be marked as false. +def _normalize_users(u_cfg, def_user_cfg=None): + if isinstance(u_cfg, (dict)): + ad_ucfg = [] + for (k, v) in u_cfg.items(): + if isinstance(v, (bool, int, basestring, str, float)): + if util.is_true(v): + ad_ucfg.append(str(k)) + elif isinstance(v, (dict)): + v['name'] = k + ad_ucfg.append(v) + else: + raise TypeError(("Unmappable user value type %s" + " for key %s") % (util.obj_name(v), k)) + u_cfg = ad_ucfg + elif isinstance(u_cfg, (str, basestring)): + u_cfg = util.uniq_merge_sorted(u_cfg) + + users = {} + for user_config in u_cfg: + if isinstance(user_config, (str, basestring, list)): + for u in util.uniq_merge(user_config): + if u and u not in users: + users[u] = {} + elif isinstance(user_config, (dict)): + if 'name' in user_config: + n = user_config.pop('name') + prev_config = users.get(n) or {} + users[n] = util.mergemanydict([prev_config, + user_config]) + else: + # Assume the default user then + prev_config = users.get('default') or {} + users['default'] = util.mergemanydict([prev_config, + user_config]) + else: + raise TypeError(("User config must be dictionary/list " + " or string types only and not %s") % + util.obj_name(user_config)) + + # Ensure user options are in the right python friendly format + if users: + c_users = {} + for (uname, uconfig) in users.items(): + c_uconfig = {} + for (k, v) in uconfig.items(): + k = k.replace('-', '_').strip() + if k: + c_uconfig[k] = v + c_users[uname] = c_uconfig + users = c_users + + # Fixup the default user into the real + # default user name and replace it... + def_user = None + if users and 'default' in users: + def_config = users.pop('default') + if def_user_cfg: + # Pickup what the default 'real name' is + # and any groups that are provided by the + # default config + def_user = def_user_cfg.pop('name') + def_groups = def_user_cfg.pop('groups', []) + # Pickup any config + groups for that user name + # that we may have previously extracted + parsed_config = users.pop(def_user, {}) + parsed_groups = parsed_config.get('groups', []) + # Now merge our extracted groups with + # anything the default config provided + users_groups = util.uniq_merge_sorted(parsed_groups, def_groups) + parsed_config['groups'] = ",".join(users_groups) + # The real config for the default user is the + # combination of the default user config provided + # by the distro, the default user config provided + # by the above merging for the user 'default' and + # then the parsed config from the user's 'real name' + # which does not have to be 'default' (but could be) + users[def_user] = util.mergemanydict([def_user_cfg, + def_config, + parsed_config]) + + # Ensure that only the default user that we + # found (if any) is actually marked as being + # the default user + if users: + for (uname, uconfig) in users.items(): + if def_user and uname == def_user: + uconfig['default'] = True + else: + uconfig['default'] = False + + return users + + +# Normalizes a set of user/users and group +# dictionary configuration into a useable +# format that the rest of cloud-init can +# understand using the default user +# provided by the input distrobution (if any) +# to allow for mapping of the 'default' user. +# +# Output is a dictionary of group names -> [member] (list) +# and a dictionary of user names -> user configuration (dict) +# +# If 'user' exists it will override +# the 'users'[0] entry (if a list) otherwise it will +# just become an entry in the returned dictionary (no override) +def normalize_users_groups(cfg, distro): + if not cfg: + cfg = {} + users = {} + groups = {} + if 'groups' in cfg: + groups = _normalize_groups(cfg['groups']) + + # Handle the previous style of doing this... + old_user = None + if 'user' in cfg and cfg['user']: + old_user = str(cfg['user']) + if not 'users' in cfg: + cfg['users'] = old_user + old_user = None + if 'users' in cfg: + default_user_config = None + try: + default_user_config = distro.get_default_user() + except NotImplementedError: + LOG.warn(("Distro has not implemented default user " + "access. No default user will be normalized.")) + base_users = cfg['users'] + if old_user: + if isinstance(base_users, (list)): + if len(base_users): + # The old user replaces user[0] + base_users[0] = {'name': old_user} + else: + # Just add it on at the end... + base_users.append({'name': old_user}) + elif isinstance(base_users, (dict)): + if old_user not in base_users: + base_users[old_user] = True + elif isinstance(base_users, (str, basestring)): + # Just append it on to be re-parsed later + base_users += ",%s" % (old_user) + users = _normalize_users(base_users, default_user_config) + return (users, groups) + + +# Given a user dictionary config it will +# extract the default user name and user config +# from that list and return that tuple or +# return (None, None) if no default user is +# found in the given input +def extract_default(users, default_name=None, default_config=None): + if not users: + users = {} + + def safe_find(entry): + config = entry[1] + if not config or 'default' not in config: + return False + else: + return config['default'] + + tmp_users = users.items() + tmp_users = dict(itertools.ifilter(safe_find, tmp_users)) + if not tmp_users: + return (default_name, default_config) + else: + name = tmp_users.keys()[0] + config = tmp_users[name] + config.pop('default', None) + return (name, config) + + def fetch(name): locs = importer.find_module(name, ['', __name__], diff --git a/cloudinit/distros/debian.py b/cloudinit/distros/debian.py index 5b4aa9f8..88f4e978 100644 --- a/cloudinit/distros/debian.py +++ b/cloudinit/distros/debian.py @@ -56,6 +56,17 @@ class Distro(distros.Distro): def _write_network(self, settings): net_fn = self._paths.join(False, "/etc/network/interfaces") util.write_file(net_fn, settings) + return ['all'] + + def _bring_up_interfaces(self, device_names): + use_all = False + for d in device_names: + if d == 'all': + use_all = True + if use_all: + return distros.Distro._bring_up_interface(self, '--all') + else: + return distros.Distro._bring_up_interfaces(self, device_names) def set_hostname(self, hostname): out_fn = self._paths.join(False, "/etc/hostname") diff --git a/cloudinit/distros/fedora.py b/cloudinit/distros/fedora.py index 9f76a116..f65a820d 100644 --- a/cloudinit/distros/fedora.py +++ b/cloudinit/distros/fedora.py @@ -28,5 +28,4 @@ LOG = logging.getLogger(__name__) class Distro(rhel.Distro): - distro_name = 'fedora' default_user = 'ec2-user' diff --git a/cloudinit/distros/rhel.py b/cloudinit/distros/rhel.py index 1c9d493d..13fd5ec8 100644 --- a/cloudinit/distros/rhel.py +++ b/cloudinit/distros/rhel.py @@ -28,6 +28,7 @@ from cloudinit.distros import helpers as d_helpers from cloudinit import helpers from cloudinit import log as logging from cloudinit import util +from cloudinit import version from cloudinit.settings import PER_INSTANCE @@ -58,6 +59,18 @@ D_QUOTE_CHARS = { } +def _make_sysconfig_bool(val): + if val: + return 'yes' + else: + return 'no' + + +def _make_header(): + ci_ver = version.version_string() + return '# Created by cloud-init v. %s' % (ci_ver) + + class Distro(distros.Distro): def __init__(self, name, cfg, paths): @@ -102,81 +115,81 @@ class Distro(distros.Distro): # Make the intermediate format as the rhel format... nameservers = [] searchservers = [] + dev_names = entries.keys() 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() + net_cfg = { + 'DEVICE': dev, + 'NETMASK': info.get('netmask'), + 'IPADDR': info.get('address'), + 'BOOTPROTO': info.get('bootproto'), + 'GATEWAY': info.get('gateway'), + 'BROADCAST': info.get('broadcast'), + 'MACADDR': info.get('hwaddress'), + 'ONBOOT': _make_sysconfig_bool(info.get('auto')), + } + self._update_sysconfig_file(net_fn, net_cfg) if 'dns-nameservers' in info: nameservers.extend(info['dns-nameservers']) if 'dns-search' in info: searchservers.extend(info['dns-search']) - 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) if nameservers or searchservers: - self._adjust_resolve(nameservers, searchservers) + self._write_resolve(nameservers, searchservers) + if dev_names: + net_cfg = { + 'NETWORKING': _make_sysconfig_bool(True), + } + self._update_sysconfig_file("/etc/sysconfig/network", net_cfg) + return dev_names + + def _update_sysconfig_file(self, fn, adjustments, allow_empty=False): + if not adjustments: + return + (exists, contents) = self._read_conf(fn) + updated_am = 0 + for (k, v) in adjustments.items(): + if v is None: + continue + v = str(v) + if len(v) == 0 and not allow_empty: + continue + contents[k] = v + updated_am += 1 + if updated_am: + lines = contents.write() + if not exists: + lines.insert(0, _make_header()) + util.write_file(fn, "\n".join(lines), 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]) + self._write_hostname(hostname, '/etc/sysconfig/network') + 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) + out_fn = '/etc/sysconfig/i18n' + locale_cfg = { + 'LANG': locale, + } + self._update_sysconfig_file(out_fn, locale_cfg) 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) + host_cfg = { + 'HOSTNAME': hostname, + } + self._update_sysconfig_file(out_fn, host_cfg) 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) + hostname_in_sys = self._read_hostname("/etc/sysconfig/network") 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) + update_files.append("/etc/sysconfig/network") for fn in update_files: try: self._write_hostname(hostname, fn) @@ -208,20 +221,24 @@ class Distro(distros.Distro): contents = [] return (exists, QuotingConfigObj(contents)) + def _bring_up_interfaces(self, device_names): + if device_names and 'all' in device_names: + raise RuntimeError(('Distro %s can not translate ' + 'the device name "all"') % (self.name)) + return distros.Distro._bring_up_interfaces(self, device_names) + 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) + clock_cfg = { + 'ZONE': tz, + } + self._update_sysconfig_file("/etc/sysconfig/clock", clock_cfg) # This ensures that the correct tz will be used for the system - util.copy(tz_file, self._paths.join(False, "/etc/localtime")) + util.copy(tz_file, "/etc/localtime") def package_command(self, command, args=None): cmd = ['yum'] diff --git a/cloudinit/distros/ubuntu.py b/cloudinit/distros/ubuntu.py index 22f8c2c5..4e697f82 100644 --- a/cloudinit/distros/ubuntu.py +++ b/cloudinit/distros/ubuntu.py @@ -29,7 +29,6 @@ LOG = logging.getLogger(__name__) class Distro(debian.Distro): - distro_name = 'ubuntu' default_user = 'ubuntu' default_user_groups = ("adm,audio,cdrom,dialout,floppy,video," "plugdev,dip,netdev,sudo") |