From 922dd0fc10aada984e8500f07fd4c575cafc18fc Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Fri, 23 Oct 2020 07:48:29 +0200 Subject: [PATCH] nmcli: module refactor (#1113) (#1159) * * Refactor `nmcli` module to use consistent parameters when creating/modifying connections and detecting changes. * Keep DNS list arguments as lists internally. * Remove duplicated code where practical. * DBus and GObject dependencies are not necessary. * Update changelog fragment. Co-authored-by: Felix Fontein * Update changelog fragment. Co-authored-by: Felix Fontein * Use identity operator instead of equality for type comparison. * Don't start changelog notes with a capital letter. * * Have `settings_type` return `str` by default instead of `None`. * Improve variable naming, use `convert_func` instead of `type_cast`. * Revert new feature of allowing ethernet types as slaves. * Bring back `list_connection_info` to list all connections with `nmcli con show`. Co-authored-by: Felix Fontein (cherry picked from commit 77228005619a72758043cf32cc41c6ba9691703c) Co-authored-by: Justin Bronn --- changelogs/fragments/nmcli-refactor.yml | 8 + plugins/modules/net_tools/nmcli.py | 1214 ++++------------- tests/sanity/ignore-2.10.txt | 2 - tests/sanity/ignore-2.11.txt | 2 - tests/sanity/ignore-2.9.txt | 1 - .../plugins/modules/net_tools/test_nmcli.py | 270 ++-- 6 files changed, 389 insertions(+), 1108 deletions(-) create mode 100644 changelogs/fragments/nmcli-refactor.yml diff --git a/changelogs/fragments/nmcli-refactor.yml b/changelogs/fragments/nmcli-refactor.yml new file mode 100644 index 0000000000..86a504ac90 --- /dev/null +++ b/changelogs/fragments/nmcli-refactor.yml @@ -0,0 +1,8 @@ +--- +bugfixes: + - nmcli - use consistent autoconnect parameters (https://github.com/ansible-collections/community.general/issues/459). + - nmcli - cannot modify ``ifname`` after connection creation (https://github.com/ansible-collections/community.general/issues/1089). +minor_changes: + - nmcli - refactor internal methods for simplicity and enhance reuse to support existing and future connection types (https://github.com/ansible-collections/community.general/pull/1113). + - nmcli - the ``dns4``, ``dns4_search``, ``dns6``, and ``dns6_search`` arguments are retained internally as lists (https://github.com/ansible-collections/community.general/pull/1113). + - nmcli - remove Python DBus and GTK Object library dependencies (https://github.com/ansible-collections/community.general/issues/1112). diff --git a/plugins/modules/net_tools/nmcli.py b/plugins/modules/net_tools/nmcli.py index 77893e6d4f..28a8223f2b 100644 --- a/plugins/modules/net_tools/nmcli.py +++ b/plugins/modules/net_tools/nmcli.py @@ -15,21 +15,13 @@ author: - Chris Long (@alcamie101) short_description: Manage Networking requirements: -- dbus -- NetworkManager-libnm (or NetworkManager-glib on older systems) - nmcli description: - - Manage the network devices. Create, modify and manage various connection and device type e.g., ethernet, teams, bonds, vlans etc. - - 'On CentOS 8 and Fedora >=29 like systems, the requirements can be met by installing the following packages: NetworkManager-libnm, - python3-libsemanage, python3-policycoreutils.' - - 'On CentOS 7 and Fedora <=28 like systems, the requirements can be met by installing the following packages: NetworkManager-glib, - libnm-qt-devel.x86_64, nm-connection-editor.x86_64, python-libsemanage, python-policycoreutils.' - - 'On Ubuntu and Debian like systems, the requirements can be met by installing the following packages: network-manager, - python-dbus (or python3-dbus, depending on the Python version in use), libnm-dev.' - - 'On older Ubuntu and Debian like systems, the requirements can be met by installing the following packages: network-manager, - python-dbus (or python3-dbus, depending on the Python version in use), libnm-glib-dev.' - - 'On openSUSE, the requirements can be met by installing the following packages: NetworkManager, python2-dbus-python (or - python3-dbus-python), typelib-1_0-NMClient-1_0 and typelib-1_0-NetworkManager-1_0.' + - 'Manage the network devices. Create, modify and manage various connection and device type e.g., ethernet, teams, bonds, vlans etc.' + - 'On CentOS 8 and Fedora >=29 like systems, the requirements can be met by installing the following packages: NetworkManager.' + - 'On CentOS 7 and Fedora <=28 like systems, the requirements can be met by installing the following packages: NetworkManager-tui.' + - 'On Ubuntu and Debian like systems, the requirements can be met by installing the following packages: network-manager' + - 'On openSUSE, the requirements can be met by installing the following packages: NetworkManager.' options: state: description: @@ -86,10 +78,12 @@ options: description: - A list of up to 3 dns servers. - IPv4 format e.g. to add two IPv4 DNS server addresses, use C(192.0.2.53 198.51.100.53). + elements: str type: list dns4_search: description: - A list of DNS search domains. + elements: str type: list ip6: description: @@ -105,10 +99,12 @@ options: description: - A list of up to 3 dns servers. - IPv6 format e.g. to add two IPv6 DNS server addresses, use C(2001:4860:4860::8888 2001:4860:4860::8844). + elements: str type: list dns6_search: description: - A list of DNS search domains. + elements: str type: list mtu: description: @@ -179,6 +175,7 @@ options: description: - This is only used with bridge - MAC address of the bridge. - Note this requires a recent kernel feature, originally introduced in 3.15 upstream kernel. + type: str slavepriority: description: - This is only used with 'bridge-slave' - [<0-63>] - STP priority of this slave. @@ -540,34 +537,8 @@ EXAMPLES = r''' RETURN = r"""# """ -import traceback - -DBUS_IMP_ERR = None -try: - import dbus - HAVE_DBUS = True -except ImportError: - DBUS_IMP_ERR = traceback.format_exc() - HAVE_DBUS = False - -NM_CLIENT_IMP_ERR = None -HAVE_NM_CLIENT = True -try: - import gi - gi.require_version('NM', '1.0') - from gi.repository import NM -except (ImportError, ValueError): - try: - import gi - gi.require_version('NMClient', '1.0') - gi.require_version('NetworkManager', '1.0') - from gi.repository import NetworkManager, NMClient - except (ImportError, ValueError): - NM_CLIENT_IMP_ERR = traceback.format_exc() - HAVE_NM_CLIENT = False - -from ansible.module_utils.basic import AnsibleModule, missing_required_lib -from ansible.module_utils._text import to_native, to_text +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_text import re @@ -590,42 +561,6 @@ class Nmcli(object): platform = 'Generic' distribution = None - if HAVE_DBUS: - bus = dbus.SystemBus() - # The following is going to be used in dbus code - DEVTYPES = { - 1: "Ethernet", - 2: "Wi-Fi", - 5: "Bluetooth", - 6: "OLPC", - 7: "WiMAX", - 8: "Modem", - 9: "InfiniBand", - 10: "Bond", - 11: "VLAN", - 12: "ADSL", - 13: "Bridge", - 14: "Generic", - 15: "Team", - 16: "VxLan", - 17: "ipip", - 18: "sit", - } - STATES = { - 0: "Unknown", - 10: "Unmanaged", - 20: "Unavailable", - 30: "Disconnected", - 40: "Prepare", - 50: "Config", - 60: "Need Auth", - 70: "IP Config", - 80: "IP Check", - 90: "Secondaries", - 100: "Activated", - 110: "Deactivating", - 120: "Failed" - } def __init__(self, module): self.module = module @@ -637,12 +572,12 @@ class Nmcli(object): self.type = module.params['type'] self.ip4 = module.params['ip4'] self.gw4 = module.params['gw4'] - self.dns4 = ' '.join(module.params['dns4']) if module.params.get('dns4') else None - self.dns4_search = ' '.join(module.params['dns4_search']) if module.params.get('dns4_search') else None + self.dns4 = module.params['dns4'] + self.dns4_search = module.params['dns4_search'] self.ip6 = module.params['ip6'] self.gw6 = module.params['gw6'] - self.dns6 = ' '.join(module.params['dns6']) if module.params.get('dns6') else None - self.dns6_search = ' '.join(module.params['dns6_search']) if module.params.get('dns6_search') else None + self.dns6 = module.params['dns6'] + self.dns6_search = module.params['dns6_search'] self.mtu = module.params['mtu'] self.stp = module.params['stp'] self.priority = module.params['priority'] @@ -696,48 +631,164 @@ class Nmcli(object): cmd = to_text(cmd) return self.module.run_command(cmd, use_unsafe_shell=use_unsafe_shell, data=data) - def merge_secrets(self, proxy, config, setting_name): - try: - # returns a dict of dicts mapping name::setting, where setting is a dict - # mapping key::value. Each member of the 'setting' dict is a secret - secrets = proxy.GetSecrets(setting_name) + def connection_options(self, detect_change=False): + # Options common to multiple connection types. + options = { + 'connection.autoconnect': self.autoconnect, + } - # Copy the secrets into our connection config - for setting in secrets: - for key in secrets[setting]: - config[setting_name][key] = secrets[setting][key] - except Exception: - pass + # IP address options. + if self.ip_conn_type: + options.update({ + 'ipv4.addresses': self.ip4, + 'ipv4.dhcp-client-id': self.dhcp_client_id, + 'ipv4.dns': self.dns4, + 'ipv4.dns-search': self.dns4_search, + 'ipv4.gateway': self.gw4, + 'ipv4.method': self.ipv4_method, + 'ipv6.addresses': self.ip6, + 'ipv6.dns': self.dns6, + 'ipv6.dns-search': self.dns6_search, + 'ipv6.gateway': self.gw6, + 'ipv6.method': self.ipv6_method, + }) - def dict_to_string(self, d): - # Try to trivially translate a dictionary's elements into nice string - # formatting. - dstr = "" - for key in d: - val = d[key] - str_val = "" - add_string = True - if isinstance(val, dbus.Array): - for elt in val: - if isinstance(elt, dbus.Byte): - str_val += "%s " % int(elt) - elif isinstance(elt, dbus.String): - str_val += "%s" % elt - elif isinstance(val, dbus.Dictionary): - dstr += self.dict_to_string(val) - add_string = False - else: - str_val = val - if add_string: - dstr += "%s: %s\n" % (key, str_val) - return dstr + # Layer 2 options. + if self.mac_conn_type: + options.update({self.mac_setting: self.mac}) - def connection_to_string(self, config): - # dump a connection configuration to use in list_connection_info - setting_list = [] - for setting_name in config: - setting_list.append(self.dict_to_string(config[setting_name])) - return setting_list + if self.mtu_conn_type: + options.update({self.mtu_setting: self.mtu}) + + # Connections that can have a master. + if self.slave_conn_type: + options.update({ + 'connection.master': self.master, + }) + + # Options specific to a connection type. + if self.type == 'bond': + options.update({ + 'arp-interval': self.arp_interval, + 'arp-ip-target': self.arp_ip_target, + 'downdelay': self.downdelay, + 'miimon': self.miimon, + 'mode': self.mode, + 'primary': self.primary, + 'updelay': self.updelay, + }) + elif self.type == 'bridge': + options.update({ + 'bridge.ageing-time': self.ageingtime, + 'bridge.forward-delay': self.forwarddelay, + 'bridge.hello-time': self.hellotime, + 'bridge.max-age': self.maxage, + 'bridge.priority': self.priority, + 'bridge.stp': self.stp, + }) + elif self.type == 'bridge-slave': + options.update({ + 'bridge-port.path-cost': self.path_cost, + 'bridge-port.hairpin-mode': self.hairpin, + 'bridge-port.priority': self.slavepriority, + }) + elif self.tunnel_conn_type: + options.update({ + 'ip-tunnel.local': self.ip_tunnel_local, + 'ip-tunnel.mode': self.type, + 'ip-tunnel.parent': self.ip_tunnel_dev, + 'ip-tunnel.remote': self.ip_tunnel_remote, + }) + elif self.type == 'vlan': + options.update({ + 'vlan.id': self.vlanid, + 'vlan.parent': self.vlandev, + }) + elif self.type == 'vxlan': + options.update({ + 'vxlan.id': self.vxlan_id, + 'vxlan.local': self.vxlan_local, + 'vxlan.remote': self.vxlan_remote, + }) + + # Convert settings values based on the situation. + for setting, value in options.items(): + setting_type = self.settings_type(setting) + convert_func = None + if setting_type is bool: + # Convert all bool options to yes/no. + convert_func = self.bool_to_string + if detect_change: + if setting in ('vlan.id', 'vxlan.id'): + # Convert VLAN/VXLAN IDs to text when detecting changes. + convert_func = to_text + elif setting == self.mtu_setting: + # MTU is 'auto' by default when detecting changes. + convert_func = self.mtu_to_string + elif setting_type is list: + # Convert lists to strings for nmcli create/modify commands. + convert_func = self.list_to_string + + if callable(convert_func): + options[setting] = convert_func(options[setting]) + + return options + + @property + def ip_conn_type(self): + return self.type in ( + 'bond', + 'bridge', + 'bridge-slave', + 'ethernet', + 'generic', + 'team', + 'vlan', + ) + + @property + def mac_conn_type(self): + return self.type == 'bridge' + + @property + def mac_setting(self): + if self.type == 'bridge': + return 'bridge.mac-address' + else: + return '802-3-ethernet.cloned-mac-address' + + @property + def mtu_conn_type(self): + return self.type in ( + 'ethernet', + 'team-slave', + ) + + @property + def mtu_setting(self): + return '802-3-ethernet.mtu' + + @staticmethod + def mtu_to_string(mtu): + if not mtu: + return 'auto' + else: + return to_text(mtu) + + @property + def slave_conn_type(self): + return self.type in ( + 'bond-slave', + 'bridge-slave', + 'team-slave', + ) + + @property + def tunnel_conn_type(self): + return self.type in ( + 'ipip', + 'sit', + ) @staticmethod def bool_to_string(boolean): @@ -746,53 +797,32 @@ class Nmcli(object): else: return "no" + @staticmethod + def list_to_string(lst): + return ",".join(lst or [""]) + + @staticmethod + def settings_type(setting): + if setting in ('bridge.stp', + 'bridge-port.hairpin-mode', + 'connection.autoconnect'): + return bool + elif setting in ('ipv4.dns', + 'ipv4.dns-search', + 'ipv6.dns', + 'ipv6.dns-search'): + return list + return str + def list_connection_info(self): - # Ask the settings service for the list of connections it provides - bus = dbus.SystemBus() - - service_name = "org.freedesktop.NetworkManager" - settings = None - try: - proxy = bus.get_object(service_name, "/org/freedesktop/NetworkManager/Settings") - settings = dbus.Interface(proxy, "org.freedesktop.NetworkManager.Settings") - except dbus.exceptions.DBusException as e: - self.module.fail_json(msg="Unable to read Network Manager settings from DBus system bus: %s" % to_native(e), - details="Please check if NetworkManager is installed and" - "service network-manager is started.") - connection_paths = settings.ListConnections() - connection_list = [] - # List each connection's name, UUID, and type - for path in connection_paths: - con_proxy = bus.get_object(service_name, path) - settings_connection = dbus.Interface(con_proxy, "org.freedesktop.NetworkManager.Settings.Connection") - config = settings_connection.GetSettings() - - # Now get secrets too; we grab the secrets for each type of connection - # (since there isn't a "get all secrets" call because most of the time - # you only need 'wifi' secrets or '802.1x' secrets, not everything) and - # merge that into the configuration data - To use at a later stage - self.merge_secrets(settings_connection, config, '802-11-wireless') - self.merge_secrets(settings_connection, config, '802-11-wireless-security') - self.merge_secrets(settings_connection, config, '802-1x') - self.merge_secrets(settings_connection, config, 'gsm') - self.merge_secrets(settings_connection, config, 'cdma') - self.merge_secrets(settings_connection, config, 'ppp') - - # Get the details of the 'connection' setting - s_con = config['connection'] - connection_list.append(s_con['id']) - connection_list.append(s_con['uuid']) - connection_list.append(s_con['type']) - connection_list.append(self.connection_to_string(config)) - return connection_list + cmd = [self.nmcli_bin, '--fields', 'name', '--terse', 'con', 'show'] + (rc, out, err) = self.execute_command(cmd) + if rc != 0: + raise NmcliModuleError(err) + return out.splitlines() def connection_exists(self): - # we are going to use name and type in this instance to find if that connection exists and is of type x - connections = self.list_connection_info() - - for con_item in connections: - if self.conn_name == con_item: - return True + return self.conn_name in self.list_connection_info() def down_connection(self): cmd = [self.nmcli_bin, 'con', 'down', self.conn_name] @@ -802,615 +832,54 @@ class Nmcli(object): cmd = [self.nmcli_bin, 'con', 'up', self.conn_name] return self.execute_command(cmd) - def create_connection_team(self): - cmd = [self.nmcli_bin, 'con', 'add', 'type', 'team', 'con-name'] - # format for creating team interface - if self.conn_name is not None: - cmd.append(self.conn_name) - elif self.ifname is not None: - cmd.append(self.ifname) - cmd.append('ifname') - if self.ifname is not None: - cmd.append(self.ifname) - elif self.conn_name is not None: - cmd.append(self.conn_name) - - options = { - 'ip4': self.ip4, - 'gw4': self.gw4, - 'ip6': self.ip6, - 'gw6': self.gw6, - 'autoconnect': self.bool_to_string(self.autoconnect), - 'ipv4.dns-search': self.dns4_search, - 'ipv6.dns-search': self.dns6_search, - 'ipv4.dhcp-client-id': self.dhcp_client_id, - } - - for key, value in options.items(): - if value is not None: - cmd.extend([key, value]) - - return cmd - - def modify_connection_team(self): - cmd = [self.nmcli_bin, 'con', 'mod', self.conn_name] - options = { - 'ipv4.method': self.ipv4_method, - 'ipv4.address': self.ip4, - 'ipv4.gateway': self.gw4, - 'ipv4.dns': self.dns4, - 'ipv6.method': self.ipv6_method, - 'ipv6.address': self.ip6, - 'ipv6.gateway': self.gw6, - 'ipv6.dns': self.dns6, - 'autoconnect': self.bool_to_string(self.autoconnect), - 'ipv4.dns-search': self.dns4_search, - 'ipv6.dns-search': self.dns6_search, - 'ipv4.dhcp-client-id': self.dhcp_client_id, - } - - for key, value in options.items(): - if value is not None: - cmd.extend([key, value]) - - return cmd - - def create_connection_team_slave(self): - cmd = [self.nmcli_bin, 'connection', 'add', 'type', self.type, 'con-name'] - # format for creating team-slave interface - if self.conn_name is not None: - cmd.append(self.conn_name) - elif self.ifname is not None: - cmd.append(self.ifname) - cmd.append('ifname') - if self.ifname is not None: - cmd.append(self.ifname) - elif self.conn_name is not None: - cmd.append(self.conn_name) - cmd.append('master') - if self.conn_name is not None: - cmd.append(self.master) - return cmd - - def modify_connection_team_slave(self): - cmd = [self.nmcli_bin, 'con', 'mod', self.conn_name, 'connection.master', self.master] - # format for modifying team-slave interface - if self.mtu is not None: - cmd.extend(['802-3-ethernet.mtu', self.mtu]) - return cmd - - def create_connection_bond(self): - cmd = [self.nmcli_bin, 'con', 'add', 'type', 'bond', 'con-name'] - # format for creating bond interface - if self.conn_name is not None: - cmd.append(self.conn_name) - elif self.ifname is not None: - cmd.append(self.ifname) - cmd.append('ifname') - if self.ifname is not None: - cmd.append(self.ifname) - elif self.conn_name is not None: - cmd.append(self.conn_name) - options = { - 'mode': self.mode, - 'ip4': self.ip4, - 'gw4': self.gw4, - 'ip6': self.ip6, - 'gw6': self.gw6, - 'autoconnect': self.bool_to_string(self.autoconnect), - 'ipv4.dns-search': self.dns4_search, - 'ipv6.dns-search': self.dns6_search, - 'miimon': self.miimon, - 'downdelay': self.downdelay, - 'updelay': self.updelay, - 'arp-interval': self.arp_interval, - 'arp-ip-target': self.arp_ip_target, - 'primary': self.primary, - 'ipv4.dhcp-client-id': self.dhcp_client_id, - } - - for key, value in options.items(): - if value is not None: - cmd.extend([key, value]) - return cmd - - def modify_connection_bond(self): - cmd = [self.nmcli_bin, 'con', 'mod', self.conn_name] - # format for modifying bond interface - - options = { - 'ipv4.method': self.ipv4_method, - 'ipv4.address': self.ip4, - 'ipv4.gateway': self.gw4, - 'ipv4.dns': self.dns4, - 'ipv6.method': self.ipv6_method, - 'ipv6.address': self.ip6, - 'ipv6.gateway': self.gw6, - 'ipv6.dns': self.dns6, - 'autoconnect': self.bool_to_string(self.autoconnect), - 'ipv4.dns-search': self.dns4_search, - 'ipv6.dns-search': self.dns6_search, - 'miimon': self.miimon, - 'downdelay': self.downdelay, - 'updelay': self.updelay, - 'arp-interval': self.arp_interval, - 'arp-ip-target': self.arp_ip_target, - 'ipv4.dhcp-client-id': self.dhcp_client_id, - } - - for key, value in options.items(): - if value is not None: - cmd.extend([key, value]) - - return cmd - - def create_connection_bond_slave(self): - cmd = [self.nmcli_bin, 'connection', 'add', 'type', 'bond-slave', 'con-name'] - # format for creating bond-slave interface - if self.conn_name is not None: - cmd.append(self.conn_name) - elif self.ifname is not None: - cmd.append(self.ifname) - cmd.append('ifname') - if self.ifname is not None: - cmd.append(self.ifname) - elif self.conn_name is not None: - cmd.append(self.conn_name) - cmd.append('master') - if self.conn_name is not None: - cmd.append(self.master) - return cmd - - def modify_connection_bond_slave(self): - cmd = [self.nmcli_bin, 'con', 'mod', self.conn_name, 'connection.master', self.master] - # format for modifying bond-slave interface - return cmd - - def create_connection_ethernet(self, conn_type='ethernet'): - # format for creating ethernet interface - # To add an Ethernet connection with static IP configuration, issue a command as follows - # - community.general.nmcli: name=add conn_name=my-eth1 ifname=eth1 type=ethernet ip4=192.0.2.100/24 gw4=192.0.2.1 state=present - # nmcli con add con-name my-eth1 ifname eth1 type ethernet ip4 192.0.2.100/24 gw4 192.0.2.1 - cmd = [self.nmcli_bin, 'con', 'add', 'type'] - if conn_type == 'ethernet': - cmd.append('ethernet') - elif conn_type == 'generic': - cmd.append('generic') - cmd.append('con-name') - if self.conn_name is not None: - cmd.append(self.conn_name) - elif self.ifname is not None: - cmd.append(self.ifname) - cmd.append('ifname') - if self.ifname is not None: - cmd.append(self.ifname) - elif self.conn_name is not None: - cmd.append(self.conn_name) - - options = { - 'ip4': self.ip4, - 'gw4': self.gw4, - 'ip6': self.ip6, - 'gw6': self.gw6, - 'autoconnect': self.bool_to_string(self.autoconnect), - 'ipv4.dns-search': self.dns4_search, - 'ipv6.dns-search': self.dns6_search, - 'ipv4.dhcp-client-id': self.dhcp_client_id, - } - - for key, value in options.items(): - if value is not None: - cmd.extend([key, value]) - - return cmd - - def modify_connection_ethernet(self, conn_type='ethernet'): - cmd = [self.nmcli_bin, 'con', 'mod', self.conn_name] - # format for modifying ethernet interface - # To modify an Ethernet connection with static IP configuration, issue a command as follows - # - community.general.nmcli: conn_name=my-eth1 ifname=eth1 type=ethernet ip4=192.0.2.100/24 gw4=192.0.2.1 state=present - # nmcli con mod con-name my-eth1 ifname eth1 type ethernet ipv4.address 192.0.2.100/24 ipv4.gateway 192.0.2.1 - options = { - 'ipv4.method': self.ipv4_method, - 'ipv4.address': self.ip4, - 'ipv4.gateway': self.gw4, - 'ipv4.dns': self.dns4, - 'ipv6.method': self.ipv6_method, - 'ipv6.address': self.ip6, - 'ipv6.gateway': self.gw6, - 'ipv6.dns': self.dns6, - 'autoconnect': self.bool_to_string(self.autoconnect), - 'ipv4.dns-search': self.dns4_search, - 'ipv6.dns-search': self.dns6_search, - '802-3-ethernet.mtu': self.mtu, - 'ipv4.dhcp-client-id': self.dhcp_client_id, - } - - for key, value in options.items(): - if value is not None: - if key == '802-3-ethernet.mtu' and conn_type != 'ethernet': - continue - cmd.extend([key, value]) - - return cmd - - def create_connection_bridge(self): - # format for creating bridge interface - # To add an Bridge connection with static IP configuration, issue a command as follows - # - community.general.nmcli: name=add conn_name=my-eth1 ifname=eth1 type=bridge ip4=192.0.2.100/24 gw4=192.0.2.1 state=present - # nmcli con add con-name my-eth1 ifname eth1 type bridge ip4 192.0.2.100/24 gw4 192.0.2.1 - cmd = [self.nmcli_bin, 'con', 'add', 'type', 'bridge', 'con-name'] - if self.conn_name is not None: - cmd.append(self.conn_name) - elif self.ifname is not None: - cmd.append(self.ifname) - cmd.append('ifname') - if self.ifname is not None: - cmd.append(self.ifname) - elif self.conn_name is not None: - cmd.append(self.conn_name) - - options = { - 'ip4': self.ip4, - 'gw4': self.gw4, - 'ip6': self.ip6, - 'gw6': self.gw6, - 'autoconnect': self.bool_to_string(self.autoconnect), - 'bridge.ageing-time': self.ageingtime, - 'bridge.forward-delay': self.forwarddelay, - 'bridge.hello-time': self.hellotime, - 'bridge.mac-address': self.mac, - 'bridge.max-age': self.maxage, - 'bridge.priority': self.priority, - 'bridge.stp': self.bool_to_string(self.stp) - } - - for key, value in options.items(): - if value is not None: - cmd.extend([key, value]) - - return cmd - - def modify_connection_bridge(self): - # format for modifying bridge interface - # To add an Bridge connection with static IP configuration, issue a command as follows - # - community.general.nmcli: name=mod conn_name=my-eth1 ifname=eth1 type=bridge ip4=192.0.2.100/24 gw4=192.0.2.1 state=present - # nmcli con mod my-eth1 ifname eth1 type bridge ip4 192.0.2.100/24 gw4 192.0.2.1 - cmd = [self.nmcli_bin, 'con', 'mod', self.conn_name] - - options = { - 'ipv4.method': self.ipv4_method, - 'ipv4.address': self.ip4, - 'ipv4.gateway': self.gw4, - 'ipv6.method': self.ipv6_method, - 'ipv6.address': self.ip6, - 'ipv6.gateway': self.gw6, - 'autoconnect': self.bool_to_string(self.autoconnect), - 'bridge.ageing-time': self.ageingtime, - 'bridge.forward-delay': self.forwarddelay, - 'bridge.hello-time': self.hellotime, - 'bridge.mac-address': self.mac, - 'bridge.max-age': self.maxage, - 'bridge.priority': self.priority, - 'bridge.stp': self.bool_to_string(self.stp) - } - - for key, value in options.items(): - if value is not None: - cmd.extend([key, value]) - - return cmd - - def create_connection_bridge_slave(self): - # format for creating bond-slave interface - cmd = [self.nmcli_bin, 'con', 'add', 'type', 'bridge-slave', 'con-name'] - if self.conn_name is not None: - cmd.append(self.conn_name) - elif self.ifname is not None: - cmd.append(self.ifname) - cmd.append('ifname') - if self.ifname is not None: - cmd.append(self.ifname) - elif self.conn_name is not None: - cmd.append(self.conn_name) - - options = { - 'master': self.master, - 'bridge-port.path-cost': self.path_cost, - 'bridge-port.hairpin': self.bool_to_string(self.hairpin), - 'bridge-port.priority': self.slavepriority, - } - - for key, value in options.items(): - if value is not None: - cmd.extend([key, value]) - - return cmd - - def modify_connection_bridge_slave(self): - # format for modifying bond-slave interface - cmd = [self.nmcli_bin, 'con', 'mod', self.conn_name] - options = { - 'master': self.master, - 'bridge-port.path-cost': self.path_cost, - 'bridge-port.hairpin': self.bool_to_string(self.hairpin), - 'bridge-port.priority': self.slavepriority, - } - - for key, value in options.items(): - if value is not None: - cmd.extend([key, value]) - - return cmd - - def create_connection_vlan(self): - cmd = [self.nmcli_bin] - cmd.append('con') - cmd.append('add') - cmd.append('type') - cmd.append('vlan') - cmd.append('con-name') - - if self.conn_name is not None: - cmd.append(self.conn_name) - elif self.ifname is not None: - cmd.append(self.ifname) + def connection_update(self, nmcli_command): + if nmcli_command == 'create': + cmd = [self.nmcli_bin, 'con', 'add', 'type'] + if self.tunnel_conn_type: + cmd.append('ip-tunnel') + else: + cmd.append(self.type) + cmd.append('con-name') + elif nmcli_command == 'modify': + cmd = [self.nmcli_bin, 'con', 'modify'] else: - cmd.append('vlan%s' % to_text(self.vlanid)) + self.module.fail_json(msg="Invalid nmcli command.") + cmd.append(self.conn_name) - cmd.append('ifname') - if self.ifname is not None: - cmd.append(self.ifname) - elif self.conn_name is not None: - cmd.append(self.conn_name) + # Use connection name as default for interface name on creation. + if nmcli_command == 'create' and self.ifname is None: + ifname = self.conn_name else: - cmd.append('vlan%s' % to_text(self.vlanid)) + ifname = self.ifname - params = {'dev': self.vlandev, - 'id': self.vlanid, - 'ip4': self.ip4 or '', - 'gw4': self.gw4 or '', - 'ip6': self.ip6 or '', - 'gw6': self.gw6 or '', - 'autoconnect': self.bool_to_string(self.autoconnect) - } - for k, v in params.items(): - cmd.extend([k, v]) + options = { + 'connection.interface-name': ifname, + } - return cmd + options.update(self.connection_options()) - def modify_connection_vlan(self): - cmd = [self.nmcli_bin] - cmd.append('con') - cmd.append('mod') + # Constructing the command. + for key, value in options.items(): + if value is not None: + cmd.extend([key, value]) - if self.conn_name is not None: - cmd.append(self.conn_name) - elif self.ifname is not None: - cmd.append(self.ifname) - else: - cmd.append('vlan%s' % to_text(self.vlanid)) - - params = {'vlan.parent': self.vlandev, - 'vlan.id': self.vlanid, - 'ipv4.method': self.ipv4_method, - 'ipv4.address': self.ip4 or '', - 'ipv4.gateway': self.gw4 or '', - 'ipv4.dns': self.dns4 or '', - 'ipv6.method': self.ipv6_method, - 'ipv6.address': self.ip6 or '', - 'ipv6.gateway': self.gw6 or '', - 'ipv6.dns': self.dns6 or '', - 'autoconnect': self.bool_to_string(self.autoconnect) - } - - for k, v in params.items(): - cmd.extend([k, v]) - - return cmd - - def create_connection_vxlan(self): - cmd = [self.nmcli_bin, 'con', 'add', 'type', 'vxlan', 'con-name'] - - if self.conn_name is not None: - cmd.append(self.conn_name) - elif self.ifname is not None: - cmd.append(self.ifname) - else: - cmd.append('vxlan%s' % to_text(self.vxlan_id)) - - cmd.append('ifname') - if self.ifname is not None: - cmd.append(self.ifname) - elif self.conn_name is not None: - cmd.append(self.conn_name) - else: - cmd.append('vxan%s' % to_text(self.vxlan_id)) - - params = {'vxlan.id': self.vxlan_id, - 'vxlan.local': self.vxlan_local, - 'vxlan.remote': self.vxlan_remote, - 'autoconnect': self.bool_to_string(self.autoconnect) - } - for k, v in params.items(): - cmd.extend([k, v]) - - return cmd - - def modify_connection_vxlan(self): - cmd = [self.nmcli_bin, 'con', 'mod'] - - if self.conn_name is not None: - cmd.append(self.conn_name) - elif self.ifname is not None: - cmd.append(self.ifname) - else: - cmd.append('vxlan%s' % to_text(self.vxlan_id)) - - params = {'vxlan.id': self.vxlan_id, - 'vxlan.local': self.vxlan_local, - 'vxlan.remote': self.vxlan_remote, - 'autoconnect': self.bool_to_string(self.autoconnect) - } - for k, v in params.items(): - cmd.extend([k, v]) - return cmd - - def create_connection_ipip(self): - cmd = [self.nmcli_bin, 'con', 'add', 'type', 'ip-tunnel', 'mode', 'ipip', 'con-name'] - - if self.conn_name is not None: - cmd.append(self.conn_name) - elif self.ifname is not None: - cmd.append(self.ifname) - elif self.ip_tunnel_dev is not None: - cmd.append('ipip%s' % to_text(self.ip_tunnel_dev)) - - cmd.append('ifname') - if self.ifname is not None: - cmd.append(self.ifname) - elif self.conn_name is not None: - cmd.append(self.conn_name) - else: - cmd.append('ipip%s' % to_text(self.ipip_dev)) - - if self.ip_tunnel_dev is not None: - cmd.extend(['dev', self.ip_tunnel_dev]) - - params = {'ip-tunnel.local': self.ip_tunnel_local, - 'ip-tunnel.remote': self.ip_tunnel_remote, - 'autoconnect': self.bool_to_string(self.autoconnect) - } - for k, v in params.items(): - cmd.extend([k, v]) - - return cmd - - def modify_connection_ipip(self): - cmd = [self.nmcli_bin, 'con', 'mod'] - - if self.conn_name is not None: - cmd.append(self.conn_name) - elif self.ifname is not None: - cmd.append(self.ifname) - elif self.ip_tunnel_dev is not None: - cmd.append('ipip%s' % to_text(self.ip_tunnel_dev)) - - params = {'ip-tunnel.local': self.ip_tunnel_local, - 'ip-tunnel.remote': self.ip_tunnel_remote, - 'autoconnect': self.bool_to_string(self.autoconnect) - } - for k, v in params.items(): - cmd.extend([k, v]) - return cmd - - def create_connection_sit(self): - cmd = [self.nmcli_bin, 'con', 'add', 'type', 'ip-tunnel', 'mode', 'sit', 'con-name'] - - if self.conn_name is not None: - cmd.append(self.conn_name) - elif self.ifname is not None: - cmd.append(self.ifname) - elif self.ip_tunnel_dev is not None: - cmd.append('sit%s' % to_text(self.ip_tunnel_dev)) - - cmd.append('ifname') - if self.ifname is not None: - cmd.append(self.ifname) - elif self.conn_name is not None: - cmd.append(self.conn_name) - else: - cmd.append('sit%s' % to_text(self.ipip_dev)) - - if self.ip_tunnel_dev is not None: - cmd.extend(['dev', self.ip_tunnel_dev]) - - params = {'ip-tunnel.local': self.ip_tunnel_local, - 'ip-tunnel.remote': self.ip_tunnel_remote, - 'autoconnect': self.bool_to_string(self.autoconnect) - } - for k, v in params.items(): - cmd.extend([k, v]) - - return cmd - - def modify_connection_sit(self): - cmd = [self.nmcli_bin, 'con', 'mod'] - - if self.conn_name is not None: - cmd.append(self.conn_name) - elif self.ifname is not None: - cmd.append(self.ifname) - elif self.ip_tunnel_dev is not None: - cmd.append('sit%s' % to_text(self.ip_tunnel_dev)) - - params = {'ip-tunnel.local': self.ip_tunnel_local, - 'ip-tunnel.remote': self.ip_tunnel_remote, - 'autoconnect': self.bool_to_string(self.autoconnect) - } - for k, v in params.items(): - cmd.extend([k, v]) - return cmd + return self.execute_command(cmd) def create_connection(self): - cmd = [] - if self.type == 'team': - if (self.dns4 is not None) or (self.dns6 is not None): - cmd = self.create_connection_team() - self.execute_command(cmd) - cmd = self.modify_connection_team() - self.execute_command(cmd) - return self.up_connection() - elif (self.dns4 is None) or (self.dns6 is None): - cmd = self.create_connection_team() - elif self.type == 'team-slave': - if self.mtu is not None: - cmd = self.create_connection_team_slave() - self.execute_command(cmd) - cmd = self.modify_connection_team_slave() - return self.execute_command(cmd) - else: - cmd = self.create_connection_team_slave() - elif self.type == 'bond': - if (self.mtu is not None) or (self.dns4 is not None) or (self.dns6 is not None): - cmd = self.create_connection_bond() - self.execute_command(cmd) - cmd = self.modify_connection_bond() - self.execute_command(cmd) - return self.up_connection() - else: - cmd = self.create_connection_bond() - elif self.type == 'bond-slave': - cmd = self.create_connection_bond_slave() - elif self.type == 'ethernet': - if (self.mtu is not None) or (self.dns4 is not None) or (self.dns6 is not None): - cmd = self.create_connection_ethernet() - self.execute_command(cmd) - cmd = self.modify_connection_ethernet() - self.execute_command(cmd) - return self.up_connection() - else: - cmd = self.create_connection_ethernet() - elif self.type == 'bridge': - cmd = self.create_connection_bridge() - elif self.type == 'bridge-slave': - cmd = self.create_connection_bridge_slave() - elif self.type == 'vlan': - cmd = self.create_connection_vlan() - elif self.type == 'vxlan': - cmd = self.create_connection_vxlan() - elif self.type == 'ipip': - cmd = self.create_connection_ipip() - elif self.type == 'sit': - cmd = self.create_connection_sit() - elif self.type == 'generic': - cmd = self.create_connection_ethernet(conn_type='generic') + status = self.connection_update('create') + if self.create_connection_up: + status = self.up_connection() + return status - if cmd: - return self.execute_command(cmd) - else: - self.module.fail_json(msg="Type of device or network connection is required " - "while performing 'create' operation. Please specify 'type' as an argument.") + @property + def create_connection_up(self): + if self.type in ('bond', 'ethernet'): + if (self.mtu is not None) or (self.dns4 is not None) or (self.dns6 is not None): + return True + elif self.type == 'team': + if (self.dns4 is not None) or (self.dns6 is not None): + return True + return False def remove_connection(self): # self.down_connection() @@ -1418,55 +887,10 @@ class Nmcli(object): return self.execute_command(cmd) def modify_connection(self): - cmd = [] - if self.type == 'team': - cmd = self.modify_connection_team() - elif self.type == 'team-slave': - cmd = self.modify_connection_team_slave() - elif self.type == 'bond': - cmd = self.modify_connection_bond() - elif self.type == 'bond-slave': - cmd = self.modify_connection_bond_slave() - elif self.type == 'ethernet': - cmd = self.modify_connection_ethernet() - elif self.type == 'bridge': - cmd = self.modify_connection_bridge() - elif self.type == 'bridge-slave': - cmd = self.modify_connection_bridge_slave() - elif self.type == 'vlan': - cmd = self.modify_connection_vlan() - elif self.type == 'vxlan': - cmd = self.modify_connection_vxlan() - elif self.type == 'ipip': - cmd = self.modify_connection_ipip() - elif self.type == 'sit': - cmd = self.modify_connection_sit() - elif self.type == 'generic': - cmd = self.modify_connection_ethernet(conn_type='generic') - if cmd: - return self.execute_command(cmd) - else: - self.module.fail_json(msg="Type of device or network connection is required " - "while performing 'modify' operation. Please specify 'type' as an argument.") + return self.connection_update('modify') def show_connection(self): - cmd = [self.nmcli_bin, 'con', 'show'] - if self.conn_name is not None: - cmd.append(self.conn_name) - elif self.ifname is not None: - cmd.append(self.ifname) - else: - if self.type == 'vlan': - cmd.append('vlan%s' % to_text(self.vlanid)) - elif self.type == 'vxlan': - cmd.append('vxlan%s' % to_text(self.vxlan_id)) - elif self.type == 'ipip': - cmd.append('ipip%s' % self.ip_tunnel_dev) - elif self.type == 'sit': - cmd.append('sit%s' % self.ip_tunnel_dev) - else: - raise NmcliModuleError( - "Invalid connection identifier for nmcli") + cmd = [self.nmcli_bin, 'con', 'show', self.conn_name] (rc, out, err) = self.execute_command(cmd) @@ -1479,6 +903,7 @@ class Nmcli(object): for line in out.splitlines(): pair = line.split(':', 1) key = pair[0].strip() + key_type = self.settings_type(key) if key and len(pair) > 1: raw_value = pair[1].lstrip() if raw_value == '--': @@ -1492,9 +917,8 @@ class Nmcli(object): alias_key = alias_pair[0] alias_value = alias_pair[1] conn_info[alias_key] = alias_value - elif key in ['ipv4.dns', 'ipv4.dns-search', 'ipv6.dns', 'ipv6.dns-search']: - values = raw_value.split(',') - conn_info[key] = values + elif key_type == list: + conn_info[key] = [s.strip() for s in raw_value.split(',')] else: m_enum = p_enum_value.match(raw_value) if m_enum is not None: @@ -1512,6 +936,7 @@ class Nmcli(object): 'con-name': 'connection.id', 'autoconnect': 'connection.autoconnect', 'ifname': 'connection.interface-name', + 'mac': self.mac_setting, 'master': 'connection.master', 'slave-type': 'connection.slave-type', } @@ -1524,11 +949,6 @@ class Nmcli(object): if not value: continue - # TODO: retain typed list arguments in Nmcli object instead of encoded strings - if key in ['ipv4.dns', 'ipv4.dns-search', 'ipv6.dns', 'ipv6.dns-search']: - list_values = value.split() - value = list_values - if key in conn_info: current_value = conn_info[key] elif key in param_alias: @@ -1560,145 +980,11 @@ class Nmcli(object): return (changed, diff) def is_connection_changed(self): - conn_info = self.show_connection() - changed = False - - if self.type == 'team': - changed, diff = self._compare_conn_params(conn_info, { - 'ipv4.method': self.ipv4_method, - 'ipv4.addresses': self.ip4, - 'ipv4.gateway': self.gw4, - 'ipv4.dns': self.dns4, - 'ipv6.method': self.ipv6_method, - 'ipv6.addresses': self.ip6, - 'ipv6.gateway': self.gw6, - 'ipv6.dns': self.dns6, - 'autoconnect': self.bool_to_string(self.autoconnect), - 'ipv4.dns-search': self.dns4_search, - 'ipv6.dns-search': self.dns6_search, - 'ipv4.dhcp-client-id': self.dhcp_client_id, - }) - elif self.type == 'team-slave': - changed, diff = self._compare_conn_params(conn_info, { - 'connection.master': self.master, - '802-3-ethernet.mtu': to_text(self.mtu) - }) - elif self.type == 'bond': - changed, diff = self._compare_conn_params(conn_info, { - 'ipv4.method': self.ipv4_method, - 'ipv4.addresses': self.ip4, - 'ipv4.gateway': self.gw4, - 'ipv4.dns': self.dns4, - 'ipv6.method': self.ipv6_method, - 'ipv6.addresses': self.ip6, - 'ipv6.gateway': self.gw6, - 'ipv6.dns': self.dns6, - 'autoconnect': self.bool_to_string(self.autoconnect), - 'ipv4.dns-search': self.dns4_search, - 'ipv6.dns-search': self.dns6_search, - 'miimon': self.miimon, - 'downdelay': self.downdelay, - 'updelay': self.updelay, - 'arp-interval': self.arp_interval, - 'arp-ip-target': self.arp_ip_target, - 'ipv4.dhcp-client-id': self.dhcp_client_id, - }) - elif self.type == 'bond-slave': - changed, diff = self._compare_conn_params(conn_info, { - 'connection.master': self.master, - }) - elif self.type == 'ethernet': - changed, diff = self._compare_conn_params(conn_info, { - 'ipv4.method': self.ipv4_method, - 'ipv4.addresses': self.ip4, - 'ipv4.gateway': self.gw4, - 'ipv4.dns': self.dns4, - 'ipv6.method': self.ipv6_method, - 'ipv6.addresses': self.ip6, - 'ipv6.gateway': self.gw6, - 'ipv6.dns': self.dns6, - 'autoconnect': self.bool_to_string(self.autoconnect), - 'ipv4.dns-search': self.dns4_search, - 'ipv6.dns-search': self.dns6_search, - '802-3-ethernet.mtu': self.mtu, - 'ipv4.dhcp-client-id': self.dhcp_client_id, - }) - elif self.type == 'bridge': - changed, diff = self._compare_conn_params(conn_info, { - 'ipv4.method': self.ipv4_method, - 'ipv4.addresses': self.ip4, - 'ipv4.gateway': self.gw4, - 'ipv6.method': self.ipv6_method, - 'ipv6.addresses': self.ip6, - 'ipv6.gateway': self.gw6, - 'autoconnect': self.bool_to_string(self.autoconnect), - 'bridge.ageing-time': self.ageingtime, - 'bridge.forward-delay': self.forwarddelay, - 'bridge.hello-time': self.hellotime, - 'bridge.mac-address': self.mac, - 'bridge.max-age': self.maxage, - 'bridge.priority': self.priority, - 'bridge.stp': self.bool_to_string(self.stp) - }) - elif self.type == 'bridge-slave': - changed, diff = self._compare_conn_params(conn_info, { - 'master': self.master, - 'bridge-port.path-cost': self.path_cost, - 'bridge-port.hairpin-mode': self.bool_to_string(self.hairpin), - 'bridge-port.priority': self.slavepriority, - }) - elif self.type == 'vlan': - changed, diff = self._compare_conn_params(conn_info, { - 'vlan.parent': self.vlandev, - 'vlan.id': self.vlanid, - 'ipv4.method': self.ipv4_method, - 'ipv4.addresses': self.ip4 or '', - 'ipv4.gateway': self.gw4 or '', - 'ipv4.dns': self.dns4 or '', - 'ipv6.method': self.ipv6_method, - 'ipv6.addresses': self.ip6 or '', - 'ipv6.gateway': self.gw6 or '', - 'ipv6.dns': self.dns6 or '', - 'autoconnect': self.bool_to_string(self.autoconnect), - }) - elif self.type == 'vxlan': - changed, diff = self._compare_conn_params(conn_info, { - 'vxlan.id': self.vxlan_id, - 'vxlan.local': self.vxlan_local, - 'vxlan.remote': self.vxlan_remote, - 'autoconnect': self.bool_to_string(self.autoconnect), - }) - elif self.type == 'ipip': - changed, diff = self._compare_conn_params(conn_info, { - 'ip-tunnel.local': self.ip_tunnel_local, - 'ip-tunnel.remote': self.ip_tunnel_remote, - 'autoconnect': self.bool_to_string(self.autoconnect), - }) - elif self.type == 'sit': - changed, diff = self._compare_conn_params(conn_info, { - 'ip-tunnel.local': self.ip_tunnel_local, - 'ip-tunnel.remote': self.ip_tunnel_remote, - 'autoconnect': self.bool_to_string(self.autoconnect), - }) - elif self.type == 'generic': - changed, diff = self._compare_conn_params(conn_info, { - 'ipv4.method': self.ipv4_method, - 'ipv4.addresses': self.ip4, - 'ipv4.gateway': self.gw4, - 'ipv4.dns': self.dns4, - 'ipv6.method': self.ipv6_method, - 'ipv6.addresses': self.ip6, - 'ipv6.gateway': self.gw6, - 'ipv6.dns': self.dns6, - 'autoconnect': self.bool_to_string(self.autoconnect), - 'ipv4.dns-search': self.dns4_search, - 'ipv6.dns-search': self.dns6_search, - 'ipv4.dhcp-client-id': self.dhcp_client_id, - }) - else: - raise NmcliModuleError("Unknown type of device: %s" % self.type) - - return changed, diff + options = { + 'connection.interface-name': self.ifname, + } + options.update(self.connection_options(detect_change=True)) + return self._compare_conn_params(self.show_connection(), options) def main(): @@ -1714,13 +1000,13 @@ def main(): choices=['bond', 'bond-slave', 'bridge', 'bridge-slave', 'ethernet', 'generic', 'ipip', 'sit', 'team', 'team-slave', 'vlan', 'vxlan']), ip4=dict(type='str'), gw4=dict(type='str'), - dns4=dict(type='list'), - dns4_search=dict(type='list'), + dns4=dict(type='list', elements='str'), + dns4_search=dict(type='list', elements='str'), dhcp_client_id=dict(type='str'), ip6=dict(type='str'), gw6=dict(type='str'), - dns6=dict(type='list'), - dns6_search=dict(type='list'), + dns6=dict(type='list', elements='str'), + dns6_search=dict(type='list', elements='str'), # Bond Specific vars mode=dict(type='str', default='balance-rr', choices=['802.3ad', 'active-backup', 'balance-alb', 'balance-rr', 'balance-tlb', 'balance-xor', 'broadcast']), @@ -1762,12 +1048,6 @@ def main(): ) module.run_command_environ_update = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C', LC_CTYPE='C') - if not HAVE_DBUS: - module.fail_json(msg=missing_required_lib('dbus'), exception=DBUS_IMP_ERR) - - if not HAVE_NM_CLIENT: - module.fail_json(msg=missing_required_lib('NetworkManager glib API'), exception=NM_CLIENT_IMP_ERR) - nmcli = Nmcli(module) (rc, out, err) = (None, '', '') diff --git a/tests/sanity/ignore-2.10.txt b/tests/sanity/ignore-2.10.txt index ddb7535341..5e7d1d0774 100644 --- a/tests/sanity/ignore-2.10.txt +++ b/tests/sanity/ignore-2.10.txt @@ -840,8 +840,6 @@ plugins/modules/net_tools/nios/nios_zone.py validate-modules:invalid-ansiblemodu plugins/modules/net_tools/nios/nios_zone.py validate-modules:parameter-alias-self plugins/modules/net_tools/nios/nios_zone.py validate-modules:parameter-type-not-in-doc plugins/modules/net_tools/nios/nios_zone.py validate-modules:undocumented-parameter -plugins/modules/net_tools/nmcli.py validate-modules:parameter-list-no-elements -plugins/modules/net_tools/nmcli.py validate-modules:parameter-type-not-in-doc plugins/modules/net_tools/nsupdate.py validate-modules:parameter-list-no-elements plugins/modules/net_tools/nsupdate.py validate-modules:parameter-type-not-in-doc plugins/modules/net_tools/omapi_host.py validate-modules:parameter-list-no-elements diff --git a/tests/sanity/ignore-2.11.txt b/tests/sanity/ignore-2.11.txt index ddb7535341..5e7d1d0774 100644 --- a/tests/sanity/ignore-2.11.txt +++ b/tests/sanity/ignore-2.11.txt @@ -840,8 +840,6 @@ plugins/modules/net_tools/nios/nios_zone.py validate-modules:invalid-ansiblemodu plugins/modules/net_tools/nios/nios_zone.py validate-modules:parameter-alias-self plugins/modules/net_tools/nios/nios_zone.py validate-modules:parameter-type-not-in-doc plugins/modules/net_tools/nios/nios_zone.py validate-modules:undocumented-parameter -plugins/modules/net_tools/nmcli.py validate-modules:parameter-list-no-elements -plugins/modules/net_tools/nmcli.py validate-modules:parameter-type-not-in-doc plugins/modules/net_tools/nsupdate.py validate-modules:parameter-list-no-elements plugins/modules/net_tools/nsupdate.py validate-modules:parameter-type-not-in-doc plugins/modules/net_tools/omapi_host.py validate-modules:parameter-list-no-elements diff --git a/tests/sanity/ignore-2.9.txt b/tests/sanity/ignore-2.9.txt index 126c1e336a..f521947459 100644 --- a/tests/sanity/ignore-2.9.txt +++ b/tests/sanity/ignore-2.9.txt @@ -652,7 +652,6 @@ plugins/modules/net_tools/nios/nios_zone.py validate-modules:doc-default-does-no plugins/modules/net_tools/nios/nios_zone.py validate-modules:doc-missing-type plugins/modules/net_tools/nios/nios_zone.py validate-modules:parameter-type-not-in-doc plugins/modules/net_tools/nios/nios_zone.py validate-modules:undocumented-parameter -plugins/modules/net_tools/nmcli.py validate-modules:parameter-type-not-in-doc plugins/modules/net_tools/nsupdate.py validate-modules:parameter-type-not-in-doc plugins/modules/notification/bearychat.py validate-modules:parameter-type-not-in-doc plugins/modules/notification/campfire.py validate-modules:doc-missing-type diff --git a/tests/unit/plugins/modules/net_tools/test_nmcli.py b/tests/unit/plugins/modules/net_tools/test_nmcli.py index e5f8824191..40a67e3542 100644 --- a/tests/unit/plugins/modules/net_tools/test_nmcli.py +++ b/tests/unit/plugins/modules/net_tools/test_nmcli.py @@ -238,7 +238,7 @@ TESTCASE_VXLAN = [ TESTCASE_VXLAN_SHOW_OUTPUT = """\ connection.id: non_existent_nw_device -connection.interface-name: existent_nw_device +connection.interface-name: vxlan-existent_nw_device connection.autoconnect: yes vxlan.id: 11 vxlan.local: 192.168.225.5 @@ -306,6 +306,7 @@ TESTCASE_ETHERNET_DHCP_SHOW_OUTPUT = """\ connection.id: non_existent_nw_device connection.interface-name: ethernet_non_existant connection.autoconnect: yes +802-3-ethernet.mtu: auto ipv4.method: auto ipv4.dhcp-client-id: 00:11:22:AA:BB:CC:DD ipv6.method: auto @@ -328,6 +329,7 @@ TESTCASE_ETHERNET_STATIC_SHOW_OUTPUT = """\ connection.id: non_existent_nw_device connection.interface-name: ethernet_non_existant connection.autoconnect: yes +802-3-ethernet.mtu: auto ipv4.method: manual ipv4.addresses: 10.10.10.10/24 ipv4.gateway: 10.10.10.1 @@ -336,150 +338,138 @@ ipv6.method: auto """ -def mocker_set(mocker, connection_exists=False): +def mocker_set(mocker, + connection_exists=False, + execute_return=(0, "", ""), + execute_side_effect=None, + changed_return=None): """ Common mocker object """ - mocker.patch('ansible_collections.community.general.plugins.modules.net_tools.nmcli.HAVE_DBUS', True) - mocker.patch('ansible_collections.community.general.plugins.modules.net_tools.nmcli.HAVE_NM_CLIENT', True) get_bin_path = mocker.patch('ansible.module_utils.basic.AnsibleModule.get_bin_path') get_bin_path.return_value = '/usr/bin/nmcli' connection = mocker.patch.object(nmcli.Nmcli, 'connection_exists') connection.return_value = connection_exists - return connection + execute_command = mocker.patch.object(nmcli.Nmcli, 'execute_command') + if execute_return: + execute_command.return_value = execute_return + if execute_side_effect: + execute_command.side_effect = execute_side_effect + if changed_return: + is_connection_changed = mocker.patch.object(nmcli.Nmcli, 'is_connection_changed') + is_connection_changed.return_value = changed_return @pytest.fixture def mocked_generic_connection_create(mocker): mocker_set(mocker) - command_result = mocker.patch.object(nmcli.Nmcli, 'execute_command') - command_result.return_value = (0, "", "") - return command_result @pytest.fixture def mocked_connection_exists(mocker): - connection = mocker_set(mocker, connection_exists=True) - return connection + mocker_set(mocker, connection_exists=True) @pytest.fixture def mocked_generic_connection_modify(mocker): - mocker_set(mocker, connection_exists=True) - connection_changed = mocker.patch.object( - nmcli.Nmcli, 'is_connection_changed') - connection_changed.return_value = (True, dict()) - command_result = mocker.patch.object(nmcli.Nmcli, 'execute_command') - command_result.return_value = (0, "", "") - return command_result + mocker_set(mocker, + connection_exists=True, + changed_return=(True, dict())) @pytest.fixture def mocked_generic_connection_unchanged(mocker): - mocker_set(mocker, connection_exists=True) - command_result = mocker.patch.object(nmcli.Nmcli, 'execute_command') - command_result.return_value = (0, TESTCASE_GENERIC_SHOW_OUTPUT, "") - return command_result + mocker_set(mocker, + connection_exists=True, + execute_return=(0, TESTCASE_GENERIC_SHOW_OUTPUT, "")) @pytest.fixture def mocked_generic_connection_dns_search_unchanged(mocker): - mocker_set(mocker, connection_exists=True) - command_result = mocker.patch.object(nmcli.Nmcli, 'execute_command') - command_result.return_value = ( - 0, TESTCASE_GENERIC_DNS4_SEARCH_SHOW_OUTPUT, "") - return command_result + mocker_set(mocker, + connection_exists=True, + execute_return=(0, TESTCASE_GENERIC_DNS4_SEARCH_SHOW_OUTPUT, "")) @pytest.fixture def mocked_bond_connection_unchanged(mocker): - mocker_set(mocker, connection_exists=True) - command_result = mocker.patch.object(nmcli.Nmcli, 'execute_command') - command_result.return_value = (0, TESTCASE_BOND_SHOW_OUTPUT, "") - return command_result + mocker_set(mocker, + connection_exists=True, + execute_return=(0, TESTCASE_BOND_SHOW_OUTPUT, "")) @pytest.fixture def mocked_bridge_connection_unchanged(mocker): - mocker_set(mocker, connection_exists=True) - command_result = mocker.patch.object(nmcli.Nmcli, 'execute_command') - command_result.return_value = (0, TESTCASE_BRIDGE_SHOW_OUTPUT, "") - return command_result + mocker_set(mocker, + connection_exists=True, + execute_return=(0, TESTCASE_BRIDGE_SHOW_OUTPUT, "")) @pytest.fixture def mocked_bridge_slave_unchanged(mocker): - mocker_set(mocker, connection_exists=True) - command_result = mocker.patch.object(nmcli.Nmcli, 'execute_command') - command_result.return_value = (0, TESTCASE_BRIDGE_SLAVE_SHOW_OUTPUT, "") - return command_result + mocker_set(mocker, + connection_exists=True, + execute_return=(0, TESTCASE_BRIDGE_SLAVE_SHOW_OUTPUT, "")) @pytest.fixture def mocked_vlan_connection_unchanged(mocker): - mocker_set(mocker, connection_exists=True) - command_result = mocker.patch.object(nmcli.Nmcli, 'execute_command') - command_result.return_value = (0, TESTCASE_VLAN_SHOW_OUTPUT, "") - return command_result + mocker_set(mocker, + connection_exists=True, + execute_return=(0, TESTCASE_VLAN_SHOW_OUTPUT, "")) @pytest.fixture def mocked_vxlan_connection_unchanged(mocker): - mocker_set(mocker, connection_exists=True) - command_result = mocker.patch.object(nmcli.Nmcli, 'execute_command') - command_result.return_value = (0, TESTCASE_VXLAN_SHOW_OUTPUT, "") - return command_result + mocker_set(mocker, + connection_exists=True, + execute_return=(0, TESTCASE_VXLAN_SHOW_OUTPUT, "")) @pytest.fixture def mocked_ipip_connection_unchanged(mocker): - mocker_set(mocker, connection_exists=True) - command_result = mocker.patch.object(nmcli.Nmcli, 'execute_command') - command_result.return_value = (0, TESTCASE_IPIP_SHOW_OUTPUT, "") - return command_result + mocker_set(mocker, + connection_exists=True, + execute_return=(0, TESTCASE_IPIP_SHOW_OUTPUT, "")) @pytest.fixture def mocked_sit_connection_unchanged(mocker): - mocker_set(mocker, connection_exists=True) - command_result = mocker.patch.object(nmcli.Nmcli, 'execute_command') - command_result.return_value = (0, TESTCASE_SIT_SHOW_OUTPUT, "") - return command_result + mocker_set(mocker, + connection_exists=True, + execute_return=(0, TESTCASE_SIT_SHOW_OUTPUT, "")) @pytest.fixture def mocked_ethernet_connection_unchanged(mocker): - mocker_set(mocker, connection_exists=True) - command_result = mocker.patch.object(nmcli.Nmcli, 'execute_command') - command_result.return_value = (0, TESTCASE_ETHERNET_DHCP, "") - return command_result + mocker_set(mocker, + connection_exists=True, + execute_return=(0, TESTCASE_ETHERNET_DHCP, "")) @pytest.fixture def mocked_ethernet_connection_dhcp_unchanged(mocker): - mocker_set(mocker, connection_exists=True) - command_result = mocker.patch.object(nmcli.Nmcli, 'execute_command') - command_result.return_value = (0, TESTCASE_ETHERNET_DHCP_SHOW_OUTPUT, "") - return command_result + mocker_set(mocker, + connection_exists=True, + execute_return=(0, TESTCASE_ETHERNET_DHCP_SHOW_OUTPUT, "")) @pytest.fixture def mocked_ethernet_connection_static_unchanged(mocker): - mocker_set(mocker, connection_exists=True) - command_result = mocker.patch.object(nmcli.Nmcli, 'execute_command') - command_result.return_value = (0, TESTCASE_ETHERNET_STATIC_SHOW_OUTPUT, "") - return command_result + mocker_set(mocker, + connection_exists=True, + execute_return=(0, TESTCASE_ETHERNET_STATIC_SHOW_OUTPUT, "")) @pytest.fixture def mocked_ethernet_connection_dhcp_to_static(mocker): - mocker_set(mocker, connection_exists=True) - command_result = mocker.patch.object(nmcli.Nmcli, 'execute_command') - command_result.side_effect = [ - (0, TESTCASE_ETHERNET_DHCP_SHOW_OUTPUT, ""), - (0, "", ""), - ] - return command_result + mocker_set(mocker, + connection_exists=True, + execute_return=None, + execute_side_effect=( + (0, TESTCASE_ETHERNET_DHCP_SHOW_OUTPUT, ""), + (0, "", ""), + )) @pytest.mark.parametrize('patch_ansible_module', TESTCASE_BOND, indirect=['patch_ansible_module']) @@ -501,10 +491,10 @@ def test_bond_connection_create(mocked_generic_connection_create, capfd): assert args[0][4] == 'bond' assert args[0][5] == 'con-name' assert args[0][6] == 'non_existent_nw_device' - assert args[0][7] == 'ifname' - assert args[0][8] == 'bond_non_existant' - for param in ['gw4', 'primary', 'autoconnect', 'mode', 'active-backup', 'ip4']: + for param in ['ipv4.gateway', 'primary', 'connection.autoconnect', + 'connection.interface-name', 'bond_non_existant', + 'mode', 'active-backup', 'ipv4.addresses']: assert param in args[0] out, err = capfd.readouterr() @@ -547,7 +537,7 @@ def test_generic_connection_create(mocked_generic_connection_create, capfd): assert args[0][5] == 'con-name' assert args[0][6] == 'non_existent_nw_device' - for param in ['autoconnect', 'gw4', 'ip4']: + for param in ['connection.autoconnect', 'ipv4.gateway', 'ipv4.addresses']: assert param in args[0] out, err = capfd.readouterr() @@ -570,10 +560,10 @@ def test_generic_connection_modify(mocked_generic_connection_modify, capfd): assert args[0][0] == '/usr/bin/nmcli' assert args[0][1] == 'con' - assert args[0][2] == 'mod' + assert args[0][2] == 'modify' assert args[0][3] == 'non_existent_nw_device' - for param in ['ipv4.gateway', 'ipv4.address']: + for param in ['ipv4.gateway', 'ipv4.addresses']: assert param in args[0] out, err = capfd.readouterr() @@ -686,8 +676,9 @@ def test_create_bridge(mocked_generic_connection_create, capfd): assert args[0][5] == 'con-name' assert args[0][6] == 'non_existent_nw_device' - for param in ['ip4', '10.10.10.10/24', 'gw4', '10.10.10.1', 'bridge.max-age', '100', 'bridge.stp', 'yes']: - assert param in map(to_text, args[0]) + args_text = list(map(to_text, args[0])) + for param in ['ipv4.addresses', '10.10.10.10/24', 'ipv4.gateway', '10.10.10.1', 'bridge.max-age', '100', 'bridge.stp', 'yes']: + assert param in args_text out, err = capfd.readouterr() results = json.loads(out) @@ -710,10 +701,12 @@ def test_mod_bridge(mocked_generic_connection_modify, capfd): assert args[0][0] == '/usr/bin/nmcli' assert args[0][1] == 'con' - assert args[0][2] == 'mod' + assert args[0][2] == 'modify' assert args[0][3] == 'non_existent_nw_device' - for param in ['ipv4.address', '10.10.10.10/24', 'ipv4.gateway', '10.10.10.1', 'bridge.max-age', '100', 'bridge.stp', 'yes']: - assert param in map(to_text, args[0]) + + args_text = list(map(to_text, args[0])) + for param in ['ipv4.addresses', '10.10.10.10/24', 'ipv4.gateway', '10.10.10.1', 'bridge.max-age', '100', 'bridge.stp', 'yes']: + assert param in args_text out, err = capfd.readouterr() results = json.loads(out) @@ -756,8 +749,9 @@ def test_create_bridge_slave(mocked_generic_connection_create, capfd): 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 ['bridge-port.path-cost', '100']: - assert param in map(to_text, args[0]) + assert param in args_text out, err = capfd.readouterr() results = json.loads(out) @@ -780,11 +774,12 @@ def test_mod_bridge_slave(mocked_generic_connection_modify, capfd): assert args[0][0] == '/usr/bin/nmcli' assert args[0][1] == 'con' - assert args[0][2] == 'mod' + assert args[0][2] == 'modify' assert args[0][3] == 'non_existent_nw_device' + args_text = list(map(to_text, args[0])) for param in ['bridge-port.path-cost', '100']: - assert param in map(to_text, args[0]) + assert param in args_text out, err = capfd.readouterr() results = json.loads(out) @@ -827,8 +822,9 @@ def test_create_vlan_con(mocked_generic_connection_create, capfd): assert args[0][5] == 'con-name' assert args[0][6] == 'non_existent_nw_device' - for param in ['ip4', '10.10.10.10/24', 'gw4', '10.10.10.1', 'id', '10']: - assert param in map(to_text, args[0]) + args_text = list(map(to_text, args[0])) + for param in ['ipv4.addresses', '10.10.10.10/24', 'ipv4.gateway', '10.10.10.1', 'vlan.id', '10']: + assert param in args_text out, err = capfd.readouterr() results = json.loads(out) @@ -851,11 +847,12 @@ def test_mod_vlan_conn(mocked_generic_connection_modify, capfd): assert args[0][0] == '/usr/bin/nmcli' assert args[0][1] == 'con' - assert args[0][2] == 'mod' + assert args[0][2] == 'modify' assert args[0][3] == 'non_existent_nw_device' - for param in ['ipv4.address', '10.10.10.10/24', 'ipv4.gateway', '10.10.10.1', 'vlan.id', '10']: - assert param in map(to_text, args[0]) + args_text = list(map(to_text, args[0])) + for param in ['ipv4.addresses', '10.10.10.10/24', 'ipv4.gateway', '10.10.10.1', 'vlan.id', '10']: + assert param in args_text out, err = capfd.readouterr() results = json.loads(out) @@ -896,10 +893,11 @@ def test_create_vxlan(mocked_generic_connection_create, capfd): assert args[0][4] == 'vxlan' assert args[0][5] == 'con-name' assert args[0][6] == 'non_existent_nw_device' - assert args[0][7] == 'ifname' - for param in ['vxlan.local', '192.168.225.5', 'vxlan.remote', '192.168.225.6', 'vxlan.id', '11']: - assert param in map(to_text, args[0]) + args_text = list(map(to_text, args[0])) + for param in ['connection.interface-name', 'vxlan-existent_nw_device', + 'vxlan.local', '192.168.225.5', 'vxlan.remote', '192.168.225.6', 'vxlan.id', '11']: + assert param in args_text out, err = capfd.readouterr() results = json.loads(out) @@ -921,11 +919,12 @@ def test_vxlan_mod(mocked_generic_connection_modify, capfd): assert args[0][0] == '/usr/bin/nmcli' assert args[0][1] == 'con' - assert args[0][2] == 'mod' + assert args[0][2] == 'modify' assert args[0][3] == 'non_existent_nw_device' + args_text = list(map(to_text, args[0])) for param in ['vxlan.local', '192.168.225.5', 'vxlan.remote', '192.168.225.6', 'vxlan.id', '11']: - assert param in map(to_text, args[0]) + assert param in args_text out, err = capfd.readouterr() results = json.loads(out) @@ -964,17 +963,16 @@ def test_create_ipip(mocked_generic_connection_create, capfd): assert args[0][2] == 'add' assert args[0][3] == 'type' assert args[0][4] == 'ip-tunnel' - assert args[0][5] == 'mode' - assert args[0][6] == 'ipip' - assert args[0][7] == 'con-name' - assert args[0][8] == 'non_existent_nw_device' - assert args[0][9] == 'ifname' - assert args[0][10] == 'ipip-existent_nw_device' - assert args[0][11] == 'dev' - assert args[0][12] == 'non_existent_ipip_device' + assert args[0][5] == 'con-name' + assert args[0][6] == 'non_existent_nw_device' - for param in ['ip-tunnel.local', '192.168.225.5', 'ip-tunnel.remote', '192.168.225.6']: - assert param in map(to_text, args[0]) + args_text = list(map(to_text, args[0])) + for param in ['connection.interface-name', 'ipip-existent_nw_device', + 'ip-tunnel.local', '192.168.225.5', + 'ip-tunnel.mode', 'ipip', + 'ip-tunnel.parent', 'non_existent_ipip_device', + 'ip-tunnel.remote', '192.168.225.6']: + assert param in args_text out, err = capfd.readouterr() results = json.loads(out) @@ -996,11 +994,12 @@ def test_ipip_mod(mocked_generic_connection_modify, capfd): assert args[0][0] == '/usr/bin/nmcli' assert args[0][1] == 'con' - assert args[0][2] == 'mod' + assert args[0][2] == 'modify' assert args[0][3] == 'non_existent_nw_device' + args_text = list(map(to_text, args[0])) for param in ['ip-tunnel.local', '192.168.225.5', 'ip-tunnel.remote', '192.168.225.6']: - assert param in map(to_text, args[0]) + assert param in args_text out, err = capfd.readouterr() results = json.loads(out) @@ -1039,17 +1038,16 @@ def test_create_sit(mocked_generic_connection_create, capfd): assert args[0][2] == 'add' assert args[0][3] == 'type' assert args[0][4] == 'ip-tunnel' - assert args[0][5] == 'mode' - assert args[0][6] == 'sit' - assert args[0][7] == 'con-name' - assert args[0][8] == 'non_existent_nw_device' - assert args[0][9] == 'ifname' - assert args[0][10] == 'sit-existent_nw_device' - assert args[0][11] == 'dev' - assert args[0][12] == 'non_existent_sit_device' + assert args[0][5] == 'con-name' + assert args[0][6] == 'non_existent_nw_device' - for param in ['ip-tunnel.local', '192.168.225.5', 'ip-tunnel.remote', '192.168.225.6']: - assert param in map(to_text, args[0]) + args_text = list(map(to_text, args[0])) + for param in ['connection.interface-name', 'sit-existent_nw_device', + 'ip-tunnel.local', '192.168.225.5', + 'ip-tunnel.mode', 'sit', + 'ip-tunnel.parent', 'non_existent_sit_device', + 'ip-tunnel.remote', '192.168.225.6']: + assert param in args_text out, err = capfd.readouterr() results = json.loads(out) @@ -1071,11 +1069,12 @@ def test_sit_mod(mocked_generic_connection_modify, capfd): assert args[0][0] == '/usr/bin/nmcli' assert args[0][1] == 'con' - assert args[0][2] == 'mod' + assert args[0][2] == 'modify' assert args[0][3] == 'non_existent_nw_device' + args_text = list(map(to_text, args[0])) for param in ['ip-tunnel.local', '192.168.225.5', 'ip-tunnel.remote', '192.168.225.6']: - assert param in map(to_text, args[0]) + assert param in args_text out, err = capfd.readouterr() results = json.loads(out) @@ -1145,10 +1144,10 @@ def test_modify_ethernet_dhcp_to_static(mocked_ethernet_connection_dhcp_to_stati assert args[0][0] == '/usr/bin/nmcli' assert args[0][1] == 'con' - assert args[0][2] == 'mod' + assert args[0][2] == 'modify' assert args[0][3] == 'non_existent_nw_device' - for param in ['ipv4.method', 'ipv4.gateway', 'ipv4.address']: + for param in ['ipv4.method', 'ipv4.gateway', 'ipv4.addresses']: assert param in args[0] out, err = capfd.readouterr() @@ -1166,10 +1165,9 @@ def test_create_ethernet_static(mocked_generic_connection_create, capfd): with pytest.raises(SystemExit): nmcli.main() - assert nmcli.Nmcli.execute_command.call_count == 3 + assert nmcli.Nmcli.execute_command.call_count == 2 arg_list = nmcli.Nmcli.execute_command.call_args_list add_args, add_kw = arg_list[0] - mod_args, mod_kw = arg_list[1] assert add_args[0][0] == '/usr/bin/nmcli' assert add_args[0][1] == 'con' @@ -1178,19 +1176,19 @@ def test_create_ethernet_static(mocked_generic_connection_create, capfd): assert add_args[0][4] == 'ethernet' assert add_args[0][5] == 'con-name' assert add_args[0][6] == 'non_existent_nw_device' - assert add_args[0][7] == 'ifname' - assert add_args[0][8] == 'ethernet_non_existant' - for param in ['ip4', '10.10.10.10/24', 'gw4', '10.10.10.1']: - assert param in map(to_text, add_args[0]) + add_args_text = list(map(to_text, add_args[0])) + for param in ['connection.interface-name', 'ethernet_non_existant', + 'ipv4.addresses', '10.10.10.10/24', + 'ipv4.gateway', '10.10.10.1', + 'ipv4.dns', '1.1.1.1,8.8.8.8']: + assert param in add_args_text - assert mod_args[0][0] == '/usr/bin/nmcli' - assert mod_args[0][1] == 'con' - assert mod_args[0][2] == 'mod' - assert mod_args[0][3] == 'non_existent_nw_device' - - for param in ['ipv4.dns', '1.1.1.1 8.8.8.8']: - assert param in map(to_text, mod_args[0]) + up_args, up_kw = arg_list[1] + assert up_args[0][0] == '/usr/bin/nmcli' + assert up_args[0][1] == 'con' + assert up_args[0][2] == 'up' + assert up_args[0][3] == 'non_existent_nw_device' out, err = capfd.readouterr() results = json.loads(out)