|
|
1064ba |
From 4ada8f2ec17d11835b44ab3d7786e5f3a732df41 Mon Sep 17 00:00:00 2001
|
|
|
1064ba |
From: Scott Moser <smoser@brickies.net>
|
|
|
1064ba |
Date: Fri, 31 Mar 2017 10:56:04 -0400
|
|
|
1064ba |
Subject: [PATCH] Fix bug that resulted in an attempt to rename bonds or vlans.
|
|
|
1064ba |
|
|
|
1064ba |
When cloud-init ran in the init stage (after networking had come up).
|
|
|
1064ba |
A bug could occur where cloud-init would attempt and fail to rename
|
|
|
1064ba |
network devices that had "inherited" mac addresses.
|
|
|
1064ba |
|
|
|
1064ba |
The intent of apply_network_config_names was always to rename only
|
|
|
1064ba |
the devices that were "physical" per the network config. (This would
|
|
|
1064ba |
include veth devices in a container). The bug was in creating
|
|
|
1064ba |
the dictionary of interfaces by mac address. If there were multiple
|
|
|
1064ba |
interfaces with the same mac address then renames could fail.
|
|
|
1064ba |
This situation was guaranteed to occur with bonds or vlans or other
|
|
|
1064ba |
devices that inherit their mac.
|
|
|
1064ba |
|
|
|
1064ba |
The solution is to change get_interfaces_by_mac to skip interfaces
|
|
|
1064ba |
that have an inherited mac.
|
|
|
1064ba |
|
|
|
1064ba |
Also drop the 'devs' argument to get_interfaces_by_mac. It was
|
|
|
1064ba |
non-obvious what the result should be if a device in the input
|
|
|
1064ba |
list was filtered out. ie should the following have an entry for
|
|
|
1064ba |
bond0 or not. get_interfaces_by_mac(devs=['bond0'])
|
|
|
1064ba |
|
|
|
1064ba |
LP: #1669860
|
|
|
1064ba |
(cherry picked from commit bf7723e8092bb1f8a442aa2399dd870e130a27d9)
|
|
|
1064ba |
|
|
|
1064ba |
Resolves: rhbz#1512247
|
|
|
1064ba |
Signed-off-by: Ryan McCabe <rmccabe@redhat.com>
|
|
|
1064ba |
(cherry picked from commit f9e8f13f916fe740e46c9a0e9dd2dbb3cdb39975)
|
|
|
1064ba |
---
|
|
|
1064ba |
cloudinit/net/__init__.py | 78 ++++++++++++++++++++++++++++++++++-----------
|
|
|
1064ba |
tests/unittests/test_net.py | 76 +++++++++++++++++++++++++++++++++++++++++++
|
|
|
1064ba |
2 files changed, 135 insertions(+), 19 deletions(-)
|
|
|
1064ba |
mode change 100755 => 100644 cloudinit/net/__init__.py
|
|
|
1064ba |
|
|
|
1064ba |
diff --git a/cloudinit/net/__init__.py b/cloudinit/net/__init__.py
|
|
|
1064ba |
old mode 100755
|
|
|
1064ba |
new mode 100644
|
|
|
1064ba |
index ea649cc2..ab7e8996
|
|
|
1064ba |
--- a/cloudinit/net/__init__.py
|
|
|
1064ba |
+++ b/cloudinit/net/__init__.py
|
|
|
1064ba |
@@ -82,6 +82,10 @@ def is_wireless(devname):
|
|
|
1064ba |
return os.path.exists(sys_dev_path(devname, "wireless"))
|
|
|
1064ba |
|
|
|
1064ba |
|
|
|
1064ba |
+def is_bridge(devname):
|
|
|
1064ba |
+ return os.path.exists(sys_dev_path(devname, "bridge"))
|
|
|
1064ba |
+
|
|
|
1064ba |
+
|
|
|
1064ba |
def is_connected(devname):
|
|
|
1064ba |
# is_connected isn't really as simple as that. 2 is
|
|
|
1064ba |
# 'physically connected'. 3 is 'not connected'. but a wlan interface will
|
|
|
1064ba |
@@ -132,7 +136,7 @@ def generate_fallback_config():
|
|
|
1064ba |
for interface in potential_interfaces:
|
|
|
1064ba |
if interface.startswith("veth"):
|
|
|
1064ba |
continue
|
|
|
1064ba |
- if os.path.exists(sys_dev_path(interface, "bridge")):
|
|
|
1064ba |
+ if is_bridge(interface):
|
|
|
1064ba |
# skip any bridges
|
|
|
1064ba |
continue
|
|
|
1064ba |
carrier = read_sys_net_int(interface, 'carrier')
|
|
|
1064ba |
@@ -187,7 +191,11 @@ def apply_network_config_names(netcfg, strict_present=True, strict_busy=True):
|
|
|
1064ba |
"""read the network config and rename devices accordingly.
|
|
|
1064ba |
if strict_present is false, then do not raise exception if no devices
|
|
|
1064ba |
match. if strict_busy is false, then do not raise exception if the
|
|
|
1064ba |
- device cannot be renamed because it is currently configured."""
|
|
|
1064ba |
+ device cannot be renamed because it is currently configured.
|
|
|
1064ba |
+
|
|
|
1064ba |
+ renames are only attempted for interfaces of type 'physical'. It is
|
|
|
1064ba |
+ expected that the network system will create other devices with the
|
|
|
1064ba |
+ correct name in place."""
|
|
|
1064ba |
renames = []
|
|
|
1064ba |
for ent in netcfg.get('config', {}):
|
|
|
1064ba |
if ent.get('type') != 'physical':
|
|
|
1064ba |
@@ -201,13 +209,35 @@ def apply_network_config_names(netcfg, strict_present=True, strict_busy=True):
|
|
|
1064ba |
return _rename_interfaces(renames)
|
|
|
1064ba |
|
|
|
1064ba |
|
|
|
1064ba |
+def interface_has_own_mac(ifname, strict=False):
|
|
|
1064ba |
+ """return True if the provided interface has its own address.
|
|
|
1064ba |
+
|
|
|
1064ba |
+ Based on addr_assign_type in /sys. Return true for any interface
|
|
|
1064ba |
+ that does not have a 'stolen' address. Examples of such devices
|
|
|
1064ba |
+ are bonds or vlans that inherit their mac from another device.
|
|
|
1064ba |
+ Possible values are:
|
|
|
1064ba |
+ 0: permanent address 2: stolen from another device
|
|
|
1064ba |
+ 1: randomly generated 3: set using dev_set_mac_address"""
|
|
|
1064ba |
+
|
|
|
1064ba |
+ assign_type = read_sys_net_int(ifname, "addr_assign_type")
|
|
|
1064ba |
+ if strict and assign_type is None:
|
|
|
1064ba |
+ raise ValueError("%s had no addr_assign_type.")
|
|
|
1064ba |
+ return assign_type in (0, 1, 3)
|
|
|
1064ba |
+
|
|
|
1064ba |
+
|
|
|
1064ba |
def _get_current_rename_info(check_downable=True):
|
|
|
1064ba |
- """Collect information necessary for rename_interfaces."""
|
|
|
1064ba |
- names = get_devicelist()
|
|
|
1064ba |
+ """Collect information necessary for rename_interfaces.
|
|
|
1064ba |
+
|
|
|
1064ba |
+ returns a dictionary by mac address like:
|
|
|
1064ba |
+ {mac:
|
|
|
1064ba |
+ {'name': name
|
|
|
1064ba |
+ 'up': boolean: is_up(name),
|
|
|
1064ba |
+ 'downable': None or boolean indicating that the
|
|
|
1064ba |
+ device has only automatically assigned ip addrs.}}
|
|
|
1064ba |
+ """
|
|
|
1064ba |
bymac = {}
|
|
|
1064ba |
- for n in names:
|
|
|
1064ba |
- bymac[get_interface_mac(n)] = {
|
|
|
1064ba |
- 'name': n, 'up': is_up(n), 'downable': None}
|
|
|
1064ba |
+ for mac, name in get_interfaces_by_mac().items():
|
|
|
1064ba |
+ bymac[mac] = {'name': name, 'up': is_up(name), 'downable': None}
|
|
|
1064ba |
|
|
|
1064ba |
if check_downable:
|
|
|
1064ba |
nmatch = re.compile(r"[0-9]+:\s+(\w+)[@:]")
|
|
|
1064ba |
@@ -346,22 +376,32 @@ def get_interface_mac(ifname):
|
|
|
1064ba |
return read_sys_net_safe(ifname, path)
|
|
|
1064ba |
|
|
|
1064ba |
|
|
|
1064ba |
-def get_interfaces_by_mac(devs=None):
|
|
|
1064ba |
- """Build a dictionary of tuples {mac: name}"""
|
|
|
1064ba |
- if devs is None:
|
|
|
1064ba |
- try:
|
|
|
1064ba |
- devs = get_devicelist()
|
|
|
1064ba |
- except OSError as e:
|
|
|
1064ba |
- if e.errno == errno.ENOENT:
|
|
|
1064ba |
- devs = []
|
|
|
1064ba |
- else:
|
|
|
1064ba |
- raise
|
|
|
1064ba |
+def get_interfaces_by_mac():
|
|
|
1064ba |
+ """Build a dictionary of tuples {mac: name}.
|
|
|
1064ba |
+
|
|
|
1064ba |
+ Bridges and any devices that have a 'stolen' mac are excluded."""
|
|
|
1064ba |
+ try:
|
|
|
1064ba |
+ devs = get_devicelist()
|
|
|
1064ba |
+ except OSError as e:
|
|
|
1064ba |
+ if e.errno == errno.ENOENT:
|
|
|
1064ba |
+ devs = []
|
|
|
1064ba |
+ else:
|
|
|
1064ba |
+ raise
|
|
|
1064ba |
ret = {}
|
|
|
1064ba |
for name in devs:
|
|
|
1064ba |
+ if not interface_has_own_mac(name):
|
|
|
1064ba |
+ continue
|
|
|
1064ba |
+ if is_bridge(name):
|
|
|
1064ba |
+ continue
|
|
|
1064ba |
mac = get_interface_mac(name)
|
|
|
1064ba |
# some devices may not have a mac (tun0)
|
|
|
1064ba |
- if mac:
|
|
|
1064ba |
- ret[mac] = name
|
|
|
1064ba |
+ if not mac:
|
|
|
1064ba |
+ continue
|
|
|
1064ba |
+ if mac in ret:
|
|
|
1064ba |
+ raise RuntimeError(
|
|
|
1064ba |
+ "duplicate mac found! both '%s' and '%s' have mac '%s'" %
|
|
|
1064ba |
+ (name, ret[mac], mac))
|
|
|
1064ba |
+ ret[mac] = name
|
|
|
1064ba |
return ret
|
|
|
1064ba |
|
|
|
1064ba |
# vi: ts=4 expandtab
|
|
|
1064ba |
diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py
|
|
|
1064ba |
index 551370d4..cadaf596 100644
|
|
|
1064ba |
--- a/tests/unittests/test_net.py
|
|
|
1064ba |
+++ b/tests/unittests/test_net.py
|
|
|
1064ba |
@@ -1173,6 +1173,82 @@ class TestEniRoundTrip(TestCase):
|
|
|
1064ba |
expected, [line for line in found if line])
|
|
|
1064ba |
|
|
|
1064ba |
|
|
|
1064ba |
+class TestGetInterfacesByMac(TestCase):
|
|
|
1064ba |
+ _data = {'devices': ['enp0s1', 'enp0s2', 'bond1', 'bridge1',
|
|
|
1064ba |
+ 'bridge1-nic', 'tun0'],
|
|
|
1064ba |
+ 'bonds': ['bond1'],
|
|
|
1064ba |
+ 'bridges': ['bridge1'],
|
|
|
1064ba |
+ 'own_macs': ['enp0s1', 'enp0s2', 'bridge1-nic', 'bridge1'],
|
|
|
1064ba |
+ 'macs': {'enp0s1': 'aa:aa:aa:aa:aa:01',
|
|
|
1064ba |
+ 'enp0s2': 'aa:aa:aa:aa:aa:02',
|
|
|
1064ba |
+ 'bond1': 'aa:aa:aa:aa:aa:01',
|
|
|
1064ba |
+ 'bridge1': 'aa:aa:aa:aa:aa:03',
|
|
|
1064ba |
+ 'bridge1-nic': 'aa:aa:aa:aa:aa:03',
|
|
|
1064ba |
+ 'tun0': None}}
|
|
|
1064ba |
+ data = {}
|
|
|
1064ba |
+
|
|
|
1064ba |
+ def _se_get_devicelist(self):
|
|
|
1064ba |
+ return self.data['devices']
|
|
|
1064ba |
+
|
|
|
1064ba |
+ def _se_get_interface_mac(self, name):
|
|
|
1064ba |
+ return self.data['macs'][name]
|
|
|
1064ba |
+
|
|
|
1064ba |
+ def _se_is_bridge(self, name):
|
|
|
1064ba |
+ return name in self.data['bridges']
|
|
|
1064ba |
+
|
|
|
1064ba |
+ def _se_interface_has_own_mac(self, name):
|
|
|
1064ba |
+ return name in self.data['own_macs']
|
|
|
1064ba |
+
|
|
|
1064ba |
+ def _mock_setup(self):
|
|
|
1064ba |
+ self.data = copy.deepcopy(self._data)
|
|
|
1064ba |
+ mocks = ('get_devicelist', 'get_interface_mac', 'is_bridge',
|
|
|
1064ba |
+ 'interface_has_own_mac')
|
|
|
1064ba |
+ self.mocks = {}
|
|
|
1064ba |
+ for n in mocks:
|
|
|
1064ba |
+ m = mock.patch('cloudinit.net.' + n,
|
|
|
1064ba |
+ side_effect=getattr(self, '_se_' + n))
|
|
|
1064ba |
+ self.addCleanup(m.stop)
|
|
|
1064ba |
+ self.mocks[n] = m.start()
|
|
|
1064ba |
+
|
|
|
1064ba |
+ def test_raise_exception_on_duplicate_macs(self):
|
|
|
1064ba |
+ self._mock_setup()
|
|
|
1064ba |
+ self.data['macs']['bridge1-nic'] = self.data['macs']['enp0s1']
|
|
|
1064ba |
+ self.assertRaises(RuntimeError, net.get_interfaces_by_mac)
|
|
|
1064ba |
+
|
|
|
1064ba |
+ def test_excludes_any_without_mac_address(self):
|
|
|
1064ba |
+ self._mock_setup()
|
|
|
1064ba |
+ ret = net.get_interfaces_by_mac()
|
|
|
1064ba |
+ self.assertIn('tun0', self._se_get_devicelist())
|
|
|
1064ba |
+ self.assertNotIn('tun0', ret.values())
|
|
|
1064ba |
+
|
|
|
1064ba |
+ def test_excludes_stolen_macs(self):
|
|
|
1064ba |
+ self._mock_setup()
|
|
|
1064ba |
+ ret = net.get_interfaces_by_mac()
|
|
|
1064ba |
+ self.mocks['interface_has_own_mac'].assert_has_calls(
|
|
|
1064ba |
+ [mock.call('enp0s1'), mock.call('bond1')], any_order=True)
|
|
|
1064ba |
+ self.assertEqual(
|
|
|
1064ba |
+ {'aa:aa:aa:aa:aa:01': 'enp0s1', 'aa:aa:aa:aa:aa:02': 'enp0s2',
|
|
|
1064ba |
+ 'aa:aa:aa:aa:aa:03': 'bridge1-nic'},
|
|
|
1064ba |
+ ret)
|
|
|
1064ba |
+
|
|
|
1064ba |
+ def test_excludes_bridges(self):
|
|
|
1064ba |
+ self._mock_setup()
|
|
|
1064ba |
+ # add a device 'b1', make all return they have their "own mac",
|
|
|
1064ba |
+ # set everything other than 'b1' to be a bridge.
|
|
|
1064ba |
+ # then expect b1 is the only thing left.
|
|
|
1064ba |
+ self.data['macs']['b1'] = 'aa:aa:aa:aa:aa:b1'
|
|
|
1064ba |
+ self.data['devices'].append('b1')
|
|
|
1064ba |
+ self.data['bonds'] = []
|
|
|
1064ba |
+ self.data['own_macs'] = self.data['devices']
|
|
|
1064ba |
+ self.data['bridges'] = [f for f in self.data['devices'] if f != "b1"]
|
|
|
1064ba |
+ ret = net.get_interfaces_by_mac()
|
|
|
1064ba |
+ self.assertEqual({'aa:aa:aa:aa:aa:b1': 'b1'}, ret)
|
|
|
1064ba |
+ self.mocks['is_bridge'].assert_has_calls(
|
|
|
1064ba |
+ [mock.call('bridge1'), mock.call('enp0s1'), mock.call('bond1'),
|
|
|
1064ba |
+ mock.call('b1')],
|
|
|
1064ba |
+ any_order=True)
|
|
|
1064ba |
+
|
|
|
1064ba |
+
|
|
|
1064ba |
def _gzip_data(data):
|
|
|
1064ba |
with io.BytesIO() as iobuf:
|
|
|
1064ba |
gzfp = gzip.GzipFile(mode="wb", fileobj=iobuf)
|
|
|
1064ba |
--
|
|
|
1064ba |
2.14.3
|
|
|
1064ba |
|