"block" instruction implemented

The "block" instruction allows mulitple instructions to be grouped in
order to be executed based on a single condition, in a common loop, with
a local variable scope. In addition, it provides a way to recover from
errors.
This commit is contained in:
Emmanuel BENOîT 2022-09-02 20:26:36 +02:00
parent d658089183
commit 5f719d7ab8
4 changed files with 174 additions and 47 deletions

View file

@ -106,6 +106,8 @@ skipping.
| | | | |--vm01 | | | | |--vm01
| | | | |--vm05 | | | | |--vm05
| | | | |--vm06 | | | | |--vm06
|--@reedmably_evil:
| |--evil-but-nicer-vm
|--@ungrouped: |--@ungrouped:
| |--evil-vm | |--evil-vm
| |--localhost | |--localhost

View file

@ -6,6 +6,12 @@ all:
# All of this should obviously come from some other inventory plugin. # All of this should obviously come from some other inventory plugin.
evil-vm: evil-vm:
inv__data:
network: death
service: evil
instance: chaos
unredeemable: true
evil-but-nicer-vm:
inv__data: inv__data:
network: death network: death
service: evil service: evil

View file

@ -14,16 +14,31 @@ instructions:
action: stop action: stop
# Fail when the host name starts with "evil". # Fail when the host name starts with "evil".
- action: block
block:
- when: inventory_hostname.startswith( 'evil' ) - when: inventory_hostname.startswith( 'evil' )
action: fail action: fail
msg: "{{ inventory_hostname }} is obviously evil, skipping." msg: "{{ inventory_hostname }} is obviously evil, skipping."
rescue:
# Do not crash on redeemably evil VMs, but still skip them.
- when: inv__data.unredeemable is defined
action: fail
msg: "{{ reconstructed_error }}"
- action: create_group
group: reedmably_evil
- action: add_host
group: reedmably_evil
- action: stop
# Only create the managed groups if we *have* managed hosts # Only create the managed groups if we *have* managed hosts
- loop: [managed, by_environment, by_network, by_failover_stack, by_service] - action: create_group
action: create_group group: managed
group: "{{ item }}"
- loop: [by_environment, by_network, by_failover_stack, by_service] - loop: [by_environment, by_network, by_failover_stack, by_service]
action: add_child action: block
block:
- action: create_group
group: "{{ item }}"
- action: add_child
group: managed group: managed
child: "{{ item }}" child: "{{ item }}"
@ -98,32 +113,28 @@ instructions:
# Component group. We add the host directly if there is no subcomponent. # Component group. We add the host directly if there is no subcomponent.
- when: inv__component is defined - when: inv__component is defined
action: set_var action: block
name: comp_group locals:
value: "svcm_{{ inv__service }}_{{ inv__component }}" comp_group: "svcm_{{ inv__service }}_{{ inv__component }}"
- when: inv__component is defined block:
action: create_group - action: create_group
group: "{{ comp_group }}" group: "{{ comp_group }}"
- when: inv__component is defined - action: add_child
action: add_child
group: "{{ service_group }}" group: "{{ service_group }}"
child: "{{ comp_group }}" child: "{{ comp_group }}"
- when: inv__component is defined and inv__subcomponent is not defined # Subcomponent group, or lack thereof.
- when: inv__subcomponent is not defined
action: add_host action: add_host
group: "{{ comp_group }}" group: "{{ comp_group }}"
- when: inv__subcomponent is defined
# Subcomponent group. action: block
- when: inv__component is defined and inv__subcomponent is defined locals:
action: set_var subcomp_group: "svcm_{{ inv__service }}_{{ inv__subcomponent }}"
name: subcomp_group block:
value: "svcm_{{ inv__service }}_{{ inv__subcomponent }}" - action: create_group
- when: inv__component is defined and inv__subcomponent is defined
action: create_group
group: "{{ subcomp_group }}" group: "{{ subcomp_group }}"
- when: inv__component is defined and inv__subcomponent is defined - action: add_child
action: add_child
group: "{{ comp_group }}" group: "{{ comp_group }}"
child: "{{ subcomp_group }}" child: "{{ subcomp_group }}"
- when: inv__component is defined and inv__subcomponent is defined - action: add_host
action: add_host
group: "{{ subcomp_group }}" group: "{{ subcomp_group }}"

View file

