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

« back to all changes in this revision

Viewing changes to cloudinit/net/sysconfig.py

  • Committer: Scott Moser
  • Date: 2016-08-10 15:06:15 UTC
  • Revision ID: smoser@ubuntu.com-20160810150615-ma2fv107w3suy1ma
README: Mention move of revision control to git.

cloud-init development has moved its revision control to git.
It is available at 
  https://code.launchpad.net/cloud-init

Clone with 
  git clone https://git.launchpad.net/cloud-init
or
  git clone git+ssh://git.launchpad.net/cloud-init

For more information see
  https://git.launchpad.net/cloud-init/tree/HACKING.rst

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# vi: ts=4 expandtab
2
 
#
3
 
#    This program is free software: you can redistribute it and/or modify
4
 
#    it under the terms of the GNU General Public License version 3, as
5
 
#    published by the Free Software Foundation.
6
 
#
7
 
#    This program is distributed in the hope that it will be useful,
8
 
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
9
 
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
10
 
#    GNU General Public License for more details.
11
 
#
12
 
#    You should have received a copy of the GNU General Public License
13
 
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
14
 
 
15
 
import os
16
 
import re
17
 
 
18
 
import six
19
 
 
20
 
from cloudinit.distros.parsers import resolv_conf
21
 
from cloudinit import util
22
 
 
23
 
from . import renderer
24
 
 
25
 
 
26
 
def _make_header(sep='#'):
27
 
    lines = [
28
 
        "Created by cloud-init on instance boot automatically, do not edit.",
29
 
        "",
30
 
    ]
31
 
    for i in range(0, len(lines)):
32
 
        if lines[i]:
33
 
            lines[i] = sep + " " + lines[i]
34
 
        else:
35
 
            lines[i] = sep
36
 
    return "\n".join(lines)
37
 
 
38
 
 
39
 
def _is_default_route(route):
40
 
    if route['network'] == '::' and route['netmask'] == 0:
41
 
        return True
42
 
    if route['network'] == '0.0.0.0' and route['netmask'] == '0.0.0.0':
43
 
        return True
44
 
    return False
45
 
 
46
 
 
47
 
def _quote_value(value):
48
 
    if re.search(r"\s", value):
49
 
        # This doesn't handle complex cases...
50
 
        if value.startswith('"') and value.endswith('"'):
51
 
            return value
52
 
        else:
53
 
            return '"%s"' % value
54
 
    else:
55
 
        return value
56
 
 
57
 
 
58
 
class ConfigMap(object):
59
 
    """Sysconfig like dictionary object."""
60
 
 
61
 
    # Why does redhat prefer yes/no to true/false??
62
 
    _bool_map = {
63
 
        True: 'yes',
64
 
        False: 'no',
65
 
    }
66
 
 
67
 
    def __init__(self):
68
 
        self._conf = {}
69
 
 
70
 
    def __setitem__(self, key, value):
71
 
        self._conf[key] = value
72
 
 
73
 
    def drop(self, key):
74
 
        self._conf.pop(key, None)
75
 
 
76
 
    def __len__(self):
77
 
        return len(self._conf)
78
 
 
79
 
    def to_string(self):
80
 
        buf = six.StringIO()
81
 
        buf.write(_make_header())
82
 
        if self._conf:
83
 
            buf.write("\n")
84
 
        for key in sorted(self._conf.keys()):
85
 
            value = self._conf[key]
86
 
            if isinstance(value, bool):
87
 
                value = self._bool_map[value]
88
 
            if not isinstance(value, six.string_types):
89
 
                value = str(value)
90
 
            buf.write("%s=%s\n" % (key, _quote_value(value)))
91
 
        return buf.getvalue()
92
 
 
93
 
 
94
 
class Route(ConfigMap):
95
 
    """Represents a route configuration."""
96
 
 
97
 
    route_fn_tpl = '%(base)s/network-scripts/route-%(name)s'
