Add NIC Mapping Reporting Feature

The -i or --interfaces flag will print out a dict of the NIC
mappings if no interfaces are specified, or it will print a
list of the real interface names that match the interfaces
given. If a real interface name is given, it will be returned
in the list without translation only if it is live.

It has always been technically possible to map an interface
using an alias that matches an inactive real interface. This
change preserves that functionality, but will prevent mapping
using the name of an active interface. A warning will be
printed if an alias matches an inactive inteface.

Also change _is_active_nic and _mapped_nics to public functions
since they are called outside of utils.

Change-Id: I74af5391165e10f04800ea05e4204a7d1f74f526
Closes-bug: 1713137
This commit is contained in:
Dan Sneddon 2016-10-06 17:54:45 -07:00
parent 69fca4aa70
commit ebc1c1bc80
9 changed files with 192 additions and 50 deletions

View File

@ -0,0 +1,5 @@
interface_mapping:
nic1: em1
nic2: em2
nic3: em4
nic4: em3

View File

@ -273,7 +273,7 @@ class NetConfig(object):
msg = 'renaming %s to %s: ' % (oldname, newname)
# ifdown isn't enough when renaming, we need the link down
for name in (oldname, newname):
if utils._is_active_nic(name):
if utils.is_active_nic(name):
self.execute(msg, '/sbin/ip',
'link', 'set', 'dev', name, 'down')
self.execute(msg, '/sbin/ip',

View File

@ -42,8 +42,14 @@ def parse_opts(argv):
parser.add_argument('-m', '--mapping-file', metavar='MAPPING_FILE',
help="""path to the interface mapping file.""",
default='/etc/os-net-config/mapping.yaml')
parser.add_argument('-i', '--interfaces', metavar='INTERFACES',
help="""Identify the real interface for a nic name. """
"""If a real name is given, it is returned if live. """
"""If no value is given, display full NIC mapping. """
"""Exit after printing, ignoring other parameters. """,
nargs='*', default=None)
parser.add_argument('-p', '--provider', metavar='PROVIDER',
help="""The provider to use."""
help="""The provider to use. """
"""One of: ifcfg, eni, iproute.""",
default=None)
parser.add_argument('-r', '--root-dir', metavar='ROOT_DIR',
@ -53,7 +59,7 @@ def parse_opts(argv):
action='store_true',
help="""Enable detailed exit codes. """
"""If enabled an exit code of '2' means """
"""that files were modified."""
"""that files were modified. """
"""Disabled by default.""",
default=False)
@ -160,19 +166,6 @@ def main(argv=sys.argv):
logger.error('Unable to set provider for this operating system.')
return 1
# Read config file containing network configs to apply
if os.path.exists(opts.config_file):
with open(opts.config_file) as cf:
iface_array = yaml.load(cf.read()).get("network_config")
logger.debug('network_config JSON: %s' % str(iface_array))
else:
logger.error('No config file exists at: %s' % opts.config_file)
return 1
if not isinstance(iface_array, list):
logger.error('No interfaces defined in config: %s' % opts.config_file)
return 1
# Read the interface mapping file, if it exists
# This allows you to override the default network naming abstraction
# mappings by specifying a specific nicN->name or nicN->MAC mapping
@ -187,6 +180,59 @@ def main(argv=sys.argv):
iface_mapping = None
persist_mapping = False
# If --interfaces is specified, either return the real name of the
# interfaces specified, or return the map of all nic abstractions/names.
if opts.interfaces is not None:
reported_nics = {}
mapped_nics = objects.mapped_nics(iface_mapping)
retval = 0
if len(opts.interfaces) > 0:
for requested_nic in opts.interfaces:
found = False
# Check to see if requested iface is a mapped NIC name.
if requested_nic in mapped_nics:
reported_nics[requested_nic] = mapped_nics[requested_nic]
found = True
# Check to see if the requested iface is a real NIC name
if requested_nic in mapped_nics.values():
if found is True: # Name matches alias and real NIC
# (return the mapped NIC, but warn of overlap).
logger.warning('"%s" overlaps with real NIC name.'
% (requested_nic))
else:
reported_nics[requested_nic] = requested_nic
found = True
if not found:
retval = 1
if reported_nics:
logger.debug("Interface mapping requested for interface: "
"%s" % reported_nics.keys())
else:
logger.debug("Interface mapping requested for all interfaces")
reported_nics = mapped_nics
# Return the report on the mapped NICs. If all NICs were found, exit
# cleanly, otherwise exit with status 1.
logger.debug("Interface report requested, exiting after report.")
print(reported_nics)
return retval
# Read config file containing network configs to apply
if os.path.exists(opts.config_file):
try:
with open(opts.config_file) as cf:
iface_array = yaml.load(cf.read()).get("network_config")
logger.debug('network_config JSON: %s' % str(iface_array))
except IOError:
logger.error("Error reading file: %s" % opts.config_file)
return 1
else:
logger.error('No config file exists at: %s' % opts.config_file)
return 1
if not isinstance(iface_array, list):
logger.error('No interfaces defined in config: %s' % opts.config_file)
return 1
for iface_json in iface_array:
iface_json.update({'nic_mapping': iface_mapping})
iface_json.update({'persist_mapping': persist_mapping})

View File

@ -112,7 +112,7 @@ def _update_members(json, nic_mapping, persist_mapping):
return members
def _mapped_nics(nic_mapping=None):
def mapped_nics(nic_mapping=None):
mapping = nic_mapping or {}
global _MAPPED_NICS
if _MAPPED_NICS:
@ -155,6 +155,17 @@ def _mapped_nics(nic_mapping=None):
% nic_mapped)
raise InvalidConfigException(msg)
# Using a mapping name that overlaps with a real NIC is not allowed
# (However using the name of an inactive NIC as an alias is
# permitted).
if utils.is_active_nic(nic_alias):
msg = ('cannot map %s to alias %s, alias overlaps with active '
'NIC.' % (nic_mapped, nic_alias))
raise InvalidConfigException(msg)
elif utils.is_real_nic(nic_alias):
logger.warning("Mapped nic %s overlaps with name of inactive "
"NIC." % (nic_alias))
_MAPPED_NICS[nic_alias] = nic_mapped
logger.info("%s in mapping file mapped to: %s"
% (nic_alias, nic_mapped))
@ -230,7 +241,7 @@ class _BaseOpts(object):
addresses = addresses or []
routes = routes or []
dns_servers = dns_servers or []
mapped_nic_names = _mapped_nics(nic_mapping)
mapped_nic_names = mapped_nics(nic_mapping)
self.hwaddr = None
self.hwname = None
self.renamed = False
@ -379,7 +390,7 @@ class Vlan(_BaseOpts):
dns_servers, nm_controlled)
self.vlan_id = int(vlan_id)
mapped_nic_names = _mapped_nics(nic_mapping)
mapped_nic_names = mapped_nics(nic_mapping)
if device in mapped_nic_names:
self.device = mapped_nic_names[device]
else:

