summaryrefslogtreecommitdiff
path: root/cloudinit/distros/parsers
diff options
context:
space:
mode:
Diffstat (limited to 'cloudinit/distros/parsers')
-rw-r--r--cloudinit/distros/parsers/__init__.py28
-rw-r--r--cloudinit/distros/parsers/hostname.py88
-rw-r--r--cloudinit/distros/parsers/hosts.py92
-rw-r--r--cloudinit/distros/parsers/resolv_conf.py169
-rw-r--r--cloudinit/distros/parsers/sys_conf.py113
5 files changed, 490 insertions, 0 deletions
diff --git a/cloudinit/distros/parsers/__init__.py b/cloudinit/distros/parsers/__init__.py
new file mode 100644
index 00000000..1c413eaa
--- /dev/null
+++ b/cloudinit/distros/parsers/__init__.py
@@ -0,0 +1,28 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2012 Yahoo! Inc.
+#
+# Author: Joshua Harlow <harlowja@yahoo-inc.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3, as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+def chop_comment(text, comment_chars):
+ comment_locations = [text.find(c) for c in comment_chars]
+ comment_locations = [c for c in comment_locations if c != -1]
+ if not comment_locations:
+ return (text, '')
+ min_comment = min(comment_locations)
+ before_comment = text[0:min_comment]
+ comment = text[min_comment:]
+ return (before_comment, comment)
diff --git a/cloudinit/distros/parsers/hostname.py b/cloudinit/distros/parsers/hostname.py
new file mode 100644
index 00000000..617b3c36
--- /dev/null
+++ b/cloudinit/distros/parsers/hostname.py
@@ -0,0 +1,88 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2012 Yahoo! Inc.
+#
+# Author: Joshua Harlow <harlowja@yahoo-inc.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3, as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from StringIO import StringIO
+
+from cloudinit.distros.parsers import chop_comment
+
+
+# Parser that knows how to work with /etc/hostname format
+class HostnameConf(object):
+ def __init__(self, text):
+ self._text = text
+ self._contents = None
+
+ def parse(self):
+ if self._contents is None:
+ self._contents = self._parse(self._text)
+
+ def __str__(self):
+ self.parse()
+ contents = StringIO()
+ for (line_type, components) in self._contents:
+ if line_type == 'blank':
+ contents.write("%s\n" % (components[0]))
+ elif line_type == 'all_comment':
+ contents.write("%s\n" % (components[0]))
+ elif line_type == 'hostname':
+ (hostname, tail) = components
+ contents.write("%s%s\n" % (hostname, tail))
+ # Ensure trailing newline
+ contents = contents.getvalue()
+ if not contents.endswith("\n"):
+ contents += "\n"
+ return contents
+
+ @property
+ def hostname(self):
+ self.parse()
+ for (line_type, components) in self._contents:
+ if line_type == 'hostname':
+ return components[0]
+ return None
+
+ def set_hostname(self, your_hostname):
+ your_hostname = your_hostname.strip()
+ if not your_hostname:
+ return
+ self.parse()
+ replaced = False
+ for (line_type, components) in self._contents:
+ if line_type == 'hostname':
+ components[0] = str(your_hostname)
+ replaced = True
+ if not replaced:
+ self._contents.append(('hostname', [str(your_hostname), '']))
+
+ def _parse(self, contents):
+ entries = []
+ hostnames_found = set()
+ for line in contents.splitlines():
+ if not len(line.strip()):
+ entries.append(('blank', [line]))
+ continue
+ (head, tail) = chop_comment(line.strip(), '#')
+ if not len(head):
+ entries.append(('all_comment', [line]))
+ continue
+ entries.append(('hostname', [head, tail]))
+ hostnames_found.add(head)
+ if len(hostnames_found) > 1:
+ raise IOError("Multiple hostnames (%s) found!"
+ % (hostnames_found))
+ return entries
diff --git a/cloudinit/distros/parsers/hosts.py b/cloudinit/distros/parsers/hosts.py
new file mode 100644
index 00000000..94c97051
--- /dev/null
+++ b/cloudinit/distros/parsers/hosts.py
@@ -0,0 +1,92 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2012 Yahoo! Inc.
+#
+# Author: Joshua Harlow <harlowja@yahoo-inc.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3, as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from StringIO import StringIO
+
+from cloudinit.distros.parsers import chop_comment
+
+
+# See: man hosts
+# or http://unixhelp.ed.ac.uk/CGI/man-cgi?hosts
+# or http://tinyurl.com/6lmox3
+class HostsConf(object):
+ def __init__(self, text):
+ self._text = text
+ self._contents = None
+
+ def parse(self):
+ if self._contents is None:
+ self._contents = self._parse(self._text)
+
+ def get_entry(self, ip):
+ self.parse()
+ options = []
+ for (line_type, components) in self._contents:
+ if line_type == 'option':
+ (pieces, _tail) = components
+ if len(pieces) and pieces[0] == ip:
+ options.append(pieces[1:])
+ return options
+
+ def del_entries(self, ip):
+ self.parse()
+ n_entries = []
+ for (line_type, components) in self._contents:
+ if line_type != 'option':
+ n_entries.append((line_type, components))
+ continue
+ else:
+ (pieces, _tail) = components
+ if len(pieces) and pieces[0] == ip:
+ pass
+ elif len(pieces):
+ n_entries.append((line_type, list(components)))
+ self._contents = n_entries
+
+ def add_entry(self, ip, canonical_hostname, *aliases):
+ self.parse()
+ self._contents.append(('option',
+ ([ip, canonical_hostname] + list(aliases), '')))
+
+ def _parse(self, contents):
+ entries = []
+ for line in contents.splitlines():
+ if not len(line.strip()):
+ entries.append(('blank', [line]))
+ continue
+ (head, tail) = chop_comment(line.strip(), '#')
+ if not len(head):
+ entries.append(('all_comment', [line]))
+ continue
+ entries.append(('option', [head.split(None), tail]))
+ return entries
+
+ def __str__(self):
+ self.parse()
+ contents = StringIO()
+ for (line_type, components) in self._contents:
+ if line_type == 'blank':
+ contents.write("%s\n" % (components[0]))
+ elif line_type == 'all_comment':
+ contents.write("%s\n" % (components[0]))
+ elif line_type == 'option':
+ (pieces, tail) = components
+ pieces = [str(p) for p in pieces]
+ pieces = "\t".join(pieces)
+ contents.write("%s%s\n" % (pieces, tail))
+ return contents.getvalue()
diff --git a/cloudinit/distros/parsers/resolv_conf.py b/cloudinit/distros/parsers/resolv_conf.py
new file mode 100644
index 00000000..5733c25a
--- /dev/null
+++ b/cloudinit/distros/parsers/resolv_conf.py
@@ -0,0 +1,169 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2012 Yahoo! Inc.
+#
+# Author: Joshua Harlow <harlowja@yahoo-inc.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3, as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from StringIO import StringIO
+
+from cloudinit import util
+
+from cloudinit.distros.parsers import chop_comment
+
+
+# See: man resolv.conf
+class ResolvConf(object):
+ def __init__(self, text):
+ self._text = text
+ self._contents = None
+
+ def parse(self):
+ if self._contents is None:
+ self._contents = self._parse(self._text)
+
+ @property
+ def nameservers(self):
+ self.parse()
+ return self._retr_option('nameserver')
+
+ @property
+ def local_domain(self):
+ self.parse()
+ dm = self._retr_option('domain')
+ if dm:
+ return dm[0]
+ return None
+
+ @property
+ def search_domains(self):
+ self.parse()
+ current_sds = self._retr_option('search')
+ flat_sds = []
+ for sdlist in current_sds:
+ for sd in sdlist.split(None):
+ if sd:
+ flat_sds.append(sd)
+ return flat_sds
+
+ def __str__(self):
+ self.parse()
+ contents = StringIO()
+ for (line_type, components) in self._contents:
+ if line_type == 'blank':
+ contents.write("\n")
+ elif line_type == 'all_comment':
+ contents.write("%s\n" % (components[0]))
+ elif line_type == 'option':
+ (cfg_opt, cfg_value, comment_tail) = components
+ line = "%s %s" % (cfg_opt, cfg_value)
+ if len(comment_tail):
+ line += comment_tail
+ contents.write("%s\n" % (line))
+ return contents.getvalue()
+
+ def _retr_option(self, opt_name):
+ found = []
+ for (line_type, components) in self._contents:
+ if line_type == 'option':
+ (cfg_opt, cfg_value, _comment_tail) = components
+ if cfg_opt == opt_name:
+ found.append(cfg_value)
+ return found
+
+ def add_nameserver(self, ns):
+ self.parse()
+ current_ns = self._retr_option('nameserver')
+ new_ns = list(current_ns)
+ new_ns.append(str(ns))
+ new_ns = util.uniq_list(new_ns)
+ if len(new_ns) == len(current_ns):
+ return current_ns
+ if len(current_ns) >= 3:
+ # Hard restriction on only 3 name servers
+ raise ValueError(("Adding %r would go beyond the "
+ "'3' maximum name servers") % (ns))
+ self._remove_option('nameserver')
+ for n in new_ns:
+ self._contents.append(('option', ['nameserver', n, '']))
+ return new_ns
+
+ def _remove_option(self, opt_name):
+
+ def remove_opt(item):
+ line_type, components = item
+ if line_type != 'option':
+ return False
+ (cfg_opt, _cfg_value, _comment_tail) = components
+ if cfg_opt != opt_name:
+ return False
+ return True
+
+ new_contents = []
+ for c in self._contents:
+ if not remove_opt(c):
+ new_contents.append(c)
+ self._contents = new_contents
+
+ def add_search_domain(self, search_domain):
+ flat_sds = self.search_domains
+ new_sds = list(flat_sds)
+ new_sds.append(str(search_domain))
+ new_sds = util.uniq_list(new_sds)
+ if len(flat_sds) == len(new_sds):
+ return new_sds
+ if len(flat_sds) >= 6:
+ # Hard restriction on only 6 search domains
+ raise ValueError(("Adding %r would go beyond the "
+ "'6' maximum search domains") % (search_domain))
+ s_list = " ".join(new_sds)
+ if len(s_list) > 256:
+ # Some hard limit on 256 chars total
+ raise ValueError(("Adding %r would go beyond the "
+ "256 maximum search list character limit")
+ % (search_domain))
+ self._remove_option('search')
+ self._contents.append(('option', ['search', s_list, '']))
+ return flat_sds
+
+ @local_domain.setter
+ def local_domain(self, domain):
+ self.parse()
+ self._remove_option('domain')
+ self._contents.append(('option', ['domain', str(domain), '']))
+ return domain
+
+ def _parse(self, contents):
+ entries = []
+ for (i, line) in enumerate(contents.splitlines()):
+ sline = line.strip()
+ if not sline:
+ entries.append(('blank', [line]))
+ continue
+ (head, tail) = chop_comment(line, ';#')
+ if not len(head.strip()):
+ entries.append(('all_comment', [line]))
+ continue
+ if not tail:
+ tail = ''
+ try:
+ (cfg_opt, cfg_values) = head.split(None, 1)
+ except (IndexError, ValueError):
+ raise IOError("Incorrectly formatted resolv.conf line %s"
+ % (i + 1))
+ if cfg_opt not in ['nameserver', 'domain',
+ 'search', 'sortlist', 'options']:
+ raise IOError("Unexpected resolv.conf option %s" % (cfg_opt))
+ entries.append(("option", [cfg_opt, cfg_values, tail]))
+ return entries
diff --git a/cloudinit/distros/parsers/sys_conf.py b/cloudinit/distros/parsers/sys_conf.py
new file mode 100644
index 00000000..20ca1871
--- /dev/null
+++ b/cloudinit/distros/parsers/sys_conf.py
@@ -0,0 +1,113 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2012 Yahoo! Inc.
+#
+# Author: Joshua Harlow <harlowja@yahoo-inc.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3, as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from StringIO import StringIO
+
+import pipes
+import re
+
+# This library is used to parse/write
+# out the various sysconfig files edited (best attempt effort)
+#
+# It has to be slightly modified though
+# to ensure that all values are quoted/unquoted correctly
+# since these configs are usually sourced into
+# bash scripts...
+import configobj
+
+# See: http://pubs.opengroup.org/onlinepubs/000095399/basedefs/xbd_chap08.html
+# or look at the 'param_expand()' function in the subst.c file in the bash
+# source tarball...
+SHELL_VAR_RULE = r'[a-zA-Z_]+[a-zA-Z0-9_]*'
+SHELL_VAR_REGEXES = [
+ # Basic variables
+ re.compile(r"\$" + SHELL_VAR_RULE),
+ # Things like $?, $0, $-, $@
+ re.compile(r"\$[0-9#\?\-@\*]"),
+ # Things like ${blah:1} - but this one
+ # gets very complex so just try the
+ # simple path
+ re.compile(r"\$\{.+\}"),
+]
+
+
+def _contains_shell_variable(text):
+ for r in SHELL_VAR_REGEXES:
+ if r.search(text):
+ return True
+ return False
+
+
+class SysConf(configobj.ConfigObj):
+ def __init__(self, contents):
+ configobj.ConfigObj.__init__(self, contents,
+ interpolation=False,
+ write_empty_values=True)
+
+ def __str__(self):
+ contents = self.write()
+ out_contents = StringIO()
+ if isinstance(contents, (list, tuple)):
+ out_contents.write("\n".join(contents))
+ else:
+ out_contents.write(str(contents))
+ return out_contents.getvalue()
+
+ def _quote(self, value, multiline=False):
+ if not isinstance(value, (str, basestring)):
+ raise ValueError('Value "%s" is not a string' % (value))
+ if len(value) == 0:
+ return ''
+ quot_func = None
+ if value[0] in ['"', "'"] and value[-1] in ['"', "'"]:
+ if len(value) == 1:
+ quot_func = (lambda x:
+ self._get_single_quote(x) % x)
+ else:
+ # Quote whitespace if it isn't the start + end of a shell command
+ if value.strip().startswith("$(") and value.strip().endswith(")"):
+ pass
+ else:
+ if re.search(r"[\t\r\n ]", value):
+ if _contains_shell_variable(value):
+ # If it contains shell variables then we likely want to
+ # leave it alone since the pipes.quote function likes
+ # to use single quotes which won't get expanded...
+ if re.search(r"[\n\"']", value):
+ quot_func = (lambda x:
+ self._get_triple_quote(x) % x)
+ else:
+ quot_func = (lambda x:
+ self._get_single_quote(x) % x)
+ else:
+ quot_func = pipes.quote
+ if not quot_func:
+ return value
+ return quot_func(value)
+
+ def _write_line(self, indent_string, entry, this_entry, comment):
+ # Ensure it is formatted fine for
+ # how these sysconfig scripts are used
+ val = self._decode_element(self._quote(this_entry))
+ key = self._decode_element(self._quote(entry))
+ cmnt = self._decode_element(comment)
+ return '%s%s%s%s%s' % (indent_string,
+ key,
+ self._a_to_u('='),
+ val,
+ cmnt)