Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

nmcli: Add macvlan connection type support #6312

Merged
merged 7 commits into from
Apr 16, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
minor_changes:
- nmcli - add ``macvlan`` connection type (https://github.com/ansible-collections/community.general/pull/6312).
65 changes: 61 additions & 4 deletions plugins/modules/nmcli.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,12 @@
- 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(macvlan) is added in community.general 6.6.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, vpn ]
choices: [ bond, bond-slave, bridge, bridge-slave, dummy, ethernet, generic, gre, infiniband, ipip, macvlan, sit, team, team-slave, vlan, vxlan,
wifi, gsm, wireguard, vpn ]
mode:
description:
- This is the type of device or network connection that you wish to create for a bond or bridge.
Expand Down Expand Up @@ -879,6 +880,41 @@
- 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.
macvlan:
description:
- The configuration of the MAC VLAN 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-macvlan.html).'
type: dict
version_added: 6.6.0
suboptions:
mode:
russoz marked this conversation as resolved.
Show resolved Hide resolved
description:
- The macvlan mode, which specifies the communication mechanism between multiple macvlans on the same lower device.
- 'Following choices are allowed:
C(0) B(unknown) (default)
C(1) B(vepa)
C(2) B(bridge)
C(3) B(private)
C(4) B(passthru)
C(5) B(source)'
psvmcc marked this conversation as resolved.
Show resolved Hide resolved
type: int
choices: [ 0, 1, 2, 3, 4, 5 ]
parent:
description:
- If given, specifies the parent interface name or parent connection UUID from which this MAC-VLAN interface should
be created. If this property is not specified, the connection must contain an "802-3-ethernet" setting with a
"mac-address" property.
type: str
promiscuous:
description:
- Whether the interface should be put in promiscuous mode.
type: bool
tap:
description:
- Whether the interface should be a MACVTAP.
type: bool
wireguard:
description:
- The configuration of the Wireguard connection.
Expand Down Expand Up @@ -1357,6 +1393,17 @@
autoconnect: true
state: present

- name: Create a macvlan connection
community.general.nmcli:
type: macvlan
conn_name: my-macvlan-connection
ifname: mymacvlan0
macvlan:
mode: 2
parent: eth1
autoconnect: true
state: present

- name: Create a wireguard connection
community.general.nmcli:
type: wireguard
Expand Down Expand Up @@ -1502,13 +1549,14 @@ def __init__(self, module):
self.wifi = module.params['wifi']
self.wifi_sec = module.params['wifi_sec']
self.gsm = module.params['gsm']
self.macvlan = module.params['macvlan']
self.wireguard = module.params['wireguard']
self.vpn = module.params['vpn']
self.transport_mode = module.params['transport_mode']

if self.method4:
self.ipv4_method = self.method4
elif self.type in ('dummy', 'wireguard') and not self.ip4:
elif self.type in ('dummy', 'macvlan', 'wireguard') and not self.ip4:
self.ipv4_method = 'disabled'
elif self.ip4:
self.ipv4_method = 'manual'
Expand All @@ -1517,7 +1565,7 @@ def __init__(self, module):

if self.method6:
self.ipv6_method = self.method6
elif self.type in ('dummy', 'wireguard') and not self.ip6:
elif self.type in ('dummy', 'macvlan', 'wireguard') and not self.ip6:
self.ipv6_method = 'disabled'
elif self.ip6:
self.ipv6_method = 'manual'
Expand Down Expand Up @@ -1700,6 +1748,12 @@ def connection_options(self, detect_change=False):
options.update({
'gsm.%s' % name: value,
})
elif self.type == 'macvlan':
if self.macvlan:
for name, value in self.macvlan.items():
options.update({
'macvlan.%s' % name: value,
})
elif self.type == 'wireguard':
if self.wireguard:
for name, value in self.wireguard.items():
Expand Down Expand Up @@ -1777,6 +1831,7 @@ def ip_conn_type(self):
'wifi',
'802-11-wireless',
'gsm',
'macvlan',
'wireguard',
'vpn',
)
Expand Down Expand Up @@ -2239,6 +2294,7 @@ def main():
'vxlan',
'wifi',
'gsm',
'macvlan',
'wireguard',
'vpn',
]),
Expand Down Expand Up @@ -2342,6 +2398,7 @@ def main():
wifi=dict(type='dict'),
wifi_sec=dict(type='dict', no_log=True),
gsm=dict(type='dict'),
macvlan=dict(type='dict'),
russoz marked this conversation as resolved.
Show resolved Hide resolved
wireguard=dict(type='dict'),
vpn=dict(type='dict'),
transport_mode=dict(type='str', choices=['datagram', 'connected']),
Expand Down
132 changes: 132 additions & 0 deletions tests/unit/plugins/modules/test_nmcli.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,12 @@
'state': 'absent',
'_ansible_check_mode': True,
},
{
'type': 'macvlan',
'conn_name': 'non_existent_nw_device',
'state': 'absent',
'_ansible_check_mode': True,
},
]

