mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
nmcli: Add wireguard connection type support (#3985)
* nmcli: add wireguard connection type * nmcli: fix wireguard unit tests * nmcli: set ipv4.method to disabled if ip4 not set Method 'auto' is not supported for WireGuard * nmcli: add wireguard documentation * nmcli: clean up wireguard documentation * nmcli: add wireguard changelog fragment * nmcli: fix wireguard documentation * Apply suggestions from code review Co-authored-by: Andrew Pantuso <ajpantuso@gmail.com> Co-authored-by: Andrew Pantuso <ajpantuso@gmail.com>
This commit is contained in:
parent
26a91e811f
commit
4ea58fba75
3 changed files with 218 additions and 3 deletions
|
@ -0,0 +1,2 @@
|
||||||
|
minor_changes:
|
||||||
|
- nmcli - add ``wireguard`` connection type (https://github.com/ansible-collections/community.general/pull/3985).
|
|
@ -55,8 +55,10 @@ 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: 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 ]
|
||||||
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.
|
||||||
|
@ -754,6 +756,62 @@ options:
|
||||||
- The username used to authenticate with the network, if required.
|
- The username used to authenticate with the network, if required.
|
||||||
- Many providers do not require a username, or accept any username.
|
- Many providers do not require a username, or accept any username.
|
||||||
- But if a username is required, it is specified here.
|
- But if a username is required, it is specified here.
|
||||||
|
wireguard:
|
||||||
|
description:
|
||||||
|
- The configuration of the Wireguard 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-wireguard.html).'
|
||||||
|
- 'For instance to configure a listen port:
|
||||||
|
C({listen-port: 12345}).'
|
||||||
|
type: dict
|
||||||
|
version_added: 4.3.0
|
||||||
|
suboptions:
|
||||||
|
fwmark:
|
||||||
|
description:
|
||||||
|
- The 32-bit fwmark for outgoing packets.
|
||||||
|
- The use of fwmark is optional and is by default off. Setting it to 0 disables it.
|
||||||
|
- Note that I(wireguard.ip4-auto-default-route) or I(wireguard.ip6-auto-default-route) enabled, implies to automatically choose a fwmark.
|
||||||
|
type: int
|
||||||
|
ip4-auto-default-route:
|
||||||
|
description:
|
||||||
|
- Whether to enable special handling of the IPv4 default route.
|
||||||
|
- If enabled, the IPv4 default route from I(wireguard.peer-routes) will be placed to a dedicated routing-table and two policy
|
||||||
|
routing rules will be added.
|
||||||
|
- The fwmark number is also used as routing-table for the default-route, and if fwmark is zero, an unused fwmark/table is chosen
|
||||||
|
automatically. This corresponds to what wg-quick does with Table=auto and what WireGuard calls "Improved Rule-based Routing"
|
||||||
|
type: bool
|
||||||
|
ip6-auto-default-route:
|
||||||
|
description:
|
||||||
|
- Like I(wireguard.ip4-auto-default-route), but for the IPv6 default route.
|
||||||
|
type: bool
|
||||||
|
listen-port:
|
||||||
|
description: The WireGuard connection listen-port. If not specified, the port will be chosen randomly when the
|
||||||
|
interface comes up.
|
||||||
|
type: int
|
||||||
|
mtu:
|
||||||
|
description:
|
||||||
|
- If non-zero, only transmit packets of the specified size or smaller, breaking larger packets up into multiple fragments.
|
||||||
|
- If zero a default MTU is used. Note that contrary to wg-quick's MTU setting, this does not take into account the current routes
|
||||||
|
at the time of activation.
|
||||||
|
type: int
|
||||||
|
peer-routes:
|
||||||
|
description:
|
||||||
|
- Whether to automatically add routes for the AllowedIPs ranges of the peers.
|
||||||
|
- If C(true) (the default), NetworkManager will automatically add routes in the routing tables according to C(ipv4.route-table) and
|
||||||
|
C(ipv6.route-table). Usually you want this automatism enabled.
|
||||||
|
- If C(false), no such routes are added automatically. In this case, the user may want to configure static routes in C(ipv4.routes)
|
||||||
|
and C(ipv6.routes), respectively.
|
||||||
|
- Note that if the peer's AllowedIPs is C(0.0.0.0/0) or C(::/0) and the profile's C(ipv4.never-default) or C(ipv6.never-default)
|
||||||
|
setting is enabled, the peer route for this peer won't be added automatically.
|
||||||
|
type: bool
|
||||||
|
private-key:
|
||||||
|
description: The 256 bit private-key in base64 encoding.
|
||||||
|
type: str
|
||||||
|
private-key-flags:
|
||||||
|
description: C(NMSettingSecretFlags) indicating how to handle the I(wireguard.private-key) property.
|
||||||
|
type: int
|
||||||
|
choices: [ 0, 1, 2 ]
|
||||||
'''
|
'''
|
||||||
|
|
||||||
EXAMPLES = r'''
|
EXAMPLES = r'''
|
||||||
|
@ -1126,6 +1184,17 @@ EXAMPLES = r'''
|
||||||
autoconnect: true
|
autoconnect: true
|
||||||
state: present
|
state: present
|
||||||
|
|
||||||
|
- name: Create a wireguard connection
|
||||||
|
community.general.nmcli:
|
||||||
|
type: wireguard
|
||||||
|
conn_name: my-wg-provider
|
||||||
|
ifname: mywg0
|
||||||
|
wireguard:
|
||||||
|
listen-port: 51820
|
||||||
|
private-key: my-private-key
|
||||||
|
autoconnect: true
|
||||||
|
state: present
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
|
||||||
RETURN = r"""#
|
RETURN = r"""#
|
||||||
|
@ -1236,10 +1305,11 @@ class Nmcli(object):
|
||||||
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']
|
self.gsm = module.params['gsm']
|
||||||
|
self.wireguard = module.params['wireguard']
|
||||||
|
|
||||||
if self.method4:
|
if self.method4:
|
||||||
self.ipv4_method = self.method4
|
self.ipv4_method = self.method4
|
||||||
elif self.type == 'dummy' and not self.ip4:
|
elif self.type in ('dummy', 'wireguard') and not self.ip4:
|
||||||
self.ipv4_method = 'disabled'
|
self.ipv4_method = 'disabled'
|
||||||
elif self.ip4:
|
elif self.ip4:
|
||||||
self.ipv4_method = 'manual'
|
self.ipv4_method = 'manual'
|
||||||
|
@ -1248,7 +1318,7 @@ class Nmcli(object):
|
||||||
|
|
||||||
if self.method6:
|
if self.method6:
|
||||||
self.ipv6_method = self.method6
|
self.ipv6_method = self.method6
|
||||||
elif self.type == 'dummy' and not self.ip6:
|
elif self.type in ('dummy', 'wireguard') and not self.ip6:
|
||||||
self.ipv6_method = 'disabled'
|
self.ipv6_method = 'disabled'
|
||||||
elif self.ip6:
|
elif self.ip6:
|
||||||
self.ipv6_method = 'manual'
|
self.ipv6_method = 'manual'
|
||||||
|
@ -1404,6 +1474,12 @@ class Nmcli(object):
|
||||||
options.update({
|
options.update({
|
||||||
'gsm.%s' % name: value,
|
'gsm.%s' % name: value,
|
||||||
})
|
})
|
||||||
|
elif self.type == 'wireguard':
|
||||||
|
if self.wireguard:
|
||||||
|
for name, value in self.wireguard.items():
|
||||||
|
options.update({
|
||||||
|
'wireguard.%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)
|
||||||
|
@ -1445,6 +1521,7 @@ class Nmcli(object):
|
||||||
'vlan',
|
'vlan',
|
||||||
'wifi',
|
'wifi',
|
||||||
'gsm',
|
'gsm',
|
||||||
|
'wireguard',
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -1834,6 +1911,7 @@ def main():
|
||||||
'vxlan',
|
'vxlan',
|
||||||
'wifi',
|
'wifi',
|
||||||
'gsm',
|
'gsm',
|
||||||
|
'wireguard',
|
||||||
]),
|
]),
|
||||||
ip4=dict(type='list', elements='str'),
|
ip4=dict(type='list', elements='str'),
|
||||||
gw4=dict(type='str'),
|
gw4=dict(type='str'),
|
||||||
|
@ -1907,6 +1985,7 @@ def main():
|
||||||
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'),
|
gsm=dict(type='dict'),
|
||||||
|
wireguard=dict(type='dict'),
|
||||||
),
|
),
|
||||||
mutually_exclusive=[['never_default4', 'gw4']],
|
mutually_exclusive=[['never_default4', 'gw4']],
|
||||||
required_if=[("type", "wifi", [("ssid")])],
|
required_if=[("type", "wifi", [("ssid")])],
|
||||||
|
|
|
@ -92,6 +92,12 @@ TESTCASE_CONNECTION = [
|
||||||
'state': 'absent',
|
'state': 'absent',
|
||||||
'_ansible_check_mode': True,
|
'_ansible_check_mode': True,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'type': 'wireguard',
|
||||||
|
'conn_name': 'non_existent_nw_device',
|
||||||
|
'state': 'absent',
|
||||||
|
'_ansible_check_mode': True,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
TESTCASE_GENERIC = [
|
TESTCASE_GENERIC = [
|
||||||
|
@ -900,6 +906,49 @@ gsm.sim-operator-id: --
|
||||||
gsm.mtu: auto
|
gsm.mtu: auto
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
TESTCASE_WIREGUARD = [
|
||||||
|
{
|
||||||
|
'type': 'wireguard',
|
||||||
|
'conn_name': 'non_existent_nw_device',
|
||||||
|
'ifname': 'wg_non_existant',
|
||||||
|
'wireguard': {
|
||||||
|
'listen-port': '51820',
|
||||||
|
'private-key': '<hidden>',
|
||||||
|
},
|
||||||
|
'method4': 'manual',
|
||||||
|
'ip4': '10.10.10.10/24',
|
||||||
|
'method6': 'manual',
|
||||||
|
'ip6': '2001:db8::1/128',
|
||||||
|
'state': 'present',
|
||||||
|
'_ansible_check_mode': False,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
TESTCASE_WIREGUARD_SHOW_OUTPUT = """\
|
||||||
|
connection.id: non_existent_nw_device
|
||||||
|
connection.type: wireguard
|
||||||
|
connection.interface-name: wg_non_existant
|
||||||
|
connection.autoconnect: yes
|
||||||
|
ipv4.method: manual
|
||||||
|
ipv4.addresses: 10.10.10.10/24
|
||||||
|
ipv4.never-default: no
|
||||||
|
ipv4.may-fail: yes
|
||||||
|
ipv4.ignore-auto-dns: no
|
||||||
|
ipv4.ignore-auto-routes: no
|
||||||
|
ipv6.method: manual
|
||||||
|
ipv6.addresses: 2001:db8::1/128
|
||||||
|
ipv6.ignore-auto-dns: no
|
||||||
|
ipv6.ignore-auto-routes: no
|
||||||
|
wireguard.private-key: <hidden>
|
||||||
|
wireguard.private-key-flags: 0 (none)
|
||||||
|
wireguard.listen-port: 51820
|
||||||
|
wireguard.fwmark: 0x0
|
||||||
|
wireguard.peer-routes: yes
|
||||||
|
wireguard.mtu: 0
|
||||||
|
wireguard.ip4-auto-default-route: -1 (default)
|
||||||
|
wireguard.ip6-auto-default-route: -1 (default)
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
def mocker_set(mocker,
|
def mocker_set(mocker,
|
||||||
connection_exists=False,
|
connection_exists=False,
|
||||||
|
@ -1201,6 +1250,13 @@ def mocked_gsm_connection_unchanged(mocker):
|
||||||
execute_return=(0, TESTCASE_GSM_SHOW_OUTPUT, ""))
|
execute_return=(0, TESTCASE_GSM_SHOW_OUTPUT, ""))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mocked_wireguard_connection_unchanged(mocker):
|
||||||
|
mocker_set(mocker,
|
||||||
|
connection_exists=True,
|
||||||
|
execute_return=(0, TESTCASE_WIREGUARD_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):
|
||||||
"""
|
"""
|
||||||
|
@ -2828,3 +2884,81 @@ def test_ethernet_connection_static_with_mulitple_ip4_addresses_unchanged(mocked
|
||||||
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_WIREGUARD, indirect=['patch_ansible_module'])
|
||||||
|
def test_create_wireguard(mocked_generic_connection_create, capfd):
|
||||||
|
"""
|
||||||
|
Test : Create wireguard connection with static IP configuration
|
||||||
|
"""
|
||||||
|
|
||||||
|
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] == 'wireguard'
|
||||||
|
assert add_args[0][5] == 'con-name'
|
||||||
|
assert add_args[0][6] == 'non_existent_nw_device'
|
||||||
|
|
||||||
|
add_args_text = list(map(to_text, add_args[0]))
|
||||||
|
for param in ['connection.interface-name', 'wg_non_existant',
|
||||||
|
'ipv4.method', 'manual',
|
||||||
|
'ipv4.addresses', '10.10.10.10/24',
|
||||||
|
'ipv6.method', 'manual',
|
||||||
|
'ipv6.addresses', '2001:db8::1/128',
|
||||||
|
'wireguard.listen-port', '51820',
|
||||||
|
'wireguard.private-key', '<hidden>']:
|
||||||
|
assert param in add_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_WIREGUARD, indirect=['patch_ansible_module'])
|
||||||
|
def test_wireguard_connection_unchanged(mocked_wireguard_connection_unchanged, capfd):
|
||||||
|
"""
|
||||||
|
Test : Wireguard connection with static IP configuration 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_WIREGUARD, indirect=['patch_ansible_module'])
|
||||||
|
def test_wireguard_mod(mocked_generic_connection_modify, capfd):
|
||||||
|
"""
|
||||||
|
Test : Modify wireguard connection
|
||||||
|
"""
|
||||||
|
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 ['wireguard.listen-port', '51820']:
|
||||||
|
assert param in args_text
|
||||||
|
|
||||||
|
out, err = capfd.readouterr()
|
||||||
|
results = json.loads(out)
|
||||||
|
assert not results.get('failed')
|
||||||
|
assert results['changed']
|
||||||
|
|
Loading…
Reference in a new issue