Merge lp:~hopem/charm-helpers/allow-list-nics-return-all into lp:charm-helpers

Proposed by Edward Hope-Morley
Status: Superseded
Proposed branch: lp:~hopem/charm-helpers/allow-list-nics-return-all
Merge into: lp:charm-helpers
Diff against target: 343 lines (+132/-45)
6 files modified
charmhelpers/contrib/openstack/context.py (+17/-10)
charmhelpers/contrib/openstack/neutron.py (+29/-16)
charmhelpers/core/host.py (+45/-11)
tests/contrib/openstack/test_neutron_utils.py (+3/-3)
tests/contrib/openstack/test_os_contexts.py (+7/-2)
tests/core/test_host.py (+31/-3)
To merge this branch: bzr merge lp:~hopem/charm-helpers/allow-list-nics-return-all
Reviewer Review Type Date Requested Status
Liam Young (community) Needs Fixing
Review via email: mp+268264@code.launchpad.net

This proposal has been superseded by a proposal from 2015-08-18.

To post a comment you must log in.
Revision history for this message
Liam Young (gnuoy) wrote :

Please could you update the unit tests so that the macs of IP_LINE_ETH0 and IP_LINE_ETH0_VLAN match and add a test for list_nics(['eth'], include_vlans=False) ?

As a nitpick I think it's cleaner parse the output of ip when using '-o' so that all entries are on one line but that's how it was before so I won't block on it.

review: Needs Fixing
432. By Edward Hope-Morley

[hopem,r=]

Add support to core.host.list_nics() to allow for listing
all nics i.e. unfiltered. Ensure that neutron resolve_ports
only resolves physical interfaces.

433. By Edward Hope-Morley

Add support for identiying bonded nics and resolving
their master.

434. By Edward Hope-Morley

ensure master is bond

435. By Edward Hope-Morley

added multimac unit test

436. By Edward Hope-Morley

more unit test

437. By Edward Hope-Morley

*fix* py3 unit test error

438. By Edward Hope-Morley

