This is part of a series of posts on ideas for an ansible-like provisioning system, implemented in Transilience.
I like many of the modules provided with Ansible: they are convenient, platform-independent implementations of common provisioning steps. They'd be fantastic to have in a library that I could use in normal programs.
This doesn't look easy to do with Ansible code as it is. Also, the code quality of various Ansible modules doesn't fit something I'd want in a standard library of cross-platform provisioning functions.
Modeling Actions
I want to keep the declarative, idempotent aspect of describing actions on a
system. A good place to start could be a hierarchy of
dataclasses that hold
the same parameters as ansible modules, plus a run()
method that performs the
action:
@dataclass
class Action:
"""
Base class for all action implementations.
An Action is the equivalent of an ansible module: a declarative
representation of an idempotent operation on a system.
An Action can be run immediately, or serialized, sent to a remote system,
run, and sent back with its results.
"""
uuid: str = field(default_factory=lambda: str(uuid.uuid4()))
result: Result = field(default_factory=Result)
def summary(self):
"""
Return a short text description of this action
"""
return self.__class__.__name__
def run(self, system: transilience.system.System):
"""
Perform the action
"""
self.result.state = ResultState.NOOP
I like that Ansible tasks have names, and I hate having to give names to
trivial tasks like "Create directory /foo/bar", so I added a summary()
method
so that trivial tasks like that can take care of naming themselves.
Dataclasses allow to introspect fields and annotate them with extra metadata, and together with docstrings, I can make actions reasonably self-documeting.
I ported some of Ansible's modules over: see complete list in the git repository.
Running Actions in a script
With a bit of glue code I can now run Ansible-style functions from a plain Python script:
#!/usr/bin/python3
from transilience.runner import Script
script = Script()
for i in range(10):
script.builtin.file(state="touch", path=f"/tmp/test{i}")
Running Actions remotely
Dataclasses have an asdict function that makes them trivially serializable. If their members stick to data types that can be serialized with Mitogen and the run implementation doesn't use non-pure, non-stdlib Python modules, then I can trivially run actions on all sorts of remote systems using Mitogen:
#!/usr/bin/python3
from transilience.runner import Script
from transilience.system import Mitogen
script = Script(system=Mitogen("my server", "ssh", hostname="machine.example.org", username="user"))
for i in range(10):
script.builtin.file(state="touch", path=f"/tmp/test{i}")
How fast would that be, compared to Ansible?
$ time ansible-playbook test.yaml
[...]
real 0m15.232s
user 0m4.033s
sys 0m1.336s
$ time ./test_script
real 0m4.934s
user 0m0.547s
sys 0m0.049s
With a network round-trip for each single operation I'm already 3x faster than Ansible, and it can run on nspawn containers, too!
I always wanted to have a library of ansible modules useable in normal scripts, and I've always been angry with Ansible for not bundling their backend code in a generic library. Well, now there's the beginning of one!
Sweet! Next step, pipelining.