#!/usr/bin/python ''' Ansible action plugin for configuring Wildfly ''' from __future__ import (absolute_import, division, print_function) import re import json from ansible.plugins.action import ActionBase from ansible.utils.display import Display from ansible.utils.vars import merge_hash from six import string_types display = Display() class WildflyError(Exception): ''' Basic error class for this module. ''' class ActionModule(ActionBase): ''' The action module. ''' def run(self, tmp=None, task_vars=None): super(ActionModule, self).run(tmp, task_vars) # Define support for check mode and async self._supports_check_mode = True self._supports_async = False task_args = self._task.args.copy() check_mode = self._play_context.check_mode cli_command, config, dry_run = read_args(task_args, check_mode) # read current config cur_result = self.execute_command( '{} --command="/:read-resource(recursive)"'.format( cli_command), check_mode_unsafe=False ) if cur_result.get('failed'): return cur_result try: cur_state = json.loads(cur_result.get('stdout')).get('result') except json.JSONDecodeError as ex: return merge_hash(cur_result, dict( failed=True, msg='Error parsing JSON from Wildfly CLI: {}'.format(str(ex)) )) # generate batch script batch_script = '' for item in config: try: batch_script = batch_script + wildfly_batch( cur_state, item.get('root', '/'), item.get('config', {}), item.get('state', 'present'), False) except WildflyError as ex: return merge_hash(cur_result, dict( failed=True, msg='Error while generating Wildfly batch: {}'.format( str(ex)), stdout=None, stdout_lines=[], config_item=item, batch_script=batch_script )) result = dict(changed=False, batch_script=batch_script, previous_state=cur_state) # apply changes if batch_script: batch_script = 'batch\n{}run-batch\n'.format(batch_script) result = merge_hash(result, dict(changed=True)) if not dry_run: result = merge_hash(result, self.execute_command( cli_command, stdin=batch_script, check_mode_unsafe=True )) return result def execute_command(self, command, stdin=None, check_mode_unsafe=True): ''' Execute command module and return result ''' return self._execute_module( module_name='command', module_args=dict( _raw_params=command, stdin=stdin, _ansible_check_mode=(self._play_context.check_mode and check_mode_unsafe) ) ) def read_args(task_args, check_mode): ''' Read and validate invocation arguments ''' # CLI command for connecting to Wildfly admin interface cli_path = task_args.get('cli_path', 'jboss-cli.sh') cli_options = task_args.get('cli_options', False) controller = task_args.get('controller', False) user = task_args.get('user', False) password = task_args.get('password', '') local_auth = task_args.get('local_auth', True) cli_command = ( '{} {} {} {} {} --connect --output-json' '--no-color-output --no-output-paging' ).format( cli_path, (cli_options if cli_options else ''), ('--controller={}'.format(controller) if controller else ''), ('--user={} --password={}'.format(user, password) if user else ''), ('' if local_auth else '--no-local-auth') ) # deprecated cli_cmd option if task_args.get('cli_cmd', False): display.deprecated( '"cli_cmd" argument should not be used. Please use ' '"cli_path", "cli_options", "conroller", "user", "password", ' 'and "local_auth" arguments instead') cli_command = ( '{} --connect --output-json' ' --no-color-output --no-output-paging' ).format(task_args.get('cli_cmd')) # desired configuration config = task_args.get('config', False) if not config and task_args.get('config_list', False): config = task_args.get('config_list') display.deprecated('"config_list" argument should not be used. ' 'Please use "config" argument instead') # validate config argument if isinstance(config, list): pass elif isinstance(config, dict): config = [dict( config=config, root=task_args.get('root'), state=task_args.get('state', 'present') )] else: raise WildflyError('config argument should be list or dict') # dry run argument dry_run = (task_args.get('dry_run', False) or check_mode) return cli_command, config, dry_run def wildfly_batch(prev, root, attrs, state='present', wrap=True): ''' Generate Wildfly batch to assert state of configuration item under specified _root_, given current configuration in _prev_. Arguments: - item: requested configuration tree under _root_ - root: root path - prev: previous (current) state of configuration with root=/ - state: required state: present or absent - wrap: if True, wrap script in batch ... run-batch instructions Pitfalls (TODO): - Only node (i.e. not attribute) removal is supported - For hash attribute values, only first depth is supported ''' # output batch script output = '' # check current config cur = config_query(prev, root) display.v("current config: {}".format(cur)) # consider dict attrs whose values are also dicts as sub-nodes sub_nodes = [k for k in sorted(attrs) if ( isinstance(attrs[k], dict) and all( [isinstance(attrs[k][v], dict) for v in attrs[k]] )) ] if state.lower() == 'present': if cur: # node exists, add missing attrs for k in sorted(attrs): if k not in sub_nodes and (k not in cur or cur[k] != attrs[k]): output = '{}{}:write-attribute(name={},value={})\n'.format( output, root, k, format_attr(attrs[k])) # update configuration tree cur[k] = attrs[k] else: # node doesn't exist, add it output = '{}{}:add({})\n'.format( output, root, ','.join(['{}={}'.format( k, format_attr(attrs[k])) for k in sorted(attrs) if k not in sub_nodes]) ) # config_add_node(prev, root, attrs) # add sub-nodes recursively for key in sub_nodes: for val in attrs[key]: output = output + wildfly_batch( prev, '{}/{}={}'.format(root, key, val), attrs[key][val], state, False ) if state.lower() == 'absent' and cur: # remove existing node output = '{}{}:remove()\n'.format(output, root) if len(output) > 0 and wrap: output = 'batch\n{}\nrun-batch\n'.format(output) return output def config_query(config, path): ''' Given Wildfly configuration as a dict, follow requested path and return corresponding entry. ''' path = WildflyPath(path) ptr = config while len(path) > 0: try: qry = path.lpop() ptr = ptr[qry] except (KeyError, TypeError): # either ptr is none (Type), or ptr[qry] not found (Key) return None except Exception as ex: raise WildflyError("Error looking up config") from ex return ptr def format_attr(attr): ''' Return valid text representation for attribute as understood by Wildfly CLI. ''' # None => 'undefined' if attr is None: return 'undefined' if isinstance(attr, (float)): return '{:f}'.format(attr) if isinstance(attr, (int, bool)): return str(attr) # list => '[ format(item), ... ]' if isinstance(attr, (list,tuple)): return '[' + ', '.join( ['{}'.format( 'undefined' if k_ is None else format_attr(k_)) for k_ in attr] ) + ']' # dict => '{ "key" => format(value), ... }' if isinstance(attr, dict): return '{' + ', '.join( ['"{}" => {}'.format( k_, 'undefined' if attr[k_] is None else format_attr(attr[k_]) ) for k_ in list(attr.keys())] ) + '}' # wrap strings with double quotes return '"{}"'.format(str(attr).strip('"')) class WildflyPath: ''' Utility class for handling Wildfly configuration paths. Converts a path string into an array of components (parts). Supports whitespace around separator characters '=' and '/' and escaping them with backslashes. ''' def __init__(self, path): if isinstance(path, string_types): self.parts = [ i.replace('\\', '').strip() for i in re.split(r'((?