diff --git a/lib/ansible/modules/network/vyos/vyos_static_route.py b/lib/ansible/modules/network/vyos/vyos_static_route.py new file mode 100644 index 00000000000..0e557ee0eca --- /dev/null +++ b/lib/ansible/modules/network/vyos/vyos_static_route.py @@ -0,0 +1,240 @@ +#!/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_static_route +version_added: "2.4" +author: "Trishna Guha (@trishnag)" +short_description: Manage static IP routes on Cisco VyOS network devices +description: + - This module provides declarative management of static + IP routes on Vyatta VyOS network devices. +options: + prefix: + description: + - Network prefix of the static route. + C(mask) param should be ignored if C(prefix) is provided + with C(mask) value C(prefix/mask). + mask: + description: + - Network prefix mask of the static route. + next_hop: + description: + - Next hop IP of the static route. + admin_distance: + description: + - Admin distance of the static route. + collection: + description: List of static route definitions + purge: + description: + - Purge static routes not defined in the collections parameter. + default: no + state: + description: + - State of the static route configuration. + default: present + choices: ['present', 'absent'] +""" + +EXAMPLES = """ +- name: configure static route + vyos_static_route: + prefix: 192.168.2.0 + mask: 24 + next_hop: 10.0.0.1 +- name: configure static route prefix/mask + vyos_static_route: + prefix: 192.168.2.0/16 + next_hop: 10.0.0.1 +- name: remove configuration + vyos_static_route: + prefix: 192.168.2.0 + mask: 16 + next_hop: 10.0.0.1 + state: absent +- name: configure collections of static routes + vyos_static_route: + collection: + - { prefix: 192.168.2.0, mask: 24, next_hop: 10.0.0.1 } + - { prefix: 192.168.3.0, mask: 16, next_hop: 10.0.2.1 } + - { prefix: 192.168.3.0/16, next_hop: 10.0.2.1 } +""" + +RETURN = """ +commands: + description: The list of configuration mode commands to send to the device + returned: always + type: list + sample: + - set protocols static route 192.168.2.0/16 next-hop 10.0.0.1 +""" + +import re + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.vyos import get_config, load_config +from ansible.module_utils.vyos import vyos_argument_spec, check_args + + +def spec_to_commands(updates, module): + commands = list() + want, have = updates + for w in want: + prefix = w['prefix'] + mask = w['mask'] + next_hop = w['next_hop'] + admin_distance = w['admin_distance'] + state = w['state'] + del w['state'] + + if state == 'absent' and w in have: + commands.append('delete protocols static route %s/%s' % (prefix, mask)) + elif state == 'present' and w not in have: + cmd = 'set protocols static route %s/%s next-hop %s' % (prefix, mask, next_hop) + if admin_distance != 'None': + cmd += ' distance %s' % (admin_distance) + commands.append(cmd) + + return commands + + +def config_to_dict(module): + data = get_config(module) + obj = [] + + for line in data.split('\n'): + if line.startswith('set protocols static route'): + match = re.search(r'static route (\S+)', line, re.M) + prefix = match.group(1).split('/')[0] + mask = match.group(1).split('/')[1] + if 'next-hop' in line: + match_hop = re.search(r'next-hop (\S+)', line, re.M) + next_hop = match_hop.group(1).strip("'") + + match_distance = re.search(r'distance (\S+)', line, re.M) + if match_distance is not None: + admin_distance = match_distance.group(1)[1:-1] + else: + admin_distance = None + + if admin_distance is not None: + obj.append({'prefix': prefix, + 'mask': mask, + 'next_hop': next_hop, + 'admin_distance': admin_distance}) + else: + obj.append({'prefix': prefix, + 'mask': mask, + 'next_hop': next_hop, + 'admin_distance': 'None'}) + + return obj + + +def map_params_to_obj(module): + obj = [] + + if 'collection' in module.params and module.params['collection']: + for c in module.params['collection']: + d = c.copy() + if '/' in d['prefix']: + d['mask'] = d['prefix'].split('/')[1] + d['prefix'] = d['prefix'].split('/')[0] + + if 'state' not in d: + d['state'] = module.params['state'] + if 'admin_distance' not in d: + d['admin_distance'] = str(module.params['admin_distance']) + + obj.append(d) + else: + prefix = module.params['prefix'].strip() + if '/' in prefix: + mask = prefix.split('/')[1] + prefix = prefix.split('/')[0] + else: + mask = module.params['mask'].strip() + next_hop = module.params['next_hop'].strip() + admin_distance = str(module.params['admin_distance']) + state = module.params['state'] + + obj.append({ + 'prefix': prefix, + 'mask': mask, + 'next_hop': next_hop, + 'admin_distance': admin_distance, + 'state': state + }) + + return obj + + +def main(): + """ main entry point for module execution + """ + argument_spec = dict( + prefix=dict(type='str'), + mask=dict(type='str'), + next_hop=dict(type='str'), + admin_distance=dict(type='int'), + collection=dict(type='list'), + purge=dict(type='bool'), + state=dict(default='present', choices=['present', 'absent']) + ) + + argument_spec.update(vyos_argument_spec) + required_one_of = [['collection', 'prefix']] + required_together = [['prefix', 'next_hop']] + mutually_exclusive = [['collection', 'prefix']] + + module = AnsibleModule(argument_spec=argument_spec, + required_one_of=required_one_of, + 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 = config_to_dict(module) + + commands = spec_to_commands((want, have), module) + result['commands'] = commands + + if commands: + commit = not module.check_mode + load_config(module, commands, commit=commit) + result['changed'] = True + + module.exit_json(**result) + +if __name__ == '__main__': + main() diff --git a/test/integration/targets/net_static_route/tests/cli/basic.yaml b/test/integration/targets/net_static_route/tests/cli/basic.yaml index a76e75ee24d..ab94d350651 100644 --- a/test/integration/targets/net_static_route/tests/cli/basic.yaml +++ b/test/integration/targets/net_static_route/tests/cli/basic.yaml @@ -1,3 +1,9 @@ --- - include: "{{ role_path }}/tests/ios/basic.yaml" when: hostvars[inventory_hostname]['ansible_network_os'] == 'ios' + +- include: "{{ role_path }}/tests/junos/basic.yaml" + when: hostvars[inventory_hostname]['ansible_network_os'] == 'junos' + +- include: "{{ role_path }}/tests/vyos/basic.yaml" + when: hostvars[inventory_hostname]['ansible_network_os'] == 'vyos' diff --git a/test/integration/targets/net_static_route/tests/vyos/basic.yaml b/test/integration/targets/net_static_route/tests/vyos/basic.yaml new file mode 100644 index 00000000000..7b641f128ee --- /dev/null +++ b/test/integration/targets/net_static_route/tests/vyos/basic.yaml @@ -0,0 +1,127 @@ +--- +- name: create static route + net_static_route: + prefix: 172.24.0.0/24 + next_hop: 192.168.42.64 + state: present + provider: "{{ cli }}" + register: result + +- assert: + that: + - 'result.changed == true' + - '"set protocols static route 172.24.0.0/24 next-hop 192.168.42.64" in result.commands' + +- name: create static route again (idempotent) + net_static_route: + prefix: 172.24.0.0 + mask: 24 + next_hop: 192.168.42.64 + state: present + provider: "{{ cli }}" + register: result + +- assert: + that: + - 'result.changed == false' + +- name: modify admin distance of static route + net_static_route: + prefix: 172.24.0.0/24 + next_hop: 192.168.42.64 + admin_distance: 1 + state: present + provider: "{{ cli }}" + register: result + +- assert: + that: + - 'result.changed == true' + - '"set protocols static route 172.24.0.0/24 next-hop 192.168.42.64 distance 1" in result.commands' + +- name: modify admin distance of static route again (idempotent) + net_static_route: + prefix: 172.24.0.0 + mask: 24 + next_hop: 192.168.42.64 + admin_distance: 1 + state: present + provider: "{{ cli }}" + register: result + +- assert: + that: + - 'result.changed == false' + +- name: delete static route + net_static_route: + prefix: 172.24.0.0/24 + next_hop: 192.168.42.64 + admin_distance: 1 + state: absent + provider: "{{ cli }}" + register: result + +- assert: + that: + - 'result.changed == true' + - '"delete protocols static route 172.24.0.0/24" in result.commands' + +- name: delete static route again (idempotent) + net_static_route: + prefix: 172.24.0.0/24 + next_hop: 192.168.42.64 + admin_distance: 1 + state: absent + provider: "{{ cli }}" + register: result + +- assert: + that: + - 'result.changed == false' + +- name: Add static route collections + net_static_route: + collection: + - { prefix: 172.24.1.0/24, next_hop: 192.168.42.64 } + - { prefix: 172.24.2.0, mask: 24, next_hop: 192.168.42.64 } + state: present + provider: "{{ cli }}" + register: result + +- assert: + that: + - 'result.changed == true' + - '"set protocols static route 172.24.1.0/24 next-hop 192.168.42.64" in result.commands' + - '"set protocols static route 172.24.2.0/24 next-hop 192.168.42.64" in result.commands' + +- name: Add and remove static route collections with overrides + net_static_route: + collection: + - { prefix: 172.24.1.0/24, next_hop: 192.168.42.64 } + - { prefix: 172.24.2.0/24, next_hop: 192.168.42.64, state: absent } + - { prefix: 172.24.3.0/24, next_hop: 192.168.42.64 } + state: present + provider: "{{ cli }}" + register: result + +- assert: + that: + - 'result.changed == true' + - '"delete protocols static route 172.24.2.0/24" in result.commands' + - '"set protocols static route 172.24.3.0/24 next-hop 192.168.42.64" in result.commands' + +- name: Remove static route collections + net_static_route: + collection: + - { prefix: 172.24.1.0/24, next_hop: 192.168.42.64 } + - { prefix: 172.24.3.0/24, next_hop: 192.168.42.64 } + state: absent + provider: "{{ cli }}" + register: result + +- assert: + that: + - 'result.changed == true' + - '"delete protocols static route 172.24.1.0/24" in result.commands' + - '"delete protocols static route 172.24.3.0/24" in result.commands' diff --git a/test/integration/targets/vyos_static_route/defaults/main.yaml b/test/integration/targets/vyos_static_route/defaults/main.yaml new file mode 100644 index 00000000000..9ef5ba51651 --- /dev/null +++ b/test/integration/targets/vyos_static_route/defaults/main.yaml @@ -0,0 +1,3 @@ +--- +testcase: "*" +test_items: [] diff --git a/test/integration/targets/vyos_static_route/tasks/cli.yaml b/test/integration/targets/vyos_static_route/tasks/cli.yaml new file mode 100644 index 00000000000..d675462dd02 --- /dev/null +++ b/test/integration/targets/vyos_static_route/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_static_route/tasks/main.yaml b/test/integration/targets/vyos_static_route/tasks/main.yaml new file mode 100644 index 00000000000..415c99d8b12 --- /dev/null +++ b/test/integration/targets/vyos_static_route/tasks/main.yaml @@ -0,0 +1,2 @@ +--- +- { include: cli.yaml, tags: ['cli'] } diff --git a/test/integration/targets/vyos_static_route/tests/cli/basic.yaml b/test/integration/targets/vyos_static_route/tests/cli/basic.yaml new file mode 100644 index 00000000000..f17ffcbf7db --- /dev/null +++ b/test/integration/targets/vyos_static_route/tests/cli/basic.yaml @@ -0,0 +1,127 @@ +--- +- name: create static route + vyos_static_route: + prefix: 172.24.0.0/24 + next_hop: 192.168.42.64 + state: present + provider: "{{ cli }}" + register: result + +- assert: + that: + - 'result.changed == true' + - '"set protocols static route 172.24.0.0/24 next-hop 192.168.42.64" in result.commands' + +- name: create static route again (idempotent) + vyos_static_route: + prefix: 172.24.0.0 + mask: 24 + next_hop: 192.168.42.64 + state: present + provider: "{{ cli }}" + register: result + +- assert: + that: + - 'result.changed == false' + +- name: modify admin distance of static route + vyos_static_route: + prefix: 172.24.0.0/24 + next_hop: 192.168.42.64 + admin_distance: 1 + state: present + provider: "{{ cli }}" + register: result + +- assert: + that: + - 'result.changed == true' + - '"set protocols static route 172.24.0.0/24 next-hop 192.168.42.64 distance 1" in result.commands' + +- name: modify admin distance of static route again (idempotent) + vyos_static_route: + prefix: 172.24.0.0 + mask: 24 + next_hop: 192.168.42.64 + admin_distance: 1 + state: present + provider: "{{ cli }}" + register: result + +- assert: + that: + - 'result.changed == false' + +- name: delete static route + vyos_static_route: + prefix: 172.24.0.0/24 + next_hop: 192.168.42.64 + admin_distance: 1 + state: absent + provider: "{{ cli }}" + register: result + +- assert: + that: + - 'result.changed == true' + - '"delete protocols static route 172.24.0.0/24" in result.commands' + +- name: delete static route again (idempotent) + vyos_static_route: + prefix: 172.24.0.0/24 + next_hop: 192.168.42.64 + admin_distance: 1 + state: absent + provider: "{{ cli }}" + register: result + +- assert: + that: + - 'result.changed == false' + +- name: Add static route collections + vyos_static_route: + collection: + - { prefix: 172.24.1.0/24, next_hop: 192.168.42.64 } + - { prefix: 172.24.2.0, mask: 24, next_hop: 192.168.42.64 } + state: present + provider: "{{ cli }}" + register: result + +- assert: + that: + - 'result.changed == true' + - '"set protocols static route 172.24.1.0/24 next-hop 192.168.42.64" in result.commands' + - '"set protocols static route 172.24.2.0/24 next-hop 192.168.42.64" in result.commands' + +- name: Add and remove static route collections with overrides + vyos_static_route: + collection: + - { prefix: 172.24.1.0/24, next_hop: 192.168.42.64 } + - { prefix: 172.24.2.0/24, next_hop: 192.168.42.64, state: absent } + - { prefix: 172.24.3.0/24, next_hop: 192.168.42.64 } + state: present + provider: "{{ cli }}" + register: result + +- assert: + that: + - 'result.changed == true' + - '"delete protocols static route 172.24.2.0/24" in result.commands' + - '"set protocols static route 172.24.3.0/24 next-hop 192.168.42.64" in result.commands' + +- name: Remove static route collections + vyos_static_route: + collection: + - { prefix: 172.24.1.0/24, next_hop: 192.168.42.64 } + - { prefix: 172.24.3.0/24, next_hop: 192.168.42.64 } + state: absent + provider: "{{ cli }}" + register: result + +- assert: + that: + - 'result.changed == true' + - '"delete protocols static route 172.24.1.0/24" in result.commands' + - '"delete protocols static route 172.24.3.0/24" in result.commands' diff --git a/test/integration/vyos.yaml b/test/integration/vyos.yaml index 8315d19871b..1eda0fec951 100644 --- a/test/integration/vyos.yaml +++ b/test/integration/vyos.yaml @@ -49,6 +49,13 @@ rescue: - set_fact: test_failed=true + - block: + - include_role: + name: vyos_static_route + when: "limit_to in ['*', 'vyos_static_route']" + rescue: + - set_fact: test_failed=true + ########### - name: Has any previous test failed? diff --git a/test/units/modules/network/vyos/test_vyos_static_route.py b/test/units/modules/network/vyos/test_vyos_static_route.py new file mode 100644 index 00000000000..2162818ce22 --- /dev/null +++ b/test/units/modules/network/vyos/test_vyos_static_route.py @@ -0,0 +1,49 @@ +# (c) 2016 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 . + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from ansible.compat.tests.mock import patch +from ansible.modules.network.vyos import vyos_static_route +from .vyos_module import TestVyosModule, load_fixture, set_module_args + + +class TestVyosStaticRouteModule(TestVyosModule): + + module = vyos_static_route + + def setUp(self): + self.mock_get_config = patch('ansible.modules.network.vyos.vyos_static_route.get_config') + self.get_config = self.mock_get_config.start() + + self.mock_load_config = patch('ansible.modules.network.vyos.vyos_static_route.load_config') + self.load_config = self.mock_load_config.start() + + def tearDown(self): + self.mock_get_config.stop() + self.mock_load_config.stop() + + def load_fixtures(self, commands=None, transport='cli'): + self.load_config.return_value = dict(diff=None, session='session') + + def test_vyos_static_route_present(self): + set_module_args(dict(prefix='172.26.0.0/16', next_hop='172.26.4.1', admin_distance='1')) + result = self.execute_module(changed=True) + self.assertEqual(result['commands'], + ['set protocols static route 172.26.0.0/16 next-hop 172.26.4.1 distance 1'])