309 lines
8.6 KiB
Python
309 lines
8.6 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 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:
|
|
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'((?<!\\)[=/])', 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
|