diff options
Diffstat (limited to 'cloudinit/distros/__init__.py')
-rw-r--r-- | cloudinit/distros/__init__.py | 626 |
1 files changed, 482 insertions, 144 deletions
diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py index 40c6aa4f..6a684b89 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 @@ -34,54 +33,22 @@ from cloudinit import log as logging from cloudinit import ssh_util from cloudinit import util -# TODO(harlowja): Make this via config?? -IFACE_ACTIONS = { - 'up': ['ifup', '--all'], - 'down': ['ifdown', '--all'], -} +from cloudinit.distros.parsers import hosts LOG = logging.getLogger(__name__) class Distro(object): - __metaclass__ = abc.ABCMeta - default_user = None - default_user_groups = None + hosts_fn = "/etc/hosts" + ci_sudoers_fn = "/etc/sudoers.d/90-cloud-init-users" + hostname_conf_fn = "/etc/hostname" def __init__(self, name, cfg, paths): self._paths = paths 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() @@ -95,13 +62,10 @@ class Distro(object): 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() + def set_hostname(self, hostname, fqdn=None): + writeable_hostname = self._select_hostname(hostname, fqdn) + self._write_hostname(writeable_hostname, self.hostname_conf_fn) + self._apply_hostname(hostname) @abc.abstractmethod def package_command(self, cmd, args=None): @@ -118,26 +82,25 @@ class Distro(object): return arch def _get_arch_package_mirror_info(self, arch=None): - mirror_info = self.get_option("package_mirrors", None) - if arch == None: + mirror_info = self.get_option("package_mirrors", []) + if not arch: arch = self.get_primary_arch() return _get_arch_package_mirror_info(mirror_info, arch) def get_package_mirror_info(self, arch=None, availability_zone=None): - # this resolves the package_mirrors config option + # 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 @@ -151,51 +114,137 @@ class Distro(object): def _get_localhost_ip(self): return "127.0.0.1" + @abc.abstractmethod + def _read_hostname(self, filename, default=None): + raise NotImplementedError() + + @abc.abstractmethod + def _write_hostname(self, hostname, filename): + raise NotImplementedError() + + @abc.abstractmethod + def _read_system_hostname(self): + raise NotImplementedError() + + def _apply_hostname(self, hostname): + # This really only sets the hostname + # temporarily (until reboot so it should + # not be depended on). Use the write + # hostname functions for 'permanent' adjustments. + LOG.debug("Non-persistently setting the system hostname to %s", + hostname) + try: + util.subp(['hostname', hostname]) + except util.ProcessExecutionError: + util.logexc(LOG, ("Failed to non-persistently adjust" + " the system hostname to %s"), hostname) + + @abc.abstractmethod + def _select_hostname(self, hostname, fqdn): + raise NotImplementedError() + + def update_hostname(self, hostname, fqdn, prev_hostname_fn): + applying_hostname = hostname + + # Determine what the actual written hostname should be + hostname = self._select_hostname(hostname, fqdn) + + # If the previous hostname file exists lets see if we + # can get a hostname from it + if prev_hostname_fn and os.path.exists(prev_hostname_fn): + prev_hostname = self._read_hostname(prev_hostname_fn) + else: + prev_hostname = None + + # Lets get where we should write the system hostname + # and what the system hostname is + (sys_fn, sys_hostname) = self._read_system_hostname() + update_files = [] + + # If there is no previous hostname or it differs + # from what we want, lets update it or create the + # file in the first place + if not prev_hostname or prev_hostname != hostname: + update_files.append(prev_hostname_fn) + + # If the system hostname is different than the previous + # one or the desired one lets update it as well + if (not sys_hostname) or (sys_hostname == prev_hostname + and sys_hostname != hostname): + update_files.append(sys_fn) + + # Remove duplicates (incase the previous config filename) + # is the same as the system config filename, don't bother + # doing it twice + update_files = set([f for f in update_files if f]) + LOG.debug("Attempting to update hostname to %s in %s files", + hostname, len(update_files)) + + for fn in update_files: + try: + self._write_hostname(hostname, fn) + except IOError: + util.logexc(LOG, "Failed to write hostname %s to %s", + hostname, fn) + + if (sys_hostname and prev_hostname and + sys_hostname != prev_hostname): + LOG.debug("%s differs from %s, assuming user maintained hostname.", + prev_hostname_fn, sys_fn) + + # If the system hostname file name was provided set the + # non-fqdn as the transient hostname. + if sys_fn in update_files: + self._apply_hostname(applying_hostname) + 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()) + header = '' + if os.path.exists(self.hosts_fn): + eh = hosts.HostsConf(util.load_file(self.hosts_fn)) + else: + eh = hosts.HostsConf('') + header = util.make_header(base="added") 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)) + prev_info = eh.get_entry(local_ip) + need_change = False + if not prev_info: + eh.add_entry(local_ip, fqdn, hostname) + need_change = True + else: + need_change = True + for entry in prev_info: + entry_fqdn = None + entry_aliases = [] + if len(entry) >= 1: + entry_fqdn = entry[0] + if len(entry) >= 2: + entry_aliases = entry[1:] + if entry_fqdn is not None and entry_fqdn == fqdn: + if hostname in entry_aliases: + # Exists already, leave it be + need_change = False + if need_change: + # Doesn't exist, add that entry in... + new_entries = list(prev_info) + new_entries.append([fqdn, hostname]) + eh.del_entries(local_ip) + for entry in new_entries: + if len(entry) == 1: + eh.add_entry(local_ip, entry[0]) + elif len(entry) >= 2: + eh.add_entry(local_ip, *entry) 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] + contents = StringIO() + if header: + contents.write("%s\n" % (header)) + contents.write("%s\n" % (eh)) + util.write_file(self.hosts_fn, contents.getvalue(), mode=0644) + + 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) @@ -204,18 +253,17 @@ 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 + return self.get_option('default_user') def create_user(self, name, **kwargs): """ @@ -231,22 +279,23 @@ class Distro(object): # inputs. If something goes wrong, we can end up with a system # that nobody can login to. adduser_opts = { - "gecos": '--comment', - "homedir": '--home', - "primary_group": '--gid', - "groups": '--groups', - "passwd": '--password', - "shell": '--shell', - "expiredate": '--expiredate', - "inactive": '--inactive', - } + "gecos": '--comment', + "homedir": '--home', + "primary_group": '--gid', + "groups": '--groups', + "passwd": '--password', + "shell": '--shell', + "expiredate": '--expiredate', + "inactive": '--inactive', + "selinux_user": '--selinux-user', + } adduser_opts_flags = { - "no_user_group": '--no-user-group', - "system": '--system', - "no_log_init": '--no-log-init', - "no_create_home": "-M", - } + "no_user_group": '--no-user-group', + "system": '--system', + "no_log_init": '--no-log-init', + "no_create_home": "-M", + } # Now check the value and create the command for option in kwargs: @@ -271,10 +320,10 @@ 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) + LOG.debug("Adding user named %s", name) try: util.subp(adduser_cmd, logstring=x_adduser_cmd) except Exception as e: @@ -303,7 +352,7 @@ class Distro(object): # Import SSH keys if 'ssh_authorized_keys' in kwargs: keys = set(kwargs['ssh_authorized_keys']) or [] - ssh_util.setup_user_keys(keys, name, None, self._paths) + ssh_util.setup_user_keys(keys, name, key_prefix=None) return True @@ -322,44 +371,89 @@ class Distro(object): return True - def write_sudo_rules(self, - user, - rules, - sudo_file="/etc/sudoers.d/90-cloud-init-users", - ): + def ensure_sudo_dir(self, path, sudo_base='/etc/sudoers'): + # Ensure the dir is included and that + # it actually exists as a directory + sudoers_contents = '' + base_exists = False + if os.path.exists(sudo_base): + sudoers_contents = util.load_file(sudo_base) + base_exists = True + found_include = False + for line in sudoers_contents.splitlines(): + line = line.strip() + include_match = re.search(r"^#includedir\s+(.*)$", line) + if not include_match: + continue + included_dir = include_match.group(1).strip() + if not included_dir: + continue + included_dir = os.path.abspath(included_dir) + if included_dir == path: + found_include = True + break + if not found_include: + try: + if not base_exists: + lines = [('# See sudoers(5) for more information' + ' on "#include" directives:'), '', + util.make_header(base="added"), + "#includedir %s" % (path), ''] + sudoers_contents = "\n".join(lines) + util.write_file(sudo_base, sudoers_contents, 0440) + else: + lines = ['', util.make_header(base="added"), + "#includedir %s" % (path), ''] + sudoers_contents = "\n".join(lines) + util.append_file(sudo_base, sudoers_contents) + LOG.debug("Added '#includedir %s' to %s" % (path, sudo_base)) + except IOError as e: + util.logexc(LOG, "Failed to write %s" % sudo_base, e) + raise e + util.ensure_dir(path, 0750) - content_header = "# user rules for %s" % user - content = "%s\n%s %s\n\n" % (content_header, user, rules) + def write_sudo_rules(self, user, rules, sudo_file=None): + if not sudo_file: + sudo_file = self.ci_sudoers_fn - if isinstance(rules, list): - content = "%s\n" % content_header + lines = [ + '', + "# User rules for %s" % user, + ] + if isinstance(rules, (list, tuple)): for rule in rules: - content += "%s %s\n" % (user, rule) - content += "\n" + lines.append("%s %s" % (user, rule)) + elif isinstance(rules, (basestring, str)): + lines.append("%s %s" % (user, rules)) + else: + msg = "Can not create sudoers rule addition with type %r" + raise TypeError(msg % (util.obj_name(rules))) + content = "\n".join(lines) + content += "\n" # trailing newline + self.ensure_sudo_dir(os.path.dirname(sudo_file)) if not os.path.exists(sudo_file): - util.write_file(sudo_file, content, 0644) - + contents = [ + util.make_header(), + content, + ] + try: + util.write_file(sudo_file, "\n".join(contents), 0440) + except IOError as e: + util.logexc(LOG, "Failed to write sudoers file %s", sudo_file) + raise e else: try: - with open(sudo_file, 'a') as f: - f.write(content) + util.append_file(sudo_file, content) except IOError as e: - util.logexc(LOG, "Failed to write %s" % sudo_file, e) + util.logexc(LOG, "Failed to append sudoers file %s", sudo_file) 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: @@ -371,7 +465,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 @@ -385,6 +479,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") @@ -429,6 +525,248 @@ 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)): + grp_cfg = grp_cfg.strip().split(",") + if isinstance(grp_cfg, (list)): + c_grp_cfg = {} + for i in grp_cfg: + if isinstance(i, (dict)): + for k, v in i.items(): + if k not in c_grp_cfg: + if isinstance(v, (list)): + c_grp_cfg[k] = list(v) + elif isinstance(v, (basestring, str)): + c_grp_cfg[k] = [v] + else: + raise TypeError("Bad group member type %s" % + util.obj_name(v)) + else: + if isinstance(v, (list)): + c_grp_cfg[k].extend(v) + elif isinstance(v, (basestring, str)): + c_grp_cfg[k].append(v) + else: + raise TypeError("Bad group member type %s" % + util.obj_name(v)) + elif isinstance(i, (str, basestring)): + if i not in c_grp_cfg: + c_grp_cfg[i] = [] + else: + raise TypeError("Unknown group name type %s" % + util.obj_name(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_cfg = def_user_cfg.copy() + 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__], |