TESTCASE_GENERIC = [
Expand Down Expand Up @@ -1406,6 +1412,45 @@
infiniband.transport_mode: connected
"""

TESTCASE_MACVLAN = [
{
'type': 'macvlan',
'conn_name': 'non_existent_nw_device',
'ifname': 'macvlan_non_existant',
'macvlan': {
'mode': '2',
'parent': 'non_existent_parent',
},
'method4': 'manual',
'ip4': '10.10.10.10/24',
'method6': 'manual',
'ip6': '2001:db8::1/128',
'state': 'present',
'_ansible_check_mode': False,
}
]

TESTCASE_MACVLAN_SHOW_OUTPUT = """\
connection.id: non_existent_nw_device
connection.type: macvlan
connection.interface-name: macvlan_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
macvlan.parent: non_existent_parent
macvlan.mode: 2 (bridge)
macvlan.promiscuous: yes
macvlan.tap: no
"""


def mocker_set(mocker,
connection_exists=False,
Expand Down Expand Up @@ -1815,6 +1860,13 @@ def mocked_infiniband_connection_static_transport_mode_connected_modify(mocker):
))


@pytest.fixture
def mocked_macvlan_connection_unchanged(mocker):
mocker_set(mocker,
connection_exists=True,
execute_return=(0, TESTCASE_MACVLAN_SHOW_OUTPUT, ""))


@pytest.fixture
def mocked_generic_connection_diff_check(mocker):
mocker_set(mocker,
Expand Down Expand Up @@ -3998,6 +4050,7 @@ def test_bond_connection_unchanged(mocked_generic_connection_diff_check, capfd):
'vxlan',
'wifi',
'gsm',
'macvlan',
'wireguard',
'vpn',
]),
Expand Down Expand Up @@ -4101,6 +4154,7 @@ def test_bond_connection_unchanged(mocked_generic_connection_diff_check, capfd):
wifi=dict(type='dict'),
wifi_sec=dict(type='dict', no_log=True),
gsm=dict(type='dict'),
macvlan=dict(type='dict'),
wireguard=dict(type='dict'),
vpn=dict(type='dict'),
transport_mode=dict(type='str', choices=['datagram', 'connected']),
Expand All @@ -4125,3 +4179,81 @@ def test_bond_connection_unchanged(mocked_generic_connection_diff_check, capfd):
num_of_diff_params += 1

assert num_of_diff_params == 1


@pytest.mark.parametrize('patch_ansible_module', TESTCASE_MACVLAN, indirect=['patch_ansible_module'])
def test_create_macvlan(mocked_generic_connection_create, capfd):
"""
Test : Create macvlan 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] == 'macvlan'
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', 'macvlan_non_existant',
'ipv4.method', 'manual',
'ipv4.addresses', '10.10.10.10/24',
'ipv6.method', 'manual',
'ipv6.addresses', '2001:db8::1/128',
'macvlan.mode', '2',
'macvlan.parent', 'non_existent_parent']:
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_MACVLAN, indirect=['patch_ansible_module'])
def test_macvlan_connection_unchanged(mocked_macvlan_connection_unchanged, capfd):
"""
Test : Macvlan 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_MACVLAN, indirect=['patch_ansible_module'])
def test_macvlan_mod(mocked_generic_connection_modify, capfd):
"""
Test : Modify macvlan 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 ['macvlan.mode', '2']:
assert param in args_text

out, err = capfd.readouterr()
results = json.loads(out)
assert not results.get('failed')
assert results['changed']