View File

@ -41,7 +41,7 @@ class TestCase(testtools.TestCase):
def dummy_mapped_nics(nic_mapping=None):
return self.stubbed_mapped_nics
if self.stub_mapped_nics:
self.stubs.Set(objects, '_mapped_nics', dummy_mapped_nics)
self.stubs.Set(objects, 'mapped_nics', dummy_mapped_nics)
test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
try:

View File

@ -16,10 +16,12 @@
import os.path
import sys
import yaml
import os_net_config
from os_net_config import cli
from os_net_config import impl_ifcfg
from os_net_config import objects
from os_net_config.tests import base
import six
@ -235,3 +237,39 @@ class TestCli(base.TestCase):
for dev in sanity_devices:
self.assertIn(dev, stdout_yaml)
self.assertEqual(stdout_yaml, stdout_json)
def test_nic_mapping_report_output(self):
mapping_report = os.path.join(SAMPLE_BASE, 'mapping_report.yaml')
def dummy_mapped_nics(nic_mapping=None):
return nic_mapping
self.stubs.Set(objects, 'mapped_nics', dummy_mapped_nics)
stdout, stderr = self.run_cli('ARG0 --interfaces '
'--exit-on-validation-errors '
'-m %s' % mapping_report)
self.assertEqual('', stderr)
stdout_list = yaml.load(stdout)
self.assertEqual(stdout_list['nic1'], 'em1')
self.assertEqual(stdout_list['nic2'], 'em2')
self.assertEqual(stdout_list['nic3'], 'em4')
self.assertEqual(stdout_list['nic4'], 'em3')
def test_nic_mapping_report_with_explicit_interface_name(self):
mapping_report = os.path.join(SAMPLE_BASE, 'mapping_report.yaml')
def dummy_mapped_nics(nic_mapping=None):
return nic_mapping
self.stubs.Set(objects, 'mapped_nics', dummy_mapped_nics)
stdout, stderr = self.run_cli('ARG0 --interfaces em2 em3 '
'--exit-on-validation-errors '
'-m %s' % mapping_report)
self.assertEqual('', stderr)
stdout_list = yaml.load(stdout)
self.assertNotIn('em1', stdout_list.keys())
self.assertNotIn('em1', stdout_list.values())
self.assertEqual(stdout_list['em2'], 'em2')
self.assertEqual(stdout_list['em3'], 'em3')
self.assertNotIn('em4', stdout_list.keys())
self.assertNotIn('em4', stdout_list.values())

