diff --git a/README.md b/README.md index dc4599b..57a1aad 100644 --- a/README.md +++ b/README.md @@ -151,18 +151,22 @@ to match a non-virtual device with the profile. ### `interface_name` -For type `ethernet`, this option restricts the profile to the -given interface by name. This argument is optional and by default -a profile is not restricted to any interface by name. -Note that with [persistent interface naming](https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/7/html/Networking_Guide/ch-Consistent_Network_Device_Naming.html), +For type `ethernet`, this option restricts the profile to the given interface +by name. This argument is optional and by default the profile name is used +unless a mac address is specified using the `mac` key. Specifying an empty +string (`""`) allows to specify that the profile is not restricted to a network +interface. + + +**Note:** With [persistent interface naming](https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/7/html/Networking_Guide/ch-Consistent_Network_Device_Naming.html), the interface is predictable based on the hardware configuration. Otherwise, the `mac` address might be an option. For virtual interface types like bridges, this argument is the name of the created interface. In case of a missing `interface_name`, the profile name `name` is used. -Note the destinction between the profile name `name` and the device -name `interface_name`, which may or may not be the same. +**Note:** The profile name `name` and the device name `interface_name` may be +different or the profile may not be tied to an interface at all. ### `zone` diff --git a/library/network_connections.py b/library/network_connections.py index d57494b..4da3ae8 100644 --- a/library/network_connections.py +++ b/library/network_connections.py @@ -1080,7 +1080,7 @@ class ArgValidator_DictConnection(ArgValidatorDict): enum_values=ArgValidator_DictConnection.VALID_SLAVE_TYPES, ), ArgValidatorStr("master"), - ArgValidatorStr("interface_name"), + ArgValidatorStr("interface_name", allow_empty=True), ArgValidatorMac("mac"), ArgValidatorNum( "mtu", val_min=0, val_max=0xFFFFFFFF, default_value=None @@ -1291,21 +1291,36 @@ class ArgValidator_DictConnection(ArgValidatorDict): ) if "interface_name" in result: - if not Util.ifname_valid(result["interface_name"]): + # Ignore empty interface_name + if result["interface_name"] == "": + del result["interface_name"] + elif not Util.ifname_valid(result["interface_name"]): raise ValidationError( name + ".interface_name", "invalid 'interface_name' '%s'" % (result["interface_name"]), ) else: - if result["type"] in ["bridge", "bond", "team", "vlan", "macvlan"]: + if not result.get("mac"): if not Util.ifname_valid(result["name"]): raise ValidationError( name + ".interface_name", - 'requires \'interface_name\' as "name" "%s" is not valid' + '\'interface_name\' as "name" "%s" is not valid' % (result["name"]), ) result["interface_name"] = result["name"] + if "interface_name" not in result and result["type"] in [ + "bond", + "bridge", + "macvlan", + "team", + "vlan", + ]: + raise ValidationError( + name + ".interface_name", + "type '%s' requires 'interface_name'" % (result["type"]), + ) + if result["type"] == "vlan": if "vlan" not in result: if "vlan_id" not in result: diff --git a/tests/test_network_connections.py b/tests/test_network_connections.py index e25a8f4..a386044 100755 --- a/tests/test_network_connections.py +++ b/tests/test_network_connections.py @@ -2,11 +2,12 @@ """ Tests for network_connections Ansible module """ # SPDX-License-Identifier: BSD-3-Clause -import sys -import os -import unittest -import socket import itertools +import os +import pprint as pprint_ +import socket +import sys +import unittest TESTS_BASEDIR = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(1, os.path.join(TESTS_BASEDIR, "..", "library")) @@ -43,9 +44,8 @@ if nmutil: def pprint(msg, obj): print("PRINT: %s\n" % (msg)) - import pprint - p = pprint.PrettyPrinter(indent=4) + p = pprint_.PrettyPrinter(indent=4) p.pprint(obj) if nmutil is not None and isinstance(obj, NM.Connection): obj.dump() @@ -281,7 +281,7 @@ class TestValidator(unittest.TestCase): "zone": None, "master": None, "ignore_errors": None, - "interface_name": None, + "interface_name": "5", "check_iface_exists": True, "slave_type": None, }, @@ -324,7 +324,7 @@ class TestValidator(unittest.TestCase): "zone": None, "master": None, "ignore_errors": None, - "interface_name": None, + "interface_name": "5", "check_iface_exists": True, "force_state_change": None, "slave_type": None, @@ -362,7 +362,7 @@ class TestValidator(unittest.TestCase): "zone": None, "master": None, "ignore_errors": None, - "interface_name": None, + "interface_name": "5", "check_iface_exists": True, "force_state_change": None, "slave_type": None, @@ -379,6 +379,7 @@ class TestValidator(unittest.TestCase): "NM_CONTROLLED": "no", "ONBOOT": "no", "TYPE": "Ethernet", + "DEVICE": "5", }, "keys": None, "route": None, @@ -488,7 +489,7 @@ class TestValidator(unittest.TestCase): "master": None, "mtu": None, "ignore_errors": None, - "interface_name": None, + "interface_name": "prod1", "type": "ethernet", "slave_type": None, "wait": None, @@ -1185,7 +1186,7 @@ class TestValidator(unittest.TestCase): "zone": None, "master": None, "ignore_errors": None, - "interface_name": None, + "interface_name": "5", "check_iface_exists": True, "force_state_change": None, "slave_type": None, @@ -1224,7 +1225,7 @@ class TestValidator(unittest.TestCase): "zone": None, "master": None, "ignore_errors": None, - "interface_name": None, + "interface_name": "5", "check_iface_exists": True, "force_state_change": None, "slave_type": None, @@ -1250,6 +1251,7 @@ class TestValidator(unittest.TestCase): "NM_CONTROLLED": "no", "ONBOOT": "yes", "TYPE": "Ethernet", + "DEVICE": "5", }, "keys": None, "route": None, @@ -1301,7 +1303,7 @@ class TestValidator(unittest.TestCase): "ethernet": {"autoneg": None, "duplex": None, "speed": 0}, "force_state_change": None, "ignore_errors": None, - "interface_name": None, + "interface_name": "6643", "ip": { "address": [], "auto6": True, @@ -1376,7 +1378,14 @@ class TestValidator(unittest.TestCase): "zone": None, } ], - [{"name": "infiniband.1", "state": "up", "type": "infiniband"}], + [ + { + "name": "infiniband.1", + "interface_name": "", + "state": "up", + "type": "infiniband", + } + ], initscripts_dict_expected=[ { "ifcfg": { @@ -1512,7 +1521,7 @@ class TestValidator(unittest.TestCase): "zone": None, "master": None, "ignore_errors": None, - "interface_name": None, + "interface_name": "555", "check_iface_exists": True, "force_state_change": None, "slave_type": None, @@ -1543,6 +1552,7 @@ class TestValidator(unittest.TestCase): "NM_CONTROLLED": "no", "ONBOOT": "yes", "TYPE": "Ethernet", + "DEVICE": "555", }, "keys": None, "route": "192.168.45.0/24 metric 545\n192.168.46.0/30\n", @@ -1604,7 +1614,7 @@ class TestValidator(unittest.TestCase): "zone": "external", "master": None, "ignore_errors": None, - "interface_name": None, + "interface_name": "e556", "check_iface_exists": True, "force_state_change": None, "slave_type": None, @@ -1666,6 +1676,7 @@ class TestValidator(unittest.TestCase): "ONBOOT": "yes", "TYPE": "Ethernet", "ZONE": "external", + "DEVICE": "e556", }, "keys": None, "route": "192.168.40.0/24 metric 545\n192.168.46.0/30\n" @@ -1681,6 +1692,80 @@ class TestValidator(unittest.TestCase): [{"name": "b", "type": "ethernet", "mac": "aa:b"}] ) + def test_interface_name_ethernet_default(self): + """ Use the profile name as interface_name for ethernet profiles """ + cons_without_interface_name = [{"name": "eth0", "type": "ethernet"}] + connections = ARGS_CONNECTIONS.validate(cons_without_interface_name) + self.assertTrue(connections[0]["interface_name"] == "eth0") + + def test_interface_name_ethernet_mac(self): + """ Do not set interface_name when mac is specified """ + cons_without_interface_name = [ + {"name": "eth0", "type": "ethernet", "mac": "3b:0b:88:16:6d:1a"} + ] + connections = ARGS_CONNECTIONS.validate(cons_without_interface_name) + self.assertTrue(connections[0]["interface_name"] is None) + + def test_interface_name_ethernet_empty(self): + """ Allow not to restrict the profile to an interface """ + network_connections = [ + {"name": "internal_network", "type": "ethernet", "interface_name": ""} + ] + connections = ARGS_CONNECTIONS.validate(network_connections) + + self.assertTrue(connections[0]["interface_name"] is None) + + def test_interface_name_ethernet_None(self): + """ Check that inerface_name cannot be None """ + network_connections = [ + {"name": "internal_network", "type": "ethernet", "interface_name": None} + ] + self.assertRaises( + n.ValidationError, ARGS_CONNECTIONS.validate, network_connections + ) + + def test_interface_name_ethernet_explicit(self): + """ Use the explicitly provided interface name """ + network_connections = [ + {"name": "internal", "type": "ethernet", "interface_name": "eth0"} + ] + connections = ARGS_CONNECTIONS.validate(network_connections) + self.assertEquals(connections[0]["interface_name"], "eth0") + + def test_interface_name_ethernet_invalid_profile(self): + """ Require explicit interface_name when the profile name is not a + valid interface_name """ + network_connections = [{"name": "internal:main", "type": "ethernet"}] + self.assertRaises( + n.ValidationError, ARGS_CONNECTIONS.validate, network_connections + ) + network_connections = [ + {"name": "internal:main", "type": "ethernet", "interface_name": "eth0"} + ] + connections = ARGS_CONNECTIONS.validate(network_connections) + self.assertTrue(connections[0]["interface_name"] == "eth0") + + def test_interface_name_ethernet_invalid_interface_name(self): + network_connections = [ + {"name": "internal", "type": "ethernet", "interface_name": "invalid:name"} + ] + self.assertRaises( + n.ValidationError, ARGS_CONNECTIONS.validate, network_connections + ) + + def test_interface_name_bond_empty_interface_name(self): + network_connections = [ + {"name": "internal", "type": "bond", "interface_name": "invalid:name"} + ] + self.assertRaises( + n.ValidationError, ARGS_CONNECTIONS.validate, network_connections + ) + + def test_interface_name_bond_profile_as_interface_name(self): + network_connections = [{"name": "internal", "type": "bond"}] + connections = ARGS_CONNECTIONS.validate(network_connections) + self.assertEquals(connections[0]["interface_name"], "internal") + @my_test_skipIf(nmutil is None, "no support for NM (libnm via pygobject)") class TestNM(unittest.TestCase):