98
 
 
99
 
    def __init__(self, route_name, base_sysconf_dir):
100
 
        super(Route, self).__init__()
101
 
        self.last_idx = 1
102
 
        self.has_set_default = False
103
 
        self._route_name = route_name
104
 
        self._base_sysconf_dir = base_sysconf_dir
105
 
 
106
 
    def copy(self):
107
 
        r = Route(self._route_name, self._base_sysconf_dir)
108
 
        r._conf = self._conf.copy()
109
 
        r.last_idx = self.last_idx
110
 
        r.has_set_default = self.has_set_default
111
 
        return r
112
 
 
113
 
    @property
114
 
    def path(self):
115
 
        return self.route_fn_tpl % ({'base': self._base_sysconf_dir,
116
 
                                     'name': self._route_name})
117
 
 
118
 
 
119
 
class NetInterface(ConfigMap):
120
 
    """Represents a sysconfig/networking-script (and its config + children)."""
121
 
 
122
 
    iface_fn_tpl = '%(base)s/network-scripts/ifcfg-%(name)s'
123
 
 
124
 
    iface_types = {
125
 
        'ethernet': 'Ethernet',
126
 
        'bond': 'Bond',
127
 
        'bridge': 'Bridge',
128
 
    }
129
 
 
130
 
    def __init__(self, iface_name, base_sysconf_dir, kind='ethernet'):
131
 
        super(NetInterface, self).__init__()
132
 
        self.children = []
133
 
        self.routes = Route(iface_name, base_sysconf_dir)
134
 
        self._kind = kind
135
 
        self._iface_name = iface_name
136
 
        self._conf['DEVICE'] = iface_name
137
 
        self._conf['TYPE'] = self.iface_types[kind]
138
 
        self._base_sysconf_dir = base_sysconf_dir
139
 
 
140
 
    @property
141
 
    def name(self):
142
 
        return self._iface_name
143
 
 
144
 
    @name.setter
145
 
    def name(self, iface_name):
146
 
        self._iface_name = iface_name
147
 
        self._conf['DEVICE'] = iface_name
148
 
 
149
 
    @property
150
 
    def kind(self):
151
 
        return self._kind
152
 
 
153
 
    @kind.setter
154
 
    def kind(self, kind):
155
 
        self._kind = kind
156
 
        self._conf['TYPE'] = self.iface_types[kind]
157
 
 
158
 
    @property
159
 
    def path(self):
160
 
        return self.iface_fn_tpl % ({'base': self._base_sysconf_dir,
161
 
                                     'name': self.name})
162
 
 
163
 
    def copy(self, copy_children=False, copy_routes=False):
164
 
        c = NetInterface(self.name, self._base_sysconf_dir, kind=self._kind)
165
 
        c._conf = self._conf.copy()
166
 
        if copy_children:
167
 
            c.children = list(self.children)
168
 
        if copy_routes:
169
 
            c.routes = self.routes.copy()
170
 
        return c
171
 
 
172
 
 
173
 
class Renderer(renderer.Renderer):
174
 
    """Renders network information in a /etc/sysconfig format."""
175
 
 
176
 
    # See: https://access.redhat.com/documentation/en-US/\
177
 
    #      Red_Hat_Enterprise_Linux/6/html/Deployment_Guide/\
178
 
    #      s1-networkscripts-interfaces.html (or other docs for
179
 
    #                                         details about this)
180
 
 
181
 
    iface_defaults = tuple([
182
 
        ('ONBOOT', True),
183
 
        ('USERCTL', False),
184
 
        ('NM_CONTROLLED', False),
185
 
        ('BOOTPROTO', 'none'),
186
 
    ])
187
 
 
188
 
    # If these keys exist, then there values will be used to form
189
 
    # a BONDING_OPTS grouping; otherwise no grouping will be set.
