eos_static_route DI module (#32587)

* eos_static_route DI module

Signed-off-by: Trishna Guha <trishnaguha17@gmail.com>

* Integration test

Signed-off-by: Trishna Guha <trishnaguha17@gmail.com>

* Add net_static_route test

Signed-off-by: Trishna Guha <trishnaguha17@gmail.com>

* Validate ip address

Signed-off-by: Trishna Guha <trishnaguha17@gmail.com>
This commit is contained in:
Trishna Guha 2017-11-07 11:13:03 +00:00 committed by GitHub
parent 1857d11034
commit 48ab1a1334
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 487 additions and 0 deletions

View file

@ -28,6 +28,7 @@
import re import re
import ast import ast
import operator import operator
import socket
from itertools import chain from itertools import chain
@ -337,6 +338,20 @@ def remove_default_spec(spec):
del spec[item]['default'] del spec[item]['default']
def validate_ip_address(address):
try:
socket.inet_aton(address)
except socket.error:
return False
return address.count('.') == 3
def validate_prefix(prefix):
if prefix and not 0 <= int(prefix) <= 32:
return False
return True
def load_provider(spec, args): def load_provider(spec, args):
provider = args.get('provider', {}) provider = args.get('provider', {})
for key, value in iteritems(spec): for key, value in iteritems(spec):

View file

@ -0,0 +1,222 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# (c) 2017, Ansible by Red Hat, inc
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'network'}
DOCUMENTATION = """
---
module: eos_static_route
version_added: "2.5"
author: "Trishna Guha (@trishnaguha)"
short_description: Manage static IP routes on Arist EOS network devices
description:
- This module provides declarative management of static
IP routes on Arista EOS network devices.
notes:
- Tested against EOS 4.15
options:
address:
description:
- Network address with prefix of the static route.
required: true
aliases: ['prefix']
next_hop:
description:
- Next hop IP of the static route.
required: true
admin_distance:
description:
- Admin distance of the static route.
default: 1
aggregate:
description: List of static route definitions
state:
description:
- State of the static route configuration.
default: present
choices: ['present', 'absent']
"""
EXAMPLES = """
- name: configure static route
eos_static_route:
address: 10.0.2.0/24
next_hop: 10.8.38.1
admin_distance: 2
- name: delete static route
eos_static_route:
address: 10.0.2.0/24
next_hop: 10.8.38.1
state: absent
- name: configure static routes using aggregate
eos_static_route:
aggregate:
- { address: 10.0.1.0/24, next_hop: 10.8.38.1 }
- { address: 10.0.3.0/24, next_hop: 10.8.38.1 }
- name: Delete static route using aggregate
eos_static_route:
aggregate:
- { address: 10.0.1.0/24, next_hop: 10.8.38.1 }
- { address: 10.0.3.0/24, next_hop: 10.8.38.1 }
state: absent
"""
RETURN = """
commands:
description: The list of configuration mode commands to send to the device
returned: always
type: list
sample:
- ip route 10.0.2.0/24 10.8.38.1 3
- no ip route 10.0.2.0/24 10.8.38.1
"""
from copy import deepcopy
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.network_common import remove_default_spec
from ansible.module_utils.network_common import validate_ip_address, validate_prefix
from ansible.module_utils.eos import load_config, run_commands
from ansible.module_utils.eos import eos_argument_spec, check_args
def map_obj_to_commands(updates, module):
commands = list()
want, have = updates
for w in want:
address = w['address']
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' % (address, next_hop))
elif state == 'present' and w not in have:
commands.append('ip route %s %s %d' % (address, next_hop, admin_distance))
return commands
def map_params_to_obj(module, required_together=None):
obj = []
aggregate = module.params.get('aggregate')
if aggregate:
for item in aggregate:
for key in item:
if item.get(key) is None:
item[key] = module.params[key]
module._check_required_together(required_together, item)
d = item.copy()
obj.append(d)
else:
obj.append({
'address': module.params['address'].strip(),
'next_hop': module.params['next_hop'].strip(),
'admin_distance': module.params['admin_distance'],
'state': module.params['state']
})
return obj
def map_config_to_obj(module):
objs = []
try:
out = run_commands(module, ['show ip route | json'])[0]
except IndexError:
out = {}
if out:
try:
vrfs = out['vrfs']['default']['routes']
except (AttributeError, KeyError, TypeError):
vrfs = {}
if vrfs:
for address in vrfs:
obj = {}
obj['address'] = address
obj['admin_distance'] = vrfs[address].get('preference')
obj['next_hop'] = vrfs[address].get('vias')[0].get('nexthopAddr')
objs.append(obj)
return objs
def main():
""" main entry point for module execution
"""
element_spec = dict(
address=dict(type='str', aliases=['prefix']),
next_hop=dict(type='str'),
admin_distance=dict(default=1, type='int'),
state=dict(default='present', choices=['present', 'absent'])
)
aggregate_spec = deepcopy(element_spec)
aggregate_spec['address'] = dict(required=True)
# remove default in aggregate spec, to handle common arguments
remove_default_spec(aggregate_spec)
argument_spec = dict(
aggregate=dict(type='list', elements='dict', options=aggregate_spec),
)
argument_spec.update(element_spec)
argument_spec.update(eos_argument_spec)
required_one_of = [['aggregate', 'address']]
required_together = [['address', 'next_hop']]
mutually_exclusive = [['aggregate', 'address']]
module = AnsibleModule(argument_spec=argument_spec,
required_one_of=required_one_of,
required_together=required_together,
mutually_exclusive=mutually_exclusive,
supports_check_mode=True)
address = module.params['address']
prefix = address.split('/')[-1]
warnings = list()
check_args(module, warnings)
if '/' not in address or not validate_ip_address(address.split('/')[0]):
module.fail_json(msg='{} is not a valid IP address'.format(address))
if not validate_prefix(prefix):
module.fail_json(msg='Length of prefix should be between 0 and 32 bits')
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:
commit = not module.check_mode
response = load_config(module, commands, commit=commit)
if response.get('diff') and module._diff:
result['diff'] = {'prepared': response.get('diff')}
result['session_name'] = response.get('session')
result['changed'] = True
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -103,6 +103,15 @@
failed_modules: "{{ failed_modules }} + [ 'eos_logging' ]" failed_modules: "{{ failed_modules }} + [ 'eos_logging' ]"
test_failed: true test_failed: true
- block:
- include_role:
name: eos_static_route
when: "limit_to in ['*', 'eos_static_route']"
rescue:
- set_fact:
failed_modules: "{{ failed_modules }} + [ 'eos_static_route' ]"
test_failed: true
########### ###########

