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:
parent
8ba3d94740
commit
e5e485390d
3 changed files with 304 additions and 4 deletions
2
changelogs/fragments/4746-add-vpn-support-nmcli.yaml
Normal file
2
changelogs/fragments/4746-add-vpn-support-nmcli.yaml
Normal 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).
|
|
@ -45,8 +45,8 @@ options:
|
|||
- The interface to bind the connection to.
|
||||
- The connection will only be applicable to this interface name.
|
||||
- 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.
|
||||
- This parameter defaults to C(conn_name) when left unset.
|
||||
- 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 for all connection types except vpn that removes it.
|
||||
type: str
|
||||
type:
|
||||
description:
|
||||
|
@ -55,10 +55,11 @@ options:
|
|||
- Type C(generic) is added in Ansible 2.5.
|
||||
- Type C(infiniband) is added in community.general 2.0.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
|
||||
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:
|
||||
description:
|
||||
- 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.
|
||||
type: int
|
||||
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'''
|
||||
|
@ -1288,6 +1341,23 @@ EXAMPLES = r'''
|
|||
autoconnect: true
|
||||
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"""#
|
||||
|
@ -1404,6 +1474,7 @@ class Nmcli(object):
|
|||
self.wifi_sec = module.params['wifi_sec']
|
||||
self.gsm = module.params['gsm']
|
||||
self.wireguard = module.params['wireguard']
|
||||
self.vpn = module.params['vpn']
|
||||
|
||||
if self.method4:
|
||||
self.ipv4_method = self.method4
|
||||
|
@ -1592,6 +1663,29 @@ class Nmcli(object):
|
|||
options.update({
|
||||
'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.
|
||||
for setting, value in options.items():
|
||||
setting_type = self.settings_type(setting)
|
||||
|
@ -1832,6 +1926,10 @@ class Nmcli(object):
|
|||
'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())
|
||||
|
||||
# Constructing the command.
|
||||
|
@ -1997,6 +2095,9 @@ class Nmcli(object):
|
|||
current_value = current_value.strip('"')
|
||||
if key == self.mtu_setting and self.mtu is None:
|
||||
self.mtu = 0
|
||||
if key == 'vpn.data':
|
||||
current_value = list(map(str.strip, current_value.split(',')))
|
||||
value = list(map(str.strip, value.split(',')))
|
||||
else:
|
||||
# parameter does not exist
|
||||
current_value = None
|
||||
|
@ -2025,6 +2126,10 @@ class Nmcli(object):
|
|||
'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:
|
||||
current_con_type = self.show_connection().get('connection.type')
|
||||
if current_con_type:
|
||||
|
@ -2064,6 +2169,7 @@ def main():
|
|||
'wifi',
|
||||
'gsm',
|
||||
'wireguard',
|
||||
'vpn',
|
||||
]),
|
||||
ip4=dict(type='list', elements='str'),
|
||||
gw4=dict(type='str'),
|
||||
|
@ -2163,6 +2269,7 @@ def main():
|
|||
wifi_sec=dict(type='dict', no_log=True),
|
||||
gsm=dict(type='dict'),
|
||||
wireguard=dict(type='dict'),
|
||||
vpn=dict(type='dict'),
|
||||
),
|
||||
mutually_exclusive=[['never_default4', 'gw4'],
|
||||
['routes4_extended', 'routes4'],
|
||||
|
|
|
@ -98,6 +98,12 @@ TESTCASE_CONNECTION = [
|
|||
'state': 'absent',
|
||||
'_ansible_check_mode': True,
|
||||
},
|
||||
{
|
||||
'type': 'vpn',
|
||||
'conn_name': 'non_existent_nw_device',
|
||||
'state': 'absent',
|
||||
'_ansible_check_mode': True,
|
||||
},
|
||||
]
|
||||
|
||||
TESTCASE_GENERIC = [
|
||||
|
@ -1177,6 +1183,69 @@ wireguard.ip4-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,
|
||||
connection_exists=False,
|
||||
|
@ -1547,6 +1616,20 @@ def mocked_wireguard_connection_unchanged(mocker):
|
|||
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'])
|
||||
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)
|
||||
assert not results.get('failed')
|
||||
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']
|
||||
|
|
Loading…
Reference in a new issue