From b5bbac29e54076507dad81bc6b2b37ee045571be Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Mon, 22 Aug 2016 20:23:36 -0400 Subject: [PATCH] updates eos shared module * adds support for netcli methods * adds support for netcfg methods * Cli class now derives from CliBase * adds eos_config action plugin --- lib/ansible/module_utils/eos.py | 235 ++++++++--------------- lib/ansible/plugins/action/eos_config.py | 28 +++ 2 files changed, 108 insertions(+), 155 deletions(-) create mode 100644 lib/ansible/plugins/action/eos_config.py diff --git a/lib/ansible/module_utils/eos.py b/lib/ansible/module_utils/eos.py index 8f596df50d3..0dd58c082d3 100644 --- a/lib/ansible/module_utils/eos.py +++ b/lib/ansible/module_utils/eos.py @@ -29,186 +29,87 @@ import re from ansible.module_utils.basic import json, get_exception -from ansible.module_utils.network import NetworkModule, NetworkError -from ansible.module_utils.network import NetCli, Command, ModuleStub +from ansible.module_utils.network import NetworkModule, NetworkError, ModuleStub from ansible.module_utils.network import add_argument, register_transport, to_list from ansible.module_utils.netcfg import NetworkConfig +from ansible.module_utils.netcli import Command +from ansible.module_utils.shell import CliBase from ansible.module_utils.urls import fetch_url, url_argument_spec -# temporary fix until modules are update. to be removed before 2.2 final -from ansible.module_utils.network import get_module - EAPI_FORMATS = ['json', 'text'] add_argument('use_ssl', dict(default=True, type='bool')) add_argument('validate_certs', dict(default=True, type='bool')) -def argument_spec(): - return dict( - # config options - running_config=dict(aliases=['config']), - config_session=dict(default='ansible_session'), - save_config=dict(default=False, aliases=['save']), - force=dict(type='bool', default=False) - ) -eos_argument_spec = argument_spec() - -def get_config(module): - config = module.params['running_config'] - if not config: - config = module.config.get_config(include_defaults=False) - return NetworkConfig(indent=3, contents=config) - -def load_config(module, candidate): - - if not module.params['force']: - config = get_config(module) - commands = candidate.difference(config) - else: - commands = str(candidate) - - commands = [str(c).strip() for c in commands] - - session = module.params['config_session'] - save_config = module.params['save_config'] - - result = dict(changed=False) - - if commands: - if module._diff: - diff = module.config.load_config(commands, session_name=session) - if diff: - result['diff'] = dict(prepared=diff) - - if not module.check_mode: - module.config.commit_config(session) - if save_config: - module.config.save_config() - else: - module.config.abort_config(session_name=session) - - if not module.check_mode: - module.config(commands) - if save_config: - module.config.save_config() - - result['changed'] = True - result['updates'] = commands - - return result - -def expand_intf_range(interfaces): - match = re.match(r'([a-zA-Z]+)(.+)', interfaces) - if not match: - raise ValueError('could not parse interface range') - - name = match.group(1) - values = match.group(2).split(',') - - indicies = list() - - for val in values: - tokens = val.split('-') - - # single index value to handle - if len(tokens) == 1: - indicies.append(tokens[0]) - - elif len(tokens) == 2: - pairs = list() - mod = 0 - - for token in tokens: - parts = token.split('/') - - if len(parts) == 1: - port = parts[0] - if port == '$': - port = last_port - pairs.append((mod, int(port))) - - elif len(parts) == 2: - mod = int(parts[0]) - port = parts[1] - if port == '$': - port = last_port - pairs.append((mod, int(port))) - - else: - raise ValueError('unable to parse interface') - - if pairs[0][0] == pairs[1][0]: - # same module - mod = pairs[0][0] - start = pairs[0][1] - end = pairs[1][1] + 1 - - for i in range(start, end): - if mod == 0: - indicies.append(i) - else: - indicies.append('%s/%s' % (mod, i)) - else: - # span modules - start_mod, start_port = pairs[0] - end_mod, end_port = pairs[1] - end_port += 1 - - for i in range(start_port, last_port+1): - indicies.append('%s/%s' % (start_mod, i)) - - for i in range(first_port, end_port): - indicies.append('%s/%s' % (end_mod, i)) - - return ['%s%s' % (name, index) for index in indicies] - class EosConfigMixin(object): + ### implementation of netcfg.Config ### + def configure(self, commands, **kwargs): - commands = prepare_config(commands) + cmds = ['configure terminal'] + cmds.extend(to_list(commands)) + cmds.append('end') responses = self.execute(commands) - responses.pop(0) - return responses + return responses[1:-1] - def get_config(self, **kwargs): + def get_config(self, include_defaults=False, **kwargs): cmd = 'show running-config' - if kwargs.get('include_defaults') is True: + if include_defaults: cmd += ' all' return self.execute([cmd])[0] - def load_config(self, commands, session_name='ansible_temp_session', **kwargs): - commands = to_list(commands) - commands.insert(0, 'configure session %s' % session_name) - commands.append('show session-config diffs') - commands.append('end') - responses = self.execute(commands) - return responses[-2] + def load_config(self, config, session, commit=False, replace=False, **kwargs): + """ Loads the configuration into the remote device - def replace_config(self, contents, params, **kwargs): - remote_user = params['username'] - remote_path = '/home/%s/ansible-config' % remote_user + This method handles the actual loading of the config + commands into the remote EOS device. By default the + config specified is merged with the current running-config. - commands = [ - 'bash echo "%s" > %s' % (contents, remote_path), - 'diff running-config file:/%s' % remote_path, - 'config replace file:/%s' % remote_path, - ] + :param config: ordered list of config commands to load + :param replace: replace current config when True otherwise merge - responses = self.run_commands(commands) - return responses[-2] + :returns list: ordered set of responses from device + """ + commands = ['configure session %s' % session] + if replace: + commands.append('rollback clean-config') - def commit_config(self, session_name): - session = 'configure session %s' % session_name - commands = [session, 'commit', 'no %s' % session] - self.execute(commands) + commands.extend(config) - def abort_config(self, session_name): - command = 'no configure session %s' % session_name - self.execute([command]) + if commands[-1] != 'end': + commands.append('end') + + try: + self.execute(commands) + diff = self.diff_config(session) + if commit: + self.commit_config(session) + except NetworkError: + self.abort_config(session) + diff = None + raise + return diff def save_config(self): self.execute(['copy running-config startup-config']) + ### end netcfg.Config ### + + def diff_config(self, session): + commands = ['configure session %s' % session, + 'show session-config diffs', + 'end'] + response = self.execute(commands) + return response[-2] + + def commit_config(self, session): + commands = ['configure session %s' % session, 'commit'] + self.execute(commands) + + def abort_config(self, session): + commands = ['configure session %s' % session, 'abort'] + self.execute(commands) + class Eapi(EosConfigMixin): def __init__(self): @@ -295,6 +196,7 @@ class Eapi(EosConfigMixin): """ if self.url is None: raise NetworkError('Not connected to endpoint.') + if self.enable is not None: commands.insert(0, self.enable) @@ -330,10 +232,12 @@ class Eapi(EosConfigMixin): def get_config(self, **kwargs): return self.run_commands(['show running-config'], format='text')[0] + Eapi = register_transport('eapi')(Eapi) -class Cli(NetCli, EosConfigMixin): +class Cli(CliBase, EosConfigMixin): + CLI_PROMPTS_RE = [ re.compile(r"[\r\n]?[\w+\-\.:\/\[\]]+(?:\([^\)]+\)){,3}(?:>|#) ?$"), re.compile(r"\[\w+\@[\w\-\.]+(?: [^\]])\] ?[>#\$] ?$") @@ -357,9 +261,23 @@ class Cli(NetCli, EosConfigMixin): super(Cli, self).connect(params, kickstart=True, **kwargs) self.shell.send('terminal length 0') + def authorize(self, params, **kwargs): + passwd = params['auth_pass'] + self.execute(Command('enable', prompt=self.NET_PASSWD_RE, response=passwd)) + ### implementation of network.Cli ### def run_commands(self, commands): + """execute the ordered set of commands on the remote host + + This method will take a list of Command objects, convert them + to a list of dict objects and execute them on the shell + connection. + + :param commands: list of Command objects + + :returns: set of ordered responses + """ cmds = list(prepare_commands(commands)) responses = self.execute(cmds) for index, cmd in enumerate(commands): @@ -372,16 +290,23 @@ class Cli(NetCli, EosConfigMixin): response=responses[index] ) return responses + Cli = register_transport('cli', default=True)(Cli) + def prepare_config(commands): commands = to_list(commands) commands.insert(0, 'configure terminal') commands.append('end') return commands - def prepare_commands(commands): + """ transforms a list of Command objects to dict + + :param commands: list of Command objects + + :returns: list of dict objects + """ jsonify = lambda x: '%s | json' % x for cmd in to_list(commands): if cmd.output == 'json': diff --git a/lib/ansible/plugins/action/eos_config.py b/lib/ansible/plugins/action/eos_config.py new file mode 100644 index 00000000000..ffcb0f057f8 --- /dev/null +++ b/lib/ansible/plugins/action/eos_config.py @@ -0,0 +1,28 @@ +# +# Copyright 2015 Peter Sprygada +# +# 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 . +# +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from ansible.plugins.action import ActionBase +from ansible.plugins.action.net_config import ActionModule as NetActionModule + +class ActionModule(NetActionModule, ActionBase): + pass + +