View file

@ -0,0 +1,3 @@
---
testcase: "*"
test_items: []

View file

@ -0,0 +1,2 @@
dependencies:
- prepare_eos_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,114 @@
---
- debug: msg="START cli/basic.yaml"
- name: setup - remove config used in test
eos_config:
lines:
- no ip route 192.168.3.0/24 192.168.0.1
- no ip route 192.168.4.0/24 192.168.0.1
- no ip route 192.168.5.0/24 192.168.0.1
authorize: yes
provider: "{{ cli }}"
- name: configure static route
eos_static_route: &single_route
address: 192.168.3.0/24
next_hop: 192.168.0.1
admin_distance: 2
authorize: yes
provider: "{{ cli }}"
register: result
- assert:
that:
- "result.changed == true"
- "'ip route 192.168.3.0/24 192.168.0.1 2' in result.commands"
- name: configure static route(Idempotence)
eos_static_route: *single_route
register: result
- assert:
that:
- "result.changed == false"
- name: delete static route
eos_static_route: &delete_single
address: 192.168.3.0/24
next_hop: 192.168.0.1
admin_distance: 2
authorize: yes
provider: "{{ cli }}"
state: absent
regiser: result
- assert:
that:
- "result.changed == true"
- "'no ip route 192.168.3.0/24 192.168.0.1' in result.commands"
- name: delete static route
eos_static_route: *delete_single
register: result
- assert:
that:
- "result.changed == false"
- name: configure static routes using aggregate
eos_static_route: &configure_aggregate
aggregate:
- { address: 192.168.4.0/24, next_hop: 192.168.0.1 }
- { address: 192.168.5.0/24, next_hop: 192.168.0.1 }
authorize: yes
provider: "{{ cli }}"
register: result
- assert:
that:
- "result.changed == true"
- "'ip route 192.168.4.0/24 192.168.0.1 1' in result.commands"
- "'ip route 192.168.5.0/24 192.168.0.1 1' in result.commands"
- name: configure static routes using aggregate(Idemporence)
eos_static_route: *configure_aggregate
register: result
- assert:
that:
- "result.changed == false"
- name: delete static routes using aggregate
eos_static_route: &delete_aggregate
aggregate:
- { address: 192.168.4.0/24, next_hop: 192.168.0.1 }
- { address: 192.168.5.0/24, next_hop: 192.168.0.1 }
authorize: yes
state: absent
provider: "{{ cli }}"
register: result
- assert:
that:
- "result.changed == true"
- "'no ip route 192.168.4.0/24 192.168.0.1' in result.commands"
- "'no ip route 192.168.5.0/24 192.168.0.1' in result.commands"
- name: delete static routes using aggregate(Idempotence)
eos_static_route: *delete_aggregate
register: result
- assert:
that:
- "result.changed == false"
- name: teardown
eos_config:
lines:
- no ip route 192.168.3.0/24 192.168.0.1
- no ip route 192.168.4.0/24 192.168.0.1
- no ip route 192.168.5.0/24 192.168.0.1
authorize: yes
provider: "{{ cli }}"
- debug: msg="END cli/basic.yaml"

