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

« back to all changes in this revision

Viewing changes to cloudinit/distros/rhel.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:
23
23
import os
24
24
 
25
25
from cloudinit import distros
 
26
 
 
27
from cloudinit.distros.parsers.resolv_conf import ResolvConf
 
28
from cloudinit.distros.parsers.sys_conf import SysConf
 
29
 
26
30
from cloudinit import helpers
27
31
from cloudinit import log as logging
28
32
from cloudinit import util
29
 
from cloudinit import version
30
33
 
31
34
from cloudinit.settings import PER_INSTANCE
32
35
 
33
36
LOG = logging.getLogger(__name__)
34
37
 
35
 
NETWORK_FN_TPL = '/etc/sysconfig/network-scripts/ifcfg-%s'
36
 
 
37
 
# See: http://tiny.cc/6r99fw
38
 
# For what alot of these files that are being written
39
 
# are and the format of them
40
 
 
41
 
# This library is used to parse/write
42
 
# out the various sysconfig files edited
43
 
#
44
 
# It has to be slightly modified though
45
 
# to ensure that all values are quoted
46
 
# since these configs are usually sourced into
47
 
# bash scripts...
48
 
from configobj import ConfigObj
49
 
 
50
 
# See: http://tiny.cc/oezbgw
51
 
D_QUOTE_CHARS = {
52
 
    "\"": "\\\"",
53
 
    "(": "\\(",
54
 
    ")": "\\)",
55
 
    "$": '\$',
56
 
    '`': '\`',
57
 
}
58
 
 
59
38
 
60
39
def _make_sysconfig_bool(val):
61
40
    if val:
64
43
        return 'no'
65
44
 
66
45
 
67
 
def _make_header():
68
 
    ci_ver = version.version_string()
69
 
    return '# Created by cloud-init v. %s' % (ci_ver)
70
 
 
71
 
 
72
46
class Distro(distros.Distro):
 
47
    # See: http://tiny.cc/6r99fw
 
48
    clock_conf_fn = "/etc/sysconfig/clock"
 
49
    locale_conf_fn = '/etc/sysconfig/i18n'
 
50
    network_conf_fn = "/etc/sysconfig/network"
 
51
    hostname_conf_fn = "/etc/sysconfig/network"
 
52
    network_script_tpl = '/etc/sysconfig/network-scripts/ifcfg-%s'
 
53
    resolve_conf_fn = "/etc/resolv.conf"
 
54
    tz_local_fn = "/etc/localtime"
 
55
    tz_zone_dir = "/usr/share/zoneinfo"
73
56
 
74
57
    def __init__(self, name, cfg, paths):
75
58
        distros.Distro.__init__(self, name, cfg, paths)
81
64
    def install_packages(self, pkglist):
82
65
        self.package_command('install', pkglist)
83
66
 
84
 
    def _write_resolve(self, dns_servers, search_servers):
85
 
        contents = []
 
67
    def _adjust_resolve(self, dns_servers, search_servers):
 
68
        try:
 
69
            r_conf = ResolvConf(util.load_file(self.resolve_conf_fn))
 
70
            r_conf.parse()
 
71
        except IOError:
 
72
            util.logexc(LOG,
 
73
                        "Failed at parsing %s reverting to an empty instance",
 
74
                        self.resolve_conf_fn)
 
75
            r_conf = ResolvConf('')
 
76
            r_conf.parse()
86
77
        if dns_servers:
87
78
            for s in dns_servers:
88
 
                contents.append("nameserver %s" % (s))
 
79
                try:
 
80
                    r_conf.add_nameserver(s)
 
81
                except ValueError:
 
82
                    util.logexc(LOG, "Failed at adding nameserver %s", s)
89
83
        if search_servers:
90
 
            contents.append("search %s" % (" ".join(search_servers)))
91
 
        if contents:
92
 
            contents.insert(0, _make_header())
93
 
            util.write_file("/etc/resolv.conf", "\n".join(contents), 0644)
 
84
            for s in search_servers:
 
85
                try:
 
86
                    r_conf.add_search_domain(s)
 
87
                except ValueError:
 
88
                    util.logexc(LOG, "Failed at adding search domain %s", s)
 
89
        util.write_file(self.resolve_conf_fn, str(r_conf), 0644)
94
90
 
