diff --git a/lib/ansible/module_utils/vyos.py b/lib/ansible/module_utils/vyos.py index b4db13ab7f..a21b5de609 100644 --- a/lib/ansible/module_utils/vyos.py +++ b/lib/ansible/module_utils/vyos.py @@ -113,7 +113,11 @@ def load_config(module, commands, commit=False, comment=None): cmd = 'commit' if comment: cmd += ' comment "%s"' % comment - exec_command(module, cmd) + rc, out, err = exec_command(module, cmd) + if rc != 0: + # discard any changes in case of failure + exec_command(module, 'exit discard') + module.fail_json(msg='commit failed: %s' % err) if not commit: exec_command(module, 'exit discard') diff --git a/lib/ansible/modules/network/vyos/vyos_interface.py b/lib/ansible/modules/network/vyos/vyos_interface.py new file mode 100644 index 0000000000..bd47f5ae01 --- /dev/null +++ b/lib/ansible/modules/network/vyos/vyos_interface.py @@ -0,0 +1,321 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2017, Ansible by Red Hat, inc +# +# This file is part of Ansible by Red Hat +# +# 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 . +# + +ANSIBLE_METADATA = {'metadata_version': '1.0', + 'status': ['preview'], + 'supported_by': 'core'} + + +DOCUMENTATION = """ +--- +module: vyos_interface +version_added: "2.4" +author: "Ganesh Nalawade (@ganeshrn)" +short_description: Manage Interface on VyOS network devices +description: + - This module provides declarative management of Interfaces + on VyOS network devices. +options: + name: + description: + - Name of the Interface. + required: true + description: + description: + - Description of Interface. + enabled: + description: + - Interface link status. + speed: + description: + - Interface link speed. + mtu: + description: + - Maximum size of transmit packet. + duplex: + description: + - Interface link status. + default: auto + choices: ['full', 'half', 'auto'] + tx_rate: + description: + - Transmit rate. + rx_rate: + description: + - Receiver rate. + aggregate: + description: List of Interfaces definitions. + purge: + description: + - Purge Interfaces not defined in the aggregate parameter. + This applies only for logical interface. + default: no + state: + description: + - State of the Interface configuration, C(up) means present and + operationally up and C(down) means present and operationally C(down) + default: present + choices: ['present', 'absent', 'up', 'down'] +""" + +EXAMPLES = """ +- name: configure interface + vyos_interface: + name: eth0 + description: test-interface + +- name: remove interface + vyos_interface: + name: eth0 + state: absent + +- name: make interface down + vyos_interface: + name: eth0 + state: down + +- name: make interface up + vyos_interface: + name: eth0 + state: up + +- name: Configure interface speed, mtu, duplex + vyos_interface: + name: eth5 + state: present + speed: 100 + mtu: 256 + duplex: full +""" + +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: + - set interfaces ethernet eth0 description "test-interface" + - set interfaces ethernet eth0 speed 100 + - set interfaces ethernet eth0 mtu 256 + - set interfaces ethernet eth0 duplex full +""" +import re + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.vyos import load_config, get_config +from ansible.module_utils.vyos import vyos_argument_spec, check_args + + +def search_obj_in_list(name, lst): + for o in lst: + if o['name'] == name: + return o + + return None + + +def map_obj_to_commands(updates): + commands = list() + want, have = updates + + params = ('speed', 'description', 'duplex', 'mtu') + for w in want: + name = w['name'] + disable = w['disable'] + state = w['state'] + + obj_in_have = search_obj_in_list(name, have) + set_interface = 'set interfaces ethernet ' + name + delete_interface = 'delete interfaces ethernet ' + name + + if state == 'absent' and obj_in_have: + commands.append(delete_interface) + elif state in ('present', 'up', 'down'): + if obj_in_have: + for item in params: + value = w.get(item) + + if value and value != obj_in_have.get(item): + commands.append(set_interface + ' ' + item + ' ' + str(value)) + + if disable and not obj_in_have.get('disable', False): + commands.append(set_interface + ' disable') + elif not disable and obj_in_have.get('disable', False): + commands.append(delete_interface + ' disable') + else: + commands.append(set_interface) + for item in params: + value = w.get(item) + if value: + commands.append(set_interface + ' ' + item + ' ' + str(value)) + + if disable: + commands.append(set_interface + ' disable') + return commands + + +def map_config_to_obj(module): + data = get_config(module) + obj = [] + for line in data.split('\n'): + if line.startswith('set interfaces ethernet'): + match = re.search(r'set interfaces ethernet (\S+)', line, re.M) + name = match.group(1) + if name: + interface = {} + for item in obj: + if item['name'] == name: + interface = item + break + + if not interface: + interface = {'name': name} + obj.append(interface) + + match = re.search(r'%s (\S+)' % name, line, re.M) + if match: + + param = match.group(1) + if param == 'description': + match = re.search(r'description (\S+)', line, re.M) + description = match.group(1).strip("'") + interface['description'] = description + elif param == 'speed': + match = re.search(r'speed (\S+)', line, re.M) + speed = match.group(1).strip("'") + interface['speed'] = speed + elif param == 'mtu': + match = re.search(r'mtu (\S+)', line, re.M) + mtu = match.group(1).strip("'") + interface['mtu'] = int(mtu) + elif param == 'duplex': + match = re.search(r'duplex (\S+)', line, re.M) + duplex = match.group(1).strip("'") + interface['duplex'] = duplex + elif param.strip("'") == 'disable': + interface['disable'] = True + + return obj + + +def map_params_to_obj(module): + obj = [] + + params = ['speed', 'description', 'duplex', 'mtu'] + aggregate = module.params.get('aggregate') + + if aggregate: + for c in aggregate: + d = c.copy() + if 'name' not in d: + module.fail_json(msg="missing required arguments: %s" % 'name') + + for item in params: + if item not in d: + d[item] = None + + if not d.get('state'): + d['state'] = module.params['state'] + + if d['state'] in ('present', 'up'): + d['disable'] = False + else: + d['disable'] = True + + if d.get('speed'): + d['speed'] = str(d['speed']) + + obj.append(d) + else: + params = { + 'name': module.params['name'], + 'description': module.params['description'], + 'speed': module.params['speed'], + 'mtu': module.params['mtu'], + 'duplex': module.params['duplex'], + 'state': module.params['state'] + } + + state = module.params['state'] + if state == 'present' or state == 'up': + params.update({'disable': False}) + else: + params.update({'disable': True}) + + obj.append(params) + return obj + + +def main(): + """ main entry point for module execution + """ + argument_spec = dict( + name=dict(), + description=dict(), + speed=dict(), + mtu=dict(type='int'), + duplex=dict(choices=['full', 'half', 'auto']), + tx_rate=dict(), + rx_rate=dict(), + aggregate=dict(type='list'), + purge=dict(default=False, type='bool'), + state=dict(default='present', + choices=['present', 'absent', 'up', 'down']) + ) + + argument_spec.update(vyos_argument_spec) + + required_one_of = [['name', 'aggregate']] + mutually_exclusive = [['name', 'aggregate']] + + required_together = (['speed', 'duplex']) + module = AnsibleModule(argument_spec=argument_spec, + required_one_of=required_one_of, + mutually_exclusive=mutually_exclusive, + required_together=required_together, + supports_check_mode=True) + + warnings = list() + check_args(module, warnings) + + result = {'changed': False} + + if warnings: + result['warnings'] = warnings + + want = map_params_to_obj(module) + have = map_config_to_obj(module) + + commands = map_obj_to_commands((want, have)) + result['commands'] = commands + + if commands: + commit = not module.check_mode + diff = load_config(module, commands, commit=commit) + if diff: + if module._diff: + result['diff'] = {'prepared': diff} + result['changed'] = True + + module.exit_json(**result) + +if __name__ == '__main__': + main() diff --git a/lib/ansible/modules/network/vyos/vyos_l3_interface.py b/lib/ansible/modules/network/vyos/vyos_l3_interface.py index 01deb4273d..38125d2062 100644 --- a/lib/ansible/modules/network/vyos/vyos_l3_interface.py +++ b/lib/ansible/modules/network/vyos/vyos_l3_interface.py @@ -195,6 +195,7 @@ def main(): 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() diff --git a/test/integration/targets/net_interface/tests/cli/basic.yaml b/test/integration/targets/net_interface/tests/cli/basic.yaml index b1bec77f7c..d2bab54d28 100644 --- a/test/integration/targets/net_interface/tests/cli/basic.yaml +++ b/test/integration/targets/net_interface/tests/cli/basic.yaml @@ -1,4 +1,10 @@ --- +- debug: msg="START cli/contains.yaml" - include: "{{ role_path }}/tests/eos/basic.yaml" when: hostvars[inventory_hostname]['ansible_network_os'] == 'eos' + +- include: "{{ role_path }}/tests/vyos/basic.yaml" + when: hostvars[inventory_hostname]['ansible_network_os'] == 'vyos' + +- debug: msg="END cli/contains.yaml" diff --git a/test/integration/targets/net_interface/tests/vyos/basic.yaml b/test/integration/targets/net_interface/tests/vyos/basic.yaml new file mode 100644 index 0000000000..94ca750764 --- /dev/null +++ b/test/integration/targets/net_interface/tests/vyos/basic.yaml @@ -0,0 +1,188 @@ +--- +- debug: msg="START net_interface vyos/basic.yaml" + +- name: Set up - delete interface + net_interface: + name: eth1 + state: absent + +- name: Configure interface params + net_interface: + name: eth1 + state: present + description: test-interface + speed: 100 + duplex: half + mtu: 256 + register: result + +- assert: + that: + - 'result.changed == true' + - '"set interfaces ethernet eth1 description test-interface" in result.commands' + - '"set interfaces ethernet eth1 speed 100" in result.commands' + - '"set interfaces ethernet eth1 duplex half" in result.commands' + - '"set interfaces ethernet eth1 mtu 256" in result.commands' + +- name: Configure interface params (idempotent) + net_interface: + name: eth1 + state: present + description: test-interface + speed: 100 + duplex: half + mtu: 256 + register: result + +- assert: + that: + - 'result.changed == false' + +- name: Change interface params + net_interface: + name: eth1 + state: present + description: test-interface-1 + speed: 1000 + duplex: full + mtu: 512 + register: result + +- assert: + that: + - 'result.changed == true' + - '"set interfaces ethernet eth1 description test-interface-1" in result.commands' + - '"set interfaces ethernet eth1 speed 1000" in result.commands' + - '"set interfaces ethernet eth1 duplex full" in result.commands' + - '"set interfaces ethernet eth1 mtu 512" in result.commands' + +- name: Disable interface + net_interface: + name: eth1 + state: down + register: result + +- assert: + that: + - 'result.changed == true' + - '"set interfaces ethernet eth1 disable" in result.commands' + +- name: Enable interface + net_interface: + name: eth1 + state: up + register: result + +- assert: + that: + - 'result.changed == true' + - '"delete interfaces ethernet eth1 disable" in result.commands' + +- name: Delete interface + net_interface: + name: eth1 + state: absent + register: result + +- assert: + that: + - 'result.changed == true' + - '"delete interfaces ethernet eth1" in result.commands' + +- name: Delete interface (idempotent) + net_interface: + name: eth1 + state: absent + register: result + +- assert: + that: + - 'result.changed == false' + +- name: Aggregate setup- delete interface + net_interface: + name: eth2 + state: absent + register: result + +- name: Set interface on aggregate + net_interface: + aggregate: + - { name: eth1, description: test-interface-1, speed: 100, duplex: half, mtu: 512} + - { name: eth2, description: test-interface-2, speed: 1000, duplex: full, mtu: 256} + register: result + +- assert: + that: + - 'result.changed == true' + - '"set interfaces ethernet eth1 description test-interface-1" in result.commands' + - '"set interfaces ethernet eth1 speed 100" in result.commands' + - '"set interfaces ethernet eth1 duplex half" in result.commands' + - '"set interfaces ethernet eth1 mtu 512" in result.commands' + - '"set interfaces ethernet eth2 description test-interface-2" in result.commands' + - '"set interfaces ethernet eth2 speed 1000" in result.commands' + - '"set interfaces ethernet eth2 duplex full" in result.commands' + - '"set interfaces ethernet eth2 mtu 256" in result.commands' + +- name: Set interface on aggregate (idempotent) + net_interface: + aggregate: + - { name: eth1, description: test-interface-1, speed: 100, duplex: half, mtu: 512} + - { name: eth2, description: test-interface-2, speed: 1000, duplex: full, mtu: 256} + register: result + +- assert: + that: + - 'result.changed == false' + +- name: Disable interface on aggregate + net_interface: + aggregate: + - { name: eth1, description: test-interface-1, speed: 100, duplex: half, mtu: 512, state: down} + - { name: eth2, description: test-interface-2, speed: 1000, duplex: full, mtu: 256, state: down} + register: result + +- assert: + that: + - 'result.changed == true' + - '"set interfaces ethernet eth1 disable" in result.commands' + - '"set interfaces ethernet eth2 disable" in result.commands' + +- name: Enable interface on aggregate + net_interface: + aggregate: + - { name: eth1, description: test-interface-1, speed: 100, duplex: half, mtu: 512, state: present} + - { name: eth2, description: test-interface-2, speed: 1000, duplex: full, mtu: 256, state: present} + register: result + +- assert: + that: + - 'result.changed == true' + - '"delete interfaces ethernet eth1 disable" in result.commands' + - '"delete interfaces ethernet eth2 disable" in result.commands' + +- name: Delete interface aggregate + net_interface: + aggregate: + - { name: eth1, state: absent} + - { name: eth2, state: absent} + register: result + +- assert: + that: + - 'result.changed == true' + - '"delete interfaces ethernet eth1" in result.commands' + - '"delete interfaces ethernet eth2" in result.commands' + +- name: Delete interface aggregate (idempotent) + net_interface: + aggregate: + - { name: eth1, state: absent} + - { name: eth2, state: absent} + register: result + +- assert: + that: + - 'result.changed == false' + +- debug: msg="END net_interface vyos/basic.yaml" diff --git a/test/integration/targets/vyos_interface/defaults/main.yaml b/test/integration/targets/vyos_interface/defaults/main.yaml new file mode 100644 index 0000000000..9ef5ba5165 --- /dev/null +++ b/test/integration/targets/vyos_interface/defaults/main.yaml @@ -0,0 +1,3 @@ +--- +testcase: "*" +test_items: [] diff --git a/test/integration/targets/vyos_interface/tasks/cli.yaml b/test/integration/targets/vyos_interface/tasks/cli.yaml new file mode 100644 index 0000000000..d675462dd0 --- /dev/null +++ b/test/integration/targets/vyos_interface/tasks/cli.yaml @@ -0,0 +1,15 @@ +--- +- name: collect all cli test cases + find: + paths: "{{ role_path }}/tests/cli" + patterns: "{{ testcase }}.yaml" + register: test_cases + +- name: set test_items + set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}" + +- name: run test case + include: "{{ test_case_to_run }}" + with_items: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run diff --git a/test/integration/targets/vyos_interface/tasks/main.yaml b/test/integration/targets/vyos_interface/tasks/main.yaml new file mode 100644 index 0000000000..415c99d8b1 --- /dev/null +++ b/test/integration/targets/vyos_interface/tasks/main.yaml @@ -0,0 +1,2 @@ +--- +- { include: cli.yaml, tags: ['cli'] } diff --git a/test/integration/targets/vyos_interface/tests/cli/basic.yaml b/test/integration/targets/vyos_interface/tests/cli/basic.yaml new file mode 100644 index 0000000000..8ffb47692c --- /dev/null +++ b/test/integration/targets/vyos_interface/tests/cli/basic.yaml @@ -0,0 +1,186 @@ +--- +- debug: msg="START vyos_interface cli/basic.yaml" + +- name: Set up - delete interface + vyos_interface: + name: eth1 + state: absent + +- name: Configure interface params + vyos_interface: + name: eth1 + state: present + description: test-interface + speed: 100 + duplex: half + mtu: 256 + register: result + +- assert: + that: + - 'result.changed == true' + - '"set interfaces ethernet eth1 description test-interface" in result.commands' + - '"set interfaces ethernet eth1 speed 100" in result.commands' + - '"set interfaces ethernet eth1 duplex half" in result.commands' + - '"set interfaces ethernet eth1 mtu 256" in result.commands' + +- name: Configure interface params (idempotent) + vyos_interface: + name: eth1 + state: present + description: test-interface + speed: 100 + duplex: half + mtu: 256 + register: result + +- assert: + that: + - 'result.changed == false' + +- name: Change interface params + vyos_interface: + name: eth1 + state: present + description: test-interface-1 + speed: 1000 + duplex: full + mtu: 512 + register: result + +- assert: + that: + - 'result.changed == true' + - '"set interfaces ethernet eth1 description test-interface-1" in result.commands' + - '"set interfaces ethernet eth1 speed 1000" in result.commands' + - '"set interfaces ethernet eth1 duplex full" in result.commands' + - '"set interfaces ethernet eth1 mtu 512" in result.commands' + +- name: Disable interface + vyos_interface: + name: eth1 + state: down + register: result + +- assert: + that: + - 'result.changed == true' + - '"set interfaces ethernet eth1 disable" in result.commands' + +- name: Enable interface + vyos_interface: + name: eth1 + state: up + register: result + +- assert: + that: + - 'result.changed == true' + - '"delete interfaces ethernet eth1 disable" in result.commands' + +- name: Delete interface + vyos_interface: + name: eth1 + state: absent + register: result + +- assert: + that: + - 'result.changed == true' + - '"delete interfaces ethernet eth1" in result.commands' + +- name: Delete interface (idempotent) + vyos_interface: + name: eth1 + state: absent + register: result + +- assert: + that: + - 'result.changed == false' + +- name: Aggregate setup- delete interface + vyos_interface: + name: eth2 + state: absent + register: result + +- name: Set interface on aggregate + vyos_interface: + aggregate: + - { name: eth1, description: test-interface-1, speed: 100, duplex: half, mtu: 512} + - { name: eth2, description: test-interface-2, speed: 1000, duplex: full, mtu: 256} + register: result + +- assert: + that: + - 'result.changed == true' + - '"set interfaces ethernet eth1 description test-interface-1" in result.commands' + - '"set interfaces ethernet eth1 speed 100" in result.commands' + - '"set interfaces ethernet eth1 duplex half" in result.commands' + - '"set interfaces ethernet eth1 mtu 512" in result.commands' + - '"set interfaces ethernet eth2 description test-interface-2" in result.commands' + - '"set interfaces ethernet eth2 speed 1000" in result.commands' + - '"set interfaces ethernet eth2 duplex full" in result.commands' + - '"set interfaces ethernet eth2 mtu 256" in result.commands' + +- name: Set interface on aggregate (idempotent) + vyos_interface: + aggregate: + - { name: eth1, description: test-interface-1, speed: 100, duplex: half, mtu: 512} + - { name: eth2, description: test-interface-2, speed: 1000, duplex: full, mtu: 256} + register: result + +- assert: + that: + - 'result.changed == false' + +- name: Disable interface on aggregate + vyos_interface: + aggregate: + - { name: eth1, description: test-interface-1, speed: 100, duplex: half, mtu: 512, state: down} + - { name: eth2, description: test-interface-2, speed: 1000, duplex: full, mtu: 256, state: down} + register: result + +- assert: + that: + - 'result.changed == true' + - '"set interfaces ethernet eth1 disable" in result.commands' + - '"set interfaces ethernet eth2 disable" in result.commands' + +- name: Enable interface on aggregate + vyos_interface: + aggregate: + - { name: eth1, description: test-interface-1, speed: 100, duplex: half, mtu: 512, state: present} + - { name: eth2, description: test-interface-2, speed: 1000, duplex: full, mtu: 256, state: present} + register: result + +- assert: + that: + - 'result.changed == true' + - '"delete interfaces ethernet eth1 disable" in result.commands' + - '"delete interfaces ethernet eth2 disable" in result.commands' + +- name: Delete interface aggregate + vyos_interface: + aggregate: + - { name: eth1, state: absent} + - { name: eth2, state: absent} + register: result + +- assert: + that: + - 'result.changed == true' + - '"delete interfaces ethernet eth1" in result.commands' + - '"delete interfaces ethernet eth2" in result.commands' + +- name: Delete interface aggregate (idempotent) + vyos_interface: + aggregate: + - { name: eth1, state: absent} + - { name: eth2, state: absent} + register: result + +- assert: + that: + - 'result.changed == false' diff --git a/test/integration/vyos.yaml b/test/integration/vyos.yaml index 157e138c85..fb6d33f0df 100644 --- a/test/integration/vyos.yaml +++ b/test/integration/vyos.yaml @@ -84,6 +84,12 @@ rescue: - set_fact: test_failed=true + - block: + - include_role: + name: vyos_interface + when: "limit_to in ['*', 'vyos_interface']" + rescue: + - set_fact: test_failed=true ########### - name: Has any previous test failed?