2 Commits
Author SHA1 Message Date
DrClaw fb4b41e2ff Unbound mod variable 2020-02-19 01:59:09 +01:00
DrClaw 16f1bef125 Autocompletion & Makefile 2020-02-19 00:28:43 +01:00
32 changed files with 292 additions and 526 deletions
+10
View File
@@ -0,0 +1,10 @@
all:
@echo -e "\n\033[1;36m --> Installing the module ...\033[0m\n"
pip install --user -e .
@echo -ne "\n\033[1;36m"; \
read -p " --> Install autocompletion ?[Y|n] " RESP; \
echo -e "\n\033[0m"; \
case "$$RESP" in \
y*|Y*|"")sudo cp -v completion.bash /usr/share/bash-completion/completions/shlax;; \
*);; \
esac;
+14
View File
@@ -0,0 +1,14 @@
#!/usr/bin/bash
_action(){
COMPREPLY=()
cur=${COMP_WORDS[COMP_CWORD]}
if [[ "$COMP_CWORD" -eq 1 ]] ; then
COMPREPLY=($(compgen -W "$(ls shlax/repo/*.py | sed s/^.*\\/\// | cut -d "." -f 1)" "${cur}"))
else
action=$(grep "^[^ #)]\w* =" shlax/repo/${COMP_WORDS[1]}.py | cut -d " " -f 1)
COMPREPLY=($(compgen -W "$action" "${cur}"))
fi
}
complete -F _action shlax
+1 -1
View File
@@ -5,7 +5,7 @@ setup(
name='shlax',
versioning='dev',
setup_requires='setupmeta',
install_requires=['cli2'],
install_requires=['cli2>=1.1.6'],
extras_require=dict(
full=[
'pyyaml',
+7
View File
@@ -0,0 +1,7 @@
from .actions import *
from .image import Image
from .strategies import *
from .output import Output
from .proc import Proc
from .targets import *
from .shlaxfile import Shlaxfile
+7
View File
@@ -0,0 +1,7 @@
from .copy import Copy
from .packages import Packages # noqa
from .base import Action # noqa
from .htpasswd import Htpasswd
from .run import Run # noqa
from .pip import Pip
from .service import Service
+75 -97
View File
@@ -6,27 +6,9 @@ import sys
from ..output import Output
from ..exceptions import WrongResult
from ..result import Result
class class_or_instance_method:
def __init__(self, f):
self.f = f
def __get__(self, instance, owner):
def newfunc(*args, **kwargs):
return self.f(
instance if instance is not None else owner,
*args,
**kwargs
)
return newfunc
class Action:
display_variables = []
hide_variables = ['output']
default_steps = ['apply']
parent = None
contextualize = []
regexps = {
@@ -42,21 +24,28 @@ class Action:
'''.strip(),
immediate=True,
),
verbose=dict(
alias='v',
default=False,
help='Verbose, like -d=visit,cmd,out',
immediate=True,
),
)
def __init__(self, *args, **kwargs):
def __init__(self, *args, doc=None, **kwargs):
self.args = args
self.kwargs = kwargs
for key, value in kwargs.items():
setattr(self, key, value)
if isinstance(value, Action):
getattr(self, key).shlaxstep = True
self.call_args = []
self.call_kwargs = {}
self._doc = doc
self.menu = {
name: value
for name, value in kwargs.items()
if isinstance(value, Action)
}
@property
def context(self):
if not self.parent:
if '_context' not in self.__dict__:
self._context = dict()
return self._context
else:
return self.parent.context
def actions_filter(self, results, f=None, **filters):
if f:
@@ -111,78 +100,77 @@ class Action:
add(self)
return self.actions_filter(children, f, **filters)
async def __call__(self, *targets, **options):
if not targets:
from ..targets.localhost import Localhost
targets = [Localhost()]
def __getattr__(self, name):
for a in self.parents() + self.sibblings() + self.children():
if name in a.contextualize:
return getattr(a, name)
raise AttributeError(f'{type(self).__name__} has no {name}')
output = Output(
regexp=self.regexps,
debug='cmd,visit,out' if options['verbose'] else options['debug'],
)
results = []
for target in targets:
target.output = output
if len(targets) > 1:
output.prefix = target
from copy import deepcopy
action = deepcopy(self)
action.target = target
result = Result(action, target)
results.append(result)
action.result = result
action.output = output
for step in options.get('steps', None) or self.default_steps:
if step not in action.steps():
print(f'Failed to find {type(action).__name__}.{step}')
continue
action.step = step
output.start(action)
async def call(self, *args, **kwargs):
print(f'{self}.call(*args, **kwargs) not implemented')
sys.exit(1)
def output_factory(self, *args, **kwargs):
kwargs.setdefault('regexps', self.regexps)
return Output(**kwargs)
async def __call__(self, *args, **kwargs):
self.call_args = list(self.call_args) + list(args)
self.call_kwargs.update(kwargs)
self.output = self.output_factory(*args, **kwargs)
self.output_start()
self.status = 'running'
try:
if isinstance(getattr(action, step), Action):
await getattr(action, step)(**options)
else:
await getattr(action, step)()
result = await self.call(*args, **kwargs)
except Exception as e:
output.fail(action, e)
action.result.status = 'fail'
self.output_fail(e)
self.status = 'fail'
proc = getattr(e, 'proc', None)
if proc:
result = proc.rc
else:
raise
else:
output.success(action)
result.status = 'success'
self.output_success()
if self.status == 'running':
self.status = 'success'
finally:
clean = getattr(action, 'clean', None)
clean = getattr(self, 'clean', None)
if clean:
output.clean(action)
await clean(target)
self.output.clean(self)
await clean(*args, **kwargs)
return result
return results
def output_start(self):
if self.kwargs.get('quiet', False):
return
self.output.start(self)
def output_fail(self, exception=None):
if self.kwargs.get('quiet', False):
return
self.output.fail(self, exception)
def output_success(self):
if self.kwargs.get('quiet', False):
return
self.output.success(self)
def __repr__(self):
return ' '.join([type(self).__name__] + [
return ' '.join([type(self).__name__] + list(self.args) + [
f'{k}={v}'
for k, v in self.__dict__.items()
if (k in self.display_variables or not self.display_variables)
and (k not in self.hide_variables)
for k, v in self.kwargs.items()
])
def colorized(self, colors):
def colorized(self):
return ' '.join([
colors['pink1']
self.output.colors['pink1']
+ type(self).__name__
+ '.'
+ self.step
+ colors['yellow']
] + [
f'{colors["blue"]}{k}{colors["gray"]}={colors["green2"]}{v}'
for k, v in self.__dict__.items()
if (k in self.display_variables or not self.display_variables)
and (k not in self.hide_variables)
] + [colors['reset']])
+ self.output.colors['yellow']
] + list(self.args) + [
f'{self.output.colors["blue"]}{k}{self.output.colors["gray"]}={self.output.colors["green2"]}{v}'
for k, v in self.kwargs_output().items()
] + [self.output.colors['reset']])
def callable(self):
from ..targets import Localhost
@@ -211,7 +199,6 @@ class Action:
def action(self, action, *args, **kwargs):
if isinstance(action, str):
# import dotted module path string to action
import cli2
a = cli2.Callable.factory(action).target
if not a:
@@ -222,26 +209,17 @@ class Action:
action = a
p = action(*args, **kwargs)
p.parent = self
for parent in self.parents():
if hasattr(parent, 'actions'):
p.parent = parent
break
p.parent = parent
if 'actions' not in self.__dict__:
# "mutate" to Strategy
from ..strategies.script import Actions
self.actions = Actions(self, [p])
return p
@class_or_instance_method
def steps(self):
return {
key: getattr(self, key)
for key in dir(self)
if key != 'steps' # avoid recursion
and (
key in self.default_steps
or getattr(getattr(self, key), 'shlaxstep', False)
)
}
def bind(self, *args):
clone = deepcopy(self)
clone.call_args = args
return clone
-1
View File
@@ -2,6 +2,5 @@ from .base import Action
class Copy(Action):
"""Copy files or directories to target."""
async def call(self, *args, **kwargs):
await self.copy(*self.args)
+6 -14
View File
@@ -6,22 +6,14 @@ from .base import Action
class Htpasswd(Action):
"""Ensure a user is present in an htpasswd file."""
display_variables = ('user', 'path')
regexps = {
r'(.*)': '{red}\\1{gray}:${blue}\\2${blue}',
r'([^:]*):\\$([^$]*)\\$(.*)$': '{red}\\1{gray}:${blue}\\2${blue}\\3',
}
def __init__(self, user, path, **kwargs):
self.user = user
def __init__(self, path, user, *args, **kwargs):
self.path = path
super().__init__(**kwargs)
self.user = user
super().__init__(*args, **kwargs)
async def apply(self):
async def call(self, *args, **kwargs):
found = False
htpasswd = await self.target.exec(
'cat', self.path, raises=False)
htpasswd = await self.exec('cat', self.path, raises=False)
if htpasswd.rc == 0:
for line in htpasswd.out.split('\n'):
if line.startswith(self.user + ':'):
@@ -34,4 +26,4 @@ class Htpasswd(Action):
) for i in range(20))
hashed = hashlib.sha1(self.password.encode('utf8'))
line = f'{self.user}:\\$sha1\\${hashed.hexdigest()}'
await self.target.exec(f'echo {line} >> {self.path}')
await self.exec(f'echo {line} >> {self.path}')
+9 -8
View File
@@ -12,13 +12,14 @@ from .base import Action
class Packages(Action):
"""
Package manager abstract layer with caching.
The Packages visitor wraps around the container's package manager.
It's a central piece of the build process, and does iterate over other
container visitors in order to pick up packages. For example, the Pip
visitor will declare ``self.packages = dict(apt=['python3-pip'])``, and the
Packages visitor will pick it up.
"""
contextualize = ['mgr']
regexps = {
#r'Installing ([\w\d-]+)': '{cyan}\\1',
r'Installing': '{cyan}lol',
@@ -94,7 +95,7 @@ class Packages(Action):
f.write(str(os.getpid()))
try:
await self.target.rexec(self.cmds['update'])
await self.rexec(self.cmds['update'])
finally:
os.unlink(lockfile)
@@ -102,15 +103,15 @@ class Packages(Action):
f.write(str(now))
else:
while os.path.exists(lockfile):
print(f'{self.target} | Waiting for {lockfile} ...')
print(f'{self.container.name} | Waiting for update ...')
await asyncio.sleep(1)
async def apply(self):
cached = getattr(self.target, 'pkgmgr', None)
async def call(self, *args, **kwargs):
cached = getattr(self, '_pagkages_mgr', None)
if cached:
self.mgr = cached
else:
mgr = await self.target.which(*self.mgrs.keys())
mgr = await self.which(*self.mgrs.keys())
if mgr:
self.mgr = mgr[0].split('/')[-1]
@@ -121,7 +122,7 @@ class Packages(Action):
if not getattr(self, '_packages_upgraded', None):
await self.update()
if self.kwargs.get('upgrade', True):
await self.target.exec(self.cmds['upgrade'], user='root')
await self.rexec(self.cmds['upgrade'])
self._packages_upgraded = True
packages = []
@@ -135,7 +136,7 @@ class Packages(Action):
else:
packages.append(package)
await self.target.exec(*self.cmds['install'].split(' ') + packages, user='root')
await self.rexec(*self.cmds['install'].split(' ') + packages)
async def apk_setup(self):
cachedir = os.path.join(self.cache_root, self.mgr)
-2
View File
@@ -5,8 +5,6 @@ from .base import Action
class Pip(Action):
"""Pip abstraction layer."""
def __init__(self, *pip_packages, pip=None, requirements=None):
self.requirements = requirements
super().__init__(*pip_packages, pip=pip, requirements=requirements)
+8 -14
View File
@@ -1,22 +1,16 @@
from ..targets.buildah import Buildah
from ..targets.docker import Docker
from .base import Action
class Run(Action):
"""Run a script or command on a target."""
def __init__(self, *args, image=None, **kwargs):
super().__init__(**kwargs)
self.args = args
self.kwargs = kwargs
self.image = image
async def apply(self):
if not self.image:
return await self.target.exec(*self.args, **self.kwargs)
from ..targets.buildah import Buildah
from ..targets.docker import Docker
async def call(self, *args, **kwargs):
image = self.kwargs.get('image', None)
if not image:
return await self.exec(*self.args, **self.kwargs)
if isinstance(image, Buildah):
breakpoint()
result = await self.action(image, *args, **kwargs)
return await Docker(
-3
View File
@@ -4,9 +4,6 @@ from .base import Action
class Service(Action):
"""
Manage a systemd service.
"""
def __init__(self, *names, state=None):
self.state = state or 'started'
self.names = names
+54 -214
View File
@@ -1,241 +1,80 @@
"""
Shlax automation tool manual
'''
shlax is a micro-framework to orchestrate commands.
Shlax is built mostly around 3 moving pieces:
- Target: a target host and protocol
- Action: execute a shlax action
- Strategy: defines how to apply actions on targets (scripted only)
shlax yourfile.py: to list actions you have declared.
shlax yourfile.py <action>: to execute a given action
#!/usr/bin/env shlax: when making yourfile.py an executable.
'''
Shlax executes mostly in 3 ways:
- Execute actions on targets with the command line
- With your shlaxfile as first argument: offer defined Actions
- With the name of a module in shlax.repo: a community maintained shlaxfile
"""
import copy
import asyncio
import cli2
import copy
import inspect
import importlib
import glob
import os
import sys
from .actions.base import Action
from .exceptions import ShlaxException, WrongResult
from .strategies import Script
from .exceptions import *
from .shlaxfile import Shlaxfile
from .targets import Localhost
class ConsoleScript(cli2.ConsoleScript):
class Parser(cli2.Parser):
def __init__(self, *args, **kwargs):
self.targets = dict()
super().__init__(*args, **kwargs)
def __call__(self, *args, **kwargs):
self.shlaxfile = None
shlaxfile = sys.argv.pop(1) if len(sys.argv) > 1 else ''
if shlaxfile:
if not os.path.exists(shlaxfile):
try: # missing shlaxfile, what are we gonna do !!
mod = importlib.import_module('shlax.repo.' + shlaxfile)
except ImportError:
print('Could not find ' + shlaxfile)
self.exit_code = 1
return
shlaxfile = mod.__file__
self._doc = inspect.getdoc(mod)
def append(self, arg):
if '=' not in arg and '@' in arg:
if '://' in arg:
kind, spec = arg.split('://')
else:
kind = 'ssh'
spec = arg
mod = importlib.import_module('shlax.targets.' + kind)
target = getattr(mod, kind.capitalize())(spec)
self.targets[str(target)] = target
else:
super().append(arg)
def __call__(self):
if len(self.argv) > 1 and os.path.exists(self.argv[1]):
self.argv = sys.argv[1:]
spec = importlib.util.spec_from_file_location('shlaxfile', sys.argv[1])
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
self.doc = (inspect.getdoc(mod) or '').split("\n")[0]
for name, value in mod.__dict__.items():
if isinstance(value, Action):
self.shlaxfile = Shlaxfile()
self.shlaxfile.parse(shlaxfile)
if 'main' in self.shlaxfile.actions:
action = self.shlaxfile.actions['main']
for name, child in self.shlaxfile.actions['main'].menu.items():
self[name] = cli2.Callable(
name,
self.action(value),
doc=type(value).__doc__,
child.callable(),
options={
option: cli2.Option(option, **cfg)
for option, cfg in value.options.items()
}
k: cli2.Option(name=k, **v)
for k, v in action.options.items()
},
color=getattr(action, 'color', cli2.YELLOW),
)
#self[name] = value
#elif callable(value) and getattr(value, '__name__', '').startswith('test_'):
# self.tests[value.__name__] = value
#modname = sys.argv[1].split('/')[-1].replace('.py', '')
#mod = importlib.import_module('shlax.actions.' + modname)
else:
scripts = glob.glob(os.path.join(
os.path.dirname(__file__), 'actions', '*.py'))
for script in scripts:
modname = script.split('/')[-1].replace('.py', '')
if modname == '__init__':
continue
mod = importlib.import_module('shlax.actions.' + modname)
for key, value in mod.__dict__.items():
if key == '__builtins__':
continue
if key.lower() != modname:
continue
break
self[modname] = cli2.Callable(
modname,
self.action_class(value),
for name, action in self.shlaxfile.actions.items():
self[name] = cli2.Callable(
name,
action.callable(),
options={
option: cli2.Option(option, **cfg)
for option, cfg in value.options.items()
}
)
scripts = glob.glob(os.path.join(
os.path.dirname(__file__), 'repo', '*.py'))
for script in scripts:
modname = script.split('/')[-1].replace('.py', '')
mod = importlib.import_module('shlax.repo.' + modname)
self[modname] = cli2.Group(key, doc=inspect.getdoc(mod))
for key, value in mod.__dict__.items():
if not isinstance(value, Action):
continue
doc = (inspect.getdoc(mod) or '').split("\n")[0]
if key == 'main':
if len(value.steps()) == 1:
self[modname] = cli2.Callable(
modname,
self.action(value),
doc=doc,
options={
option: cli2.Option(option, **cfg)
for option, cfg in value.options.items()
}
k: cli2.Option(name=k, **v)
for k, v in action.options.items()
},
color=getattr(action, 'color', cli2.YELLOW),
doc=inspect.getdoc(getattr(action, name, None)) or action._doc,
)
else:
for name, step in value.steps().items():
if isinstance(step, Action):
self[modname][name] = cli2.Callable(
modname,
self.action(step),
doc=inspect.getdoc(step),
options={
option: cli2.Option(option, **cfg)
for option, cfg in value.options.items()
}
)
else:
# should be a method, just clone the
# original action and replace default_steps
action = copy.deepcopy(value)
action.default_steps = [name]
self[modname][name] = cli2.Callable(
modname,
self.action(action),
doc=inspect.getdoc(step),
options={
option: cli2.Option(option, **cfg)
for option, cfg in value.options.items()
}
)
else:
if len(value.steps()) == 1:
self[modname][key] = cli2.Callable(
modname,
self.action(value),
doc=doc,
options={
option: cli2.Option(option, **cfg)
for option, cfg in value.options.items()
}
)
else:
self[modname][key] = cli2.Group('steps')
for step in value.steps():
self[modname][key][step] = cli2.Callable(
modname,
self.action(value),
doc='lol',
options={
option: cli2.Option(option, **cfg)
for option, cfg in value.options.items()
}
)
from shlax import repo
path = repo.__path__._path[0]
for shlaxfile in glob.glob(os.path.join(path, '*.py')):
name = shlaxfile.split('/')[-1].split('.')[0]
mod = importlib.import_module('shlax.repo.' + name)
self[name] = cli2.Callable(name, mod)
return super().__call__()
def action(self, action):
async def cb(*args, **kwargs):
options = dict(steps=args)
options.update(self.parser.options)
# UnboundLocalError: local variable 'action' referenced before assignment
# ??? gotta be missing something, commenting meanwhile
# action = copy.deepcopy(action)
return await action(*self.parser.targets, **options)
cb.__name__ = type(action).__name__
return cb
def action_class(self, action_class):
async def cb(*args, **kwargs):
argspec = inspect.getfullargspec(action_class)
required = argspec.args[1:]
missing = []
for i, name in enumerate(required):
if len(args) - 1 <= i:
continue
if name in kwargs:
continue
missing.append(name)
if missing:
if not args:
print('No args provided after action name ' + action_class.__name__.lower())
print('Required arguments: ' + ', '.join(argspec.args[1:]))
if args:
print('Provided: ' + ', '.join(args))
print('Missing arguments: ' + ', '.join(missing))
print('Try to just add args on the command line separated with a space')
print(inspect.getdoc(action_class))
example = 'Example: shlax action '
example += action_class.__name__.lower()
if args:
example += ' ' + ' '.join(args)
example += ' ' + ' '.join(missing)
print(example)
return
_args = []
steps = []
for arg in args:
if arg in action_class.steps():
steps.append(arg)
else:
_args.append(arg)
options = dict(steps=steps)
'''
varargs = argspec.varargs
if varargs:
extra = args[len(argspec.args) - 1:]
args = args[:len(argspec.args) - 1]
options = dict(steps=extra)
else:
extra = args[len(argspec.args) - 1:]
args = args[:len(argspec.args) - 1]
options = dict(steps=extra)
'''
options.update(self.parser.options)
return await action_class(*_args, **kwargs)(*self.parser.targets, **options)
cb.__doc__ = (inspect.getdoc(action_class) or '').split("\n")[0]
cb.__name__ = action_class.__name__
return cb
return super().__call__(*args, **kwargs)
def call(self, command):
kwargs = copy.copy(self.parser.funckwargs)
kwargs.update(self.parser.options)
try:
return super().call(command)
return command(*self.parser.funcargs, **kwargs)
except WrongResult as e:
print(e)
self.exit_code = e.proc.rc
@@ -243,4 +82,5 @@ class ConsoleScript(cli2.ConsoleScript):
print(e)
self.exit_code = 1
cli = ConsoleScript(__doc__).add_module('shlax.cli')
+37
View File
@@ -0,0 +1,37 @@
from copy import deepcopy
import yaml
from shlax import *
class GitLabCI(Script):
async def call(self, *args, write=True, **kwargs):
output = dict()
for key, value in self.kwargs.items():
if isinstance(value, dict):
output[key] = deepcopy(value)
image = output[key].get('image', 'alpine')
if hasattr(image, 'image'):
output[key]['image'] = image.image.repository + ':$CI_COMMIT_SHORT_SHA'
else:
output[key] = value
output = yaml.dump(output)
if kwargs['debug'] is True:
self.output(output)
if write:
with open('.gitlab-ci.yml', 'w+') as f:
f.write(output)
from shlax.cli import cli
for arg in args:
job = self.kwargs[arg]
_args = []
if not isinstance(job['image'], str):
image = str(job['image'].image)
else:
image = job['image']
await self.action('Docker', Run(job['script']), image=image)(*_args, **kwargs)
def colorized(self):
return type(self).__name__
+2 -2
View File
@@ -12,10 +12,10 @@ class WrongResult(ShlaxException):
msg = f'FAIL exit with {proc.rc} ' + proc.args[0]
if not proc.output.debug or 'cmd' not in str(proc.output.debug):
if not proc.debug or 'cmd' not in str(proc.debug):
msg += '\n' + proc.cmd
if not proc.output.debug or 'out' not in str(proc.output.debug):
if not proc.debug or 'out' not in str(proc.debug):
msg += '\n' + proc.out
msg += '\n' + proc.err
+5 -5
View File
@@ -104,7 +104,7 @@ class Output:
self.colors['purplebold'],
'! TEST ',
self.colors['reset'],
action.colorized(self.colors),
action.colorized(),
'\n',
]))
@@ -114,7 +114,7 @@ class Output:
self.colors['bluebold'],
'+ CLEAN ',
self.colors['reset'],
action.colorized(self.colors),
action.colorized(),
'\n',
]))
@@ -124,7 +124,7 @@ class Output:
self.colors['orangebold'],
'⚠ START ',
self.colors['reset'],
action.colorized(self.colors),
action.colorized(),
'\n',
]))
@@ -134,7 +134,7 @@ class Output:
self.colors['greenbold'],
'✔ SUCCESS ',
self.colors['reset'],
action.colorized(self.colors) if hasattr(action, 'colorized') else str(action),
action.colorized() if hasattr(action, 'colorized') else str(action),
'\n',
]))
@@ -144,6 +144,6 @@ class Output:
self.colors['redbold'],
'✘ FAIL ',
self.colors['reset'],
action.colorized(self.colors) if hasattr(action, 'colorized') else str(action),
action.colorized() if hasattr(action, 'colorized') else str(action),
'\n',
]))
-9
View File
@@ -1,9 +0,0 @@
from .targets import Localhost
class Play:
def __init__(self, *actions, targets=None, options=None):
self.options = options or {}
self.targets = targets or dict(localhost=Localhost())
self.actions =
+3 -2
View File
@@ -54,7 +54,8 @@ class Proc:
"""
test = False
def __init__(self, *args, prefix=None, raises=True, output=None):
def __init__(self, *args, prefix=None, raises=True, debug=None, output=None):
self.debug = debug if not self.test else False
self.output = output or Output()
self.cmd = ' '.join(args)
self.args = args
@@ -86,7 +87,7 @@ class Proc:
if self.called:
raise Exception('Already called: ' + self.cmd)
if 'cmd' in str(self.output.debug):
if self.debug is True or 'cmd' in str(self.debug):
self.output.cmd(self.cmd)
if self.test:
+11 -5
View File
@@ -3,15 +3,11 @@
Manage a traefik container maintained by Shlax community.
"""
from shlax.shortcuts import *
from shlax import *
main = Docker(
name='traefik',
image='traefik:v2.0.0',
install=Htpasswd(
'./htpasswd', 'root', doc='Install root user in ./htpasswd'
),
networks=['web'],
command=[
'--entrypoints.web.address=:80',
@@ -32,4 +28,14 @@ main = Docker(
'traefik.http.routers.traefik.service=api@internal',
'traefik.http.routers.traefik.entrypoints=web',
],
doc='Current traefik instance',
)
install = Script(
Htpasswd('./htpasswd', 'root'),
main.bind('up'),
doc='Deploy a Traefik instance',
)
up = main.bind('up')
rm = main.bind('rm')
-7
View File
@@ -1,7 +0,0 @@
class Result:
def __init__(self, action, target):
self.action = action
self.target = target
self.status = 'pending'
+1 -1
View File
@@ -16,7 +16,7 @@ class Shlaxfile:
spec.loader.exec_module(mod)
for name, value in mod.__dict__.items():
if isinstance(value, Action):
value.__name__ = name
value.name = name
self.actions[name] = value
elif callable(value) and getattr(value, '__name__', '').startswith('test_'):
self.tests[value.__name__] = value
-14
View File
@@ -1,14 +0,0 @@
from .actions.copy import Copy
from .actions.packages import Packages # noqa
from .actions.base import Action # noqa
from .actions.htpasswd import Htpasswd
from .actions.run import Run # noqa
from .actions.pip import Pip
from .actions.service import Service
from .targets.buildah import Buildah
from .targets.docker import Docker
from .targets.localhost import Localhost
from .targets.ssh import Ssh
-7
View File
@@ -1,7 +0,0 @@
from .script import Script
class Test(Script):
async def call(self, *args, backend=None, **kwargs):
backend = backend or 'Docker'
breakpoint()
return await self.action(backend, self.actions, **kwargs)
+4
View File
@@ -0,0 +1,4 @@
from .buildah import Buildah
from .docker import Docker
from .localhost import Localhost
from .ssh import Ssh
+23 -35
View File
@@ -7,9 +7,7 @@ from .localhost import Localhost
class Docker(Localhost):
"""Manage a docker container."""
default_steps = ['install', 'up']
contextualize = ['image', 'home']
contextualize = Localhost.contextualize + ['mnt', 'ctr', 'mount']
def __init__(self, *args, **kwargs):
self.image = kwargs.get('image', 'alpine')
@@ -53,28 +51,33 @@ class Docker(Localhost):
# raises=False
# )
# ).out.split('\n')[0]
if step('install') and 'install' in self.kwargs:
await self.action(self.kwargs['install'], *args, **kwargs)
if step('rm') and await self.exists():
await self.exec('docker', 'rm', '-f', self.name)
if step('rm'):
await self.rm(*args, **kwargs)
if step('down') and self.name:
await self.exec('docker', 'down', '-f', self.name)
if step('up'):
if await self.exists():
self.name = (await self.exec('docker', 'start', self.name)).out
else:
self.id = (await self.exec(
'docker', 'run', '-d', '--name', self.name, str(self.image))
).out
await self.up(*args, **kwargs)
return await super().call(*args, **kwargs)
async def exists(self):
proc = await self.exec(
'docker', 'ps', '-aq', '--filter',
'name=' + self.name,
raises=False
)
return bool(proc.out.strip())
async def rm(self, *args, **kwargs):
return await self.exec('docker', 'rm', '-f', self.name)
async def down(self, *args, **kwargs):
"""Remove instance, except persistent data if any"""
if self.name:
self.name = (await self.exec('docker', 'start', self.name)).out
else:
self.name = (await self.exec('docker', 'run', '-d', '--name', self.name)).out
async def up(self, *args, **kwargs):
"""Perform start or run"""
if self.name:
self.name = (await self.exec('docker', 'start', self.name)).out
else:
self.id = (await self.exec('docker', 'run', '-d', '--name', self.name)).out
async def copy(self, *args):
src = args[:-1]
@@ -94,18 +97,3 @@ class Docker(Localhost):
procs.append(self.exec(*args))
return await asyncio.gather(*procs)
async def up(self):
"""Ensure container is up and running."""
if await self.exists():
self.name = (await self.exec('docker', 'start', self.name)).out
else:
self.id = (await self.exec(
'docker', 'run', '-d', '--name', self.name, str(self.image))
).out
up.shlaxstep = True
async def rm(self):
"""Remove container."""
await self.exec('docker', 'rm', '-f', self.name)
rm.shlaxstep = True
+3 -1
View File
@@ -35,7 +35,9 @@ class Localhost(Script):
return args, kwargs
async def exec(self, *args, **kwargs):
kwargs['output'] = self.output
if 'debug' not in kwargs:
kwargs['debug'] = getattr(self, 'call_kwargs', {}).get('debug', False)
kwargs.setdefault('output', self.output)
args, kwargs = self.shargs(*args, **kwargs)
proc = await Proc(*args, **kwargs)()
if kwargs.get('wait', True):
+1 -1
View File
@@ -12,7 +12,7 @@ build = Buildah(
'quay.io/podman/stable',
Packages('python38', 'buildah', 'unzip', 'findutils', 'python3-yaml', upgrade=False),
Async(
# dancing for pip on centos python3.8
# python3.8 on centos with pip dance ...
Run('''
curl -o setuptools.zip https://files.pythonhosted.org/packages/42/3e/2464120172859e5d103e5500315fb5555b1e908c0dacc73d80d35a9480ca/setuptools-45.1.0.zip
unzip setuptools.zip
-7
View File
@@ -1,7 +0,0 @@
from shlax.cli import ConsoleScript
def test_parser():
parser = ConsoleScript.Parser(['@host'])
parser.parse()
assert parser.targets['host'] == Ssh('host')
-7
View File
@@ -1,7 +0,0 @@
from shlax.cli import ConsoleScript
def test_parser():
parser = ConsoleScript.Parser(['@host'])
parser.parse()
assert parser.targets['host'] == Ssh('host')
-5
View File
@@ -1,5 +0,0 @@
from shlax.play import Play
def test_play_call():
-47
View File
@@ -1,47 +0,0 @@
import copy
class Action:
args = dict(
step=None,
)
class
user=dict(
doc='Username',
required=True,
),
steps=dict(
up='Started',
down='Stopped',
),
)
def __init__(self, *args, **kwargs):
self.args = args
self.kwargs = kwargs
def __call__(self, *args, **kwargs):
pass
class Target(Action):
def __call__(self, action):
action = copy.deepcopy(action)
action.target = self
class FakeAction(Action):
def __init__(self, user, path, *steps, **kwargs)
self.user = user
self.path = path
self.steps = steps
self.kwargs = kwargs
action = Action('root', '/test', 'up', 'rm')
target = Target()
-6
View File
@@ -1,6 +0,0 @@
import os
import sys
import pytest
if not os.getenv('CI'):
pytest.skip('Please run with ./shlaxfile.py test', allow_module_level=True)