diff --git a/lib/ansible/modules/network/ios/ios_static_route.py b/lib/ansible/modules/network/ios/ios_static_route.py new file mode 100644 index 0000000000..37020e2ae5 --- /dev/null +++ b/lib/ansible/modules/network/ios/ios_static_route.py @@ -0,0 +1,221 @@ +#!/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: ios_static_route +version_added: "2.4" +author: "Ricardo Carrillo Cruz (@rcarrillocruz)" +short_description: Manage static IP routes on Cisco IOS network devices +description: + - This module provides declarative management of static + IP routes on Cisco IOS network devices. +options: + prefix: + description: + - Network prefix of the static route. + 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. + default: 1 + 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 + ios_static_route: + prefix: 192.168.2.0 + mask: 255.255.255.0 + next_hop: 10.0.0.1 + +- name: remove configuration + ios_static_route: + prefix: 192.168.2.0 + mask: 255.255.255.0 + next_hop: 10.0.0.1 + state: absent + +- name: configure collections of static routes + ios_static_route: + collection: + - { prefix: 192.168.2.0, mask 255.255.255.0, next_hop: 10.0.0.1 } + - { prefix: 192.168.3.0, mask 255.255.255.0, 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: + - ip route 192.168.2.0 255.255.255.0 10.0.0.1 +""" + +from ansible.module_utils._text import to_text +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.connection import exec_command +from ansible.module_utils.ios import load_config, run_commands +from ansible.module_utils.ios import ios_argument_spec, check_args +from ipaddress import ip_network +import re + + +def map_obj_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('no ip route %s %s %s' % (prefix, mask, next_hop)) + elif state == 'present' and w not in have: + commands.append('ip route %s %s %s %s' % (prefix, mask, next_hop, + admin_distance)) + + return commands + + +def map_config_to_obj(module): + obj = [] + + rc, out, err = exec_command(module, 'show ip static route') + match = re.search(r'.*Static local RIB for default\s*(.*)$', out, re.DOTALL) + + if match and match.group(1): + for r in match.group(1).splitlines(): + splitted_line = r.split() + + cidr = ip_network(to_text(splitted_line[1])) + prefix = str(cidr.network_address) + mask = str(cidr.netmask) + next_hop = splitted_line[4] + admin_distance = splitted_line[2][1] + + obj.append({'prefix': prefix, 'mask': mask, + 'next_hop': next_hop, + 'admin_distance': admin_distance}) + + 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 '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() + 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(default=1, type='int'), + collection=dict(type='list'), + purge=dict(type='bool'), + state=dict(default='present', choices=['present', 'absent']) + ) + + argument_spec.update(ios_argument_spec) + required_one_of = [['collection', 'prefix']] + required_together = [['prefix', 'mask', '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 = map_config_to_obj(module) + + commands = map_obj_to_commands((want, have), module) + result['commands'] = commands + + if commands: + if not module.check_mode: + load_config(module, commands) + + result['changed'] = True + + module.exit_json(**result) + +if __name__ == '__main__': + main() diff --git a/test/integration/ios.yaml b/test/integration/ios.yaml index d331922df1..720a4fe136 100644 --- a/test/integration/ios.yaml +++ b/test/integration/ios.yaml @@ -15,3 +15,4 @@ - { role: ios_template, when: "limit_to in ['*', 'ios_template']" } - { role: ios_system, when: "limit_to in ['*', 'ios_system']" } - { role: ios_user, when: "limit_to in ['*', 'ios_user']" } + - { role: ios_static_route, when: "limit_to in ['*', 'ios_static_route']" } diff --git a/test/integration/platform_agnostic.yaml b/test/integration/platform_agnostic.yaml index e993fea92c..98c2d7edce 100644 --- a/test/integration/platform_agnostic.yaml +++ b/test/integration/platform_agnostic.yaml @@ -15,3 +15,4 @@ - { role: net_vlan, when: "limit_to in ['*', 'net_vlan']" } - { role: net_vrf, when: "limit_to in ['*', 'net_vrf']" } - { role: net_interface, when: "limit_to in ['*', 'net_interface']" } + - { role: net_static_route, when: "limit_to in ['*', 'net_static_route']" } diff --git a/test/integration/targets/ios_static_route/defaults/main.yaml b/test/integration/targets/ios_static_route/defaults/main.yaml new file mode 100644 index 0000000000..5f709c5aac --- /dev/null +++ b/test/integration/targets/ios_static_route/defaults/main.yaml @@ -0,0 +1,2 @@ +--- +testcase: "*" diff --git a/test/integration/targets/ios_static_route/meta/main.yaml b/test/integration/targets/ios_static_route/meta/main.yaml new file mode 100644 index 0000000000..159cea8d38 --- /dev/null +++ b/test/integration/targets/ios_static_route/meta/main.yaml @@ -0,0 +1,2 @@ +dependencies: + - prepare_ios_tests diff --git a/test/integration/targets/ios_static_route/tasks/cli.yaml b/test/integration/targets/ios_static_route/tasks/cli.yaml new file mode 100644 index 0000000000..d675462dd0 --- /dev/null +++ b/test/integration/targets/ios_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/ios_static_route/tasks/main.yaml b/test/integration/targets/ios_static_route/tasks/main.yaml new file mode 100644 index 0000000000..415c99d8b1 --- /dev/null +++ b/test/integration/targets/ios_static_route/tasks/main.yaml @@ -0,0 +1,2 @@ +--- +- { include: cli.yaml, tags: ['cli'] } diff --git a/test/integration/targets/ios_static_route/tests/cli/basic.yaml b/test/integration/targets/ios_static_route/tests/cli/basic.yaml new file mode 100644 index 0000000000..030ce02e48 --- /dev/null +++ b/test/integration/targets/ios_static_route/tests/cli/basic.yaml @@ -0,0 +1,138 @@ +--- +- name: create static route + ios_static_route: + prefix: 172.16.31.0 + mask: 255.255.255.0 + next_hop: 10.0.0.8 + state: present + authorize: yes + provider: "{{ cli }}" + register: result + +- assert: + that: + - 'result.changed == true' + - 'result.commands == ["ip route 172.16.31.0 255.255.255.0 10.0.0.8 1"]' + +- name: create static route again (idempotent) + ios_static_route: + prefix: 172.16.31.0 + mask: 255.255.255.0 + next_hop: 10.0.0.8 + state: present + authorize: yes + provider: "{{ cli }}" + register: result + +- assert: + that: + - 'result.changed == false' + +- name: modify admin distance of static route + ios_static_route: + prefix: 172.16.31.0 + mask: 255.255.255.0 + next_hop: 10.0.0.8 + admin_distance: 2 + state: present + authorize: yes + provider: "{{ cli }}" + register: result + +- assert: + that: + - 'result.changed == true' + - 'result.commands == ["ip route 172.16.31.0 255.255.255.0 10.0.0.8 2"]' + +- name: modify admin distance of static route again (idempotent) + ios_static_route: + prefix: 172.16.31.0 + mask: 255.255.255.0 + next_hop: 10.0.0.8 + admin_distance: 2 + state: present + authorize: yes + provider: "{{ cli }}" + register: result + +- assert: + that: + - 'result.changed == false' + +- name: delete static route + ios_static_route: + prefix: 172.16.31.0 + mask: 255.255.255.0 + next_hop: 10.0.0.8 + admin_distance: 2 + state: absent + authorize: yes + provider: "{{ cli }}" + register: result + +- assert: + that: + - 'result.changed == true' + - 'result.commands == ["no ip route 172.16.31.0 255.255.255.0 10.0.0.8"]' + +- name: delete static route again (idempotent) + ios_static_route: + prefix: 172.16.31.0 + mask: 255.255.255.0 + next_hop: 10.0.0.8 + admin_distance: 2 + state: absent + authorize: yes + provider: "{{ cli }}" + register: result + +- assert: + that: + - 'result.changed == false' + +- name: Add static route collections + ios_static_route: + collection: + - { prefix: 172.16.32.0, mask: 255.255.255.0, next_hop: 10.0.0.8 } + - { prefix: 172.16.33.0, mask: 255.255.255.0, next_hop: 10.0.0.8 } + state: present + authorize: yes + provider: "{{ cli }}" + register: result + +- assert: + that: + - 'result.changed == true' + - 'result.commands == ["ip route 172.16.32.0 255.255.255.0 10.0.0.8 1", "ip route 172.16.33.0 255.255.255.0 10.0.0.8 1"]' + +- name: Add and remove static route collections with overrides + ios_static_route: + collection: + - { prefix: 172.16.32.0, mask: 255.255.255.0, next_hop: 10.0.0.8 } + - { prefix: 172.16.33.0, mask: 255.255.255.0, next_hop: 10.0.0.8, state: absent } + - { prefix: 172.16.34.0, mask: 255.255.255.0, next_hop: 10.0.0.8 } + state: present + authorize: yes + provider: "{{ cli }}" + register: result + +- assert: + that: + - 'result.changed == true' + - 'result.commands == ["no ip route 172.16.33.0 255.255.255.0 10.0.0.8", "ip route 172.16.34.0 255.255.255.0 10.0.0.8 1"]' + +- name: Remove static route collections + ios_static_route: + collection: + - { prefix: 172.16.32.0, mask: 255.255.255.0, next_hop: 10.0.0.8 } + - { prefix: 172.16.33.0, mask: 255.255.255.0, next_hop: 10.0.0.8 } + - { prefix: 172.16.34.0, mask: 255.255.255.0, next_hop: 10.0.0.8 } + state: absent + authorize: yes + provider: "{{ cli }}" + register: result + +- assert: + that: + - 'result.changed == true' + - 'result.commands == ["no ip route 172.16.32.0 255.255.255.0 10.0.0.8", "no ip route 172.16.34.0 255.255.255.0 10.0.0.8"]' diff --git a/test/integration/targets/net_static_route/aliases b/test/integration/targets/net_static_route/aliases new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/integration/targets/net_static_route/defaults/main.yaml b/test/integration/targets/net_static_route/defaults/main.yaml new file mode 100644 index 0000000000..5f709c5aac --- /dev/null +++ b/test/integration/targets/net_static_route/defaults/main.yaml @@ -0,0 +1,2 @@ +--- +testcase: "*" diff --git a/test/integration/targets/net_static_route/tasks/cli.yaml b/test/integration/targets/net_static_route/tasks/cli.yaml new file mode 100644 index 0000000000..46d86dd698 --- /dev/null +++ b/test/integration/targets/net_static_route/tasks/cli.yaml @@ -0,0 +1,16 @@ +--- +- 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 case + include: "{{ test_case_to_run }}" + with_items: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run diff --git a/test/integration/targets/net_static_route/tasks/main.yaml b/test/integration/targets/net_static_route/tasks/main.yaml new file mode 100644 index 0000000000..415c99d8b1 --- /dev/null +++ b/test/integration/targets/net_static_route/tasks/main.yaml @@ -0,0 +1,2 @@ +--- +- { include: cli.yaml, tags: ['cli'] } diff --git a/test/integration/targets/net_static_route/tests/cli/basic.yaml b/test/integration/targets/net_static_route/tests/cli/basic.yaml new file mode 100644 index 0000000000..a76e75ee24 --- /dev/null +++ b/test/integration/targets/net_static_route/tests/cli/basic.yaml @@ -0,0 +1,3 @@ +--- +- include: "{{ role_path }}/tests/ios/basic.yaml" + when: hostvars[inventory_hostname]['ansible_network_os'] == 'ios' diff --git a/test/integration/targets/net_static_route/tests/ios/basic.yaml b/test/integration/targets/net_static_route/tests/ios/basic.yaml new file mode 100644 index 0000000000..0ab1260630 --- /dev/null +++ b/test/integration/targets/net_static_route/tests/ios/basic.yaml @@ -0,0 +1,140 @@ +--- +- debug: msg="START net_static_route ios/basic.yaml" + +- name: create static route + net_static_route: + prefix: 172.16.31.0 + mask: 255.255.255.0 + next_hop: 10.0.0.8 + state: present + authorize: yes + provider: "{{ cli }}" + register: result + +- assert: + that: + - 'result.changed == true' + - 'result.commands == ["ip route 172.16.31.0 255.255.255.0 10.0.0.8 1"]' + +- name: create static route again (idempotent) + net_static_route: + prefix: 172.16.31.0 + mask: 255.255.255.0 + next_hop: 10.0.0.8 + state: present + authorize: yes + provider: "{{ cli }}" + register: result + +- assert: + that: + - 'result.changed == false' + +- name: modify admin distance of static route + net_static_route: + prefix: 172.16.31.0 + mask: 255.255.255.0 + next_hop: 10.0.0.8 + admin_distance: 2 + state: present + authorize: yes + provider: "{{ cli }}" + register: result + +- assert: + that: + - 'result.changed == true' + - 'result.commands == ["ip route 172.16.31.0 255.255.255.0 10.0.0.8 2"]' + +- name: modify admin distance of static route again (idempotent) + net_static_route: + prefix: 172.16.31.0 + mask: 255.255.255.0 + next_hop: 10.0.0.8 + admin_distance: 2 + state: present + authorize: yes + provider: "{{ cli }}" + register: result + +- assert: + that: + - 'result.changed == false' + +- name: delete static route + net_static_route: + prefix: 172.16.31.0 + mask: 255.255.255.0 + next_hop: 10.0.0.8 + admin_distance: 2 + state: absent + authorize: yes + provider: "{{ cli }}" + register: result + +- assert: + that: + - 'result.changed == true' + - 'result.commands == ["no ip route 172.16.31.0 255.255.255.0 10.0.0.8"]' + +- name: delete static route again (idempotent) + net_static_route: + prefix: 172.16.31.0 + mask: 255.255.255.0 + next_hop: 10.0.0.8 + admin_distance: 2 + state: absent + authorize: yes + provider: "{{ cli }}" + register: result + +- assert: + that: + - 'result.changed == false' + +- name: Add static route collections + net_static_route: + collection: + - { prefix: 172.16.32.0, mask: 255.255.255.0, next_hop: 10.0.0.8 } + - { prefix: 172.16.33.0, mask: 255.255.255.0, next_hop: 10.0.0.8 } + state: present + authorize: yes + provider: "{{ cli }}" + register: result + +- assert: + that: + - 'result.changed == true' + - 'result.commands == ["ip route 172.16.32.0 255.255.255.0 10.0.0.8 1", "ip route 172.16.33.0 255.255.255.0 10.0.0.8 1"]' + +- name: Add and remove static route collections with overrides + net_static_route: + collection: + - { prefix: 172.16.32.0, mask: 255.255.255.0, next_hop: 10.0.0.8 } + - { prefix: 172.16.33.0, mask: 255.255.255.0, next_hop: 10.0.0.8, state: absent } + - { prefix: 172.16.34.0, mask: 255.255.255.0, next_hop: 10.0.0.8 } + state: present + authorize: yes + provider: "{{ cli }}" + register: result + +- assert: + that: + - 'result.changed == true' + - 'result.commands == ["no ip route 172.16.33.0 255.255.255.0 10.0.0.8", "ip route 172.16.34.0 255.255.255.0 10.0.0.8 1"]' + +- name: Remove static route collections + net_static_route: + collection: + - { prefix: 172.16.32.0, mask: 255.255.255.0, next_hop: 10.0.0.8 } + - { prefix: 172.16.33.0, mask: 255.255.255.0, next_hop: 10.0.0.8 } + - { prefix: 172.16.34.0, mask: 255.255.255.0, next_hop: 10.0.0.8 } + state: absent + authorize: yes + provider: "{{ cli }}" + register: result + +- assert: + that: + - 'result.changed == true' + - 'result.commands == ["no ip route 172.16.32.0 255.255.255.0 10.0.0.8", "no ip route 172.16.34.0 255.255.255.0 10.0.0.8"]'