190
 
    bond_tpl_opts = tuple([
191
 
        ('bond_mode', "mode=%s"),
192
 
        ('bond_xmit_hash_policy', "xmit_hash_policy=%s"),
193
 
        ('bond_miimon', "miimon=%s"),
194
 
    ])
195
 
 
196
 
    bridge_opts_keys = tuple([
197
 
        ('bridge_stp', 'STP'),
198
 
        ('bridge_ageing', 'AGEING'),
199
 
        ('bridge_bridgeprio', 'PRIO'),
200
 
    ])
201
 
 
202
 
    def __init__(self, config=None):
203
 
        if not config:
204
 
            config = {}
205
 
        self.sysconf_dir = config.get('sysconf_dir', 'etc/sysconfig/')
206
 
        self.netrules_path = config.get(
207
 
            'netrules_path', 'etc/udev/rules.d/70-persistent-net.rules')
208
 
        self.dns_path = config.get('dns_path', 'etc/resolv.conf')
209
 
 
210
 
    @classmethod
211
 
    def _render_iface_shared(cls, iface, iface_cfg):
212
 
        for k, v in cls.iface_defaults:
213
 
            iface_cfg[k] = v
214
 
        for (old_key, new_key) in [('mac_address', 'HWADDR'), ('mtu', 'MTU')]:
215
 
            old_value = iface.get(old_key)
216
 
            if old_value is not None:
217
 
                iface_cfg[new_key] = old_value
218
 
 
219
 
    @classmethod
220
 
    def _render_subnet(cls, iface_cfg, route_cfg, subnet):
221
 
        subnet_type = subnet.get('type')
222
 
        if subnet_type == 'dhcp6':
223
 
            iface_cfg['DHCPV6C'] = True
224
 
            iface_cfg['IPV6INIT'] = True
225
 
            iface_cfg['BOOTPROTO'] = 'dhcp'
226
 
        elif subnet_type in ['dhcp4', 'dhcp']:
227
 
            iface_cfg['BOOTPROTO'] = 'dhcp'
228
 
        elif subnet_type == 'static':
229
 
            iface_cfg['BOOTPROTO'] = 'static'
230
 
            if subnet.get('ipv6'):
231
 
                iface_cfg['IPV6ADDR'] = subnet['address']
232
 
                iface_cfg['IPV6INIT'] = True
233
 
            else:
234
 
                iface_cfg['IPADDR'] = subnet['address']
235
 
        else:
236
 
            raise ValueError("Unknown subnet type '%s' found"
237
 
                             " for interface '%s'" % (subnet_type,
238
 
                                                      iface_cfg.name))
239
 
        if 'netmask' in subnet:
240
 
            iface_cfg['NETMASK'] = subnet['netmask']
241
 
        for route in subnet.get('routes', []):
242
 
            if _is_default_route(route):
243
 
                if route_cfg.has_set_default:
244
 
                    raise ValueError("Duplicate declaration of default"
245
 
                                     " route found for interface '%s'"
246
 
                                     % (iface_cfg.name))
247
 
                # NOTE(harlowja): ipv6 and ipv4 default gateways
248
 
                gw_key = 'GATEWAY0'
249
 
                nm_key = 'NETMASK0'
250
 
                addr_key = 'ADDRESS0'
251
 
                # The owning interface provides the default route.
252
 
                #
253
 
                # TODO(harlowja): add validation that no other iface has
254
 
                # also provided the default route?
255
 
                iface_cfg['DEFROUTE'] = True
256
 
                if 'gateway' in route:
257
 
                    iface_cfg['GATEWAY'] = route['gateway']
258
 
                route_cfg.has_set_default = True
259
 
            else:
260
 
                gw_key = 'GATEWAY%s' % route_cfg.last_idx
261
 
                nm_key = 'NETMASK%s' % route_cfg.last_idx
262
 
                addr_key = 'ADDRESS%s' % route_cfg.last_idx
263
 
                route_cfg.last_idx += 1