View File

@ -198,7 +198,7 @@ class TestInterface(base.TestCase):
def test_from_json_dhcp_nic1(self):
def dummy_mapped_nics(nic_mapping=None):
return {"nic1": "em3"}
self.stubs.Set(objects, '_mapped_nics', dummy_mapped_nics)
self.stubs.Set(objects, 'mapped_nics', dummy_mapped_nics)
data = '{"type": "interface", "name": "nic1", "use_dhcp": true}'
interface = objects.object_from_json(json.loads(data))
@ -250,7 +250,7 @@ class TestVlan(base.TestCase):
def test_from_json_dhcp_nic1(self):
def dummy_mapped_nics(nic_mapping=None):
return {"nic1": "em4"}
self.stubs.Set(objects, '_mapped_nics', dummy_mapped_nics)
self.stubs.Set(objects, 'mapped_nics', dummy_mapped_nics)
data = '{"type": "vlan", "device": "nic1", "vlan_id": 16,' \
'"use_dhcp": true}'
@ -284,7 +284,7 @@ class TestBridge(base.TestCase):
def test_from_json_dhcp_with_nic1(self):
def dummy_mapped_nics(nic_mapping=None):
return {"nic1": "em5"}
self.stubs.Set(objects, '_mapped_nics', dummy_mapped_nics)
self.stubs.Set(objects, 'mapped_nics', dummy_mapped_nics)
data = """{
"type": "ovs_bridge",
@ -388,7 +388,7 @@ class TestLinuxBridge(base.TestCase):
def test_from_json_dhcp_with_nic1(self):
def dummy_mapped_nics(nic_mapping=None):
return {"nic1": "em5"}
self.stubs.Set(objects, '_mapped_nics', dummy_mapped_nics)
self.stubs.Set(objects, 'mapped_nics', dummy_mapped_nics)
data = """{
"type": "linux_bridge",
@ -595,7 +595,7 @@ class TestBond(base.TestCase):
def dummy_mapped_nics(nic_mapping=None):
return {"nic1": "em1", "nic2": "em2"}
self.stubs.Set(objects, '_mapped_nics', dummy_mapped_nics)
self.stubs.Set(objects, 'mapped_nics', dummy_mapped_nics)
data = """{
"type": "ovs_bond",
@ -686,7 +686,7 @@ class TestLinuxBond(base.TestCase):
def dummy_mapped_nics(nic_mapping=None):
return {"nic1": "em1", "nic2": "em2"}
self.stubs.Set(objects, '_mapped_nics', dummy_mapped_nics)
self.stubs.Set(objects, 'mapped_nics', dummy_mapped_nics)
data = """{
"type": "ovs_bond",
@ -873,7 +873,7 @@ class TestIbInterface(base.TestCase):
def test_from_json_dhcp_nic1(self):
def dummy_mapped_nics(nic_mapping=None):
return {"nic1": "ib0"}
self.stubs.Set(objects, '_mapped_nics', dummy_mapped_nics)
self.stubs.Set(objects, 'mapped_nics', dummy_mapped_nics)
data = '{"type": "ib_interface", "name": "nic1", "use_dhcp": true}'
ib_interface = objects.object_from_json(json.loads(data))
@ -933,28 +933,28 @@ class TestNicMapping(base.TestCase):
def test_mapped_nics_default(self):
self._stub_active_nics(['em1', 'em2'])
expected = {'nic1': 'em1', 'nic2': 'em2'}
self.assertEqual(expected, objects._mapped_nics())
self.assertEqual(expected, objects.mapped_nics())
def test_mapped_nics_mapped(self):
self._stub_active_nics(['em1', 'em2'])
self._stub_available_nics(['em1', 'em2'])
mapping = {'nic1': 'em2', 'nic2': 'em1'}
expected = {'nic1': 'em2', 'nic2': 'em1'}
self.assertEqual(expected, objects._mapped_nics(nic_mapping=mapping))
self.assertEqual(expected, objects.mapped_nics(nic_mapping=mapping))
def test_mapped_nics_mapped_partial(self):
self._stub_active_nics(['em1', 'em2', 'em3', 'em4'])
self._stub_available_nics(['em1', 'em2', 'em3', 'em4'])
mapping = {'nic1': 'em2', 'nic2': 'em1'}
expected = {'nic1': 'em2', 'nic2': 'em1', 'nic3': 'em3', 'nic4': 'em4'}
self.assertEqual(expected, objects._mapped_nics(nic_mapping=mapping))
self.assertEqual(expected, objects.mapped_nics(nic_mapping=mapping))
def test_mapped_nics_mapped_partial_reordered(self):
self._stub_active_nics(['em1', 'em2', 'em3', 'em4'])
self._stub_available_nics(['em1', 'em2', 'em3', 'em4'])
mapping = {'nic1': 'em1', 'nic2': 'em3'}
expected = {'nic1': 'em1', 'nic2': 'em3', 'nic4': 'em4'}
self.assertEqual(expected, objects._mapped_nics(nic_mapping=mapping))
self.assertEqual(expected, objects.mapped_nics(nic_mapping=mapping))
def test_mapped_nics_mapped_unnumbered(self):
self._stub_active_nics(['em1', 'em2', 'em3', 'em4'])
@ -962,21 +962,21 @@ class TestNicMapping(base.TestCase):
mapping = {'John': 'em1', 'Paul': 'em2', 'George': 'em3'}
expected = {'John': 'em1', 'Paul': 'em2', 'George': 'em3',
'nic4': 'em4'}
self.assertEqual(expected, objects._mapped_nics(nic_mapping=mapping))
self.assertEqual(expected, objects.mapped_nics(nic_mapping=mapping))
def test_mapped_nics_map_error_notactive(self):
self._stub_active_nics(['em2'])
self._stub_available_nics(['em1', 'em2', 'em3'])
mapping = {'nic2': 'em1'}
expected = {'nic1': 'em2', 'nic2': 'em1'}
self.assertEqual(expected, objects._mapped_nics(nic_mapping=mapping))
self.assertEqual(expected, objects.mapped_nics(nic_mapping=mapping))
def test_mapped_nics_map_error_duplicate(self):
self._stub_active_nics(['em1', 'em2'])
self._stub_available_nics(['em1', 'em2'])
mapping = {'nic1': 'em1', 'nic2': 'em1'}
err = self.assertRaises(objects.InvalidConfigException,
objects._mapped_nics, nic_mapping=mapping)
objects.mapped_nics, nic_mapping=mapping)
expected = 'em1 already mapped, check mapping file for duplicates'
self.assertIn(expected, six.text_type(err))
@ -985,7 +985,7 @@ class TestNicMapping(base.TestCase):
self._stub_available_nics(['em1', 'em2'])
mapping = {'nic1': 'em1', 'nic2': 'foo'}
expected = {'nic1': 'em1'}
self.assertEqual(expected, objects._mapped_nics(nic_mapping=mapping))
self.assertEqual(expected, objects.mapped_nics(nic_mapping=mapping))
def test_mapped_nics_map_mac(self):
def dummy_interface_mac(name):
@ -997,7 +997,7 @@ class TestNicMapping(base.TestCase):
self._stub_available_nics(['em1', 'em2'])
mapping = {'nic1': '12:34:56:de:f0:12', 'nic2': '12:34:56:78:9a:bc'}
expected = {'nic1': 'em2', 'nic2': 'em1'}
self.assertEqual(expected, objects._mapped_nics(nic_mapping=mapping))
self.assertEqual(expected, objects.mapped_nics(nic_mapping=mapping))
def test_mapped_nics_map_invalid_mac(self):
def dummy_interface_mac(name):
@ -1010,13 +1010,43 @@ class TestNicMapping(base.TestCase):
self._stub_available_nics(['em1', 'em2'])
mapping = {'nic1': '12:34:56:de:f0:12', 'nic2': 'aa:bb:cc:dd:ee:ff'}
expected = {'nic1': 'em2'}
self.assertEqual(expected, objects._mapped_nics(nic_mapping=mapping))
self.assertEqual(expected, objects.mapped_nics(nic_mapping=mapping))
def test_mapped_nics_no_active(self):
self._stub_active_nics([])
expected = {}
# This only emits a warning, so it should still work
self.assertEqual(expected, objects._mapped_nics())
self.assertEqual(expected, objects.mapped_nics())
def test_mapped_nics_mapping_overlap_real_nic_name(self):
def dummy_is_active_nic(nic):
if nic == 'em1':
return True
elif nic == 'nic1':
return False
self.stubs.Set(utils, 'is_active_nic', dummy_is_active_nic)
self._stub_available_nics(['em1', 'em2'])
mapping = {'nic1': 'em1', 'em1': 'em2'}
err = self.assertRaises(objects.InvalidConfigException,
objects.mapped_nics, nic_mapping=mapping)
expected = 'cannot map em2 to alias em1, alias overlaps'
self.assertIn(expected, six.text_type(err))
def test_mapped_nics_mapping_inactive_name_as_alias(self):
def dummy_is_active_nic(nic):
return False
def dummy_is_real_nic(nic):
return True
self.stubs.Set(utils, 'is_active_nic', dummy_is_active_nic)
self.stubs.Set(utils, 'is_real_nic', dummy_is_real_nic)
self._stub_active_nics([])
self._stub_available_nics(['em1', 'em2'])
mapping = {'em2': 'em1', 'nic1': 'em2'}
expected = {'em2': 'em1', 'nic1': 'em2'}
self.assertEqual(expected, objects.mapped_nics(nic_mapping=mapping))
# Test that mapping file is passed to interface members from parent object
def _test_mapped_nics_with_parent(self, type, name):

View File

@ -333,8 +333,8 @@ class TestUtils(base.TestCase):
nic_path = os.path.join(tmpdir, 'enp129s2', 'device', 'physfn')
os.makedirs(nic_path)
self.assertEqual(utils._is_active_nic('ens802f0'), True)
self.assertEqual(utils._is_active_nic('enp129s2'), False)
self.assertEqual(utils.is_active_nic('ens802f0'), True)
self.assertEqual(utils.is_active_nic('enp129s2'), False)
shutil.rmtree(tmpdir)

View File

@ -99,18 +99,36 @@ def interface_mac(name):
raise
def _is_active_nic(interface_name):
def is_active_nic(interface_name):
return _is_available_nic(interface_name, True)
def is_real_nic(interface_name):
if interface_name == 'lo':
return True
device_dir = _SYS_CLASS_NET + '/%s/device' % interface_name
has_device_dir = os.path.isdir(device_dir)
address = None
try:
with open(_SYS_CLASS_NET + '/%s/address' % interface_name, 'r') as f:
address = f.read().rstrip()
except IOError:
return False
if has_device_dir and address:
return True
else:
return False
def _is_available_nic(interface_name, check_active=True):
try:
if interface_name == 'lo':
return False
device_dir = _SYS_CLASS_NET + '/%s/device' % interface_name
has_device_dir = os.path.isdir(device_dir)
if not has_device_dir:
if not is_real_nic(interface_name):
return False
operstate = None
@ -119,12 +137,6 @@ def _is_available_nic(interface_name, check_active=True):
if check_active and operstate != 'up':
return False
address = None
with open(_SYS_CLASS_NET + '/%s/address' % interface_name, 'r') as f:
address = f.read().rstrip()
if not address:
return False
# If SR-IOV Virtual Functions (VF) are enabled in an interface, there
# will be additional nics created for each VF. It has to be ignored in
# the nic numbering. All the VFs will have a reference to the PF with