shlax/README.md
2020-04-21 00:18:02 +02:00

3.5 KiB

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. It may be viewed as "async fabric rewrite by a megalomanic Django fanboy".

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__"""

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"