264
 
            for (old_key, new_key) in [('gateway', gw_key),
265
 
                                       ('netmask', nm_key),
266
 
                                       ('network', addr_key)]:
267
 
                if old_key in route:
268
 
                    route_cfg[new_key] = route[old_key]
269
 
 
270
 
    @classmethod
271
 
    def _render_bonding_opts(cls, iface_cfg, iface):
272
 
        bond_opts = []
273
 
        for (bond_key, value_tpl) in cls.bond_tpl_opts:
274
 
            # Seems like either dash or underscore is possible?
275
 
            bond_keys = [bond_key, bond_key.replace("_", "-")]
276
 
            for bond_key in bond_keys:
277
 
                if bond_key in iface:
278
 
                    bond_value = iface[bond_key]
279
 
                    if isinstance(bond_value, (tuple, list)):
280
 
                        bond_value = " ".join(bond_value)
281
 
                    bond_opts.append(value_tpl % (bond_value))
282
 
                    break
283
 
        if bond_opts:
284
 
            iface_cfg['BONDING_OPTS'] = " ".join(bond_opts)
285
 
 
286
 
    @classmethod
287
 
    def _render_physical_interfaces(cls, network_state, iface_contents):
288
 
        physical_filter = renderer.filter_by_physical
289
 
        for iface in network_state.iter_interfaces(physical_filter):
290
 
            iface_name = iface['name']
291
 
            iface_subnets = iface.get("subnets", [])
292
 
            iface_cfg = iface_contents[iface_name]
293
 
            route_cfg = iface_cfg.routes
294
 
            if len(iface_subnets) == 1:
295
 
                cls._render_subnet(iface_cfg, route_cfg, iface_subnets[0])
296
 
            elif len(iface_subnets) > 1:
297
 
                for i, iface_subnet in enumerate(iface_subnets,
298
 
                                                 start=len(iface.children)):
299
 
                    iface_sub_cfg = iface_cfg.copy()
300
 
                    iface_sub_cfg.name = "%s:%s" % (iface_name, i)
301
 
                    iface.children.append(iface_sub_cfg)
302
 
                    cls._render_subnet(iface_sub_cfg, route_cfg, iface_subnet)
303
 
 
304
 
    @classmethod
305
 
    def _render_bond_interfaces(cls, network_state, iface_contents):
306
 
        bond_filter = renderer.filter_by_type('bond')
307
 
        for iface in network_state.iter_interfaces(bond_filter):
308
 
            iface_name = iface['name']
309
 
            iface_cfg = iface_contents[iface_name]
310
 
            cls._render_bonding_opts(iface_cfg, iface)
311
 
            iface_master_name = iface['bond-master']
312
 
            iface_cfg['MASTER'] = iface_master_name
313
 
            iface_cfg['SLAVE'] = True
314
 
            # Ensure that the master interface (and any of its children)
315
 
            # are actually marked as being bond types...
316
 
            master_cfg = iface_contents[iface_master_name]
317
 
            master_cfgs = [master_cfg]
318
 
            master_cfgs.extend(master_cfg.children)
319
 
            for master_cfg in master_cfgs:
320
 
                master_cfg['BONDING_MASTER'] = True
321
 
                master_cfg.kind = 'bond'
322
 
 
323
 
    @staticmethod
324
 
    def _render_vlan_interfaces(network_state, iface_contents):
325
 
        vlan_filter = renderer.filter_by_type('vlan')
326
 
        for iface in network_state.iter_interfaces(vlan_filter):
327
 
            iface_name = iface['name']
328
 
            iface_cfg = iface_contents[iface_name]
329
 
            iface_cfg['VLAN'] = True
330
 
            iface_cfg['PHYSDEV'] = iface_name[:iface_name.rfind('.')]
331
 
 
332
 
    @staticmethod
333
 
    def _render_dns(network_state, existing_dns_path=None):
