Skip to content

Commit

Permalink
Update nmcli.py to support VRF commands
Browse files Browse the repository at this point in the history
Adding VRF support and documentation to the nmcli module

Signed-off-by: Andreas Karis <[email protected]>
  • Loading branch information
andreaskaris committed Feb 3, 2025
1 parent 19d0049 commit 6f1fd32
Show file tree
Hide file tree
Showing 3 changed files with 157 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
minor_changes:
- nmcli - adds VRF support with new ``type`` value ``vrf`` and new ``slave_type`` value ``vrf`` as well as new ``table`` parameter (https://github.com/ansible-collections/community.general/pull/9658, https://github.com/ansible-collections/community.general/issues/8014).
48 changes: 44 additions & 4 deletions plugins/modules/nmcli.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,14 @@
- Type V(ovs-port) is added in community.general 8.6.0.
- Type V(wireguard) is added in community.general 4.3.0.
- Type V(vpn) is added in community.general 5.1.0.
- Type V(vrf) is added in community.general 10.4.0.
- Using V(bond-slave), V(bridge-slave), or V(team-slave) implies V(ethernet) connection type with corresponding O(slave_type)
option.
- If you want to control non-ethernet connection attached to V(bond), V(bridge), or V(team) consider using O(slave_type)
option.
type: str
choices: [bond, bond-slave, bridge, bridge-slave, dummy, ethernet, generic, gre, infiniband, ipip, macvlan, sit, team,
team-slave, vlan, vxlan, wifi, gsm, wireguard, ovs-bridge, ovs-port, ovs-interface, vpn, loopback]
team-slave, vlan, vxlan, wifi, gsm, wireguard, ovs-bridge, ovs-port, ovs-interface, vpn, vrf, loopback]
mode:
description:
- This is the type of device or network connection that you wish to create for a bond or bridge.
Expand All @@ -103,7 +104,7 @@
- Type of the device of this slave's master connection (for example V(bond)).
- Type V(ovs-port) is added in community.general 8.6.0.
type: str
choices: ['bond', 'bridge', 'team', 'ovs-port']
choices: ['bond', 'bridge', 'team', 'ovs-port', 'vrf']
version_added: 7.0.0
master:
description:
Expand Down Expand Up @@ -521,6 +522,11 @@
- Only used when O(type=gre).
type: str
version_added: 3.6.0
table:
description:
- This is only used with VRF - VRF table number.
type: int
version_added: 10.4.0
zone:
description:
- The trust level of the connection.
Expand Down Expand Up @@ -1569,6 +1575,29 @@
vlanid: 5
state: present
## Creating VRF and adding VLAN interface to it
- name: Create VRF
community.general.nmcli:
type: vrf
ifname: vrf10
table: 10
state: present
conn_name: vrf10
method4: disabled
method6: disabled
- name: Create VLAN interface inside VRF
community.general.nmcli:
conn_name: "eth0.124"
type: vlan
vlanid: "124"
vlandev: "eth0"
master: "vrf10"
slave_type: vrf
state: "present"
ip4: '192.168.124.50'
gw4: '192.168.124.1'
## Defining ip rules while setting a static IP
## table 'production' is set with id 200 in this example.
- name: Set Static ips for interface with ip rules and routes
Expand Down Expand Up @@ -1755,6 +1784,9 @@ def __init__(self, module):
else:
self.ipv6_method = None

if self.type == "vrf":
self.table = module.params['table']

self.edit_commands = []

self.extra_options_validation()
Expand Down Expand Up @@ -1787,7 +1819,8 @@ def connection_options(self, detect_change=False):