95
91
    def _write_network(self, settings):
96
92
        # TODO(harlowja) fix this... since this is the ubuntu format
102
98
        searchservers = []
103
99
        dev_names = entries.keys()
104
100
        for (dev, info) in entries.iteritems():
105
 
            net_fn = NETWORK_FN_TPL % (dev)
 
101
            net_fn = self.network_script_tpl % (dev)
106
102
            net_cfg = {
107
103
                'DEVICE': dev,
108
104
                'NETMASK': info.get('netmask'),
119
115
            if 'dns-search' in info:
120
116
                searchservers.extend(info['dns-search'])
121
117
        if nameservers or searchservers:
122
 
            self._write_resolve(nameservers, searchservers)
 
118
            self._adjust_resolve(nameservers, searchservers)
123
119
        if dev_names:
124
120
            net_cfg = {
125
121
                'NETWORKING': _make_sysconfig_bool(True),
126
122
            }
127
 
            self._update_sysconfig_file("/etc/sysconfig/network", net_cfg)
 
123
            self._update_sysconfig_file(self.network_conf_fn, net_cfg)
128
124
        return dev_names
129
125
 
130
126
    def _update_sysconfig_file(self, fn, adjustments, allow_empty=False):
141
137
            contents[k] = v
142
138
            updated_am += 1
143
139
        if updated_am:
144
 
            lines = contents.write()
 
140
            lines = [
 
141
                str(contents),
 
142
            ]
145
143
            if not exists:
146
 
                lines.insert(0, _make_header())
 
144
                lines.insert(0, util.make_header())
147
145
            util.write_file(fn, "\n".join(lines), 0644)
148
146
 
149
 
    def set_hostname(self, hostname, fqdn=None):
150
 
        # See: http://bit.ly/TwitgL
151
 
        # Should be fqdn if we can use it
152
 
        sysconfig_hostname = fqdn
153
 
        if not sysconfig_hostname:
154
 
            sysconfig_hostname = hostname
155
 
        self._write_hostname(sysconfig_hostname, '/etc/sysconfig/network')
156
 
        LOG.debug("Setting hostname to %s", hostname)
157
 
        util.subp(['hostname', hostname])
158
 
 
159
147
    def apply_locale(self, locale, out_fn=None):
160
148
        if not out_fn:
161
 
            out_fn = '/etc/sysconfig/i18n'
 
149
            out_fn = self.locale_conf_fn
162
150
        locale_cfg = {
163
151
            'LANG': locale,
164
152
        }
170
158
        }
171
159
        self._update_sysconfig_file(out_fn, host_cfg)
172
160
 
173
 
    def update_hostname(self, hostname, fqdn, prev_file):
 
161
    def _select_hostname(self, hostname, fqdn):
174
162
        # See: http://bit.ly/TwitgL
175
163
        # Should be fqdn if we can use it
176
 
        sysconfig_hostname = fqdn
177
 
        if not sysconfig_hostname:
178
 
            sysconfig_hostname = hostname
179
 
        hostname_prev = self._read_hostname(prev_file)
180
 
        hostname_in_sys = self._read_hostname("/etc/sysconfig/network")
181
 
        update_files = []
182
 
        if not hostname_prev or hostname_prev != sysconfig_hostname:
183
 
            update_files.append(prev_file)
184
 
        if (not hostname_in_sys or
185
 
            (hostname_in_sys == hostname_prev
186
 
             and hostname_in_sys != sysconfig_hostname)):
187
 
            update_files.append("/etc/sysconfig/network")
188
 
        for fn in update_files:
189
 
            try:
190
 
                self._write_hostname(sysconfig_hostname, fn)
191
 
            except:
192
 
                util.logexc(LOG, "Failed to write hostname %s to %s",
193
 
                            sysconfig_hostname, fn)
194
 
        if (hostname_in_sys and hostname_prev and
195
 
            hostname_in_sys != hostname_prev):
196
 
            LOG.debug(("%s differs from /etc/sysconfig/network."
197
 
                        " Assuming user maintained hostname."), prev_file)
198
 
        if "/etc/sysconfig/network" in update_files:
199
 
            LOG.debug("Setting hostname to %s", hostname)
200
 
            util.subp(['hostname', hostname])
 
164
        if fqdn:
 
165
            return fqdn
 
166
        return hostname
 
