From 923974e3c05ff27410c2b7b68d7897b514fdb8bd Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Thu, 7 Apr 2016 19:14:30 -0400 Subject: [PATCH] add support to junos_command module for netconf transport This adds support for using netconf to the junos_command option and removes the use of cli as a transport --- .../modules/network/junos/junos_command.py | 253 +++++++++--------- 1 file changed, 121 insertions(+), 132 deletions(-) diff --git a/lib/ansible/modules/network/junos/junos_command.py b/lib/ansible/modules/network/junos/junos_command.py index ad1e977541b..440fd6aeb0e 100644 --- a/lib/ansible/modules/network/junos/junos_command.py +++ b/lib/ansible/modules/network/junos/junos_command.py @@ -20,32 +20,40 @@ DOCUMENTATION = """ --- module: junos_command version_added: "2.1" -author: "Peter sprygada (@privateip)" -short_description: Execute arbitrary commands on Juniper JUNOS devices +author: "Peter Sprygada (@privateip)" +short_description: Execute arbitrary commands on a remote device running Junos description: - - Netork devices running Juniper JUNOS can execute a variety of commands - to display both configuration and state data. This module will take - an ordered set of commands and execute them in JUNOS and return the - command results. It also supports adding conditionals the - argument set to force the module to meet a set of criteria before - proceeding. + - Network devices running the Junos operating system provide a command + driven interface both over CLI and RPC. This module provides an + interface to execute commands using these functions and return the + results to the Ansible playbook. In addition, the M(junos_command) + module can specify a set of conditionals to be evaluated against the + returned output, only returning control to the playbook once the + entire set of conditionals has been met. extends_documentation_fragment: junos options: commands: description: - - The commands to send to the remote JUNOS device over the - configured provider. The resulting output from the command - is returned. If the I(waitfor) argument is provided, the - module is not returned until the condition is satisfied or - the number of retires as expired. - required: true + - An ordered set of CLI commands to be executed on the remote + device. The output from the commands is then returned to + the playbook in the task results. + required: false + default: null + rpcs: + description: + - The O(rpcs) argument accepts a list of RPCs to be executed + over a netconf session and the results from the RPC execution + is return to the playbook via the modules results dictionary. + required: false + default: null waitfor: description: - Specifies what to evaluate from the output of the command and what conditionals to apply. This argument will cause - the task to wait for a particular conditional to be true - before moving forward. If the conditional is not true - by the configured retries, the task fails. See examples. + the task to wait for a particular conditional or set of + considitonals to be true before moving forward. If the + conditional is not true by the configured retries, the + task fails. See examples. required: false default: null retries: @@ -64,6 +72,19 @@ options: trying the command again. required: false default: 1 + format: + description: + - Configures the encoding scheme to use when serializing output + from the device. This handles how to properly understand the + output and apply the conditionals path to the result set. + required: false + default: 'xml' + choices: ['xml', 'text'] +requirements: + - junos-eznc +notes: + - This module requires the netconf system service be enabled on + the remote device being managed """ EXAMPLES = """ @@ -78,10 +99,15 @@ EXAMPLES = """ junos_command: commands: - show version - - show interfaces fxp0 | display json + - show interfaces fxp0 waitfor: - - "result[1].interface-information[0].physical-interface[0].name[0].data - eq fxp0" + - "result[1].interface-information.physical-interface.name eq fxp0" + +- name: collect interface information using rpc + junos_command: + rpcs: + - "get_interface_information interface=em0 media=True" + - "get_interface_information interface=fxp0 media=True" """ RETURN = """ @@ -97,137 +123,93 @@ stdout_lines: type: list sample: [['...', '...'], ['...', '...']] +xml: + description: The raw XML reply from the device + returned: when format is xml + type: list + sample: [['...', '...'], ['...', '...']] + failed_conditionals: description: the conditionals that failed retured: failed type: list sample: ['...', '...'] """ - -import time import shlex -import re -import json -import itertools - -INDEX_RE = re.compile(r'(\[\d+\])') - -class Conditional(object): - - OPERATORS = { - 'eq': ['eq', '=='], - 'neq': ['neq', 'ne', '!='], - 'gt': ['gt', '>'], - 'ge': ['ge', '>='], - 'lt': ['lt', '<'], - 'le': ['le', '<='], - 'contains': ['contains', 'in'] - } - - def __init__(self, conditional): - self.raw = conditional - - key, op, val = shlex.split(conditional) - self.key = key - self.func = self.func(op) - self.value = self._cast_value(val) - - def __call__(self, data): - try: - value = self.get_value(dict(result=data)) - return self.func(value) - except Exception: - raise - - def _cast_value(self, value): - if value in BOOLEANS_TRUE: - return True - elif value in BOOLEANS_FALSE: - return False - elif re.match(r'^\d+\.d+$', value): - return float(value) - elif re.match(r'^\d+$', value): - return int(value) - else: - return unicode(value) - - def func(self, oper): - for func, operators in self.OPERATORS.items(): - if oper in operators: - return getattr(self, func) - raise AttributeError('unknown operator: %s' % oper) - - def get_value(self, result): - for key in self.key.split('.'): - match = re.match(r'^(.+)\[(\d+)\]', key) - if match: - key, index = match.groups() - result = result[key][int(index)] - else: - result = result.get(key) - return result - - def number(self, value): - if '.' in str(value): - return float(value) - else: - return int(value) - - def eq(self, value): - return value == self.value - def neq(self, value): - return value != self.value - - def gt(self, value): - return self.number(value) > self.value - - def ge(self, value): - return self.number(value) >= self.value - - def lt(self, value): - return self.number(value) < self.value - - def le(self, value): - return self.number(value) <= self.value - - def contains(self, value): - return self.value in value - -def parse_response(module, responses): - commands = module.params['commands'] - result = dict(stdout_json=list(), stdout=list(), stdout_lines=list()) - for cmd, resp in itertools.izip(commands, responses): - result['stdout'].append(resp) - if cmd.endswith('json'): - result['stdout_json'].append(module.jsonify(resp)) +def split(value): + lex = shlex.shlex(value) + lex.quotes = '"' + lex.whitespace_split = True + lex.commenters = '' + return list(lex) + +def rpc_args(args): + kwargs = dict() + args = split(args) + name = args.pop(0) + for arg in args: + key, value = arg.split('=') + if str(value).upper() in ['TRUE', 'FALSE']: + kwargs[key] = bool(value) + elif re.match(r'\d+', value): + kwargs[key] = int(value) + else: + kwargs[key] = str(value) + return (name, kwargs) + +def parse_rpcs(rpcs): + parsed = list() + for rpc in (rpcs or list()): + parsed.append(rpc_args(rpc)) + return parsed + +def run_rpcs(module, items, format): + response = list() + for name, kwargs in items: + kwargs['format'] = format + result = module.connection.rpc(name, **kwargs) + if format == 'text': + response.append(result.text) else: - result['stdout_lines'].append(resp.split('\n')) - return result + response.append(result) + return response -def to_lines(stdout): +def iterlines(stdout): for item in stdout: if isinstance(item, basestring): item = str(item).split('\n') yield item def main(): + """main entry point for Ansible module + """ + spec = dict( commands=dict(type='list'), + rpcs=dict(type='list'), + format=dict(default='xml', choices=['text', 'xml']), waitfor=dict(type='list'), retries=dict(default=10, type='int'), - interval=dict(default=1, type='int') + interval=dict(default=1, type='int'), + transport=dict(default='netconf', choices=['netconf']) ) + mutually_exclusive = [('commands', 'rpcs')] + module = get_module(argument_spec=spec, + mutually_exclusive=mutually_exclusive, supports_check_mode=True) commands = module.params['commands'] + rpcs = parse_rpcs(module.params['rpcs']) + encoding = module.params['format'] retries = module.params['retries'] interval = module.params['interval'] + try: queue = set() for entry in (module.params['waitfor'] or list()): @@ -238,15 +220,18 @@ def main(): result = dict(changed=False) while retries > 0: - try: - response = module.execute(commands) - result['stdout'] = response - except ShellError: - module.fail_json(msg='failed to run commands') + if commands: + response = module.run_commands(commands, format=encoding) + else: + response = run_rpcs(module, rpcs, format=encoding) - for index, cmd in enumerate(commands): - if cmd.endswith('json'): - response[index] = json.loads(response[index]) + result['stdout'] = response + xmlout = list() + + for index in range(0, len(response)): + if encoding == 'xml': + xmlout.append(xml_to_string(response[index])) + response[index] = xml_to_json(response[index]) for item in list(queue): if item(response): @@ -261,13 +246,17 @@ def main(): failed_conditions = [item.raw for item in queue] module.fail_json(msg='timeout waiting for value', failed_conditions=failed_conditions) - result['stdout_lines'] = list(to_lines(result['stdout'])) - return module.exit_json(**result) + if xmlout: + result['xml'] = xmlout + + result['stdout_lines'] = list(iterlines(result['stdout'])) + 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.junos import * + if __name__ == '__main__': - main() + main()