#!/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( '{} --output-json --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 Exception 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 Exception 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( '{} --output-json'.format(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_command = task_args.get('cli_cmd', 'jboss-cli.sh --connect') # 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)) if state.lower() == 'present': if cur: # node exists, add missing attrs for k in sorted(attrs): if 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)]) ) # config_add_node(prev, root, attrs) 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("config query error: {}".format(str(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'((?