From 0049b6e7bc7f52fd442f1b8902294ca8c379df5c Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Thu, 20 Sep 2012 17:26:42 -0700 Subject: Remove the need to have 'default_user' and 'default_user_groups' groups be hard coded into the distro class, instead let that set of configuration be located in the config file where it should be specified instead. --- cloudinit/distros/__init__.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) (limited to 'cloudinit/distros/__init__.py') diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py index 3e9d934d..cdb72b5a 100644 --- a/cloudinit/distros/__init__.py +++ b/cloudinit/distros/__init__.py @@ -44,10 +44,7 @@ LOG = logging.getLogger(__name__) class Distro(object): - __metaclass__ = abc.ABCMeta - default_user = None - default_user_groups = None def __init__(self, name, cfg, paths): self._paths = paths @@ -61,7 +58,6 @@ class Distro(object): user = self.get_default_user() groups = self.get_default_user_groups() - if not user: raise NotImplementedError("No Default user") @@ -71,12 +67,12 @@ class Distro(object): 'home': "/home/%s" % user, 'shell': "/bin/bash", 'lock_passwd': True, - 'gecos': "%s%s" % (user[0:1].upper(), user[1:]), + 'gecos': user.title(), 'sudo': "ALL=(ALL) NOPASSWD:ALL", } if groups: - user_dict['groups'] = groups + user_dict['groups'] = ",".join(groups) self.create_user(**user_dict) @@ -212,10 +208,10 @@ class Distro(object): return False def get_default_user(self): - return self.default_user + return self.get_option('default_user') def get_default_user_groups(self): - return self.default_user_groups + return self.get_option('default_user_groups') def create_user(self, name, **kwargs): """ -- cgit v1.2.3 From e8a10a41d22876d555084def823817337d9c2a80 Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Thu, 20 Sep 2012 17:56:22 -0700 Subject: Use only util methods for reading/loading/appending/peeking at files since it is likely soon that we will add a new way of adjusting the root of files read, also it is useful for debugging to track what is being read/written in a central fashion. --- cloudinit/distros/__init__.py | 3 +-- cloudinit/sources/DataSourceAltCloud.py | 19 ++++++++----------- cloudinit/sources/DataSourceConfigDrive.py | 20 +++++++++----------- cloudinit/sources/DataSourceMAAS.py | 6 ++---- cloudinit/sources/DataSourceOVF.py | 5 ++--- cloudinit/util.py | 10 ++++++++++ 6 files changed, 32 insertions(+), 31 deletions(-) (limited to 'cloudinit/distros/__init__.py') diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py index 3e9d934d..f6aa8d99 100644 --- a/cloudinit/distros/__init__.py +++ b/cloudinit/distros/__init__.py @@ -343,8 +343,7 @@ class Distro(object): 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) raise e diff --git a/cloudinit/sources/DataSourceAltCloud.py b/cloudinit/sources/DataSourceAltCloud.py index 69c376a5..d7e1204f 100644 --- a/cloudinit/sources/DataSourceAltCloud.py +++ b/cloudinit/sources/DataSourceAltCloud.py @@ -73,13 +73,11 @@ def read_user_data_callback(mount_dir): # First try deltacloud_user_data_file. On failure try user_data_file. try: - with open(deltacloud_user_data_file, 'r') as user_data_f: - user_data = user_data_f.read().strip() - except: + user_data = util.load_file(deltacloud_user_data_file).strip() + except IOError: try: - with open(user_data_file, 'r') as user_data_f: - user_data = user_data_f.read().strip() - except: + user_data = util.load_file(user_data_file).strip() + except IOError: util.logexc(LOG, ('Failed accessing user data file.')) return None @@ -157,11 +155,10 @@ class DataSourceAltCloud(sources.DataSource): if os.path.exists(CLOUD_INFO_FILE): try: - cloud_info = open(CLOUD_INFO_FILE) - cloud_type = cloud_info.read().strip().upper() - cloud_info.close() - except: - util.logexc(LOG, 'Unable to access cloud info file.') + cloud_type = util.load_file(CLOUD_INFO_FILE).strip().upper() + except IOError: + util.logexc(LOG, 'Unable to access cloud info file at %s.', + CLOUD_INFO_FILE) return False else: cloud_type = self.get_cloud_type() diff --git a/cloudinit/sources/DataSourceConfigDrive.py b/cloudinit/sources/DataSourceConfigDrive.py index b8154367..b477560c 100644 --- a/cloudinit/sources/DataSourceConfigDrive.py +++ b/cloudinit/sources/DataSourceConfigDrive.py @@ -227,19 +227,19 @@ def read_config_drive_dir_v2(source_dir, version="2012-08-10"): found = False if os.path.isfile(fpath): try: - with open(fpath) as fp: - data = fp.read() - except Exception as exc: - raise BrokenConfigDriveDir("failed to read: %s" % fpath) + data = util.load_file(fpath) + except IOError: + raise BrokenConfigDriveDir("Failed to read: %s" % fpath) found = True elif required: - raise NonConfigDriveDir("missing mandatory %s" % fpath) + raise NonConfigDriveDir("Missing mandatory path: %s" % fpath) if found and process: try: data = process(data) except Exception as exc: - raise BrokenConfigDriveDir("failed to process: %s" % fpath) + raise BrokenConfigDriveDir(("Failed to process " + "path: %s") % fpath) if found: results[name] = data @@ -255,8 +255,7 @@ def read_config_drive_dir_v2(source_dir, version="2012-08-10"): # do not use os.path.join here, as content_path starts with / cpath = os.path.sep.join((source_dir, "openstack", "./%s" % item['content_path'])) - with open(cpath) as fp: - return(fp.read()) + return util.load_file(cpath) files = {} try: @@ -270,7 +269,7 @@ def read_config_drive_dir_v2(source_dir, version="2012-08-10"): if item: results['network_config'] = read_content_path(item) except Exception as exc: - raise BrokenConfigDriveDir("failed to read file %s: %s" % (item, exc)) + raise BrokenConfigDriveDir("Failed to read file %s: %s" % (item, exc)) # to openstack, user can specify meta ('nova boot --meta=key=value') and # those will appear under metadata['meta']. @@ -385,8 +384,7 @@ def get_previous_iid(paths): # hasn't declared itself found. fname = os.path.join(paths.get_cpath('data'), 'instance-id') try: - with open(fname) as fp: - return fp.read() + return util.load_file(fname) except IOError: return None diff --git a/cloudinit/sources/DataSourceMAAS.py b/cloudinit/sources/DataSourceMAAS.py index c568d365..d166e9e3 100644 --- a/cloudinit/sources/DataSourceMAAS.py +++ b/cloudinit/sources/DataSourceMAAS.py @@ -301,9 +301,7 @@ if __name__ == "__main__": 'token_secret': args.tsec, 'consumer_secret': args.csec} if args.config: - import yaml - with open(args.config) as fp: - cfg = yaml.safe_load(fp) + cfg = util.read_conf(args.config) if 'datasource' in cfg: cfg = cfg['datasource']['MAAS'] for key in creds.keys(): @@ -312,7 +310,7 @@ if __name__ == "__main__": def geturl(url, headers_cb): req = urllib2.Request(url, data=None, headers=headers_cb(url)) - return(urllib2.urlopen(req).read()) + return (urllib2.urlopen(req).read()) def printurl(url, headers_cb): print "== %s ==\n%s\n" % (url, geturl(url, headers_cb)) diff --git a/cloudinit/sources/DataSourceOVF.py b/cloudinit/sources/DataSourceOVF.py index 771e64eb..e90150c6 100644 --- a/cloudinit/sources/DataSourceOVF.py +++ b/cloudinit/sources/DataSourceOVF.py @@ -204,9 +204,8 @@ def transport_iso9660(require_iso=True): try: # See if we can read anything at all...?? - with open(fullp, 'rb') as fp: - fp.read(512) - except: + util.peek_file(fullp, 512) + except IOError: continue try: diff --git a/cloudinit/util.py b/cloudinit/util.py index 33da73eb..18000301 100644 --- a/cloudinit/util.py +++ b/cloudinit/util.py @@ -952,6 +952,12 @@ def find_devs_with(criteria=None, oformat='device', return entries +def peek_file(fname, max_bytes): + LOG.debug("Peeking at %s (max_bytes=%s)", fname, max_bytes) + with open(fname, 'rb') as ifh: + return ifh.read(max_bytes) + + def load_file(fname, read_cb=None, quiet=False): LOG.debug("Reading from %s (quiet=%s)", fname, quiet) ofh = StringIO() @@ -1281,6 +1287,10 @@ def uptime(): return uptime_str +def append_file(path, content): + write_file(path, content, omode="ab", mode=None) + + def ensure_file(path, mode=0644): write_file(path, content='', omode="ab", mode=mode) -- cgit v1.2.3 From 4b159cdb12d1ceb3ed7a7a2b044182890618889f Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Fri, 21 Sep 2012 17:59:14 -0700 Subject: Add in another helper that can understand the 'etc/hosts' format and add in a unit test to make sure that format can be correctly handled and added onto in a nice manner + update the distro code to use this new code instead of the previous function that did the same thing. --- cloudinit/distros/__init__.py | 69 ++++++++++++++++--------------- cloudinit/distros/helpers.py | 96 ++++++++++++++++++++++++++++++++++++------- 2 files changed, 118 insertions(+), 47 deletions(-) (limited to 'cloudinit/distros/__init__.py') diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py index 3e9d934d..bf4317f1 100644 --- a/cloudinit/distros/__init__.py +++ b/cloudinit/distros/__init__.py @@ -34,6 +34,8 @@ from cloudinit import log as logging from cloudinit import ssh_util from cloudinit import util +from cloudinit.distros import helpers + # TODO(harlowja): Make this via config?? IFACE_ACTIONS = { 'up': ['ifup', '--all'], @@ -152,42 +154,43 @@ class Distro(object): return "127.0.0.1" def update_etc_hosts(self, hostname, fqdn): - # Format defined at - # http://unixhelp.ed.ac.uk/CGI/man-cgi?hosts - header = "# Added by cloud-init" - real_header = "%s on %s" % (header, util.time_rfc2822()) + header = '' + if os.path.exists('/etc/hosts'): + eh = helpers.HostsConf(util.load_file("/etc/hosts")) + else: + eh = helpers.HostsConf('') + header = "# Added by cloud-init" + header = "%s on %s" % (header, util.time_rfc2822()) local_ip = self._get_localhost_ip() - hosts_line = "%s\t%s %s" % (local_ip, fqdn, hostname) - new_etchosts = StringIO() - need_write = False - need_change = True - hosts_ro_fn = self._paths.join(True, "/etc/hosts") - for line in util.load_file(hosts_ro_fn).splitlines(): - if line.strip().startswith(header): - continue - if not line.strip() or line.strip().startswith("#"): - new_etchosts.write("%s\n" % (line)) - continue - split_line = [s.strip() for s in line.split()] - if len(split_line) < 2: - new_etchosts.write("%s\n" % (line)) - continue - (ip, hosts) = split_line[0], split_line[1:] - if ip == local_ip: - if sorted([hostname, fqdn]) == sorted(hosts): - need_change = False - if need_change: - line = "%s\n%s" % (real_header, hosts_line) + 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: + if sorted(entry) == sorted([fqdn, hostname]): + # Exists already, leave it be need_change = False - need_write = True - new_etchosts.write("%s\n" % (line)) + break + if need_change: + # Doesn't exist, change the first + # entry to be this entry + new_entries = list(prev_info) + new_entries[0] = [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("/etc/hosts", contents.getvalue(), mode=0644) def _interface_action(self, action): if action not in IFACE_ACTIONS: diff --git a/cloudinit/distros/helpers.py b/cloudinit/distros/helpers.py index 251d5051..916935eb 100644 --- a/cloudinit/distros/helpers.py +++ b/cloudinit/distros/helpers.py @@ -23,6 +23,87 @@ from StringIO import StringIO from cloudinit import util +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) + + +# See: man hosts +# or http://unixhelp.ed.ac.uk/CGI/man-cgi?hosts +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") + 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() + + # See: man resolv.conf class ResolvConf(object): def __init__(self, text): @@ -151,20 +232,7 @@ class ResolvConf(object): if not sline: entries.append(('blank', [line])) continue - comment_s_loc = sline.find(";") - comment_h_loc = sline.find("#") - comment_loc = -1 - if comment_s_loc != -1 and comment_h_loc != -1: - comment_loc = min(comment_h_loc, comment_s_loc) - elif comment_s_loc != -1: - comment_loc = comment_s_loc - elif comment_h_loc != -1: - comment_loc = comment_h_loc - head = line - tail = None - if comment_loc != -1: - head = line[:comment_loc] - tail = line[comment_loc:] + (head, tail) = _chop_comment(line, ';#') if not len(head.strip()): entries.append(('all_comment', [line])) continue -- cgit v1.2.3 From f255d068c5d4251762b83467d1927ab72da57482 Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Fri, 28 Sep 2012 18:39:46 -0700 Subject: Ensure that the directory where the sudoers file is being added actually exists before it is written into and ensure that the directory is included in the main sudoers file. --- cloudinit/distros/__init__.py | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) (limited to 'cloudinit/distros/__init__.py') diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py index 86ab557c..11422644 100644 --- a/cloudinit/distros/__init__.py +++ b/cloudinit/distros/__init__.py @@ -296,6 +296,38 @@ class Distro(object): return True + 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 = '' + if os.path.exists(sudo_base): + sudoers_contents = util.load_file(sudo_base) + found_include = False + for line in sudoers_contents.splitlines(): + line = line.strip() + mtch = re.search(r"#includedir\s+(.*)$", line) + if not mtch: + continue + included_dir = mtch.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: + sudoers_contents += "\n#includedir %s\n" % (path) + try: + if not os.path.exists(sudo_base): + util.write_file(sudo_base, sudoers_contents, 0440) + else: + with open(sudo_base, 'a') as f: + f.write(sudoers_contents) + except IOError as e: + util.logexc(LOG, "Failed to write %s" % sudo_base, e) + raise e + util.ensure_dir(path, 0440) + def write_sudo_rules(self, user, rules, @@ -311,9 +343,10 @@ class Distro(object): content += "%s %s\n" % (user, rule) content += "\n" + self.ensure_sudo_dir(os.path.dirname(sudo_file)) + if not os.path.exists(sudo_file): util.write_file(sudo_file, content, 0440) - else: try: with open(sudo_file, 'a') as f: -- cgit v1.2.3 From 9e7320df7a6fb6f1e9a5401735c471467b1fe365 Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Fri, 28 Sep 2012 20:32:00 -0700 Subject: Dir should be 0755, not 0440 --- cloudinit/distros/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'cloudinit/distros/__init__.py') diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py index 11422644..301eeed2 100644 --- a/cloudinit/distros/__init__.py +++ b/cloudinit/distros/__init__.py @@ -326,7 +326,7 @@ class Distro(object): except IOError as e: util.logexc(LOG, "Failed to write %s" % sudo_base, e) raise e - util.ensure_dir(path, 0440) + util.ensure_dir(path, 0755) def write_sudo_rules(self, user, -- cgit v1.2.3 From 9a252b3a41ad757587cba729b31079c04b253faa Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Fri, 28 Sep 2012 21:04:00 -0700 Subject: Update the log statement used here to be a little more relevant. --- cloudinit/distros/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'cloudinit/distros/__init__.py') diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py index 301eeed2..500147c6 100644 --- a/cloudinit/distros/__init__.py +++ b/cloudinit/distros/__init__.py @@ -248,7 +248,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: -- cgit v1.2.3 From e2b85e2087b796c7a130ee20f688dbd5d161739f Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Sun, 30 Sep 2012 13:47:00 -0700 Subject: Ensure that the include dir starts the line and is not a part of a comment or other part of the line. --- cloudinit/distros/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'cloudinit/distros/__init__.py') diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py index 500147c6..cdae1add 100644 --- a/cloudinit/distros/__init__.py +++ b/cloudinit/distros/__init__.py @@ -305,10 +305,10 @@ class Distro(object): found_include = False for line in sudoers_contents.splitlines(): line = line.strip() - mtch = re.search(r"#includedir\s+(.*)$", line) - if not mtch: + include_match = re.search(r"^#includedir\s+(.*)$", line) + if not include_match: continue - included_dir = mtch.group(1).strip() + included_dir = include_match.group(1).strip() if not included_dir: continue included_dir = os.path.abspath(included_dir) -- cgit v1.2.3 From 80eb53deb9f80694c5fd0e0190197de1ff496716 Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Wed, 10 Oct 2012 16:21:22 -0700 Subject: System config niceness! 1. Move out the old helpers that provided oop access/reading/writing to various standard conf files and place those in parsers instead. 2. Unify the 'update_hostname' which varied very little between distros and make it generic so that subclasses can only provide a couple of functions to obtain the hostname updating functionality 3. Implement that new set of functions in rhel/debian 4. Use the new parsers chop_comment function for similar use cases as well as add a new utils make header function that can be used for configuration files that are newly generated to use (less duplication here of this same thing being done in multiple places. 5. Add in a distro '_apply_hostname' which calls out to the 'hostname' program to set the system hostname (more duplication elimination). 6. Make the 'constant' filenames being written to for configuration by the various distros be instance members instead of string constants 'sprinkled' throughout the code --- cloudinit/distros/__init__.py | 89 ++++++--- cloudinit/distros/debian.py | 88 ++++----- cloudinit/distros/helpers.py | 252 ------------------------- cloudinit/distros/parsers/__init__.py | 27 +++ cloudinit/distros/parsers/hosts.py | 92 +++++++++ cloudinit/distros/parsers/quoting_conf.py | 80 ++++++++ cloudinit/distros/parsers/resolv_conf.py | 171 +++++++++++++++++ cloudinit/distros/rhel.py | 149 ++++----------- cloudinit/util.py | 15 +- tests/unittests/test_distros/test_hosts.py | 8 +- tests/unittests/test_distros/test_netconfig.py | 2 +- tests/unittests/test_distros/test_resolv.py | 14 +- 12 files changed, 532 insertions(+), 455 deletions(-) delete mode 100644 cloudinit/distros/helpers.py create mode 100644 cloudinit/distros/parsers/__init__.py create mode 100644 cloudinit/distros/parsers/hosts.py create mode 100644 cloudinit/distros/parsers/quoting_conf.py create mode 100644 cloudinit/distros/parsers/resolv_conf.py (limited to 'cloudinit/distros/__init__.py') diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py index a5d41bdb..07f03159 100644 --- a/cloudinit/distros/__init__.py +++ b/cloudinit/distros/__init__.py @@ -33,7 +33,7 @@ from cloudinit import log as logging from cloudinit import ssh_util from cloudinit import util -from cloudinit.distros import helpers +from cloudinit.distros.parsers import hosts LOG = logging.getLogger(__name__) @@ -43,6 +43,8 @@ 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" def __init__(self, name, cfg, paths): self._paths = paths @@ -66,10 +68,6 @@ class Distro(object): def set_hostname(self, hostname): raise NotImplementedError() - @abc.abstractmethod - def update_hostname(self, hostname, prev_hostname_fn): - raise NotImplementedError() - @abc.abstractmethod def package_command(self, cmd, args=None): raise NotImplementedError() @@ -117,14 +115,62 @@ 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): + LOG.debug("Setting system hostname to %s", hostname) + util.subp(['hostname', hostname]) + + def update_hostname(self, hostname, prev_hostname_fn): + if not hostname: + return + + prev_hostname = self._read_hostname(prev_hostname_fn) + (sys_fn, sys_hostname) = self._read_system_hostname() + update_files = [] + if not prev_hostname or prev_hostname != hostname: + update_files.append(prev_hostname_fn) + + if (not sys_hostname) or (sys_hostname == prev_hostname + and sys_hostname != hostname): + update_files.append(sys_fn) + + 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 sys_fn in update_files: + self._apply_hostname(hostname) + def update_etc_hosts(self, hostname, fqdn): header = '' - if os.path.exists('/etc/hosts'): - eh = helpers.HostsConf(util.load_file("/etc/hosts")) + if os.path.exists(self.hosts_fn): + eh = hosts.HostsConf(util.load_file(self.hosts_fn)) else: - eh = helpers.HostsConf('') - header = "# Added by cloud-init" - header = "%s on %s" % (header, util.time_rfc2822()) + eh = hosts.HostsConf('') + header = util.make_header(base="added") local_ip = self._get_localhost_ip() prev_info = eh.get_entry(local_ip) need_change = False @@ -154,7 +200,7 @@ class Distro(object): if header: contents.write("%s\n" % (header)) contents.write("%s\n" % (eh)) - util.write_file("/etc/hosts", contents.getvalue(), mode=0644) + util.write_file(self.hosts_fn, contents.getvalue(), mode=0644) def _bring_up_interface(self, device_name): cmd = ['ifup', device_name] @@ -302,30 +348,31 @@ class Distro(object): return True - def write_sudo_rules(self, - user, - rules, - sudo_file="/etc/sudoers.d/90-cloud-init-users", - ): + def write_sudo_rules(self, user, rules, sudo_file=None): + if not sudo_file: + sudo_file = self.ci_sudoers_fn - content_header = "# user rules for %s" % user + content_header = "# User rules for %s" % user content = "%s\n%s %s\n\n" % (content_header, user, rules) - if isinstance(rules, list): + if isinstance(rules, (list, tuple, set)): content = "%s\n" % content_header for rule in rules: content += "%s %s\n" % (user, rule) content += "\n" if not os.path.exists(sudo_file): - util.write_file(sudo_file, content, 0440) - + contents = [ + util.make_header(), + content, + ] + util.write_file(sudo_file, "\n".join(contents), 0440) else: try: with open(sudo_file, 'a') as f: f.write(content) except IOError as e: - util.logexc(LOG, "Failed to write %s" % sudo_file, e) + util.logexc(LOG, "Failed to write sudoers file %s", sudo_file) raise e def create_group(self, name, members): diff --git a/cloudinit/distros/debian.py b/cloudinit/distros/debian.py index 88f4e978..20962937 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 import chop_comment + 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) @@ -43,10 +51,15 @@ class Distro(distros.Distro): 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): @@ -54,8 +67,7 @@ class Distro(distros.Distro): self.package_command('install', pkglist) def _write_network(self, settings): - net_fn = self._paths.join(False, "/etc/network/interfaces") - util.write_file(net_fn, settings) + util.write_file(self.network_conf_fn, settings) return ['all'] def _bring_up_interfaces(self, device_names): @@ -69,54 +81,29 @@ class Distro(distros.Distro): 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]) + self._write_hostname(hostname, self.hostname_conf_fn) + self._apply_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]) + hostname_lines = [ + str(hostname), + "", + ] + util.write_file(out_fn, "\n".join(hostname_lines), 0644) + + def _read_system_hostname(self): + return (self.hostname_conf_fn, + self._read_hostname(self.hostname_conf_fn)) 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 + (before_comment, _comment) = chop_comment(line, "#") + before_comment = before_comment.strip() + if len(before_comment): + return before_comment return default def _get_localhost_ip(self): @@ -124,15 +111,18 @@ class Distro(distros.Distro): return "127.0.1.1" def set_timezone(self, tz): - tz_file = os.path.join("/usr/share/zoneinfo", tz) + tz_file = os.path.join(self.tz_zone_dir, 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")) + tz_lines = [ + util.make_header(), + str(tz), + "", + ] + util.write_file(self.tz_conf_fn, "\n".join(tz_lines)) + util.copy(tz_file, self.tz_local_fn) def package_command(self, command, args=None): e = os.environ.copy() diff --git a/cloudinit/distros/helpers.py b/cloudinit/distros/helpers.py deleted file mode 100644 index 916935eb..00000000 --- a/cloudinit/distros/helpers.py +++ /dev/null @@ -1,252 +0,0 @@ -# vi: ts=4 expandtab -# -# Copyright (C) 2012 Canonical Ltd. -# Copyright (C) 2012 Yahoo! Inc. -# -# Author: Scott Moser -# Author: Joshua Harlow -# -# 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 . - -from StringIO import StringIO - -from cloudinit import util - - -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) - - -# See: man hosts -# or http://unixhelp.ed.ac.uk/CGI/man-cgi?hosts -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") - 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() - - -# 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/__init__.py b/cloudinit/distros/parsers/__init__.py new file mode 100644 index 00000000..8334a5e6 --- /dev/null +++ b/cloudinit/distros/parsers/__init__.py @@ -0,0 +1,27 @@ +# vi: ts=4 expandtab +# +# Copyright (C) 2012 Yahoo! Inc. +# +# Author: Joshua Harlow +# +# 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 . + +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/hosts.py b/cloudinit/distros/parsers/hosts.py new file mode 100644 index 00000000..5374ab0b --- /dev/null +++ b/cloudinit/distros/parsers/hosts.py @@ -0,0 +1,92 @@ +# vi: ts=4 expandtab +# +# Copyright (C) 2012 Yahoo! Inc. +# +# Author: Joshua Harlow +# +# 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 . + +from StringIO import StringIO + +from cloudinit.distros.parsers import chop_comment + + +# See: man hosts +# or http://unixhelp.ed.ac.uk/CGI/man-cgi?hosts +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") + 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/quoting_conf.py b/cloudinit/distros/parsers/quoting_conf.py new file mode 100644 index 00000000..953ccfe9 --- /dev/null +++ b/cloudinit/distros/parsers/quoting_conf.py @@ -0,0 +1,80 @@ +# vi: ts=4 expandtab +# +# Copyright (C) 2012 Yahoo! Inc. +# +# Author: Joshua Harlow +# +# 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 . + +# 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 = { + "\"": "\\\"", + "(": "\\(", + ")": "\\)", + "$": '\$', + '`': '\`', +} + +# 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) + diff --git a/cloudinit/distros/parsers/resolv_conf.py b/cloudinit/distros/parsers/resolv_conf.py new file mode 100644 index 00000000..377ada6b --- /dev/null +++ b/cloudinit/distros/parsers/resolv_conf.py @@ -0,0 +1,171 @@ +# vi: ts=4 expandtab +# +# Copyright (C) 2012 Yahoo! Inc. +# +# Author: Joshua Harlow +# +# 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 . + +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/rhel.py b/cloudinit/distros/rhel.py index 13fd5ec8..21f2216e 100644 --- a/cloudinit/distros/rhel.py +++ b/cloudinit/distros/rhel.py @@ -23,41 +23,17 @@ import os from cloudinit import distros -from cloudinit.distros import helpers as d_helpers + +from cloudinit.distros.parsers import (resolv_conf, quoting_conf) 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: @@ -66,12 +42,15 @@ 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" + 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) @@ -84,14 +63,14 @@ class Distro(distros.Distro): self.package_command('install', pkglist) def _adjust_resolve(self, dns_servers, search_servers): - r_conf = d_helpers.ResolvConf(util.load_file("/etc/resolv.conf")) + r_conf = resolv_conf.ResolvConf(util.load_file(self.resolve_conf_fn)) try: r_conf.parse() except IOError: util.logexc(LOG, "Failed at parsing %s reverting to an empty instance", - "/etc/resolv.conf") - r_conf = d_helpers.ResolvConf('') + self.resolve_conf_fn) + r_conf = resolv_conf.ResolvConf('') r_conf.parse() if dns_servers: for s in dns_servers: @@ -105,7 +84,7 @@ class Distro(distros.Distro): r_conf.add_search_domain(s) except ValueError: util.logexc(LOG, "Failed at adding search domain %s", s) - util.write_file("/etc/resolv.conf", str(r_conf), 0644) + 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 @@ -117,7 +96,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'), @@ -134,12 +113,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): @@ -158,17 +137,16 @@ class Distro(distros.Distro): if updated_am: lines = contents.write() 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]) + self._write_hostname(hostname, self.network_conf_fn) + self._apply_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, } @@ -180,30 +158,9 @@ 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 _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) @@ -219,7 +176,8 @@ class Distro(distros.Distro): exists = True else: contents = [] - return (exists, QuotingConfigObj(contents)) + return (exists, + quoting_conf.QuotingConfigObj(contents)) def _bring_up_interfaces(self, device_names): if device_names and 'all' in device_names: @@ -228,17 +186,19 @@ 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) + # Ensure that this timezone is actually + # available on this system, if not give up + 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): cmd = ['yum'] @@ -262,51 +222,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/util.py b/cloudinit/util.py index 79676305..918deba2 100644 --- a/cloudinit/util.py +++ b/cloudinit/util.py @@ -52,6 +52,7 @@ from cloudinit import importer from cloudinit import log as logging from cloudinit import safeyaml from cloudinit import url_helper as uhelp +from cloudinit import version from cloudinit.settings import (CFG_BUILTIN) @@ -272,11 +273,7 @@ def uniq_merge(*lists): # Kickout the empty ones a_list = [a for a in a_list if len(a)] combined_list.extend(a_list) - uniq_list = [] - for i in combined_list: - if i not in uniq_list: - uniq_list.append(i) - return uniq_list + return uniq_list(combined_list) def clean_filename(fn): @@ -1429,6 +1426,14 @@ def subp(args, data=None, rcs=None, env=None, capture=True, shell=False, return (out, err) +def make_header(comment_char="#", base='created'): + ci_ver = version.version_string() + header = str(comment_char) + header += " %s by cloud-init v. %s" % (base.title(), ci_ver) + header += " on %s" % time_rfc2822() + return header + + def abs_join(*paths): return os.path.abspath(os.path.join(*paths)) diff --git a/tests/unittests/test_distros/test_hosts.py b/tests/unittests/test_distros/test_hosts.py index 621837ab..687a0dab 100644 --- a/tests/unittests/test_distros/test_hosts.py +++ b/tests/unittests/test_distros/test_hosts.py @@ -1,6 +1,6 @@ from mocker import MockerTestCase -from cloudinit.distros import helpers +from cloudinit.distros.parsers import hosts BASE_ETC = ''' @@ -16,7 +16,7 @@ BASE_ETC = BASE_ETC.strip() class TestHostsHelper(MockerTestCase): def test_parse(self): - eh = helpers.HostsConf(BASE_ETC) + eh = hosts.HostsConf(BASE_ETC) self.assertEquals(eh.get_entry('127.0.0.1'), [['localhost']]) self.assertEquals(eh.get_entry('192.168.1.10'), [['foo.mydomain.org', 'foo'], @@ -25,7 +25,7 @@ class TestHostsHelper(MockerTestCase): self.assertTrue(eh.startswith('# Example')) def test_add(self): - eh = helpers.HostsConf(BASE_ETC) + eh = hosts.HostsConf(BASE_ETC) eh.add_entry('127.0.0.0', 'blah') self.assertEquals(eh.get_entry('127.0.0.0'), [['blah']]) eh.add_entry('127.0.0.3', 'blah', 'blah2', 'blah3') @@ -33,7 +33,7 @@ class TestHostsHelper(MockerTestCase): [['blah', 'blah2', 'blah3']]) def test_del(self): - eh = helpers.HostsConf(BASE_ETC) + eh = hosts.HostsConf(BASE_ETC) eh.add_entry('127.0.0.0', 'blah') self.assertEquals(eh.get_entry('127.0.0.0'), [['blah']]) diff --git a/tests/unittests/test_distros/test_netconfig.py b/tests/unittests/test_distros/test_netconfig.py index 55765f0c..b7ce6fea 100644 --- a/tests/unittests/test_distros/test_netconfig.py +++ b/tests/unittests/test_distros/test_netconfig.py @@ -83,7 +83,7 @@ class TestNetCfgDistro(MockerTestCase): self.assertEquals(write_buf.mode, 0644) def assertCfgEquals(self, blob1, blob2): - cfg_tester = distros.rhel.QuotingConfigObj + cfg_tester = distros.parsers.quoting_conf.QuotingConfigObj b1 = dict(cfg_tester(blob1.strip().splitlines())) b2 = dict(cfg_tester(blob2.strip().splitlines())) self.assertEquals(b1, b2) diff --git a/tests/unittests/test_distros/test_resolv.py b/tests/unittests/test_distros/test_resolv.py index 5f122833..d947dda0 100644 --- a/tests/unittests/test_distros/test_resolv.py +++ b/tests/unittests/test_distros/test_resolv.py @@ -1,6 +1,8 @@ from mocker import MockerTestCase -from cloudinit.distros import helpers +from cloudinit.distros.parsers import resolv_conf + +import re BASE_RESOLVE = ''' @@ -14,12 +16,12 @@ BASE_RESOLVE = BASE_RESOLVE.strip() class TestResolvHelper(MockerTestCase): def test_parse_same(self): - rp = helpers.ResolvConf(BASE_RESOLVE) + rp = resolv_conf.ResolvConf(BASE_RESOLVE) rp_r = str(rp).strip() self.assertEquals(BASE_RESOLVE, rp_r) def test_local_domain(self): - rp = helpers.ResolvConf(BASE_RESOLVE) + rp = resolv_conf.ResolvConf(BASE_RESOLVE) self.assertEquals(None, rp.local_domain) rp.local_domain = "bob" @@ -27,7 +29,7 @@ class TestResolvHelper(MockerTestCase): self.assertIn('domain bob', str(rp)) def test_nameservers(self): - rp = helpers.ResolvConf(BASE_RESOLVE) + rp = resolv_conf.ResolvConf(BASE_RESOLVE) self.assertIn('10.15.44.14', rp.nameservers) self.assertIn('10.15.30.92', rp.nameservers) rp.add_nameserver('10.2') @@ -41,12 +43,12 @@ class TestResolvHelper(MockerTestCase): self.assertNotIn('10.3', rp.nameservers) def test_search_domains(self): - rp = helpers.ResolvConf(BASE_RESOLVE) + rp = resolv_conf.ResolvConf(BASE_RESOLVE) self.assertIn('yahoo.com', rp.search_domains) self.assertIn('blah.yahoo.com', rp.search_domains) rp.add_search_domain('bbb.y.com') self.assertIn('bbb.y.com', rp.search_domains) - self.assertRegexpMatches(str(rp), r'search(.*)bbb.y.com(.*)') + self.assertTrue(re.search(r'search(.*)bbb.y.com(.*)', str(rp))) self.assertIn('bbb.y.com', rp.search_domains) rp.add_search_domain('bbb.y.com') self.assertEquals(len(rp.search_domains), 3) -- cgit v1.2.3 From fe1ec4d4cbb682731e8f65be5dab60f4593ed9d6 Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Wed, 10 Oct 2012 16:59:40 -0700 Subject: Add comment explaining why the '_apply_hostname' function will not be permanent and catch the exception that occurs if it fails and log that instead of blowing up (which isn't typically useful for something that is temporary anyway). --- cloudinit/distros/__init__.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) (limited to 'cloudinit/distros/__init__.py') diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py index 07f03159..c6427401 100644 --- a/cloudinit/distros/__init__.py +++ b/cloudinit/distros/__init__.py @@ -128,8 +128,16 @@ class Distro(object): raise NotImplementedError() def _apply_hostname(self, hostname): - LOG.debug("Setting system hostname to %s", hostname) - util.subp(['hostname', 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("Temporarily setting the system hostname to %s", hostname) + try: + util.subp(['hostname', hostname]) + except util.ProcessExecutionError: + util.logexc(LOG, ("Failed to temporarily adjust" + " the system hostname to %s"), hostname) def update_hostname(self, hostname, prev_hostname_fn): if not hostname: -- cgit v1.2.3 From 059a4c45ab2b4439872d138452d6296bfe82be07 Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Thu, 11 Oct 2012 12:52:19 -0700 Subject: Update log message to use the more appropriate 'non-persistently' wording. --- cloudinit/distros/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'cloudinit/distros/__init__.py') diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py index c6427401..bafa69d3 100644 --- a/cloudinit/distros/__init__.py +++ b/cloudinit/distros/__init__.py @@ -132,11 +132,11 @@ class Distro(object): # temporarily (until reboot so it should # not be depended on). Use the write # hostname functions for 'permanent' adjustments. - LOG.debug("Temporarily setting the system hostname to %s", hostname) + LOG.debug("Non-persistently setting the system hostname to %s", hostname) try: util.subp(['hostname', hostname]) except util.ProcessExecutionError: - util.logexc(LOG, ("Failed to temporarily adjust" + util.logexc(LOG, ("Failed to non-persistently adjust" " the system hostname to %s"), hostname) def update_hostname(self, hostname, prev_hostname_fn): -- cgit v1.2.3 From bbe325c902ef3a3b8845cd3c1bb8bee0c3c74a89 Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Thu, 11 Oct 2012 21:24:30 -0700 Subject: Add some more sysconfig tests + pylint cleanups. --- cloudinit/distros/__init__.py | 3 +- cloudinit/distros/parsers/sys_conf.py | 59 +++++++++++++++++--------- tests/unittests/test_distros/test_sysconfig.py | 21 ++++++++- 3 files changed, 59 insertions(+), 24 deletions(-) (limited to 'cloudinit/distros/__init__.py') diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py index bafa69d3..fa7cc1ca 100644 --- a/cloudinit/distros/__init__.py +++ b/cloudinit/distros/__init__.py @@ -132,7 +132,8 @@ class Distro(object): # 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) + LOG.debug("Non-persistently setting the system hostname to %s", + hostname) try: util.subp(['hostname', hostname]) except util.ProcessExecutionError: diff --git a/cloudinit/distros/parsers/sys_conf.py b/cloudinit/distros/parsers/sys_conf.py index 1cefb8bc..5cd765fc 100644 --- a/cloudinit/distros/parsers/sys_conf.py +++ b/cloudinit/distros/parsers/sys_conf.py @@ -22,7 +22,7 @@ import pipes import re # This library is used to parse/write -# out the various sysconfig files edited +# 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 @@ -30,11 +30,26 @@ import re # 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): - if (re.search(r"\$\{.+\}", text) or - re.search(r"\$[a-zA-Z_]+[a-zA-Z0-9_]*", text)): - return True + for r in SHELL_VAR_REGEXES: + if r.search(text): + return True return False @@ -58,33 +73,36 @@ class SysConf(configobj.ConfigObj): raise ValueError('Value "%s" is not a string' % (value)) if len(value) == 0: return '' - quot_func = (lambda x: str(x)) + 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) + quot_func = (lambda x: + self._get_single_quote(x) % x) else: # Quote whitespace if it isn't the start + end of a shell command - white_space_ok = False if value.strip().startswith("$(") and value.strip().endswith(")"): - white_space_ok = True - if re.search(r"[\t\r\n ]", value) and not white_space_ok: - 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) + 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 = (lambda x: self._get_single_quote(x) % x) - else: - quot_func = pipes.quote + 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 - if this_entry.startswith("'") or this_entry.startswith('"'): - val = this_entry val = self._decode_element(self._quote(this_entry)) key = self._decode_element(self._quote(entry)) cmnt = self._decode_element(comment) @@ -93,4 +111,3 @@ class SysConf(configobj.ConfigObj): self._a_to_u('='), val, cmnt) - diff --git a/tests/unittests/test_distros/test_sysconfig.py b/tests/unittests/test_distros/test_sysconfig.py index 21e161ad..1e34909d 100644 --- a/tests/unittests/test_distros/test_sysconfig.py +++ b/tests/unittests/test_distros/test_sysconfig.py @@ -9,7 +9,8 @@ from cloudinit.distros.parsers.sys_conf import SysConf # http://content.hccfl.edu/pollock/AUnix1/SysconfigFilesDesc.txt class TestSysConfHelper(MockerTestCase): - def assertRegexpMatches(self, text, regexp): + # This function was added in 2.7, make it work for 2.6 + def assertRegMatches(self, text, regexp): regexp = re.compile(regexp) self.assertTrue(regexp.search(text), msg="%s must match %s!" % (text, regexp.pattern)) @@ -34,6 +35,21 @@ USEMD5=no''' '-G ${DEVICE} rx 256 tx 256')) self.assertEquals(contents, str(conf)) + def test_parse_shell_vars(self): + contents = 'USESMBAUTH=$XYZ' + conf = SysConf(contents.splitlines()) + self.assertEquals(contents, str(conf)) + conf = SysConf('') + conf['B'] = '${ZZ}d apples' + # Should be quoted + self.assertEquals('B="${ZZ}d apples"', str(conf)) + conf = SysConf('') + conf['B'] = '$? d apples' + self.assertEquals('B="$? d apples"', str(conf)) + contents = 'IPMI_WATCHDOG_OPTIONS="timeout=60"' + conf = SysConf(contents.splitlines()) + self.assertEquals('IPMI_WATCHDOG_OPTIONS=timeout=60', str(conf)) + def test_parse_adjust(self): contents = 'IPV6TO4_ROUTING="eth0-:0004::1/64 eth1-:0005::1/64"' conf = SysConf(contents.splitlines()) @@ -43,7 +59,8 @@ USEMD5=no''' conf['IPV6TO4_ROUTING'] = "blah \tblah" contents2 = str(conf).strip() # Should be requoted due to whitespace - self.assertRegexpMatches(contents2, r'IPV6TO4_ROUTING=[\']blah\s+blah[\']') + self.assertRegMatches(contents2, + r'IPV6TO4_ROUTING=[\']blah\s+blah[\']') def test_parse_no_adjust_shell(self): conf = SysConf(''.splitlines()) -- cgit v1.2.3 From 923f5c70fbff04ff538a5df17c300a9f39a85180 Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Tue, 23 Oct 2012 06:18:24 -0400 Subject: fix pep8/pylint --- cloudinit/distros/__init__.py | 2 +- tests/unittests/test_datasource/test_configdrive.py | 5 ++--- tests/unittests/test_distros/test_user_data_normalize.py | 1 + 3 files changed, 4 insertions(+), 4 deletions(-) (limited to 'cloudinit/distros/__init__.py') diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py index abd6cc48..cac3ed7a 100644 --- a/cloudinit/distros/__init__.py +++ b/cloudinit/distros/__init__.py @@ -424,7 +424,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 diff --git a/tests/unittests/test_datasource/test_configdrive.py b/tests/unittests/test_datasource/test_configdrive.py index 4fa13db8..00379e03 100644 --- a/tests/unittests/test_datasource/test_configdrive.py +++ b/tests/unittests/test_datasource/test_configdrive.py @@ -6,11 +6,10 @@ import os.path import mocker from mocker import MockerTestCase -from cloudinit.sources import DataSourceConfigDrive as ds +from cloudinit import helpers from cloudinit import settings +from cloudinit.sources import DataSourceConfigDrive as ds from cloudinit import util -from cloudinit import helpers - PUBKEY = u'ssh-rsa AAAAB3NzaC1....sIkJhq8wdX+4I3A4cYbYP ubuntu@server-460\n' diff --git a/tests/unittests/test_distros/test_user_data_normalize.py b/tests/unittests/test_distros/test_user_data_normalize.py index f27af826..8f0d8896 100644 --- a/tests/unittests/test_distros/test_user_data_normalize.py +++ b/tests/unittests/test_distros/test_user_data_normalize.py @@ -14,6 +14,7 @@ bcfg = { 'groups': ["foo"] } + class TestUGNormalize(MockerTestCase): def _make_distro(self, dtype, def_user=None): -- cgit v1.2.3 From 2953d9d448fde3af19fc96ae00f41066f510d6fd Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Tue, 23 Oct 2012 07:54:40 -0700 Subject: No need for the get default users groups function when its provided by the get user function. --- cloudinit/distros/__init__.py | 3 --- 1 file changed, 3 deletions(-) (limited to 'cloudinit/distros/__init__.py') diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py index 6ef63442..11a72da1 100644 --- a/cloudinit/distros/__init__.py +++ b/cloudinit/distros/__init__.py @@ -175,9 +175,6 @@ class Distro(object): def get_default_user(self): return self.get_option('default_user') - def get_default_user_groups(self): - return self.get_option('default_user_groups') - def create_user(self, name, **kwargs): """ Creates users for the system using the GNU passwd tools. This -- cgit v1.2.3 From aa8b51a48a30e3a3c863ca0ddb8bc4667026d57a Mon Sep 17 00:00:00 2001 From: harlowja Date: Sat, 27 Oct 2012 19:25:48 -0700 Subject: Helpful cleanups. 1. Remove the usage of the path.join function now that all code should be going through the util file methods (and they can be mocked out as needed). 2. Adjust all occurences of the above join function to either not use it or replace it with the standard os.path.join (which can also be mocked out as needed) 3. Fix pylint from complaining about the tests folder 'helpers.py' not being found 4. Add a pylintrc file that is used instead of the options hidden in the 'run_pylint' tool. --- Makefile | 8 +-- cloudinit/config/cc_apt_pipelining.py | 12 ++-- cloudinit/config/cc_apt_update_upgrade.py | 13 ++-- cloudinit/config/cc_ca_certs.py | 24 ++++---- cloudinit/config/cc_chef.py | 30 +++++----- cloudinit/config/cc_landscape.py | 14 ++--- cloudinit/config/cc_mcollective.py | 22 +++---- cloudinit/config/cc_mounts.py | 9 ++- cloudinit/config/cc_phone_home.py | 4 +- cloudinit/config/cc_puppet.py | 70 ++++++++++------------ cloudinit/config/cc_resizefs.py | 5 +- cloudinit/config/cc_rsyslog.py | 3 +- cloudinit/config/cc_runcmd.py | 2 +- cloudinit/config/cc_salt_minion.py | 6 +- cloudinit/config/cc_set_passwords.py | 6 +- cloudinit/config/cc_ssh.py | 16 +++-- cloudinit/config/cc_ssh_authkey_fingerprints.py | 7 +-- cloudinit/config/cc_update_etc_hosts.py | 3 +- cloudinit/distros/__init__.py | 8 +-- cloudinit/distros/debian.py | 26 +++----- cloudinit/helpers.py | 29 +-------- cloudinit/sources/__init__.py | 2 - cloudinit/ssh_util.py | 26 ++++---- pylintrc | 19 ++++++ tests/__init__.py | 0 tests/unittests/__init__.py | 0 tests/unittests/test_datasource/__init__.py | 0 tests/unittests/test_distros/__init__.py | 0 tests/unittests/test_filters/__init__.py | 0 tests/unittests/test_filters/test_launch_index.py | 10 +--- tests/unittests/test_handler/__init__.py | 0 .../test_handler/test_handler_ca_certs.py | 18 +++--- tests/unittests/test_runs/__init__.py | 0 tests/unittests/test_runs/test_simple_run.py | 10 +--- tools/run-pylint | 19 ++---- 35 files changed, 170 insertions(+), 251 deletions(-) create mode 100644 pylintrc create mode 100644 tests/__init__.py create mode 100644 tests/unittests/__init__.py create mode 100644 tests/unittests/test_datasource/__init__.py create mode 100644 tests/unittests/test_distros/__init__.py create mode 100644 tests/unittests/test_filters/__init__.py create mode 100644 tests/unittests/test_handler/__init__.py create mode 100644 tests/unittests/test_runs/__init__.py (limited to 'cloudinit/distros/__init__.py') diff --git a/Makefile b/Makefile index 49324ca0..8f5646b7 100644 --- a/Makefile +++ b/Makefile @@ -1,20 +1,20 @@ CWD=$(shell pwd) -PY_FILES=$(shell find cloudinit bin tests tools -name "*.py") +PY_FILES=$(shell find cloudinit bin tests tools -type f -name "*.py") PY_FILES+="bin/cloud-init" all: test pep8: - $(CWD)/tools/run-pep8 $(PY_FILES) + @$(CWD)/tools/run-pep8 $(PY_FILES) pylint: - $(CWD)/tools/run-pylint $(PY_FILES) + @$(CWD)/tools/run-pylint $(PY_FILES) pyflakes: pyflakes $(PY_FILES) test: - nosetests $(noseopts) tests/unittests/ + @nosetests $(noseopts) tests/ 2to3: 2to3 $(PY_FILES) diff --git a/cloudinit/config/cc_apt_pipelining.py b/cloudinit/config/cc_apt_pipelining.py index 02056ee0..e5629175 100644 --- a/cloudinit/config/cc_apt_pipelining.py +++ b/cloudinit/config/cc_apt_pipelining.py @@ -34,26 +34,24 @@ APT_PIPE_TPL = ("//Written by cloud-init per 'apt_pipelining'\n" # on TCP connections - otherwise data corruption will occur. -def handle(_name, cfg, cloud, log, _args): +def handle(_name, cfg, _cloud, log, _args): apt_pipe_value = util.get_cfg_option_str(cfg, "apt_pipelining", False) apt_pipe_value_s = str(apt_pipe_value).lower().strip() if apt_pipe_value_s == "false": - write_apt_snippet(cloud, "0", log, DEFAULT_FILE) + write_apt_snippet("0", log, DEFAULT_FILE) elif apt_pipe_value_s in ("none", "unchanged", "os"): return elif apt_pipe_value_s in [str(b) for b in xrange(0, 6)]: - write_apt_snippet(cloud, apt_pipe_value_s, log, DEFAULT_FILE) + write_apt_snippet(apt_pipe_value_s, log, DEFAULT_FILE) else: log.warn("Invalid option for apt_pipeling: %s", apt_pipe_value) -def write_apt_snippet(cloud, setting, log, f_name): +def write_apt_snippet(setting, log, f_name): """Writes f_name with apt pipeline depth 'setting'.""" file_contents = APT_PIPE_TPL % (setting) - - util.write_file(cloud.paths.join(False, f_name), file_contents) - + util.write_file(f_name, file_contents) log.debug("Wrote %s with apt pipeline depth setting %s", f_name, setting) diff --git a/cloudinit/config/cc_apt_update_upgrade.py b/cloudinit/config/cc_apt_update_upgrade.py index 356bb98d..59c34b59 100644 --- a/cloudinit/config/cc_apt_update_upgrade.py +++ b/cloudinit/config/cc_apt_update_upgrade.py @@ -78,8 +78,7 @@ def handle(name, cfg, cloud, log, _args): try: # See man 'apt.conf' contents = PROXY_TPL % (proxy) - util.write_file(cloud.paths.join(False, proxy_filename), - contents) + util.write_file(proxy_filename, contents) except Exception as e: util.logexc(log, "Failed to write proxy to %s", proxy_filename) elif os.path.isfile(proxy_filename): @@ -90,7 +89,7 @@ def handle(name, cfg, cloud, log, _args): params = mirrors params['RELEASE'] = release params['MIRROR'] = mirror - errors = add_sources(cloud, cfg['apt_sources'], params) + errors = add_sources(cfg['apt_sources'], params) for e in errors: log.warn("Source Error: %s", ':'.join(e)) @@ -196,11 +195,10 @@ def generate_sources_list(codename, mirrors, cloud, log): params = {'codename': codename} for k in mirrors: params[k] = mirrors[k] - out_fn = cloud.paths.join(False, '/etc/apt/sources.list') - templater.render_to_file(template_fn, out_fn, params) + templater.render_to_file(template_fn, '/etc/apt/sources.list', params) -def add_sources(cloud, srclist, template_params=None): +def add_sources(srclist, template_params=None): """ add entries in /etc/apt/sources.list.d for each abbreviated sources.list entry in 'srclist'. When rendering template, also @@ -250,8 +248,7 @@ def add_sources(cloud, srclist, template_params=None): try: contents = "%s\n" % (source) - util.write_file(cloud.paths.join(False, ent['filename']), - contents, omode="ab") + util.write_file(ent['filename'], contents, omode="ab") except: errorlist.append([source, "failed write to file %s" % ent['filename']]) diff --git a/cloudinit/config/cc_ca_certs.py b/cloudinit/config/cc_ca_certs.py index dc046bda..20f24357 100644 --- a/cloudinit/config/cc_ca_certs.py +++ b/cloudinit/config/cc_ca_certs.py @@ -22,6 +22,7 @@ CA_CERT_PATH = "/usr/share/ca-certificates/" CA_CERT_FILENAME = "cloud-init-ca-certs.crt" CA_CERT_CONFIG = "/etc/ca-certificates.conf" CA_CERT_SYSTEM_PATH = "/etc/ssl/certs/" +CA_CERT_FULL_PATH = os.path.join(CA_CERT_PATH, CA_CERT_FILENAME) distros = ['ubuntu', 'debian'] @@ -33,7 +34,7 @@ def update_ca_certs(): util.subp(["update-ca-certificates"], capture=False) -def add_ca_certs(paths, certs): +def add_ca_certs(certs): """ Adds certificates to the system. To actually apply the new certificates you must also call L{update_ca_certs}. @@ -43,27 +44,24 @@ def add_ca_certs(paths, certs): if certs: # First ensure they are strings... cert_file_contents = "\n".join([str(c) for c in certs]) - cert_file_fullpath = os.path.join(CA_CERT_PATH, CA_CERT_FILENAME) - cert_file_fullpath = paths.join(False, cert_file_fullpath) - util.write_file(cert_file_fullpath, cert_file_contents, mode=0644) + util.write_file(CA_CERT_FULL_PATH, cert_file_contents, mode=0644) # Append cert filename to CA_CERT_CONFIG file. - util.write_file(paths.join(False, CA_CERT_CONFIG), - "\n%s" % CA_CERT_FILENAME, omode="ab") + util.write_file(CA_CERT_CONFIG, "\n%s" % CA_CERT_FILENAME, omode="ab") -def remove_default_ca_certs(paths): +def remove_default_ca_certs(): """ Removes all default trusted CA certificates from the system. To actually apply the change you must also call L{update_ca_certs}. """ - util.delete_dir_contents(paths.join(False, CA_CERT_PATH)) - util.delete_dir_contents(paths.join(False, CA_CERT_SYSTEM_PATH)) - util.write_file(paths.join(False, CA_CERT_CONFIG), "", mode=0644) + util.delete_dir_contents(CA_CERT_PATH) + util.delete_dir_contents(CA_CERT_SYSTEM_PATH) + util.write_file(CA_CERT_CONFIG, "", mode=0644) debconf_sel = "ca-certificates ca-certificates/trust_new_crts select no" util.subp(('debconf-set-selections', '-'), debconf_sel) -def handle(name, cfg, cloud, log, _args): +def handle(name, cfg, _cloud, log, _args): """ Call to handle ca-cert sections in cloud-config file. @@ -85,14 +83,14 @@ def handle(name, cfg, cloud, log, _args): # default trusted CA certs first. if ca_cert_cfg.get("remove-defaults", False): log.debug("Removing default certificates") - remove_default_ca_certs(cloud.paths) + remove_default_ca_certs() # If we are given any new trusted CA certs to add, add them. if "trusted" in ca_cert_cfg: trusted_certs = util.get_cfg_option_list(ca_cert_cfg, "trusted") if trusted_certs: log.debug("Adding %d certificates" % len(trusted_certs)) - add_ca_certs(cloud.paths, trusted_certs) + add_ca_certs(trusted_certs) # Update the system with the new cert configuration. log.debug("Updating certificates") diff --git a/cloudinit/config/cc_chef.py b/cloudinit/config/cc_chef.py index 6f568261..7a3d6a31 100644 --- a/cloudinit/config/cc_chef.py +++ b/cloudinit/config/cc_chef.py @@ -26,6 +26,15 @@ from cloudinit import util RUBY_VERSION_DEFAULT = "1.8" +CHEF_DIRS = [ + '/etc/chef', + '/var/log/chef', + '/var/lib/chef', + '/var/cache/chef', + '/var/backups/chef', + '/var/run/chef', +] + def handle(name, cfg, cloud, log, _args): @@ -37,24 +46,15 @@ def handle(name, cfg, cloud, log, _args): chef_cfg = cfg['chef'] # Ensure the chef directories we use exist - c_dirs = [ - '/etc/chef', - '/var/log/chef', - '/var/lib/chef', - '/var/cache/chef', - '/var/backups/chef', - '/var/run/chef', - ] - for d in c_dirs: - util.ensure_dir(cloud.paths.join(False, d)) + for d in CHEF_DIRS: + util.ensure_dir(d) # Set the validation key based on the presence of either 'validation_key' # or 'validation_cert'. In the case where both exist, 'validation_key' # takes precedence for key in ('validation_key', 'validation_cert'): if key in chef_cfg and chef_cfg[key]: - v_fn = cloud.paths.join(False, '/etc/chef/validation.pem') - util.write_file(v_fn, chef_cfg[key]) + util.write_file('/etc/chef/validation.pem', chef_cfg[key]) break # Create the chef config from template @@ -68,8 +68,7 @@ def handle(name, cfg, cloud, log, _args): '_default'), 'validation_name': chef_cfg['validation_name'] } - out_fn = cloud.paths.join(False, '/etc/chef/client.rb') - templater.render_to_file(template_fn, out_fn, params) + templater.render_to_file(template_fn, '/etc/chef/client.rb', params) else: log.warn("No template found, not rendering to /etc/chef/client.rb") @@ -81,8 +80,7 @@ def handle(name, cfg, cloud, log, _args): initial_attributes = chef_cfg['initial_attributes'] for k in list(initial_attributes.keys()): initial_json[k] = initial_attributes[k] - firstboot_fn = cloud.paths.join(False, '/etc/chef/firstboot.json') - util.write_file(firstboot_fn, json.dumps(initial_json)) + util.write_file('/etc/chef/firstboot.json', json.dumps(initial_json)) # If chef is not installed, we install chef based on 'install_type' if not os.path.isfile('/usr/bin/chef-client'): diff --git a/cloudinit/config/cc_landscape.py b/cloudinit/config/cc_landscape.py index 56ab0ce3..02610dd0 100644 --- a/cloudinit/config/cc_landscape.py +++ b/cloudinit/config/cc_landscape.py @@ -66,22 +66,16 @@ def handle(_name, cfg, cloud, log, _args): merge_data = [ LSC_BUILTIN_CFG, - cloud.paths.join(True, LSC_CLIENT_CFG_FILE), + LSC_CLIENT_CFG_FILE, ls_cloudcfg, ] merged = merge_together(merge_data) - - lsc_client_fn = cloud.paths.join(False, LSC_CLIENT_CFG_FILE) - lsc_dir = cloud.paths.join(False, os.path.dirname(lsc_client_fn)) - if not os.path.isdir(lsc_dir): - util.ensure_dir(lsc_dir) - contents = StringIO() merged.write(contents) - contents.flush() - util.write_file(lsc_client_fn, contents.getvalue()) - log.debug("Wrote landscape config file to %s", lsc_client_fn) + util.ensure_dir(os.path.dirname(LSC_CLIENT_CFG_FILE)) + util.write_file(LSC_CLIENT_CFG_FILE, contents.getvalue()) + log.debug("Wrote landscape config file to %s", LSC_CLIENT_CFG_FILE) util.write_file(LS_DEFAULT_FILE, "RUN=1\n") util.subp(["service", "landscape-client", "restart"]) diff --git a/cloudinit/config/cc_mcollective.py b/cloudinit/config/cc_mcollective.py index 2acdbc6f..b670390d 100644 --- a/cloudinit/config/cc_mcollective.py +++ b/cloudinit/config/cc_mcollective.py @@ -29,6 +29,7 @@ from cloudinit import util PUBCERT_FILE = "/etc/mcollective/ssl/server-public.pem" PRICERT_FILE = "/etc/mcollective/ssl/server-private.pem" +SERVER_CFG = '/etc/mcollective/server.cfg' def handle(name, cfg, cloud, log, _args): @@ -48,26 +49,23 @@ def handle(name, cfg, cloud, log, _args): if 'conf' in mcollective_cfg: # Read server.cfg values from the # original file in order to be able to mix the rest up - server_cfg_fn = cloud.paths.join(True, '/etc/mcollective/server.cfg') - mcollective_config = ConfigObj(server_cfg_fn) + mcollective_config = ConfigObj(SERVER_CFG) # See: http://tiny.cc/jh9agw for (cfg_name, cfg) in mcollective_cfg['conf'].iteritems(): if cfg_name == 'public-cert': - pubcert_fn = cloud.paths.join(True, PUBCERT_FILE) - util.write_file(pubcert_fn, cfg, mode=0644) - mcollective_config['plugin.ssl_server_public'] = pubcert_fn + util.write_file(PUBCERT_FILE, cfg, mode=0644) + mcollective_config['plugin.ssl_server_public'] = PUBCERT_FILE mcollective_config['securityprovider'] = 'ssl' elif cfg_name == 'private-cert': - pricert_fn = cloud.paths.join(True, PRICERT_FILE) - util.write_file(pricert_fn, cfg, mode=0600) - mcollective_config['plugin.ssl_server_private'] = pricert_fn + util.write_file(PRICERT_FILE, cfg, mode=0600) + mcollective_config['plugin.ssl_server_private'] = PRICERT_FILE mcollective_config['securityprovider'] = 'ssl' else: if isinstance(cfg, (basestring, str)): # Just set it in the 'main' section mcollective_config[cfg_name] = cfg elif isinstance(cfg, (dict)): - # Iterate throug the config items, create a section + # Iterate through the config items, create a section # if it is needed and then add/or create items as needed if cfg_name not in mcollective_config.sections: mcollective_config[cfg_name] = {} @@ -78,14 +76,12 @@ def handle(name, cfg, cloud, log, _args): mcollective_config[cfg_name] = str(cfg) # We got all our config as wanted we'll rename # the previous server.cfg and create our new one - old_fn = cloud.paths.join(False, '/etc/mcollective/server.cfg.old') - util.rename(server_cfg_fn, old_fn) + util.rename(SERVER_CFG, "%s.old" % (SERVER_CFG)) # Now we got the whole file, write to disk... contents = StringIO() mcollective_config.write(contents) contents = contents.getvalue() - server_cfg_rw = cloud.paths.join(False, '/etc/mcollective/server.cfg') - util.write_file(server_cfg_rw, contents, mode=0644) + util.write_file(SERVER_CFG, contents, mode=0644) # Start mcollective util.subp(['service', 'mcollective', 'start'], capture=False) diff --git a/cloudinit/config/cc_mounts.py b/cloudinit/config/cc_mounts.py index 14c965bb..cb772c86 100644 --- a/cloudinit/config/cc_mounts.py +++ b/cloudinit/config/cc_mounts.py @@ -28,6 +28,7 @@ from cloudinit import util SHORTNAME_FILTER = r"^[x]{0,1}[shv]d[a-z][0-9]*$" SHORTNAME = re.compile(SHORTNAME_FILTER) WS = re.compile("[%s]+" % (whitespace)) +FSTAB_PATH = "/etc/fstab" def is_mdname(name): @@ -167,8 +168,7 @@ def handle(_name, cfg, cloud, log, _args): cc_lines.append('\t'.join(line)) fstab_lines = [] - fstab = util.load_file(cloud.paths.join(True, "/etc/fstab")) - for line in fstab.splitlines(): + for line in util.load_file(FSTAB_PATH).splitlines(): try: toks = WS.split(line) if toks[3].find(comment) != -1: @@ -179,7 +179,7 @@ def handle(_name, cfg, cloud, log, _args): fstab_lines.extend(cc_lines) contents = "%s\n" % ('\n'.join(fstab_lines)) - util.write_file(cloud.paths.join(False, "/etc/fstab"), contents) + util.write_file(FSTAB_PATH, contents) if needswap: try: @@ -188,9 +188,8 @@ def handle(_name, cfg, cloud, log, _args): util.logexc(log, "Activating swap via 'swapon -a' failed") for d in dirs: - real_dir = cloud.paths.join(False, d) try: - util.ensure_dir(real_dir) + util.ensure_dir(d) except: util.logexc(log, "Failed to make '%s' config-mount", d) diff --git a/cloudinit/config/cc_phone_home.py b/cloudinit/config/cc_phone_home.py index ae1349eb..886487f8 100644 --- a/cloudinit/config/cc_phone_home.py +++ b/cloudinit/config/cc_phone_home.py @@ -84,10 +84,10 @@ def handle(name, cfg, cloud, log, args): for (n, path) in pubkeys.iteritems(): try: - all_keys[n] = util.load_file(cloud.paths.join(True, path)) + all_keys[n] = util.load_file(path) except: util.logexc(log, ("%s: failed to open, can not" - " phone home that data"), path) + " phone home that data!"), path) submit_keys = {} for k in post_list: diff --git a/cloudinit/config/cc_puppet.py b/cloudinit/config/cc_puppet.py index 74ee18e1..8fe3af57 100644 --- a/cloudinit/config/cc_puppet.py +++ b/cloudinit/config/cc_puppet.py @@ -21,12 +21,32 @@ from StringIO import StringIO import os -import pwd import socket from cloudinit import helpers from cloudinit import util +PUPPET_CONF_PATH = '/etc/puppet/puppet.conf' +PUPPET_SSL_CERT_DIR = '/var/lib/puppet/ssl/certs/' +PUPPET_SSL_DIR = '/var/lib/puppet/ssl' +PUPPET_SSL_CERT_PATH = '/var/lib/puppet/ssl/certs/ca.pem' + + +def _autostart_puppet(log): + # Set puppet to automatically start + if os.path.exists('/etc/default/puppet'): + util.subp(['sed', '-i', + '-e', 's/^START=.*/START=yes/', + '/etc/default/puppet'], capture=False) + elif os.path.exists('/bin/systemctl'): + util.subp(['/bin/systemctl', 'enable', 'puppet.service'], + capture=False) + elif os.path.exists('/sbin/chkconfig'): + util.subp(['/sbin/chkconfig', 'puppet', 'on'], capture=False) + else: + log.warn(("Sorry we do not know how to enable" + " puppet services on this system")) + def handle(name, cfg, cloud, log, _args): # If there isn't a puppet key in the configuration don't do anything @@ -43,8 +63,7 @@ def handle(name, cfg, cloud, log, _args): # ... and then update the puppet configuration if 'conf' in puppet_cfg: # Add all sections from the conf object to puppet.conf - puppet_conf_fn = cloud.paths.join(True, '/etc/puppet/puppet.conf') - contents = util.load_file(puppet_conf_fn) + contents = util.load_file(PUPPET_CONF_PATH) # Create object for reading puppet.conf values puppet_config = helpers.DefaultingConfigParser() # Read puppet.conf values from original file in order to be able to @@ -53,28 +72,19 @@ def handle(name, cfg, cloud, log, _args): cleaned_lines = [i.lstrip() for i in contents.splitlines()] cleaned_contents = '\n'.join(cleaned_lines) puppet_config.readfp(StringIO(cleaned_contents), - filename=puppet_conf_fn) + filename=PUPPET_CONF_PATH) for (cfg_name, cfg) in puppet_cfg['conf'].iteritems(): # Cert configuration is a special case # Dump the puppet master ca certificate in the correct place if cfg_name == 'ca_cert': # Puppet ssl sub-directory isn't created yet # Create it with the proper permissions and ownership - pp_ssl_dir = cloud.paths.join(False, '/var/lib/puppet/ssl') - util.ensure_dir(pp_ssl_dir, 0771) - util.chownbyid(pp_ssl_dir, - pwd.getpwnam('puppet').pw_uid, 0) - pp_ssl_certs = cloud.paths.join(False, - '/var/lib/puppet/ssl/certs/') - util.ensure_dir(pp_ssl_certs) - util.chownbyid(pp_ssl_certs, - pwd.getpwnam('puppet').pw_uid, 0) - pp_ssl_ca_certs = cloud.paths.join(False, - ('/var/lib/puppet/' - 'ssl/certs/ca.pem')) - util.write_file(pp_ssl_ca_certs, cfg) - util.chownbyid(pp_ssl_ca_certs, - pwd.getpwnam('puppet').pw_uid, 0) + util.ensure_dir(PUPPET_SSL_DIR, 0771) + util.chownbyname(PUPPET_SSL_DIR, 'puppet', 'root') + util.ensure_dir(PUPPET_SSL_CERT_DIR) + util.chownbyname(PUPPET_SSL_CERT_DIR, 'puppet', 'root') + util.write_file(PUPPET_SSL_CERT_PATH, str(cfg)) + util.chownbyname(PUPPET_SSL_CERT_PATH, 'puppet', 'root') else: # Iterate throug the config items, we'll use ConfigParser.set # to overwrite or create new items as needed @@ -90,25 +100,11 @@ def handle(name, cfg, cloud, log, _args): puppet_config.set(cfg_name, o, v) # We got all our config as wanted we'll rename # the previous puppet.conf and create our new one - conf_old_fn = cloud.paths.join(False, - '/etc/puppet/puppet.conf.old') - util.rename(puppet_conf_fn, conf_old_fn) - puppet_conf_rw = cloud.paths.join(False, '/etc/puppet/puppet.conf') - util.write_file(puppet_conf_rw, puppet_config.stringify()) + util.rename(PUPPET_CONF_PATH, "%s.old" % (PUPPET_CONF_PATH)) + util.write_file(PUPPET_CONF_PATH, puppet_config.stringify()) - # Set puppet to automatically start - if os.path.exists('/etc/default/puppet'): - util.subp(['sed', '-i', - '-e', 's/^START=.*/START=yes/', - '/etc/default/puppet'], capture=False) - elif os.path.exists('/bin/systemctl'): - util.subp(['/bin/systemctl', 'enable', 'puppet.service'], - capture=False) - elif os.path.exists('/sbin/chkconfig'): - util.subp(['/sbin/chkconfig', 'puppet', 'on'], capture=False) - else: - log.warn(("Sorry we do not know how to enable" - " puppet services on this system")) + # Set it up so it autostarts + _autostart_puppet(log) # Start puppetd util.subp(['service', 'puppet', 'start'], capture=False) diff --git a/cloudinit/config/cc_resizefs.py b/cloudinit/config/cc_resizefs.py index e7f27944..b958f332 100644 --- a/cloudinit/config/cc_resizefs.py +++ b/cloudinit/config/cc_resizefs.py @@ -62,7 +62,7 @@ def get_fs_type(st_dev, path, log): raise -def handle(name, cfg, cloud, log, args): +def handle(name, cfg, _cloud, log, args): if len(args) != 0: resize_root = args[0] else: @@ -74,11 +74,10 @@ def handle(name, cfg, cloud, log, args): # TODO(harlowja) is the directory ok to be used?? resize_root_d = util.get_cfg_option_str(cfg, "resize_rootfs_tmp", "/run") - resize_root_d = cloud.paths.join(False, resize_root_d) util.ensure_dir(resize_root_d) # TODO(harlowja): allow what is to be resized to be configurable?? - resize_what = cloud.paths.join(False, "/") + resize_what = "/" with util.ExtendedTemporaryFile(prefix="cloudinit.resizefs.", dir=resize_root_d, delete=True) as tfh: devpth = tfh.name diff --git a/cloudinit/config/cc_rsyslog.py b/cloudinit/config/cc_rsyslog.py index 78327526..0c2c6880 100644 --- a/cloudinit/config/cc_rsyslog.py +++ b/cloudinit/config/cc_rsyslog.py @@ -71,8 +71,7 @@ def handle(name, cfg, cloud, log, _args): try: contents = "%s\n" % (content) - util.write_file(cloud.paths.join(False, filename), - contents, omode=omode) + util.write_file(filename, contents, omode=omode) except Exception: util.logexc(log, "Failed to write to %s", filename) diff --git a/cloudinit/config/cc_runcmd.py b/cloudinit/config/cc_runcmd.py index 65064cfb..598c3a3e 100644 --- a/cloudinit/config/cc_runcmd.py +++ b/cloudinit/config/cc_runcmd.py @@ -33,6 +33,6 @@ def handle(name, cfg, cloud, log, _args): cmd = cfg["runcmd"] try: content = util.shellify(cmd) - util.write_file(cloud.paths.join(False, out_fn), content, 0700) + util.write_file(out_fn, content, 0700) except: util.logexc(log, "Failed to shellify %s into file %s", cmd, out_fn) diff --git a/cloudinit/config/cc_salt_minion.py b/cloudinit/config/cc_salt_minion.py index 8a1440d9..f3eede18 100644 --- a/cloudinit/config/cc_salt_minion.py +++ b/cloudinit/config/cc_salt_minion.py @@ -34,8 +34,7 @@ def handle(name, cfg, cloud, log, _args): cloud.distro.install_packages(["salt-minion"]) # Ensure we can configure files at the right dir - config_dir = cloud.paths.join(False, salt_cfg.get("config_dir", - '/etc/salt')) + config_dir = salt_cfg.get("config_dir", '/etc/salt') util.ensure_dir(config_dir) # ... and then update the salt configuration @@ -47,8 +46,7 @@ def handle(name, cfg, cloud, log, _args): # ... copy the key pair if specified if 'public_key' in salt_cfg and 'private_key' in salt_cfg: - pki_dir = cloud.paths.join(False, salt_cfg.get('pki_dir', - '/etc/salt/pki')) + pki_dir = salt_cfg.get('pki_dir', '/etc/salt/pki') with util.umask(077): util.ensure_dir(pki_dir) pub_name = os.path.join(pki_dir, 'minion.pub') diff --git a/cloudinit/config/cc_set_passwords.py b/cloudinit/config/cc_set_passwords.py index 26c558ad..c6bf62fd 100644 --- a/cloudinit/config/cc_set_passwords.py +++ b/cloudinit/config/cc_set_passwords.py @@ -114,8 +114,7 @@ def handle(_name, cfg, cloud, log, args): replaced_auth = False # See: man sshd_config - conf_fn = cloud.paths.join(True, ssh_util.DEF_SSHD_CFG) - old_lines = ssh_util.parse_ssh_config(conf_fn) + old_lines = ssh_util.parse_ssh_config(ssh_util.DEF_SSHD_CFG) new_lines = [] i = 0 for (i, line) in enumerate(old_lines): @@ -134,8 +133,7 @@ def handle(_name, cfg, cloud, log, args): pw_auth)) lines = [str(e) for e in new_lines] - ssh_rw_fn = cloud.paths.join(False, ssh_util.DEF_SSHD_CFG) - util.write_file(ssh_rw_fn, "\n".join(lines)) + util.write_file(ssh_util.DEF_SSHD_CFG, "\n".join(lines)) try: cmd = ['service'] diff --git a/cloudinit/config/cc_ssh.py b/cloudinit/config/cc_ssh.py index 32e48c30..b623d476 100644 --- a/cloudinit/config/cc_ssh.py +++ b/cloudinit/config/cc_ssh.py @@ -59,7 +59,7 @@ def handle(_name, cfg, cloud, log, _args): # remove the static keys from the pristine image if cfg.get("ssh_deletekeys", True): - key_pth = cloud.paths.join(False, "/etc/ssh/", "ssh_host_*key*") + key_pth = os.path.join("/etc/ssh/", "ssh_host_*key*") for f in glob.glob(key_pth): try: util.del_file(f) @@ -72,8 +72,7 @@ def handle(_name, cfg, cloud, log, _args): if key in KEY_2_FILE: tgt_fn = KEY_2_FILE[key][0] tgt_perms = KEY_2_FILE[key][1] - util.write_file(cloud.paths.join(False, tgt_fn), - val, tgt_perms) + util.write_file(tgt_fn, val, tgt_perms) for (priv, pub) in PRIV_2_PUB.iteritems(): if pub in cfg['ssh_keys'] or not priv in cfg['ssh_keys']: @@ -94,7 +93,7 @@ def handle(_name, cfg, cloud, log, _args): 'ssh_genkeytypes', GENERATE_KEY_NAMES) for keytype in genkeys: - keyfile = cloud.paths.join(False, KEY_FILE_TPL % (keytype)) + keyfile = KEY_FILE_TPL % (keytype) util.ensure_dir(os.path.dirname(keyfile)) if not os.path.exists(keyfile): cmd = ['ssh-keygen', '-t', keytype, '-N', '', '-f', keyfile] @@ -118,17 +117,16 @@ def handle(_name, cfg, cloud, log, _args): cfgkeys = cfg["ssh_authorized_keys"] keys.extend(cfgkeys) - apply_credentials(keys, user, cloud.paths, - disable_root, disable_root_opts) + apply_credentials(keys, user, disable_root, disable_root_opts) except: util.logexc(log, "Applying ssh credentials failed!") -def apply_credentials(keys, user, paths, disable_root, disable_root_opts): +def apply_credentials(keys, user, disable_root, disable_root_opts): keys = set(keys) if user: - ssh_util.setup_user_keys(keys, user, '', paths) + ssh_util.setup_user_keys(keys, user, '') if disable_root: if not user: @@ -137,4 +135,4 @@ def apply_credentials(keys, user, paths, disable_root, disable_root_opts): else: key_prefix = '' - ssh_util.setup_user_keys(keys, 'root', key_prefix, paths) + ssh_util.setup_user_keys(keys, 'root', key_prefix) diff --git a/cloudinit/config/cc_ssh_authkey_fingerprints.py b/cloudinit/config/cc_ssh_authkey_fingerprints.py index 8c9a8806..c38bcea2 100644 --- a/cloudinit/config/cc_ssh_authkey_fingerprints.py +++ b/cloudinit/config/cc_ssh_authkey_fingerprints.py @@ -97,9 +97,8 @@ def handle(name, cfg, cloud, log, _args): "logging of ssh fingerprints disabled"), name) hash_meth = util.get_cfg_option_str(cfg, "authkey_hash", "md5") - extract_func = ssh_util.extract_authorized_keys (users, _groups) = ds.normalize_users_groups(cfg, cloud.distro) for (user_name, _cfg) in users.items(): - (auth_key_fn, auth_key_entries) = extract_func(user_name, cloud.paths) - _pprint_key_entries(user_name, auth_key_fn, - auth_key_entries, hash_meth) + (key_fn, key_entries) = ssh_util.extract_authorized_keys(user_name) + _pprint_key_entries(user_name, key_fn, + key_entries, hash_meth) diff --git a/cloudinit/config/cc_update_etc_hosts.py b/cloudinit/config/cc_update_etc_hosts.py index 4d75000f..96103615 100644 --- a/cloudinit/config/cc_update_etc_hosts.py +++ b/cloudinit/config/cc_update_etc_hosts.py @@ -42,8 +42,7 @@ def handle(name, cfg, cloud, log, _args): raise RuntimeError(("No hosts template could be" " found for distro %s") % (cloud.distro.name)) - out_fn = cloud.paths.join(False, '/etc/hosts') - templater.render_to_file(tpl_fn_name, out_fn, + templater.render_to_file(tpl_fn_name, '/etc/hosts', {'hostname': hostname, 'fqdn': fqdn}) elif manage_hosts == "localhost": diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py index 2fbb0e9b..869540d2 100644 --- a/cloudinit/distros/__init__.py +++ b/cloudinit/distros/__init__.py @@ -122,8 +122,7 @@ class Distro(object): 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(): + for line in util.load_file("/etc/hosts").splitlines(): if line.strip().startswith(header): continue if not line.strip() or line.strip().startswith("#"): @@ -147,8 +146,7 @@ class Distro(object): need_write = True if need_write: contents = new_etchosts.getvalue() - util.write_file(self._paths.join(False, "/etc/hosts"), - contents, mode=0644) + util.write_file("/etc/hosts", contents, mode=0644) def _bring_up_interface(self, device_name): cmd = ['ifup', device_name] @@ -262,7 +260,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 diff --git a/cloudinit/distros/debian.py b/cloudinit/distros/debian.py index 88f4e978..cc7e53a0 100644 --- a/cloudinit/distros/debian.py +++ b/cloudinit/distros/debian.py @@ -43,7 +43,7 @@ class Distro(distros.Distro): def apply_locale(self, locale, out_fn=None): if not out_fn: - out_fn = self._paths.join(False, '/etc/default/locale') + out_fn = '/etc/default/locale' util.subp(['locale-gen', locale], capture=False) util.subp(['update-locale', locale], capture=False) lines = ["# Created by cloud-init", 'LANG="%s"' % (locale), ""] @@ -54,8 +54,7 @@ class Distro(distros.Distro): self.package_command('install', pkglist) def _write_network(self, settings): - net_fn = self._paths.join(False, "/etc/network/interfaces") - util.write_file(net_fn, settings) + util.write_file("/etc/network/interfaces", settings) return ['all'] def _bring_up_interfaces(self, device_names): @@ -69,12 +68,9 @@ class Distro(distros.Distro): 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]) + self._write_hostname(hostname, "/etc/hostname") + LOG.debug("Setting hostname to %s", hostname) + util.subp(['hostname', hostname]) def _write_hostname(self, hostname, out_fn): # "" gives trailing newline. @@ -82,16 +78,14 @@ class Distro(distros.Distro): 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) + hostname_in_etc = self._read_hostname("/etc/hostname") 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) + update_files.append("/etc/hostname") for fn in update_files: try: self._write_hostname(hostname, fn) @@ -103,7 +97,6 @@ class Distro(distros.Distro): 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]) @@ -130,9 +123,8 @@ class Distro(distros.Distro): " 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")) + util.write_file("/etc/timezone", "\n".join(tz_lines)) + util.copy(tz_file, "/etc/localtime") def package_command(self, command, args=None): e = os.environ.copy() diff --git a/cloudinit/helpers.py b/cloudinit/helpers.py index a4b20208..985ce3e5 100644 --- a/cloudinit/helpers.py +++ b/cloudinit/helpers.py @@ -302,14 +302,10 @@ class Paths(object): def __init__(self, path_cfgs, ds=None): self.cfgs = path_cfgs # Populate all the initial paths - self.cloud_dir = self.join(False, - path_cfgs.get('cloud_dir', - '/var/lib/cloud')) + self.cloud_dir = path_cfgs.get('cloud_dir', '/var/lib/cloud') self.instance_link = os.path.join(self.cloud_dir, 'instance') self.boot_finished = os.path.join(self.instance_link, "boot-finished") self.upstart_conf_d = path_cfgs.get('upstart_dir') - if self.upstart_conf_d: - self.upstart_conf_d = self.join(False, self.upstart_conf_d) self.seed_dir = os.path.join(self.cloud_dir, 'seed') # This one isn't joined, since it should just be read-only template_dir = path_cfgs.get('templates_dir', '/etc/cloud/templates/') @@ -328,29 +324,6 @@ class Paths(object): # Set when a datasource becomes active self.datasource = ds - # joins the paths but also appends a read - # or write root if available - def join(self, read_only, *paths): - if read_only: - root = self.cfgs.get('read_root') - else: - root = self.cfgs.get('write_root') - if not paths: - return root - if len(paths) > 1: - joined = os.path.join(*paths) - else: - joined = paths[0] - if root: - pre_joined = joined - # Need to remove any starting '/' since this - # will confuse os.path.join - joined = joined.lstrip("/") - joined = os.path.join(root, joined) - LOG.debug("Translated %s to adjusted path %s (read-only=%s)", - pre_joined, joined, read_only) - return joined - # get_ipath_cur: get the current instance path for an item def get_ipath_cur(self, name=None): ipath = self.instance_link diff --git a/cloudinit/sources/__init__.py b/cloudinit/sources/__init__.py index b22369a8..745627d0 100644 --- a/cloudinit/sources/__init__.py +++ b/cloudinit/sources/__init__.py @@ -20,8 +20,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from email.mime.multipart import MIMEMultipart - import abc import os diff --git a/cloudinit/ssh_util.py b/cloudinit/ssh_util.py index 88a11a1a..dd6b742f 100644 --- a/cloudinit/ssh_util.py +++ b/cloudinit/ssh_util.py @@ -212,17 +212,15 @@ def update_authorized_keys(old_entries, keys): return '\n'.join(lines) -def users_ssh_info(username, paths): +def users_ssh_info(username): pw_ent = pwd.getpwnam(username) - if not pw_ent: + if not pw_ent or not pw_ent.pw_dir: raise RuntimeError("Unable to get ssh info for user %r" % (username)) - ssh_dir = paths.join(False, os.path.join(pw_ent.pw_dir, '.ssh')) - return (ssh_dir, pw_ent) + return (os.path.join(pw_ent.pw_dir, '.ssh'), pw_ent) -def extract_authorized_keys(username, paths): - (ssh_dir, pw_ent) = users_ssh_info(username, paths) - sshd_conf_fn = paths.join(True, DEF_SSHD_CFG) +def extract_authorized_keys(username): + (ssh_dir, pw_ent) = users_ssh_info(username) auth_key_fn = None with util.SeLinuxGuard(ssh_dir, recursive=True): try: @@ -231,7 +229,7 @@ def extract_authorized_keys(username, paths): # The following tokens are defined: %% is replaced by a literal # '%', %h is replaced by the home directory of the user being # authenticated and %u is replaced by the username of that user. - ssh_cfg = parse_ssh_config_map(sshd_conf_fn) + ssh_cfg = parse_ssh_config_map(DEF_SSHD_CFG) auth_key_fn = ssh_cfg.get("authorizedkeysfile", '').strip() if not auth_key_fn: auth_key_fn = "%h/.ssh/authorized_keys" @@ -240,7 +238,6 @@ def extract_authorized_keys(username, paths): auth_key_fn = auth_key_fn.replace("%%", '%') if not auth_key_fn.startswith('/'): auth_key_fn = os.path.join(pw_ent.pw_dir, auth_key_fn) - auth_key_fn = paths.join(False, auth_key_fn) except (IOError, OSError): # Give up and use a default key filename auth_key_fn = os.path.join(ssh_dir, 'authorized_keys') @@ -248,14 +245,13 @@ def extract_authorized_keys(username, paths): " in ssh config" " from %r, using 'AuthorizedKeysFile' file" " %r instead"), - sshd_conf_fn, auth_key_fn) - auth_key_entries = parse_authorized_keys(auth_key_fn) - return (auth_key_fn, auth_key_entries) + DEF_SSHD_CFG, auth_key_fn) + return (auth_key_fn, parse_authorized_keys(auth_key_fn)) -def setup_user_keys(keys, username, key_prefix, paths): +def setup_user_keys(keys, username, key_prefix): # Make sure the users .ssh dir is setup accordingly - (ssh_dir, pwent) = users_ssh_info(username, paths) + (ssh_dir, pwent) = users_ssh_info(username) if not os.path.isdir(ssh_dir): util.ensure_dir(ssh_dir, mode=0700) util.chownbyid(ssh_dir, pwent.pw_uid, pwent.pw_gid) @@ -267,7 +263,7 @@ def setup_user_keys(keys, username, key_prefix, paths): key_entries.append(parser.parse(str(k), def_opt=key_prefix)) # Extract the old and make the new - (auth_key_fn, auth_key_entries) = extract_authorized_keys(username, paths) + (auth_key_fn, auth_key_entries) = extract_authorized_keys(username) with util.SeLinuxGuard(ssh_dir, recursive=True): content = update_authorized_keys(auth_key_entries, key_entries) util.ensure_dir(os.path.dirname(auth_key_fn), mode=0700) diff --git a/pylintrc b/pylintrc new file mode 100644 index 00000000..ee886510 --- /dev/null +++ b/pylintrc @@ -0,0 +1,19 @@ +[General] +init-hook='import sys; sys.path.append("tests/")' + +[MESSAGES CONTROL] +# See: http://pylint-messages.wikidot.com/all-codes +# W0142: *args and **kwargs are fine. +# W0511: TODOs in code comments are fine. +# W0702: No exception type(s) specified +# W0703: Catch "Exception" +# C0103: Invalid name +# C0111: Missing docstring +disable=W0142,W0511,W0702,W0703,C0103,C0111 + +[REPORTS] +reports=no +include-ids=yes + +[FORMAT] +max-line-length=79 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unittests/__init__.py b/tests/unittests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unittests/test_datasource/__init__.py b/tests/unittests/test_datasource/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unittests/test_distros/__init__.py b/tests/unittests/test_distros/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unittests/test_filters/__init__.py b/tests/unittests/test_filters/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unittests/test_filters/test_launch_index.py b/tests/unittests/test_filters/test_launch_index.py index 1e9b9053..773bb312 100644 --- a/tests/unittests/test_filters/test_launch_index.py +++ b/tests/unittests/test_filters/test_launch_index.py @@ -1,14 +1,6 @@ import copy -import os -import sys -top_dir = os.path.join(os.path.dirname(__file__), os.pardir, "helpers.py") -top_dir = os.path.abspath(top_dir) -if os.path.exists(top_dir): - sys.path.insert(0, os.path.dirname(top_dir)) - - -import helpers +from tests.unittests import helpers import itertools diff --git a/tests/unittests/test_handler/__init__.py b/tests/unittests/test_handler/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unittests/test_handler/test_handler_ca_certs.py b/tests/unittests/test_handler/test_handler_ca_certs.py index d3df5c50..d73c9fa9 100644 --- a/tests/unittests/test_handler/test_handler_ca_certs.py +++ b/tests/unittests/test_handler/test_handler_ca_certs.py @@ -77,7 +77,7 @@ class TestConfig(MockerTestCase): """Test that a single cert gets passed to add_ca_certs.""" config = {"ca-certs": {"trusted": ["CERT1"]}} - self.mock_add(self.paths, ["CERT1"]) + self.mock_add(["CERT1"]) self.mock_update() self.mocker.replay() @@ -87,7 +87,7 @@ class TestConfig(MockerTestCase): """Test that multiple certs get passed to add_ca_certs.""" config = {"ca-certs": {"trusted": ["CERT1", "CERT2"]}} - self.mock_add(self.paths, ["CERT1", "CERT2"]) + self.mock_add(["CERT1", "CERT2"]) self.mock_update() self.mocker.replay() @@ -97,7 +97,7 @@ class TestConfig(MockerTestCase): """Test remove_defaults works as expected.""" config = {"ca-certs": {"remove-defaults": True}} - self.mock_remove(self.paths) + self.mock_remove() self.mock_update() self.mocker.replay() @@ -116,8 +116,8 @@ class TestConfig(MockerTestCase): """Test remove_defaults is not called when config value is False.""" config = {"ca-certs": {"remove-defaults": True, "trusted": ["CERT1"]}} - self.mock_remove(self.paths) - self.mock_add(self.paths, ["CERT1"]) + self.mock_remove() + self.mock_add(["CERT1"]) self.mock_update() self.mocker.replay() @@ -136,7 +136,7 @@ class TestAddCaCerts(MockerTestCase): """Test that no certificate are written if not provided.""" self.mocker.replace(util.write_file, passthrough=False) self.mocker.replay() - cc_ca_certs.add_ca_certs(self.paths, []) + cc_ca_certs.add_ca_certs([]) def test_single_cert(self): """Test adding a single certificate to the trusted CAs.""" @@ -149,7 +149,7 @@ class TestAddCaCerts(MockerTestCase): "\ncloud-init-ca-certs.crt", omode="ab") self.mocker.replay() - cc_ca_certs.add_ca_certs(self.paths, [cert]) + cc_ca_certs.add_ca_certs([cert]) def test_multiple_certs(self): """Test adding multiple certificates to the trusted CAs.""" @@ -163,7 +163,7 @@ class TestAddCaCerts(MockerTestCase): "\ncloud-init-ca-certs.crt", omode="ab") self.mocker.replay() - cc_ca_certs.add_ca_certs(self.paths, certs) + cc_ca_certs.add_ca_certs(certs) class TestUpdateCaCerts(MockerTestCase): @@ -198,4 +198,4 @@ class TestRemoveDefaultCaCerts(MockerTestCase): "ca-certificates ca-certificates/trust_new_crts select no") self.mocker.replay() - cc_ca_certs.remove_default_ca_certs(self.paths) + cc_ca_certs.remove_default_ca_certs() diff --git a/tests/unittests/test_runs/__init__.py b/tests/unittests/test_runs/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unittests/test_runs/test_simple_run.py b/tests/unittests/test_runs/test_simple_run.py index 1e852e1e..22d6cf2c 100644 --- a/tests/unittests/test_runs/test_simple_run.py +++ b/tests/unittests/test_runs/test_simple_run.py @@ -1,14 +1,6 @@ import os -import sys -# Allow running this test individually -top_dir = os.path.join(os.path.dirname(__file__), os.pardir, "helpers.py") -top_dir = os.path.abspath(top_dir) -if os.path.exists(top_dir): - sys.path.insert(0, os.path.dirname(top_dir)) - - -import helpers +from tests.unittests import helpers from cloudinit.settings import (PER_INSTANCE) from cloudinit import stages diff --git a/tools/run-pylint b/tools/run-pylint index 7ef44ac5..b74efda9 100755 --- a/tools/run-pylint +++ b/tools/run-pylint @@ -6,23 +6,16 @@ else files=( "$@" ); fi +RC_FILE="pylintrc" +if [ ! -f $RC_FILE ]; then + RC_FILE="../pylintrc" +fi + cmd=( pylint - --reports=n - --include-ids=y - --max-line-length=79 - + --rcfile=$RC_FILE --disable=R --disable=I - - --disable=W0142 # Used * or ** magic - --disable=W0511 # TODO/FIXME note - --disable=W0702 # No exception type(s) specified - --disable=W0703 # Catch "Exception" - - --disable=C0103 # Invalid name - --disable=C0111 # Missing docstring - "${files[@]}" ) -- cgit v1.2.3 From b80c2401123e16b9038ff3fb6f6d660717ee68e1 Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Thu, 8 Nov 2012 17:37:16 -0800 Subject: Fix the case where on a redhat based system the fully qualified domain name should end up in /etc/sysconfig/network by passing the fqdn to the update and set hostname methods and using it accordingly. LP: #1076759 --- cloudinit/config/cc_set_hostname.py | 10 ++++++---- cloudinit/config/cc_update_hostname.py | 8 +++++--- cloudinit/distros/__init__.py | 4 ++-- cloudinit/distros/debian.py | 4 ++-- cloudinit/distros/rhel.py | 25 +++++++++++++++++-------- 5 files changed, 32 insertions(+), 19 deletions(-) (limited to 'cloudinit/distros/__init__.py') diff --git a/cloudinit/config/cc_set_hostname.py b/cloudinit/config/cc_set_hostname.py index b0f27ebf..2b32fc94 100644 --- a/cloudinit/config/cc_set_hostname.py +++ b/cloudinit/config/cc_set_hostname.py @@ -27,9 +27,11 @@ def handle(name, cfg, cloud, log, _args): " not setting the hostname in module %s"), name) return - (hostname, _fqdn) = util.get_hostname_fqdn(cfg, cloud) + (hostname, fqdn) = util.get_hostname_fqdn(cfg, cloud) try: - log.debug("Setting hostname to %s", hostname) - cloud.distro.set_hostname(hostname) + log.debug("Setting the hostname to %s (%s)", fqdn, hostname) + cloud.distro.set_hostname(hostname, fqdn) except Exception: - util.logexc(log, "Failed to set hostname to %s", hostname) + util.logexc(log, "Failed to set the hostname to %s (%s)", + fqdn, hostname) + raise diff --git a/cloudinit/config/cc_update_hostname.py b/cloudinit/config/cc_update_hostname.py index 1d6679ea..52225cd8 100644 --- a/cloudinit/config/cc_update_hostname.py +++ b/cloudinit/config/cc_update_hostname.py @@ -32,10 +32,12 @@ def handle(name, cfg, cloud, log, _args): " not updating the hostname in module %s"), name) return - (hostname, _fqdn) = util.get_hostname_fqdn(cfg, cloud) + (hostname, fqdn) = util.get_hostname_fqdn(cfg, cloud) try: prev_fn = os.path.join(cloud.get_cpath('data'), "previous-hostname") - cloud.distro.update_hostname(hostname, prev_fn) + log.debug("Updating hostname to %s (%s)", fqdn, hostname) + cloud.distro.update_hostname(hostname, fqdn, prev_fn) except Exception: - util.logexc(log, "Failed to set the hostname to %s", hostname) + util.logexc(log, "Failed to update the hostname to %s (%s)", + fqdn, hostname) raise diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py index 869540d2..bd04ba79 100644 --- a/cloudinit/distros/__init__.py +++ b/cloudinit/distros/__init__.py @@ -58,11 +58,11 @@ class Distro(object): return self._cfg.get(opt_name, default) @abc.abstractmethod - def set_hostname(self, hostname): + def set_hostname(self, hostname, fqdn=None): raise NotImplementedError() @abc.abstractmethod - def update_hostname(self, hostname, prev_hostname_fn): + def update_hostname(self, hostname, fqdn, prev_hostname_fn): raise NotImplementedError() @abc.abstractmethod diff --git a/cloudinit/distros/debian.py b/cloudinit/distros/debian.py index cc7e53a0..ed4070b4 100644 --- a/cloudinit/distros/debian.py +++ b/cloudinit/distros/debian.py @@ -67,7 +67,7 @@ class Distro(distros.Distro): else: return distros.Distro._bring_up_interfaces(self, device_names) - def set_hostname(self, hostname): + def set_hostname(self, hostname, fqdn=None): self._write_hostname(hostname, "/etc/hostname") LOG.debug("Setting hostname to %s", hostname) util.subp(['hostname', hostname]) @@ -76,7 +76,7 @@ class Distro(distros.Distro): # "" gives trailing newline. util.write_file(out_fn, "%s\n" % str(hostname), 0644) - def update_hostname(self, hostname, prev_fn): + def update_hostname(self, hostname, fqdn, prev_fn): hostname_prev = self._read_hostname(prev_fn) hostname_in_etc = self._read_hostname("/etc/hostname") update_files = [] diff --git a/cloudinit/distros/rhel.py b/cloudinit/distros/rhel.py index bf3c18d2..e4c27216 100644 --- a/cloudinit/distros/rhel.py +++ b/cloudinit/distros/rhel.py @@ -146,8 +146,13 @@ class Distro(distros.Distro): lines.insert(0, _make_header()) util.write_file(fn, "\n".join(lines), 0644) - def set_hostname(self, hostname): - self._write_hostname(hostname, '/etc/sysconfig/network') + def set_hostname(self, hostname, fqdn=None): + # See: http://bit.ly/TwitgL + # Should be fqdn if we can use it + sysconfig_hostname = fqdn + if not sysconfig_hostname: + sysconfig_hostname = hostname + self._write_hostname(sysconfig_hostname, '/etc/sysconfig/network') LOG.debug("Setting hostname to %s", hostname) util.subp(['hostname', hostname]) @@ -165,28 +170,32 @@ class Distro(distros.Distro): } self._update_sysconfig_file(out_fn, host_cfg) - def update_hostname(self, hostname, prev_file): + def update_hostname(self, hostname, fqdn, prev_file): + # See: http://bit.ly/TwitgL + # Should be fqdn if we can use it + sysconfig_hostname = fqdn + if not sysconfig_hostname: + sysconfig_hostname = hostname 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: + if not hostname_prev or hostname_prev != sysconfig_hostname: update_files.append(prev_file) if (not hostname_in_sys or (hostname_in_sys == hostname_prev - and hostname_in_sys != hostname)): + and hostname_in_sys != sysconfig_hostname)): update_files.append("/etc/sysconfig/network") for fn in update_files: try: - self._write_hostname(hostname, fn) + self._write_hostname(sysconfig_hostname, fn) except: util.logexc(LOG, "Failed to write hostname %s to %s", - hostname, fn) + sysconfig_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]) -- cgit v1.2.3 From 8c006684034c13719171672836edfc65bf02ebe9 Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Fri, 9 Nov 2012 14:40:41 -0800 Subject: Fix pep8 warnings. --- cloudinit/distros/__init__.py | 2 +- tools/run-pep8 | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) (limited to 'cloudinit/distros/__init__.py') diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py index bd04ba79..3392a065 100644 --- a/cloudinit/distros/__init__.py +++ b/cloudinit/distros/__init__.py @@ -81,7 +81,7 @@ 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) diff --git a/tools/run-pep8 b/tools/run-pep8 index 1dfa92c3..20e594bc 100755 --- a/tools/run-pep8 +++ b/tools/run-pep8 @@ -31,6 +31,7 @@ IGNORE="$IGNORE,E125" # Continuation line does not distinguish itself from next IGNORE="$IGNORE,E126" # Continuation line over-indented for hanging indent IGNORE="$IGNORE,E127" # Continuation line over-indented for visual indent IGNORE="$IGNORE,E128" # Continuation line under-indented for visual indent +IGNORE="$IGNORE,E502" # The backslash is redundant between brackets cmd=( ${base}/hacking.py -- cgit v1.2.3 From 1169dcc5f18fd9a5adbf353bec87e48d563550a5 Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Fri, 9 Nov 2012 15:28:35 -0800 Subject: Fix the merging of group configuration when that group configuration is a dict => members. LP: #1077245 --- cloudinit/distros/__init__.py | 32 +++++++++++++++++++--- .../test_distros/test_user_data_normalize.py | 22 +++++++++++++++ 2 files changed, 50 insertions(+), 4 deletions(-) (limited to 'cloudinit/distros/__init__.py') diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py index 3392a065..2d01efc3 100644 --- a/cloudinit/distros/__init__.py +++ b/cloudinit/distros/__init__.py @@ -425,12 +425,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(): diff --git a/tests/unittests/test_distros/test_user_data_normalize.py b/tests/unittests/test_distros/test_user_data_normalize.py index 8f0d8896..50400c8a 100644 --- a/tests/unittests/test_distros/test_user_data_normalize.py +++ b/tests/unittests/test_distros/test_user_data_normalize.py @@ -30,6 +30,28 @@ class TestUGNormalize(MockerTestCase): def _norm(self, cfg, distro): return distros.normalize_users_groups(cfg, distro) + def test_group_dict(self): + distro = self._make_distro('ubuntu') + g = {'groups': [ + { + 'ubuntu': ['foo', 'bar'], + 'bob': 'users', + }, + 'cloud-users', + { + 'bob': 'users2', + }, + ] + } + (users, groups) = self._norm(g, distro) + self.assertIn('ubuntu', groups) + ub_members = groups['ubuntu'] + self.assertEquals(sorted(['foo', 'bar']), sorted(ub_members)) + self.assertIn('bob', groups) + b_members = groups['bob'] + self.assertEquals(sorted(['users', 'users2']), + sorted(b_members)) + def test_basic_groups(self): distro = self._make_distro('ubuntu') ug_cfg = { -- cgit v1.2.3 From a17a69c35c1de0a6bd6f054f76d3da9e4a9c5364 Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Sat, 10 Nov 2012 10:15:16 -0800 Subject: Sudoers.d creation cleanups + tests. --- cloudinit/distros/__init__.py | 20 +++++++++++------ tests/unittests/helpers.py | 1 + tests/unittests/test_distros/test_generic.py | 32 +++++++++++++++++++++++++--- tests/unittests/test_runs/test_merge_run.py | 2 +- 4 files changed, 45 insertions(+), 10 deletions(-) (limited to 'cloudinit/distros/__init__.py') diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py index 2d01efc3..d2cb0a8b 100644 --- a/cloudinit/distros/__init__.py +++ b/cloudinit/distros/__init__.py @@ -283,8 +283,10 @@ class Distro(object): # 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() @@ -299,18 +301,24 @@ class Distro(object): found_include = True break if not found_include: - sudoers_contents += "\n#includedir %s\n" % (path) try: - if not os.path.exists(sudo_base): + if not base_exists: + lines = [('# See sudoers(5) for more information' + ' on "#include" directives:'), '', + '# Added by cloud-init', + "#includedir %s" % (path), ''] + sudoers_contents = "\n".join(lines) util.write_file(sudo_base, sudoers_contents, 0440) else: - with open(sudo_base, 'a') as f: - f.write(sudoers_contents) - LOG.debug("added '#includedir %s' to %s" % (path, sudo_base)) + lines = ['', '# Added by cloud-init', + "#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, 0755) + util.ensure_dir(path, 0750) def write_sudo_rules(self, user, diff --git a/tests/unittests/helpers.py b/tests/unittests/helpers.py index 2c5dcad2..e8080668 100644 --- a/tests/unittests/helpers.py +++ b/tests/unittests/helpers.py @@ -103,6 +103,7 @@ class FilesystemMockingTestCase(ResourceUsingTestCase): def patchUtils(self, new_root): patch_funcs = { util: [('write_file', 1), + ('append_file', 1), ('load_file', 1), ('ensure_dir', 1), ('chmod', 1), diff --git a/tests/unittests/test_distros/test_generic.py b/tests/unittests/test_distros/test_generic.py index 2df4c2f0..704699b5 100644 --- a/tests/unittests/test_distros/test_generic.py +++ b/tests/unittests/test_distros/test_generic.py @@ -1,6 +1,9 @@ -from mocker import MockerTestCase - from cloudinit import distros +from cloudinit import util + +from tests.unittests import helpers + +import os unknown_arch_info = { 'arches': ['default'], @@ -27,7 +30,7 @@ gpmi = distros._get_package_mirror_info # pylint: disable=W0212 gapmi = distros._get_arch_package_mirror_info # pylint: disable=W0212 -class TestGenericDistro(MockerTestCase): +class TestGenericDistro(helpers.FilesystemMockingTestCase): def return_first(self, mlist): if not mlist: @@ -52,6 +55,29 @@ class TestGenericDistro(MockerTestCase): # Make a temp directoy for tests to use. self.tmp = self.makeDir() + def test_sudoers_ensure_new(self): + cls = distros.fetch("ubuntu") + d = cls("ubuntu", {}, None) + self.patchOS(self.tmp) + self.patchUtils(self.tmp) + d.ensure_sudo_dir("/b") + contents = util.load_file("/etc/sudoers") + self.assertIn("includedir /b", contents) + self.assertTrue(os.path.isdir("/b")) + + def test_sudoers_ensure_append(self): + cls = distros.fetch("ubuntu") + d = cls("ubuntu", {}, None) + self.patchOS(self.tmp) + self.patchUtils(self.tmp) + util.write_file("/etc/sudoers", "josh, josh\n") + d.ensure_sudo_dir("/b") + contents = util.load_file("/etc/sudoers") + self.assertIn("includedir /b", contents) + self.assertTrue(os.path.isdir("/b")) + self.assertIn("josh", contents) + self.assertEquals(2, contents.count("josh")) + def test_arch_package_mirror_info_unknown(self): """for an unknown arch, we should get back that with arch 'default'.""" arch_mirrors = gapmi(package_mirrors, arch="unknown") diff --git a/tests/unittests/test_runs/test_merge_run.py b/tests/unittests/test_runs/test_merge_run.py index 04c03730..36de97ae 100644 --- a/tests/unittests/test_runs/test_merge_run.py +++ b/tests/unittests/test_runs/test_merge_run.py @@ -7,7 +7,7 @@ from cloudinit import stages from cloudinit import util -class TestSimpleRun(helpers.FilesystemMockingTestCase): +class TestMergeRun(helpers.FilesystemMockingTestCase): def _patchIn(self, root): self.restore() self.patchOS(root) -- cgit v1.2.3 From 3248ac9bbb2008e88a3bd9c030ba0fcbc14b7fce Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Sat, 10 Nov 2012 22:32:49 -0500 Subject: whitespace / indentation cleanups These changes were pulled out of the previous merge (cc_yum_add_repo) as they were unrelated there. Re-applying them here. --- cloudinit/distros/__init__.py | 38 +++++++++++++--------------- cloudinit/sources/DataSourceAltCloud.py | 2 +- cloudinit/util.py | 3 +-- tests/unittests/test_runs/test_simple_run.py | 6 +++-- tools/hacking.py | 4 +-- 5 files changed, 26 insertions(+), 27 deletions(-) (limited to 'cloudinit/distros/__init__.py') diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py index d2cb0a8b..8a98e334 100644 --- a/cloudinit/distros/__init__.py +++ b/cloudinit/distros/__init__.py @@ -187,23 +187,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', - "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", + } # Now check the value and create the command for option in kwargs: @@ -320,11 +320,9 @@ class Distro(object): raise e util.ensure_dir(path, 0750) - def write_sudo_rules(self, - user, - rules, - sudo_file="/etc/sudoers.d/90-cloud-init-users", - ): + def write_sudo_rules(self, user, rules, sudo_file=None): + if not sudo_file: + sudo_file = "/etc/sudoers.d/90-cloud-init-users" content_header = "# user rules for %s" % user content = "%s\n%s %s\n\n" % (content_header, user, rules) diff --git a/cloudinit/sources/DataSourceAltCloud.py b/cloudinit/sources/DataSourceAltCloud.py index d7e1204f..9812bdcb 100644 --- a/cloudinit/sources/DataSourceAltCloud.py +++ b/cloudinit/sources/DataSourceAltCloud.py @@ -47,7 +47,7 @@ META_DATA_NOT_SUPPORTED = { 'instance-id': 455, 'local-hostname': 'localhost', 'placement': {}, - } +} def read_user_data_callback(mount_dir): diff --git a/cloudinit/util.py b/cloudinit/util.py index 7890a3d6..4f5b15ee 100644 --- a/cloudinit/util.py +++ b/cloudinit/util.py @@ -1193,8 +1193,7 @@ def yaml_dumps(obj): indent=4, explicit_start=True, explicit_end=True, - default_flow_style=False, - ) + default_flow_style=False) return formatted diff --git a/tests/unittests/test_runs/test_simple_run.py b/tests/unittests/test_runs/test_simple_run.py index 22d6cf2c..60ef812a 100644 --- a/tests/unittests/test_runs/test_simple_run.py +++ b/tests/unittests/test_runs/test_simple_run.py @@ -37,11 +37,13 @@ class TestSimpleRun(helpers.FilesystemMockingTestCase): self.replicateTestRoot('simple_ubuntu', new_root) cfg = { 'datasource_list': ['None'], - 'write_files': [{ + 'write_files': [ + { 'path': '/etc/blah.ini', 'content': 'blah', 'permissions': 0755, - }], + }, + ], 'cloud_init_modules': ['write-files'], } cloud_cfg = util.yaml_dumps(cfg) diff --git a/tools/hacking.py b/tools/hacking.py index 11163df3..26a07c53 100755 --- a/tools/hacking.py +++ b/tools/hacking.py @@ -66,8 +66,8 @@ def cloud_import_alphabetical(physical_line, line_number, lines): # handle import x # use .lower since capitalization shouldn't dictate order split_line = import_normalize(physical_line.strip()).lower().split() - split_previous = import_normalize(lines[line_number - 2] - ).strip().lower().split() + split_previous = import_normalize(lines[line_number - 2]) + split_previous = split_previous.strip().lower().split() # with or without "as y" length = [2, 4] if (len(split_line) in length and len(split_previous) in length and -- cgit v1.2.3 From 82e90789f11b5371b352a477b75cad0c5d1457ec Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Mon, 12 Nov 2012 14:30:08 -0800 Subject: Cleanup of /etc/hosts ordering and pep8/pylint adjustments. Fix how the comparison of a fqdn and its aliases was done via sorting instead of existence checking which is the better way to check if a alias already exists as well as cleanup the new files pep8/pylint issues. LP: #1078097 --- cloudinit/distros/__init__.py | 19 ++++++++++++------- cloudinit/distros/parsers/__init__.py | 1 + cloudinit/distros/parsers/hostname.py | 2 -- cloudinit/distros/parsers/resolv_conf.py | 4 +--- cloudinit/distros/parsers/sys_conf.py | 2 +- 5 files changed, 15 insertions(+), 13 deletions(-) (limited to 'cloudinit/distros/__init__.py') diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py index fa7cc1ca..4bde2393 100644 --- a/cloudinit/distros/__init__.py +++ b/cloudinit/distros/__init__.py @@ -189,15 +189,20 @@ class Distro(object): else: need_change = True for entry in prev_info: - if sorted(entry) == sorted([fqdn, hostname]): - # Exists already, leave it be - need_change = False - break + 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, change the first - # entry to be this entry + # Doesn't exist, add that entry in... new_entries = list(prev_info) - new_entries[0] = [fqdn, hostname] + new_entries.append([fqdn, hostname]) eh.del_entries(local_ip) for entry in new_entries: if len(entry) == 1: diff --git a/cloudinit/distros/parsers/__init__.py b/cloudinit/distros/parsers/__init__.py index 8334a5e6..1c413eaa 100644 --- a/cloudinit/distros/parsers/__init__.py +++ b/cloudinit/distros/parsers/__init__.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . + 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] diff --git a/cloudinit/distros/parsers/hostname.py b/cloudinit/distros/parsers/hostname.py index 7e19f017..617b3c36 100644 --- a/cloudinit/distros/parsers/hostname.py +++ b/cloudinit/distros/parsers/hostname.py @@ -86,5 +86,3 @@ class HostnameConf(object): raise IOError("Multiple hostnames (%s) found!" % (hostnames_found)) return entries - - diff --git a/cloudinit/distros/parsers/resolv_conf.py b/cloudinit/distros/parsers/resolv_conf.py index 377ada6b..5733c25a 100644 --- a/cloudinit/distros/parsers/resolv_conf.py +++ b/cloudinit/distros/parsers/resolv_conf.py @@ -127,7 +127,7 @@ class ResolvConf(object): # 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) + 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 " @@ -167,5 +167,3 @@ class ResolvConf(object): 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 index 5cd765fc..20ca1871 100644 --- a/cloudinit/distros/parsers/sys_conf.py +++ b/cloudinit/distros/parsers/sys_conf.py @@ -40,7 +40,7 @@ SHELL_VAR_REGEXES = [ # Things like $?, $0, $-, $@ re.compile(r"\$[0-9#\?\-@\*]"), # Things like ${blah:1} - but this one - # gets very complex so just try the + # gets very complex so just try the # simple path re.compile(r"\$\{.+\}"), ] -- cgit v1.2.3 From d5aeda9535ab530fd2c09e6ad37443c9013c3b4d Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Mon, 12 Nov 2012 22:08:26 -0800 Subject: Fix variable. --- cloudinit/distros/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'cloudinit/distros/__init__.py') diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py index 464ae550..3fc4483b 100644 --- a/cloudinit/distros/__init__.py +++ b/cloudinit/distros/__init__.py @@ -146,8 +146,7 @@ class Distro(object): def _select_hostname(self, hostname, fqdn): raise NotImplementedError() - def update_hostname(self, hostname, fqdn, - previous_hostname_filename): + def update_hostname(self, hostname, fqdn, prev_hostname_fn): applying_hostname = hostname hostname = self._select_hostname(hostname, fqdn) prev_hostname = self._read_hostname(prev_hostname_fn) -- cgit v1.2.3 From 8e86b79e8a5ab299ca77ec5e69facb807ede322f Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Mon, 12 Nov 2012 22:19:40 -0800 Subject: Remove these lines which are not needed. --- cloudinit/distros/__init__.py | 2 -- 1 file changed, 2 deletions(-) (limited to 'cloudinit/distros/__init__.py') diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py index 3fc4483b..10e07e82 100644 --- a/cloudinit/distros/__init__.py +++ b/cloudinit/distros/__init__.py @@ -41,8 +41,6 @@ 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" -- cgit v1.2.3 From c7c6ac0aa83192ffc267c27878712652dade35d1 Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Tue, 13 Nov 2012 13:47:30 -0800 Subject: Only attempt to read the previous hostname file if it exists. Instead of always reading the previous hostname file even if it did not exist lets only read it if it is a valid variable and is actually a existent file instead of just attempting to read it always. LP: #1078452 --- cloudinit/distros/__init__.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) (limited to 'cloudinit/distros/__init__.py') diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py index 10e07e82..ea0bac23 100644 --- a/cloudinit/distros/__init__.py +++ b/cloudinit/distros/__init__.py @@ -146,17 +146,37 @@ class Distro(object): 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) - prev_hostname = self._read_hostname(prev_hostname_fn) + + # 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)) @@ -173,6 +193,8 @@ class Distro(object): 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) -- cgit v1.2.3 From 75c3482a8685151407c186ce5b1f3b8af3db49d4 Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Wed, 14 Nov 2012 19:22:38 -0800 Subject: Fix sudoers being written multiple times when strings are used. LP: #1079002 --- cloudinit/distros/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'cloudinit/distros/__init__.py') diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py index ea0bac23..24e6f637 100644 --- a/cloudinit/distros/__init__.py +++ b/cloudinit/distros/__init__.py @@ -24,7 +24,6 @@ from StringIO import StringIO import abc -import collections import itertools import os import re @@ -421,7 +420,7 @@ class Distro(object): '', "# User rules for %s" % user, ] - if isinstance(rules, collections.Iterable): + if isinstance(rules, (list, tuple)): for rule in rules: lines.append("%s %s" % (user, rule)) else: -- cgit v1.2.3 From ef915a6ec712d89b9e0b3672947571976a49b68f Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Thu, 15 Nov 2012 12:32:05 -0800 Subject: Raise a type error when a sudoers rule is not an accepted type. --- cloudinit/distros/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'cloudinit/distros/__init__.py') diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py index 24e6f637..e724a418 100644 --- a/cloudinit/distros/__init__.py +++ b/cloudinit/distros/__init__.py @@ -423,8 +423,11 @@ class Distro(object): if isinstance(rules, (list, tuple)): for rule in rules: lines.append("%s %s" % (user, rule)) - else: + 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) self.ensure_sudo_dir(os.path.dirname(sudo_file)) -- cgit v1.2.3 From 3cb9a6ed620ab9200a18bf69cdac5ac518ca214c Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Tue, 20 Nov 2012 01:04:31 -0500 Subject: pep8 and pylint --- cloudinit/distros/__init__.py | 1 + tests/unittests/test_distros/test_generic.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) (limited to 'cloudinit/distros/__init__.py') diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py index e724a418..6a684b89 100644 --- a/cloudinit/distros/__init__.py +++ b/cloudinit/distros/__init__.py @@ -429,6 +429,7 @@ class Distro(object): 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): diff --git a/tests/unittests/test_distros/test_generic.py b/tests/unittests/test_distros/test_generic.py index 3ca769b4..7befb8c8 100644 --- a/tests/unittests/test_distros/test_generic.py +++ b/tests/unittests/test_distros/test_generic.py @@ -55,7 +55,7 @@ class TestGenericDistro(helpers.FilesystemMockingTestCase): # Make a temp directoy for tests to use. self.tmp = self.makeDir() - def _write_load_sudoers(self, user, rules): + def _write_load_sudoers(self, _user, rules): cls = distros.fetch("ubuntu") d = cls("ubuntu", {}, None) os.makedirs(os.path.join(self.tmp, "etc")) -- cgit v1.2.3 From 15a33d190f2a9247accf8834b005521c615cb6b3 Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Sat, 5 Jan 2013 10:04:58 -0800 Subject: Make which fields are redacted come from a field array. LP: #1096417 --- cloudinit/distros/__init__.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) (limited to 'cloudinit/distros/__init__.py') diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py index 6a684b89..8a3e0570 100644 --- a/cloudinit/distros/__init__.py +++ b/cloudinit/distros/__init__.py @@ -297,22 +297,26 @@ class Distro(object): "no_create_home": "-M", } + redact_fields = ['passwd'] + # Now check the value and create the command for option in kwargs: value = kwargs[option] 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. -- cgit v1.2.3 From 6cfd12c96608eb5fd086da49c4c685635e40e6e0 Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Sat, 5 Jan 2013 10:18:01 -0800 Subject: Fix the password locking logic. Instead of only not locking when system is present the logic should handle the correct case when lock password is set and system is not present. LP: #1096423 --- cloudinit/distros/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'cloudinit/distros/__init__.py') diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py index 6a684b89..be32757d 100644 --- a/cloudinit/distros/__init__.py +++ b/cloudinit/distros/__init__.py @@ -335,9 +335,10 @@ class Distro(object): 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): + # + # Which means if lock_passwd is False (on non-existent its true) + # then lock or if system is True (on non-existent its false) then lock. + if (kwargs.get('lock_passwd', True) or kwargs.get('system', False)): try: util.subp(['passwd', '--lock', name]) except Exception as e: -- cgit v1.2.3 From 361738c6a9a14e32bd2123828fab8d8b70c6bc3a Mon Sep 17 00:00:00 2001 From: ctracey Date: Tue, 15 Jan 2013 16:08:43 -0500 Subject: add support for operating system families often it is convenient to classify a distro as being part of an operating system family. for instance, file templates may be identical for both debian and ubuntu, but to support this under the current templating code, one would need multiple templates for the same code. similarly, configuration handlers often fall into the same bucket: the configuraton is known to work/has been tested on a particular family of operating systems. right now this is handled with a declaration like: distros = ['fedora', 'rhel'] this fix seeks to address both of these issues. it allows for the simplification of the above line to: osfamilies = ['redhat'] and provides a mechanism for operating system family templates. --- cloudinit/config/__init__.py | 4 +++- cloudinit/distros/__init__.py | 16 +++++++++++++++- cloudinit/distros/debian.py | 1 + cloudinit/distros/rhel.py | 1 + cloudinit/stages.py | 9 +++++++-- 5 files changed, 27 insertions(+), 4 deletions(-) (limited to 'cloudinit/distros/__init__.py') diff --git a/cloudinit/config/__init__.py b/cloudinit/config/__init__.py index 69a8cc68..d57453be 100644 --- a/cloudinit/config/__init__.py +++ b/cloudinit/config/__init__.py @@ -52,5 +52,7 @@ def fixup_module(mod, def_freq=PER_INSTANCE): if freq and freq not in FREQUENCIES: LOG.warn("Module %s has an unknown frequency %s", mod, freq) if not hasattr(mod, 'distros'): - setattr(mod, 'distros', None) + setattr(mod, 'distros', []) + if not hasattr(mod, 'osfamilies'): + setattr(mod, 'osfamilies', []) return mod diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py index 38b2f829..ff325b40 100644 --- a/cloudinit/distros/__init__.py +++ b/cloudinit/distros/__init__.py @@ -35,6 +35,11 @@ from cloudinit import util from cloudinit.distros.parsers import hosts +OSFAMILIES = { + 'debian': ['debian', 'ubuntu'], + 'redhat': ['fedora', 'rhel'] +} + LOG = logging.getLogger(__name__) @@ -143,6 +148,16 @@ class Distro(object): 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 @@ -515,7 +530,6 @@ def _get_package_mirror_info(mirror_info, availability_zone=None, return results - def _get_arch_package_mirror_info(package_mirrors, arch): # pull out the specific arch from a 'package_mirrors' config option default = None diff --git a/cloudinit/distros/debian.py b/cloudinit/distros/debian.py index 7422f4f0..49b73477 100644 --- a/cloudinit/distros/debian.py +++ b/cloudinit/distros/debian.py @@ -48,6 +48,7 @@ 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: diff --git a/cloudinit/distros/rhel.py b/cloudinit/distros/rhel.py index bc0877d5..e65be8d7 100644 --- a/cloudinit/distros/rhel.py +++ b/cloudinit/distros/rhel.py @@ -60,6 +60,7 @@ 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) diff --git a/cloudinit/stages.py b/cloudinit/stages.py index 8d3213b4..d7d1dea0 100644 --- a/cloudinit/stages.py +++ b/cloudinit/stages.py @@ -529,11 +529,16 @@ class Modules(object): freq = mod.frequency if not freq in FREQUENCIES: freq = PER_INSTANCE - worked_distros = mod.distros + + worked_distros = set(mod.distros) + worked_distros.update( + distros.Distro.expand_osfamily(mod.osfamilies)) + if (worked_distros and d_name not in worked_distros): LOG.warn(("Module %s is verified on %s distros" " but not on %s distro. It may or may not work" - " correctly."), name, worked_distros, d_name) + " correctly."), name, list(worked_distros), + d_name) # Use the configs logger and not our own # TODO(harlowja): possibly check the module # for having a LOG attr and just give it back -- cgit v1.2.3 From 5d4f4df6804995d74e7962f60dcd72b26bcac69b Mon Sep 17 00:00:00 2001 From: ctracey Date: Tue, 15 Jan 2013 16:25:20 -0500 Subject: cleanup a pep8 failure accidentally removed a line between two functions. --- cloudinit/distros/__init__.py | 1 + 1 file changed, 1 insertion(+) (limited to 'cloudinit/distros/__init__.py') diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py index ff325b40..5a2092c0 100644 --- a/cloudinit/distros/__init__.py +++ b/cloudinit/distros/__init__.py @@ -530,6 +530,7 @@ def _get_package_mirror_info(mirror_info, availability_zone=None, return results + def _get_arch_package_mirror_info(package_mirrors, arch): # pull out the specific arch from a 'package_mirrors' config option default = None -- cgit v1.2.3 From baefd17a9d997e11f85bf89d9337c2d40748bc37 Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Fri, 18 Jan 2013 10:57:20 -0800 Subject: Adjust how the legacy user: XYZ config alters the normalized user list Previously if a legacy user: XYZ entry was found, XYZ would not automatically be promoted to the default user but would instead just be added on as a new entry to the normalized user list. It appears the behavior that is wanted is for the XYZ entry to be added on as the default user (thus overriding a distro provided default user), which better matches how the code previous worked. LP: #1100920 --- cloudinit/distros/__init__.py | 67 +++++++++++++++------- .../test_distros/test_user_data_normalize.py | 10 +++- 2 files changed, 52 insertions(+), 25 deletions(-) (limited to 'cloudinit/distros/__init__.py') diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py index 38b2f829..c74be4e2 100644 --- a/cloudinit/distros/__init__.py +++ b/cloudinit/distros/__init__.py @@ -705,41 +705,64 @@ 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... + # 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 = None if 'user' in cfg and cfg['user']: - old_user = str(cfg['user']) - if not 'users' in cfg: - cfg['users'] = old_user + 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 = None - if 'users' in cfg: - default_user_config = None + + default_user_config = None + if not 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. 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) + else: + default_user_config = dict(old_user) + + 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/tests/unittests/test_distros/test_user_data_normalize.py b/tests/unittests/test_distros/test_user_data_normalize.py index 5d9d4311..50398c74 100644 --- a/tests/unittests/test_distros/test_user_data_normalize.py +++ b/tests/unittests/test_distros/test_user_data_normalize.py @@ -173,26 +173,29 @@ class TestUGNormalize(MockerTestCase): 'users': 'default' } (users, _groups) = self._norm(ug_cfg, distro) - self.assertIn('bob', users) + self.assertNotIn('bob', users) # Bob is not the default now, zetta is self.assertIn('zetta', users) + self.assertTrue(users['zetta']['default']) self.assertNotIn('default', users) ug_cfg = { 'user': 'zetta', 'users': 'default, joe' } (users, _groups) = self._norm(ug_cfg, distro) - self.assertIn('bob', users) + self.assertNotIn('bob', users) # Bob is not the default now, zetta is self.assertIn('joe', users) self.assertIn('zetta', users) + self.assertTrue(users['zetta']['default']) self.assertNotIn('default', users) ug_cfg = { 'user': 'zetta', 'users': ['bob', 'joe'] } (users, _groups) = self._norm(ug_cfg, distro) - self.assertNotIn('bob', users) + self.assertIn('bob', users) self.assertIn('joe', users) self.assertIn('zetta', users) + self.assertTrue(users['zetta']['default']) ug_cfg = { 'user': 'zetta', 'users': { @@ -204,6 +207,7 @@ class TestUGNormalize(MockerTestCase): self.assertIn('bob', users) self.assertIn('joe', users) self.assertIn('zetta', users) + self.assertTrue(users['zetta']['default']) ug_cfg = { 'user': 'zetta', } -- cgit v1.2.3 From 06ca24c39289f2d1f0f3f810abf155043a36d2f2 Mon Sep 17 00:00:00 2001 From: harlowja Date: Sat, 19 Jan 2013 17:51:24 -0800 Subject: Merge the old user style with the distro provided config. When the old user: style entry is found, don't forget that we need to use the distro settings that are provided but override the name with the new name, this is now accomplished by merging them together in the correct order (using the standard cloud-init merging algo). --- cloudinit/distros/__init__.py | 42 +++++++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 19 deletions(-) (limited to 'cloudinit/distros/__init__.py') diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py index c74be4e2..ddea8417 100644 --- a/cloudinit/distros/__init__.py +++ b/cloudinit/distros/__init__.py @@ -714,7 +714,7 @@ def normalize_users_groups(cfg, distro): # 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 = None + old_user = {} if 'user' in cfg and cfg['user']: old_user = cfg['user'] # Translate it into the format that is more useful @@ -724,28 +724,32 @@ def normalize_users_groups(cfg, distro): 'name': old_user, } if not isinstance(old_user, (dict)): - LOG.warn(("Format for 'user:' key must be a string or " + LOG.warn(("Format for 'user' key must be a string or " "dictionary and not %s"), util.obj_name(old_user)) - old_user = None - - default_user_config = None - if not 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. - 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.")) - else: - default_user_config = dict(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" + 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 = [] -- cgit v1.2.3