327 lines
9.3 KiB
Python

#!/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 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(
'{} --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))
# 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'((?<!\\)[=/])', path)
]
elif isinstance(path, WildflyPath):
self.parts = path.parts
elif isinstance(path, (list, tuple)):
self.parts = [
i.replace('\\', '').strip()
for i in path
]
else:
raise WildflyError('Bad path assignment')
def __str__(self):
return ''.join([
(i if i in ('', '/', '=') else i.replace('/', '\\/'))
for i in self.parts
])
def __len__(self):
return sum([1 for i in self.parts if i not in ('', '=', '/')])
def lpop(self):
'''
remove first path component and return it
'''
ret = self.parts.pop(0)
while ret in ('', '/', '='):
ret = self.parts.pop(0)
return ret