summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cloudinit/distros/__init__.py89
-rw-r--r--cloudinit/distros/debian.py88
-rw-r--r--cloudinit/distros/parsers/__init__.py27
-rw-r--r--cloudinit/distros/parsers/hosts.py92
-rw-r--r--cloudinit/distros/parsers/quoting_conf.py80
-rw-r--r--cloudinit/distros/parsers/resolv_conf.py (renamed from cloudinit/distros/helpers.py)85
-rw-r--r--cloudinit/distros/rhel.py149
-rw-r--r--cloudinit/util.py15
-rw-r--r--tests/unittests/test_distros/test_hosts.py8
-rw-r--r--tests/unittests/test_distros/test_netconfig.py2
-rw-r--r--tests/unittests/test_distros/test_resolv.py14
11 files changed, 363 insertions, 286 deletions
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
@@ -67,10 +69,6 @@ class Distro(object):
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/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 <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/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 <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
+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 <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/>.
+
+# 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/helpers.py b/cloudinit/distros/parsers/resolv_conf.py
index 916935eb..377ada6b 100644
--- a/cloudinit/distros/helpers.py
+++ b/cloudinit/distros/parsers/resolv_conf.py
@@ -1,9 +1,7 @@
# vi: ts=4 expandtab
#
-# Copyright (C) 2012 Canonical Ltd.
# Copyright (C) 2012 Yahoo! Inc.
#
-# Author: Scott Moser <scott.moser@canonical.com>
# Author: Joshua Harlow <harlowja@yahoo-inc.com>
#
# This program is free software: you can redistribute it and/or modify
@@ -22,86 +20,7 @@ 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()
+from cloudinit.distros.parsers import chop_comment
# See: man resolv.conf
@@ -232,7 +151,7 @@ class ResolvConf(object):
if not sline:
entries.append(('blank', [line]))
continue
- (head, tail) = _chop_comment(line, ';#')
+ (head, tail) = chop_comment(line, ';#')
if not len(head.strip()):
entries.append(('all_comment', [line]))
continue
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)