Pythonic automation tool
Project description
Shlax: Pythonic automation tool
Shlax is a Python framework for system automation, initially with the purpose of replacing docker, docker-compose and ansible with a single tool with the purpose of code-reuse made possible by target abstraction.
Development status: Design state
I got the thing to work with an ugly PoC that I basically brute-forced, I'm currently rewriting the codebase with a proper design.
The stories are in development in this order:
- replacing docker build, that's in the state of polishing
- replacing docker-compose, not in use but the PoC works so far
- replacing ansible, also working in working PoC state, the shlax command line demonstrates
This project is supposed to unblock me from adding the CI feature to the Sentry/GitLab/Portainer implementation I'm doing in pure python on top of Django, CRUDLFA+ and Ryzom (isomorphic components in Python to replace templates). So, as you can see, I'm really deep in it with a strong determination.
Shlax builds its container itself, so check the shlaxfile.py of this repository to see what it currently looks like, and check the build job of the CI pipeline to see the output.
Design
The pattern resolves around two moving parts: Actions and Targets.
Action
An action is a function that takes a target argument, it may execute nested actions by passing over the target argument which collects the results.
Example:
async def hello_world(target):
"""Bunch of silly commands to demonstrate action programming."""
await target.mkdir('foo')
python = await target.which('python3', 'python')
await target.exec(f'{python} --version > foo/test')
version = target.exec('cat foo/test').output
print('version')
Recursion
An action may call other actions recursively. There are two ways:
async def something(target):
# just run the other action code
hello_world(target)
# or delegate the call to target
target(hello_world)
In the first case, the resulting count of ran actions will remain 1: "something" action.
In the second case, the resulting count of ran actions will be 2: "something" and "hello_world".
Callable classes
Actually in practice, Actions are basic callable Python classes, here's a basic example to run a command:
class Run:
def __init__(self, cmd):
self.cmd = cmd
async def __call__(self, target):
return await target.exec(self.cmd)
This allows to create callable objects which may be called just like functions and as such be appropriate actions, instead of:
async def one(target):
target.exec('one')
async def two(target):
target.exec('two')
You can do:
one = Run('one')
two = Run('two')
Parallel execution
Actions may be executed in parallel with an action named ... Parallel. This defines an action that will execute three actions in parallel:
action = Parallel(
hello_world,
something,
Run('echo hi'),
)
In this case, all actions must succeed for the parallel action to be considered a success.
Methods
An action may also be a method, as long as it just takes a target argument, for example:
class Thing:
def start(self, target):
"""Starts thing"""
def stop(self, target):
"""Stops thing"""
action = Thing().start
Cleaning
If an action defines a clean
method, it will always be called wether or not
the action succeeded. Example:
class Thing:
def __call__(self, target):
"""Do some thing"""
def clean(self, target):
"""Clean-up target after __call__"""
Colorful actions
If an action defines a colorize
method, it will be called with the colorset
as argument for every output, this allows to code custom output rendering.
Target
A Target is mainly an object providing an abstraction layer over the system we want to automate with actions. It defines functions to execute a command, mount a directory, copy a file, manage environment variables and so on.
Pre-configuration
A Target can be pre-configured with a list of Actions in which case calling the target without argument will execute its Actions until one fails by raising an Exception:
say_hello = Localhost(
hello_world,
Run('echo hi'),
)
await say_hello()
Results
Every time a target execute an action, it will set the "status" attribute on it to "success" or "failure", and add it to the "results" attribute:
say_hello = Localhost(Run('echo hi'))
await say_hello()
say_hello.results # contains the action with status="success"
Targets as Actions: the nesting story
We've seen that any callable taking a target argument is good to be considered an action, and that targets are callables.
To make a Target runnable like any action, all we had to do is add the target
keyword argument to Target.__call__
.
But target()
fills self.results
, so nested action results would not
propagate to the parent target.
That's why if Target receives a non-None target argument, it will has to set
self.parent
with it.
This allows nested targets to traverse parents and get to the root Target
with target.caller
, where it can then attach results to.
This opens the nice side effect that a target implementation may call the parent target if any, you could write a Docker target as such:
class Docker(Target):
def __init__(self, *actions, name):
self.name = name
super().__init__(*actions)
async def exec(self, *args):
return await self.parent.exec(*['docker', 'exec', self.name] + args)
This also means that you always need a parent with an exec implementation, there are two:
- Localhost, executes on localhost
- Stub, for testing
The result of that design is that the following use cases are available:
# This action installs my favorite package on any distro
action = Packages('python3')
# Run it right here: apt install python3
Localhost()(action)
# Or remotely: ssh yourhost apt install python3
Ssh(host='yourhost')(action)
# Let's make a container build receipe with that action
build = Buildah(package)
# Run it locally: buildah exec apt install python3
Localhost()(build)
# Or on a server: ssh yourhost build exec apt install python3
Ssh(host='yourhost')(build)
# Or on a server behingh a bastion:
# ssh yourbastion ssh yourhost build exec apt install python3
Localhost()(Ssh(host='bastion')(Ssh(host='yourhost')(build))
# That's going to do the same
Localhost(Ssh(
Ssh(
build,
host='yourhost'
),
host='bastion'
))()
CLI
You can execute Shlax actions directly on the command line with the shlax
CLI
command.
For your own Shlaxfiles, you can build your CLI with your favorite CLI
framework. If you decide to use cli2
, then Shlax provides a thin layer on top
of it: Group and Command objects made for Shlax objects.
For example:
yourcontainer = Container(
build=Buildah(
User('app', '/app', 1000),
Packages('python', 'unzip', 'findutils'),
Copy('setup.py', 'yourdir', '/app'),
base='archlinux',
commit='yourimage',
),
)
if __name__ == '__main__':
print(Group(doc=__doc__).load(yourcontainer).entry_point())
The above will execute a cli2 command with each method of yourcontainer as a sub-command.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
File details
Details for the file shlax-0.3.0.tar.gz
.
File metadata
- Download URL: shlax-0.3.0.tar.gz
- Upload date:
- Size: 22.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/3.3.0 pkginfo/1.7.0 requests/2.25.1 setuptools/54.1.2 requests-toolbelt/0.9.1 tqdm/4.54.1 CPython/3.9.2
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | c32ac6e68c6e2f459adc9614263a1724959a96f73c76d46a549a921458f34e77 |
|
MD5 | c844c595c36eb97cfff4d3fc95332e44 |
|
BLAKE2b-256 | ef50a969222a814fcc82916dc8bace606e60a3941d2c8d317211a8458c36c919 |