334
 
        content = resolv_conf.ResolvConf("")
335
 
        if existing_dns_path and os.path.isfile(existing_dns_path):
336
 
            content = resolv_conf.ResolvConf(util.load_file(existing_dns_path))
337
 
        for nameserver in network_state.dns_nameservers:
338
 
            content.add_nameserver(nameserver)
339
 
        for searchdomain in network_state.dns_searchdomains:
340
 
            content.add_search_domain(searchdomain)
341
 
        return "\n".join([_make_header(';'), str(content)])
342
 
 
343
 
    @classmethod
344
 
    def _render_bridge_interfaces(cls, network_state, iface_contents):
345
 
        bridge_filter = renderer.filter_by_type('bridge')
346
 
        for iface in network_state.iter_interfaces(bridge_filter):
347
 
            iface_name = iface['name']
348
 
            iface_cfg = iface_contents[iface_name]
349
 
            iface_cfg.kind = 'bridge'
350
 
            for old_key, new_key in cls.bridge_opts_keys:
351
 
                if old_key in iface:
352
 
                    iface_cfg[new_key] = iface[old_key]
353
 
            # Is this the right key to get all the connected interfaces?
354
 
            for bridged_iface_name in iface.get('bridge_ports', []):
355
 
                # Ensure all bridged interfaces are correctly tagged
356
 
                # as being bridged to this interface.
357
 
                bridged_cfg = iface_contents[bridged_iface_name]
358
 
                bridged_cfgs = [bridged_cfg]
359
 
                bridged_cfgs.extend(bridged_cfg.children)
360
 
                for bridge_cfg in bridged_cfgs:
361
 
                    bridge_cfg['BRIDGE'] = iface_name
362
 
 
363
 
    @classmethod
364
 
    def _render_sysconfig(cls, base_sysconf_dir, network_state):
365
 
        '''Given state, return /etc/sysconfig files + contents'''
366
 
        iface_contents = {}
367
 
        for iface in network_state.iter_interfaces():
368
 
            iface_name = iface['name']
369
 
            iface_cfg = NetInterface(iface_name, base_sysconf_dir)
370
 
            cls._render_iface_shared(iface, iface_cfg)
371
 
            iface_contents[iface_name] = iface_cfg
372
 
        cls._render_physical_interfaces(network_state, iface_contents)
373
 
        cls._render_bond_interfaces(network_state, iface_contents)
374
 
        cls._render_vlan_interfaces(network_state, iface_contents)
375
 
        cls._render_bridge_interfaces(network_state, iface_contents)
376
 
        contents = {}
377
 
        for iface_name, iface_cfg in iface_contents.items():
378
 
            if iface_cfg or iface_cfg.children:
379
 
                contents[iface_cfg.path] = iface_cfg.to_string()
380
 
                for iface_cfg in iface_cfg.children:
381
 
                    if iface_cfg:
382
 
                        contents[iface_cfg.path] = iface_cfg.to_string()
383
 
            if iface_cfg.routes:
384
 
                contents[iface_cfg.routes.path] = iface_cfg.routes.to_string()
385
 
        return contents
386
 
 
387
 
    def render_network_state(self, target, network_state):
388
 
        base_sysconf_dir = os.path.join(target, self.sysconf_dir)
389
 
        for path, data in self._render_sysconfig(base_sysconf_dir,
390
 
                                                 network_state).items():
391
 
            util.write_file(path, data)
392
 
        if self.dns_path:
393
 
            dns_path = os.path.join(target, self.dns_path)
394
 
            resolv_content = self._render_dns(network_state,
395
 
                                              existing_dns_path=dns_path)
396
 
            util.write_file(dns_path, resolv_content)
397
 
        if self.netrules_path:
398
 
            netrules_content = self._render_persistent_net(network_state)
399
 
            netrules_path = os.path.join(target, self.netrules_path)
400
 
            util.write_file(netrules_path, netrules_content)