@ -37,6 +37,16 @@ DOCUMENTATION = """
representing a condition which will be checked before the instruction representing a condition which will be checked before the instruction
is executed. is executed.
- The C(action) field must be set to one of the following values. - 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.
- C(create_group) creates a group. The name of the group must be - 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 provided using the C(group) field, which must be a valid name or a
Jinja template that evaluates to a valid name. Jinja template that evaluates to a valid name.
@ -157,8 +167,10 @@ class RcInstruction:
if self._loop is None: if self._loop is None:
return self.run_once(host_name, merged_vars, host_vars, script_vars) return self.run_once(host_name, merged_vars, host_vars, script_vars)
loop_values = self.evaluate_loop(host_name, merged_vars) loop_values = self.evaluate_loop(host_name, merged_vars)
script_vars = script_vars.copy()
for value in loop_values: for value in loop_values:
merged_vars[self._loop_var] = value merged_vars[self._loop_var] = value
script_vars[self._loop_var] = value
if not self.run_once(host_name, merged_vars, host_vars, script_vars): if not self.run_once(host_name, merged_vars, host_vars, script_vars):
return False return False
return True return True
@ -370,9 +382,104 @@ class RciFail(RcInstruction):
raise AnsibleRuntimeError(message) raise AnsibleRuntimeError(message)
class RciBlock(RcInstruction):
def __init__(self, inventory, templar):
super().__init__(
inventory, templar, "block", ("block", "rescue", "always", "locals")
)
self._block = None
self._rescue = None
self._always = None
self._locals = None
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, record)
)
return instructions
def execute_action(self, host_name, merged_vars, host_vars, script_vars):
assert not (
self._block is None
or self._rescue is None
or self._always is None
or self._locals is None
)
merged_vars = merged_vars.copy()
script_vars = script_vars.copy()
self._templar.available_variables = merged_vars
for key, value in self._locals.items():
result = self._templar.template(value)
script_vars[key] = result
merged_vars[key] = result
try:
try:
return self.run_block(
self._block, host_name, merged_vars, host_vars, script_vars
)
except AnsibleError as e:
script_vars["reconstructed_error"] = str(e)
merged_vars["reconstructed_error"] = str(e)
return self.run_block(
self._rescue, host_name, merged_vars, host_vars, script_vars
)
finally:
self.run_block(self._always, host_name, merged_vars, host_vars, script_vars)
def run_block(self, block, host_name, merged_vars, host_vars, script_vars):
for instruction in block:
if not instruction.run_for(host_name, host_vars, script_vars):
return False
return True
INSTRUCTIONS = { INSTRUCTIONS = {
"add_child": RciAddChild, "add_child": RciAddChild,
"add_host": RciAddHost, "add_host": RciAddHost,
"block": RciBlock,
"create_group": RciCreateGroup, "create_group": RciCreateGroup,
"fail": RciFail, "fail": RciFail,
"set_fact": lambda i, t: RciSetVarOrFact(i, t, True), "set_fact": lambda i, t: RciSetVarOrFact(i, t, True),
@ -381,6 +488,15 @@ INSTRUCTIONS = {
} }
def parse_instruction(inventory, templar, record):
action = record["action"]
if action not in INSTRUCTIONS:
raise AnsibleParserError("Unknown action '%s'" % (action,))
instruction = INSTRUCTIONS[action](inventory, templar)
instruction.parse(record)
return instruction
class InventoryModule(BaseInventoryPlugin): class InventoryModule(BaseInventoryPlugin):
"""Constructs groups based on lists of instructions.""" """Constructs groups based on lists of instructions."""
@ -395,7 +511,7 @@ class InventoryModule(BaseInventoryPlugin):
instr_src = self.get_option("instructions") instr_src = self.get_option("instructions")
instructions = [] instructions = []
for record in instr_src: for record in instr_src:
instructions.append(self.get_instruction(record)) instructions.append(parse_instruction(self.inventory, self.templar, record))
for host in inventory.hosts: for host in inventory.hosts:
try: try:
self.exec_for_host(host, instructions) self.exec_for_host(host, instructions)
@ -412,11 +528,3 @@ class InventoryModule(BaseInventoryPlugin):
for instruction in instructions: for instruction in instructions:
if not instruction.run_for(host, host_vars, script_vars): if not instruction.run_for(host, host_vars, script_vars):
return return
def get_instruction(self, record):
action = record["action"]
if action not in INSTRUCTIONS:
raise AnsibleParserError("Unknown action '%s'" % (action,))
instruction = INSTRUCTIONS[action](self.inventory, self.templar)
instruction.parse(record)
return instruction