~harlowja/cloud-init/cloud-init-tag-distros

« back to all changes in this revision

Viewing changes to cloudinit/sources/DataSourceConfigDrive.py

improve network configuration

This branch accomplishes several things:
 - centrally handle 'dsmode' to be 'local' or 'net.
   This allows local data sources to run before networking
   but still have user-data read by default when networking is available.

 - support networking information being read on dreamcompute
   dreamcompute's openstack declares networking via the
   /etc/network/interfaces style 'network_config' format.
 
 - support reading and applying networking information on SmartOS

 - improve reading networking from openstack network_data.json (LP: #1577982)
   add support for mtu and routes and many miscellaneous fixes.

 - support for renaming devices in a container (LP: #1579130).
   Also rename network devices as instructed by the host on
   every boot where cloud-init networking is enabled.  This is required
   because a.) containers do not get systemd.link files applied
   as they do not have udev. b.) if the initramfs is out of date
   then we need to apply them.

 - remove blocking of udev rules (LP: #1577844, LP: #1571761)

Show diffs side-by-side

added added

removed removed

Lines of Context:
22
22
import os
23
23
 
24
24
from cloudinit import log as logging
 
25
from cloudinit import net
25
26
from cloudinit import sources
26
27
from cloudinit import util
27
28
 
35
36
DEFAULT_METADATA = {
36
37
    "instance-id": DEFAULT_IID,
37
38
}
38
 
VALID_DSMODES = ("local", "net", "pass", "disabled")
39
39
FS_TYPES = ('vfat', 'iso9660')
40
40
LABEL_TYPES = ('config-2',)
41
41
POSSIBLE_MOUNTS = ('sr', 'cd')
47
47
    def __init__(self, sys_cfg, distro, paths):
48
48
        super(DataSourceConfigDrive, self).__init__(sys_cfg, distro, paths)
49
49
        self.source = None
50
 
        self.dsmode = 'local'
51
50
        self.seed_dir = os.path.join(paths.seed_dir, 'config_drive')
52
51
        self.version = None
53
52
        self.ec2_metadata = None
54
53
        self._network_config = None
55
54
        self.network_json = None
 
55
        self.network_eni = None
56
56
        self.files = {}
57
57
 
58
58
    def __str__(self):
98
98
 
99
99
        md = results.get('metadata', {})
100
100
        md = util.mergemanydict([md, DEFAULT_METADATA])
101
 
        user_dsmode = results.get('dsmode', None)
102
 
        if user_dsmode not in VALID_DSMODES + (None,):
103
 
            LOG.warn("User specified invalid mode: %s", user_dsmode)
104
 
            user_dsmode = None
105
 
 
106
 
        dsmode = get_ds_mode(cfgdrv_ver=results['version'],
107
 
                             ds_cfg=self.ds_cfg.get('dsmode'),
108
 
                             user=user_dsmode)
109
 
 
110
 
        if dsmode == "disabled":
111
 
            # most likely user specified
 
101
 
 
102
        self.dsmode = self._determine_dsmode(
 
103
            [results.get('dsmode'), self.ds_cfg.get('dsmode'),
 
104
             sources.DSMODE_PASS if results['version'] == 1 else None])
 
105
 
 
106
        if self.dsmode == sources.DSMODE_DISABLED:
112
107
            return False
113
108
 
114
 
        # TODO(smoser): fix this, its dirty.
115
 
        # we want to do some things (writing files and network config)
116
 
        # only on first boot, and even then, we want to do so in the
117
 
        # local datasource (so they happen earlier) even if the configured
118
 
        # dsmode is 'net' or 'pass'. To do this, we check the previous
119
 
        # instance-id
 
109
        # This is legacy and sneaky.  If dsmode is 'pass' then write
 
110
        # 'injected files' and apply legacy ENI network format.
120
111
        prev_iid = get_previous_iid(self.paths)
121
112
        cur_iid = md['instance-id']
122
 
        if prev_iid != cur_iid and self.dsmode == "local":
 
113
        if prev_iid != cur_iid and self.dsmode == sources.DSMODE_PASS:
123
114
            on_first_boot(results, distro=self.distro)
124
 
 
125
 
        # dsmode != self.dsmode here if:
126
 
        #  * dsmode = "pass",  pass means it should only copy files and then
127
 
        #    pass to another datasource
128
 
        #  * dsmode = "net" and self.dsmode = "local"
129
 
        #    so that user boothooks would be applied with network, the
130
 
        #    local datasource just gets out of the way, and lets the net claim
131
 
        if dsmode != self.dsmode:
132
 
            LOG.debug("%s: not claiming datasource, dsmode=%s", self, dsmode)
 
115
            LOG.debug("%s: not claiming datasource, dsmode=%s", self,
 
116
                      self.dsmode)
133
117
            return False
134
118
 
135
119
        self.source = found
147
131
            LOG.warn("Invalid content in vendor-data: %s", e)
148
132
            self.vendordata_raw = None
149
133
 
150
 
        try:
151
 
            self.network_json = results.get('networkdata')
152
 
        except ValueError as e:
153
 
            LOG.warn("Invalid content in network-data: %s", e)
154
 
            self.network_json = None
155
 
 
 
134
        # network_config is an /etc/network/interfaces formated file and is
 
135
        # obsolete compared to networkdata (from network_data.json) but both
 
136
        # might be present.
 
137
        self.network_eni = results.get("network_config")
 
138
        self.network_json = results.get('networkdata')
156
139
        return True
157
140
 
158
141
    def check_instance_id(self, sys_cfg):
163
146
    def network_config(self):
164
147
        if self._network_config is None:
165
148
            if self.network_json is not None:
 
149
                LOG.debug("network config provided via network_json")
166
150
                self._network_config = convert_network_data(self.network_json)
 
151
            elif self.network_eni is not None:
 
152
                self._network_config = net.convert_eni_data(self.network_eni)
 
153
                LOG.debug("network config provided via converted eni data")
 
154
            else:
 
155
                LOG.debug("no network configuration available")
167
156
        return self._network_config
168
157
 
169
158
 
170
 
class DataSourceConfigDriveNet(DataSourceConfigDrive):
171
 
    def __init__(self, sys_cfg, distro, paths):
172
 
        DataSourceConfigDrive.__init__(self, sys_cfg, distro, paths)
173
 
        self.dsmode = 'net'
174
 
 
175
 
 
176
 
def get_ds_mode(cfgdrv_ver, ds_cfg=None, user=None):
177
 
    """Determine what mode should be used.
178
 
    valid values are 'pass', 'disabled', 'local', 'net'
179
 
    """
180
 
    # user passed data trumps everything
181
 
    if user is not None:
182
 
        return user
183
 
 
184
 
    if ds_cfg is not None:
185
 
        return ds_cfg
186
 
 
187
 
    # at config-drive version 1, the default behavior was pass.  That
188
 
    # meant to not use use it as primary data source, but expect a ec2 metadata
189
 
    # source. for version 2, we default to 'net', which means
190
 
    # the DataSourceConfigDriveNet, would be used.
191
 
    #
192
 
    # this could change in the future.  If there was definitive metadata
193
 
    # that indicated presense of an openstack metadata service, then
194
 
    # we could change to 'pass' by default also. The motivation for that
195
 
    # would be 'cloud-init query' as the web service could be more dynamic
196
 
    if cfgdrv_ver == 1:
197
 
        return "pass"
198
 
    return "net"
199
 
 
200
 
 
201
159
def read_config_drive(source_dir):
202
160
    reader = openstack.ConfigDriveReader(source_dir)
203
161
    finders = [
231
189
                        % (type(data)))
232
190
    net_conf = data.get("network_config", '')
233
191
    if net_conf and distro:
234
 
        LOG.debug("Updating network interfaces from config drive")
 
192
        LOG.warn("Updating network interfaces from config drive")
235
193
        distro.apply_network(net_conf)
236
 
    files = data.get('files', {})
 
194
    write_injected_files(data.get('files'))
 
195
 
 
196
 
 
197
def write_injected_files(files):
237
198
    if files:
238
199
        LOG.debug("Writing %s injected files", len(files))
239
200
        for (filename, content) in files.items():
293
254
    return devices
294
255
 
295
256
 
296
 
# Used to match classes to dependencies
297
 
datasources = [
298
 
    (DataSourceConfigDrive, (sources.DEP_FILESYSTEM, )),
299
 
    (DataSourceConfigDriveNet, (sources.DEP_FILESYSTEM, sources.DEP_NETWORK)),
300
 
]
301
 
 
302
 
 
303
 
# Return a list of data sources that match this set of dependencies
304
 
def get_datasource_list(depends):
305
 
    return sources.list_from_depends(depends, datasources)
306
 
 
307
 
 
308
257
# Convert OpenStack ConfigDrive NetworkData json to network_config yaml
309
 
def convert_network_data(network_json=None):
 
258
def convert_network_data(network_json=None, known_macs=None):
310
259
    """Return a dictionary of network_config by parsing provided
311
260
       OpenStack ConfigDrive NetworkData json format
312
261
 
344
293
            'mac_address',
345
294
            'subnets',
346
295
            'params',
 
296
            'mtu',
347
297
        ],
348
298
        'subnet': [
349
299
            'type',
353
303
            'metric',
354
304
            'gateway',
355
305
            'pointopoint',
356
 
            'mtu',
357
306
            'scope',
358
307
            'dns_nameservers',
359
308
            'dns_search',
370
319
        subnets = []
371
320
        cfg = {k: v for k, v in link.items()
372
321
               if k in valid_keys['physical']}
373
 
        cfg.update({'name': link['id']})
374
 
        for network in [net for net in networks
375
 
                        if net['link'] == link['id']]:
 
322
        # 'name' is not in openstack spec yet, but we will support it if it is
 
323
        # present.  The 'id' in the spec is currently implemented as the host
 
324
        # nic's name, meaning something like 'tap-adfasdffd'.  We do not want
 
325
        # to name guest devices with such ugly names.
 
326
        if 'name' in link:
 
327
            cfg['name'] = link['name']
 
328
 
 
329
        for network in [n for n in networks
 
330
                        if n['link'] == link['id']]:
376
331
            subnet = {k: v for k, v in network.items()
377
332
                      if k in valid_keys['subnet']}
378
333
            if 'dhcp' in network['type']:
387
342
                })
388
343
            subnets.append(subnet)
389
344
        cfg.update({'subnets': subnets})
390
 
        if link['type'] in ['ethernet', 'vif', 'ovs', 'phy']:
 
345
        if link['type'] in ['ethernet', 'vif', 'ovs', 'phy', 'bridge']:
391
346
            cfg.update({
392
347
                'type': 'physical',
393
348
                'mac_address': link['ethernet_mac_address']})
416
371
 
417
372
        config.append(cfg)
418
373
 
 
374
    need_names = [d for d in config
 
375
                  if d.get('type') == 'physical' and 'name' not in d]
 
376
 
 
377
    if need_names:
 
378
        if known_macs is None:
 
379
            known_macs = net.get_interfaces_by_mac()
 
380
 
 
381
        for d in need_names:
 
382
            mac = d.get('mac_address')
 
383
            if not mac:
 
384
                raise ValueError("No mac_address or name entry for %s" % d)
 
385
            if mac not in known_macs:
 
386
                raise ValueError("Unable to find a system nic for %s" % d)
 
387
            d['name'] = known_macs[mac]
 
388
 
419
389
    for service in services:
420
390
        cfg = service
421
391
        cfg.update({'type': 'nameserver'})
422
392
        config.append(cfg)
423
393
 
424
394
    return {'version': 1, 'config': config}
 
395
 
 
396
 
 
397
# Legacy: Must be present in case we load an old pkl object
 
398
DataSourceConfigDriveNet = DataSourceConfigDrive
 
399
 
 
400
# Used to match classes to dependencies
 
401
datasources = [
 
402
    (DataSourceConfigDrive, (sources.DEP_FILESYSTEM, )),
 
403
]
 
404
 
 
405
 
 
406
# Return a list of data sources that match this set of dependencies
 
407
def get_datasource_list(depends):
 
408
    return sources.list_from_depends(depends, datasources)