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

« back to all changes in this revision

Viewing changes to cloudinit/net/__init__.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
 
#   Copyright (C) 2013-2014 Canonical Ltd.
2
 
#
3
 
#   Author: Scott Moser <scott.moser@canonical.com>
4
 
#   Author: Blake Rouse <blake.rouse@canonical.com>
5
 
#
6
 
#   Curtin is free software: you can redistribute it and/or modify it under
7
 
#   the terms of the GNU Affero General Public License as published by the
8
 
#   Free Software Foundation, either version 3 of the License, or (at your
9
 
#   option) any later version.
10
 
#
11
 
#   Curtin is distributed in the hope that it will be useful, but WITHOUT ANY
12
 
#   WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13
 
#   FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for
14
 
#   more details.
15
 
#
16
 
#   You should have received a copy of the GNU Affero General Public License
17
 
#   along with Curtin.  If not, see <http://www.gnu.org/licenses/>.
18
 
 
19
 
import errno
20
 
import logging
21
 
import os
22
 
import re
23
 
 
24
 
from cloudinit import util
25
 
 
26
 
LOG = logging.getLogger(__name__)
27
 
SYS_CLASS_NET = "/sys/class/net/"
28
 
DEFAULT_PRIMARY_INTERFACE = 'eth0'
29
 
 
30
 
 
31
 
def sys_dev_path(devname, path=""):
32
 
    return SYS_CLASS_NET + devname + "/" + path
33
 
 
34
 
 
35
 
def read_sys_net(devname, path, translate=None, enoent=None, keyerror=None):
36
 
    try:
37
 
        contents = util.load_file(sys_dev_path(devname, path))
38
 
    except (OSError, IOError) as e:
39
 
        if getattr(e, 'errno', None) == errno.ENOENT:
40
 
            if enoent is not None:
41
 
                return enoent
42
 
        raise
43
 
    contents = contents.strip()
44
 
    if translate is None:
45
 
        return contents
46
 
    try:
47
 
        return translate.get(contents)
48
 
    except KeyError:
49
 
        LOG.debug("found unexpected value '%s' in '%s/%s'", contents,
50
 
                  devname, path)
51
 
        if keyerror is not None:
52
 
            return keyerror
53
 
        raise
54
 
 
55
 
 
56
 
def is_up(devname):
57
 
    # The linux kernel says to consider devices in 'unknown'
58
 
    # operstate as up for the purposes of network configuration. See
59
 
    # Documentation/networking/operstates.txt in the kernel source.
60
 
    translate = {'up': True, 'unknown': True, 'down': False}
61
 
    return read_sys_net(devname, "operstate", enoent=False, keyerror=False,
62
 
                        translate=translate)
63
 
 
64
 
 
65
 
def is_wireless(devname):
66
 
    return os.path.exists(sys_dev_path(devname, "wireless"))
67
 
 
68
 
 
69
 
def is_connected(devname):
70
 
    # is_connected isn't really as simple as that.  2 is
71
 
    # 'physically connected'. 3 is 'not connected'. but a wlan interface will
72
 
    # always show 3.
73
 
    try:
74
 
        iflink = read_sys_net(devname, "iflink", enoent=False)
75
 
        if iflink == "2":
76
 
            return True
77
 
        if not is_wireless(devname):
78
 
            return False
79
 
        LOG.debug("'%s' is wireless, basing 'connected' on carrier", devname)
80
 
 
81
 
        return read_sys_net(devname, "carrier", enoent=False, keyerror=False,
82
 
                            translate={'0': False, '1': True})
83
 
 
84
 
    except IOError as e:
85
 
        if e.errno == errno.EINVAL:
86
 
            return False
87
 
        raise
88
 
 
89
 
 
90
 
def is_physical(devname):
91
 
    return os.path.exists(sys_dev_path(devname, "device"))
92
 
 
93
 
 
94
 
def is_present(devname):
95
 
    return os.path.exists(sys_dev_path(devname))
96
 
 
97
 
 
98
 
def get_devicelist():
99
 
    return os.listdir(SYS_CLASS_NET)
100
 
 
101
 
 
102
 
class ParserError(Exception):
103
 
    """Raised when a parser has issue parsing a file/content."""
104
 
 
105
 
 
106
 
def is_disabled_cfg(cfg):
107
 
    if not cfg or not isinstance(cfg, dict):
108
 
        return False
109
 
    return cfg.get('config') == "disabled"
110
 
 
111
 
 
112
 
def sys_netdev_info(name, field):
113
 
    if not os.path.exists(os.path.join(SYS_CLASS_NET, name)):
114
 
        raise OSError("%s: interface does not exist in %s" %
115
 
                      (name, SYS_CLASS_NET))
116
 
    fname = os.path.join(SYS_CLASS_NET, name, field)
117
 
    if not os.path.exists(fname):
118
 
        raise OSError("%s: could not find sysfs entry: %s" % (name, fname))
119
 
    data = util.load_file(fname)
120
 
    if data[-1] == '\n':
121
 
        data = data[:-1]
122
 
    return data
123
 
 
124
 
 
125
 
