diff --git a/wildfly.py b/wildfly.py index 6fc9c6f..4b73007 100644 --- a/wildfly.py +++ b/wildfly.py @@ -10,11 +10,18 @@ __metaclass__ = type from ansible.plugins.action import ActionBase from ansible.utils.display import Display +from ansible.utils.vars import merge_hash display = Display() import json +import re from six import string_types + +class WildflyError(Exception): + pass + + class ActionModule(ActionBase): def run(self, tmp=None, task_vars=None): @@ -24,26 +31,6 @@ class ActionModule(ActionBase): self._supports_check_mode = True self._supports_async = False - # module_args = self._task.args.copy() - # module_return = self._execute_module(module_name='setup', - # module_args=module_args, - # task_vars=task_vars, tmp=tmp) - # ret = dict() - # remote_date = None - # if not module_return.get('failed'): - # for key, value in module_return['ansible_facts'].items(): - # if key == 'ansible_date_time': - # remote_date = value['iso8601'] - - # if remote_date: - # remote_date_obj = datetime.strptime(remote_date, '%Y-%m-%dT%H:%M:%SZ') - # time_delta = datetime.now() - remote_date_obj - # ret['delta_seconds'] = time_delta.seconds - # ret['delta_days'] = time_delta.days - # ret['delta_microseconds'] = time_delta.microseconds - - # return dict(ansible_facts=dict(ret)) - task_args = self._task.args.copy() check_mode = self._play_context.check_mode cli_command, config, dry_run = read_args(task_args, check_mode) @@ -63,10 +50,46 @@ class ActionModule(ActionBase): except Exception as e: return merge_hash(cur_result, dict( failed=True, - msg="Error parsing JSON from Wildfly CLI: {}".format(str(e)) + msg='Error parsing JSON from Wildfly CLI: {}'.format(str(e)) )) - return cur_result + # 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 e: + return merge_hash(cur_result, dict( + failed=True, + msg='Error while generating Wildfly batch: {}'.format(str(e)), + stdout=None, + stdout_lines=[], + config_item=item, + batch_script=batch_script + )) + + result = dict(changed=False,batch_script=batch_script) + + # apply changes + if batch_script: + batch_script = 'batch\n{}run-batch\n'.format(batch_script) + result = dict(changed=True,batch_script=batch_script) + + if not dry_run: + 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): ''' @@ -111,9 +134,179 @@ def read_args(task_args, check_mode): state=task_args.get('state', 'present') )] else: - raise Exception("config argument should be list or dict") + 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 = wildfly_config_query(prev, root) + + display.v("current config: {}".format(cur)) + + if state.lower() == 'present': + + if cur: + # node exists, add missing attrs + for k in 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 attrs]) + ) + wildfly_config_add_node(prev, root, attrs) + + if state.lower() == 'absent' and cur: + # remove existing node + output = '{}{}:remove()\n'.format(output, root) + + if len(output) and wrap: + output = 'batch\n{}\nrun-batch\n'.format(output) + + return output + + +def wildfly_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): + 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 e: + raise WildflyError( "config query error: {}".format(str(e))) + return ptr + + +def wildfly_config_add_node(config, path, node): + ''' + Given Wildfly configuration as a dict, follow + requested path and add given node. + + ''' + path = WildflyPath(path) + ptr = config + while len(path): + try: + qry = path.lpop() + ptr = ptr[qry] + except TypeError: + ptr = dict() + except KeyError: + ptr[qry] = dict() + except Exception as e: + raise Exception( "unknown error: {}".format(str(e))) + ptr = merge_hash(ptr,node) + + +def format_attr(attr): + ''' + Return valid text representation for attribute as understood by + Wildfly CLI. + + ''' + + # None => 'undefined' + if attr is None: + return 'undefined' + + # list => '[ format(item), ... ]' + if isinstance(attr,list): + 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'((?