diff --git a/lib/ansible/module_utils/network/cnos/cnos.py b/lib/ansible/module_utils/network/cnos/cnos.py index b0c12f86aa7..2e16b82ced8 100644 --- a/lib/ansible/module_utils/network/cnos/cnos.py +++ b/lib/ansible/module_utils/network/cnos/cnos.py @@ -34,6 +34,7 @@ import time import socket import re +import json try: from ansible.module_utils.network.cnos import cnos_errorcodes from ansible.module_utils.network.cnos import cnos_devicerules @@ -192,11 +193,23 @@ def run_cnos_commands(module, commands, check_rc=True): return str(retVal) +def get_capabilities(module): + if hasattr(module, '_cnos_capabilities'): + return module._cnos_capabilities + try: + capabilities = Connection(module._socket_path).get_capabilities() + except ConnectionError as exc: + module.fail_json(msg=to_text(exc, errors='surrogate_then_replace')) + module._cnos_capabilities = json.loads(capabilities) + return module._cnos_capabilities + + def load_config(module, config): try: conn = get_connection(module) conn.get('enable') - conn.edit_config(config) + resp = conn.edit_config(config) + return resp.get('response') except ConnectionError as exc: module.fail_json(msg=to_text(exc)) diff --git a/lib/ansible/modules/network/cnos/cnos_l3_interface.py b/lib/ansible/modules/network/cnos/cnos_l3_interface.py new file mode 100644 index 00000000000..59e121f1ebd --- /dev/null +++ b/lib/ansible/modules/network/cnos/cnos_l3_interface.py @@ -0,0 +1,424 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +# +# Copyright (C) 2019 Lenovo, Inc. +# (c) 2019, Ansible by Red Hat, inc +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# +# Module to work on Link Aggregation with Lenovo Switches +# Lenovo Networking +# +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = """ +--- +module: cnos_l3_interface +version_added: "2.8" +author: "Anil Kumar Muraleedharan (@amuraleedhar)" +short_description: Manage Layer-3 interfaces on Lenovo CNOS network devices. +description: + - This module provides declarative management of Layer-3 interfaces + on CNOS network devices. +notes: + - Tested against CNOS 10.8.1 +options: + name: + description: + - Name of the Layer-3 interface to be configured eg. GigabitEthernet0/2 + ipv4: + description: + - IPv4 address to be set for the Layer-3 interface mentioned in I(name) + option. The address format is /, the mask is number + in range 0-32 eg. 10.241.107.1/24 + ipv6: + description: + - IPv6 address to be set for the Layer-3 interface mentioned in I(name) + option. The address format is /, the mask is number + in range 0-128 eg. fd5d:12c9:2201:1::1/64 + aggregate: + description: + - List of Layer-3 interfaces definitions. Each of the entry in aggregate + list should define name of interface C(name) and a optional C(ipv4) or + C(ipv6) address. + state: + description: + - State of the Layer-3 interface configuration. It indicates if the + configuration should be present or absent on remote device. + default: present + choices: ['present', 'absent'] + provider: + description: + - B(Deprecated) + - "Starting with Ansible 2.5 we recommend using + C(connection: network_cli)." + - For more information please see the + L(CNOS Platform Options guide, ../network/user_guide/platform_cnos.html). + - HORIZONTALLINE + - A dict object containing connection details. + suboptions: + host: + description: + - Specifies the DNS host name or address for connecting to the remote + device over the specified transport. The value of host is used as + the destination address for the transport. + required: true + port: + description: + - Specifies the port to use when building the connection to the + remote device. + default: 22 + username: + description: + - Configures the username to use to authenticate the connection to + the remote device. This value is used to authenticate + the SSH session. If the value is not specified in the task, the + value of environment variable C(ANSIBLE_NET_USERNAME) will be used + instead. + password: + description: + - Specifies the password to use to authenticate the connection to + the remote device. This value is used to authenticate + the SSH session. If the value is not specified in the task, the + value of environment variable C(ANSIBLE_NET_PASSWORD) will be used + instead. + timeout: + description: + - Specifies the timeout in seconds for communicating with the network + device for either connecting or sending commands. If the timeout + is exceeded before the operation is completed, the module will + error. + default: 10 + ssh_keyfile: + description: + - Specifies the SSH key to use to authenticate the connection to + the remote device. This value is the path to the + key used to authenticate the SSH session. If the value is not + specified in the task, the value of environment variable + C(ANSIBLE_NET_SSH_KEYFILE)will be used instead. + authorize: + description: + - Instructs the module to enter privileged mode on the remote device + before sending any commands. If not specified, the device will + attempt to execute all commands in non-privileged mode. If the + value is not specified in the task, the value of environment + variable C(ANSIBLE_NET_AUTHORIZE) will be used instead. + type: bool + default: 'no' + auth_pass: + description: + - Specifies the password to use if required to enter privileged mode + on the remote device. If I(authorize) is false, then this argument + does nothing. If the value is not specified in the task, the value + of environment variable C(ANSIBLE_NET_AUTH_PASS) will be used + instead. +""" + +EXAMPLES = """ +- name: Remove Ethernet1/33 IPv4 and IPv6 address + cnos_l3_interface: + name: Ethernet1/33 + state: absent + +- name: Set Ethernet1/33 IPv4 address + cnos_l3_interface: + name: Ethernet1/33 + ipv4: 10.241.107.1/24 + +- name: Set Ethernet1/33 IPv6 address + cnos_l3_interface: + name: Ethernet1/33 + ipv6: "fd5d:12c9:2201:1::1/64" + +- name: Set Ethernet1/33 in dhcp + cnos_l3_interface: + name: Ethernet1/33 + ipv4: dhcp + ipv6: dhcp + +- name: Set interface Vlan1 (SVI) IPv4 address + cnos_l3_interface: + name: Vlan1 + ipv4: 192.168.0.5/24 + +- name: Set IP addresses on aggregate + cnos_l3_interface: + aggregate: + - { name: Ethernet1/33, ipv4: 10.241.107.1/24 } + - { name: GigabitEthernet1/33, ipv4: 10.241.107.1/24, + ipv6: "fd5d:12c9:2201:1::1/64" } + +- name: Remove IP addresses on aggregate + cnos_l3_interface: + aggregate: + - { name: Ethernet1/33, ipv4: 10.241.107.1/24 } + - { name: Ethernet1/3``3, ipv4: 10.241.107.1/24, + ipv6: "fd5d:12c9:2201:1::1/64" } + state: absent +""" + +RETURN = """ +commands: + description: The list of configuration mode commands to send to the device + returned: always, except for the platforms that use Netconf transport to + manage the device. + type: list + sample: + - interface Ethernet1/33 + - ip address 10.241.107.1 255.255.255.0 + - ipv6 address fd5d:12c9:2201:1::1/64 +""" +import re + +from copy import deepcopy + +from ansible.module_utils._text import to_text +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.network.cnos.cnos import get_config, load_config +from ansible.module_utils.network.cnos.cnos import cnos_argument_spec +from ansible.module_utils.network.common.config import NetworkConfig +from ansible.module_utils.network.common.utils import remove_default_spec +from ansible.module_utils.network.common.utils import is_netmask, is_masklen +from ansible.module_utils.network.common.utils import to_netmask, to_masklen + + +def validate_ipv4(value, module): + if value: + address = value.split('/') + if len(address) != 2: + module.fail_json( + msg='address format is /,got invalid format %s' % value) + if not is_masklen(address[1]): + module.fail_json( + msg='invalid value for mask: %s, mask should be in range 0-32' % address[1]) + + +def validate_ipv6(value, module): + if value: + address = value.split('/') + if len(address) != 2: + module.fail_json( + msg='address format is /, got invalid format %s' % value) + else: + if not 0 <= int(address[1]) <= 128: + module.fail_json( + msg='invalid value for mask: %s, mask should be in range 0-128' % address[1]) + + +def validate_param_values(module, obj, param=None): + if param is None: + param = module.params + for key in obj: + # validate the param value (if validator func exists) + validator = globals().get('validate_%s' % key) + if callable(validator): + validator(param.get(key), module) + + +def parse_config_argument(configobj, name, arg=None): + cfg = configobj['interface %s' % name] + cfg = '\n'.join(cfg.children) + + values = [] + matches = re.finditer(r'%s (.+)$' % arg, cfg, re.M) + for match in matches: + match_str = match.group(1).strip() + if arg == 'ipv6 address': + values.append(match_str) + else: + values = match_str + break + + return values or None + + +def search_obj_in_list(name, lst): + for o in lst: + if o['name'].lower() == name.lower(): + return o + + return None + + +def map_obj_to_commands(updates, module): + commands = list() + want, have = updates + for w in want: + name = w['name'] + ipv4 = w['ipv4'] + ipv6 = w['ipv6'] + state = w['state'] + + interface = 'interface ' + name + commands.append(interface) + + obj_in_have = search_obj_in_list(name, have) + if state == 'absent' and obj_in_have: + if obj_in_have['ipv4']: + if ipv4: + address = ipv4.split('/') + if len(address) == 2: + ipv4 = '{0} {1}'.format( + address[0], to_netmask(address[1])) + commands.append('no ip address %s' % ipv4) + else: + commands.append('no ip address') + if obj_in_have['ipv6']: + if ipv6: + commands.append('no ipv6 address %s' % ipv6) + else: + commands.append('no ipv6 address') + if 'dhcp' in obj_in_have['ipv6']: + commands.append('no ipv6 address dhcp') + + elif state == 'present': + if ipv4: + if obj_in_have is None or obj_in_have.get('ipv4') is None or ipv4 != obj_in_have['ipv4']: + address = ipv4.split('/') + if len(address) == 2: + ipv4 = '{0} {1}'.format( + address[0], to_netmask(address[1])) + commands.append('ip address %s' % ipv4) + + if ipv6: + if obj_in_have is None or obj_in_have.get('ipv6') is None or ipv6.lower() not in [addr.lower() for addr in obj_in_have['ipv6']]: + commands.append('ipv6 address %s' % ipv6) + if commands[-1] == interface: + commands.pop(-1) + + return commands + + +def map_config_to_obj(module): + config = get_config(module) + configobj = NetworkConfig(indent=1, contents=config) + + match = re.findall(r'^interface (\S+)', config, re.M) + if not match: + return list() + + instances = list() + + for item in set(match): + ipv4 = parse_config_argument(configobj, item, 'ip address') + if ipv4: + # eg. 192.168.2.10 255.255.255.0 -> 192.168.2.10/24 + address = ipv4.strip().split(' ') + if len(address) == 2 and is_netmask(address[1]): + ipv4 = '{0}/{1}'.format(address[0], to_text(to_masklen(address[1]))) + + obj = { + 'name': item, + 'ipv4': ipv4, + 'ipv6': parse_config_argument(configobj, item, 'ipv6 address'), + 'state': 'present' + } + instances.append(obj) + + return instances + + +def map_params_to_obj(module): + obj = [] + + aggregate = module.params.get('aggregate') + if aggregate: + for item in aggregate: + for key in item: + if item.get(key) is None: + item[key] = module.params[key] + + validate_param_values(module, item, item) + obj.append(item.copy()) + else: + obj.append({ + 'name': module.params['name'], + 'ipv4': module.params['ipv4'], + 'ipv6': module.params['ipv6'], + 'state': module.params['state'] + }) + + validate_param_values(module, obj) + + return obj + + +def main(): + """ main entry point for module execution + """ + element_spec = dict( + name=dict(), + ipv4=dict(), + ipv6=dict(), + state=dict(default='present', + choices=['present', 'absent']) + ) + + aggregate_spec = deepcopy(element_spec) + aggregate_spec['name'] = dict(required=True) + + # remove default in aggregate spec, to handle common arguments + remove_default_spec(aggregate_spec) + + argument_spec = dict( + aggregate=dict(type='list', elements='dict', options=aggregate_spec), + ) + + argument_spec.update(element_spec) + argument_spec.update(cnos_argument_spec) + + required_one_of = [['name', 'aggregate']] + mutually_exclusive = [['name', 'aggregate']] + module = AnsibleModule(argument_spec=argument_spec, + required_one_of=required_one_of, + mutually_exclusive=mutually_exclusive, + supports_check_mode=True) + + warnings = list() + + result = {'changed': False} + + want = map_params_to_obj(module) + have = map_config_to_obj(module) + + commands = map_obj_to_commands((want, have), module) + result['commands'] = commands + + if commands: + if not module.check_mode: + resp = load_config(module, commands) + if resp is not None: + warnings.extend((out for out in resp if out)) + + result['changed'] = True + + if warnings: + result['warnings'] = warnings + if 'overlaps with address configured on' in warnings[0]: + result['failed'] = True + result['msg'] = warnings[0] + if 'Cannot set overlapping address' in warnings[0]: + result['failed'] = True + result['msg'] = warnings[0] + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/lib/ansible/plugins/cliconf/cnos.py b/lib/ansible/plugins/cliconf/cnos.py index 8b02a95032d..187e49f00ef 100644 --- a/lib/ansible/plugins/cliconf/cnos.py +++ b/lib/ansible/plugins/cliconf/cnos.py @@ -20,7 +20,7 @@ import re import json from itertools import chain - +from ansible.module_utils.common._collections_compat import Mapping from ansible.module_utils._text import to_bytes, to_text from ansible.module_utils.network.common.utils import to_list from ansible.plugins.cliconf import CliconfBase, enable_mode @@ -76,12 +76,34 @@ class Cliconf(CliconfBase): return self.send_command(cmd) @enable_mode - def edit_config(self, command): - for cmd in chain(['configure terminal'], to_list(command), ['end']): - self.send_command(cmd) + def edit_config(self, candidate=None, commit=True, + replace=None, comment=None): + resp = {} + results = [] + requests = [] + if commit: + self.send_command('configure terminal') + for line in to_list(candidate): + if not isinstance(line, Mapping): + line = {'command': line} + + cmd = line['command'] + if cmd != 'end' and cmd[0] != '!': + results.append(self.send_command(**line)) + requests.append(cmd) + + self.send_command('end') + else: + raise ValueError('check mode is not supported') + + resp['request'] = requests + resp['response'] = results + return resp - def get(self, command, prompt=None, answer=None, sendonly=False, check_all=False): - return self.send_command(command=command, prompt=prompt, answer=answer, sendonly=sendonly, check_all=check_all) + def get(self, command, prompt=None, answer=None, sendonly=False, + check_all=False): + return self.send_command(command=command, prompt=prompt, answer=answer, + sendonly=sendonly, check_all=check_all) def get_capabilities(self): result = super(Cliconf, self).get_capabilities() diff --git a/test/integration/targets/cnos_l3_interface/aliases b/test/integration/targets/cnos_l3_interface/aliases new file mode 100644 index 00000000000..cdb50333531 --- /dev/null +++ b/test/integration/targets/cnos_l3_interface/aliases @@ -0,0 +1,2 @@ +# No Lenovo Switch simulator yet, so not enabled +unsupported \ No newline at end of file diff --git a/test/integration/targets/cnos_l3_interface/cnos_l3_interface_sample_hosts b/test/integration/targets/cnos_l3_interface/cnos_l3_interface_sample_hosts new file mode 100644 index 00000000000..95ed7af99df --- /dev/null +++ b/test/integration/targets/cnos_l3_interface/cnos_l3_interface_sample_hosts @@ -0,0 +1,14 @@ +# You have to paste this dummy information in /etc/ansible/hosts +# Notes: +# - Comments begin with the '#' character +# - Blank lines are ignored +# - Groups of hosts are delimited by [header] elements +# - You can enter hostnames or ip addresses +# - A hostname/ip can be a member of multiple groups +# +# In the /etc/ansible/hosts file u have to enter [cnos_l3_interface_sample] tag +# Following you should specify IP Adresses details +# Please change and with appropriate value for your switch. + +[cnos_l3_interface_sample] +10.241.107.39 ansible_network_os=cnos ansible_ssh_user=admin ansible_ssh_pass=admin deviceType=g8272_cnos test_interface=ethernet1/33 test_interface2=ethernet1/44 diff --git a/test/integration/targets/cnos_l3_interface/defaults/main.yaml b/test/integration/targets/cnos_l3_interface/defaults/main.yaml new file mode 100644 index 00000000000..5f709c5aac1 --- /dev/null +++ b/test/integration/targets/cnos_l3_interface/defaults/main.yaml @@ -0,0 +1,2 @@ +--- +testcase: "*" diff --git a/test/integration/targets/cnos_l3_interface/tasks/cli.yaml b/test/integration/targets/cnos_l3_interface/tasks/cli.yaml new file mode 100644 index 00000000000..303af407622 --- /dev/null +++ b/test/integration/targets/cnos_l3_interface/tasks/cli.yaml @@ -0,0 +1,22 @@ +--- +- name: collect all cli test cases + find: + paths: "{{ role_path }}/tests/cli" + patterns: "{{ testcase }}.yaml" + register: test_cases + delegate_to: localhost + +- name: set test_items + set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}" + +- name: run test cases (connection=network_cli) + include: "{{ test_case_to_run }}" + with_items: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run + +- name: run test case (connection=local) + include: "{{ test_case_to_run }} ansible_connection=local" + with_first_found: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run diff --git a/test/integration/targets/cnos_l3_interface/tasks/main.yaml b/test/integration/targets/cnos_l3_interface/tasks/main.yaml new file mode 100644 index 00000000000..415c99d8b12 --- /dev/null +++ b/test/integration/targets/cnos_l3_interface/tasks/main.yaml @@ -0,0 +1,2 @@ +--- +- { include: cli.yaml, tags: ['cli'] } diff --git a/test/integration/targets/cnos_l3_interface/tests/cli/basic.yaml b/test/integration/targets/cnos_l3_interface/tests/cli/basic.yaml new file mode 100644 index 00000000000..375f8cd5190 --- /dev/null +++ b/test/integration/targets/cnos_l3_interface/tests/cli/basic.yaml @@ -0,0 +1,277 @@ +--- +- debug: msg="START cnos_l3_interface cli/basic.yaml on connection={{ ansible_connection }}" + +- name: Delete interface ipv4 and ipv6 address(setup) + cnos_l3_interface: + name: "{{ test_interface }}" + state: absent + provider: "{{ cli }}" + register: result + +- name: Delete interface ipv4 and ipv6 address 2 (setup) + cnos_l3_interface: + name: "{{ test_interface2 }}" + state: absent + provider: "{{ cli }}" + register: result + +- name: Setup - Ensure interfaces are switchport + cnos_config: + lines: + - no shutdown + parents: + - "interface {{ item }}" + provider: "{{ cli }}" + loop: + - "{{ test_interface }}" + - "{{ test_interface2 }}" + +- name: Configure interface ipv4 address + cnos_l3_interface: + name: "{{ test_interface }}" + ipv4: 10.241.113.1/24 + state: present + provider: "{{ cli }}" + register: result + +- assert: + that: + - 'result.changed == true' + - '"interface {{ test_interface }}" in result.commands' + - '"ip address 10.241.113.1 255.255.255.0" in result.commands' + +- name: Configure interface ipv4 address (idempotent) + cnos_l3_interface: + name: "{{ test_interface }}" + ipv4: 10.241.113.1/24 + state: present + provider: "{{ cli }}" + register: result + +- assert: &unchanged + that: + - 'result.changed == false' + +- name: Assign same ipv4 address to other interface (fail) + cnos_l3_interface: + name: "{{ test_interface2 }}" + ipv4: 10.241.113.1/24 + state: present + provider: "{{ cli }}" + ignore_errors: yes + register: result + +- assert: + that: + - "result.failed == true" + - "result.msg is defined" + +- name: Change interface ipv4 address + cnos_l3_interface: + name: "{{ test_interface }}" + ipv4: dhcp + state: present + provider: "{{ cli }}" + register: result + +- assert: + that: + - 'result.changed == true' + - '"interface {{ test_interface }}" in result.commands' + - '"ip address dhcp" in result.commands' + +- name: Configure interface ipv6 address + cnos_l3_interface: &ipv6-1 + name: "{{ test_interface }}" + ipv6: fd5d:12c9:2201:1::1/64 + state: present + provider: "{{ cli }}" + register: result + +- assert: + that: + - 'result.changed == true' + - '"interface {{ test_interface }}" in result.commands' + - '"ipv6 address fd5d:12c9:2201:1::1/64" in result.commands' + +- name: Configure interface ipv6 address (idempotent) + cnos_l3_interface: *ipv6-1 + register: result + +- assert: + that: + - 'result.changed == false' + +- name: Configure second ipv6 address on interface + cnos_l3_interface: &ipv6-2 + name: "{{ test_interface }}" + ipv6: fd5d:12c9:2291:1::1/64 + state: present + provider: "{{ cli }}" + register: result + +- assert: + that: + - 'result.changed == true' + - '"interface {{ test_interface }}" in result.commands' + - '"ipv6 address fd5d:12c9:2291:1::1/64" in result.commands' + +- name: Ensure first ipv6 address still associated with interface + cnos_l3_interface: *ipv6-1 + register: result + +- assert: + that: + - 'result.changed == true' + +- name: Ensure second ipv6 address still associated with interface + cnos_l3_interface: *ipv6-2 + register: result + +- assert: + that: + - 'result.changed == true' + +- name: Assign same ipv6 address to other interface (fail) + cnos_l3_interface: + name: "{{ test_interface2 }}" + ipv6: fd5d:12c9:2201:1::1/64 + state: present + provider: "{{ cli }}" + ignore_errors: yes + register: result + +- assert: + that: + - 'result.changed == true' + - '"interface {{ test_interface2 }}" in result.commands' + - '"ipv6 address fd5d:12c9:2201:1::1/64" in result.commands' + +- name: Change interface ipv6 address + cnos_l3_interface: + name: "{{ test_interface }}" + ipv6: dhcp + state: present + provider: "{{ cli }}" + register: result + +- assert: + that: + - 'result.changed == true' + - '"interface {{ test_interface }}" in result.commands' + - '"ipv6 address dhcp" in result.commands' + +- name: Delete interface ipv4 and ipv6 address + cnos_l3_interface: + name: "{{ test_interface }}" + state: absent + provider: "{{ cli }}" + register: result + +- assert: + that: + - 'result.changed == true' + - '"interface {{ test_interface }}" in result.commands' + - '"no ip address" in result.commands' + - '"no ipv6 address" in result.commands' + +- name: Delete interface ipv4 and ipv6 address (idempotent) + cnos_l3_interface: + name: "{{ test_interface }}" + state: absent + provider: "{{ cli }}" + register: result + +- assert: + that: + - 'result.changed == false' + +- name: Delete second interface ipv4 and ipv6 address (setup) + cnos_l3_interface: + name: "{{ test_interface2 }}" + state: absent + provider: "{{ cli }}" + register: result + +- name: Configure ipv4 and ipv6 address using aggregate + cnos_l3_interface: + aggregate: + - { name: "{{ test_interface }}", ipv4: 10.241.113.1/24, ipv6: "fd5d:12c9:2201:2::2/64" } + - { name: "{{ test_interface2 }}", ipv4: 10.141.233.2/16, ipv6: "fd5e:12c9:2201:3::3/32" } + provider: "{{ cli }}" + register: result + +- assert: + that: + - 'result.changed == true' + - '"interface {{ test_interface }}" in result.commands' + - '"ip address 10.241.113.1 255.255.255.0" in result.commands' + - '"ipv6 address fd5d:12c9:2201:2::2/64" in result.commands' + - '"interface {{ test_interface2 }}" in result.commands' + - '"ip address 10.141.233.2 255.255.0.0" in result.commands' + - '"ipv6 address fd5e:12c9:2201:3::3/32" in result.commands' + +- name: Configure ipv4 and ipv6 address using aggregate (idempotent) + cnos_l3_interface: + aggregate: + - { name: "{{ test_interface }}", ipv4: 10.241.113.1/24, ipv6: "fd5d:12c9:2201:2::2/64" } + - { name: "{{ test_interface2 }}", ipv4: 10.141.233.2/16, ipv6: "fd5e:12c9:2201:3::3/32" } + provider: "{{ cli }}" + register: result + +- assert: + that: + - 'result.changed == false' + +- name: Change ipv4 and ipv6 address using aggregate + cnos_l3_interface: + aggregate: + - { name: "{{ test_interface }}", ipv4: 10.241.113.1/16, ipv6: "fd5a:12c9:2201:4::4/32" } + - { name: "{{ test_interface2 }}", ipv4: 10.141.233.2/24, ipv6: "fd5b:12c9:2201:5::5/90" } + provider: "{{ cli }}" + register: result + +- assert: + that: + - 'result.changed == true' + - '"interface {{ test_interface }}" in result.commands' + - '"ip address 10.241.113.1 255.255.0.0" in result.commands' + - '"ipv6 address fd5a:12c9:2201:4::4/32" in result.commands' + - '"interface {{ test_interface2 }}" in result.commands' + - '"ip address 10.141.233.2 255.255.255.0" in result.commands' + - '"ipv6 address fd5b:12c9:2201:5::5/90" in result.commands' + + +- name: Delete ipv4 and ipv6 address using aggregate + cnos_l3_interface: + aggregate: + - { name: "{{ test_interface }}" } + - { name: "{{ test_interface2 }}" } + state: absent + provider: "{{ cli }}" + register: result + +- assert: + that: + - 'result.changed == true' + - '"interface {{ test_interface }}" in result.commands' + - '"no ip address" in result.commands' + - '"no ipv6 address" in result.commands' + - '"interface {{ test_interface2 }}" in result.commands' + - '"no ip address" in result.commands' + - '"no ipv6 address" in result.commands' + +- name: Delete ipv4 and ipv6 address using aggregate (idempotent) + cnos_l3_interface: + aggregate: + - { name: "{{ test_interface }}" } + - { name: "{{ test_interface2 }}" } + state: absent + provider: "{{ cli }}" + register: result + +- assert: + that: + - 'result.changed == false' + +- debug: msg="END cnos_l3_interface cli/basic.yaml on connection={{ ansible_connection }}" diff --git a/test/integration/targets/cnos_l3_interface/vars/main.yaml b/test/integration/targets/cnos_l3_interface/vars/main.yaml new file mode 100644 index 00000000000..aa25153ec86 --- /dev/null +++ b/test/integration/targets/cnos_l3_interface/vars/main.yaml @@ -0,0 +1,9 @@ +--- +cli: + host: "{{ inventory_hostname }}" + port: 22 + username: admin + password: admin + timeout: 30 + authorize: True + auth_pass: diff --git a/test/units/modules/network/cnos/fixtures/l3_interface_config.cfg b/test/units/modules/network/cnos/fixtures/l3_interface_config.cfg new file mode 100644 index 00000000000..ada22464026 --- /dev/null +++ b/test/units/modules/network/cnos/fixtures/l3_interface_config.cfg @@ -0,0 +1,27 @@ +! +version "10.8.0.42" +! +hostname ip10-241-107-39 +! +vlan 13 + name dave +! +interface Ethernet1/9 + ip address 10.201.107.1 255.255.255.0 + ipv6 address dead::beaf/64 + description Bleh +! +interface Ethernet1/33 + description Hentammoo + load-interval counter 2 33 + switchport access vlan 33 + storm-control broadcast level 12.50 + mtu 66 + microburst-detection enable threshold 25 + lldp tlv-select max-frame-size + lacp port-priority 33 + spanning-tree mst 33-35 cost 33 + spanning-tree bpduguard enable +! +! +end diff --git a/test/units/modules/network/cnos/test_cnos_l3_interface.py b/test/units/modules/network/cnos/test_cnos_l3_interface.py new file mode 100644 index 00000000000..4369c31f8f8 --- /dev/null +++ b/test/units/modules/network/cnos/test_cnos_l3_interface.py @@ -0,0 +1,77 @@ +# +# (c) 2018 Lenovo. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import re +import json + +from units.compat.mock import patch +from ansible.modules.network.cnos import cnos_l3_interface +from units.modules.utils import set_module_args +from .cnos_module import TestCnosModule, load_fixture + + +class TestCnosL3InterfaceModule(TestCnosModule): + module = cnos_l3_interface + + def setUp(self): + super(TestCnosL3InterfaceModule, self).setUp() + self._patch_get_config = patch( + 'ansible.modules.network.cnos.cnos_l3_interface.get_config' + ) + self._patch_load_config = patch( + 'ansible.modules.network.cnos.cnos_l3_interface.load_config' + ) + + self._get_config = self._patch_get_config.start() + self._load_config = self._patch_load_config.start() + + def tearDown(self): + super(TestCnosL3InterfaceModule, self).tearDown() + self._patch_get_config.stop() + self._patch_load_config.stop() + + def load_fixtures(self, commands=None): + config_file = 'l3_interface_config.cfg' + self._get_config.return_value = load_fixture(config_file) + self._load_config.return_value = None + + def test_cnos_l3_interface_ipv4_address(self, *args, **kwargs): + set_module_args(dict( + name='Ethernet 1/35', + ipv4='192.168.4.1/24' + )) + commands = [ + 'interface Ethernet 1/35', + 'ip address 192.168.4.1 255.255.255.0' + ] + result = self.execute_module(changed=True, commands=commands) + + def test_cnos_l3_interface_absent(self, *args, **kwargs): + set_module_args(dict( + name='Ethernet1/9', + state='absent' + )) + commands = [ + 'interface Ethernet1/9', + 'no ip address', + 'no ipv6 address' + ] + result = self.execute_module(changed=True, commands=commands)