def generate_fallback_config():
126
 
    """Determine which attached net dev is most likely to have a connection and
127
 
       generate network state to run dhcp on that interface"""
128
 
    # by default use eth0 as primary interface
129
 
    nconf = {'config': [], 'version': 1}
130
 
 
131
 
    # get list of interfaces that could have connections
132
 
    invalid_interfaces = set(['lo'])
133
 
    potential_interfaces = set(get_devicelist())
134
 
    potential_interfaces = potential_interfaces.difference(invalid_interfaces)
135
 
    # sort into interfaces with carrier, interfaces which could have carrier,
136
 
    # and ignore interfaces that are definitely disconnected
137
 
    connected = []
138
 
    possibly_connected = []
139
 
    for interface in potential_interfaces:
140
 
        if interface.startswith("veth"):
141
 
            continue
142
 
        if os.path.exists(sys_dev_path(interface, "bridge")):
143
 
            # skip any bridges
144
 
            continue
145
 
        try:
146
 
            carrier = int(sys_netdev_info(interface, 'carrier'))
147
 
            if carrier:
148
 
                connected.append(interface)
149
 
                continue
150
 
        except OSError:
151
 
            pass
152
 
        # check if nic is dormant or down, as this may make a nick appear to
153
 
        # not have a carrier even though it could acquire one when brought
154
 
        # online by dhclient
155
 
        try:
156
 
            dormant = int(sys_netdev_info(interface, 'dormant'))
157
 
            if dormant:
158
 
                possibly_connected.append(interface)
159
 
                continue
160
 
        except OSError:
161
 
            pass
162
 
        try:
163
 
            operstate = sys_netdev_info(interface, 'operstate')
164
 
            if operstate in ['dormant', 'down', 'lowerlayerdown', 'unknown']:
165
 
                possibly_connected.append(interface)
166
 
                continue
167
 
        except OSError:
168
 
            pass
169
 
 
170
 
    # don't bother with interfaces that might not be connected if there are
171
 
    # some that definitely are
172
 
    if connected:
173
 
        potential_interfaces = connected
174
 
    else:
175
 
        potential_interfaces = possibly_connected
176
 
    # if there are no interfaces, give up
177
 
    if not potential_interfaces:
178
 
        return
179
 
    # if eth0 exists use it above anything else, otherwise get the interface
180
 
    # that looks 'first'
181
 
    if DEFAULT_PRIMARY_INTERFACE in potential_interfaces:
182
 
        name = DEFAULT_PRIMARY_INTERFACE
183
 
    else:
184
 
        name = sorted(potential_interfaces)[0]
185
 
 
186
 
    mac = sys_netdev_info(name, 'address')
187
 
    target_name = name
188
 
 
189
 
    nconf['config'].append(
190
 
        {'type': 'physical', 'name': target_name,
191
 
         'mac_address': mac, 'subnets': [{'type': 'dhcp'}]})
192
 
    return nconf
193
 
 
194
 
 
195
 
def apply_network_config_names(netcfg, strict_present=True, strict_busy=True):
196
 
    """read the network config and rename devices accordingly.
197
 
    if strict_present is false, then do not raise exception if no devices
198
 
    match.  if strict_busy is false, then do not raise exception if the
199
 
    device cannot be renamed because it is currently configured."""
200
 
    renames = []
201
 
    for ent in netcfg.get('config', {}):
202
 
        if ent.get('type') != 'physical':
203
 
            continue
204
 
        mac = ent.get('mac_address')
205
 
        name = ent.get('name')
206
 
        if not mac:
207
 
            continue
208
 
        renames.append([mac, name])
209
 
 
210
 
    return _rename_interfaces(renames)
211
 
 
212
 
 
213
 
def _get_current_rename_info(check_downable=True):
214
 
    """Collect information necessary for rename_interfaces."""
215
 
    names = get_devicelist()
216
 
    bymac = {}
217
 
    for n in names:
218
 
        bymac[get_interface_mac(n)] = {
219
 
            'name': n, 'up': is_up(n), 'downable': None}
220
 
 
221
 
    if check_downable:
222
 
        nmatch = re.compile(r"[0-9]+:\s+(\w+)[@:]")
223
 
        ipv6, _err = util.subp(['ip', '-6', 'addr', 'show', 'permanent',
224
 
                                'scope', 'global'], capture=True)
225
 
        ipv4, _err = util.subp(['ip', '-4', 'addr', 'show'], capture=True)
226
 
 
227
 
        nics_with_addresses = set()
228
 
        for bytes_out in (ipv6, ipv4):
229
 
            nics_with_addresses.update(nmatch.findall(bytes_out))
230
 
 
231
 
        for d in bymac.values():
232
 
            d['downable'] = (d['up'] is False or
233
 
                             d['name'] not in nics_with_addresses)
234
 
 
235
 
    return bymac
236
 
 
237
 
 
238
 
def _rename_interfaces(renames, strict_present=True, strict_busy=True,
239
 
                       current_info=None):
240
 
 
241
 
    if not len(renames):
242
 
        LOG.debug("no interfaces to rename")
243
 
        return
244
 
 
245
 
    if current_info is None:
