diff options
author | harlowja <harlowja@virtualbox.rhel> | 2013-02-21 22:51:02 -0800 |
---|---|---|
committer | harlowja <harlowja@virtualbox.rhel> | 2013-02-21 22:51:02 -0800 |
commit | 575a084808db7d5ac607a848b018abe676e73a91 (patch) | |
tree | 34e179b0623074e6cd6fc03e4b3db001f8e493bf /cloudinit/distros | |
parent | 9dfb60d3144860334ab1ad1d72920d962139461f (diff) | |
parent | d4886b65549c886499141872a9928412a74bbea2 (diff) | |
download | vyos-cloud-init-575a084808db7d5ac607a848b018abe676e73a91.tar.gz vyos-cloud-init-575a084808db7d5ac607a848b018abe676e73a91.zip |
Update to code on trunk.
Diffstat (limited to 'cloudinit/distros')
-rw-r--r-- | cloudinit/distros/__init__.py | 474 | ||||
-rw-r--r-- | cloudinit/distros/debian.py | 143 | ||||
-rw-r--r-- | cloudinit/distros/fedora.py | 2 | ||||
-rw-r--r-- | cloudinit/distros/parsers/__init__.py | 28 | ||||
-rw-r--r-- | cloudinit/distros/parsers/hostname.py | 88 | ||||
-rw-r--r-- | cloudinit/distros/parsers/hosts.py | 92 | ||||
-rw-r--r-- | cloudinit/distros/parsers/resolv_conf.py | 169 | ||||
-rw-r--r-- | cloudinit/distros/parsers/sys_conf.py | 113 | ||||
-rw-r--r-- | cloudinit/distros/rhel.py | 204 | ||||
-rw-r--r-- | cloudinit/distros/ubuntu.py | 5 |
10 files changed, 981 insertions, 337 deletions
diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py index 21efe8d9..0db4aac7 100644 --- a/cloudinit/distros/__init__.py +++ b/cloudinit/distros/__init__.py @@ -33,14 +33,21 @@ from cloudinit import log as logging from cloudinit import ssh_util from cloudinit import util +from cloudinit.distros.parsers import hosts + +OSFAMILIES = { + 'debian': ['debian', 'ubuntu'], + 'redhat': ['fedora', 'rhel'] +} + 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 @@ -60,13 +67,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): @@ -84,13 +88,13 @@ class Distro(object): def _get_arch_package_mirror_info(self, arch=None): mirror_info = self.get_option("package_mirrors", []) - if arch == None: + 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, @@ -115,43 +119,141 @@ 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() + + @staticmethod + def expand_osfamily(family_list): + distros = [] + for family in family_list: + if not family in OSFAMILIES: + raise ValueError("No distibutions found for osfamily %s" + % (family)) + distros.extend(OSFAMILIES[family]) + return distros + + 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) + 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] @@ -176,22 +278,7 @@ class Distro(object): return False def get_default_user(self): - 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 + return self.get_option('default_user') def create_user(self, name, **kwargs): """ @@ -207,23 +294,25 @@ 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', - "selinux_user": '--selinux-user', - } + "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", + } + + redact_fields = ['passwd'] # Now check the value and create the command for option in kwargs: @@ -231,16 +320,18 @@ class Distro(object): if option in adduser_opts and value \ and isinstance(value, str): adduser_cmd.extend([adduser_opts[option], value]) - - # Redact the password field from the logs - if option != "password": - x_adduser_cmd.extend([adduser_opts[option], value]) - else: + # Redact certain fields from the logs + if option in redact_fields: x_adduser_cmd.extend([adduser_opts[option], 'REDACTED']) - + else: + x_adduser_cmd.extend([adduser_opts[option], value]) elif option in adduser_opts_flags and value: adduser_cmd.append(adduser_opts_flags[option]) - x_adduser_cmd.append(adduser_opts_flags[option]) + # Redact certain fields from the logs + if option in redact_fields: + x_adduser_cmd.append('REDACTED') + else: + x_adduser_cmd.append(adduser_opts_flags[option]) # Default to creating home directory unless otherwise directed # Also, we do not create home directories for system users. @@ -251,7 +342,7 @@ class Distro(object): 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: @@ -262,10 +353,9 @@ class Distro(object): if 'plain_text_passwd' in kwargs and kwargs['plain_text_passwd']: self.set_passwd(name, kwargs['plain_text_passwd']) - # Default locking down the account. - if ('lock_passwd' not in kwargs and - ('lock_passwd' in kwargs and kwargs['lock_passwd']) or - 'system' not in kwargs): + # Default locking down the account. 'lock_passwd' defaults to True. + # lock account unless lock_password is False. + if kwargs.get('lock_passwd', True): try: util.subp(['passwd', '--lock', name]) except Exception as e: @@ -280,7 +370,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 @@ -299,30 +389,82 @@ 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, 0440) - + 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 create_group(self, name, members): @@ -412,12 +554,36 @@ def _get_arch_package_mirror_info(package_mirrors, arch): # is the standard form used in the rest # of cloud-init def _normalize_groups(grp_cfg): - if isinstance(grp_cfg, (str, basestring, list)): + if isinstance(grp_cfg, (str, basestring)): + grp_cfg = grp_cfg.strip().split(",") + if isinstance(grp_cfg, (list)): c_grp_cfg = {} - for i in util.uniq_merge(grp_cfg): - c_grp_cfg[i] = [] + 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(): @@ -439,7 +605,7 @@ def _normalize_groups(grp_cfg): # configuration. # # The output is a dictionary of user -# names => user config which is the standard +# 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 @@ -504,6 +670,7 @@ def _normalize_users(u_cfg, def_user_cfg=None): # 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 @@ -553,41 +720,68 @@ def _normalize_users(u_cfg, def_user_cfg=None): 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 + # Handle the previous style of doing this where the first user + # overrides the concept of the default user if provided in the user: XYZ + # format. + old_user = {} 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) + old_user = cfg['user'] + # Translate it into the format that is more useful + # going forward + if isinstance(old_user, (basestring, str)): + old_user = { + 'name': old_user, + } + if not isinstance(old_user, (dict)): + LOG.warn(("Format for 'user' key must be a string or " + "dictionary and not %s"), util.obj_name(old_user)) + old_user = {} + + # If no old user format, then assume the distro + # provides what the 'default' user maps to, but notice + # that if this is provided, we won't automatically inject + # a 'default' user into the users list, while if a old user + # format is provided we will. + distro_user_config = {} + try: + distro_user_config = distro.get_default_user() + except NotImplementedError: + LOG.warn(("Distro has not implemented default user " + "access. No distribution provided default user" + " will be normalized.")) + + # Merge the old user (which may just be an empty dict when not + # present with the distro provided default user configuration so + # that the old user style picks up all the distribution specific + # attributes (if any) + default_user_config = util.mergemanydict([old_user, distro_user_config]) + + base_users = cfg.get('users', []) + if not isinstance(base_users, (list, dict, str, basestring)): + LOG.warn(("Format for 'users' key must be a comma separated string" + " or a dictionary or a list and not %s"), + util.obj_name(base_users)) + base_users = [] + + if old_user: + # Ensure that when user: is provided that this user + # always gets added (as the default user) + if isinstance(base_users, (list)): + # Just add it on at the end... + base_users.append({'name': 'default'}) + elif isinstance(base_users, (dict)): + base_users['default'] = base_users.get('default', True) + elif isinstance(base_users, (str, basestring)): + # Just append it on to be re-parsed later + base_users += ",default" + + users = _normalize_users(base_users, default_user_config) return (users, groups) diff --git a/cloudinit/distros/debian.py b/cloudinit/distros/debian.py index 88f4e978..1a8e927b 100644 --- a/cloudinit/distros/debian.py +++ b/cloudinit/distros/debian.py @@ -27,12 +27,20 @@ from cloudinit import helpers from cloudinit import log as logging from cloudinit import util +from cloudinit.distros.parsers.hostname import HostnameConf + from cloudinit.settings import PER_INSTANCE LOG = logging.getLogger(__name__) class Distro(distros.Distro): + hostname_conf_fn = "/etc/hostname" + locale_conf_fn = "/etc/default/locale" + network_conf_fn = "/etc/network/interfaces" + tz_conf_fn = "/etc/timezone" + tz_local_fn = "/etc/localtime" + tz_zone_dir = "/usr/share/zoneinfo" def __init__(self, name, cfg, paths): distros.Distro.__init__(self, name, cfg, paths) @@ -40,22 +48,27 @@ class Distro(distros.Distro): # calls from repeatly happening (when they # should only happen say once per instance...) self._runner = helpers.Runners(paths) + self.osfamily = 'debian' def apply_locale(self, locale, out_fn=None): if not out_fn: - out_fn = self._paths.join(False, '/etc/default/locale') + out_fn = self.locale_conf_fn util.subp(['locale-gen', locale], capture=False) util.subp(['update-locale', locale], capture=False) - lines = ["# Created by cloud-init", 'LANG="%s"' % (locale), ""] + # "" provides trailing newline during join + lines = [ + util.make_header(), + 'LANG="%s"' % (locale), + "", + ] util.write_file(out_fn, "\n".join(lines)) def install_packages(self, pkglist): self.update_package_sources() - self.package_command('install', pkglist) + self.package_command('install', pkgs=pkglist) def _write_network(self, settings): - net_fn = self._paths.join(False, "/etc/network/interfaces") - util.write_file(net_fn, settings) + util.write_file(self.network_conf_fn, settings) return ['all'] def _bring_up_interfaces(self, device_names): @@ -68,81 +81,85 @@ class Distro(distros.Distro): else: return distros.Distro._bring_up_interfaces(self, device_names) - 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): - # "" gives trailing newline. - util.write_file(out_fn, "%s\n" % str(hostname), 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 _select_hostname(self, hostname, fqdn): + # Prefer the short hostname over the long + # fully qualified domain name + if not hostname: + return fqdn + return hostname + + def _write_hostname(self, your_hostname, out_fn): + conf = None + try: + # Try to update the previous one + # so lets see if we can read it first. + conf = self._read_hostname_conf(out_fn) + except IOError: + pass + if not conf: + conf = HostnameConf('') + conf.set_hostname(your_hostname) + util.write_file(out_fn, str(conf), 0644) + + def _read_system_hostname(self): + sys_hostname = self._read_hostname(self.hostname_conf_fn) + return (self.hostname_conf_fn, sys_hostname) + + def _read_hostname_conf(self, filename): + conf = HostnameConf(util.load_file(filename)) + conf.parse() + return conf 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 + hostname = None + try: + conf = self._read_hostname_conf(filename) + hostname = conf.hostname + except IOError: + pass + if not hostname: + return default + return hostname 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) + # TODO(harlowja): move this code into + # the parent distro... + tz_file = os.path.join(self.tz_zone_dir, str(tz)) if not os.path.isfile(tz_file): raise RuntimeError(("Invalid timezone %s," " no file found at %s") % (tz, tz_file)) - # "" provides trailing newline during join - tz_lines = ["# Created by cloud-init", str(tz), ""] - tz_fn = self._paths.join(False, "/etc/timezone") - util.write_file(tz_fn, "\n".join(tz_lines)) - util.copy(tz_file, self._paths.join(False, "/etc/localtime")) - - def package_command(self, command, args=None): + # Note: "" provides trailing newline during join + tz_lines = [ + util.make_header(), + str(tz), + "", + ] + util.write_file(self.tz_conf_fn, "\n".join(tz_lines)) + # This ensures that the correct tz will be used for the system + util.copy(tz_file, self.tz_local_fn) + + def package_command(self, command, args=None, pkgs=[]): 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: + '--assume-yes', '--quiet'] + + if args and isinstance(args, str): + cmd.append(args) + elif args and isinstance(args, list): cmd.extend(args) + + cmd.append(command) + + pkglist = util.expand_package_list('%s=%s', pkgs) + cmd.extend(pkglist) + # Allow the output of this to flow outwards (ie not be captured) util.subp(cmd, env=e, capture=False) diff --git a/cloudinit/distros/fedora.py b/cloudinit/distros/fedora.py index f65a820d..c777845d 100644 --- a/cloudinit/distros/fedora.py +++ b/cloudinit/distros/fedora.py @@ -28,4 +28,4 @@ LOG = logging.getLogger(__name__) class Distro(rhel.Distro): - default_user = 'ec2-user' + pass diff --git a/cloudinit/distros/parsers/__init__.py b/cloudinit/distros/parsers/__init__.py new file mode 100644 index 00000000..1c413eaa --- /dev/null +++ b/cloudinit/distros/parsers/__init__.py @@ -0,0 +1,28 @@ +# vi: ts=4 expandtab +# +# Copyright (C) 2012 Yahoo! Inc. +# +# 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/>. + + +def chop_comment(text, comment_chars): + comment_locations = [text.find(c) for c in comment_chars] + comment_locations = [c for c in comment_locations if c != -1] + if not comment_locations: + return (text, '') + min_comment = min(comment_locations) + before_comment = text[0:min_comment] + comment = text[min_comment:] + return (before_comment, comment) diff --git a/cloudinit/distros/parsers/hostname.py b/cloudinit/distros/parsers/hostname.py new file mode 100644 index 00000000..617b3c36 --- /dev/null +++ b/cloudinit/distros/parsers/hostname.py @@ -0,0 +1,88 @@ +# vi: ts=4 expandtab +# +# Copyright (C) 2012 Yahoo! Inc. +# +# 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 + +from cloudinit.distros.parsers import chop_comment + + +# Parser that knows how to work with /etc/hostname format +class HostnameConf(object): + def __init__(self, text): + self._text = text + self._contents = None + + def parse(self): + if self._contents is None: + self._contents = self._parse(self._text) + + def __str__(self): + self.parse() + contents = StringIO() + for (line_type, components) in self._contents: + if line_type == 'blank': + contents.write("%s\n" % (components[0])) + elif line_type == 'all_comment': + contents.write("%s\n" % (components[0])) + elif line_type == 'hostname': + (hostname, tail) = components + contents.write("%s%s\n" % (hostname, tail)) + # Ensure trailing newline + contents = contents.getvalue() + if not contents.endswith("\n"): + contents += "\n" + return contents + + @property + def hostname(self): + self.parse() + for (line_type, components) in self._contents: + if line_type == 'hostname': + return components[0] + return None + + def set_hostname(self, your_hostname): + your_hostname = your_hostname.strip() + if not your_hostname: + return + self.parse() + replaced = False + for (line_type, components) in self._contents: + if line_type == 'hostname': + components[0] = str(your_hostname) + replaced = True + if not replaced: + self._contents.append(('hostname', [str(your_hostname), ''])) + + def _parse(self, contents): + entries = [] + hostnames_found = set() + for line in contents.splitlines(): + if not len(line.strip()): + entries.append(('blank', [line])) + continue + (head, tail) = chop_comment(line.strip(), '#') + if not len(head): + entries.append(('all_comment', [line])) + continue + entries.append(('hostname', [head, tail])) + hostnames_found.add(head) + if len(hostnames_found) > 1: + raise IOError("Multiple hostnames (%s) found!" + % (hostnames_found)) + return entries diff --git a/cloudinit/distros/parsers/hosts.py b/cloudinit/distros/parsers/hosts.py new file mode 100644 index 00000000..94c97051 --- /dev/null +++ b/cloudinit/distros/parsers/hosts.py @@ -0,0 +1,92 @@ +# vi: ts=4 expandtab +# +# Copyright (C) 2012 Yahoo! Inc. +# +# 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 + +from cloudinit.distros.parsers import chop_comment + + +# See: man hosts +# or http://unixhelp.ed.ac.uk/CGI/man-cgi?hosts +# or http://tinyurl.com/6lmox3 +class HostsConf(object): + def __init__(self, text): + self._text = text + self._contents = None + + def parse(self): + if self._contents is None: + self._contents = self._parse(self._text) + + def get_entry(self, ip): + self.parse() + options = [] + for (line_type, components) in self._contents: + if line_type == 'option': + (pieces, _tail) = components + if len(pieces) and pieces[0] == ip: + options.append(pieces[1:]) + return options + + def del_entries(self, ip): + self.parse() + n_entries = [] + for (line_type, components) in self._contents: + if line_type != 'option': + n_entries.append((line_type, components)) + continue + else: + (pieces, _tail) = components + if len(pieces) and pieces[0] == ip: + pass + elif len(pieces): + n_entries.append((line_type, list(components))) + self._contents = n_entries + + def add_entry(self, ip, canonical_hostname, *aliases): + self.parse() + self._contents.append(('option', + ([ip, canonical_hostname] + list(aliases), ''))) + + def _parse(self, contents): + entries = [] + for line in contents.splitlines(): + if not len(line.strip()): + entries.append(('blank', [line])) + continue + (head, tail) = chop_comment(line.strip(), '#') + if not len(head): + entries.append(('all_comment', [line])) + continue + entries.append(('option', [head.split(None), tail])) + return entries + + def __str__(self): + self.parse() + contents = StringIO() + for (line_type, components) in self._contents: + if line_type == 'blank': + contents.write("%s\n" % (components[0])) + elif line_type == 'all_comment': + contents.write("%s\n" % (components[0])) + elif line_type == 'option': + (pieces, tail) = components + pieces = [str(p) for p in pieces] + pieces = "\t".join(pieces) + contents.write("%s%s\n" % (pieces, tail)) + return contents.getvalue() diff --git a/cloudinit/distros/parsers/resolv_conf.py b/cloudinit/distros/parsers/resolv_conf.py new file mode 100644 index 00000000..5733c25a --- /dev/null +++ b/cloudinit/distros/parsers/resolv_conf.py @@ -0,0 +1,169 @@ +# vi: ts=4 expandtab +# +# Copyright (C) 2012 Yahoo! Inc. +# +# 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 + +from cloudinit import util + +from cloudinit.distros.parsers import chop_comment + + +# See: man resolv.conf +class ResolvConf(object): + def __init__(self, text): + self._text = text + self._contents = None + + def parse(self): + if self._contents is None: + self._contents = self._parse(self._text) + + @property + def nameservers(self): + self.parse() + return self._retr_option('nameserver') + + @property + def local_domain(self): + self.parse() + dm = self._retr_option('domain') + if dm: + return dm[0] + return None + + @property + def search_domains(self): + self.parse() + current_sds = self._retr_option('search') + flat_sds = [] + for sdlist in current_sds: + for sd in sdlist.split(None): + if sd: + flat_sds.append(sd) + return flat_sds + + def __str__(self): + self.parse() + contents = StringIO() + for (line_type, components) in self._contents: + if line_type == 'blank': + contents.write("\n") + elif line_type == 'all_comment': + contents.write("%s\n" % (components[0])) + elif line_type == 'option': + (cfg_opt, cfg_value, comment_tail) = components + line = "%s %s" % (cfg_opt, cfg_value) + if len(comment_tail): + line += comment_tail + contents.write("%s\n" % (line)) + return contents.getvalue() + + def _retr_option(self, opt_name): + found = [] + for (line_type, components) in self._contents: + if line_type == 'option': + (cfg_opt, cfg_value, _comment_tail) = components + if cfg_opt == opt_name: + found.append(cfg_value) + return found + + def add_nameserver(self, ns): + self.parse() + current_ns = self._retr_option('nameserver') + new_ns = list(current_ns) + new_ns.append(str(ns)) + new_ns = util.uniq_list(new_ns) + if len(new_ns) == len(current_ns): + return current_ns + if len(current_ns) >= 3: + # Hard restriction on only 3 name servers + raise ValueError(("Adding %r would go beyond the " + "'3' maximum name servers") % (ns)) + self._remove_option('nameserver') + for n in new_ns: + self._contents.append(('option', ['nameserver', n, ''])) + return new_ns + + def _remove_option(self, opt_name): + + def remove_opt(item): + line_type, components = item + if line_type != 'option': + return False + (cfg_opt, _cfg_value, _comment_tail) = components + if cfg_opt != opt_name: + return False + return True + + new_contents = [] + for c in self._contents: + if not remove_opt(c): + new_contents.append(c) + self._contents = new_contents + + def add_search_domain(self, search_domain): + flat_sds = self.search_domains + new_sds = list(flat_sds) + new_sds.append(str(search_domain)) + new_sds = util.uniq_list(new_sds) + if len(flat_sds) == len(new_sds): + return new_sds + if len(flat_sds) >= 6: + # Hard restriction on only 6 search domains + raise ValueError(("Adding %r would go beyond the " + "'6' maximum search domains") % (search_domain)) + s_list = " ".join(new_sds) + if len(s_list) > 256: + # Some hard limit on 256 chars total + raise ValueError(("Adding %r would go beyond the " + "256 maximum search list character limit") + % (search_domain)) + self._remove_option('search') + self._contents.append(('option', ['search', s_list, ''])) + return flat_sds + + @local_domain.setter + def local_domain(self, domain): + self.parse() + self._remove_option('domain') + self._contents.append(('option', ['domain', str(domain), ''])) + return domain + + def _parse(self, contents): + entries = [] + for (i, line) in enumerate(contents.splitlines()): + sline = line.strip() + if not sline: + entries.append(('blank', [line])) + continue + (head, tail) = chop_comment(line, ';#') + if not len(head.strip()): + entries.append(('all_comment', [line])) + continue + if not tail: + tail = '' + try: + (cfg_opt, cfg_values) = head.split(None, 1) + except (IndexError, ValueError): + raise IOError("Incorrectly formatted resolv.conf line %s" + % (i + 1)) + if cfg_opt not in ['nameserver', 'domain', + 'search', 'sortlist', 'options']: + raise IOError("Unexpected resolv.conf option %s" % (cfg_opt)) + entries.append(("option", [cfg_opt, cfg_values, tail])) + return entries diff --git a/cloudinit/distros/parsers/sys_conf.py b/cloudinit/distros/parsers/sys_conf.py new file mode 100644 index 00000000..20ca1871 --- /dev/null +++ b/cloudinit/distros/parsers/sys_conf.py @@ -0,0 +1,113 @@ +# vi: ts=4 expandtab +# +# Copyright (C) 2012 Yahoo! Inc. +# +# 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 pipes +import re + +# This library is used to parse/write +# out the various sysconfig files edited (best attempt effort) +# +# It has to be slightly modified though +# to ensure that all values are quoted/unquoted correctly +# since these configs are usually sourced into +# bash scripts... +import configobj + +# See: http://pubs.opengroup.org/onlinepubs/000095399/basedefs/xbd_chap08.html +# or look at the 'param_expand()' function in the subst.c file in the bash +# source tarball... +SHELL_VAR_RULE = r'[a-zA-Z_]+[a-zA-Z0-9_]*' +SHELL_VAR_REGEXES = [ + # Basic variables + re.compile(r"\$" + SHELL_VAR_RULE), + # Things like $?, $0, $-, $@ + re.compile(r"\$[0-9#\?\-@\*]"), + # Things like ${blah:1} - but this one + # gets very complex so just try the + # simple path + re.compile(r"\$\{.+\}"), +] + + +def _contains_shell_variable(text): + for r in SHELL_VAR_REGEXES: + if r.search(text): + return True + return False + + +class SysConf(configobj.ConfigObj): + def __init__(self, contents): + configobj.ConfigObj.__init__(self, contents, + interpolation=False, + write_empty_values=True) + + def __str__(self): + contents = self.write() + out_contents = StringIO() + if isinstance(contents, (list, tuple)): + out_contents.write("\n".join(contents)) + else: + out_contents.write(str(contents)) + return out_contents.getvalue() + + def _quote(self, value, multiline=False): + if not isinstance(value, (str, basestring)): + raise ValueError('Value "%s" is not a string' % (value)) + if len(value) == 0: + return '' + quot_func = None + if value[0] in ['"', "'"] and value[-1] in ['"', "'"]: + if len(value) == 1: + quot_func = (lambda x: + self._get_single_quote(x) % x) + else: + # Quote whitespace if it isn't the start + end of a shell command + if value.strip().startswith("$(") and value.strip().endswith(")"): + pass + else: + if re.search(r"[\t\r\n ]", value): + if _contains_shell_variable(value): + # If it contains shell variables then we likely want to + # leave it alone since the pipes.quote function likes + # to use single quotes which won't get expanded... + if re.search(r"[\n\"']", value): + quot_func = (lambda x: + self._get_triple_quote(x) % x) + else: + quot_func = (lambda x: + self._get_single_quote(x) % x) + else: + quot_func = pipes.quote + if not quot_func: + return value + return quot_func(value) + + 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)) + key = self._decode_element(self._quote(entry)) + cmnt = self._decode_element(comment) + return '%s%s%s%s%s' % (indent_string, + key, + self._a_to_u('='), + val, + cmnt) diff --git a/cloudinit/distros/rhel.py b/cloudinit/distros/rhel.py index bf3c18d2..2f91e386 100644 --- a/cloudinit/distros/rhel.py +++ b/cloudinit/distros/rhel.py @@ -23,39 +23,18 @@ import os from cloudinit import distros + +from cloudinit.distros.parsers.resolv_conf import ResolvConf +from cloudinit.distros.parsers.sys_conf import SysConf + from cloudinit import helpers from cloudinit import log as logging from cloudinit import util -from cloudinit import version 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 = { - "\"": "\\\"", - "(": "\\(", - ")": "\\)", - "$": '\$', - '`': '\`', -} - def _make_sysconfig_bool(val): if val: @@ -64,12 +43,16 @@ def _make_sysconfig_bool(val): return 'no' -def _make_header(): - ci_ver = version.version_string() - return '# Created by cloud-init v. %s' % (ci_ver) - - class Distro(distros.Distro): + # See: http://tiny.cc/6r99fw + clock_conf_fn = "/etc/sysconfig/clock" + locale_conf_fn = '/etc/sysconfig/i18n' + network_conf_fn = "/etc/sysconfig/network" + hostname_conf_fn = "/etc/sysconfig/network" + network_script_tpl = '/etc/sysconfig/network-scripts/ifcfg-%s' + resolve_conf_fn = "/etc/resolv.conf" + tz_local_fn = "/etc/localtime" + tz_zone_dir = "/usr/share/zoneinfo" def __init__(self, name, cfg, paths): distros.Distro.__init__(self, name, cfg, paths) @@ -77,20 +60,34 @@ class Distro(distros.Distro): # calls from repeatly happening (when they # should only happen say once per instance...) self._runner = helpers.Runners(paths) + self.osfamily = 'redhat' def install_packages(self, pkglist): - self.package_command('install', pkglist) - - def _write_resolve(self, dns_servers, search_servers): - contents = [] + self.package_command('install', pkgs=pkglist) + + def _adjust_resolve(self, dns_servers, search_servers): + try: + r_conf = ResolvConf(util.load_file(self.resolve_conf_fn)) + r_conf.parse() + except IOError: + util.logexc(LOG, + "Failed at parsing %s reverting to an empty instance", + self.resolve_conf_fn) + r_conf = ResolvConf('') + r_conf.parse() if dns_servers: for s in dns_servers: - contents.append("nameserver %s" % (s)) + try: + r_conf.add_nameserver(s) + except ValueError: + util.logexc(LOG, "Failed at adding nameserver %s", s) if search_servers: - contents.append("search %s" % (" ".join(search_servers))) - if contents: - contents.insert(0, _make_header()) - util.write_file("/etc/resolv.conf", "\n".join(contents), 0644) + for s in search_servers: + try: + r_conf.add_search_domain(s) + except ValueError: + util.logexc(LOG, "Failed at adding search domain %s", s) + util.write_file(self.resolve_conf_fn, str(r_conf), 0644) def _write_network(self, settings): # TODO(harlowja) fix this... since this is the ubuntu format @@ -102,7 +99,7 @@ class Distro(distros.Distro): searchservers = [] dev_names = entries.keys() for (dev, info) in entries.iteritems(): - net_fn = NETWORK_FN_TPL % (dev) + net_fn = self.network_script_tpl % (dev) net_cfg = { 'DEVICE': dev, 'NETMASK': info.get('netmask'), @@ -119,12 +116,12 @@ class Distro(distros.Distro): if 'dns-search' in info: searchservers.extend(info['dns-search']) if nameservers or searchservers: - self._write_resolve(nameservers, searchservers) + self._adjust_resolve(nameservers, searchservers) if dev_names: net_cfg = { 'NETWORKING': _make_sysconfig_bool(True), } - self._update_sysconfig_file("/etc/sysconfig/network", net_cfg) + self._update_sysconfig_file(self.network_conf_fn, net_cfg) return dev_names def _update_sysconfig_file(self, fn, adjustments, allow_empty=False): @@ -141,19 +138,16 @@ class Distro(distros.Distro): contents[k] = v updated_am += 1 if updated_am: - lines = contents.write() + lines = [ + str(contents), + ] if not exists: - lines.insert(0, _make_header()) + lines.insert(0, util.make_header()) util.write_file(fn, "\n".join(lines), 0644) - def set_hostname(self, 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 = '/etc/sysconfig/i18n' + out_fn = self.locale_conf_fn locale_cfg = { 'LANG': locale, } @@ -165,30 +159,16 @@ class Distro(distros.Distro): } self._update_sysconfig_file(out_fn, host_cfg) - def update_hostname(self, hostname, prev_file): - hostname_prev = self._read_hostname(prev_file) - 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)): - update_files.append("/etc/sysconfig/network") - 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 _select_hostname(self, hostname, fqdn): + # See: http://bit.ly/TwitgL + # Should be fqdn if we can use it + if fqdn: + return fqdn + return hostname + + def _read_system_hostname(self): + return (self.network_conf_fn, + self._read_hostname(self.network_conf_fn)) def _read_hostname(self, filename, default=None): (_exists, contents) = self._read_conf(filename) @@ -199,12 +179,13 @@ class Distro(distros.Distro): def _read_conf(self, fn): exists = False - if os.path.isfile(fn): + try: contents = util.load_file(fn).splitlines() exists = True - else: + except IOError: contents = [] - return (exists, QuotingConfigObj(contents)) + return (exists, + SysConf(contents)) def _bring_up_interfaces(self, device_names): if device_names and 'all' in device_names: @@ -213,19 +194,21 @@ class Distro(distros.Distro): return distros.Distro._bring_up_interfaces(self, device_names) def set_timezone(self, tz): - tz_file = os.path.join("/usr/share/zoneinfo", tz) + # TODO(harlowja): move this code into + # the parent distro... + tz_file = os.path.join(self.tz_zone_dir, str(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 clock_cfg = { - 'ZONE': tz, + 'ZONE': str(tz), } - self._update_sysconfig_file("/etc/sysconfig/clock", clock_cfg) + self._update_sysconfig_file(self.clock_conf_fn, clock_cfg) # This ensures that the correct tz will be used for the system - util.copy(tz_file, "/etc/localtime") + util.copy(tz_file, self.tz_local_fn) - def package_command(self, command, args=None): + def package_command(self, command, args=None, pkgs=[]): cmd = ['yum'] # If enabled, then yum will be tolerant of errors on the command line # with regard to packages. @@ -236,9 +219,17 @@ class Distro(distros.Distro): # 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: + + if args and isinstance(args, str): + cmd.append(args) + elif args and isinstance(args, list): cmd.extend(args) + + cmd.append(command) + + pkglist = util.expand_package_list('%s-%s', pkgs) + cmd.extend(pkglist) + # Allow the output of this to flow outwards (ie not be captured) util.subp(cmd, capture=False) @@ -247,51 +238,6 @@ class Distro(distros.Distro): ["makecache"], 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(harlowja) remove when we have python-netcf active... diff --git a/cloudinit/distros/ubuntu.py b/cloudinit/distros/ubuntu.py index 4e697f82..c527f248 100644 --- a/cloudinit/distros/ubuntu.py +++ b/cloudinit/distros/ubuntu.py @@ -28,7 +28,4 @@ LOG = logging.getLogger(__name__) class Distro(debian.Distro): - - default_user = 'ubuntu' - default_user_groups = ("adm,audio,cdrom,dialout,floppy,video," - "plugdev,dip,netdev,sudo") + pass |