154 lines
3.5 KiB
Markdown
154 lines
3.5 KiB
Markdown
# 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:
|
|
|
|
```python
|
|
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:
|
|
|
|
```python
|
|
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:
|
|
|
|
```python
|
|
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:
|
|
|
|
```python
|
|
async def one(target):
|
|
target.exec('one')
|
|
|
|
async def two(target):
|
|
target.exec('two')
|
|
```
|
|
|
|
You can do:
|
|
|
|
```python
|
|
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:
|
|
|
|
```python
|
|
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:
|
|
|
|
```python
|
|
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:
|
|
|
|
```python
|
|
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:
|
|
|
|
```python
|
|
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"
|
|
```
|