1
# Copyright (C) 2013-2014 Canonical Ltd.
3
# Author: Scott Moser <scott.moser@canonical.com>
4
# Author: Blake Rouse <blake.rouse@canonical.com>
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.
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
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/>.
24
from cloudinit import util
26
LOG = logging.getLogger(__name__)
27
SYS_CLASS_NET = "/sys/class/net/"
28
DEFAULT_PRIMARY_INTERFACE = 'eth0'
31
def sys_dev_path(devname, path=""):
32
return SYS_CLASS_NET + devname + "/" + path
35
def read_sys_net(devname, path, translate=None, enoent=None, keyerror=None):
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:
43
contents = contents.strip()
47
return translate.get(contents)
49
LOG.debug("found unexpected value '%s' in '%s/%s'", contents,
51
if keyerror is not None:
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,
65
def is_wireless(devname):
66
return os.path.exists(sys_dev_path(devname, "wireless"))
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
74
iflink = read_sys_net(devname, "iflink", enoent=False)
77
if not is_wireless(devname):
79
LOG.debug("'%s' is wireless, basing 'connected' on carrier", devname)
81
return read_sys_net(devname, "carrier", enoent=False, keyerror=False,
82
translate={'0': False, '1': True})
85
if e.errno == errno.EINVAL:
90
def is_physical(devname):
91
return os.path.exists(sys_dev_path(devname, "device"))
94
def is_present(devname):
95
return os.path.exists(sys_dev_path(devname))
99
return os.listdir(SYS_CLASS_NET)
102
class ParserError(Exception):
103
"""Raised when a parser has issue parsing a file/content."""
106
def is_disabled_cfg(cfg):
107
if not cfg or not isinstance(cfg, dict):
109
return cfg.get('config') == "disabled"
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)
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}
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
138
possibly_connected = []
139
for interface in potential_interfaces:
140
if interface.startswith("veth"):
142
if os.path.exists(sys_dev_path(interface, "bridge")):
146
carrier = int(sys_netdev_info(interface, 'carrier'))
148
connected.append(interface)
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
156
dormant = int(sys_netdev_info(interface, 'dormant'))
158
possibly_connected.append(interface)
163
operstate = sys_netdev_info(interface, 'operstate')
164
if operstate in ['dormant', 'down', 'lowerlayerdown', 'unknown']:
165
possibly_connected.append(interface)
170
# don't bother with interfaces that might not be connected if there are
171
# some that definitely are
173
potential_interfaces = connected
175
potential_interfaces = possibly_connected
176
# if there are no interfaces, give up
177
if not potential_interfaces:
179
# if eth0 exists use it above anything else, otherwise get the interface
181
if DEFAULT_PRIMARY_INTERFACE in potential_interfaces:
182
name = DEFAULT_PRIMARY_INTERFACE
184
name = sorted(potential_interfaces)[0]
186
mac = sys_netdev_info(name, 'address')
189
nconf['config'].append(
190
{'type': 'physical', 'name': target_name,
191
'mac_address': mac, 'subnets': [{'type': 'dhcp'}]})
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."""
201
for ent in netcfg.get('config', {}):
202
if ent.get('type') != 'physical':
204
mac = ent.get('mac_address')
205
name = ent.get('name')
208
renames.append([mac, name])
210
return _rename_interfaces(renames)
213
def _get_current_rename_info(check_downable=True):
214
"""Collect information necessary for rename_interfaces."""
215
names = get_devicelist()
218
bymac[get_interface_mac(n)] = {
219
'name': n, 'up': is_up(n), 'downable': None}
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)
227
nics_with_addresses = set()
228
for bytes_out in (ipv6, ipv4):
229
nics_with_addresses.update(nmatch.findall(bytes_out))
231
for d in bymac.values():
232
d['downable'] = (d['up'] is False or
233
d['name'] not in nics_with_addresses)
238
def _rename_interfaces(renames, strict_present=True, strict_busy=True,
242
LOG.debug("no interfaces to rename")
245
if current_info is None:
246
current_info = _get_current_rename_info()
249
for mac, data in current_info.items():
254
def update_byname(bymac):
255
return dict((data['name'], data)
256
for data in bymac.values())
258
def rename(cur, new):
259
util.subp(["ip", "link", "set", cur, "name", new], capture=True)
262
util.subp(["ip", "link", "set", name, "down"], capture=True)
265
util.subp(["ip", "link", "set", name, "up"], capture=True)
270
cur_byname = update_byname(cur_bymac)
271
tmpname_fmt = "cirename%d"
274
for mac, new_name in renames:
275
cur = cur_bymac.get(mac, {})
276
cur_name = cur.get('name')
278
if cur_name == new_name:
285
"[nic not present] Cannot rename mac=%s to %s"
286
", not available." % (mac, new_name))
290
msg = "[busy] Error renaming mac=%s from %s to %s"
291
if not cur['downable']:
293
errors.append(msg % (mac, cur_name, new_name))
296
cur_ops.append(("down", mac, new_name, (cur_name,)))
297
ups.append(("up", mac, new_name, (new_name,)))
299
if new_name in cur_byname:
300
target = cur_byname[new_name]
302
msg = "[busy-target] Error renaming mac=%s from %s to %s."
303
if not target['downable']:
305
errors.append(msg % (mac, cur_name, new_name))
308
cur_ops.append(("down", mac, new_name, (new_name,)))
311
while tmp_name is None or tmp_name in cur_byname:
313
tmp_name = tmpname_fmt % tmpi
315
cur_ops.append(("rename", mac, new_name, (new_name, tmp_name)))
316
target['name'] = tmp_name
317
cur_byname = update_byname(cur_bymac)
319
ups.append(("up", mac, new_name, (tmp_name,)))
321
cur_ops.append(("rename", mac, new_name, (cur['name'], new_name)))
322
cur['name'] = new_name
323
cur_byname = update_byname(cur_bymac)
326
opmap = {'rename': rename, 'down': down, 'up': up}
328
if len(ops) + len(ups) == 0:
330
LOG.debug("unable to do any work for renaming of %s", renames)
332
LOG.debug("no work necessary for renaming of %s", renames)
334
LOG.debug("achieving renaming of %s with ops %s", renames, ops + ups)
336
for op, mac, new_name, params in ops + ups:
338
opmap.get(op)(*params)
339
except Exception as e:
341
"[unknown] Error performing %s%s for %s, %s: %s" %
342
(op, params, mac, new_name, e))
345
raise Exception('\n'.join(errors))
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)
353
def get_interfaces_by_mac(devs=None):
354
"""Build a dictionary of tuples {mac: name}"""
357
devs = get_devicelist()
359
if e.errno == errno.ENOENT:
365
mac = get_interface_mac(name)
366
# some devices may not have a mac (tun0)
371
# vi: ts=4 expandtab syntax=python