# IP address options.
# The ovs-interface type can be both ip_conn_type and have a master
if (self.ip_conn_type and not self.master) or self.type == "ovs-interface":
# An interface that has a master but is of slave type vrf can have an IP address
if (self.ip_conn_type and (not self.master or self.slave_type == "vrf")) or self.type == "ovs-interface":
options.update({
'ipv4.addresses': self.enforce_ipv4_cidr_notation(self.ip4),
'ipv4.dhcp-client-id': self.dhcp_client_id,
Expand Down Expand Up @@ -2001,6 +2034,10 @@ def connection_options(self, detect_change=False):
options.update({
'infiniband.transport-mode': self.transport_mode,
})
elif self.type == 'vrf':
options.update({
'table': self.table,
})

if self.type == 'ethernet':
if self.sriov:
Expand Down Expand Up @@ -2057,6 +2094,7 @@ def ip_conn_type(self):
'vpn',
'loopback',
'ovs-interface',
'vrf'
)

@property
Expand Down Expand Up @@ -2528,7 +2566,7 @@ def main():
conn_name=dict(type='str', required=True),
conn_reload=dict(type='bool', default=False),
master=dict(type='str'),
slave_type=dict(type='str', choices=['bond', 'bridge', 'team', 'ovs-port']),
slave_type=dict(type='str', choices=['bond', 'bridge', 'team', 'ovs-port', 'vrf']),
ifname=dict(type='str'),
type=dict(type='str',
choices=[
Expand Down Expand Up @@ -2556,6 +2594,7 @@ def main():
'ovs-interface',
'ovs-bridge',
'ovs-port',
'vrf',
]),
ip4=dict(type='list', elements='str'),
gw4=dict(type='str'),
Expand Down Expand Up @@ -2669,6 +2708,7 @@ def main():
vpn=dict(type='dict'),
transport_mode=dict(type='str', choices=['datagram', 'connected']),
sriov=dict(type='dict'),
table=dict(type='int'),
),
mutually_exclusive=[['never_default4', 'gw4'],
['routes4_extended', 'routes4'],
Expand Down
111 changes: 111 additions & 0 deletions tests/unit/plugins/modules/test_nmcli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1570,6 +1570,37 @@
macvlan.tap: no
"""

TESTCASE_VRF = [
{
'type': 'vrf',
'conn_name': 'non_existent_nw_device',
'ifname': 'vrf_not_exists',
'ip4': '10.10.10.10/24',
'gw4': '10.10.10.1',
'table': 10,
'state': 'present',
'_ansible_check_mode': False,
}
]

TESTCASE_VRF_SHOW_OUTPUT = """\
connection.id: non_existent_nw_device
connection.interface-name: vrf_not_exists
connection.autoconnect: yes
ipv4.method: manual
ipv4.addresses: 10.10.10.10/24
ipv4.gateway: 10.10.10.1
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
table: 10
802-3-ethernet.mtu: auto
"""


def mocker_set(mocker,
connection_exists=False,
Expand Down Expand Up @@ -2035,6 +2066,13 @@ def mocked_loopback_connection_modify(mocker):
))


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


@pytest.mark.parametrize('patch_ansible_module', TESTCASE_BOND, indirect=['patch_ansible_module'])
def test_bond_connection_create(mocked_generic_connection_create, capfd):
"""
Expand Down Expand Up @@ -4911,3 +4949,76 @@ def test_add_second_ip4_address_to_loopback_connection(mocked_loopback_connectio
results = json.loads(out)
assert not results.get('failed')
assert results['changed']


@pytest.mark.parametrize('patch_ansible_module', TESTCASE_VRF, indirect=['patch_ansible_module'])
def test_create_vrf_con(mocked_generic_connection_create, capfd):
"""
Test if VRF 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] == 'vrf'
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 ['ipv4.addresses', '10.10.10.10/24', 'ipv4.gateway', '10.10.10.1', 'table', '10']:
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_VRF, indirect=['patch_ansible_module'])
def test_mod_vrf_conn(mocked_generic_connection_modify, capfd):
"""
Test if VRF 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 ['ipv4.addresses', '10.10.10.10/24', 'ipv4.gateway', '10.10.10.1', 'table', '10']:
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_VRF, indirect=['patch_ansible_module'])
def test_vrf_connection_unchanged(mocked_vrf_connection_unchanged, capfd):
"""
Test : VRF 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']

0 comments on commit 6f1fd32

Please sign in to comment.