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
This commit is contained in:
Peter Sprygada 2016-08-30 17:52:31 -04:00 committed by Matt Clay
parent ef46627f1f
commit 54232c3b2d

View file

@ -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
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 build_candidate(lines, parents, config, strategy):
candidate = list()
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')
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)
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'),
update=dict(choices=['merge', 'check'], default='merge'),
backup=dict(type='bool', default=False),
config=dict(),
default=dict(type='bool', default=False),
save=dict(type='bool', default=False),
state=dict(choices=['present', 'absent'], default='present'),
# ops_config is only supported over Cli transport so force
# the value of transport to be cli
transport=dict(default='cli', choices=['cli'])
)
module = get_module(argument_spec=argument_spec,
supports_check_mode=True)
mutually_exclusive = [('lines', 'src')]
lines = module.params['lines']
parents = module.params['parents'] or list()
module = NetworkModule(argument_spec=argument_spec,
connect_on_load=False,
mutually_exclusive=mutually_exclusive,
supports_check_mode=True)
before = module.params['before']
after = module.params['after']
state = module.params['state']
match = module.params['match']
replace = module.params['replace']
if module.params['force'] is True:
module.params['match'] = 'none'
contents = get_config(module)
config = module.parse_config(contents)
warnings = list()
check_args(module, warnings)
if parents:
for parent in parents:
for item in config:
if item.text == parent:
config = item
result = dict(changed=False, warnings=warnings)
try:
children = [c.text for c in config.children]
except AttributeError:
children = [c.text for c in config]
if module.params['backup']:
result['__backup__'] = module.config.get_config()
else:
children = [c.text for c in config if not c.parents]
try:
invoke(state, module, result)
except NetworkError:
exc = get_exception()
module.fail_json(msg=str(exc))
result = dict(changed=False)
module.exit_json(**result)
candidate = build_candidate(lines, parents, children, match)
if candidate:
if replace == 'line':
candidate[:0] = parents
else:
candidate = list(parents)
candidate.extend(lines)
if before:
candidate[:0] = before
if after:
candidate.extend(after)
if not module.check_mode:
response = module.configure(candidate)
result['responses'] = response
result['changed'] = True
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()