246
 
        current_info = _get_current_rename_info()
247
 
 
248
 
    cur_bymac = {}
249
 
    for mac, data in current_info.items():
250
 
        cur = data.copy()
251
 
        cur['mac'] = mac
252
 
        cur_bymac[mac] = cur
253
 
 
254
 
    def update_byname(bymac):
255
 
        return dict((data['name'], data)
256
 
                    for data in bymac.values())
257
 
 
258
 
    def rename(cur, new):
259
 
        util.subp(["ip", "link", "set", cur, "name", new], capture=True)
260
 
 
261
 
    def down(name):
262
 
        util.subp(["ip", "link", "set", name, "down"], capture=True)
263
 
 
264
 
    def up(name):
265
 
        util.subp(["ip", "link", "set", name, "up"], capture=True)
266
 
 
267
 
    ops = []
268
 
    errors = []
269
 
    ups = []
270
 
    cur_byname = update_byname(cur_bymac)
271
 
    tmpname_fmt = "cirename%d"
272
 
    tmpi = -1
273
 
 
274
 
    for mac, new_name in renames:
275
 
        cur = cur_bymac.get(mac, {})
276
 
        cur_name = cur.get('name')
277
 
        cur_ops = []
278
 
        if cur_name == new_name:
279
 
            # nothing to do
280
 
            continue
281
 
 
282
 
        if not cur_name:
283
 
            if strict_present:
284
 
                errors.append(
285
 
                    "[nic not present] Cannot rename mac=%s to %s"
286
 
                    ", not available." % (mac, new_name))
287
 
            continue
288
 
 
289
 
        if cur['up']:
290
 
            msg = "[busy] Error renaming mac=%s from %s to %s"
291
 
            if not cur['downable']:
292
 
                if strict_busy:
293
 
                    errors.append(msg % (mac, cur_name, new_name))
294
 
                continue
295
 
            cur['up'] = False
296
 
            cur_ops.append(("down", mac, new_name, (cur_name,)))
297
 
            ups.append(("up", mac, new_name, (new_name,)))
298
 
 
299
 
        if new_name in cur_byname:
300
 
            target = cur_byname[new_name]
301
 
            if target['up']:
302
 
                msg = "[busy-target] Error renaming mac=%s from %s to %s."
303
 
                if not target['downable']:
304
 
                    if strict_busy:
305
 
                        errors.append(msg % (mac, cur_name, new_name))
306
 
                    continue
307
 
                else:
308
 
                    cur_ops.append(("down", mac, new_name, (new_name,)))
309
 
 
310
 
            tmp_name = None
311
 
            while tmp_name is None or tmp_name in cur_byname:
312
 
                tmpi += 1
313
 
                tmp_name = tmpname_fmt % tmpi
314
 
 
315
 
            cur_ops.append(("rename", mac, new_name, (new_name, tmp_name)))
316
 
            target['name'] = tmp_name
317
 
            cur_byname = update_byname(cur_bymac)
318
 
            if target['up']:
319
 
                ups.append(("up", mac, new_name, (tmp_name,)))
320
 
 
321
 
        cur_ops.append(("rename", mac, new_name, (cur['name'], new_name)))
322
 
        cur['name'] = new_name
323
 
        cur_byname = update_byname(cur_bymac)
324
 
        ops += cur_ops
325
 
 
326
 
    opmap = {'rename': rename, 'down': down, 'up': up}
327
 
 
328
 
    if len(ops) + len(ups) == 0:
329
 
        if len(errors):
330
 
            LOG.debug("unable to do any work for renaming of %s", renames)
331
 
        else:
332
 
            LOG.debug("no work necessary for renaming of %s", renames)
333
 
    else:
334
 
        LOG.debug("achieving renaming of %s with ops %s", renames, ops + ups)
335
 
 
336
 
        for op, mac, new_name, params in ops + ups:
337
 
            try:
338
 
                opmap.get(op)(*params)
339
 
            except Exception as e:
340
 
                errors.append(
341
 
                    "[unknown] Error performing %s%s for %s, %s: %s" %
342
 
                    (op, params, mac, new_name, e))
343
 
 
344
 
    if len(errors):
345
 
        raise Exception('\n'.join(errors))
346
 
 
347
 
 
348
 
def get_interface_mac(ifname):
349
 
    """Returns the string value of an interface's MAC Address"""
350
 
    return read_sys_net(ifname, "address", enoent=False)
351
 
 
352
 
 
353
 
def get_interfaces_by_mac(devs=None):
354
 
    """Build a dictionary of tuples {mac: name}"""
355
 
    if devs is None:
356
 
        try:
357
 
            devs = get_devicelist()
358
 
        except OSError as e:
359
 
            if e.errno == errno.ENOENT:
360
 
                devs = []
361
 
            else:
362
 
                raise
363
 
    ret = {}
364
 
    for name in devs:
365
 
        mac = get_interface_mac(name)
366
 
        # some devices may not have a mac (tun0)
367
 
        if mac:
368
 
            ret[mac] = name
369
 
    return ret
370
 
 
371
 
# vi: ts=4 expandtab syntax=python