1
0
Fork 0
mirror of https://github.com/ansible-collections/community.general.git synced 2024-09-14 20:13:21 +02:00

nmcli: Support GSM connections (#3313)

* nmcli: Support GSM connections

* Add GSM support
* Add GSM unit test

* nmcli: Add changelog fragment

* nmcli: Fix GSM unit test

* Fix copy-paste error in test_gsm_mod

* nmcli: Fix yaml formatting

* nmcli: Fix yaml formatting

* nmcli: Fix typeerror

* type must be str not string

* nmcli: Fix gsm_show_output

* gsm.username did not match input

* nmcli: Fix gsm_show_output

* doublechecked generated output with test-client

* nmcli: GSM fix unit test

* Removed `mocked_gsm_connection_unchanged`

* Revert "nmcli: GSM fix unit test"

This reverts commit 2d112b779a.

* nmcli: gsm fix unit test

* Add needed output to `TESTCASE_GSM_SHOW_OUTPUT`
* Move `mocked_gsm_connection_unchanged`to sort correctly

* nmcli: gsm fix _compare_conn_params

* Strip double-qoutes of gsm.apn if exist

* nmcli: GSM apply suggestions from code review

Co-authored-by: Ajpantuso <ajpantuso@gmail.com>

* nmcli: GSM: Fix documentation

* Shorten too long lines

* nmcli: GSM apply suggestions from code review

Co-authored-by: Ajpantuso <ajpantuso@gmail.com>

* nmcli: GSM add version

Co-authored-by: Felix Fontein <felix@fontein.de>

Co-authored-by: Ajpantuso <ajpantuso@gmail.com>
Co-authored-by: Felix Fontein <felix@fontein.de>
This commit is contained in:
Manuel Gayer 2021-09-05 18:28:04 +02:00 committed by GitHub
parent 3502f3b486
commit 76317d1f64
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 265 additions and 3 deletions

View file

@ -0,0 +1,2 @@
minor_changes:
- "nmcli - add ``gsm`` support (https://github.com/ansible-collections/community.general/pull/3313)."

View file

@ -54,8 +54,9 @@ options:
- Type C(dummy) is added in community.general 3.5.0. - Type C(dummy) is added in community.general 3.5.0.
- Type C(generic) is added in Ansible 2.5. - Type C(generic) is added in Ansible 2.5.
- Type C(infiniband) is added in community.general 2.0.0. - Type C(infiniband) is added in community.general 2.0.0.
- Type C(gsm) is added in community.general 3.7.0.
type: str 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: mode:
description: description:
- This is the type of device or network connection that you wish to create for a bond or bridge. - This is the type of device or network connection that you wish to create for a bond or bridge.
@ -183,7 +184,7 @@ options:
mtu: mtu:
description: 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. - 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. - This parameter defaults to C(1500) when unset.
type: int type: int
dhcp_client_id: dhcp_client_id:
@ -643,6 +644,101 @@ options:
type: bool type: bool
default: false default: false
version_added: 3.6.0 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
version_added: 3.7.0
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: str
auto-config:
description: When C(true), the settings such as I(gsm.apn), I(gsm.username), or I(gsm.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: str
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: str
number:
description: Legacy setting that used to help establishing PPP data sessions for GSM-based modems.
type: str
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: str
password-flags:
description:
- NMSettingSecretFlags indicating how to handle the I(password) property.
- 'Following choices are allowed:
C(0) B(NONE): The system is responsible for providing and storing this secret (default),
C(1) B(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) B(NOT_SAVED): This secret should not be saved, but should be requested from the user each time it is needed
C(4) B(NOT_REQUIRED): In situations where it cannot be automatically determined that the secret is required
(some VPNs and PPP providers do not 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: str
pin-flags:
description:
- NMSettingSecretFlags indicating how to handle the I(gsm.pin) property.
- See I(gsm.password-flags) for NMSettingSecretFlags choices.
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(gsm.device-id) which contains a SIM card matching
the given identifier.'
type: str
sim-operator-id:
description:
- A MCC/MNC string like C(310260) or C(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(gsm.device-id) and I(gsm.sim-id) which contains a SIM card
provisioned by the given operator.'
type: str
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''' EXAMPLES = r'''
@ -979,6 +1075,19 @@ EXAMPLES = r'''
autoconnect: true autoconnect: true
state: present 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"""# RETURN = r"""#
@ -1086,6 +1195,7 @@ class Nmcli(object):
self.ssid = module.params['ssid'] self.ssid = module.params['ssid']
self.wifi = module.params['wifi'] self.wifi = module.params['wifi']
self.wifi_sec = module.params['wifi_sec'] self.wifi_sec = module.params['wifi_sec']
self.gsm = module.params['gsm']
if self.method4: if self.method4:
self.ipv4_method = self.method4 self.ipv4_method = self.method4
@ -1243,6 +1353,12 @@ class Nmcli(object):
options.update({ options.update({
'802-11-wireless-security.%s' % name: value '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. # Convert settings values based on the situation.
for setting, value in options.items(): for setting, value in options.items():
setting_type = self.settings_type(setting) setting_type = self.settings_type(setting)
@ -1280,7 +1396,8 @@ class Nmcli(object):
'sit', 'sit',
'team', 'team',
'vlan', 'vlan',
'wifi' 'wifi',
'gsm',
) )
@property @property
@ -1573,6 +1690,10 @@ class Nmcli(object):
value = value.upper() value = value.upper()
# ensure current_value is also converted to uppercase in case nmcli changes behaviour # ensure current_value is also converted to uppercase in case nmcli changes behaviour
current_value = current_value.upper() current_value = current_value.upper()
if key == 'gsm.apn':
# Depending on version nmcli adds double-qoutes to gsm.apn
# Need to strip them in order to compare both
current_value = current_value.strip('"')
else: else:
# parameter does not exist # parameter does not exist
current_value = None current_value = None
@ -1630,6 +1751,7 @@ def main():
'vlan', 'vlan',
'vxlan', 'vxlan',
'wifi', 'wifi',
'gsm',
]), ]),
ip4=dict(type='str'), ip4=dict(type='str'),
gw4=dict(type='str'), gw4=dict(type='str'),
@ -1700,6 +1822,7 @@ def main():
ssid=dict(type='str'), ssid=dict(type='str'),
wifi=dict(type='dict'), wifi=dict(type='dict'),
wifi_sec=dict(type='dict', no_log=True), wifi_sec=dict(type='dict', no_log=True),
gsm=dict(type='dict'),
), ),
mutually_exclusive=[['never_default4', 'gw4']], mutually_exclusive=[['never_default4', 'gw4']],
required_if=[("type", "wifi", [("ssid")])], required_if=[("type", "wifi", [("ssid")])],

View file

@ -86,6 +86,12 @@ TESTCASE_CONNECTION = [
'state': 'absent', 'state': 'absent',
'_ansible_check_mode': True, '_ansible_check_mode': True,
}, },
{
'type': 'gsm',
'conn_name': 'non_existent_nw_device',
'state': 'absent',
'_ansible_check_mode': True,
},
] ]
TESTCASE_GENERIC = [ TESTCASE_GENERIC = [
@ -603,6 +609,7 @@ TESTCASE_DEFAULT_SECURE_WIRELESS_SHOW_OUTPUT = \
802-11-wireless-security.fils: 0 (default) 802-11-wireless-security.fils: 0 (default)
""" """
TESTCASE_DUMMY_STATIC = [ TESTCASE_DUMMY_STATIC = [
{ {
'type': 'dummy', 'type': 'dummy',
@ -638,6 +645,53 @@ 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
connection.autoconnect: yes
ipv4.method: auto
ipv4.ignore-auto-dns: no
ipv4.ignore-auto-routes: no
ipv4.never-default: no
ipv4.may-fail: yes
ipv6.method: auto
ipv6.ignore-auto-dns: no
ipv6.ignore-auto-routes: no
gsm.auto-config: no
gsm.number: --
gsm.username: t-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
"""
def mocker_set(mocker, def mocker_set(mocker,
connection_exists=False, connection_exists=False,
execute_return=(0, "", ""), execute_return=(0, "", ""),
@ -863,6 +917,13 @@ def mocked_dummy_connection_static_unchanged(mocker):
execute_return=(0, TESTCASE_DUMMY_STATIC_SHOW_OUTPUT, "")) execute_return=(0, TESTCASE_DUMMY_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.mark.parametrize('patch_ansible_module', TESTCASE_BOND, indirect=['patch_ansible_module']) @pytest.mark.parametrize('patch_ansible_module', TESTCASE_BOND, indirect=['patch_ansible_module'])
def test_bond_connection_create(mocked_generic_connection_create, capfd): def test_bond_connection_create(mocked_generic_connection_create, capfd):
""" """
@ -2162,3 +2223,79 @@ def test_dummy_connection_static_unchanged(mocked_dummy_connection_static_unchan
results = json.loads(out) results = json.loads(out)
assert not results.get('failed') assert not results.get('failed')
assert not results['changed'] 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.username', 't-mobile',
'gsm.password', 'tm']:
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']