Debugging support

* Support for dumping parsed programs as strings (both a basic,
    repr-like version and an indented version that is easier to read)
  * Display the parsed program if verbosity is high enough
  * Program tracing if verbosity is high enough
This commit is contained in:
Emmanuel BENOîT 2022-09-04 11:35:07 +02:00
parent 7b44cdc731
commit 5ef2ffc55c

View file

@ -93,9 +93,10 @@ class RcInstruction:
COMMON_FIELDS = ("when", "loop", "loop_var", "action") COMMON_FIELDS = ("when", "loop", "loop_var", "action")
DEFAULT_LOOP_VAR = "item" DEFAULT_LOOP_VAR = "item"
def __init__(self, inventory, templar, action, allowed_fields=()): def __init__(self, inventory, templar, display, action, allowed_fields=()):
self._inventory = inventory self._inventory = inventory
self._templar = templar self._templar = templar
self._display = display
self._condition = None self._condition = None
self._loop = None self._loop = None
self._loop_var = None self._loop_var = None
@ -103,6 +104,43 @@ class RcInstruction:
self._allowed_fields = set(allowed_fields) self._allowed_fields = set(allowed_fields)
self._allowed_fields.update(RcInstruction.COMMON_FIELDS) self._allowed_fields.update(RcInstruction.COMMON_FIELDS)
def __repr__(self):
flow = []
if self._condition is not None:
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):
return "%s()" % (self._action,)
def dump(self):
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):
return [self.repr_instruction_only()]
def parse(self, record): def parse(self, record):
assert "action" in record and record["action"] == self._action assert "action" in record and record["action"] == self._action
# Ensure there are no unsupported fields # Ensure there are no unsupported fields
@ -169,10 +207,15 @@ class RcInstruction:
merged_vars = host_vars.copy() merged_vars = host_vars.copy()
merged_vars.update(script_vars) merged_vars.update(script_vars)
if self._loop is None: if self._loop is None:
self._display.vvvv("%s : running action %s" % (host_name, self._action))
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() script_vars = script_vars.copy()
for value in loop_values: for value in loop_values:
self._display.vvvv(
"%s : running action %s for item %s"
% (host_name, self._action, repr(value))
)
merged_vars[self._loop_var] = value merged_vars[self._loop_var] = value
script_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):
@ -181,9 +224,15 @@ class RcInstruction:
def run_once(self, host_name, merged_vars, host_vars, script_vars): def run_once(self, host_name, merged_vars, host_vars, script_vars):
if self.evaluate_condition(host_name, merged_vars): if self.evaluate_condition(host_name, merged_vars):
return self.execute_action(host_name, merged_vars, host_vars, script_vars) rv = self.execute_action(host_name, merged_vars, host_vars, script_vars)
if not rv:
self._display.vvvvv(
"%s : action %s returned False, stopping"
% (host_name, self._action)
)
else: else:
return True rv = True
return rv
def evaluate_condition(self, host_name, variables): def evaluate_condition(self, host_name, variables):
if self._condition is None: if self._condition is None:
@ -195,12 +244,21 @@ class RcInstruction:
self._condition, self._condition,
t.environment.variable_end_string, t.environment.variable_end_string,
) )
return boolean(t.template(template, disable_lookups=False)) 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
def evaluate_loop(self, host_name, variables): def evaluate_loop(self, host_name, variables):
if isinstance(self._loop, list): if isinstance(self._loop, list):
return self._loop return self._loop
assert isinstance(self._loop, string_types) assert isinstance(self._loop, string_types)
self._display.vvvvv(
"host %s, action %s, evaluating loop template %s"
% (host_name, self._action, repr(self._loop))
)
self._templar.available_variables = variables self._templar.available_variables = variables
value = self._templar.template(self._loop, disable_lookups=False) value = self._templar.template(self._loop, disable_lookups=False)
if not isinstance(value, list): if not isinstance(value, list):
@ -235,9 +293,9 @@ class RcInstruction:
class RciCreateGroup(RcInstruction): class RciCreateGroup(RcInstruction):
def __init__(self, inventory, templar): def __init__(self, inventory, templar, display):
super().__init__( super().__init__(
inventory, templar, "create_group", ("group", "parent", "add_host") inventory, templar, display, "create_group", ("group", "parent", "add_host")
) )
self._group_mbt = None self._group_mbt = None
self._group_name = None self._group_name = None
@ -245,6 +303,13 @@ class RciCreateGroup(RcInstruction):
self._parent_name = None self._parent_name = None
self._add_host = None self._add_host = None
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
def parse_action(self, record): def parse_action(self, record):
assert self._group_mbt is None and self._group_name is None 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._parent_mbt is None and self._parent_name is None
@ -268,19 +333,25 @@ class RciCreateGroup(RcInstruction):
) )
name = self.get_templated_group(merged_vars, self._group_mbt, self._group_name) name = self.get_templated_group(merged_vars, self._group_mbt, self._group_name)
self._inventory.add_group(name) self._inventory.add_group(name)
self._display.vvv("- created group %s" % (name,))
if self._parent_name is not None: if self._parent_name is not None:
self._inventory.add_child(parent, name) self._inventory.add_child(parent, name)
self._display.vvv("- added group %s to %s" % (name, parent))
if self._add_host: if self._add_host:
self._inventory.add_child(name, host_name) self._inventory.add_child(name, host_name)
self._display.vvv("- added host %s to %s" % (host_name, name))
return True return True
class RciAddHost(RcInstruction): class RciAddHost(RcInstruction):
def __init__(self, inventory, templar): def __init__(self, inventory, templar, display):
super().__init__(inventory, templar, "add_host", ("group",)) super().__init__(inventory, templar, display, "add_host", ("group",))
self._may_be_template = None self._may_be_template = None
self._group = None self._group = None
def repr_instruction_only(self):
return "%s(group=%s)" % (self._action, repr(self._group))
def parse_action(self, record): def parse_action(self, record):
assert self._may_be_template is None and self._group is None assert self._may_be_template is None and self._group is None
self._may_be_template, self._group = self.parse_group_name(record, "group") self._may_be_template, self._group = self.parse_group_name(record, "group")
@ -291,17 +362,25 @@ class RciAddHost(RcInstruction):
merged_vars, self._may_be_template, self._group, must_exist=True merged_vars, self._may_be_template, self._group, must_exist=True
) )
self._inventory.add_child(name, host_name) self._inventory.add_child(name, host_name)
self._display.vvv("- added host %s to %s" % (host_name, name))
return True return True
class RciAddChild(RcInstruction): class RciAddChild(RcInstruction):
def __init__(self, inventory, templar): def __init__(self, inventory, templar, display):
super().__init__(inventory, templar, "add_child", ("group", "child")) super().__init__(inventory, templar, display, "add_child", ("group", "child"))
self._group_mbt = None self._group_mbt = None
self._group_name = None self._group_name = None
self._child_mbt = None self._child_mbt = None
self._child_name = 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),
)
def parse_action(self, record): def parse_action(self, record):
assert self._group_mbt is None and self._group_name is None assert self._group_mbt is None and self._group_name is None
assert self._child_mbt is None and self._child_name is None assert self._child_mbt is None and self._child_name is None
@ -318,18 +397,26 @@ class RciAddChild(RcInstruction):
merged_vars, self._child_mbt, self._child_name, must_exist=True merged_vars, self._child_mbt, self._child_name, must_exist=True
) )
self._inventory.add_child(group, child) self._inventory.add_child(group, child)
self._display.vvv("- added group %s to %s" % (child, group))
return True return True
class RciSetVarOrFact(RcInstruction): class RciSetVarOrFact(RcInstruction):
def __init__(self, inventory, templar, is_fact): def __init__(self, inventory, templar, display, is_fact):
action = "set_" + ("fact" if is_fact else "var") action = "set_" + ("fact" if is_fact else "var")
super().__init__(inventory, templar, action, ("name", "value")) super().__init__(inventory, templar, display, action, ("name", "value"))
self._is_fact = is_fact self._is_fact = is_fact
self._var_name = None self._var_name = None
self._name_may_be_template = None self._name_may_be_template = None
self._var_value = 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),
)
def parse_action(self, record): def parse_action(self, record):
assert ( assert (
self._var_name is None self._var_name is None
@ -379,25 +466,36 @@ class RciSetVarOrFact(RcInstruction):
host_vars[name] = value host_vars[name] = value
else: else:
script_vars[name] = value script_vars[name] = value
self._display.vvv(
"- set %s %s to %s"
% ("fact" if self._is_fact else "var", name, repr(value))
)
return True return True
class RciStop(RcInstruction): class RciStop(RcInstruction):
def __init__(self, inventory, templar): def __init__(self, inventory, templar, display):
super().__init__(inventory, templar, "stop") super().__init__(inventory, templar, display, "stop")
def parse_action(self, record): def parse_action(self, record):
pass pass
def execute_action(self, host_name, merged_vars, host_vars, script_vars): def execute_action(self, host_name, merged_vars, host_vars, script_vars):
self._display.vvv("- stopped execution")
return False return False
class RciFail(RcInstruction): class RciFail(RcInstruction):
def __init__(self, inventory, templar): def __init__(self, inventory, templar, display):
super().__init__(inventory, templar, "fail", ("msg",)) super().__init__(inventory, templar, display, "fail", ("msg",))
self._message = None 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)
def parse_action(self, record): def parse_action(self, record):
self._message = record.get("msg", None) self._message = record.get("msg", None)
@ -407,19 +505,53 @@ class RciFail(RcInstruction):
else: else:
self._templar.available_variables = merged_vars self._templar.available_variables = merged_vars
message = self._templar.template(self._message) message = self._templar.template(self._message)
self._display.vvv("- failed with message %s" % (message,))
raise AnsibleRuntimeError(message) raise AnsibleRuntimeError(message)
class RciBlock(RcInstruction): class RciBlock(RcInstruction):
def __init__(self, inventory, templar): def __init__(self, inventory, templar, display):
super().__init__( super().__init__(
inventory, templar, "block", ("block", "rescue", "always", "locals") inventory,
templar,
display,
"block",
("block", "rescue", "always", "locals"),
) )
self._block = None self._block = None
self._rescue = None self._rescue = None
self._always = None self._always = None
self._locals = 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): def parse_action(self, record):
assert ( assert (
self._block is None self._block is None
@ -465,7 +597,7 @@ class RciBlock(RcInstruction):
instructions = [] instructions = []
for record in record[key]: for record in record[key]:
instructions.append( instructions.append(
parse_instruction(self._inventory, self._templar, record) parse_instruction(self._inventory, self._templar, self._display, record)
) )
return instructions return instructions
@ -483,18 +615,25 @@ class RciBlock(RcInstruction):
result = self._templar.template(value) result = self._templar.template(value)
script_vars[key] = result script_vars[key] = result
merged_vars[key] = result merged_vars[key] = result
self._display.vvv("- set block-local %s to %s" % (key, result))
try: try:
try: try:
self._display.vvv("- running 'block' instructions")
return self.run_block( return self.run_block(
self._block, host_name, merged_vars, host_vars, script_vars self._block, host_name, merged_vars, host_vars, script_vars
) )
except AnsibleError as e: except AnsibleError as e:
if not self._rescue:
self._display.vvv("- block failed")
raise
self._display.vvv("- block failed, running 'rescue' instructions")
script_vars["reconstructed_error"] = str(e) script_vars["reconstructed_error"] = str(e)
merged_vars["reconstructed_error"] = str(e) merged_vars["reconstructed_error"] = str(e)
return self.run_block( return self.run_block(
self._rescue, host_name, merged_vars, host_vars, script_vars self._rescue, host_name, merged_vars, host_vars, script_vars
) )
finally: finally:
self._display.vvv("- block exited, running 'always' instructions")
self.run_block(self._always, host_name, merged_vars, host_vars, script_vars) 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): def run_block(self, block, host_name, merged_vars, host_vars, script_vars):
@ -510,17 +649,17 @@ INSTRUCTIONS = {
"block": RciBlock, "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, d: RciSetVarOrFact(i, t, d, True),
"set_var": lambda i, t: RciSetVarOrFact(i, t, False), "set_var": lambda i, t, d: RciSetVarOrFact(i, t, d, False),
"stop": RciStop, "stop": RciStop,
} }
def parse_instruction(inventory, templar, record): def parse_instruction(inventory, templar, display, record):
action = record["action"] action = record["action"]
if action not in INSTRUCTIONS: if action not in INSTRUCTIONS:
raise AnsibleParserError("Unknown action '%s'" % (action,)) raise AnsibleParserError("Unknown action '%s'" % (action,))
instruction = INSTRUCTIONS[action](inventory, templar) instruction = INSTRUCTIONS[action](inventory, templar, display)
instruction.parse(record) instruction.parse(record)
return instruction return instruction
@ -539,8 +678,12 @@ 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(parse_instruction(self.inventory, self.templar, record)) instructions.append(
parse_instruction(self.inventory, self.templar, self.display, record)
)
self.dump_program(instructions)
for host in inventory.hosts: for host in inventory.hosts:
self.display.vvv("executing reconstructed script for %s" % (host,))
try: try:
self.exec_for_host(host, instructions) self.exec_for_host(host, instructions)
except AnsibleError as e: except AnsibleError as e:
@ -556,3 +699,15 @@ 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 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))