From 4b910d7bca47cb4fd26394fb4d14ebc72b1a73f8 Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Thu, 20 Sep 2012 14:02:34 -0700 Subject: Add a resolv.conf parser that can be easily used for adjusting a resolv.conf formatted file and use this to adjust the resolv.conf in the redhat distro instead of replacing the previous resolv.conf completely. --- cloudinit/distros/helpers.py | 179 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 cloudinit/distros/helpers.py (limited to 'cloudinit/distros/helpers.py') diff --git a/cloudinit/distros/helpers.py b/cloudinit/distros/helpers.py new file mode 100644 index 00000000..e1db74dc --- /dev/null +++ b/cloudinit/distros/helpers.py @@ -0,0 +1,179 @@ +# 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 + + +# 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 True + (cfg_opt, cfg_value, comment_tail) = components + if cfg_opt != opt_name: + return True + return False + + new_contents = filter(remove_opt, self._contents) + 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 + 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:] + 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 + + -- cgit v1.2.3 From d27690ebe160bf3eae8dc92f7364daef5cf56208 Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Thu, 20 Sep 2012 14:09:57 -0700 Subject: Pylint cleanups. --- cloudinit/distros/helpers.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) (limited to 'cloudinit/distros/helpers.py') diff --git a/cloudinit/distros/helpers.py b/cloudinit/distros/helpers.py index e1db74dc..251d5051 100644 --- a/cloudinit/distros/helpers.py +++ b/cloudinit/distros/helpers.py @@ -77,7 +77,7 @@ class ResolvConf(object): found = [] for (line_type, components) in self._contents: if line_type == 'option': - (cfg_opt, cfg_value, comment_tail) = components + (cfg_opt, cfg_value, _comment_tail) = components if cfg_opt == opt_name: found.append(cfg_value) return found @@ -104,13 +104,16 @@ class ResolvConf(object): def remove_opt(item): line_type, components = item if line_type != 'option': - return True - (cfg_opt, cfg_value, comment_tail) = components + return False + (cfg_opt, _cfg_value, _comment_tail) = components if cfg_opt != opt_name: - return True - return False + return False + return True - new_contents = filter(remove_opt, self._contents) + 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): @@ -170,8 +173,10 @@ class ResolvConf(object): 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("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 -- 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/helpers.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