diff --git a/.yamllint.yml b/.yamllint.yml index 7ee87dd..16ce93f 100644 --- a/.yamllint.yml +++ b/.yamllint.yml @@ -31,3 +31,5 @@ rules: /tests/tests_bond_deprecated_initscripts.yml /tests/tests_ethtool_features_initscripts.yml /tests/tests_wireless_wpa3_sae_nm.yml + /tests/tests_eth_pci_address_match_nm.yml + /tests/playbooks/tests_eth_pci_address_match.yml diff --git a/README.md b/README.md index b19c8d3..58c3d99 100644 --- a/README.md +++ b/README.md @@ -328,6 +328,51 @@ interface. In case of a missing `interface_name`, the `name` of the profile name **Note:** The `name` (the profile name) and the `interface_name` (the device name) may be different or the profile may not be tied to an interface at all. +### `match` + +Settings to specify devices or systems matching a profile. Currently, only the `path` +setting is implemented. + +The settings support a list of patterns which support the following modifiers and +wildcards: + +**Special modifiers for `match` settings:** + +- `|`, the element is an alternative, the match evaluates to be true if at least one of + the alternatives matches (logical OR). By default, an element is an alternative. + This means that an element `foo` behaves the same as `|foo` + +- `&`, the element is mandatory, the match evaluates to be true if all the element + matches (logical AND) + +- `!`, an element can also be inverted with exclamation mark (`!`) between the pipe + symbol (or the ampersand) and before the pattern. Note that `!foo` is a shortcut for + the mandatory match `&!foo` + +- `\`, a backslash can be used at the beginning of the element (after the optional + special characters) to escape the start of the pattern. For example, `&\!a` is an + mandatory match for literally `!a` + +**Wildcard patterns for `match` Settings:** +In general these work like shell globs. + +- `*`, matches zero or more of any character +- `?`, matches any single character +- `[fo]` - matches any single `f` or `o` character - also supports ranges - `[0-9]` + will match any single digit character + +#### `path` + +The `path` setting is a list of patterns to match against the `ID_PATH` udev property +of devices. The `ID_PATH` udev property represents the persistent path of a device. It +consists of a subsystem string (pci, usb, platform, etc.) and a subsystem-specific +identifier. The `ID_PATH` of a device can be obtained with the command +`udevadm info /sys/class/net/$dev | grep ID_PATH=` or by looking at the `path` property +exported by NetworkManager (`nmcli -f general.path device show $dev`). The `path` +setting is optional and restricts the profile to be activated only on devices with a +matching `ID_PATH`. The `path` setting is only supported for ethernet or infiniband +profiles. It supports modifiers and wildcards as described for match settings. + ### `zone` The `zone` option sets the firewalld zone for the interface. @@ -686,6 +731,36 @@ network_connections: dhcp4: yes ``` +Specifying a connecting profile for an ethernet device with the `ID_PATH`: + +```yaml +network_connections: + - name: eth0 + type: ethernet + # For PCI devices, the path has the form "pci-$domain:$bus:$device.$function" + # The profile will only match the interface at the PCI address pci-0000:00:03.0 + match: + path: + - pci-0000:00:03.0 + ip: + address: + - 192.0.2.3/24 +``` + +```yaml + - name: eth0 + type: ethernet + # Specifying a connecting profile for an ethernet device only with the PCI address + # pci-0000:00:01.0 or pci-0000:00:03.0 + match: + path: + - pci-0000:00:0[1-3].0 + - &!pci-0000:00:02.0 + ip: + address: + - 192.0.2.3/24 +``` + Deleting a connection profile named `eth0` (if it exists): ```yaml diff --git a/examples/match_path_support.yml b/examples/match_path_support.yml new file mode 100644 index 0000000..2fbf578 --- /dev/null +++ b/examples/match_path_support.yml @@ -0,0 +1,17 @@ +# SPDX-License-Identifier: BSD-3-Clause +--- +- hosts: all + vars: + network_connections: + - name: eth0 + type: ethernet + match: + path: + - pci-0000:00:03.0 + ip: + dhcp4: no + address: + - 192.0.2.3/24 + roles: + - linux-system-roles.network +... diff --git a/library/network_connections.py b/library/network_connections.py index e1edc32..0effcd8 100644 --- a/library/network_connections.py +++ b/library/network_connections.py @@ -1184,7 +1184,9 @@ class NMUtil: NM.SETTING_802_1X_DOMAIN_SUFFIX_MATCH, connection["ieee802_1x"]["domain_suffix_match"], ) - + if connection["match"]: + s_match = self.connection_ensure_setting(con, NM.SettingMatch) + s_match.set_property(NM.SETTING_MATCH_PATH, connection["match"]["path"]) try: con.normalize() except Exception as e: diff --git a/module_utils/network_lsr/argument_validator.py b/module_utils/network_lsr/argument_validator.py index 49a4419..74e01b3 100644 --- a/module_utils/network_lsr/argument_validator.py +++ b/module_utils/network_lsr/argument_validator.py @@ -400,25 +400,34 @@ class ArgValidatorDict(ArgValidator): class ArgValidatorList(ArgValidator): - def __init__(self, name, nested, default_value=None): + def __init__( + self, + name, + nested, + default_value=None, + remove_none_or_empty=False, + ): ArgValidator.__init__(self, name, required=False, default_value=default_value) self.nested = nested + self.remove_none_or_empty = remove_none_or_empty def _validate_impl(self, value, name): - if isinstance(value, Util.STRING_TYPE): - # we expect a list. However, for convenience allow to - # specify a string, separated by space. Escaping is - # not supported. If you need that, define a proper list. - value = [s for s in value.split(" ") if s] - if value is None: # Users might want to use jinja2 templates to set properties. As such, # it's convenient to accept None as an alias for an empty list # e.g. setting like `"match": {"path": None}` will be allowed by the role - return [] + value = [] + elif isinstance(value, Util.STRING_TYPE): + # we expect a list. However, for convenience allow to + # specify a string, separated by space. Escaping is + # not supported. If you need that, define a proper list. + value = [s for s in value.split(" ") if s] + result = [] for (idx, v) in enumerate(value): + if (v is None or v == "") and self.remove_none_or_empty: + continue try: vv = self.nested._validate(v, name + "[" + str(idx) + "]") except ValidationError as e: @@ -1270,6 +1279,45 @@ class ArgValidator_DictWireless(ArgValidatorDict): return result +class ArgValidatorListMatchPath(ArgValidatorList): + def __init__(self, name, nested, default_value, remove_none_or_empty): + ArgValidatorList.__init__( + self, + name, + nested, + default_value, + remove_none_or_empty, + ) + + def _validate_impl(self, value, name): + result = ArgValidatorList._validate_impl(self, value, name) + if result == ["|"] or result == ["&"]: + raise ValidationError( + name, + "value '%s' is not a valid 'match.path' setting, after " + "normalization, '%s' will only match the devices that have no PCI " + "path" % (value, result), + ) + return result + + +class ArgValidator_DictMatch(ArgValidatorDict): + def __init__(self): + ArgValidatorDict.__init__( + self, + name="match", + nested=[ + ArgValidatorListMatchPath( + "path", + nested=ArgValidatorStr("path[?]", allow_empty=True), + default_value=None, + remove_none_or_empty=True, + ), + ], + default_value={}, + ) + + class ArgValidator_DictConnection(ArgValidatorDict): VALID_PERSISTENT_STATES = ["absent", "present"] @@ -1340,6 +1388,7 @@ class ArgValidator_DictConnection(ArgValidatorDict): ArgValidator_DictMacvlan(), ArgValidator_Dict802_1X(), ArgValidator_DictWireless(), + ArgValidator_DictMatch(), # deprecated options: ArgValidatorStr( "infiniband_transport_mode", @@ -1563,6 +1612,15 @@ class ArgValidator_DictConnection(ArgValidatorDict): "but is '%s'" % result["mac"], ) + if result.get("match"): + if "path" in result["match"]: + if result["type"] not in ["ethernet", "infiniband"]: + raise ValidationError( + name + ".match.path", + "'match.path' settings are only supported for type " + "'ethernet' or 'infiniband'", + ) + if result["type"] == "infiniband": if "infiniband" not in result: result["infiniband"] = self.nested[ @@ -1627,7 +1685,9 @@ class ArgValidator_DictConnection(ArgValidatorDict): "invalid 'interface_name' '%s'" % (result["interface_name"]), ) else: - if not result.get("mac"): + if not result.get("mac") and ( + not result.get("match") or not result["match"].get("path") + ): if not Util.ifname_valid(result["name"]): raise ValidationError( name + ".interface_name", @@ -1921,3 +1981,18 @@ class ArgValidator_ListConnections(ArgValidatorList): "Setting DNS options 'inet6', 'ip6-bytestring', 'ip6-dotint', " "'no-ip6-dotint' is not allowed when IPv6 is disabled.", ) + + if connection["match"]: + if connection["match"]["path"]: + if mode == self.VALIDATE_ONE_MODE_INITSCRIPTS: + raise ValidationError.from_connection( + idx, + "match.path is not supported by initscripts.", + ) + else: + if not hasattr(Util.NM(), "SETTING_MATCH_PATH"): + raise ValidationError.from_connection( + idx, + "match.path is not supported by the running version of " + "NetworkManger.", + ) diff --git a/tests/ensure_provider_tests.py b/tests/ensure_provider_tests.py index 73b502c..65e7c62 100755 --- a/tests/ensure_provider_tests.py +++ b/tests/ensure_provider_tests.py @@ -76,6 +76,10 @@ ibution_major_version | int < 9", EXTRA_RUN_CONDITION: "ansible_distribution != 'RedHat' or\n ansible_distr\ ibution_major_version | int < 9", }, + "playbooks/tests_eth_pci_address_match.yml": { + MINIMUM_VERSION: "'1.26.0'", + "comment": "# NetworkManager 1.26.0 added support for match.path setting", + }, "playbooks/tests_reapply.yml": {}, # team interface is not supported on Fedora "playbooks/tests_team.yml": { diff --git a/tests/playbooks/tests_eth_pci_address_match.yml b/tests/playbooks/tests_eth_pci_address_match.yml new file mode 100644 index 0000000..5211ba0 --- /dev/null +++ b/tests/playbooks/tests_eth_pci_address_match.yml @@ -0,0 +1,76 @@ +# SPDX-License-Identifier: BSD-3-Clause +--- +- hosts: all + tasks: + - import_role: + name: linux-system-roles.network + vars: + network_connections: + - name: pseudo-pci-test-only + persistent_state: present + type: ethernet + # `pci-0001:00:00.0` is a pseudo PCI address, + # which used only for testing purpose + match: + path: + - pci-0001:00:00.0 + ip: + dhcp4: no + address: + - 192.0.2.3/24 + - block: + - name: Get match.path setting for connection + command: nmcli -f match.path connection show pseudo-pci-test-only + register: match_path + ignore_errors: yes + changed_when: false + + - name: Assert that the connection profile contains the specified PCI + address + assert: + that: + - "'pci-0001:00:00.0' in match_path.stdout" + msg: The connection profile does not contain the specified PCI + address + + - name: "Remove the PCI address from match path" + import_role: + name: linux-system-roles.network + vars: + network_connections: + - name: pseudo-pci-test-only + type: ethernet + # When clearing the `match.path` setting through the role, the + # `interface_name` has to be defined, otherwise the `interface_name` + # will be the profile name. Since it is unclear if a specific + # interface exists for the test profile, by setting the + # `interface_name` to empty string, the role will accept any interface + # for the test profile. + interface_name: "" + state: down + match: + path: + + - name: Get match.path setting for connection + command: nmcli -f match.path connection show pseudo-pci-test-only + register: clear_path + ignore_errors: yes + changed_when: false + + - name: "Assert that the PCI address is removed" + assert: + that: + - "'pci-0001:00:00.0' not in clear_path.stdout" + msg: "The PCI address is not removed" + always: + - block: + - import_role: + name: linux-system-roles.network + vars: + network_connections: + - name: pseudo-pci-test-only + persistent_state: absent + state: down + ignore_errors: true + tags: + - "tests::cleanup" diff --git a/tests/tests_eth_pci_address_match_nm.yml b/tests/tests_eth_pci_address_match_nm.yml new file mode 100644 index 0000000..3df788a --- /dev/null +++ b/tests/tests_eth_pci_address_match_nm.yml @@ -0,0 +1,38 @@ +# SPDX-License-Identifier: BSD-3-Clause +# This file was generated by ensure_provider_tests.py +--- +# set network provider and gather facts +- hosts: all + name: Run playbook 'playbooks/tests_eth_pci_address_match.yml' with nm as provider + tasks: + - name: Set network provider to 'nm' + set_fact: + network_provider: nm + tags: + - always + + - block: + - name: Install NetworkManager + package: + name: NetworkManager + state: present + - name: Get NetworkManager version + command: rpm -q --qf "%{version}" NetworkManager + args: + warn: false + register: NetworkManager_version + when: true + when: + - ansible_distribution_major_version != '6' + tags: + - always + + +# The test requires or should run with NetworkManager, therefore it cannot run +# on RHEL/CentOS 6 +# NetworkManager 1.26.0 added support for match.path setting +- import_playbook: playbooks/tests_eth_pci_address_match.yml + when: + - ansible_distribution_major_version != '6' + + - NetworkManager_version.stdout is version('1.26.0', '>=') diff --git a/tests/unit/test_network_connections.py b/tests/unit/test_network_connections.py index e32e20c..3537264 100644 --- a/tests/unit/test_network_connections.py +++ b/tests/unit/test_network_connections.py @@ -185,6 +185,7 @@ class TestValidator(unittest.TestCase): "dns_search": [], }, "mac": None, + "match": {}, "controller": None, "ieee802_1x": None, "wireless": None, @@ -448,6 +449,51 @@ class TestValidator(unittest.TestCase): self.assertValidationError(v, [1, "s"]) self.assertEqual(v.validate(None), []) + def test_validate_allow_empty_string_in_list(self): + """ + Test that when ArgValidatorStr.allow_empty is True, empty string is allowed in + in ArgValidatorList + """ + v = network_lsr.argument_validator.ArgValidatorList( + "list", + nested=network_lsr.argument_validator.ArgValidatorStr( + "list[?]", allow_empty=True + ), + ) + self.assertEqual(v.validate(["pci-0001:00:00.0", ""]), ["pci-0001:00:00.0", ""]) + + def test_validate_disallow_none_in_list(self): + """ + Test that None is not allowed in ArgValidatorList + """ + v = network_lsr.argument_validator.ArgValidatorList( + "list", + nested=network_lsr.argument_validator.ArgValidatorStr( + "list[?]", allow_empty=True + ), + ) + self.assertRaisesRegexp( + ValidationError, + "must be a string but is 'None'", + v.validate, + ["pci-0001:00:00.0", None], + ) + + def test_validate_list_remove_none_or_empty(self): + """ + Test that when ArgValidatorStr.remove_none_or_empty is True, None or empty + string will be removed from ArgValidatorList + """ + v = network_lsr.argument_validator.ArgValidatorList( + "list", + nested=network_lsr.argument_validator.ArgValidatorStr( + "list[?]", allow_empty=True + ), + remove_none_or_empty=True, + ) + self.assertEqual(v.validate(["pci-0001:00:00.0", ""]), ["pci-0001:00:00.0"]) + self.assertEqual(v.validate(["pci-0001:00:00.0", None]), ["pci-0001:00:00.0"]) + def test_empty(self): self.maxDiff = None self.do_connections_validate([], []) @@ -483,6 +529,7 @@ class TestValidator(unittest.TestCase): "dns_search": [], }, "mac": None, + "match": {}, "controller": None, "ieee802_1x": None, "wireless": None, @@ -538,6 +585,7 @@ class TestValidator(unittest.TestCase): "dhcp4_send_hostname": None, }, "mac": None, + "match": {}, "controller": None, "ieee802_1x": None, "wireless": None, @@ -587,6 +635,7 @@ class TestValidator(unittest.TestCase): "dhcp4_send_hostname": None, }, "mac": None, + "match": {}, "controller": None, "ieee802_1x": None, "wireless": None, @@ -654,6 +703,7 @@ class TestValidator(unittest.TestCase): "force_state_change": None, "ignore_errors": None, "interface_name": None, + "match": {}, "ip": { "dhcp4": False, "route_metric6": None, @@ -745,6 +795,7 @@ class TestValidator(unittest.TestCase): "route": [], }, "mac": None, + "match": {}, "controller": None, "ieee802_1x": None, "wireless": None, @@ -783,6 +834,7 @@ class TestValidator(unittest.TestCase): "force_state_change": None, "ignore_errors": None, "interface_name": "prod1", + "match": {}, "ip": { "gateway6": "2001:db8::1", "gateway4": None, @@ -885,6 +937,7 @@ class TestValidator(unittest.TestCase): "force_state_change": None, "ignore_errors": None, "interface_name": None, + "match": {}, "ip": { "dhcp4": False, "auto6": True, @@ -937,6 +990,7 @@ class TestValidator(unittest.TestCase): "force_state_change": None, "ignore_errors": None, "interface_name": "prod.100", + "match": {}, "ip": { "dhcp4": False, "route_metric6": None, @@ -1051,6 +1105,7 @@ class TestValidator(unittest.TestCase): "route": [], }, "mac": None, + "match": {}, "controller": None, "ieee802_1x": None, "wireless": None, @@ -1126,6 +1181,7 @@ class TestValidator(unittest.TestCase): "route": [], }, "mac": None, + "match": {}, "controller": None, "ieee802_1x": None, "wireless": None, @@ -1200,6 +1256,7 @@ class TestValidator(unittest.TestCase): "force_state_change": None, "ignore_errors": None, "interface_name": None, + "match": {}, "ip": { "dhcp4": False, "auto6": True, @@ -1252,6 +1309,7 @@ class TestValidator(unittest.TestCase): "force_state_change": None, "ignore_errors": None, "interface_name": "prod.100", + "match": {}, "ip": { "dhcp4": False, "route_metric6": None, @@ -1347,6 +1405,7 @@ class TestValidator(unittest.TestCase): "force_state_change": None, "ignore_errors": None, "interface_name": "eth0", + "match": {}, "ip": { "dhcp4": False, "auto6": False, @@ -1393,6 +1452,7 @@ class TestValidator(unittest.TestCase): "force_state_change": None, "ignore_errors": None, "interface_name": "veth0", + "match": {}, "ip": { "dhcp4": False, "route_metric6": None, @@ -1448,6 +1508,7 @@ class TestValidator(unittest.TestCase): "force_state_change": None, "ignore_errors": None, "interface_name": "veth1", + "match": {}, "ip": { "dhcp4": False, "route_metric6": None, @@ -1572,6 +1633,7 @@ class TestValidator(unittest.TestCase): "rule_append_only": False, }, "mac": None, + "match": {}, "controller": None, "ieee802_1x": None, "wireless": None, @@ -1613,6 +1675,7 @@ class TestValidator(unittest.TestCase): "rule_append_only": False, }, "mac": None, + "match": {}, "controller": "prod2", "ieee802_1x": None, "wireless": None, @@ -1678,6 +1741,7 @@ class TestValidator(unittest.TestCase): "route": [], }, "mac": None, + "match": {}, "controller": None, "ieee802_1x": None, "wireless": None, @@ -1728,6 +1792,7 @@ class TestValidator(unittest.TestCase): "route": [], }, "mac": None, + "match": {}, "controller": None, "ieee802_1x": None, "wireless": None, @@ -1788,6 +1853,7 @@ class TestValidator(unittest.TestCase): "dns_search": [], }, "mac": "aa:bb:cc:dd:ee:ff", + "match": {}, "controller": None, "ieee802_1x": None, "wireless": None, @@ -1836,6 +1902,7 @@ class TestValidator(unittest.TestCase): "dhcp4_send_hostname": None, }, "mac": None, + "match": {}, "controller": None, "ieee802_1x": None, "wireless": None, @@ -1912,6 +1979,7 @@ class TestValidator(unittest.TestCase): "rule_append_only": False, }, "mac": None, + "match": {}, "controller": None, "ieee802_1x": None, "wireless": None, @@ -1953,6 +2021,7 @@ class TestValidator(unittest.TestCase): "rule_append_only": False, }, "mac": None, + "match": {}, "controller": "6643-controller", "ieee802_1x": None, "wireless": None, @@ -2010,6 +2079,7 @@ class TestValidator(unittest.TestCase): "rule_append_only": False, }, "mac": None, + "match": {}, "controller": None, "ieee802_1x": None, "wireless": None, @@ -2085,6 +2155,7 @@ class TestValidator(unittest.TestCase): }, "mac": "11:22:33:44:55:66:77:88:99:00:" "11:22:33:44:55:66:77:88:99:00", + "match": {}, "controller": None, "ieee802_1x": None, "wireless": None, @@ -2180,6 +2251,7 @@ class TestValidator(unittest.TestCase): "dhcp4_send_hostname": None, }, "mac": None, + "match": {}, "controller": None, "ieee802_1x": None, "wireless": None, @@ -2283,6 +2355,7 @@ class TestValidator(unittest.TestCase): "dhcp4_send_hostname": None, }, "mac": None, + "match": {}, "controller": None, "ieee802_1x": None, "wireless": None, @@ -2399,6 +2472,7 @@ class TestValidator(unittest.TestCase): "dhcp4_send_hostname": None, }, "mac": None, + "match": {}, "controller": None, "ieee802_1x": { "identity": "myhost", @@ -2477,6 +2551,7 @@ class TestValidator(unittest.TestCase): "dhcp4_send_hostname": None, }, "mac": None, + "match": {}, "controller": None, "ieee802_1x": { "identity": "myhost", @@ -2555,6 +2630,7 @@ class TestValidator(unittest.TestCase): "dhcp4_send_hostname": None, }, "mac": None, + "match": {}, "controller": None, "ieee802_1x": { "identity": "myhost", @@ -2631,6 +2707,7 @@ class TestValidator(unittest.TestCase): "dhcp4_send_hostname": None, }, "mac": None, + "match": {}, "controller": None, "ieee802_1x": None, "wireless": { @@ -2697,6 +2774,7 @@ class TestValidator(unittest.TestCase): "dhcp4_send_hostname": None, }, "mac": None, + "match": {}, "controller": None, "ieee802_1x": { "identity": "myhost", @@ -3761,6 +3839,126 @@ class TestNM(unittest.TestCase): self.assertEqual(result.get_data(), b"file:///my/test/path\x00") +class TestValidatorMatch(unittest.TestCase): + def setUp(self): + self.test_profile = { + "name": "test", + "type": "ethernet", + "ip": { + "dhcp4": False, + "address": "192.168.245.7/24", + }, + "match": None, + } + self.validator = network_lsr.argument_validator.ArgValidator_DictConnection() + + def test_match_path_empty_list(self): + """ + Test that 'match.path' setting can be defined as None, [], [""], [None] or "" + and such a setting will be normalized into [] + """ + for disabled_path in [None, [], [""], [None], ""]: + self.test_profile["match"] = {"path": disabled_path} + result = self.validator.validate(self.test_profile) + self.assertEqual(result["match"], {"path": []}) + + def test_match_path_setting_normalization(self): + """ + Test that 'match.path' setting ["", "usb123", None] will be normalized into + ["usb123"] + """ + self.test_profile["match"] = {"path": ["", "usb123", None]} + result = self.validator.validate(self.test_profile) + self.assertEqual(result["match"], {"path": ["usb123"]}) + + def test_match_path_valid_setting(self): + """ + Test that values like ["pci-0000:00:03.0"] and ["&!pci-0000:00:0[1-3].0"] are + valid values for 'match.path' setting + """ + self.test_profile["match"] = {"path": ["pci-0000:00:03.0"]} + result1 = self.validator.validate(self.test_profile) + self.assertEqual(result1["match"], {"path": ["pci-0000:00:03.0"]}) + + self.test_profile["match"] = {"path": ["&!pci-0000:00:0[1-3].0"]} + result2 = self.validator.validate(self.test_profile) + self.assertEqual(result2["match"], {"path": ["&!pci-0000:00:0[1-3].0"]}) + + def test_match_path_invalid_setting(self): + """ + Test that values like ["&"] and ["|"] are invalid values for 'match.path' + setting + """ + self.test_profile["match"] = {"path": ["&", ""]} + self.assertRaisesRegexp( + ValidationError, + "['&'] will only match the devices that have no PCI path", + self.validator.validate, + self.test_profile, + ) + self.test_profile["match"] = {"path": ["|", None]} + self.assertRaisesRegexp( + ValidationError, + "['|'] will only match the devices that have no PCI path", + self.validator.validate, + self.test_profile, + ) + + def test_match_path_invalid_connection_type(self): + """ + Test that when 'match.path' setting is correctly defined but the connection + type is neither ethernet nor infiniband, a ValidationError raises. + """ + self.test_profile["match"] = {"path": ["pci-0000:00:03.0"]} + result = self.validator.validate(self.test_profile) + self.assertEqual(result["match"], {"path": ["pci-0000:00:03.0"]}) + + self.test_profile["type"] = "dummy" + self.assertRaisesRegexp( + ValidationError, + "'match.path' settings are only supported for type 'ethernet' or " + "'infiniband'", + self.validator.validate, + self.test_profile, + ) + + def test_interface_name_when_match_not_specified(self): + """ + Test that when 'match' setting is not specified, interface name should be + profile name. + """ + result = self.validator.validate(self.test_profile) + self.assertEqual(result["interface_name"], "test") + + def test_interface_name_and_match_when_match_is_None(self): + """ + Test that when 'match' setting is None, interface name should be profile name + and 'match' setting will be normalized into {}. + """ + self.test_profile["match"] = None + result = self.validator.validate(self.test_profile) + self.assertEqual(result["match"], {}) + self.assertEqual(result["interface_name"], "test") + + def test_interface_name_when_match_path_is_empty_list(self): + """ + Test that when 'match.path' setting is empty list, interface name should be + profile name. + """ + self.test_profile["match"] = {"path": []} + result = self.validator.validate(self.test_profile) + self.assertEqual(result["interface_name"], "test") + + def test_interface_name_when_match_path_is_valid(self): + """ + Test that when 'match.path' setting contains interface path, interface name + should be unset. + """ + self.test_profile["match"] = {"path": ["pci-0000:00:03.0"]} + result = self.validator.validate(self.test_profile) + self.assertEqual(result["interface_name"], None) + + class TestUtils(unittest.TestCase): def test_mac_ntoa(self): mac_bytes = b"\xaa\xbb\xcc\xdd\xee\xff"