"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:
parent
d658089183
commit
5f719d7ab8
4 changed files with 174 additions and 47 deletions
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -14,18 +14,33 @@ instructions:
|
||||||
action: stop
|
action: stop
|
||||||
|
|
||||||
# Fail when the host name starts with "evil".
|
# Fail when the host name starts with "evil".
|
||||||
- when: inventory_hostname.startswith( 'evil' )
|
- action: block
|
||||||
action: fail
|
block:
|
||||||
msg: "{{ inventory_hostname }} is obviously evil, skipping."
|
- when: inventory_hostname.startswith( 'evil' )
|
||||||
|
action: fail
|
||||||
|
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: "{{ item }}"
|
|
||||||
- loop: [by_environment, by_network, by_failover_stack, by_service]
|
|
||||||
action: add_child
|
|
||||||
group: managed
|
group: managed
|
||||||
child: "{{ item }}"
|
- loop: [by_environment, by_network, by_failover_stack, by_service]
|
||||||
|
action: block
|
||||||
|
block:
|
||||||
|
- action: create_group
|
||||||
|
group: "{{ item }}"
|
||||||
|
- action: add_child
|
||||||
|
group: managed
|
||||||
|
child: "{{ item }}"
|
||||||
|
|
||||||
# Copy inv__data fields to separate inv__ variables
|
# Copy inv__data fields to separate inv__ variables
|
||||||
- loop:
|
- loop:
|
||||||
|
@ -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 }}"
|
# Subcomponent group, or lack thereof.
|
||||||
- when: inv__component is defined and inv__subcomponent is not defined
|
- 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
|
group: "{{ subcomp_group }}"
|
||||||
action: create_group
|
- action: add_child
|
||||||
group: "{{ subcomp_group }}"
|
group: "{{ comp_group }}"
|
||||||
- when: inv__component is defined and inv__subcomponent is defined
|
child: "{{ subcomp_group }}"
|
||||||
action: add_child
|
- action: add_host
|
||||||
group: "{{ comp_group }}"
|
group: "{{ subcomp_group }}"
|
||||||
child: "{{ subcomp_group }}"
|
|
||||||
- when: inv__component is defined and inv__subcomponent is defined
|
|
||||||
action: add_host
|
|
||||||
group: "{{ subcomp_group }}"
|
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
Loading…
Reference in a new issue