Skip to content

Commit

Permalink
Add ios_static_route module (ansible#25527)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
rcarrillocruz authored Jun 29, 2017
1 parent 5242ff1 commit c8ba8bd
Show file tree
Hide file tree
Showing 14 changed files with 545 additions and 0 deletions.
221 changes: 221 additions & 0 deletions lib/ansible/modules/network/ios/ios_static_route.py
Original file line number Diff line number Diff line change
@@ -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()
1 change: 1 addition & 0 deletions test/integration/ios.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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']" }
1 change: 1 addition & 0 deletions test/integration/platform_agnostic.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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']" }
2 changes: 2 additions & 0 deletions test/integration/targets/ios_static_route/defaults/main.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
testcase: "*"
2 changes: 2 additions & 0 deletions test/integration/targets/ios_static_route/meta/main.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
dependencies:
- prepare_ios_tests
15 changes: 15 additions & 0 deletions test/integration/targets/ios_static_route/tasks/cli.yaml
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions test/integration/targets/ios_static_route/tasks/main.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
- { include: cli.yaml, tags: ['cli'] }
138 changes: 138 additions & 0 deletions test/integration/targets/ios_static_route/tests/cli/basic.yaml
Original file line number Diff line number Diff line change
@@ -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"]'
Empty file.
2 changes: 2 additions & 0 deletions test/integration/targets/net_static_route/defaults/main.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
testcase: "*"
Loading

0 comments on commit c8ba8bd

Please sign in to comment.