reconstructed/inventory_plugins/reconstructed.py

908 lines
35 KiB
Python
Raw Normal View History

import abc
from collections import MutableMapping
2022-09-02 18:00:22 +02:00
from ansible import constants as C
from ansible.errors import AnsibleParserError, AnsibleRuntimeError, AnsibleError
from ansible.module_utils.six import string_types
from ansible.module_utils.parsing.convert_bool import boolean
from ansible.utils.vars import isidentifier
from ansible.plugins.inventory import BaseInventoryPlugin
DOCUMENTATION = """
name: reconstructed
short_description: A plugin that allows the dynamic construction of groups
author: Emmanuel BENOÎT
description:
- This inventory plugin allows the construction of groups, the optional
assignment of hosts to these groups and the computation of arbitrary
facts.
options:
plugin:
description:
- Token that ensures this is a source file for the C(group_creator)
plugin.
required: True
choices: ['reconstructed']
instructions:
description:
- The list of instructions to be executed in order to generate the
inventory parts. Each instruction is represented as a dictionnary
with at least an C(action) field which determines which instruction
must be executed. The instructions will be executed once for each
inventory host.
- Instructions may include various fields that act as control flow.
- If the C(loop) field is present, it must contain a list (or a Jinja
template that will return a list). The instruction will be repeated
for each value in the list. The C(loop_var) field may be added to
specify the name of the variable into which the current value will
be written; by default the C(item) variable will be used. Once the
loop execution ends, the loop variable's previous state is restored.
2022-09-02 18:00:22 +02:00
- The C(when) field, if present, must contain a Jinja expression
representing a condition which will be checked before the instruction
is executed.
- The C(action) field must be set to one of the following values.
- The C(block) action is another form of flow control, which can be
used to repeat multiple instructions or make them obey a single
conditional. The instruction must include a C(block) field, containing
the list of instructions which are part of the block. In addition, it
may have a C(rescue) field, containing a list of instructions which
will be executed on error, and C(always), which may contain a list
of instructions to execute in all cases. If the C(locals) field is
defined, it must contain a table of local variables to define. Any
local variable defined by the instructions under C(block), C(rescue)
or C(always) will go out of scope once the block finishes executing,
and the previous values, if any, will be restored.
2022-09-02 18:00:22 +02:00
- C(create_group) creates a group. The name of the group must be
provided using the C(group) field, which must be a valid name or a
Jinja template that evaluates to a valid name. In addition, a
C(parent) field containting the name of a single, existing parent
group (or a Jinja template generating the name) may be provided.
Finally, the C(add_host) field may be set to a truthy value if the
current host must be added to the new group.
2022-09-02 18:00:22 +02:00
- C(add_child) adds a child group to another group. The name of the
group being added must be provided in the C(child) entry, while
the name of the parent must be provided in the C(group) entry. Both
groups must exist. In addition, the names may be specified using
Jinja templates.
- C(add_host) adds the current inventory host to a group. The name
of the group must be provided in the C(group) entry. The group
must exist.
- C(fail) causes the computations for the current host to stop with
an error. The error message may be specified in the C(message)
entry; if present, it will be evaluated using Jinja.
- C(set_fact) and C(set_var) create a fact and a local variable,
respectively. Local variables will only be kept during the execution
of the script for the current host, while facts will be added to the
host's data. The C(name) entry specifies the name of the fact or
variable while the C(value) entry specifies its value. Both may be
Jinja templates.
- C(stop) stops processing the list of instructions for the current
host.
type: list
elements: dict
required: True
strictness:
description:
- The C(host) setting will cause an error to skip the host being
processed, and the C(full) setting will abort the execution
altogether.
required: False
choices: ['host', 'full']
default: host
"""
INSTR_COMMON_FIELDS = ("when", "loop", "loop_var", "action")
"""Fields that may be present on all instructions."""
INSTR_OWN_FIELDS = {
"create_group": ("group", "parent", "add_host"),
"add_host": ("group",),
"add_child": ("group", "child"),
"set_var": ("name", "value"),
"set_fact": ("name", "value"),
"stop": (),
"fail": ("msg",),
"block": ("block", "rescue", "always", "locals"),
}
"""Fields that are specific to each instruction."""
INSTR_FIELDS = {k: set(v + INSTR_COMMON_FIELDS) for k, v in INSTR_OWN_FIELDS.items()}
"""All supported fields for each instruction, including common and specific fields."""
2022-09-02 18:00:22 +02:00
class VariableStorage(MutableMapping):
def __init__(self, host_vars):
self._host_vars = host_vars
self._script_vars = {}
self._script_stack = []
self._cache = host_vars.copy()
def _script_stack_push(self, variables):
data = {}
for v in variables:
if v in self._script_vars:
se = (True, self._script_vars[v].copy())
else:
se = (False, None)
data[v] = se
self._script_stack.append(data)
def _script_stack_pop(self):
restore = self._script_stack.pop()
for vn, vv in restore.items():
existed, value = vv
if existed:
self._script_vars[vn] = value
elif vn in self._script_vars:
del self._script_vars[vn]
self._cache = self._host_vars.copy()
self._cache.update(self._script_vars)
def _set_host_var(self, name, value):
self._host_vars[name] = value
if name not in self._script_vars:
self._cache[name] = value
def __getitem__(self, k):
return self._cache[k]
def __setitem__(self, k, v):
self._script_vars[k] = v
self._cache[k] = v
def __delitem__(self, k):
del self._script_vars[k]
if k in self._host_vars:
self._cache[k] = self._host_vars[k]
else:
del self._cache[k]
def __iter__(self):
return self._cache.__iter__()
def __len__(self):
return len(self._cache)
def keys(self):
return self._cache.keys()
def items(self):
return self._cache.items()
def values(self):
return self._cache.values()
class RcInstruction(abc.ABC):
2022-09-02 18:00:22 +02:00
"""An instruction that can be executed by the plugin."""
DEFAULT_LOOP_VAR = "item"
2022-09-04 23:44:02 +02:00
"""The name of the default loop variable."""
2022-09-02 18:00:22 +02:00
def __init__(self, inventory, templar, display, action):
2022-09-02 18:00:22 +02:00
self._inventory = inventory
self._templar = templar
self._display = display
2022-09-02 18:00:22 +02:00
self._condition = None
self._loop = None
self._loop_var = None
self._action = action
def __repr__(self):
2022-09-04 23:44:02 +02:00
"""Builds a compact debugging representation of the instruction, \
including any conditional or iteration clause."""
flow = []
if self._condition is not None:
2022-09-04 12:21:09 +02:00
flow.append("when=%s" % (repr(self._condition),))
if self._loop is not None:
flow.append(
"loop=%s, loop_var=%s" % (repr(self._loop), repr(self._loop_var))
)
if flow:
output = "{%s}" % (", ".join(flow),)
else:
output = ""
output += self.repr_instruction_only()
return output
def repr_instruction_only(self):
2022-09-04 23:44:02 +02:00
"""Builds a compact debugging representation of the instruction itself."""
return "%s()" % (self._action,)
def dump(self):
2022-09-04 23:44:02 +02:00
"""Builds a representation of the instruction over multiple lines.
This method generates a representation of the instruction, including
any conditional or iteration clause, over multiple lines. It is meant
to be used when generating a dump of the parsed program for high
verbosity values.
Returns:
a list of strings (one for each line)
"""
output = []
if self._condition is not None:
output.append("{when: %s}" % (repr(self._condition),))
if self._loop is not None:
output.append("{loop[%s]: %s}" % (self._loop_var, repr(self._loop)))
output.extend(self.dump_instruction())
return output
def dump_instruction(self):
2022-09-04 23:44:02 +02:00
"""Builds the multi-line debugging representation of the instruction.
This method returns a list of strings that correspond to the output
lines that represent the instruction.
Returns:
a list of strings (one for each line)
"""
return [self.repr_instruction_only()]
2022-09-02 18:00:22 +02:00
def parse(self, record):
2022-09-04 23:44:02 +02:00
"""Parse the instruction's record.
This method ensures that no unsupported fields are present in the
instruction's record. It then extracts the conditional clause and the
iteration clause, if they are present. Finally it calls ``parse_action``
in order to extract the instruction itself.
Args:
record: the dictionnary that contains the instruction
"""
2022-09-02 18:00:22 +02:00
assert "action" in record and record["action"] == self._action
# Ensure there are no unsupported fields
extra_fields = set(record.keys()).difference(INSTR_FIELDS[self._action])
2022-09-02 18:00:22 +02:00
if extra_fields:
raise AnsibleParserError(
"%s: unsupported fields: %s" % (self._action, ", ".join(extra_fields))
)
# Extract the condition
if "when" in record:
if not isinstance(record["when"], string_types):
raise AnsibleParserError(
"%s: 'when' clause is not a string" % (self._action,)
)
self._condition = record["when"]
# Extract the loop data and configuration
if "loop" in record:
loop = record["loop"]
if not isinstance(loop, string_types + (list,)):
raise AnsibleParserError(
"%s: 'loop' clause is neither a string nor a list" % (self._action,)
)
loop_var = record.get("loop_var", RcInstruction.DEFAULT_LOOP_VAR)
if not isinstance(loop_var, string_types):
raise AnsibleParserError(
"%s: 'loop_var' clause is not a string" % (self._action,)
)
if not isidentifier(loop_var):
raise AnsibleParserError(
"%s: 'loop_var' value '%s' is not a valid identifier"
% (self._action, loop_var)
)
self._loop = loop
self._loop_var = loop_var
elif "loop_var" in record:
raise AnsibleParserError(
"%s: 'loop_var' clause found without 'loop'" % (self._action,)
)
# Process action-specific fields
self.parse_action(record)
def parse_group_name(self, record, name):
2022-09-04 23:44:02 +02:00
"""Parse a field containing the name of a group, or a template.
This helper method may be used by implementations to extract either a
group name or a template from a field. If the string cannot possibly be
a Jinja template, it will be stripped of extra spaces then checked for
invalid characters.
Args:
record: the dictionnary that contains the instruction
name: the name of the field to read
Returns:
a tuple consisting of a boolean that indicates whether the string
may be a template or not, and the string itself.
"""
2022-09-02 18:00:22 +02:00
if name not in record:
raise AnsibleParserError("%s: missing '%s' field" % (self._action, name))
group = record[name]
if not isinstance(group, string_types):
raise AnsibleParserError(
"%s: '%s' field must be a string" % (self._action, name)
)
may_be_template = self._templar.is_possibly_template(group)
if not may_be_template:
group = group.strip()
if C.INVALID_VARIABLE_NAMES.findall(group):
raise AnsibleParserError(
"%s: invalid group name '%s' in field '%s'"
% (self._action, group, name)
)
return may_be_template, group
@abc.abstractmethod
2022-09-02 18:00:22 +02:00
def parse_action(self, record):
2022-09-04 23:44:02 +02:00
"""Parse the instruction-specific fields.
This method must be overridden to read all necessary data from the input
record and configure the instruction.
Args:
record: the dictionnary that contains the instruction
"""
2022-09-02 18:00:22 +02:00
raise NotImplementedError
def run_for(self, host_name, variables):
2022-09-04 23:44:02 +02:00
"""Execute the instruction for a given host.
This method is the entry point for instruction execution. Depending on
whether an iteration clause is present or not, it will either call
``run_once()`` directly or evaluate the loop data then run it once for
each item, after setting the loop variable.
Args:
host_name: the name of the host to execute the instruction for
variables: the variable storage instance
2022-09-04 23:44:02 +02:00
Returns:
``True`` if execution must continue, ``False`` if it must be
interrupted
"""
2022-09-02 18:00:22 +02:00
if self._loop is None:
self._display.vvvv("%s : running action %s" % (host_name, self._action))
return self.run_once(host_name, variables)
# Save previous loop variable state
variables._script_stack_push([self._loop_var])
try:
# Loop over all values
for value in self.evaluate_loop(host_name, variables):
self._display.vvvv(
"%s : running action %s for item %s"
% (host_name, self._action, repr(value))
)
variables[self._loop_var] = value
if not self.run_once(host_name, variables):
return False
return True
finally:
# Restore loop variable state
variables._script_stack_pop()
2022-09-02 18:00:22 +02:00
def run_once(self, host_name, variables):
2022-09-04 23:44:02 +02:00
"""Check the condition if it exists, then run the instruction.
Args:
host_name: the name of the host to execute the instruction for
variables: the variable storage instance
2022-09-04 23:44:02 +02:00
Returns:
``True`` if execution must continue, ``False`` if it must be
interrupted
"""
if self.evaluate_condition(host_name, variables):
rv = self.execute_action(host_name, variables)
if not rv:
self._display.vvvvv(
"%s : action %s returned False, stopping"
% (host_name, self._action)
)
2022-09-02 18:00:22 +02:00
else:
rv = True
return rv
2022-09-02 18:00:22 +02:00
def evaluate_condition(self, host_name, variables):
2022-09-04 23:44:02 +02:00
"""Evaluate the condition for an instruction's execution.
Args:
host_name: the name of the host to execute the instruction for
variables: the variable storage instance
2022-09-04 23:44:02 +02:00
Returns:
``True`` if there is no conditional clause for this instruction, or
if there is one and it evaluated to a truthy value; ``False``
otherwise.
"""
2022-09-02 18:00:22 +02:00
if self._condition is None:
return True
t = self._templar
t.available_variables = variables
template = "%s%s%s" % (
t.environment.variable_start_string,
self._condition,
t.environment.variable_end_string,
)
rv = boolean(t.template(template, disable_lookups=False))
self._display.vvvvv(
"host %s, action %s, condition %s evaluating to %s"
% (host_name, self._action, repr(self._condition), repr(rv))
)
return rv
2022-09-02 18:00:22 +02:00
def evaluate_loop(self, host_name, variables):
2022-09-04 23:44:02 +02:00
"""Evaluate the values to iterate over when a ``loop`` is defined.
Args:
host_name: the name of the host to execute the instruction for
variables: the variable storage instance
2022-09-04 23:44:02 +02:00
Returns:
the list of items to iterate over
"""
self._display.vvvvv(
"host %s, action %s, evaluating loop template %s"
% (host_name, self._action, repr(self._loop))
)
2022-09-02 18:00:22 +02:00
self._templar.available_variables = variables
value = self._templar.template(self._loop, disable_lookups=False)
if not isinstance(value, list):
raise AnsibleRuntimeError(
"template '%s' did not evaluate to a list" % (self._loop,)
)
return value
def get_templated_group(self, variables, may_be_template, name, must_exist=False):
2022-09-04 23:44:02 +02:00
"""Extract a group name from its source, optionally ensure it exists, \
then return it.
Args:
variables: the variable storage instance
2022-09-04 23:44:02 +02:00
may_be_template: a flag that indicates whether the name should be \
processed with the templar.
name: the name or its template
must_exist: a flag that, if ``True``, will cause an exception to \
be raised if the group does not exist.
Returns:
the name of the group
"""
2022-09-02 18:00:22 +02:00
if may_be_template:
self._templar.available_variables = variables
real_name = self._templar.template(name)
if not isinstance(name, string_types):
raise AnsibleRuntimeError(
"%s: '%s' did not coalesce into a string" % (self._action, name)
)
real_name = real_name.strip()
if C.INVALID_VARIABLE_NAMES.findall(real_name):
raise AnsibleRuntimeError(
"%s: '%s' is not a valid group name" % (self._action, real_name)
)
else:
real_name = name
if must_exist and real_name not in self._inventory.groups:
raise AnsibleRuntimeError(
"%s: group '%s' does not exist" % (self._action, real_name)
)
return real_name
@abc.abstractmethod
def execute_action(self, host_name, variables):
2022-09-04 23:44:02 +02:00
"""Execute the instruction.
This method must be overridden to implement the actual action of the
instruction.
Args:
host_name: the name of the host to execute the instruction for
merged_vars: the variable cache, with local script variables \
taking precedence over host facts.
host_vars: the host's facts, as a mapping
script_vars: the current script variables, as a mapping
Return:
``True`` if the script's execution should continue, ``False`` if
it should be interrupted.
"""
raise NotImplementedError
2022-09-02 18:00:22 +02:00
class RciCreateGroup(RcInstruction):
def __init__(self, inventory, templar, display):
super().__init__(inventory, templar, display, "create_group")
self._group_mbt = None
self._group_name = None
self._parent_mbt = None
self._parent_name = None
self._add_host = None
2022-09-02 18:00:22 +02:00
def repr_instruction_only(self):
output = "%s(group=%s" % (self._action, repr(self._group_name))
if self._parent_name is not None:
output += ",parent=" + repr(self._parent_name)
output += ",add_host=" + repr(self._add_host) + ")"
return output
2022-09-02 18:00:22 +02:00
def parse_action(self, record):
assert self._group_mbt is None and self._group_name is None
assert self._parent_mbt is None and self._parent_name is None
assert self._add_host is None
self._add_host = record.get("add_host", False)
self._group_mbt, self._group_name = self.parse_group_name(record, "group")
if "parent" in record:
self._parent_mbt, self._parent_name = self.parse_group_name(
record, "parent"
)
2022-09-02 18:00:22 +02:00
def execute_action(self, host_name, variables):
assert not (
self._group_mbt is None
or self._group_name is None
or self._add_host is None
)
if self._parent_name is not None:
parent = self.get_templated_group(
variables, self._parent_mbt, self._parent_name, must_exist=True
)
name = self.get_templated_group(variables, self._group_mbt, self._group_name)
2022-09-02 18:00:22 +02:00
self._inventory.add_group(name)
self._display.vvv("- created group %s" % (name,))
if self._parent_name is not None:
self._inventory.add_child(parent, name)
self._display.vvv("- added group %s to %s" % (name, parent))
if self._add_host:
self._inventory.add_child(name, host_name)
self._display.vvv("- added host %s to %s" % (host_name, name))
2022-09-02 18:00:22 +02:00
return True
class RciAddHost(RcInstruction):
def __init__(self, inventory, templar, display):
super().__init__(inventory, templar, display, "add_host")
2022-09-02 18:00:22 +02:00
self._may_be_template = None
self._group = None
def repr_instruction_only(self):
return "%s(group=%s)" % (self._action, repr(self._group))
2022-09-02 18:00:22 +02:00
def parse_action(self, record):
assert self._may_be_template is None and self._group is None
self._may_be_template, self._group = self.parse_group_name(record, "group")
def execute_action(self, host_name, variables):
2022-09-02 18:00:22 +02:00
assert not (self._may_be_template is None or self._group is None)
name = self.get_templated_group(
variables, self._may_be_template, self._group, must_exist=True
2022-09-02 18:00:22 +02:00
)
self._inventory.add_child(name, host_name)
self._display.vvv("- added host %s to %s" % (host_name, name))
2022-09-02 18:00:22 +02:00
return True
class RciAddChild(RcInstruction):
def __init__(self, inventory, templar, display):
super().__init__(inventory, templar, display, "add_child")
2022-09-02 18:00:22 +02:00
self._group_mbt = None
self._group_name = None
self._child_mbt = None
self._child_name = None
def repr_instruction_only(self):
return "%s(group=%s, child=%s)" % (
self._action,
repr(self._group_name),
repr(self._child_name),
)
2022-09-02 18:00:22 +02:00
def parse_action(self, record):
assert self._group_mbt is None and self._group_name is None
assert self._child_mbt is None and self._child_name is None
self._group_mbt, self._group_name = self.parse_group_name(record, "group")
self._child_mbt, self._child_name = self.parse_group_name(record, "child")
def execute_action(self, host_name, variables):
2022-09-02 18:00:22 +02:00
assert not (self._group_mbt is None or self._group_name is None)
assert not (self._child_mbt is None or self._child_name is None)
group = self.get_templated_group(
variables, self._group_mbt, self._group_name, must_exist=True
2022-09-02 18:00:22 +02:00
)
child = self.get_templated_group(
variables, self._child_mbt, self._child_name, must_exist=True
2022-09-02 18:00:22 +02:00
)
self._inventory.add_child(group, child)
self._display.vvv("- added group %s to %s" % (child, group))
2022-09-02 18:00:22 +02:00
return True
class RciSetVarOrFact(RcInstruction):
def __init__(self, inventory, templar, display, is_fact):
2022-09-02 18:00:22 +02:00
action = "set_" + ("fact" if is_fact else "var")
super().__init__(inventory, templar, display, action)
2022-09-02 18:00:22 +02:00
self._is_fact = is_fact
self._var_name = None
self._name_may_be_template = None
self._var_value = None
def repr_instruction_only(self):
return "%s(name=%s, value=%s)" % (
self._action,
repr(self._var_name),
repr(self._var_value),
)
2022-09-02 18:00:22 +02:00
def parse_action(self, record):
assert (
self._var_name is None
and self._name_may_be_template is None
and self._var_value is None
)
if "name" not in record:
raise AnsibleParserError("%s: missing 'name' field" % (self._action,))
name = record["name"]
if not isinstance(name, string_types):
raise AnsibleParserError("%s: 'name' must be a string" % (self._action,))
if "value" not in record:
raise AnsibleParserError("%s: missing 'value' field" % (self._action,))
nmbt = self._templar.is_possibly_template(name)
if not (nmbt or isidentifier(name)):
raise AnsibleParserError(
"%s: '%s' is not a valid variable name" % (self._action, name)
)
self._name_may_be_template = nmbt
self._var_name = name
self._var_value = record["value"]
def execute_action(self, host_name, variables):
2022-09-02 18:00:22 +02:00
assert not (
self._var_name is None
or self._name_may_be_template is None
or self._var_value is None
)
self._templar.available_variables = variables
2022-09-02 18:00:22 +02:00
if self._name_may_be_template:
name = self._templar.template(self._var_name)
if not isinstance(name, string_types):
raise AnsibleRuntimeError(
"%s: '%s' did not coalesce into a string"
% (self._action, self._var_name)
)
if not isidentifier(name):
raise AnsibleRuntimeError(
"%s: '%s' is not a valid variable name" % (self._action, name)
)
else:
name = self._var_name
value = self._templar.template(self._var_value)
if self._is_fact:
self._inventory.set_variable(host_name, name, value)
variables._set_host_var(name, value)
2022-09-02 18:00:22 +02:00
else:
variables[name] = value
self._display.vvv(
"- set %s %s to %s"
% ("fact" if self._is_fact else "var", name, repr(value))
)
2022-09-02 18:00:22 +02:00
return True
class RciStop(RcInstruction):
def __init__(self, inventory, templar, display):
super().__init__(inventory, templar, display, "stop")
2022-09-02 18:00:22 +02:00
def parse_action(self, record):
pass
def execute_action(self, host_name, variables):
self._display.vvv("- stopped execution")
2022-09-02 18:00:22 +02:00
return False
class RciFail(RcInstruction):
def __init__(self, inventory, templar, display):
super().__init__(inventory, templar, display, "fail")
2022-09-02 18:00:22 +02:00
self._message = None
def repr_instruction_only(self):
if self._message is None:
return "%s()" % (self._action,)
else:
return "%s(%s)" % (self._action, self._message)
2022-09-02 18:00:22 +02:00
def parse_action(self, record):
self._message = record.get("msg", None)
def execute_action(self, host_name, variables):
2022-09-02 18:00:22 +02:00
if self._message is None:
message = "fail requested (%s)" % (host_name,)
else:
self._templar.available_variables = variables
2022-09-02 18:00:22 +02:00
message = self._templar.template(self._message)
self._display.vvv("- failed with message %s" % (message,))
2022-09-02 18:00:22 +02:00
raise AnsibleRuntimeError(message)
class RciBlock(RcInstruction):
def __init__(self, inventory, templar, display):
super().__init__(inventory, templar, display, "block")
self._block = None
self._rescue = None
self._always = None
self._locals = None
def repr_instruction_only(self):
return "%s(block=%s, rescue=%s, always=%s, locals=%s)" % (
self._action,
repr(self._block),
repr(self._rescue),
repr(self._always),
repr(self._locals),
)
def dump_instruction(self):
output = ["%s(...):" % (self._action,)]
self.dump_block(output, "block", self._block)
self.dump_block(output, "rescue", self._rescue)
self.dump_block(output, "always", self._always)
if self._locals:
output.append(" locals:")
for k, v in self._locals.items():
output.append(" " + repr(k) + "=" + repr(v))
return output
def dump_block(self, output, block_name, block_contents):
if not block_contents:
return
output.append(" " + block_name + ":")
for pos, instr in enumerate(block_contents):
if pos != 0:
output.append("")
output.extend(" " + s for s in instr.dump())
def parse_action(self, record):
assert (
self._block is None
and self._rescue is None
and self._always is None
and self._locals is None
)
if "block" not in record:
raise AnsibleParserError("%s: missing 'block' field" % (self._action,))
self._block = self.parse_block(record, "block")
if "rescue" in record:
self._rescue = self.parse_block(record, "rescue")
else:
self._rescue = []
if "always" in record:
self._always = self.parse_block(record, "always")
else:
self._always = []
if "locals" in record:
if not isinstance(record["locals"], dict):
raise AnsibleParserError(
"%s: 'locals' should be a dictionnary" % (self._action,)
)
for k, v in record["locals"].items():
if not isinstance(k, string_types):
raise AnsibleParserError(
"%s: locals identifiers must be strings" % (self._action,)
)
if not isidentifier(k):
raise AnsibleParserError(
"%s: '%s' is not a valid identifier" % (self._action, k)
)
self._locals = record["locals"]
else:
self._locals = {}
def parse_block(self, record, key):
if not isinstance(record[key], list):
raise AnsibleParserError(
"%s: '%s' field must contain a list of instructions"
% (self._action, key)
)
instructions = []
for record in record[key]:
instructions.append(
parse_instruction(self._inventory, self._templar, self._display, record)
)
return instructions
def execute_action(self, host_name, variables):
assert not (
self._block is None
or self._rescue is None
or self._always is None
or self._locals is None
)
variables._script_stack_push(self._locals.keys())
try:
self._templar.available_variables = variables
for key, value in self._locals.items():
result = self._templar.template(value)
variables[key] = result
self._display.vvv("- set block-local %s to %s" % (key, result))
try:
try:
self._display.vvv("- running 'block' instructions")
return self.run_block(self._block, host_name, variables)
except AnsibleError as e:
if not self._rescue:
self._display.vvv("- block failed")
raise
self._display.vvv("- block failed, running 'rescue' instructions")
variables["reconstructed_error"] = str(e)
return self.run_block(self._rescue, host_name, variables)
finally:
self._display.vvv("- block exited, running 'always' instructions")
self.run_block(self._always, host_name, variables)
finally:
variables._script_stack_pop()
def run_block(self, block, host_name, variables):
for instruction in block:
if not instruction.run_for(host_name, variables):
return False
return True
2022-09-02 18:00:22 +02:00
INSTRUCTIONS = {
"add_child": RciAddChild,
"add_host": RciAddHost,
"block": RciBlock,
2022-09-02 18:00:22 +02:00
"create_group": RciCreateGroup,
"fail": RciFail,
"set_fact": lambda i, t, d: RciSetVarOrFact(i, t, d, True),
"set_var": lambda i, t, d: RciSetVarOrFact(i, t, d, False),
2022-09-02 18:00:22 +02:00
"stop": RciStop,
}
def parse_instruction(inventory, templar, display, record):
action = record["action"]
if action not in INSTRUCTIONS:
raise AnsibleParserError("Unknown action '%s'" % (action,))
instruction = INSTRUCTIONS[action](inventory, templar, display)
instruction.parse(record)
return instruction
2022-09-02 18:00:22 +02:00
class InventoryModule(BaseInventoryPlugin):
"""Constructs groups based on lists of instructions."""
NAME = "reconstructed"
def verify_file(self, path):
return super().verify_file(path) and path.endswith((".yaml", ".yml"))
def parse(self, inventory, loader, path, cache=True):
super().parse(inventory, loader, path, cache)
self._read_config_data(path)
instr_src = self.get_option("instructions")
instructions = []
for record in instr_src:
instructions.append(
parse_instruction(self.inventory, self.templar, self.display, record)
)
self.dump_program(instructions)
2022-09-02 18:00:22 +02:00
for host in inventory.hosts:
self.display.vvv("executing reconstructed script for %s" % (host,))
2022-09-02 18:00:22 +02:00
try:
self.exec_for_host(host, instructions)
except AnsibleError as e:
if self.get_option("strictness") == "full":
raise
self.display.warning(
"reconstructed - error on host %s: %s" % (host, repr(e))
)
def exec_for_host(self, host, instructions):
host_vars = self.inventory.get_host(host).get_vars()
variables = VariableStorage(host_vars)
2022-09-02 18:00:22 +02:00
for instruction in instructions:
if not instruction.run_for(host, variables):
2022-09-02 18:00:22 +02:00
return
def dump_program(self, instructions):
if self.display.verbosity < 4:
if self.display.verbosity == 3:
self.display.vvv("parsed program: " + repr(instructions))
return
output = []
for pos, instr in enumerate(instructions):
if pos:
output.append("")
output.extend(instr.dump())
self.display.vvvv("parsed program:\n\n" + "\n".join(" " + s for s in output))