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

add support to create L2TP and PPTP VPN connection (#4746)

* add support to create L2TP and PPTP VPN connection

* Update plugins/modules/net_tools/nmcli.py

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

* Update plugins/modules/net_tools/nmcli.py

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

* Update plugins/modules/net_tools/nmcli.py

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

* Update plugins/modules/net_tools/nmcli.py

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

* Update plugins/modules/net_tools/nmcli.py

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

* Update plugins/modules/net_tools/nmcli.py

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

* Update plugins/modules/net_tools/nmcli.py

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

* Update plugins/modules/net_tools/nmcli.py

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

* apply changes pointed on tests and review

- add changelog fragment
- change example code to use jinja2 in place of shell command

* removes trailing whitespace

* Update plugins/modules/net_tools/nmcli.py

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

* Update plugins/modules/net_tools/nmcli.py

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

* removes linux command from examples

* remove unnecessary brakets

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

* remove unnecessary brakets

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

* simplify psk encoding on example

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

* Update plugins/modules/net_tools/nmcli.py

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

* Update plugins/modules/net_tools/nmcli.py

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

* add unit tests

- test unchenged l2tp and pptp vpn connections
- test create l2tp and pptp vpn connections
- fix is_connection_changed to remove default ifname attribuition

* improve tests on vpn.data param

- fix _compare_conn_params to handle vpn.data as lists

* removes block and set_fact from example

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

* makes line shortter to better reading

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

* Update plugins/modules/net_tools/nmcli.py

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

Co-authored-by: Felix Fontein <felix@fontein.de>
This commit is contained in:
José Roberto Emerich Junior 2022-06-06 16:16:27 -03:00 committed by GitHub
parent 8ba3d94740
commit e5e485390d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 304 additions and 4 deletions

View file

@ -0,0 +1,2 @@
minor_changes:
- nmcli - adds ``vpn`` type and parameter for supporting VPN with service type L2TP and PPTP (https://github.com/ansible-collections/community.general/pull/4746).

View file

@ -45,8 +45,8 @@ options:
- The interface to bind the connection to. - The interface to bind the connection to.
- The connection will only be applicable to this interface name. - The connection will only be applicable to this interface name.
- A special value of C('*') can be used for interface-independent connections. - A special value of C('*') can be used for interface-independent connections.
- The ifname argument is mandatory for all connection types except bond, team, bridge and vlan. - The ifname argument is mandatory for all connection types except bond, team, bridge, vlan and vpn.
- This parameter defaults to C(conn_name) when left unset. - This parameter defaults to C(conn_name) when left unset for all connection types except vpn that removes it.
type: str type: str
type: type:
description: description:
@ -55,10 +55,11 @@ options:
- 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 C(gsm) is added in community.general 3.7.0.
- Type C(wireguard) is added in community.general 4.3.0 - Type C(wireguard) is added in community.general 4.3.0.
- Type C(vpn) is added in community.general 5.1.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, gsm, choices: [ bond, bond-slave, bridge, bridge-slave, dummy, ethernet, generic, gre, infiniband, ipip, sit, team, team-slave, vlan, vxlan, wifi, gsm,
wireguard ] wireguard, vpn ]
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.
@ -905,6 +906,58 @@ options:
description: C(NMSettingSecretFlags) indicating how to handle the I(wireguard.private-key) property. description: C(NMSettingSecretFlags) indicating how to handle the I(wireguard.private-key) property.
type: int type: int
choices: [ 0, 1, 2 ] choices: [ 0, 1, 2 ]
vpn:
description:
- Configuration of a VPN connection (PPTP and L2TP).
- In order to use L2TP you need to be sure that C(network-manager-l2tp) - and C(network-manager-l2tp-gnome)
if host has UI - are installed on the host.
type: dict
version_added: 5.1.0
suboptions:
permissions:
description: User that will have permission to use the connection.
type: str
required: true
service-type:
description: This defines the service type of connection.
type: str
required: true
choices: [ pptp, l2tp ]
gateway:
description: The gateway to connection. It can be an IP address (for example C(192.0.2.1))
or a FQDN address (for example C(vpn.example.com)).
type: str
required: true
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
user:
description: Username provided by VPN administrator.
type: str
required: true
ipsec-enabled:
description:
- Enable or disable IPSec tunnel to L2TP host.
- This option is need when C(service-type) is C(l2tp).
type: bool
choices: [ yes, no ]
ipsec-psk:
description:
- The pre-shared key in base64 encoding.
- >
You can encode using this Ansible jinja2 expression: C("0s{{ '[YOUR PRE-SHARED KEY]' | ansible.builtin.b64encode }}").
- This is only used when I(ipsec-enabled=true).
type: str
''' '''
EXAMPLES = r''' EXAMPLES = r'''
@ -1288,6 +1341,23 @@ EXAMPLES = r'''
autoconnect: true autoconnect: true
state: present state: present
- name: >-
Create a VPN L2TP connection for ansible_user to connect on vpn.example.com
authenticating with user 'brittany' and pre-shared key as 'Brittany123'
community.general.nmcli:
type: vpn
conn_name: my-vpn-connection
vpn:
permissions: "{{ ansible_user }}"
service-type: l2tp
gateway: vpn.example.com
password-flags: 2
user: brittany
ipsec-enabled: true
ipsec-psk: "0s{{ 'Brittany123' | ansible.builtin.b64encode }}"
autoconnect: false
state: present
''' '''
RETURN = r"""# RETURN = r"""#
@ -1404,6 +1474,7 @@ class Nmcli(object):
self.wifi_sec = module.params['wifi_sec'] self.wifi_sec = module.params['wifi_sec']
self.gsm = module.params['gsm'] self.gsm = module.params['gsm']
self.wireguard = module.params['wireguard'] self.wireguard = module.params['wireguard']
self.vpn = module.params['vpn']
if self.method4: if self.method4:
self.ipv4_method = self.method4 self.ipv4_method = self.method4
@ -1592,6 +1663,29 @@ class Nmcli(object):
options.update({ options.update({
'wireguard.%s' % name: value, 'wireguard.%s' % name: value,
}) })
elif self.type == 'vpn':
if self.vpn:
vpn_data_values = ''
for name, value in self.vpn.items():
if name == 'service-type':
options.update({
'vpn-type': value,
})
elif name == 'permissions':
options.update({
'connection.permissions': value,
})
else:
if vpn_data_values != '':
vpn_data_values += ', '
if isinstance(value, bool):
value = self.bool_to_string(value)
vpn_data_values += '%s=%s' % (name, value)
options.update({
'vpn.data': vpn_data_values,
})
# 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)
@ -1832,6 +1926,10 @@ class Nmcli(object):
'connection.interface-name': ifname, 'connection.interface-name': ifname,
} }
# VPN doesn't need an interface but if sended it must be a valid interface.
if self.type == 'vpn' and self.ifname is None:
del options['connection.interface-name']
options.update(self.connection_options()) options.update(self.connection_options())
# Constructing the command. # Constructing the command.
@ -1997,6 +2095,9 @@ class Nmcli(object):
current_value = current_value.strip('"') current_value = current_value.strip('"')
if key == self.mtu_setting and self.mtu is None: if key == self.mtu_setting and self.mtu is None:
self.mtu = 0 self.mtu = 0
if key == 'vpn.data':
current_value = list(map(str.strip, current_value.split(',')))
value = list(map(str.strip, value.split(',')))
else: else:
# parameter does not exist # parameter does not exist
current_value = None current_value = None
@ -2025,6 +2126,10 @@ class Nmcli(object):
'connection.interface-name': self.ifname, 'connection.interface-name': self.ifname,
} }
# VPN doesn't need an interface but if sended it must be a valid interface.
if self.type == 'vpn' and self.ifname is None:
del options['connection.interface-name']
if not self.type: if not self.type:
current_con_type = self.show_connection().get('connection.type') current_con_type = self.show_connection().get('connection.type')
if current_con_type: if current_con_type:
@ -2064,6 +2169,7 @@ def main():
'wifi', 'wifi',
'gsm', 'gsm',
'wireguard', 'wireguard',
'vpn',
]), ]),
ip4=dict(type='list', elements='str'), ip4=dict(type='list', elements='str'),
gw4=dict(type='str'), gw4=dict(type='str'),
@ -2163,6 +2269,7 @@ def main():
wifi_sec=dict(type='dict', no_log=True), wifi_sec=dict(type='dict', no_log=True),
gsm=dict(type='dict'), gsm=dict(type='dict'),
wireguard=dict(type='dict'), wireguard=dict(type='dict'),
vpn=dict(type='dict'),
), ),
mutually_exclusive=[['never_default4', 'gw4'], mutually_exclusive=[['never_default4', 'gw4'],
['routes4_extended', 'routes4'], ['routes4_extended', 'routes4'],

View file

@ -98,6 +98,12 @@ TESTCASE_CONNECTION = [
'state': 'absent', 'state': 'absent',
'_ansible_check_mode': True, '_ansible_check_mode': True,
}, },
{
'type': 'vpn',
'conn_name': 'non_existent_nw_device',
'state': 'absent',
'_ansible_check_mode': True,
},
] ]
TESTCASE_GENERIC = [ TESTCASE_GENERIC = [
@ -1177,6 +1183,69 @@ wireguard.ip4-auto-default-route: -1 (default)
wireguard.ip6-auto-default-route: -1 (default) wireguard.ip6-auto-default-route: -1 (default)
""" """
TESTCASE_VPN_L2TP = [
{
'type': 'vpn',
'conn_name': 'vpn_l2tp',
'vpn': {
'permissions': 'brittany',
'service-type': 'l2tp',
'gateway': 'vpn.example.com',
'password-flags': '2',
'user': 'brittany',
'ipsec-enabled': 'true',
'ipsec-psk': 'QnJpdHRhbnkxMjM=',
},
'autoconnect': 'false',
'state': 'present',
'_ansible_check_mode': False,
},
]
TESTCASE_VPN_L2TP_SHOW_OUTPUT = """\
connection.id: vpn_l2tp
connection.type: vpn
connection.autoconnect: no
connection.permissions: brittany
ipv4.method: auto
ipv6.method: auto
vpn-type: l2tp
vpn.service-type: org.freedesktop.NetworkManager.l2tp
vpn.data: gateway=vpn.example.com, password-flags=2, user=brittany, ipsec-enabled=true, ipsec-psk=QnJpdHRhbnkxMjM=
vpn.secrets: ipsec-psk = QnJpdHRhbnkxMjM=
vpn.persistent: no
vpn.timeout: 0
"""
TESTCASE_VPN_PPTP = [
{
'type': 'vpn',
'conn_name': 'vpn_pptp',
'vpn': {
'permissions': 'brittany',
'service-type': 'pptp',
'gateway': 'vpn.example.com',
'password-flags': '2',
'user': 'brittany',
},
'autoconnect': 'false',
'state': 'present',
'_ansible_check_mode': False,
},
]
TESTCASE_VPN_PPTP_SHOW_OUTPUT = """\
connection.id: vpn_pptp
connection.type: vpn
connection.autoconnect: no
connection.permissions: brittany
ipv4.method: auto
ipv6.method: auto
vpn-type: pptp
vpn.service-type: org.freedesktop.NetworkManager.pptp
vpn.data: password-flags=2, gateway=vpn.example.com, user=brittany
"""
def mocker_set(mocker, def mocker_set(mocker,
connection_exists=False, connection_exists=False,
@ -1547,6 +1616,20 @@ def mocked_wireguard_connection_unchanged(mocker):
execute_return=(0, TESTCASE_WIREGUARD_SHOW_OUTPUT, "")) execute_return=(0, TESTCASE_WIREGUARD_SHOW_OUTPUT, ""))
@pytest.fixture
def mocked_vpn_l2tp_connection_unchanged(mocker):
mocker_set(mocker,
connection_exists=True,
execute_return=(0, TESTCASE_VPN_L2TP_SHOW_OUTPUT, ""))
@pytest.fixture
def mocked_vpn_pptp_connection_unchanged(mocker):
mocker_set(mocker,
connection_exists=True,
execute_return=(0, TESTCASE_VPN_PPTP_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):
""" """
@ -3456,3 +3539,111 @@ def test_wireguard_mod(mocked_generic_connection_modify, capfd):
results = json.loads(out) results = json.loads(out)
assert not results.get('failed') assert not results.get('failed')
assert results['changed'] assert results['changed']
@pytest.mark.parametrize('patch_ansible_module', TESTCASE_VPN_L2TP, indirect=['patch_ansible_module'])
def test_vpn_l2tp_connection_unchanged(mocked_vpn_l2tp_connection_unchanged, capfd):
"""
Test : L2TP VPN 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']
@pytest.mark.parametrize('patch_ansible_module', TESTCASE_VPN_PPTP, indirect=['patch_ansible_module'])
def test_vpn_pptp_connection_unchanged(mocked_vpn_pptp_connection_unchanged, capfd):
"""
Test : PPTP VPN 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']
@pytest.mark.parametrize('patch_ansible_module', TESTCASE_VPN_L2TP, indirect=['patch_ansible_module'])
def test_create_vpn_l2tp(mocked_generic_connection_create, capfd):
"""
Test : Create L2TP VPN connection
"""
with pytest.raises(SystemExit):
nmcli.main()
assert nmcli.Nmcli.execute_command.call_count == 1
arg_list = nmcli.Nmcli.execute_command.call_args_list
add_args, add_kw = arg_list[0]
assert add_args[0][0] == '/usr/bin/nmcli'
assert add_args[0][1] == 'con'
assert add_args[0][2] == 'add'
assert add_args[0][3] == 'type'
assert add_args[0][4] == 'vpn'
assert add_args[0][5] == 'con-name'
assert add_args[0][6] == 'vpn_l2tp'
add_args_text = list(map(to_text, add_args[0]))
for param in ['connection.autoconnect', 'no',
'connection.permissions', 'brittany',
'vpn.data', 'vpn-type', 'l2tp',
]:
assert param in add_args_text
vpn_data_index = add_args_text.index('vpn.data') + 1
args_vpn_data = add_args_text[vpn_data_index]
for vpn_data in ['gateway=vpn.example.com', 'password-flags=2', 'user=brittany', 'ipsec-enabled=true', 'ipsec-psk=QnJpdHRhbnkxMjM=']:
assert vpn_data in args_vpn_data
out, err = capfd.readouterr()
results = json.loads(out)
assert not results.get('failed')
assert results['changed']
@pytest.mark.parametrize('patch_ansible_module', TESTCASE_VPN_PPTP, indirect=['patch_ansible_module'])
def test_create_vpn_pptp(mocked_generic_connection_create, capfd):
"""
Test : Create PPTP VPN connection
"""
with pytest.raises(SystemExit):
nmcli.main()
assert nmcli.Nmcli.execute_command.call_count == 1
arg_list = nmcli.Nmcli.execute_command.call_args_list
add_args, add_kw = arg_list[0]
assert add_args[0][0] == '/usr/bin/nmcli'
assert add_args[0][1] == 'con'
assert add_args[0][2] == 'add'
assert add_args[0][3] == 'type'
assert add_args[0][4] == 'vpn'
assert add_args[0][5] == 'con-name'
assert add_args[0][6] == 'vpn_pptp'
add_args_text = list(map(to_text, add_args[0]))
for param in ['connection.autoconnect', 'no',
'connection.permissions', 'brittany',
'vpn.data', 'vpn-type', 'pptp',
]:
assert param in add_args_text
vpn_data_index = add_args_text.index('vpn.data') + 1
args_vpn_data = add_args_text[vpn_data_index]
for vpn_data in ['password-flags=2', 'gateway=vpn.example.com', 'user=brittany']:
assert vpn_data in args_vpn_data
out, err = capfd.readouterr()
results = json.loads(out)
assert not results.get('failed')
assert results['changed']