167
 
 
168
    def _read_system_hostname(self):
 
169
        return (self.network_conf_fn,
 
170
                self._read_hostname(self.network_conf_fn))
201
171
 
202
172
    def _read_hostname(self, filename, default=None):
203
173
        (_exists, contents) = self._read_conf(filename)
208
178
 
209
179
    def _read_conf(self, fn):
210
180
        exists = False
211
 
        if os.path.isfile(fn):
 
181
        try:
212
182
            contents = util.load_file(fn).splitlines()
213
183
            exists = True
214
 
        else:
 
184
        except IOError:
215
185
            contents = []
216
 
        return (exists, QuotingConfigObj(contents))
 
186
        return (exists,
 
187
                SysConf(contents))
217
188
 
218
189
    def _bring_up_interfaces(self, device_names):
219
190
        if device_names and 'all' in device_names:
222
193
        return distros.Distro._bring_up_interfaces(self, device_names)
223
194
 
224
195
    def set_timezone(self, tz):
225
 
        tz_file = os.path.join("/usr/share/zoneinfo", tz)
 
196
        # TODO(harlowja): move this code into
 
197
        # the parent distro...
 
198
        tz_file = os.path.join(self.tz_zone_dir, str(tz))
226
199
        if not os.path.isfile(tz_file):
227
200
            raise RuntimeError(("Invalid timezone %s,"
228
201
                                " no file found at %s") % (tz, tz_file))
229
202
        # Adjust the sysconfig clock zone setting
230
203
        clock_cfg = {
231
 
            'ZONE': tz,
 
204
            'ZONE': str(tz),
232
205
        }
233
 
        self._update_sysconfig_file("/etc/sysconfig/clock", clock_cfg)
 
206
        self._update_sysconfig_file(self.clock_conf_fn, clock_cfg)
234
207
        # This ensures that the correct tz will be used for the system
235
 
        util.copy(tz_file, "/etc/localtime")
 
208
        util.copy(tz_file, self.tz_local_fn)
236
209
 
237
210
    def package_command(self, command, args=None):
238
211
        cmd = ['yum']
256
229
                         ["makecache"], freq=PER_INSTANCE)
257
230
 
258
231
 
259
 
# This class helps adjust the configobj
260
 
# writing to ensure that when writing a k/v
261
 
# on a line, that they are properly quoted
262
 
# and have no spaces between the '=' sign.
263
 
# - This is mainly due to the fact that
264
 
# the sysconfig scripts are often sourced
265
 
# directly into bash/shell scripts so ensure
266
 
# that it works for those types of use cases.
267
 
class QuotingConfigObj(ConfigObj):
268
 
    def __init__(self, lines):
269
 
        ConfigObj.__init__(self, lines,
270
 
                           interpolation=False,
271
 
                           write_empty_values=True)
272
 
 
273
 
    def _quote_posix(self, text):
274
 
        if not text:
275
 
            return ''
276
 
        for (k, v) in D_QUOTE_CHARS.iteritems():
277
 
            text = text.replace(k, v)
278
 
        return '"%s"' % (text)
279
 
 
280
 
    def _quote_special(self, text):
281
 
        if text.lower() in ['yes', 'no', 'true', 'false']:
282
 
            return text
283
 
        else:
284
 
            return self._quote_posix(text)
285
 
 
286
 
    def _write_line(self, indent_string, entry, this_entry, comment):
287
 
        # Ensure it is formatted fine for
288
 
        # how these sysconfig scripts are used
289
 
        val = self._decode_element(self._quote(this_entry))
290
 
        # Single quoted strings should
291
 
        # always work.
292
 
        if not val.startswith("'"):
293
 
            # Perform any special quoting
294
 
            val = self._quote_special(val)
295
 
        key = self._decode_element(self._quote(entry, multiline=False))
296
 
        cmnt = self._decode_element(comment)
297
 
        return '%s%s%s%s%s' % (indent_string,
298
 
                               key,
299
 
                               "=",
300
 
                               val,
301
 
                               cmnt)
302
 
 
303
 
 
304
232
# This is a util function to translate a ubuntu /etc/network/interfaces 'blob'
305
233
# to a rhel equiv. that can then be written to /etc/sysconfig/network-scripts/
306
234
# TODO(harlowja) remove when we have python-netcf active...