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

nmcli: added new module option 'slave_type' to allow create non-ethernet slave connections (#6108)

* nmcli: added new module option 'slave_type' to allow create non-ethernet slave connections

* argument specs updated

* documentation updated

* examples updated

* added warning message when using type='bridge-slave'

* remove trailing whitespace

* Added warnings about rewrite 'slave-type' property when using type one of 'bond-slave', 'bridge-slave', 'team-slave'.
Added module fails when user sets contradicting values of 'slave-type' for types 'bond-slave', 'bridge-slave', 'team-slave'.
Returned back checking for types that can be a slave to assign 'master' and 'slave-type' properties.

* Extending list of slave-conn-types

* Update plugins/modules/nmcli.py

Version updated

Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com>

* Update plugins/modules/nmcli.py

Updated documentation for `slave_type`

Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com>

* Updated argspec's 'required_by' for 'master' property.

* Fixed mistake in property  naming in module argspec.

* changelog fragment and module docs updated

* Validation of 'master', 'slave_type' options improved. (rebased)

* Validation of 'master' and 'slave_type' separated to special method.

* Wrote 6 tests for slave_type option behaviour

* Removed erroneously added property 'hairpin'

* Update version_added for 'slave_type'

Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com>

* Update changelogs/fragments/473-nmcli-slave-type-implemented.yml

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

* Update plugins/modules/nmcli.py

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

* Let master be without slave_type

---------

Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com>
Co-authored-by: Felix Fontein <felix@fontein.de>
This commit is contained in:
Sam Potekhin 2023-05-09 00:44:30 +07:00 committed by GitHub
parent 9f3c86a589
commit c949f3a834
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 389 additions and 8 deletions

View file

@ -0,0 +1,2 @@
minor_changes:
- nmcli - new module option ``slave_type`` added to allow creation of various types of slave devices (https://github.com/ansible-collections/community.general/issues/473, https://github.com/ansible-collections/community.general/pull/6108).

View file

@ -66,6 +66,8 @@ options:
- Type C(macvlan) is added in community.general 6.6.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(wireguard) is added in community.general 4.3.0.
- Type C(vpn) is added in community.general 5.1.0. - Type C(vpn) is added in community.general 5.1.0.
- Using C(bond-slave), C(bridge-slave) or C(team-slave) implies C(ethernet) connection type with corresponding I(slave_type) option.
- If you want to control non-ethernet connection attached to C(bond), C(bridge) or C(team) consider using C(slave_type) option.
type: str type: str
choices: [ bond, bond-slave, bridge, bridge-slave, dummy, ethernet, generic, gre, infiniband, ipip, macvlan, sit, team, team-slave, vlan, vxlan, choices: [ bond, bond-slave, bridge, bridge-slave, dummy, ethernet, generic, gre, infiniband, ipip, macvlan, sit, team, team-slave, vlan, vxlan,
wifi, gsm, wireguard, vpn ] wifi, gsm, wireguard, vpn ]
@ -81,9 +83,16 @@ options:
type: str type: str
choices: [ datagram, connected ] choices: [ datagram, connected ]
version_added: 5.8.0 version_added: 5.8.0
slave_type:
description:
- Type of the device of this slave's master connection (for example C(bond)).
type: str
choices: [ 'bond', 'bridge', 'team' ]
version_added: 7.0.0
master: master:
description: description:
- Master <master (ifname, or connection UUID or conn_name) of bridge, team, bond master connection profile. - Master <master (ifname, or connection UUID or conn_name) of bridge, team, bond master connection profile.
- Mandatory if I(slave_type) is defined.
type: str type: str
ip4: ip4:
description: description:
@ -1429,6 +1438,39 @@ EXAMPLES = r'''
autoconnect: false autoconnect: false
state: present state: present
## Creating bond attached to bridge example
- name: Create bond attached to bridge
community.general.nmcli:
type: bond
conn_name: bond0
slave_type: bridge
master: br0
state: present
- name: Create master bridge
community.general.nmcli:
type: bridge
conn_name: br0
method4: disabled
method6: disabled
state: present
## Creating vlan connection attached to bridge
- name: Create master bridge
community.general.nmcli:
type: bridge
conn_name: br0
state: present
- name: Create VLAN 5
community.general.nmcli:
type: vlan
conn_name: eth0.5
slave_type: bridge
master: br0
vlandev: eth0
vlanid: 5
state: present
''' '''
RETURN = r"""# RETURN = r"""#
@ -1475,6 +1517,7 @@ class Nmcli(object):
self.ignore_unsupported_suboptions = module.params['ignore_unsupported_suboptions'] self.ignore_unsupported_suboptions = module.params['ignore_unsupported_suboptions']
self.autoconnect = module.params['autoconnect'] self.autoconnect = module.params['autoconnect']
self.conn_name = module.params['conn_name'] self.conn_name = module.params['conn_name']
self.slave_type = module.params['slave_type']
self.master = module.params['master'] self.master = module.params['master']
self.ifname = module.params['ifname'] self.ifname = module.params['ifname']
self.type = module.params['type'] self.type = module.params['type']
@ -1570,6 +1613,14 @@ class Nmcli(object):
self.edit_commands = [] self.edit_commands = []
self.extra_options_validation()
def extra_options_validation(self):
""" Additional validation of options set passed to module that cannot be implemented in module's argspecs. """
if self.type not in ("bridge-slave", "team-slave", "bond-slave"):
if self.master is None and self.slave_type is not None:
self.module.fail_json(msg="'master' option is required when 'slave_type' is specified.")
def execute_command(self, cmd, use_unsafe_shell=False, data=None): def execute_command(self, cmd, use_unsafe_shell=False, data=None):
if isinstance(cmd, list): if isinstance(cmd, list):
cmd = [to_text(item) for item in cmd] cmd = [to_text(item) for item in cmd]
@ -1634,6 +1685,7 @@ class Nmcli(object):
if self.slave_conn_type: if self.slave_conn_type:
options.update({ options.update({
'connection.master': self.master, 'connection.master': self.master,
'connection.slave-type': self.slave_type,
}) })
# Options specific to a connection type. # Options specific to a connection type.
@ -1649,6 +1701,14 @@ class Nmcli(object):
'xmit_hash_policy': self.xmit_hash_policy, 'xmit_hash_policy': self.xmit_hash_policy,
}) })
elif self.type == 'bond-slave': elif self.type == 'bond-slave':
if self.slave_type and self.slave_type != 'bond':
self.module.fail_json(msg="Connection type '%s' cannot be combined with '%s' slave-type. "
"Allowed slave-type for '%s' is 'bond'."
% (self.type, self.slave_type, self.type)
)
if not self.slave_type:
self.module.warn("Connection 'slave-type' property automatically set to 'bond' "
"because of using 'bond-slave' connection type.")
options.update({ options.update({
'connection.slave-type': 'bond', 'connection.slave-type': 'bond',
}) })
@ -1675,13 +1735,33 @@ class Nmcli(object):
'team.runner-fast-rate': self.runner_fast_rate, 'team.runner-fast-rate': self.runner_fast_rate,
}) })
elif self.type == 'bridge-slave': elif self.type == 'bridge-slave':
if self.slave_type and self.slave_type != 'bridge':
self.module.fail_json(msg="Connection type '%s' cannot be combined with '%s' slave-type. "
"Allowed slave-type for '%s' is 'bridge'."
% (self.type, self.slave_type, self.type)
)
if not self.slave_type:
self.module.warn("Connection 'slave-type' property automatically set to 'bridge' "
"because of using 'bridge-slave' connection type.")
options.update({'connection.slave-type': 'bridge'})
self.module.warn(
"Connection type as 'bridge-slave' implies 'ethernet' connection with 'bridge' slave-type. "
"Consider using slave_type='bridge' with necessary type."
)
options.update({ options.update({
'connection.slave-type': 'bridge',
'bridge-port.path-cost': self.path_cost, 'bridge-port.path-cost': self.path_cost,
'bridge-port.hairpin-mode': self.hairpin, 'bridge-port.hairpin-mode': self.hairpin,
'bridge-port.priority': self.slavepriority, 'bridge-port.priority': self.slavepriority,
}) })
elif self.type == 'team-slave': elif self.type == 'team-slave':
if self.slave_type and self.slave_type != 'team':
self.module.fail_json(msg="Connection type '%s' cannot be combined with '%s' slave-type. "
"Allowed slave-type for '%s' is 'team'."
% (self.type, self.slave_type, self.type)
)
if not self.slave_type:
self.module.warn("Connection 'slave-type' property automatically set to 'team' "
"because of using 'team-slave' connection type.")
options.update({ options.update({
'connection.slave-type': 'team', 'connection.slave-type': 'team',
}) })
@ -1869,6 +1949,12 @@ class Nmcli(object):
@property @property
def slave_conn_type(self): def slave_conn_type(self):
return self.type in ( return self.type in (
'ethernet',
'bridge',
'bond',
'vlan',
'team',
'wifi',
'bond-slave', 'bond-slave',
'bridge-slave', 'bridge-slave',
'team-slave', 'team-slave',
@ -2260,6 +2346,7 @@ def main():
state=dict(type='str', required=True, choices=['absent', 'present']), state=dict(type='str', required=True, choices=['absent', 'present']),
conn_name=dict(type='str', required=True), conn_name=dict(type='str', required=True),
master=dict(type='str'), master=dict(type='str'),
slave_type=dict(type='str', choices=['bond', 'bridge', 'team']),
ifname=dict(type='str'), ifname=dict(type='str'),
type=dict(type='str', type=dict(type='str',
choices=[ choices=[
@ -2416,7 +2503,7 @@ def main():
if nmcli.runner_fast_rate is not None and nmcli.runner != "lacp": if nmcli.runner_fast_rate is not None and nmcli.runner != "lacp":
nmcli.module.fail_json(msg="runner-fast-rate is only allowed for runner lacp") nmcli.module.fail_json(msg="runner-fast-rate is only allowed for runner lacp")
# team-slave checks # team-slave checks
if nmcli.type == 'team-slave': if nmcli.type == 'team-slave' or nmcli.slave_type == 'team':
if nmcli.master is None: if nmcli.master is None:
nmcli.module.fail_json(msg="Please specify a name for the master when type is %s" % nmcli.type) nmcli.module.fail_json(msg="Please specify a name for the master when type is %s" % nmcli.type)
if nmcli.ifname is None: if nmcli.ifname is None:

View file

@ -4031,6 +4031,7 @@ def test_bond_connection_unchanged(mocked_generic_connection_diff_check, capfd):
state=dict(type='str', required=True, choices=['absent', 'present']), state=dict(type='str', required=True, choices=['absent', 'present']),
conn_name=dict(type='str', required=True), conn_name=dict(type='str', required=True),
master=dict(type='str'), master=dict(type='str'),
slave_type=dict(type=str, choices=['bond', 'bridge', 'team']),
ifname=dict(type='str'), ifname=dict(type='str'),
type=dict(type='str', type=dict(type='str',
choices=[ choices=[
@ -4258,3 +4259,294 @@ def test_macvlan_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']
TESTCASE_SLAVE_TYPE_BRIDGE_CONNECTION = [
{
'type': 'ethernet',
'conn_name': 'fake_conn',
'ifname': 'fake_eth0',
'state': 'present',
'slave_type': 'bridge',
'master': 'fake_br0',
'_ansible_check_mode': False,
}
]
TESTCASE_SLAVE_TYPE_BRIDGE_CONNECTION_SHOW_OUTPUT = """\
connection.id: fake_conn
connection.type: 802-3-ethernet
connection.interface-name: fake_eth0
connection.autoconnect: yes
connection.master: --
connection.slave-type: --
802-3-ethernet.mtu: auto
"""
TESTCASE_SLAVE_TYPE_BRIDGE_CONNECTION_UNCHANGED_SHOW_OUTPUT = """\
connection.id: fake_conn
connection.type: 802-3-ethernet
connection.interface-name: fake_eth0
connection.autoconnect: yes
connection.master: fake_br0
connection.slave-type: bridge
802-3-ethernet.mtu: auto
"""
@pytest.fixture
def mocked_slave_type_bridge_create(mocker):
mocker_set(mocker,
execute_return=None,
execute_side_effect=(
(0, TESTCASE_SLAVE_TYPE_BRIDGE_CONNECTION_SHOW_OUTPUT, ""),
(0, "", ""),
))
@pytest.mark.parametrize('patch_ansible_module', TESTCASE_SLAVE_TYPE_BRIDGE_CONNECTION, indirect=['patch_ansible_module'])
def test_create_slave_type_bridge(mocked_slave_type_bridge_create, capfd):
"""
Test : slave for bridge 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] == 'ethernet'
assert args[0][5] == 'con-name'
assert args[0][6] == 'fake_conn'
con_master_index = args[0].index('connection.master')
slave_type_index = args[0].index('connection.slave-type')
assert args[0][con_master_index + 1] == 'fake_br0'
assert args[0][slave_type_index + 1] == 'bridge'
out, err = capfd.readouterr()
results = json.loads(out)
assert not results.get('failed')
assert results['changed']
@pytest.fixture
def mocked_create_slave_type_bridge_unchanged(mocker):
mocker_set(mocker,
connection_exists=True,
execute_return=(0, TESTCASE_SLAVE_TYPE_BRIDGE_CONNECTION_UNCHANGED_SHOW_OUTPUT, ""))
@pytest.mark.parametrize('patch_ansible_module', TESTCASE_SLAVE_TYPE_BRIDGE_CONNECTION, indirect=['patch_ansible_module'])
def test_slave_type_bridge_unchanged(mocked_create_slave_type_bridge_unchanged, capfd):
"""
Test : Existent slave for bridge unchanged
"""
with pytest.raises(SystemExit):
nmcli.main()
out, err = capfd.readouterr()
results = json.loads(out)
assert not results.get('failed')
assert not results['changed']
TESTCASE_SLAVE_TYPE_BOND_CONNECTION = [
{
'type': 'ethernet',
'conn_name': 'fake_conn',
'ifname': 'fake_eth0',
'state': 'present',
'slave_type': 'bond',
'master': 'fake_bond0',
'_ansible_check_mode': False,
}
]
TESTCASE_SLAVE_TYPE_BOND_CONNECTION_SHOW_OUTPUT = """\
connection.id: fake_conn
connection.type: 802-3-ethernet
connection.interface-name: fake_eth0
connection.autoconnect: yes
connection.master: --
connection.slave-type: --
802-3-ethernet.mtu: auto
"""
TESTCASE_SLAVE_TYPE_BOND_CONNECTION_UNCHANGED_SHOW_OUTPUT = """\
connection.id: fake_conn
connection.type: 802-3-ethernet
connection.interface-name: fake_eth0
connection.autoconnect: yes
connection.master: fake_bond0
connection.slave-type: bond
802-3-ethernet.mtu: auto
"""
@pytest.fixture
def mocked_slave_type_bond_create(mocker):
mocker_set(mocker,
execute_return=None,
execute_side_effect=(
(0, TESTCASE_SLAVE_TYPE_BOND_CONNECTION_SHOW_OUTPUT, ""),
(0, "", ""),
))
@pytest.mark.parametrize('patch_ansible_module', TESTCASE_SLAVE_TYPE_BOND_CONNECTION, indirect=['patch_ansible_module'])
def test_create_slave_type_bond(mocked_slave_type_bond_create, capfd):
"""
Test : slave for bond 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] == 'ethernet'
assert args[0][5] == 'con-name'
assert args[0][6] == 'fake_conn'
con_master_index = args[0].index('connection.master')
slave_type_index = args[0].index('connection.slave-type')
assert args[0][con_master_index + 1] == 'fake_bond0'
assert args[0][slave_type_index + 1] == 'bond'
out, err = capfd.readouterr()
results = json.loads(out)
assert not results.get('failed')
assert results['changed']
@pytest.fixture
def mocked_create_slave_type_bond_unchanged(mocker):
mocker_set(mocker,
connection_exists=True,
execute_return=(0, TESTCASE_SLAVE_TYPE_BOND_CONNECTION_UNCHANGED_SHOW_OUTPUT, ""))
@pytest.mark.parametrize('patch_ansible_module', TESTCASE_SLAVE_TYPE_BOND_CONNECTION, indirect=['patch_ansible_module'])
def test_slave_type_bond_unchanged(mocked_create_slave_type_bond_unchanged, capfd):
"""
Test : Existent slave for bridge unchanged
"""
with pytest.raises(SystemExit):
nmcli.main()
out, err = capfd.readouterr()
results = json.loads(out)
assert not results.get('failed')
assert not results['changed']
TESTCASE_SLAVE_TYPE_TEAM_CONNECTION = [
{
'type': 'ethernet',
'conn_name': 'fake_conn',
'ifname': 'fake_eth0',
'state': 'present',
'slave_type': 'team',
'master': 'fake_team0',
'_ansible_check_mode': False,
}
]
TESTCASE_SLAVE_TYPE_TEAM_CONNECTION_SHOW_OUTPUT = """\
connection.id: fake_conn
connection.type: 802-3-ethernet
connection.interface-name: fake_eth0
connection.autoconnect: yes
connection.master: --
connection.slave-type: --
802-3-ethernet.mtu: auto
"""
TESTCASE_SLAVE_TYPE_TEAM_CONNECTION_UNCHANGED_SHOW_OUTPUT = """\
connection.id: fake_conn
connection.type: 802-3-ethernet
connection.interface-name: fake_eth0
connection.autoconnect: yes
connection.master: fake_team0
connection.slave-type: team
802-3-ethernet.mtu: auto
"""
@pytest.fixture
def mocked_slave_type_team_create(mocker):
mocker_set(mocker,
execute_return=None,
execute_side_effect=(
(0, TESTCASE_SLAVE_TYPE_TEAM_CONNECTION_SHOW_OUTPUT, ""),
(0, "", ""),
))
@pytest.mark.parametrize('patch_ansible_module', TESTCASE_SLAVE_TYPE_TEAM_CONNECTION, indirect=['patch_ansible_module'])
def test_create_slave_type_team(mocked_slave_type_team_create, capfd):
"""
Test : slave for bond 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] == 'ethernet'
assert args[0][5] == 'con-name'
assert args[0][6] == 'fake_conn'
con_master_index = args[0].index('connection.master')
slave_type_index = args[0].index('connection.slave-type')
assert args[0][con_master_index + 1] == 'fake_team0'
assert args[0][slave_type_index + 1] == 'team'
out, err = capfd.readouterr()
results = json.loads(out)
assert not results.get('failed')
assert results['changed']
@pytest.fixture
def mocked_create_slave_type_team_unchanged(mocker):
mocker_set(mocker,
connection_exists=True,
execute_return=(0, TESTCASE_SLAVE_TYPE_TEAM_CONNECTION_UNCHANGED_SHOW_OUTPUT, ""))
@pytest.mark.parametrize('patch_ansible_module', TESTCASE_SLAVE_TYPE_TEAM_CONNECTION, indirect=['patch_ansible_module'])
def test_slave_type_team_unchanged(mocked_create_slave_type_team_unchanged, capfd):
"""
Test : Existent slave for bridge unchanged
"""
with pytest.raises(SystemExit):
nmcli.main()
out, err = capfd.readouterr()
results = json.loads(out)
assert not results.get('failed')
assert not results['changed']