diff --git a/README.md b/README.md index d826edc..b0f6af0 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ This role can be used to configure: - VLAN interfaces - MacVLAN interfaces - Infiniband interfaces +- Wireless (WiFi) interfaces - IP configuration - 802.1x authentication @@ -63,10 +64,12 @@ List of variables: provider (`nm` or `initscripts`) . Setting it to `{{ network_provider_os_default }}`, the provider is set depending on the operating system. This is usually `nm` except for RHEL 6 or CentOS 6 systems. - * `network_connections` - The connection profiles are configured as `network_connections`, which is a list of dictionaries that include specific options. - +* `network_allow_restart` - Certain configurations require the role to restart network services. + For example, if a wireless connection is configured and NetworkManager-wifi is not installed, + NetworkManager must be restarted prior to the connection being configured. Setting this to + `no` will prevent the role from restarting network service. Examples of Variables --------------------- @@ -78,6 +81,7 @@ network_provider: nm network_connections: - name: eth0 #... +network_allow_restart: yes ``` Options @@ -186,6 +190,7 @@ The `type` option can be set to the following values: - `vlan` - `macvlan` - `infiniband` + - `wireless` #### `type: ethernet` @@ -232,6 +237,19 @@ role. Similar to `master` and `vlan`, the `parent` references the connection profile in the ansible role. +#### `type: wireless` + +The `wireless` type supports WPA-PSK (password) authentication and WPA-EAP (802.1x) authentication. + +`nm` (NetworkManager) is the only supported `network_provider` for this type. + +If WPA-EAP is used, ieee802_1x settings must be defined in the [ieee802_1x](#-`ieee802_1x`) option. + +The following options are supported: + +* `ssid`: the SSID of the wireless network (required) +* `key_mgmt`: `wpa-psk` or `wpa-eap` (required) +* `password`: password for the network (required if `wpa-psk` is used) ### `autoconnect` @@ -642,6 +660,20 @@ network_connections: - 192.168.1.1/24 ``` +Configuring a wireless connection: + +```yaml +network_connections: + - name: wlan0 + type: wireless + wireless: + ssid: "My WPA2-PSK Network" + key_mgmt: "wpa-psk" + # recommend vault encrypting the wireless password + # see https://docs.ansible.com/ansible/latest/user_guide/vault.html + password: "p@55w0rD" +``` + Setting the IP configuration: ```yaml diff --git a/defaults/main.yml b/defaults/main.yml index cccf48b..9a2c2f2 100644 --- a/defaults/main.yml +++ b/defaults/main.yml @@ -2,6 +2,8 @@ --- network_connections: [] +network_allow_restart: no + # Use initscripts for RHEL/CentOS < 7, nm otherwise network_provider_os_default: "{{ 'initscripts' if ansible_distribution in [ @@ -20,10 +22,25 @@ __network_provider_current: "{{ # Default to the auto-detected value network_provider: "{{ __network_provider_current }}" -# wpa_supplicant is required if any ieee802_1x connections are defined -__network_wpa_supplicant_required: "{{ network_connections | +# check if any 802.1x connections are defined +__network_ieee802_1x_connections_defined: "{{ network_connections | selectattr('ieee802_1x', 'defined') | list | count > 0 }}" -__network_packages_default_802_1x: ["{% if __network_wpa_supplicant_required + +# check if any wireless connections are defined +__network_wireless_connections_defined: "{{ + ['wireless'] in network_connections|json_query('[*][type]') }}" + +# NetworkManager-wireless is required for wireless connections +__network_packages_default_wireless: ["{% + if __network_wireless_connections_defined + %}NetworkManager-wifi{% endif %}"] + +# wpa_supplicant is required if any 802.1x or wireless connections are defined +__network_wpa_supplicant_required: "{{ + __network_ieee802_1x_connections_defined or + __network_wireless_connections_defined }}" +__network_packages_default_wpa_supplicant: ["{% + if __network_wpa_supplicant_required %}wpa_supplicant{% endif %}"] # The python-gobject-base package depends on the python version and @@ -36,7 +53,8 @@ __network_packages_default_gobject_packages: ["python{{ __network_service_name_default_nm: NetworkManager __network_packages_default_nm: "{{['NetworkManager'] + __network_packages_default_gobject_packages|select()|list() - + __network_packages_default_802_1x|select()|list()}}" + + __network_packages_default_wpa_supplicant|select()|list() + + __network_packages_default_wireless|select()|list()}}" __network_service_name_default_initscripts: network diff --git a/examples/wireless_wpa_psk.yml b/examples/wireless_wpa_psk.yml new file mode 100644 index 0000000..9ebfcaf --- /dev/null +++ b/examples/wireless_wpa_psk.yml @@ -0,0 +1,13 @@ +# SPDX-License-Identifier: BSD-3-Clause +--- +- hosts: network-test + vars: + network_connections: + - name: wlan0 + type: wireless + wireless: + ssid: "My WPA2-PSK Network" + key_mgmt: "wpa-psk" + # recommend vault encrypting the wireless password + # see https://docs.ansible.com/ansible/latest/user_guide/vault.html + password: "p@55w0rD" diff --git a/library/network_connections.py b/library/network_connections.py index 3ae7b3b..6752911 100644 --- a/library/network_connections.py +++ b/library/network_connections.py @@ -836,6 +836,28 @@ class NMUtil: NM.SETTING_MACVLAN_PARENT, ArgUtil.connection_find_master(connection["parent"], connections, idx), ) + elif connection["type"] == "wireless": + s_con.set_property( + NM.SETTING_CONNECTION_TYPE, NM.SETTING_WIRELESS_SETTING_NAME + ) + s_wireless = self.connection_ensure_setting(con, NM.SettingWireless) + s_wireless.set_property( + NM.SETTING_WIRELESS_SSID, + Util.GLib().Bytes.new(connection["wireless"]["ssid"].encode("utf-8")), + ) + + s_wireless_sec = self.connection_ensure_setting( + con, NM.SettingWirelessSecurity + ) + s_wireless_sec.set_property( + NM.SETTING_WIRELESS_SECURITY_KEY_MGMT, + connection["wireless"]["key_mgmt"], + ) + + if connection["wireless"]["key_mgmt"] == "wpa-psk": + s_wireless_sec.set_property( + NM.SETTING_WIRELESS_SECURITY_PSK, connection["wireless"]["password"] + ) else: raise MyError("unsupported type %s" % (connection["type"])) diff --git a/module_utils/network_lsr/argument_validator.py b/module_utils/network_lsr/argument_validator.py index 234351e..0a483c9 100644 --- a/module_utils/network_lsr/argument_validator.py +++ b/module_utils/network_lsr/argument_validator.py @@ -115,11 +115,27 @@ class ArgValidatorStr(ArgValidator): default_value=None, enum_values=None, allow_empty=False, + min_length=None, + max_length=None, ): ArgValidator.__init__(self, name, required, default_value) self.enum_values = enum_values self.allow_empty = allow_empty + if max_length is not None: + if not isinstance(max_length, int): + raise ValueError("max_length must be an integer") + elif max_length < 0: + raise ValueError("max_length must be a positive integer") + self.max_length = max_length + + if min_length is not None: + if not isinstance(min_length, int): + raise ValueError("min_length must be an integer") + elif min_length < 0: + raise ValueError("min_length must be a positive integer") + self.min_length = min_length + def _validate_impl(self, value, name): if not isinstance(value, Util.STRING_TYPE): raise ValidationError(name, "must be a string but is '%s'" % (value)) @@ -132,8 +148,36 @@ class ArgValidatorStr(ArgValidator): ) if not self.allow_empty and not value: raise ValidationError(name, "cannot be empty") + if not self._validate_string_max_length(value): + raise ValidationError( + name, "maximum length is %s characters" % (self.max_length) + ) + if not self._validate_string_min_length(value): + raise ValidationError( + name, "minimum length is %s characters" % (self.min_length) + ) return value + def _validate_string_max_length(self, value): + """ + Ensures that the length of string `value` is less than or equal to + the maximum length + """ + if self.max_length is not None: + return len(str(value)) <= self.max_length + else: + return True + + def _validate_string_min_length(self, value): + """ + Ensures that the length of string `value` is more than or equal to + the minimum length + """ + if self.min_length is not None: + return len(str(value)) >= self.min_length + else: + return True + class ArgValidatorNum(ArgValidator): def __init__( # pylint: disable=too-many-arguments @@ -887,6 +931,42 @@ class ArgValidator_Dict802_1X(ArgValidatorDict): return result +class ArgValidator_DictWireless(ArgValidatorDict): + + VALID_KEY_MGMT = [ + "wpa-psk", + "wpa-eap", + ] + + def __init__(self): + ArgValidatorDict.__init__( + self, + name="wireless", + nested=[ + ArgValidatorStr("ssid", max_length=32), + ArgValidatorStr( + "key_mgmt", enum_values=ArgValidator_DictWireless.VALID_KEY_MGMT + ), + ArgValidatorStr("password", default_value=None, max_length=63), + ], + default_value=None, + ) + + def _validate_post(self, value, name, result): + if result["key_mgmt"] == "wpa-psk": + if result["password"] is None: + raise ValidationError( + name, "must supply a password if using 'wpa-psk' key management", + ) + else: + if result["password"] is not None: + raise ValidationError( + name, "password only allowed if using 'wpa-psk' key management", + ) + + return result + + class ArgValidator_DictConnection(ArgValidatorDict): VALID_PERSISTENT_STATES = ["absent", "present"] @@ -899,6 +979,7 @@ class ArgValidator_DictConnection(ArgValidatorDict): "bond", "vlan", "macvlan", + "wireless", ] VALID_SLAVE_TYPES = ["bridge", "bond", "team"] @@ -949,6 +1030,7 @@ class ArgValidator_DictConnection(ArgValidatorDict): ArgValidator_DictVlan(), ArgValidator_DictMacvlan(), ArgValidator_Dict802_1X(), + ArgValidator_DictWireless(), # deprecated options: ArgValidatorStr( "infiniband_transport_mode", @@ -1081,9 +1163,42 @@ class ArgValidator_DictConnection(ArgValidatorDict): self.VALID_FIELDS = valid_fields return result + def _validate_post_wireless(self, value, name, result): + """ + Validate wireless settings + """ + if "type" in result: + if result["type"] == "wireless": + if "wireless" in result: + if ( + result["wireless"]["key_mgmt"] == "wpa-eap" + and "ieee802_1x" not in result + ): + raise ValidationError( + name + ".wireless", + "key management set to wpa-eap but no " + "'ieee802_1x' settings defined", + ) + else: + raise ValidationError( + name + ".wireless", + "must define 'wireless' settings for 'type' 'wireless'", + ) + + else: + if "wireless" in result: + raise ValidationError( + name + ".wireless", + "'wireless' settings are not allowed for 'type' '%s'" + % (result["type"]), + ) + + return result + def _validate_post(self, value, name, result): result = self._validate_post_state(value, name, result) result = self._validate_post_fields(value, name, result) + result = self._validate_post_wireless(value, name, result) if "type" in result: @@ -1299,6 +1414,15 @@ class ArgValidator_DictConnection(ArgValidatorDict): % (result["type"]), ) + if "ieee802_1x" in result and result["type"] not in [ + "ethernet", + "wireless", + ]: + raise ValidationError( + name + ".ieee802_1x", + "802.1x settings only allowed for ethernet or wireless interfaces.", + ) + for k in self.VALID_FIELDS: if k in result: continue @@ -1409,7 +1533,12 @@ class ArgValidator_ListConnections(ArgValidatorList): "if you need to use initscripts.", ) - if connection["type"] != "ethernet": + # check if wireless connection is valid + if connection["type"] == "wireless": + if mode == self.VALIDATE_ONE_MODE_INITSCRIPTS: raise ValidationError.from_connection( - idx, "802.1x settings only allowed for ethernet interfaces." + idx, + "Wireless WPA auth is not supported by initscripts. " + "Configure wireless connection in /etc/wpa_supplicant.conf " + "if you need to use initscripts.", ) diff --git a/tasks/main.yml b/tasks/main.yml index 5bcc9cf..4ad83c9 100644 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -23,6 +23,22 @@ state: present when: - not network_packages is subset(ansible_facts.packages.keys()) + register: __network_package_install + +# If network packages have changed and wireless connections are required, +# NetworkManager must be restarted +- name: Restart NetworkManager + service: + name: NetworkManager + state: restarted + when: + - __network_wireless_connections_defined + - network_provider == "nm" + - network_allow_restart + # ansible-lint wants this to be a handler, but this is not appropriate as + # NetworkManager must be restarted prior to the connections being created. + # see (https://docs.ansible.com/ansible-lint/rules/default_rules.html) + - __network_package_install.changed # noqa 503 - name: Enable and start NetworkManager service: diff --git a/tests/ensure_provider_tests.py b/tests/ensure_provider_tests.py index 5fff16f..06747c7 100755 --- a/tests/ensure_provider_tests.py +++ b/tests/ensure_provider_tests.py @@ -61,10 +61,15 @@ NM_ONLY_TESTS = { MINIMUM_VERSION: "'1.20.0'", "comment": "# NetworkManager 1.20.0 introduced ethtool settings support", }, + "playbooks/tests_802_1x_updated.yml": {}, + "playbooks/tests_802_1x.yml": {}, "playbooks/tests_reapply.yml": {}, "playbooks/tests_states.yml": {}, - "playbooks/tests_802_1x.yml": {}, - "playbooks/tests_802_1x_updated.yml": {}, + # mac80211_hwsim (used for tests_wireless) only seems to be available + # and working on RHEL/CentOS 7 + "playbooks/tests_wireless.yml": { + EXTRA_RUN_CONDITION: "ansible_distribution_major_version == '7'", + }, } IGNORE = [ diff --git a/tests/playbooks/tests_wireless.yml b/tests/playbooks/tests_wireless.yml new file mode 100644 index 0000000..822a15e --- /dev/null +++ b/tests/playbooks/tests_wireless.yml @@ -0,0 +1,88 @@ +# SPDX-License-Identifier: BSD-3-Clause +--- +- hosts: all + vars: + interface: wlan0 + tasks: + - name: "INIT: wireless tests" + debug: + msg: "##################################################" + - include_tasks: tasks/setup_mock_wifi.yml + - name: Copy client certs + copy: + src: "{{ item }}" + dest: "/etc/pki/tls/{{ item }}" + mode: 0644 + with_items: + - client.key + - client.pem + - cacert.pem + - block: + - name: "TEST: wireless connection with WPA-PSK" + debug: + msg: "##################################################" + - import_role: + name: linux-system-roles.network + vars: + network_allow_restart: true + network_connections: + - name: "{{ interface }}" + state: up + type: wireless + ip: + address: + - 203.0.113.2/24 + dhcp4: "no" + auto6: "no" + wireless: + ssid: "mock_wifi" + key_mgmt: "wpa-psk" + password: "p@55w0rD" + - import_role: + name: linux-system-roles.network + vars: + network_connections: + - name: "{{ interface }}" + persistent_state: absent + state: down + - name: "TEST: wireless connection with 802.1x TLS-EAP" + debug: + msg: "##################################################" + - import_role: + name: linux-system-roles.network + vars: + network_allow_restart: true + network_connections: + - name: "{{ interface }}" + state: up + type: wireless + ip: + address: + - 203.0.113.2/24 + dhcp4: "no" + auto6: "no" + wireless: + ssid: "mock_wifi" + key_mgmt: "wpa-eap" + ieee802_1x: + identity: myhost + eap: tls + private_key: /etc/pki/tls/client.key + private_key_password: test + private_key_password_flags: + - none + client_cert: /etc/pki/tls/client.pem + ca_cert: /etc/pki/tls/cacert.pem + always: + - block: + - import_role: + name: linux-system-roles.network + vars: + network_connections: + - name: "{{ interface }}" + persistent_state: absent + state: down + ignore_errors: true + - include_tasks: tasks/cleanup_mock_wifi.yml + tags: + - "tests::cleanup" diff --git a/tests/tasks/cleanup_mock_wifi.yml b/tests/tasks/cleanup_mock_wifi.yml new file mode 100644 index 0000000..a80f337 --- /dev/null +++ b/tests/tasks/cleanup_mock_wifi.yml @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: BSD-3-Clause +--- +- name: Unload mac80211_hwsim module + shell: modprobe -r mac80211_hwsim + +- name: Kill hostapd process + shell: pkill hostapd diff --git a/tests/tasks/setup_mock_wifi.yml b/tests/tasks/setup_mock_wifi.yml new file mode 100644 index 0000000..d2dbba3 --- /dev/null +++ b/tests/tasks/setup_mock_wifi.yml @@ -0,0 +1,91 @@ +# SPDX-License-Identifier: BSD-3-Clause +--- +- name: Install EPEL on enterprise Linux for hostapd + # yamllint disable-line rule:line-length + command: yum install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-{{ ansible_distribution_major_version }}.noarch.rpm + args: + warn: false + creates: /etc/yum.repos.d/epel.repo + when: + - ansible_distribution in ['RedHat', 'CentOS'] + +- name: Install packages required to set up mock wifi network + package: + name: + - hostapd + - NetworkManager + - wpa_supplicant + state: present + +- name: Ensure NetworkManager is running + service: + name: NetworkManager + state: started + +- name: Copy server certificates + copy: + src: "{{ item }}" + dest: "/etc/pki/tls/{{ item }}" + with_items: + - server.key + - dh.pem + - server.pem + - cacert.pem + +- name: Create hostapd config + copy: + content: | + interface=wlan1 + driver=nl80211 + ctrl_interface=/var/run/hostapd + ctrl_interface_group=0 + ssid=mock_wifi + country_code=EN + hw_mode=g + channel=7 + auth_algs=3 + wpa=3 + ieee8021x=1 + eapol_version=1 + wpa_key_mgmt=WPA-EAP WPA-PSK + wpa_passphrase=p@55w0rD + eap_reauth_period=3600 + eap_server=1 + use_pae_group_addr=1 + eap_user_file=/etc/hostapd/hostapd.eap_user + ca_cert=/etc/pki/tls/cacert.pem + dh_file=/etc/pki/tls/dh.pem + server_cert=/etc/pki/tls/server.pem + private_key=/etc/pki/tls/server.key + private_key_passwd=test + logger_syslog=-1 + logger_syslog_level=0 + dest: /etc/hostapd/wireless.conf + +- name: Create eap_user_file config + copy: + content: | + * TLS + dest: /etc/hostapd/hostapd.eap_user + +- name: Load mac80211_hwsim kernel module to mock a wifi network + shell: modprobe mac80211_hwsim && sleep 5 + +- name: Restart NetworkManager and wpa_supplicant + service: + name: "{{ item }}" + state: restarted + with_items: + - NetworkManager + - wpa_supplicant + +- name: Configure wlan0 and wlan1 (mock wifi interfaces) + shell: | + ip link set up wlan0 + ip link set up wlan1 + nmcli device set wlan1 managed off + ip add add 203.0.113.1/24 dev wlan1 + sleep 5 + +- name: Start hostapd + shell: hostapd -B /etc/hostapd/wireless.conf && sleep 5 diff --git a/tests/tests_wireless_nm.yml b/tests/tests_wireless_nm.yml new file mode 100644 index 0000000..5d44081 --- /dev/null +++ b/tests/tests_wireless_nm.yml @@ -0,0 +1,22 @@ +# 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_wireless.yml' with nm as provider + tasks: + - name: Set network provider to 'nm' + set_fact: + network_provider: nm + + +# workaround for: https://github.com/ansible/ansible/issues/27973 +# There is no way in Ansible to abort a playbook hosts with specific OS +# releases Therefore we include the playbook with the tests only if the hosts +# would support it. +# The test requires or should run with NetworkManager, therefore it cannot run +# on RHEL/CentOS 6 +- import_playbook: playbooks/tests_wireless.yml + when: + - ansible_distribution_major_version != '6' + - ansible_distribution_major_version == '7' diff --git a/tests/unit/test_network_connections.py b/tests/unit/test_network_connections.py index d16d9b2..f51aa3f 100755 --- a/tests/unit/test_network_connections.py +++ b/tests/unit/test_network_connections.py @@ -156,6 +156,7 @@ class TestValidator(unittest.TestCase): "mac": None, "master": None, "ieee802_1x": None, + "wireless": None, "mtu": None, "name": "5", "parent": None, @@ -252,7 +253,10 @@ class TestValidator(unittest.TestCase): continue if "type" not in connection: continue - if connection["type"] in ["macvlan"] or connection["ieee802_1x"]: + if ( + connection["type"] in ["macvlan", "wireless"] + or connection["ieee802_1x"] + ): # initscripts do not support this type. Skip the test. continue content_current = kwargs.get("initscripts_content_current", None) @@ -284,6 +288,50 @@ class TestValidator(unittest.TestCase): v = network_lsr.argument_validator.ArgValidatorStr("state", required=True) self.assertValidationError(v, None) + v = network_lsr.argument_validator.ArgValidatorStr( + "test_max_length", max_length=13 + ) + self.assertEqual("less_than_13", v.validate("less_than_13")) + self.assertValidationError(v, "longer_than_13") + + v = network_lsr.argument_validator.ArgValidatorStr( + "test_min_length", min_length=13 + ) + self.assertEqual("longer_than_13", v.validate("longer_than_13")) + self.assertValidationError(v, "less_than_13") + + v = network_lsr.argument_validator.ArgValidatorStr( + "test_min_max_length", min_length=10, max_length=15 + ) + self.assertEqual("13_characters", v.validate("13_characters")) + self.assertValidationError(v, "too_short") + self.assertValidationError(v, "string_is_too_long") + + self.assertRaises( + ValueError, + network_lsr.argument_validator.ArgValidatorStr, + "non_int", + min_length="string", + ) + self.assertRaises( + ValueError, + network_lsr.argument_validator.ArgValidatorStr, + "non_int", + max_length="string", + ) + self.assertRaises( + ValueError, + network_lsr.argument_validator.ArgValidatorStr, + "negative_int", + min_length=-5, + ) + self.assertRaises( + ValueError, + network_lsr.argument_validator.ArgValidatorStr, + "negative_int", + max_length=-5, + ) + def test_validate_int(self): v = network_lsr.argument_validator.ArgValidatorNum( @@ -399,6 +447,7 @@ class TestValidator(unittest.TestCase): "mac": None, "master": None, "ieee802_1x": None, + "wireless": None, "mtu": None, "name": "5", "parent": None, @@ -450,6 +499,7 @@ class TestValidator(unittest.TestCase): "mac": None, "master": None, "ieee802_1x": None, + "wireless": None, "mtu": None, "name": "5", "parent": None, @@ -495,6 +545,7 @@ class TestValidator(unittest.TestCase): "mac": None, "master": None, "ieee802_1x": None, + "wireless": None, "mtu": None, "name": "5", "parent": None, @@ -583,6 +634,7 @@ class TestValidator(unittest.TestCase): "mac": "52:54:00:44:9f:ba", "master": None, "ieee802_1x": None, + "wireless": None, "mtu": 1450, "name": "prod1", "parent": None, @@ -645,6 +697,7 @@ class TestValidator(unittest.TestCase): "mac": None, "master": None, "ieee802_1x": None, + "wireless": None, "mtu": None, "name": "prod1", "parent": None, @@ -709,6 +762,7 @@ class TestValidator(unittest.TestCase): "mac": "52:54:00:44:9f:ba", "master": None, "ieee802_1x": None, + "wireless": None, "mtu": 1450, "name": "prod1", "parent": None, @@ -765,6 +819,7 @@ class TestValidator(unittest.TestCase): "mac": None, "master": None, "ieee802_1x": None, + "wireless": None, "mtu": None, "name": "prod.100", "parent": "prod1", @@ -848,6 +903,7 @@ class TestValidator(unittest.TestCase): "mac": "52:54:00:44:9f:ba", "master": None, "ieee802_1x": None, + "wireless": None, "mtu": 1450, "name": "prod1", "parent": None, @@ -904,6 +960,7 @@ class TestValidator(unittest.TestCase): "mac": None, "master": None, "ieee802_1x": None, + "wireless": None, "mtu": None, "name": "prod.100", "parent": "prod1", @@ -982,6 +1039,7 @@ class TestValidator(unittest.TestCase): "mac": "33:24:10:24:2f:b9", "master": None, "ieee802_1x": None, + "wireless": None, "mtu": 1450, "name": "eth0-parent", "parent": None, @@ -1033,6 +1091,7 @@ class TestValidator(unittest.TestCase): "macvlan": {"mode": "bridge", "promiscuous": True, "tap": False}, "master": None, "ieee802_1x": None, + "wireless": None, "mtu": None, "name": "veth0.0", "parent": "eth0-parent", @@ -1084,6 +1143,7 @@ class TestValidator(unittest.TestCase): "macvlan": {"mode": "passthru", "promiscuous": False, "tap": True}, "master": None, "ieee802_1x": None, + "wireless": None, "mtu": None, "name": "veth0.1", "parent": "eth0-parent", @@ -1170,6 +1230,7 @@ class TestValidator(unittest.TestCase): "mac": None, "master": None, "ieee802_1x": None, + "wireless": None, "mtu": None, "name": "prod2", "parent": None, @@ -1207,6 +1268,7 @@ class TestValidator(unittest.TestCase): "mac": None, "master": "prod2", "ieee802_1x": None, + "wireless": None, "mtu": None, "name": "prod2-slave1", "parent": None, @@ -1268,6 +1330,7 @@ class TestValidator(unittest.TestCase): "mac": None, "master": None, "ieee802_1x": None, + "wireless": None, "mtu": None, "name": "bond1", "parent": None, @@ -1314,6 +1377,7 @@ class TestValidator(unittest.TestCase): "mac": None, "master": None, "ieee802_1x": None, + "wireless": None, "mtu": None, "name": "bond1", "parent": None, @@ -1370,6 +1434,7 @@ class TestValidator(unittest.TestCase): "mac": "aa:bb:cc:dd:ee:ff", "master": None, "ieee802_1x": None, + "wireless": None, "mtu": None, "name": "5", "parent": None, @@ -1414,6 +1479,7 @@ class TestValidator(unittest.TestCase): "mac": None, "master": None, "ieee802_1x": None, + "wireless": None, "mtu": None, "name": "5", "parent": None, @@ -1486,6 +1552,7 @@ class TestValidator(unittest.TestCase): "mac": None, "master": None, "ieee802_1x": None, + "wireless": None, "mtu": None, "name": "6643-master", "parent": None, @@ -1523,6 +1590,7 @@ class TestValidator(unittest.TestCase): "mac": None, "master": "6643-master", "ieee802_1x": None, + "wireless": None, "mtu": None, "name": "6643", "parent": None, @@ -1576,6 +1644,7 @@ class TestValidator(unittest.TestCase): "mac": None, "master": None, "ieee802_1x": None, + "wireless": None, "mtu": None, "name": "infiniband.1", "parent": None, @@ -1647,6 +1716,7 @@ class TestValidator(unittest.TestCase): "11:22:33:44:55:66:77:88:99:00", "master": None, "ieee802_1x": None, + "wireless": None, "mtu": None, "name": "infiniband.2", "parent": None, @@ -1738,6 +1808,7 @@ class TestValidator(unittest.TestCase): "mac": None, "master": None, "ieee802_1x": None, + "wireless": None, "mtu": None, "name": "555", "parent": None, @@ -1837,6 +1908,7 @@ class TestValidator(unittest.TestCase): "mac": None, "master": None, "ieee802_1x": None, + "wireless": None, "mtu": None, "name": "e556", "parent": None, @@ -1960,6 +2032,7 @@ class TestValidator(unittest.TestCase): "system_ca_certs": False, "domain_suffix_match": None, }, + "wireless": None, "mtu": None, "name": "eth0", "parent": None, @@ -2034,6 +2107,7 @@ class TestValidator(unittest.TestCase): "system_ca_certs": True, "domain_suffix_match": "example.com", }, + "wireless": None, "mtu": None, "name": "eth0", "parent": None, @@ -2108,6 +2182,7 @@ class TestValidator(unittest.TestCase): "system_ca_certs": False, "domain_suffix_match": None, }, + "wireless": None, "mtu": None, "name": "eth0", "parent": None, @@ -2136,6 +2211,150 @@ class TestValidator(unittest.TestCase): ], ) + def test_wireless_psk(self): + """ + Test wireless connection with wpa-psk auth + """ + self.maxDiff = None + self.do_connections_validate( + [ + { + "actions": ["present", "up"], + "autoconnect": True, + "check_iface_exists": True, + "ethtool": ETHTOOL_DEFAULTS, + "force_state_change": None, + "ignore_errors": None, + "interface_name": "wireless1", + "ip": { + "gateway6": None, + "gateway4": None, + "route_metric4": None, + "auto6": True, + "dhcp4": True, + "address": [], + "route_append_only": False, + "rule_append_only": False, + "route": [], + "dns": [], + "dns_search": [], + "route_metric6": None, + "dhcp4_send_hostname": None, + }, + "mac": None, + "master": None, + "ieee802_1x": None, + "wireless": { + "ssid": "test wireless network", + "key_mgmt": "wpa-psk", + "password": "p@55w0rD", + }, + "mtu": None, + "name": "wireless1", + "parent": None, + "persistent_state": "present", + "slave_type": None, + "state": "up", + "type": "wireless", + "wait": None, + "zone": None, + } + ], + [ + { + "name": "wireless1", + "state": "up", + "type": "wireless", + "wireless": { + "ssid": "test wireless network", + "key_mgmt": "wpa-psk", + "password": "p@55w0rD", + }, + } + ], + ) + + def test_wireless_eap(self): + """ + Test wireless connection with wpa-eap + """ + self.maxDiff = None + self.do_connections_validate( + [ + { + "actions": ["present", "up"], + "autoconnect": True, + "check_iface_exists": True, + "ethtool": ETHTOOL_DEFAULTS, + "force_state_change": None, + "ignore_errors": None, + "interface_name": "wireless1", + "ip": { + "gateway6": None, + "gateway4": None, + "route_metric4": None, + "auto6": True, + "dhcp4": True, + "address": [], + "route_append_only": False, + "rule_append_only": False, + "route": [], + "dns": [], + "dns_search": [], + "route_metric6": None, + "dhcp4_send_hostname": None, + }, + "mac": None, + "master": None, + "ieee802_1x": { + "identity": "myhost", + "eap": "tls", + "private_key": "/etc/pki/tls/client.key", + "private_key_password": "p@55w0rD", + "private_key_password_flags": None, + "client_cert": "/etc/pki/tls/client.pem", + "ca_cert": "/etc/pki/tls/cacert.pem", + "ca_path": None, + "system_ca_certs": False, + "domain_suffix_match": None, + }, + "wireless": { + "ssid": "test wireless network", + "password": None, + "key_mgmt": "wpa-eap", + }, + "mtu": None, + "name": "wireless1", + "parent": None, + "persistent_state": "present", + "slave_type": None, + "state": "up", + "type": "wireless", + "wait": None, + "zone": None, + } + ], + [ + { + "name": "wireless1", + "state": "up", + "type": "wireless", + "wireless": { + "ssid": "test wireless network", + "key_mgmt": "wpa-eap", + }, + "ieee802_1x": { + "identity": "myhost", + "eap": "tls", + "private_key": "/etc/pki/tls/client.key", + "private_key_password": "p@55w0rD", + "client_cert": "/etc/pki/tls/client.pem", + "ca_cert": "/etc/pki/tls/cacert.pem", + }, + } + ], + ) + def test_invalid_cert_path(self): """ should fail if a relative path is used for 802.1x certs/keys @@ -2236,23 +2455,41 @@ class TestValidator(unittest.TestCase): 0, ) - def test_802_1x_non_ethernet(self): + def test_802_1x_unsupported_type(self): """ - should fail if a non-ethernet interface has 802.1x settings defined + should fail if a non ethernet/wireless connection has 802.1x settings defined """ + self.do_connections_check_invalid( + [ + { + "name": "bond0", + "state": "up", + "type": "bond", + "ieee802_1x": { + "identity": "myhost", + "eap": "tls", + "private_key": "/etc/pki/tls/client.key", + "client_cert": "/etc/pki/tls/client.pem", + "private_key_password_flags": ["not-required"], + "system_ca_certs": True, + }, + } + ] + ) + def test_wireless_initscripts(self): + """ + should fail to create wireless connection with initscripts + """ input_connections = [ { - "name": "bond0", + "name": "wireless1", "state": "up", - "type": "bond", - "ieee802_1x": { - "identity": "myhost", - "eap": "tls", - "private_key": "/etc/pki/tls/client.key", - "client_cert": "/etc/pki/tls/client.pem", - "private_key_password_flags": ["not-required"], - "system_ca_certs": True, + "type": "wireless", + "wireless": { + "ssid": "test wireless network", + "key_mgmt": "wpa-psk", + "password": "p@55w0rD", }, } ] @@ -2262,11 +2499,115 @@ class TestValidator(unittest.TestCase): self.assertRaises( n.ValidationError, ARGS_CONNECTIONS.validate_connection_one, - VALIDATE_ONE_MODE_NM, + VALIDATE_ONE_MODE_INITSCRIPTS, connections, 0, ) + def test_wireless_unsupported_type(self): + """ + should fail if a non wireless connection has wireless settings defined + """ + self.do_connections_check_invalid( + [ + { + "name": "wireless-bond", + "state": "up", + "type": "bond", + "wireless": { + "ssid": "test wireless network", + "key_mgmt": "wpa-psk", + "password": "p@55w0rD", + }, + } + ] + ) + + def test_wireless_ssid_too_long(self): + """ + should fail if ssid longer than 32 bytes + """ + self.do_connections_check_invalid( + [ + { + "name": "wireless1", + "state": "up", + "type": "wireless", + "wireless": { + "ssid": "test wireless network with ssid too long", + "key_mgmt": "wpa-psk", + "password": "p@55w0rD", + }, + } + ] + ) + + def test_wireless_no_password(self): + """ + should fail if wpa-psk is selected and no password provided + """ + self.do_connections_check_invalid( + [ + { + "name": "wireless1", + "state": "up", + "type": "wireless", + "wireless": { + "ssid": "test wireless network", + "key_mgmt": "wpa-psk", + }, + } + ] + ) + + def test_wireless_password_too_long(self): + """ + should fail if wpa-psk is selected and no password provided + """ + self.do_connections_check_invalid( + [ + { + "name": "wireless1", + "state": "up", + "type": "wireless", + "wireless": { + "ssid": "test wireless network", + "key_mgmt": "wpa-psk", + "password": "This password is too long and should " + "not be able to validate properly", + }, + } + ] + ) + + def test_wireless_no_802_1x_for_wpa_eap(self): + """ + should fail if no 802.1x parameters are defined for a wireless + connection with key_mgmt=wpa-eap + """ + self.do_connections_check_invalid( + [ + { + "name": "wireless1", + "state": "up", + "type": "wireless", + "wireless": { + "ssid": "test wireless network", + "key_mgmt": "wpa-eap", + }, + } + ] + ) + + def test_wireless_no_options_defined(self): + """ + should fail if a connection of type='wireless' does not + have any 'wireless' settings defined + """ + self.do_connections_check_invalid( + [{"name": "wireless1", "state": "up", "type": "wireless"}] + ) + def test_invalid_mac(self): self.maxDiff = None self.do_connections_check_invalid(