Source code for apssh.deferred

"""
Support for deferred evaluation; typical use case is, you want to write something like::

    somevar=$(ssh nodename some-command)
    ssh othernode other-command $somevar

but because a Scheduler is totally created before it gets to run anything, creating a
``Run()`` instance from a string means that the string must be known at sheduler-creation
time, at which point we do not yet have the value of somevar

that's where ``Deferred`` objects come in; they fill in for actual ``str`` objects, but
are actually templates that are rendered later on when the command is actually about to
trigger

typically in a kubernetes-backed scenario, we often need to get a pod's name by issuing an
ssh command to the master node, so this is not a static data that can be filled in the
code

"""

from jinja2 import Template, DebugUndefined

[docs] class Variables(dict): """ think of this class as a regular namespace, i.e. a set of associations variable → value we cannot use the regular Python, binding because at the time where a Scheduler gets built, those variables are not yet available so the ``Variables`` object typically collects values that are computed during a scheduler run just like a JS object, a ``Variables`` object can be accessed through indexing or attributes all the same, so that:: variables = Variables() variables.foo = 'bar' variables['bar'] = 'foo' variables.foo == variables['foo'] # True variables.var == variables['bar'] # True it is common to create a single ``Variables`` environment for a ``Scheduler`` run; variables inside the environment are often set by creating ``Run``-like objects with a ``Capture`` instance that specifies in what variable the result should end up """ # support for the . notation def __getattr__(self, attr): return self.get(str(attr), '') def __setattr__(self, attr, value): self[attr] = value
[docs] class Deferred: """ the ``Deferred`` class is the trick that lets you introduce what we call deferred evaluation in a scenario; main use case being when you run a remote command to compute something, that in turn is used later on by another Run or Service object; except that, because the scheduler and its jobs/commands pieces are created before it gets run, you cannot compute all the details right away, you need to have some parts replaces later on - that is, deferred Parameters: template(str): a Jinja template as a string, that may contain variables or expressions enclosed in ``{{}}`` variables(Variables): an environment object that will collect values over time, so that variables in ``{{}}`` can be expansed when the time comes a ``Deferred`` object can be used to create instances of the ``Run`` class and its siblings, or of the ``Service`` class; this is useful when the command contains a part that needs to be computed during the scenario .. warning:: **beware of f-strings !** since Jinja templates use double brackets as delimiters for expressions, it is probably unwise to create a template from an f-string, or if you do you will have to insert variable inside quadruple brackets like so `{{{{varname}}}}`, so that after f-string evaluation a double bracket remains. """ def __init__(self, template, variables): self.template = template self.variables = variables def __str__(self): """ replace expresions of the form {x} with the value of variable x as per the variables object if undefined variables are used, the {{thing}} remains this is useful in particular in the context of graphic output which is mostly done before the scenario gets run, and so no variable are known at that point """ template = Template(self.template, undefined=DebugUndefined) return template.render(**self.variables) def __repr__(self): return (f"Deferred with template {self.template} " f"and variables {self.variables} ") def replace(self, old, new): # pylint: disable=missing-function-docstring # NOTE: this method is **not** meant to be called explicitly # # it is there only because other parts of the code, in service.py notably, # do operations on commands expecting str objects, so they occasionnaly call # service.command.replace(old, new) # # this is **not** what actually performs expansion of {{}} # expressions in the template return Deferred(self.template.replace(old, new), self.variables)
[docs] def dup_from_string(self, new_template): """ Create a new ``Deferred`` object on the same ``Variables`` environment, but with a different template. """ return Deferred(new_template, self.variables)
# will become a dataclass
[docs] class Capture: # pylint: disable=too-few-public-methods """ this class has no logic in itself, it is only a convenience so that one can specify where a Run command should store it's captured output for example a shell script like:: somevar=$(ssh nodename some-command) ssh othernode other-command $somevar could be mimicked with (simplified version):: env = Variables() Sequence( SshJob(node_obj, # the output of this command ends up # as the 'foobar' variable in env commands=Run("some-command", capture=Capture('somevar', env))) SshJob(other_node_obj, # which we use here inside a jinja template commands=Run(Deferred("other-command {{somevar}}", env))) """ def __init__(self, varname: str, variables: Variables): self.varname = varname self.variables = variables