From 57660abf3376458fbd43cb8749158031d981418d Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Fri, 27 Jan 2017 08:44:57 -0500 Subject: [PATCH] refactor eos_eapi module (#20740) * eos_eapi module now requires network_cli plugin * adds unit test cases for eos_eapi module --- lib/ansible/modules/network/eos/eos_eapi.py | 307 +++++++++--------- test/units/modules/network/eos/__init__.py | 0 .../eos/fixtures/eos_eapi_show_mgmt.json | 47 +++ .../eos_eapi_show_mgmt_unconfigured.json | 42 +++ .../eos/fixtures/eos_eapi_show_vrf.text | 11 + .../modules/network/eos/test_eos_eapi.py | 188 +++++++++++ 6 files changed, 437 insertions(+), 158 deletions(-) create mode 100644 test/units/modules/network/eos/__init__.py create mode 100644 test/units/modules/network/eos/fixtures/eos_eapi_show_mgmt.json create mode 100644 test/units/modules/network/eos/fixtures/eos_eapi_show_mgmt_unconfigured.json create mode 100644 test/units/modules/network/eos/fixtures/eos_eapi_show_vrf.text create mode 100644 test/units/modules/network/eos/test_eos_eapi.py diff --git a/lib/ansible/modules/network/eos/eos_eapi.py b/lib/ansible/modules/network/eos/eos_eapi.py index ea51cdeed6..9a7a4197ae 100644 --- a/lib/ansible/modules/network/eos/eos_eapi.py +++ b/lib/ansible/modules/network/eos/eos_eapi.py @@ -16,10 +16,11 @@ # along with Ansible. If not, see . # - -ANSIBLE_METADATA = {'status': ['preview'], - 'supported_by': 'core', - 'version': '1.0'} +ANSIBLE_METADATA = { + 'status': ['preview'], + 'supported_by': 'core', + 'version': '1.0' +} DOCUMENTATION = """ --- @@ -37,7 +38,6 @@ description: Unix socket server. Use the options listed below to override the default configuration. - Requires EOS v4.12 or greater. -extends_documentation_fragment: eos options: http: description: @@ -141,18 +141,9 @@ options: """ EXAMPLES = """ -# Note: examples below use the following provider dict to handle -# transport and authentication to the node. -vars: - cli: - host: "{{ inventory_hostname }}" - username: admin - password: admin - - name: Enable eAPI access with default configuration eos_eapi: state: started - provider: "{{ cli }}" - name: Enable eAPI with no HTTP, HTTPS at port 9443, local HTTP at port 80, and socket enabled eos_eapi: @@ -162,153 +153,161 @@ vars: local_http: yes local_http_port: 80 socket: yes - provider: "{{ cli }}" - name: Shutdown eAPI access eos_eapi: state: stopped - provider: "{{ cli }}" """ RETURN = """ -updates: - description: - - Set of commands to be executed on remote device +commands: + description: The list of configuration mode commands to send to the device returned: always type: list - sample: ['management api http-commands', 'shutdown'] + sample: + - management api http-commands + - protocol http port 81 + - no protocol https urls: description: Hash of URL endpoints eAPI is listening on per interface returned: when eAPI is started type: dict sample: {'Management1': ['http://172.26.10.1:80']} +session_name: + description: The EOS config session name used to load the configuration + returned: when changed is True + type: str + sample: ansible_1479315771 +start: + description: The time the job started + returned: always + type: str + sample: "2016-11-16 10:38:15.126146" +end: + description: The time the job ended + returned: always + type: str + sample: "2016-11-16 10:38:25.595612" +delta: + description: The time elapsed to perform all operations + returned: always + type: str + sample: "0:00:10.469466" """ import re -import time -import ansible.module_utils.eos +from ansible.module_utils.local import LocalAnsibleModule +from ansible.module_utils.eos import run_commands, load_config +from ansible.module_utils.six import iteritems -from ansible.module_utils.basic import get_exception -from ansible.module_utils.network import NetworkModule, NetworkError -from ansible.module_utils.netcfg import NetworkConfig, dumps - -PRIVATE_KEYS_RE = re.compile('__.+__') - - -def invoke(name, *args, **kwargs): - func = globals().get(name) - if func: - return func(*args, **kwargs) - -def get_instance(module): - try: - resp = module.cli('show management api http-commands', 'json') - return dict( - http=resp[0]['httpServer']['configured'], - http_port=resp[0]['httpServer']['port'], - https=resp[0]['httpsServer']['configured'], - https_port=resp[0]['httpsServer']['port'], - local_http=resp[0]['localHttpServer']['configured'], - local_http_port=resp[0]['localHttpServer']['port'], - socket=resp[0]['unixSocketServer']['configured'], - vrf=resp[0]['vrf'] - ) - except NetworkError: - exc = get_exception() - module.fail_json(msg=str(exc), **exc.kwargs) - -def started(module, instance, commands): - commands.append('no shutdown') - setters = set() - for key, value in module.argument_spec.items(): - if module.params[key] is not None: - setter = value.get('setter') or 'set_%s' % key - if setter not in setters: - setters.add(setter) - invoke(setter, module, instance, commands) - -def stopped(module, instance, commands): - commands.append('shutdown') - -def set_protocol_http(module, instance, commands): - port = module.params['http_port'] - if not 1 <= port <= 65535: +def validate_http_port(value, module): + if not 1 <= value <= 65535: module.fail_json(msg='http_port must be between 1 and 65535') - elif any((module.params['http'], instance['http'])): - commands.append('protocol http port %s' % port) - elif module.params['http'] is False: - commands.append('no protocol http') -def set_protocol_https(module, instance, commands): - port = module.params['https_port'] - if not 1 <= port <= 65535: - module.fail_json(msg='https_port must be between 1 and 65535') - elif any((module.params['https'], instance['https'])): - commands.append('protocol https port %s' % port) - elif module.params['https'] is False: - commands.append('no protocol https') +def validate_https_port(value, module): + if not 1 <= value <= 65535: + module.fail_json(msg='http_port must be between 1 and 65535') -def set_local_http(module, instance, commands): - port = module.params['local_http_port'] - if not 1 <= port <= 65535: - module.fail_json(msg='local_http_port must be between 1 and 65535') - elif any((module.params['local_http'], instance['local_http'])): - commands.append('protocol http localhost port %s' % port) - elif module.params['local_http'] is False: - commands.append('no protocol http localhost port 8080') +def validate_local_http_port(value, module): + if not 1 <= value <= 65535: + module.fail_json(msg='http_port must be between 1 and 65535') -def set_socket(module, instance, commands): - if any((module.params['socket'], instance['socket'])): - commands.append('protocol unix-socket') - elif module.params['socket'] is False: - commands.append('no protocol unix-socket') +def validate_vrf(value, module): + rc, out, err = run_commands(module, ['show vrf']) + configured_vrfs = re.findall('^\s+(\w+)(?=\s)', out[0],re.M) + configured_vrfs.append('default') + if value not in configured_vrfs: + module.fail_json(msg='vrf `%s` is not configured on the system' % value) -def set_vrf(module, instance, commands): - vrf = module.params['vrf'] - if vrf != 'default': - resp = module.cli(['show vrf']) - if vrf not in resp[0]: - module.fail_json(msg="vrf '%s' is not configured" % vrf) - commands.append('vrf %s' % vrf) +def map_obj_to_commands(updates, module): + commands = list() + want, have = updates -def get_config(module): - contents = module.params['config'] - if not contents: - cmd = 'show running-config all section management api http-commands' - contents = module.cli([cmd]) - config = NetworkConfig(indent=3, contents=contents[0]) - return config + needs_update = lambda x: want.get(x) is not None and (want.get(x) != have.get(x)) -def load_config(module, instance, commands, result): - commit = not module.check_mode - diff = module.config.load_config(commands, commit=commit) - if diff: - result['diff'] = dict(prepared=diff) - result['changed'] = True + def add(cmd): + if 'management api http-commands' not in commands: + commands.insert(0, 'management api http-commands') + commands.append(cmd) -def load(module, instance, commands, result): - candidate = NetworkConfig(indent=3) - candidate.add(commands, parents=['management api http-commands']) + if any((needs_update('http'), needs_update('http_port'))): + if want['http'] is False: + add('no protocol http') + else: + port = want['http_port'] or 80 + add('protocol http port %s' % port) - config = get_config(module) - configobjs = candidate.difference(config) + if any((needs_update('https'), needs_update('https_port'))): + if want['https'] is False: + add('no protocol https') + else: + port = want['https_port'] or 443 + add('protocol https port %s' % port) - if configobjs: - commands = dumps(configobjs, 'commands').split('\n') - result['updates'] = commands - load_config(module, instance, commands, result) + if any((needs_update('local_http'), needs_update('local_http_port'))): + if want['local_http'] is False: + add('no protocol http localhost') + else: + port = want['local_http_port'] or 8080 + add('protocol http localhost port %s' % port) -def clean_result(result): - # strip out any keys that have two leading and two trailing - # underscore characters - for key in result.keys(): - if PRIVATE_KEYS_RE.match(key): - del result[key] + if needs_update('vrf'): + add('vrf %s' % want['vrf']) + + if needs_update('state'): + if want['state'] == 'stopped': + add('shutdown') + elif want['state'] == 'started': + add('no shutdown') + + return commands + +def parse_state(data): + if data[0]['enabled']: + return 'started' + else: + return 'stopped' + + +def map_config_to_obj(module): + rc, out, err = run_commands(module, ['show management api http-commands | json']) + return { + 'http': out[0]['httpServer']['configured'], + 'http_port': out[0]['httpServer']['port'], + 'https': out[0]['httpsServer']['configured'], + 'https_port': out[0]['httpsServer']['port'], + 'local_http': out[0]['localHttpServer']['configured'], + 'local_http_port': out[0]['localHttpServer']['port'], + 'socket': out[0]['unixSocketServer']['configured'], + 'vrf': out[0]['vrf'], + 'state': parse_state(out) + } + +def map_params_to_obj(module): + obj = { + 'http': module.params['http'], + 'http_port': module.params['http_port'], + 'https': module.params['https'], + 'https_port': module.params['https_port'], + 'local_http': module.params['local_http'], + 'local_http_port': module.params['local_http_port'], + 'socket': module.params['socket'], + 'vrf': module.params['vrf'], + 'state': module.params['state'] + } + + for key, value in iteritems(obj): + if value: + validator = globals().get('validate_%s' % key) + if validator: + validator(value, module) + + return obj def collect_facts(module, result): - resp = module.cli(['show management api http-commands'], output='json') + rc, out, err = run_commands(module, ['show management api http-commands | json']) facts = dict(eos_eapi_urls=dict()) - for each in resp[0]['urls']: + for each in out[0]['urls']: intf, url = each.split(' : ') key = str(intf).strip() if key not in facts['eos_eapi_urls']: @@ -316,54 +315,46 @@ def collect_facts(module, result): facts['eos_eapi_urls'][key].append(str(url).strip()) result['ansible_facts'] = facts - def main(): """ main entry point for module execution """ - argument_spec = dict( - http=dict(aliases=['enable_http'], default=False, type='bool', setter='set_protocol_http'), - http_port=dict(default=80, type='int', setter='set_protocol_http'), + http=dict(aliases=['enable_http'], type='bool'), + http_port=dict(type='int'), - https=dict(aliases=['enable_https'], default=True, type='bool', setter='set_protocol_https'), - https_port=dict(default=443, type='int', setter='set_protocol_https'), + https=dict(aliases=['enable_https'], type='bool'), + https_port=dict(type='int'), - local_http=dict(aliases=['enable_local_http'], default=False, type='bool', setter='set_local_http'), - local_http_port=dict(default=8080, type='int', setter='set_local_http'), + local_http=dict(aliases=['enable_local_http'], type='bool'), + local_http_port=dict(type='int'), - socket=dict(aliases=['enable_socket'], default=False, type='bool'), + socket=dict(aliases=['enable_socket'], type='bool'), vrf=dict(default='default'), - config=dict(), - - # Only allow use of transport cli when configuring eAPI - transport=dict(default='cli', choices=['cli']), - state=dict(default='started', choices=['stopped', 'started']), ) - module = NetworkModule(argument_spec=argument_spec, - connect_on_load=False, - supports_check_mode=True) + module = LocalAnsibleModule(argument_spec=argument_spec, + supports_check_mode=True) - state = module.params['state'] + result = {'changed': False} - result = dict(changed=False) + want = map_params_to_obj(module) + have = map_config_to_obj(module) - commands = list() - instance = get_instance(module) + commands = map_obj_to_commands((want, have), module) + result['commands'] = commands - invoke(state, module, instance, commands) - - try: - load(module, instance, commands, result) - except NetworkError: - exc = get_exception() - module.fail_json(msg=str(exc), **exc.kwargs) + 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 collect_facts(module, result) - clean_result(result) module.exit_json(**result) diff --git a/test/units/modules/network/eos/__init__.py b/test/units/modules/network/eos/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/units/modules/network/eos/fixtures/eos_eapi_show_mgmt.json b/test/units/modules/network/eos/fixtures/eos_eapi_show_mgmt.json new file mode 100644 index 0000000000..444d47d30d --- /dev/null +++ b/test/units/modules/network/eos/fixtures/eos_eapi_show_mgmt.json @@ -0,0 +1,47 @@ +{ + "httpServer": { + "running": true, + "configured": true, + "port": 80 + }, + "users": { + "admin": { + "requestCount": 17153, + "bytesOut": 19950055, + "lastHitTime": 1484142997.1464522, + "bytesIn": 3189628 + } + }, + "localHttpServer": { + "running": true, + "configured": true, + "port": 8080 + }, + "executionTime": 6642.150056908686, + "dscpValue": 0, + "bytesOut": 19950055, + "enabled": true, + "httpsServer": { + "running": true, + "configured": true, + "port": 443 + }, + "corsOrigins": [], + "hitCount": 17318, + "vrf": "default", + "urls": [ + "Management1 : https://172.26.4.24:443", + "Management1 : http://172.26.4.24:80", + "Unix Socket : unix:/var/run/command-api.sock", + "Local : http://localhost:8080/command-api" + ], + "lastHitTime": 1484142997.146127, + "unixSocketServer": { + "running": true, + "configured": true + }, + "requestCount": 17153, + "commandCount": 59298, + "bytesIn": 3189628 +} + diff --git a/test/units/modules/network/eos/fixtures/eos_eapi_show_mgmt_unconfigured.json b/test/units/modules/network/eos/fixtures/eos_eapi_show_mgmt_unconfigured.json new file mode 100644 index 0000000000..8a17b19abc --- /dev/null +++ b/test/units/modules/network/eos/fixtures/eos_eapi_show_mgmt_unconfigured.json @@ -0,0 +1,42 @@ +{ + "httpServer": { + "running": false, + "configured": false, + "port": 80 + }, + "users": { + "admin": { + "requestCount": 17153, + "bytesOut": 19950055, + "lastHitTime": 1484142997.1464522, + "bytesIn": 3189628 + } + }, + "localHttpServer": { + "running": false, + "configured": false, + "port": 8080 + }, + "executionTime": 6642.150056908686, + "dscpValue": 0, + "bytesOut": 19950055, + "enabled": false, + "httpsServer": { + "running": false, + "configured": false, + "port": 443 + }, + "corsOrigins": [], + "hitCount": 17318, + "vrf": "default", + "urls": [], + "lastHitTime": 1484142997.146127, + "unixSocketServer": { + "running": false, + "configured": false + }, + "requestCount": 17153, + "commandCount": 59298, + "bytesIn": 3189628 +} + diff --git a/test/units/modules/network/eos/fixtures/eos_eapi_show_vrf.text b/test/units/modules/network/eos/fixtures/eos_eapi_show_vrf.text new file mode 100644 index 0000000000..b320bdc63a --- /dev/null +++ b/test/units/modules/network/eos/fixtures/eos_eapi_show_vrf.text @@ -0,0 +1,11 @@ +Maximum number of vrfs allowed: 14 + Vrf RD Protocols State Interfaces +--------- ----------- --------------- -------------------- -------------------- + mgmt 1:101 ipv4,ipv6 v4:no routing, + v6:no routing + + test 1:100 ipv4,ipv6 v4:routing, Ethernet5, Ethernet6 + v6:no routing + + + diff --git a/test/units/modules/network/eos/test_eos_eapi.py b/test/units/modules/network/eos/test_eos_eapi.py new file mode 100644 index 0000000000..b0be9f2bd6 --- /dev/null +++ b/test/units/modules/network/eos/test_eos_eapi.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python +# +# (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 + +import os +import json + +import ansible.module_utils.basic + +from ansible.compat.tests import unittest +from ansible.compat.tests.mock import patch, MagicMock +from ansible.errors import AnsibleModuleExit +from ansible.modules.network.eos import eos_eapi +from ansible.module_utils import basic +from ansible.module_utils._text import to_bytes + + +def set_module_args(args): + args = json.dumps({'ANSIBLE_MODULE_ARGS': args}) + basic._ANSIBLE_ARGS = to_bytes(args) + +fixture_path = os.path.join(os.path.dirname(__file__), 'fixtures') +fixture_data = {} + +def load_fixture(name): + path = os.path.join(fixture_path, name) + + if path in fixture_data: + return fixture_data[path] + + with open(path) as f: + data = f.read() + + try: + data = json.loads(data) + except: + pass + + fixture_data[path] = data + return data + + +class TestEosEapiModule(unittest.TestCase): + + def setUp(self): + self.mock_run_commands = patch('ansible.modules.network.eos.eos_eapi.run_commands') + self.run_commands = self.mock_run_commands.start() + + self.mock_load_config = patch('ansible.modules.network.eos.eos_eapi.load_config') + self.load_config = self.mock_load_config.start() + + def tearDown(self): + self.mock_run_commands.stop() + self.mock_load_config.stop() + + def execute_module(self, failed=False, changed=False, commands=None, + sort=True, command_fixtures={}): + + def run_commands(module, commands, **kwargs): + output = list() + for cmd in commands: + output.append(load_fixture(command_fixtures[cmd])) + return (0, output, '') + + self.run_commands.side_effect = run_commands + self.load_config.return_value = dict(diff=None, session='session') + + with self.assertRaises(AnsibleModuleExit) as exc: + eos_eapi.main() + + result = exc.exception.result + + if failed: + self.assertTrue(result.get('failed'), result) + else: + self.assertEqual(result.get('changed'), changed, result) + + if commands: + if sort: + self.assertEqual(sorted(commands), sorted(result['commands']), result['commands']) + else: + self.assertEqual(commands, result['commands']) + + return result + + def start_configured(self, *args, **kwargs): + command_fixtures = { + 'show vrf': 'eos_eapi_show_vrf.text', + 'show management api http-commands | json': 'eos_eapi_show_mgmt.json' + } + kwargs['command_fixtures'] = command_fixtures + return self.execute_module(*args, **kwargs) + + def start_unconfigured(self, *args, **kwargs): + command_fixtures = { + 'show vrf': 'eos_eapi_show_vrf.text', + 'show management api http-commands | json': 'eos_eapi_show_mgmt_unconfigured.json' + } + kwargs['command_fixtures'] = command_fixtures + return self.execute_module(*args, **kwargs) + + def test_eos_eapi_http_enable(self): + set_module_args(dict(http=True)) + commands = ['management api http-commands', 'protocol http port 80', + 'no shutdown'] + self.start_unconfigured(changed=True, commands=commands) + + def test_eos_eapi_http_disable(self): + set_module_args(dict(http=False)) + commands = ['management api http-commands', 'no protocol http'] + self.start_configured(changed=True, commands=commands) + + def test_eos_eapi_http_port(self): + set_module_args(dict(http_port=81)) + commands = ['management api http-commands', 'protocol http port 81'] + self.start_configured(changed=True, commands=commands) + + def test_eos_eapi_http_invalid(self): + set_module_args(dict(port=80000)) + commands = [] + self.start_unconfigured(failed=True) + + def test_eos_eapi_https_enable(self): + set_module_args(dict(https=True)) + commands = ['management api http-commands', 'protocol https port 443', + 'no shutdown'] + self.start_unconfigured(changed=True, commands=commands) + + def test_eos_eapi_https_disable(self): + set_module_args(dict(https=False)) + commands = ['management api http-commands', 'no protocol https'] + self.start_configured(changed=True, commands=commands) + + def test_eos_eapi_https_port(self): + set_module_args(dict(https_port=8443)) + commands = ['management api http-commands', 'protocol https port 8443'] + self.start_configured(changed=True, commands=commands) + + def test_eos_eapi_local_http_enable(self): + set_module_args(dict(local_http=True)) + commands = ['management api http-commands', 'protocol http localhost port 8080', + 'no shutdown'] + self.start_unconfigured(changed=True, commands=commands) + + def test_eos_eapi_local_http_disable(self): + set_module_args(dict(local_http=False)) + commands = ['management api http-commands', 'no protocol http localhost'] + self.start_configured(changed=True, commands=commands) + + def test_eos_eapi_local_http_port(self): + set_module_args(dict(local_http_port=81)) + commands = ['management api http-commands', 'protocol http localhost port 81'] + self.start_configured(changed=True, commands=commands) + + def test_eos_eapi_vrf(self): + set_module_args(dict(vrf='test')) + commands = ['management api http-commands', 'vrf test', 'no shutdown'] + self.start_unconfigured(changed=True, commands=commands) + + def test_eos_eapi_vrf_missing(self): + set_module_args(dict(vrf='missing')) + commands = [] + self.start_unconfigured(failed=True, commands=commands) + + def test_eos_eapi_state_absent(self): + set_module_args(dict(state='stopped')) + commands = ['management api http-commands', 'shutdown'] + self.start_configured(changed=True, commands=commands) +