View file

@ -7,3 +7,6 @@
- include: "{{ role_path }}/tests/vyos/basic.yaml" - include: "{{ role_path }}/tests/vyos/basic.yaml"
when: hostvars[inventory_hostname]['ansible_network_os'] == 'vyos' when: hostvars[inventory_hostname]['ansible_network_os'] == 'vyos'
- include: "{{ role_path }}/tests/eos/basic.yaml"
when: hostvars[inventory_hostname]['ansible_network_os'] == 'eos'

View file

@ -0,0 +1,102 @@
---
- name: setup - remove config used in test
net_static_route:
aggregate:
- { address: 192.168.3.0/24, next_hop: 192.168.0.1 }
- { address: 192.168.4.0/24, next_hop: 192.168.0.1 }
- { address: 192.168.5.0/24, next_hop: 192.168.0.1 }
authorize: yes
state: absent
provider: "{{ cli }}"
- name: configure static route
net_static_route: &single_route
address: 192.168.3.0/24
next_hop: 192.168.0.1
admin_distance: 2
authorize: yes
provider: "{{ cli }}"
register: result
- assert:
that:
- "result.changed == true"
- "'ip route 192.168.3.0/24 192.168.0.1 2' in result.commands"
- name: configure static route(Idempotence)
net_static_route: *single_route
register: result
- assert:
that:
- "result.changed == false"
- name: delete static route
net_static_route: &delete_single
address: 192.168.3.0/24
next_hop: 192.168.0.1
admin_distance: 2
authorize: yes
provider: "{{ cli }}"
state: absent
regiser: result
- assert:
that:
- "result.changed == true"
- "'no ip route 192.168.3.0/24 192.168.0.1' in result.commands"
- name: delete static route
net_static_route: *delete_single
register: result
- assert:
that:
- "result.changed == false"
- name: configure static routes using aggregate
net_static_route: &configure_aggregate
aggregate:
- { address: 192.168.4.0/24, next_hop: 192.168.0.1 }
- { address: 192.168.5.0/24, next_hop: 192.168.0.1 }
authorize: yes
provider: "{{ cli }}"
register: result
- assert:
that:
- "result.changed == true"
- "'ip route 192.168.4.0/24 192.168.0.1 1' in result.commands"
- "'ip route 192.168.5.0/24 192.168.0.1 1' in result.commands"
- name: configure static routes using aggregate(Idemporence)
net_static_route: *configure_aggregate
register: result
- assert:
that:
- "result.changed == false"
- name: delete static routes using aggregate
net_static_route: &delete_aggregate
aggregate:
- { address: 192.168.4.0/24, next_hop: 192.168.0.1 }
- { address: 192.168.5.0/24, next_hop: 192.168.0.1 }
authorize: yes
state: absent
provider: "{{ cli }}"
register: result
- assert:
that:
- "result.changed == true"
- "'no ip route 192.168.4.0/24 192.168.0.1' in result.commands"
- "'no ip route 192.168.5.0/24 192.168.0.1' in result.commands"
- name: delete static routes using aggregate(Idempotence)
net_static_route: *delete_aggregate
register: result
- assert:
that:
- "result.changed == false"