Add ios_static_route module (#25527)

* Add ios_static_route module

* Add ios_static_route integration tests

* Add platform agnostic integration tests for IOS

* Replace unicode function to ansible module_utils to_text

* Add collections handling logic

* Add integration tests for collections

* Make collections and prefix mutually exclusive

* Add net_static_route integration tests for collections

* Do not store load_config return value, as it returns nothing
This commit is contained in:
Ricardo Carrillo Cruz 2017-06-29 12:05:20 +02:00 committed by GitHub
parent 5242ff1b59
commit c8ba8bdd6d
14 changed files with 545 additions and 0 deletions

View file

@ -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 <http://www.gnu.org/licenses/>.
#
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()

View file

@ -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']" }

View file

@ -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']" }

View file

@ -0,0 +1,2 @@
---
testcase: "*"

View file

@ -0,0 +1,2 @@
dependencies:
- prepare_ios_tests

View file

@ -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

View file

@ -0,0 +1,2 @@
---
- { include: cli.yaml, tags: ['cli'] }

View file

@ -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"]'

View file

@ -0,0 +1,2 @@
---
testcase: "*"

View file

@ -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

View file

@ -0,0 +1,2 @@
---
- { include: cli.yaml, tags: ['cli'] }

View file

@ -0,0 +1,3 @@
---
- include: "{{ role_path }}/tests/ios/basic.yaml"
when: hostvars[inventory_hostname]['ansible_network_os'] == 'ios'

View file

@ -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"]'