fix unit test

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'charmhelpers/contrib/openstack/context.py'
--- charmhelpers/contrib/openstack/context.py 2015-07-29 14:21:45 +0000
+++ charmhelpers/contrib/openstack/context.py 2015-08-18 11:36:40 +0000
@@ -50,6 +50,7 @@
50from charmhelpers.core.strutils import bool_from_string50from charmhelpers.core.strutils import bool_from_string
5151
52from charmhelpers.core.host import (52from charmhelpers.core.host import (
53 is_phy_iface,
53 list_nics,54 list_nics,
54 get_nic_hwaddr,55 get_nic_hwaddr,
55 mkdir,56 mkdir,
@@ -923,7 +924,6 @@
923924
924925
925class NeutronPortContext(OSContextGenerator):926class NeutronPortContext(OSContextGenerator):
926 NIC_PREFIXES = ['eth', 'bond']
927927
928 def resolve_ports(self, ports):928 def resolve_ports(self, ports):
929 """Resolve NICs not yet bound to bridge(s)929 """Resolve NICs not yet bound to bridge(s)
@@ -935,12 +935,14 @@
935935
936 hwaddr_to_nic = {}936 hwaddr_to_nic = {}
937 hwaddr_to_ip = {}937 hwaddr_to_ip = {}
938 for nic in list_nics(self.NIC_PREFIXES):938 for nic in list_nics():
939 hwaddr = get_nic_hwaddr(nic)939 # Ignore virtual interfaces
940 hwaddr_to_nic[hwaddr] = nic940 if is_phy_iface(nic):
941 addresses = get_ipv4_addr(nic, fatal=False)941 hwaddr = get_nic_hwaddr(nic)
942 addresses += get_ipv6_addr(iface=nic, fatal=False)942 hwaddr_to_nic[hwaddr] = nic
943 hwaddr_to_ip[hwaddr] = addresses943 addresses = get_ipv4_addr(nic, fatal=False)
944 addresses += get_ipv6_addr(iface=nic, fatal=False)
945 hwaddr_to_ip[hwaddr] = addresses
944946
945 resolved = []947 resolved = []
946 mac_regex = re.compile(r'([0-9A-F]{2}[:-]){5}([0-9A-F]{2})', re.I)948 mac_regex = re.compile(r'([0-9A-F]{2}[:-]){5}([0-9A-F]{2})', re.I)
@@ -961,7 +963,8 @@
961 # trust it to be the real external network).963 # trust it to be the real external network).
962 resolved.append(entry)964 resolved.append(entry)
963965
964 return resolved966 # Ensure no duplicates
967 return list(set(resolved))
965968
966969
967class OSConfigFlagContext(OSContextGenerator):970class OSConfigFlagContext(OSContextGenerator):
@@ -1280,15 +1283,19 @@
1280 def __call__(self):1283 def __call__(self):
1281 ports = config('data-port')1284 ports = config('data-port')
1282 if ports:1285 if ports:
1286 # Map of {port/mac:bridge}
1283 portmap = parse_data_port_mappings(ports)1287 portmap = parse_data_port_mappings(ports)
1284 ports = portmap.values()1288 ports = portmap.keys()
1289 # Resolve provided ports or mac addresses and filter out those
1290 # already attached to a bridge.
1285 resolved = self.resolve_ports(ports)1291 resolved = self.resolve_ports(ports)
1292 # FIXME: is this necessary?
1286 normalized = {get_nic_hwaddr(port): port for port in resolved1293 normalized = {get_nic_hwaddr(port): port for port in resolved
1287 if port not in ports}1294 if port not in ports}
1288 normalized.update({port: port for port in resolved1295 normalized.update({port: port for port in resolved
1289 if port in ports})1296 if port in ports})
1290 if resolved:1297 if resolved:
1291 return {bridge: normalized[port] for bridge, port in1298 return {bridge: normalized[port] for port, bridge in
1292 six.iteritems(portmap) if port in normalized.keys()}1299 six.iteritems(portmap) if port in normalized.keys()}
12931300
1294 return None1301 return None
12951302
=== modified file 'charmhelpers/contrib/openstack/neutron.py'
--- charmhelpers/contrib/openstack/neutron.py 2015-06-10 11:17:45 +0000
+++ charmhelpers/contrib/openstack/neutron.py 2015-08-18 11:36:40 +0000
@@ -255,17 +255,30 @@
255 return 'neutron'255 return 'neutron'
256256
257257
258def parse_mappings(mappings):258def parse_mappings(mappings, key_rvalue=False):
259 """By default mappings are lvalue keyed.
260
261 If key_rvalue is True, the mapping will be reversed to allow multiple
262 configs for the same lvalue.
263 """
259 parsed = {}264 parsed = {}
260 if mappings:265 if mappings:
261 mappings = mappings.split()266 mappings = mappings.split()
262 for m in mappings:267 for m in mappings:
263 p = m.partition(':')268 p = m.partition(':')
264 key = p[0].strip()269
265 if p[1]:270 if key_rvalue:
266 parsed[key] = p[2].strip()271 key_index = 2
272 val_index = 0
273 # if there is no rvalue skip to next
274 if not p[1]:
275 continue
267 else:276 else:
268 parsed[key] = ''277 key_index = 0
278 val_index = 2
279
280 key = p[key_index].strip()
281 parsed[key] = p[val_index].strip()
269282
270 return parsed283 return parsed
271284
@@ -283,25 +296,25 @@
283def parse_data_port_mappings(mappings, default_bridge='br-data'):296def parse_data_port_mappings(mappings, default_bridge='br-data'):
284 """Parse data port mappings.297 """Parse data port mappings.
285298
286 Mappings must be a space-delimited list of bridge:port mappings.299 Mappings must be a space-delimited list of port:bridge mappings.
287300
288 Returns dict of the form {bridge:port}.301 Returns dict of the form {port:bridge} where port may be an mac address or
302 interface name.
289 """303 """
290 _mappings = parse_mappings(mappings)304
305 # NOTE(dosaboy): we use rvalue for key to allow multiple values to be
306 # proposed for <port> since it may be a mac address which will differ
307 # across units this allowing first-known-good to be chosen.
308 _mappings = parse_mappings(mappings, key_rvalue=True)
291 if not _mappings or list(_mappings.values()) == ['']:309 if not _mappings or list(_mappings.values()) == ['']:
292 if not mappings:310 if not mappings:
293 return {}311 return {}
294312
295 # For backwards-compatibility we need to support port-only provided in313 # For backwards-compatibility we need to support port-only provided in
296 # config.314 # config.
297 _mappings = {default_bridge: mappings.split()[0]}315 _mappings = {mappings.split()[0]: default_bridge}
298316
299 bridges = _mappings.keys()317 ports = _mappings.keys()
300 ports = _mappings.values()
301 if len(set(bridges)) != len(bridges):
302 raise Exception("It is not allowed to have more than one port "
303 "configured on the same bridge")
304
305 if len(set(ports)) != len(ports):318 if len(set(ports)) != len(ports):
306 raise Exception("It is not allowed to have the same port configured "319 raise Exception("It is not allowed to have the same port configured "
307 "on more than one bridge")320 "on more than one bridge")
308321
=== modified file 'charmhelpers/core/host.py'
--- charmhelpers/core/host.py 2015-08-17 11:15:38 +0000
+++ charmhelpers/core/host.py 2015-08-18 11:36:40 +0000
@@ -417,25 +417,59 @@
417 return(''.join(random_chars))417 return(''.join(random_chars))
418418
419419
420def list_nics(nic_type):420def is_phy_iface(interface):
421 """Returns True if interface is not virtual, otherwise False."""
422 if interface:
423 sys_net = '/sys/class/net'
424 if os.path.isdir(sys_net):
425 for iface in glob.glob(os.path.join(sys_net, '*')):
426 if '/virtual/' in os.path.realpath(iface):
427 continue
428
429 if interface == os.path.basename(iface):
430 return True
431
432 return False
433
434
435def list_nics(nic_type=None):
421 '''Return a list of nics of given type(s)'''436 '''Return a list of nics of given type(s)'''
422 if isinstance(nic_type, six.string_types):437 if isinstance(nic_type, six.string_types):
423 int_types = [nic_type]438 int_types = [nic_type]
424 else:439 else:
425 int_types = nic_type440 int_types = nic_type
441
426 interfaces = []442 interfaces = []
427 for int_type in int_types:443 if nic_type:
428 cmd = ['ip', 'addr', 'show', 'label', int_type + '*']444 for int_type in int_types:
445 cmd = ['ip', 'addr', 'show', 'label', int_type + '*']
446 ip_output = subprocess.check_output(cmd).decode('UTF-8')
447 ip_output = ip_output.split('\n')
448 ip_output = (line for line in ip_output if line)
449 for line in ip_output:
450 if line.split()[1].startswith(int_type):
451 matched = re.search('.*: (' + int_type +
452 r'[0-9]+\.[0-9]+)@.*', line)
453 if matched:
454 iface = matched.groups()[0]
455 else:
456 iface = line.split()[1].replace(":", "")
457
458 if iface not in interfaces:
459 interfaces.append(iface)
460 else:
461 cmd = ['ip', 'a']
429 ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')462 ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
430 ip_output = (line for line in ip_output if line)463 ip_output = (line.strip() for line in ip_output if line)
464
465 key = re.compile('^[0-9]+:\s+(.+):')
431 for line in ip_output:466 for line in ip_output:
432 if line.split()[1].startswith(int_type):467 matched = re.search(key, line)
433 matched = re.search('.*: (' + int_type + r'[0-9]+\.[0-9]+)@.*', line)468 if matched:
434 if matched:469 iface = matched.group(1)
435 interface = matched.groups()[0]470 iface = iface.partition("@")[0]
436 else:471 if iface not in interfaces:
437 interface = line.split()[1].replace(":", "")472 interfaces.append(iface)
438 interfaces.append(interface)
439473
440 return interfaces474 return interfaces
441475
442476
=== modified file 'tests/contrib/openstack/test_neutron_utils.py'
--- tests/contrib/openstack/test_neutron_utils.py 2015-05-02 22:27:26 +0000
+++ tests/contrib/openstack/test_neutron_utils.py 2015-08-18 11:36:40 +0000
@@ -181,13 +181,13 @@
181 ret = neutron.parse_data_port_mappings(None)181 ret = neutron.parse_data_port_mappings(None)
182 self.assertEqual(ret, {})182 self.assertEqual(ret, {})
183 ret = neutron.parse_data_port_mappings('br0:eth0')183 ret = neutron.parse_data_port_mappings('br0:eth0')
184 self.assertEqual(ret, {'br0': 'eth0'})184 self.assertEqual(ret, {'eth0': 'br0'})
185 # Back-compat test185 # Back-compat test
186 ret = neutron.parse_data_port_mappings('eth0', default_bridge='br0')186 ret = neutron.parse_data_port_mappings('eth0', default_bridge='br0')
187 self.assertEqual(ret, {'br0': 'eth0'})187 self.assertEqual(ret, {'eth0': 'br0'})
188 # Multiple mappings188 # Multiple mappings
189 ret = neutron.parse_data_port_mappings('br0:eth0 br1:eth1')189 ret = neutron.parse_data_port_mappings('br0:eth0 br1:eth1')
190 self.assertEqual(ret, {'br0': 'eth0', 'br1': 'eth1'})190 self.assertEqual(ret, {'eth0': 'br0', 'eth1': 'br1'})
191191
192 def test_parse_vlan_range_mappings(self):192 def test_parse_vlan_range_mappings(self):
193 ret = neutron.parse_vlan_range_mappings(None)193 ret = neutron.parse_vlan_range_mappings(None)
194194
=== modified file 'tests/contrib/openstack/test_os_contexts.py'
--- tests/contrib/openstack/test_os_contexts.py 2015-07-29 14:21:45 +0000
+++ tests/contrib/openstack/test_os_contexts.py 2015-08-18 11:36:40 +0000
@@ -2425,6 +2425,8 @@
2425 self.assertEquals(context.ExternalPortContext()(),2425 self.assertEquals(context.ExternalPortContext()(),
2426 {'ext_port': 'eth1010'})2426 {'ext_port': 'eth1010'})
24272427
2428 @patch('charmhelpers.contrib.openstack.context.is_phy_iface',
2429 lambda arg: True)
2428 @patch('charmhelpers.contrib.openstack.context.get_nic_hwaddr')2430 @patch('charmhelpers.contrib.openstack.context.get_nic_hwaddr')
2429 @patch('charmhelpers.contrib.openstack.context.list_nics')2431 @patch('charmhelpers.contrib.openstack.context.list_nics')
2430 @patch('charmhelpers.contrib.openstack.context.get_ipv6_addr')2432 @patch('charmhelpers.contrib.openstack.context.get_ipv6_addr')
@@ -2452,6 +2454,8 @@
24522454
2453 self.assertEquals(context.ExternalPortContext()(), {})2455 self.assertEquals(context.ExternalPortContext()(), {})
24542456
2457 @patch('charmhelpers.contrib.openstack.context.is_phy_iface',
2458 lambda arg: True)
2455 @patch('charmhelpers.contrib.openstack.context.get_nic_hwaddr')2459 @patch('charmhelpers.contrib.openstack.context.get_nic_hwaddr')
2456 @patch('charmhelpers.contrib.openstack.context.list_nics')2460 @patch('charmhelpers.contrib.openstack.context.list_nics')
2457 @patch('charmhelpers.contrib.openstack.context.get_ipv6_addr')2461 @patch('charmhelpers.contrib.openstack.context.get_ipv6_addr')
@@ -2484,8 +2488,9 @@
2484 'resolve_ports')2488 'resolve_ports')
2485 def test_data_port_eth(self, mock_resolve):2489 def test_data_port_eth(self, mock_resolve):
2486 self.config.side_effect = fake_config({'data-port':2490 self.config.side_effect = fake_config({'data-port':
2487 'phybr1:eth1010'})2491 'phybr1:eth1010 '
2488 mock_resolve.side_effect = lambda ports: ports2492 'phybr1:eth1011'})
2493 mock_resolve.side_effect = lambda ports: ['eth1010']
2489 self.assertEquals(context.DataPortContext()(),2494 self.assertEquals(context.DataPortContext()(),
2490 {'phybr1': 'eth1010'})2495 {'phybr1': 'eth1010'})
24912496
24922497
=== modified file 'tests/core/test_host.py'
--- tests/core/test_host.py 2015-08-17 11:15:38 +0000
+++ tests/core/test_host.py 2015-08-18 11:36:40 +0000
@@ -35,6 +35,11 @@
35 link/ether e4:11:5b:ab:a7:3c brd ff:ff:ff:ff:ff:ff35 link/ether e4:11:5b:ab:a7:3c brd ff:ff:ff:ff:ff:ff
36"""36"""
3737
38IP_LINE_ETH100 = b"""
392: eth100: <BROADCAST,MULTICAST,SLAVE,UP,LOWER_UP> mtu 1500 qdisc mq master bond0 state UP qlen 1000
40 link/ether e4:11:5b:ab:a7:3d brd ff:ff:ff:ff:ff:ff
41"""
42
38IP_LINE_ETH0_VLAN = b"""43IP_LINE_ETH0_VLAN = b"""
396: eth0.10@eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default446: eth0.10@eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
40 link/ether 08:00:27:16:b9:5f brd ff:ff:ff:ff:ff:ff45 link/ether 08:00:27:16:b9:5f brd ff:ff:ff:ff:ff:ff
@@ -47,7 +52,7 @@
4752
48IP_LINE_HWADDR = b"""2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000\ link/ether e4:11:5b:ab:a7:3c brd ff:ff:ff:ff:ff:ff"""53IP_LINE_HWADDR = b"""2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000\ link/ether e4:11:5b:ab:a7:3c brd ff:ff:ff:ff:ff:ff"""
4954
50IP_LINES = IP_LINE_ETH0 + IP_LINE_ETH1 + IP_LINE_ETH0_VLAN55IP_LINES = IP_LINE_ETH0 + IP_LINE_ETH1 + IP_LINE_ETH0_VLAN + IP_LINE_ETH100
5156
52IP_LINE_BONDS = b"""57IP_LINE_BONDS = b"""
536: bond0.10@bond0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default586: bond0.10@bond0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
@@ -962,13 +967,36 @@
962 pw2 = host.pwgen(10)967 pw2 = host.pwgen(10)
963 self.assertNotEqual(pw, pw2, 'Duplicated password')968 self.assertNotEqual(pw, pw2, 'Duplicated password')
964969
970 @patch.object(host, 'glob')
971 @patch('os.path.realpath')
972 @patch('os.path.isdir')
973 def test_is_phy_iface(self, mock_isdir, mock_realpath, mock_glob):
974 mock_isdir.return_value = True
975 mock_glob.glob.return_value = ['/sys/class/net/eth0',
976 '/sys/class/net/veth0']
977
978 def fake_realpath(soft):
979 if soft.endswith('/eth0'):
980 hard = \
981 '/sys/devices/pci0000:00/0000:00:1c.4/0000:02:00.1/net/eth0'
982 else:
983 hard = '/sys/devices/virtual/net/veth0'
984
985 return hard
986
987 mock_realpath.side_effect = fake_realpath
988 self.assertTrue(host.is_phy_iface('eth0'))
989 self.assertFalse(host.is_phy_iface('veth0'))
990
965 @patch('subprocess.check_output')991 @patch('subprocess.check_output')
966 def test_list_nics(self, check_output):992 def test_list_nics(self, check_output):
967 check_output.return_value = IP_LINES993 check_output.return_value = IP_LINES
994 nics = host.list_nics()
995 self.assertEqual(nics, ['eth0', 'eth1', 'eth0.10', 'eth100'])
968 nics = host.list_nics('eth')996 nics = host.list_nics('eth')
969 self.assertEqual(nics, ['eth0', 'eth1', 'eth0.10'])997 self.assertEqual(nics, ['eth0', 'eth1', 'eth0.10', 'eth100'])
970 nics = host.list_nics(['eth'])998 nics = host.list_nics(['eth'])
971 self.assertEqual(nics, ['eth0', 'eth1', 'eth0.10'])999 self.assertEqual(nics, ['eth0', 'eth1', 'eth0.10', 'eth100'])
9721000
973 @patch('subprocess.check_output')1001 @patch('subprocess.check_output')
974 def test_list_nics_with_bonds(self, check_output):1002 def test_list_nics_with_bonds(self, check_output):

Subscribers

People subscribed via source and target branches