~cloud-init-dev/cloud-init/trunk

« back to all changes in this revision

Viewing changes to cloudinit/distros/__init__.py

  • Committer: Scott Moser
  • Author(s): Joshua Harlow
  • Date: 2012-11-13 13:54:35 UTC
  • mfrom: (725.1.6 system-conf-goodies)
  • Revision ID: smoser@ubuntu.com-20121113135435-ot6gbl56zlutf0ym
Use a set of helper/parsing classes to perform system configuration

Previously file modification of system configuration was done
in a functional and hard to test manner. Now instead this patch
allows for a manner that provides a nice object oriented
interface to those objects as well as makes it possible to test
those parsing entities without having to invoke distro class code.
   - Created parsers for:
    - /etc/sysconfig
    - /etc/hostname
    - resolv.conf
    - /etc/hosts

Moved duplicated functionality into the root level distro class including:
 - apply_hostname
 - set_hostname
 - *various shared configuration file names/paths*

Show diffs side-by-side

added added

removed removed

Lines of Context:
24
24
from StringIO import StringIO
25
25
 
26
26
import abc
 
27
import collections
27
28
import itertools
28
29
import os
29
30
import re
33
34
from cloudinit import ssh_util
34
35
from cloudinit import util
35
36
 
 
37
from cloudinit.distros.parsers import hosts
 
38
 
36
39
LOG = logging.getLogger(__name__)
37
40
 
38
41
 
39
42
class Distro(object):
40
43
    __metaclass__ = abc.ABCMeta
 
44
    hosts_fn = "/etc/hosts"
 
45
    ci_sudoers_fn = "/etc/sudoers.d/90-cloud-init-users"
 
46
    hostname_conf_fn = "/etc/hostname"
41
47
 
42
48
    def __init__(self, name, cfg, paths):
43
49
        self._paths = paths
57
63
    def get_option(self, opt_name, default=None):
58
64
        return self._cfg.get(opt_name, default)
59
65
 
60
 
    @abc.abstractmethod
61
66
    def set_hostname(self, hostname, fqdn=None):
62
 
        raise NotImplementedError()
63
 
 
64
 
    @abc.abstractmethod
65
 
    def update_hostname(self, hostname, fqdn, prev_hostname_fn):
66
 
        raise NotImplementedError()
 
67
        writeable_hostname = self._select_hostname(hostname, fqdn)
 
68
        self._write_hostname(writeable_hostname, self.hostname_conf_fn)
 
69
        self._apply_hostname(hostname)
67
70
 
68
71
    @abc.abstractmethod
69
72
    def package_command(self, cmd, args=None):
87
90
 
88
91
    def get_package_mirror_info(self, arch=None,
89
92
                                availability_zone=None):
90
 
        # this resolves the package_mirrors config option
 
93
        # This resolves the package_mirrors config option
91
94
        # down to a single dict of {mirror_name: mirror_url}
92
95
        arch_info = self._get_arch_package_mirror_info(arch)
