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
This commit is contained in:
Joshua Harlow 2012-10-10 16:21:22 -07:00
parent f897623f57
commit 114525e386
11 changed files with 362 additions and 285 deletions

View File

@ -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):

View File

@ -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)
hostname_lines = [
str(hostname),
"",
]
util.write_file(out_fn, "\n".join(hostname_lines), 0644)
def update_hostname(self, hostname, prev_fn):
hostname_prev = self._read_hostname(prev_fn)
read_fn = self._paths.join(True, "/etc/hostname")
hostname_in_etc = self._read_hostname(read_fn)
update_files = []
if not hostname_prev or hostname_prev != hostname:
update_files.append(prev_fn)
if (not hostname_in_etc or
(hostname_in_etc == hostname_prev and
hostname_in_etc != hostname)):
write_fn = self._paths.join(False, "/etc/hostname")
update_files.append(write_fn)
for fn in update_files:
try:
self._write_hostname(hostname, fn)
except:
util.logexc(LOG, "Failed to write hostname %s to %s",
hostname, fn)
if (hostname_in_etc and hostname_prev and
hostname_in_etc != hostname_prev):
LOG.debug(("%s differs from /etc/hostname."
" Assuming user maintained hostname."), prev_fn)
if "/etc/hostname" in update_files:
# Only do this if we are running in non-adjusted root mode
LOG.debug("Setting hostname to %s", hostname)
util.subp(['hostname', hostname])
def _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()

View File

@ -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)

View File

@ -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()

View File

@ -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)

View File

@ -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

View File

@ -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...

View File

@ -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))

View File

@ -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']])

View File

@ -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)

View File

@ -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)