diff --git a/plugins/modules/net_tools/nmcli.py b/plugins/modules/net_tools/nmcli.py index 7bc8a6b775..13eee10787 100644 --- a/plugins/modules/net_tools/nmcli.py +++ b/plugins/modules/net_tools/nmcli.py @@ -55,7 +55,7 @@ options: - Type C(generic) is added in Ansible 2.5. - Type C(infiniband) is added in community.general 2.0.0. type: str - choices: [ bond, bond-slave, bridge, bridge-slave, dummy, ethernet, generic, gre, infiniband, ipip, sit, team, team-slave, vlan, vxlan, wifi ] + choices: [ bond, bond-slave, bridge, bridge-slave, dummy, ethernet, generic, gre, infiniband, ipip, sit, team, team-slave, vlan, vxlan, wifi, gsm ] mode: description: - This is the type of device or network connection that you wish to create for a bond or bridge. @@ -183,7 +183,7 @@ options: mtu: description: - The connection MTU, e.g. 9000. This can't be applied when creating the interface and is done once the interface has been created. - - Can be used when modifying Team, VLAN, Ethernet (Future plans to implement wifi, pppoe, infiniband) + - Can be used when modifying Team, VLAN, Ethernet (Future plans to implement wifi, gsm, pppoe, infiniband) - This parameter defaults to C(1500) when unset. type: int dhcp_client_id: @@ -643,6 +643,98 @@ options: type: bool default: false version_added: 3.6.0 + gsm: + description: + - The configuration of the GSM connection. + - Note the list of suboption attributes may vary depending on which version of NetworkManager/nmcli is installed on the host. + - 'An up-to-date list of supported attributes can be found here: + U(https://networkmanager.dev/docs/api/latest/settings-gsm.html)' + - 'For instance to use apn, pin, username and password:' + C({apn: provider.apn, pin: 1234, username: apn.username, password: apn.password})' + type: dict + suboptions: + apn: + description: + - The GPRS Access Point Name specifying the APN used when establishing a data session with the GSM-based network. + - The APN often determines how the user will be billed for their network usage and whether the user has access to the Internet or + just a provider-specific walled-garden, so it is important to use the correct APN for the user's mobile broadband plan. + - The APN may only be composed of the characters a-z, 0-9, ., and - per GSM 03.60 Section 14.9. + type: string + auto-config: + description: When C(true), the settings such as C(APN), username, or password will default to values that match the network + the modem will register to in the Mobile Broadband Provider database. + type: bool + default: false + device-id: + description: + - The device unique identifier (as given by the C(WWAN) management service) which this connection applies to. + - If given, the connection will only apply to the specified device. + type: string + home-only: + description: + - When C(true), only connections to the home network will be allowed. + - Connections to roaming networks will not be made. + type: bool + default: false + mtu: + description: If non-zero, only transmit packets of the specified size or smaller, breaking larger packets up into multiple Ethernet frames. + type: int + default: 0 + network-id: + description: + - The Network ID (GSM LAI format, ie MCC-MNC) to force specific network registration. + - If the Network ID is specified, NetworkManager will attempt to force the device to register only on the specified network. + - This can be used to ensure that the device does not roam when direct roaming control of the device is not otherwise possible. + type: string + number: + description: Legacy setting that used to help establishing PPP data sessions for GSM-based modems. + type: string + password: + description: + - The password used to authenticate with the network, if required. + - Many providers do not require a password, or accept any password. + - But if a password is required, it is specified here. + type: string + password-flags: + description: + - NMSettingSecretFlags indicating how to handle the I(password) property. + - Following choices are allowed: + C(0) I(NONE): The system is responsible for providing and storing this secret (default), + C(1) I(AGENT_OWNED): A user secret agent is responsible for providing and storing this secret; when it is required agents will be + asked to retrieve it + C(2) I(NOT_SAVED): This secret should not be saved, but should be requested from the user each time it is needed + C(4) I(NOT_REQUIRED): in situations where it cannot be automatically determined that the secret is required + (some VPNs and PPP providers don't require all secrets) this flag indicates that the specific secret is not required + type: int + choices: [ 0, 1, 2 , 4 ] + default: 0 + pin: + description: + - If the SIM is locked with a PIN it must be unlocked before any other operations are requested. + - Specify the PIN here to allow operation of the device. + type: string + pin-flags: + description: + - NMSettingSecretFlags indicating how to handle the I(pin) property. + - See C(password-flags) for NMSettingSecretFlags coices + type: int + choices: [ 0, 1, 2 , 4 ] + default: 0 + sim-id: + description: + - The SIM card unique identifier (as given by the C(WWAN) management service) which this connection applies to. + - If given, the connection will apply to any device also allowed by I(device-id) which contains a SIM card matching the given identifier. + type: string + sim-operator-id: + description: + - A MCC/MNC string like I(310260) or I(21601I) identifying the specific mobile network operator which this connection applies to. + - If given, the connection will apply to any device also allowed by I(device-id) and I(sim-id) which contains a SIM card provisioned by the given operator. + type: string + username: + description: + - The username used to authenticate with the network, if required. + - Many providers do not require a username, or accept any username. + - But if a username is required, it is specified here. ''' EXAMPLES = r''' @@ -979,6 +1071,19 @@ EXAMPLES = r''' autoconnect: true state: present +- name: Create a gsm connection + community.general.nmcli: + type: gsm + conn_name: my-gsm-provider + ifname: cdc-wdm0 + gsm: + apn: my.provider.apn + username: my-provider-username + password: my-provider-password + pin: my-sim-pin + autoconnect: true + state: present + ''' RETURN = r"""# @@ -1086,6 +1191,7 @@ class Nmcli(object): self.ssid = module.params['ssid'] self.wifi = module.params['wifi'] self.wifi_sec = module.params['wifi_sec'] + self.gsm = module.params['gsm'] if self.method4: self.ipv4_method = self.method4 @@ -1243,6 +1349,12 @@ class Nmcli(object): options.update({ '802-11-wireless-security.%s' % name: value }) + elif self.type == 'gsm': + if self.gsm: + for name, value in self.gsm.items(): + options.update({ + 'gsm.%s' % name: value, + }) # Convert settings values based on the situation. for setting, value in options.items(): setting_type = self.settings_type(setting) @@ -1280,7 +1392,8 @@ class Nmcli(object): 'sit', 'team', 'vlan', - 'wifi' + 'wifi', + 'gsm', ) @property @@ -1630,6 +1743,7 @@ def main(): 'vlan', 'vxlan', 'wifi', + 'gsm', ]), ip4=dict(type='str'), gw4=dict(type='str'), @@ -1700,6 +1814,7 @@ def main(): ssid=dict(type='str'), wifi=dict(type='dict'), wifi_sec=dict(type='dict', no_log=True), + gsm=dict(type='dict'), ), mutually_exclusive=[['never_default4', 'gw4']], required_if=[("type", "wifi", [("ssid")])], diff --git a/tests/unit/plugins/modules/net_tools/test_nmcli.py b/tests/unit/plugins/modules/net_tools/test_nmcli.py index 9277bd5fb6..24c4f3ea6b 100644 --- a/tests/unit/plugins/modules/net_tools/test_nmcli.py +++ b/tests/unit/plugins/modules/net_tools/test_nmcli.py @@ -86,6 +86,12 @@ TESTCASE_CONNECTION = [ 'state': 'absent', '_ansible_check_mode': True, }, + { + 'type': 'gsm', + 'conn_name': 'non_existent_nw_device', + 'state': 'absent', + '_ansible_check_mode': True, + }, ] TESTCASE_GENERIC = [ @@ -603,6 +609,7 @@ TESTCASE_DEFAULT_SECURE_WIRELESS_SHOW_OUTPUT = \ 802-11-wireless-security.fils: 0 (default) """ + TESTCASE_DUMMY_STATIC = [ { 'type': 'dummy', @@ -638,6 +645,44 @@ ipv6.addresses: 2001:db8::1/128 """ +TESTCASE_GSM = [ + { + 'type': 'gsm', + 'conn_name': 'non_existent_nw_device', + 'ifname': 'gsm_non_existant', + 'gsm': { + 'apn': 'internet.telekom', + 'username': 't-mobile', + 'password': 'tm', + 'pin': '1234', + }, + 'method4': 'auto', + 'state': 'present', + '_ansible_check_mode': False, + } +] + +TESTCASE_GSM_SHOW_OUTPUT = """\ +connection.id: non_existent_nw_device +connection.type: gsm +connection.interface-name: gsm_non_existant +gsm.number: *99# +gsm.username: tm-mobile +gsm.password: tm +gsm.password-flags: 0 (none) +gsm.apn: internet.telekom +gsm.network-id: -- +gsm.pin: 1234 +gsm.pin-flags: 0 (none) +gsm.home-only: no +gsm.device-id: -- +gsm.sim-id: -- +gsm.sim-operator-id: -- +gsm.mtu: auto +gsm.auto-config: no +""" + + def mocker_set(mocker, connection_exists=False, execute_return=(0, "", ""), @@ -789,6 +834,13 @@ def mocked_ethernet_connection_static_unchanged(mocker): execute_return=(0, TESTCASE_ETHERNET_STATIC_SHOW_OUTPUT, "")) +@pytest.fixture +def mocked_gsm_connection_unchanged(mocker): + mocker_set(mocker, + connection_exists=True, + execute_return=(0, TESTCASE_GSM_SHOW_OUTPUT, "")) + + @pytest.fixture def mocked_ethernet_connection_dhcp_to_static(mocker): mocker_set(mocker, @@ -2162,3 +2214,84 @@ def test_dummy_connection_static_unchanged(mocked_dummy_connection_static_unchan results = json.loads(out) assert not results.get('failed') assert not results['changed'] + + +@pytest.mark.parametrize('patch_ansible_module', TESTCASE_GSM, indirect=['patch_ansible_module']) +def test_create_gsm(mocked_generic_connection_create, capfd): + """ + Test if gsm created + """ + with pytest.raises(SystemExit): + nmcli.main() + + assert nmcli.Nmcli.execute_command.call_count == 1 + arg_list = nmcli.Nmcli.execute_command.call_args_list + args, kwargs = arg_list[0] + + assert args[0][0] == '/usr/bin/nmcli' + assert args[0][1] == 'con' + assert args[0][2] == 'add' + assert args[0][3] == 'type' + assert args[0][4] == 'gsm' + assert args[0][5] == 'con-name' + assert args[0][6] == 'non_existent_nw_device' + + args_text = list(map(to_text, args[0])) + for param in [ + 'connection.interface-name', 'gsm_non_existant', + 'gsm.apn', 'internet.telekom', + 'gsm.username', 't-mobile', + 'gsm.password', 'tm', + 'gsm.pin', '1234', + ]: + assert param in args_text + + out, err = capfd.readouterr() + results = json.loads(out) + assert not results.get('failed') + assert results['changed'] + + +@pytest.mark.parametrize('patch_ansible_module', TESTCASE_GSM, indirect=['patch_ansible_module']) +def test_gsm_mod(mocked_generic_connection_modify, capfd): + """ + Test if gsm modified + """ + with pytest.raises(SystemExit): + nmcli.main() + + assert nmcli.Nmcli.execute_command.call_count == 1 + arg_list = nmcli.Nmcli.execute_command.call_args_list + args, kwargs = arg_list[0] + + assert args[0][0] == '/usr/bin/nmcli' + assert args[0][1] == 'con' + assert args[0][2] == 'modify' + assert args[0][3] == 'non_existent_nw_device' + + args_text = list(map(to_text, args[0])) + for param in [ + 'gsm.apn', 'web.vodafone.de', + 'gsm.username', '', + 'gsm.password', '', + ]: + assert param in args_text + + out, err = capfd.readouterr() + results = json.loads(out) + assert not results.get('failed') + assert results['changed'] + + +@pytest.mark.parametrize('patch_ansible_module', TESTCASE_GSM, indirect=['patch_ansible_module']) +def test_gsm_connection_unchanged(mocked_gsm_connection_unchanged, capfd): + """ + Test if gsm connection unchanged + """ + with pytest.raises(SystemExit): + nmcli.main() + + out, err = capfd.readouterr() + results = json.loads(out) + assert not results.get('failed') + assert not results['changed'] \ No newline at end of file