From 2133b9298024e4f09101fd9ff51e49b9d8f90bc9 Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Tue, 30 Aug 2016 17:52:31 -0400 Subject: [PATCH] update ops_config module with new enhancements * add src argument to provide path to config file * add new choice to match used to ignore current running config * add update argument with choices merge or check * add backup argument to backup current running config to control host * add save argument to save current running config to startup config * add state argument to control state of config file * deprecated force argument, use match=none instead Note: this module only supports transport=cli Tested on OpenSwitch 0.4.0 --- network/openswitch/ops_config.py | 291 +++++++++++++++++++++---------- 1 file changed, 199 insertions(+), 92 deletions(-) diff --git a/network/openswitch/ops_config.py b/network/openswitch/ops_config.py index 37960038b2b..0370eb79686 100644 --- a/network/openswitch/ops_config.py +++ b/network/openswitch/ops_config.py @@ -20,7 +20,7 @@ DOCUMENTATION = """ --- module: ops_config version_added: "2.1" -author: "Peter sprygada (@privateip)" +author: "Peter Sprygada (@privateip)" short_description: Manage OpenSwitch configuration using CLI description: - OpenSwitch configurations use a simple block indent file syntax @@ -36,7 +36,8 @@ options: in the device running-config. Be sure to note the configuration command syntax as some commands are automatically modified by the device config parser. - required: true + required: false + default: null parents: description: - The ordered set of parents that uniquely identify the section @@ -45,6 +46,17 @@ options: level or global commands. required: false default: null + src: + description: + - The I(src) argument provides a path to the configuration file + to load into the remote system. The path can either be a full + system path to the configuration file if the value starts with / + or relative to the root of the implemented role or playbook. + This arugment is mutually exclusive with the I(lines) and + I(parents) arguments. + required: false + default: null + version_added: "2.2" before: description: - The ordered set of commands to push on to the command stack if @@ -72,7 +84,7 @@ options: must be an equal match. required: false default: line - choices: ['line', 'strict', 'exact'] + choices: ['line', 'strict', 'exact', 'none'] replace: description: - Instructs the module on the way to perform the configuration @@ -90,9 +102,25 @@ options: current devices running-config. When set to true, this will cause the module to push the contents of I(src) into the device without first checking if already configured. + - Note this argument should be considered deprecated. To achieve + the equivalent, set the match argument to none. This argument + will be removed in a future release. required: false default: false - choices: ['true', 'false'] + choices: ['yes', 'no'] + update: + description: + - The I(update) argument controls how the configuration statements + are processed on the remote device. Valid choices for the I(update) + argument are I(merge) I(replace) and I(check). When the argument is + set to I(merge), the configuration changes are merged with the current + device running configuration. When the argument is set to I(check) + the configuration updates are determined but not actually configured + on the remote device. + required: false + default: merge + choices: ['merge', 'check'] + version_added: "2.2" config: description: - The module, by default, will connect to the remote device and @@ -104,20 +132,55 @@ options: config for comparison. required: false default: null + save: + description: + - The C(save) argument instructs the module to save the running- + config to the startup-config at the conclusion of the module + running. If check mode is specified, this argument is ignored. + required: false + default: no + choices: ['yes', 'no'] + version_added: "2.2" + state: + description: + - This argument specifies whether or not the running-config is + present on the remote device. When set to I(absent) the + running-config on the remote device is erased. + required: false + default: no + choices: ['yes', 'no'] + version_added: "2.2" """ EXAMPLES = """ +# Note: examples below use the following provider dict to handle +# transport and authentication to the node. +vars: + cli: + host: "{{ inventory_hostname }}" + username: netop + password: netop + - name: configure hostname over cli ops_config: - lines: - - "hostname {{ inventory_hostname }}" + lines: + - "hostname {{ inventory_hostname }}" + provider: "{{ cli }}" + - name: configure vlan 10 over cli ops_config: - lines: - - no shutdown - parents: - - vlan 10 + lines: + - no shutdown + parents: + - vlan 10 + provider: "{{ cli }}" + +- name: load config from file + ops_config: + src: ops01.cfg + backup: yes + provider: "{{ cli }}" """ RETURN = """ @@ -126,122 +189,166 @@ updates: returned: always type: list sample: ['...', '...'] - +backup_path: + description: The full path to the backup file + returned: when backup is yes + type: path + sample: /playbooks/ansible/backup/ios_config.2016-07-16@22:28:34 responses: description: The set of responses from issuing the commands on the device - retured: when not check_mode + returned: when not check_mode type: list sample: ['...', '...'] """ import re -import itertools - -def get_config(module): - config = module.params['config'] or dict() - if not config and not module.params['force']: - config = module.config - return config - - -def build_candidate(lines, parents, config, strategy): - candidate = list() - - if strategy == 'strict': - for index, cmd in enumerate(lines): - try: - if cmd != config[index]: - candidate.append(cmd) - except IndexError: - candidate.append(cmd) - - elif strategy == 'exact': - if len(lines) != len(config): - candidate = list(lines) - else: - for cmd, cfg in itertools.izip(lines, config): - if cmd != cfg: - candidate = list(lines) - break - else: - for cmd in lines: - if cmd not in config: - candidate.append(cmd) +from ansible.module_utils.basic import get_exception +from ansible.module_utils.openswitch import NetworkModule, NetworkError +from ansible.module_utils.netcfg import NetworkConfig, dumps +from ansible.module_utils.netcli import Command +def invoke(name, *args, **kwargs): + func = globals().get(name) + if func: + return func(*args, **kwargs) + +def check_args(module, warnings): + if module.params['parents']: + if not module.params['lines'] or module.params['src']: + warnings.append('ignoring unnecessary argument parents') + if module.params['force']: + warnings.append('The force argument is deprecated, please use ' + 'match=none instead. This argument will be ' + 'removed in the future') + +def get_config(module, result): + contents = module.params['config'] + if not contents: + contents = module.config.get_config() + return NetworkConfig(indent=4, contents=contents) + +def get_candidate(module): + candidate = NetworkConfig(indent=4) + if module.params['src']: + candidate.load(module.params['src']) + elif module.params['lines']: + parents = module.params['parents'] or list() + candidate.add(module.params['lines'], parents=parents) return candidate +def load_backup(module): + try: + module.cli(['exit', 'config replace flash:/ansible-rollback force']) + except NetworkError: + module.fail_json(msg='unable to rollback configuration') + +def backup_config(module): + cmd = 'copy running-config flash:/ansible-rollback' + cmd = Command(cmd, prompt=re.compile('\? $'), response='\n') + module.cli(cmd) + +def load_config(module, commands, result): + if not module.check_mode and module.params['update'] != 'check': + module.config(commands) + result['changed'] = module.params['update'] != 'check' + result['updates'] = commands.split('\n') + +def present(module, result): + match = module.params['match'] + replace = module.params['replace'] + + candidate = get_candidate(module) + + if match != 'none': + config = get_config(module, result) + configobjs = candidate.difference(config, match=match, replace=replace) + else: + config = None + configobjs = candidate.items + + if configobjs: + commands = dumps(configobjs, 'commands') + + if module.params['before']: + commands[:0] = module.params['before'] + + if module.params['after']: + commands.extend(module.params['after']) + + # send the configuration commands to the device and merge + # them with the current running config + load_config(module, commands, result) + + if module.params['save'] and not module.check_mode: + module.config.save_config() + +def absent(module, result): + if not module.check_mode: + module.cli('erase startup-config') + result['changed'] = True def main(): argument_spec = dict( - lines=dict(aliases=['commands'], required=True, type='list'), + lines=dict(aliases=['commands'], type='list'), parents=dict(type='list'), + + src=dict(type='path'), + before=dict(type='list'), after=dict(type='list'), - match=dict(default='line', choices=['line', 'strict', 'exact']), + + match=dict(default='line', choices=['line', 'strict', 'exact', 'none']), replace=dict(default='line', choices=['line', 'block']), + + # this argument is deprecated in favor of setting match: none + # it will be removed in a future version force=dict(default=False, type='bool'), - config=dict(), - transport=dict(default='cli', choices=['cli']) - ) - module = get_module(argument_spec=argument_spec, - supports_check_mode=True) + update=dict(choices=['merge', 'check'], default='merge'), + backup=dict(type='bool', default=False), - lines = module.params['lines'] - parents = module.params['parents'] or list() + config=dict(), + default=dict(type='bool', default=False), - before = module.params['before'] - after = module.params['after'] + save=dict(type='bool', default=False), - match = module.params['match'] - replace = module.params['replace'] + state=dict(choices=['present', 'absent'], default='present'), - contents = get_config(module) - config = module.parse_config(contents) + # ops_config is only supported over Cli transport so force + # the value of transport to be cli + transport=dict(default='cli', choices=['cli']) + ) - if parents: - for parent in parents: - for item in config: - if item.text == parent: - config = item + mutually_exclusive = [('lines', 'src')] - try: - children = [c.text for c in config.children] - except AttributeError: - children = [c.text for c in config] + module = NetworkModule(argument_spec=argument_spec, + connect_on_load=False, + mutually_exclusive=mutually_exclusive, + supports_check_mode=True) - else: - children = [c.text for c in config if not c.parents] + state = module.params['state'] - result = dict(changed=False) + if module.params['force'] is True: + module.params['match'] = 'none' - candidate = build_candidate(lines, parents, children, match) + warnings = list() + check_args(module, warnings) - if candidate: - if replace == 'line': - candidate[:0] = parents - else: - candidate = list(parents) - candidate.extend(lines) + result = dict(changed=False, warnings=warnings) - if before: - candidate[:0] = before + if module.params['backup']: + result['__backup__'] = module.config.get_config() - if after: - candidate.extend(after) + try: + invoke(state, module, result) + except NetworkError: + exc = get_exception() + module.fail_json(msg=str(exc)) - if not module.check_mode: - response = module.configure(candidate) - result['responses'] = response - result['changed'] = True + module.exit_json(**result) - result['updates'] = candidate - return module.exit_json(**result) -from ansible.module_utils.basic import * -from ansible.module_utils.shell import * -from ansible.module_utils.netcfg import * -from ansible.module_utils.openswitch import * if __name__ == '__main__': main() +