93
96
        return _get_package_mirror_info(availability_zone=availability_zone,
112
115
    def _get_localhost_ip(self):
113
116
        return "127.0.0.1"
114
117
 
 
118
    @abc.abstractmethod
 
119
    def _read_hostname(self, filename, default=None):
 
120
        raise NotImplementedError()
 
121
 
 
122
    @abc.abstractmethod
 
123
    def _write_hostname(self, hostname, filename):
 
124
        raise NotImplementedError()
 
125
 
 
126
    @abc.abstractmethod
 
127
    def _read_system_hostname(self):
 
128
        raise NotImplementedError()
 
129
 
 
130
    def _apply_hostname(self, hostname):
 
131
        # This really only sets the hostname
 
132
        # temporarily (until reboot so it should
 
133
        # not be depended on). Use the write
 
134
        # hostname functions for 'permanent' adjustments.
 
135
        LOG.debug("Non-persistently setting the system hostname to %s",
 
136
                  hostname)
 
137
        try:
 
138
            util.subp(['hostname', hostname])
 
139
        except util.ProcessExecutionError:
 
140
            util.logexc(LOG, ("Failed to non-persistently adjust"
 
141
                              " the system hostname to %s"), hostname)
 
142
 
 
143
    @abc.abstractmethod
 
144
    def _select_hostname(self, hostname, fqdn):
 
145
        raise NotImplementedError()
 
146
 
 
147
    def update_hostname(self, hostname, fqdn, prev_hostname_fn):
 
148
        applying_hostname = hostname
 
149
        hostname = self._select_hostname(hostname, fqdn)
 
150
        prev_hostname = self._read_hostname(prev_hostname_fn)
 
151
        (sys_fn, sys_hostname) = self._read_system_hostname()
 
152
        update_files = []
 
153
        if not prev_hostname or prev_hostname != hostname:
 
154
            update_files.append(prev_hostname_fn)
 
155
 
 
156
        if (not sys_hostname) or (sys_hostname == prev_hostname
 
157
                                  and sys_hostname != hostname):
 
158
            update_files.append(sys_fn)
 
159
 
 
160
        update_files = set([f for f in update_files if f])
 
161
        LOG.debug("Attempting to update hostname to %s in %s files",
 
162
                  hostname, len(update_files))
 
163
 
 
164
        for fn in update_files:
 
165
            try:
 
166
                self._write_hostname(hostname, fn)
 
167
            except IOError:
 
168
                util.logexc(LOG, "Failed to write hostname %s to %s",
 
169
                            hostname, fn)
 
170
 
 
171
        if (sys_hostname and prev_hostname and
 
172
            sys_hostname != prev_hostname):
 
173
            LOG.debug("%s differs from %s, assuming user maintained hostname.",
 
174
                       prev_hostname_fn, sys_fn)
 
175
 
 
176
        if sys_fn in update_files:
 
177
            self._apply_hostname(applying_hostname)
 
178
 
115
179
    def update_etc_hosts(self, hostname, fqdn):
116
 
        # Format defined at
117
 
        # http://unixhelp.ed.ac.uk/CGI/man-cgi?hosts
118
 
        header = "# Added by cloud-init"
119
 
        real_header = "%s on %s" % (header, util.time_rfc2822())
 
180
        header = ''
 
181
        if os.path.exists(self.hosts_fn):
 
182
            eh = hosts.HostsConf(util.load_file(self.hosts_fn))
 
183
        else:
 
184
            eh = hosts.HostsConf('')
 
185
            header = util.make_header(base="added")
120
186
        local_ip = self._get_localhost_ip()
121
 
        hosts_line = "%s\t%s %s" % (local_ip, fqdn, hostname)
122
 
        new_etchosts = StringIO()
123
 
        need_write = False
124
 
        need_change = True
125
 
        for line in util.load_file("/etc/hosts").splitlines():
126
 
            if line.strip().startswith(header):
127
 
                continue
128
 
            if not line.strip() or line.strip().startswith("#"):
129
 
                new_etchosts.write("%s\n" % (line))
130
 
                continue
131
 
            split_line = [s.strip() for s in line.split()]
132
 
            if len(split_line) < 2:
133
 
                new_etchosts.write("%s\n" % (line))
134
 
                continue
135
 
            (ip, hosts) = split_line[0], split_line[1:]
136
 
            if ip == local_ip:
137
 
                if sorted([hostname, fqdn]) == sorted(hosts):
138
 
                    need_change = False
139
 
                if need_change:
140
 
                    line = "%s\n%s" % (real_header, hosts_line)
141
 
                    need_change = False
142
 
                    need_write = True
143
 
            new_etchosts.write("%s\n" % (line))
 
187
        prev_info = eh.get_entry(local_ip)
 
188
        need_change = False
 
189
        if not prev_info:
 
190
            eh.add_entry(local_ip, fqdn, hostname)
 
191
            need_change = True
 
192
        else:
 
193
            need_change = True
 
194
            for entry in prev_info:
 
195
                entry_fqdn = None
 
196
                entry_aliases = []
 
197
                if len(entry) >= 1:
 
198
                    entry_fqdn = entry[0]
 
199
                if len(entry) >= 2:
 
200
                    entry_aliases = entry[1:]
 
201
                if entry_fqdn is not None and entry_fqdn == fqdn:
 
202
                    if hostname in entry_aliases:
 
203
                        # Exists already, leave it be
 
204
                        need_change = False
 
205
            if need_change:
 
206
                # Doesn't exist, add that entry in...
 
207
                new_entries = list(prev_info)
 
208
                new_entries.append([fqdn, hostname])
 
209
                eh.del_entries(local_ip)
 
210
                for entry in new_entries:
 
211
                    if len(entry) == 1:
 
212
                        eh.add_entry(local_ip, entry[0])
 
213
                    elif len(entry) >= 2:
 
214
                        eh.add_entry(local_ip, *entry)
144
215
        if need_change:
145
 
            new_etchosts.write("%s\n%s\n" % (real_header, hosts_line))
146
 
            need_write = True
147
 
        if need_write:
148
 
            contents = new_etchosts.getvalue()
149
 
            util.write_file("/etc/hosts", contents, mode=0644)
 
216
            contents = StringIO()
 
217
            if header:
 
218
                contents.write("%s\n" % (header))
 
219
            contents.write("%s\n" % (eh))
 
220
            util.write_file(self.hosts_fn, contents.getvalue(), mode=0644)
150
221
 
151
222
    def _bring_up_interface(self, device_name):
152
223
        cmd = ['ifup', device_name]
305
376
                if not base_exists:
306
377
                    lines = [('# See sudoers(5) for more information'
307
378
                              ' on "#include" directives:'), '',
308
 
                             '# Added by cloud-init',
 
379
                             util.make_header(base="added"),
309
380
                             "#includedir %s" % (path), '']
310
381
                    sudoers_contents = "\n".join(lines)
311
382
                    util.write_file(sudo_base, sudoers_contents, 0440)
312
383
                else:
313
 
                    lines = ['', '# Added by cloud-init',
 
384
                    lines = ['', util.make_header(base="added"),
314
385
                             "#includedir %s" % (path), '']
315
386
                    sudoers_contents = "\n".join(lines)
316
387
                    util.append_file(sudo_base, sudoers_contents)
322
393
 
323
394
    def write_sudo_rules(self, user, rules, sudo_file=None):
324
395
        if not sudo_file:
325
 
            sudo_file = "/etc/sudoers.d/90-cloud-init-users"
326
 
 
327
 
        content_header = "# user rules for %s" % user
328
 
        content = "%s\n%s %s\n\n" % (content_header, user, rules)
329
 
 
330
 
        if isinstance(rules, list):
331
 
            content = "%s\n" % content_header
 
396
            sudo_file = self.ci_sudoers_fn
 
397
 
 
398
        lines = [
 
399
            '',
 
400
            "# User rules for %s" % user,
 
401
        ]
 
402
        if isinstance(rules, collections.Iterable):
332
403
            for rule in rules:
333
 
                content += "%s %s\n" % (user, rule)
334
 
            content += "\n"
 
404
                lines.append("%s %s" % (user, rule))
 
405
        else:
 
406
            lines.append("%s %s" % (user, rules))
 
407
        content = "\n".join(lines)
335
408
 
336
409
        self.ensure_sudo_dir(os.path.dirname(sudo_file))
337
 
 
338
410
        if not os.path.exists(sudo_file):
339
 
            util.write_file(sudo_file, content, 0440)
 
411
            contents = [
 
412
                util.make_header(),
 
413
                content,
 
414
            ]
 
415
            try:
 
416
                util.write_file(sudo_file, "\n".join(contents), 0440)
 
417
            except IOError as e:
 
418
                util.logexc(LOG, "Failed to write sudoers file %s", sudo_file)
 
419
                raise e
340
420
        else:
341
421
            try:
342
422
                util.append_file(sudo_file, content)
343
423
            except IOError as e:
344
 
                util.logexc(LOG, "Failed to write %s" % sudo_file, e)
 
424
                util.logexc(LOG, "Failed to append sudoers file %s", sudo_file)
345
425
                raise e
346
426
 
347
427
    def create_group(self, name, members):