3 Commits
Author SHA1 Message Date
DrClaw c28be15ed6 ... 2020-02-19 01:51:02 +01:00
DrClaw 3e980c9d14 Unbound mod variablewq 2020-02-19 01:12:31 +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
#test
_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', name='shlax',
versioning='dev', versioning='dev',
setup_requires='setupmeta', setup_requires='setupmeta',
install_requires=['cli2'], install_requires=['cli2>=1.1.6'],
extras_require=dict( extras_require=dict(
full=[ full=[
'pyyaml', '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 ..output import Output
from ..exceptions import WrongResult 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: class Action:
display_variables = []
hide_variables = ['output']
default_steps = ['apply']
parent = None parent = None
contextualize = [] contextualize = []
regexps = { regexps = {
@@ -42,21 +24,28 @@ class Action:
'''.strip(), '''.strip(),
immediate=True, 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.args = args
self.kwargs = kwargs self.kwargs = kwargs
for key, value in kwargs.items(): self.call_args = []
setattr(self, key, value) self.call_kwargs = {}
if isinstance(value, Action): self._doc = doc
getattr(self, key).shlaxstep = True 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): def actions_filter(self, results, f=None, **filters):
if f: if f:
@@ -111,78 +100,77 @@ class Action:
add(self) add(self)
return self.actions_filter(children, f, **filters) return self.actions_filter(children, f, **filters)
async def __call__(self, *targets, **options): def __getattr__(self, name):
if not targets: for a in self.parents() + self.sibblings() + self.children():
from ..targets.localhost import Localhost if name in a.contextualize:
targets = [Localhost()] return getattr(a, name)
raise AttributeError(f'{type(self).__name__} has no {name}')
output = Output( async def call(self, *args, **kwargs):
regexp=self.regexps, print(f'{self}.call(*args, **kwargs) not implemented')
debug='cmd,visit,out' if options['verbose'] else options['debug'], sys.exit(1)
)
results = [] def output_factory(self, *args, **kwargs):
for target in targets: kwargs.setdefault('regexps', self.regexps)
target.output = output return Output(**kwargs)
if len(targets) > 1:
output.prefix = target async def __call__(self, *args, **kwargs):
from copy import deepcopy self.call_args = list(self.call_args) + list(args)
action = deepcopy(self) self.call_kwargs.update(kwargs)
action.target = target self.output = self.output_factory(*args, **kwargs)
result = Result(action, target) self.output_start()
results.append(result) self.status = 'running'
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)
try: try:
if isinstance(getattr(action, step), Action): result = await self.call(*args, **kwargs)
await getattr(action, step)(**options)
else:
await getattr(action, step)()
except Exception as e: except Exception as e:
output.fail(action, e) self.output_fail(e)
action.result.status = 'fail' self.status = 'fail'
proc = getattr(e, 'proc', None) proc = getattr(e, 'proc', None)
if proc: if proc:
result = proc.rc result = proc.rc
else: else:
raise raise
else: else:
output.success(action) self.output_success()
result.status = 'success' if self.status == 'running':
self.status = 'success'
finally: finally:
clean = getattr(action, 'clean', None) clean = getattr(self, 'clean', None)
if clean: if clean:
output.clean(action) self.output.clean(self)
await clean(target) 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): def __repr__(self):
return ' '.join([type(self).__name__] + [ return ' '.join([type(self).__name__] + list(self.args) + [
f'{k}={v}' f'{k}={v}'
for k, v in self.__dict__.items() for k, v in self.kwargs.items()
if (k in self.display_variables or not self.display_variables)
and (k not in self.hide_variables)
]) ])
def colorized(self, colors): def colorized(self):
return ' '.join([ return ' '.join([
colors['pink1'] self.output.colors['pink1']
+ type(self).__name__ + type(self).__name__
+ '.' + self.output.colors['yellow']
+ self.step ] + list(self.args) + [
+ colors['yellow'] f'{self.output.colors["blue"]}{k}{self.output.colors["gray"]}={self.output.colors["green2"]}{v}'
] + [ for k, v in self.kwargs_output().items()
f'{colors["blue"]}{k}{colors["gray"]}={colors["green2"]}{v}' ] + [self.output.colors['reset']])
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']])
def callable(self): def callable(self):
from ..targets import Localhost from ..targets import Localhost
@@ -211,7 +199,6 @@ class Action:
def action(self, action, *args, **kwargs): def action(self, action, *args, **kwargs):
if isinstance(action, str): if isinstance(action, str):
# import dotted module path string to action
import cli2 import cli2
a = cli2.Callable.factory(action).target a = cli2.Callable.factory(action).target
if not a: if not a:
@@ -222,26 +209,17 @@ class Action:
action = a action = a
p = action(*args, **kwargs) p = action(*args, **kwargs)
p.parent = self
for parent in self.parents(): for parent in self.parents():
if hasattr(parent, 'actions'): if hasattr(parent, 'actions'):
p.parent = parent
break break
p.parent = parent
if 'actions' not in self.__dict__: if 'actions' not in self.__dict__:
# "mutate" to Strategy # "mutate" to Strategy
from ..strategies.script import Actions from ..strategies.script import Actions
self.actions = Actions(self, [p]) self.actions = Actions(self, [p])
return p return p
@class_or_instance_method def bind(self, *args):
def steps(self): clone = deepcopy(self)
return { clone.call_args = args
key: getattr(self, key) return clone
for key in dir(self)
if key != 'steps' # avoid recursion
and (
key in self.default_steps
or getattr(getattr(self, key), 'shlaxstep', False)
)
}
-1
View File
@@ -2,6 +2,5 @@ from .base import Action
class Copy(Action): class Copy(Action):
"""Copy files or directories to target."""
async def call(self, *args, **kwargs): async def call(self, *args, **kwargs):
await self.copy(*self.args) await self.copy(*self.args)
+6 -14
View File
@@ -6,22 +6,14 @@ from .base import Action
class Htpasswd(Action): class Htpasswd(Action):
"""Ensure a user is present in an htpasswd file.""" def __init__(self, path, user, *args, **kwargs):
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
self.path = path 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 found = False
htpasswd = await self.target.exec( htpasswd = await self.exec('cat', self.path, raises=False)
'cat', self.path, raises=False)
if htpasswd.rc == 0: if htpasswd.rc == 0:
for line in htpasswd.out.split('\n'): for line in htpasswd.out.split('\n'):
if line.startswith(self.user + ':'): if line.startswith(self.user + ':'):
@@ -34,4 +26,4 @@ class Htpasswd(Action):
) for i in range(20)) ) for i in range(20))
hashed = hashlib.sha1(self.password.encode('utf8')) hashed = hashlib.sha1(self.password.encode('utf8'))
line = f'{self.user}:\\$sha1\\${hashed.hexdigest()}' 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): 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 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 container visitors in order to pick up packages. For example, the Pip
visitor will declare ``self.packages = dict(apt=['python3-pip'])``, and the visitor will declare ``self.packages = dict(apt=['python3-pip'])``, and the
Packages visitor will pick it up. Packages visitor will pick it up.
""" """
contextualize = ['mgr']
regexps = { regexps = {
#r'Installing ([\w\d-]+)': '{cyan}\\1', #r'Installing ([\w\d-]+)': '{cyan}\\1',
r'Installing': '{cyan}lol', r'Installing': '{cyan}lol',
@@ -94,7 +95,7 @@ class Packages(Action):
f.write(str(os.getpid())) f.write(str(os.getpid()))
try: try:
await self.target.rexec(self.cmds['update']) await self.rexec(self.cmds['update'])
finally: finally:
os.unlink(lockfile) os.unlink(lockfile)
@@ -102,15 +103,15 @@ class Packages(Action):
f.write(str(now)) f.write(str(now))
else: else:
while os.path.exists(lockfile): while os.path.exists(lockfile):
print(f'{self.target} | Waiting for {lockfile} ...') print(f'{self.container.name} | Waiting for update ...')
await asyncio.sleep(1) await asyncio.sleep(1)
async def apply(self): async def call(self, *args, **kwargs):
cached = getattr(self.target, 'pkgmgr', None) cached = getattr(self, '_pagkages_mgr', None)
if cached: if cached:
self.mgr = cached self.mgr = cached
else: else:
mgr = await self.target.which(*self.mgrs.keys()) mgr = await self.which(*self.mgrs.keys())
if mgr: if mgr:
self.mgr = mgr[0].split('/')[-1] self.mgr = mgr[0].split('/')[-1]
@@ -121,7 +122,7 @@ class Packages(Action):
if not getattr(self, '_packages_upgraded', None): if not getattr(self, '_packages_upgraded', None):
await self.update() await self.update()
if self.kwargs.get('upgrade', True): 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 self._packages_upgraded = True
packages = [] packages = []
@@ -135,7 +136,7 @@ class Packages(Action):
else: else:
packages.append(package) 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): async def apk_setup(self):
cachedir = os.path.join(self.cache_root, self.mgr) cachedir = os.path.join(self.cache_root, self.mgr)
-2
View File
@@ -5,8 +5,6 @@ from .base import Action
class Pip(Action): class Pip(Action):
"""Pip abstraction layer."""
def __init__(self, *pip_packages, pip=None, requirements=None): def __init__(self, *pip_packages, pip=None, requirements=None):
self.requirements = requirements self.requirements = requirements
super().__init__(*pip_packages, pip=pip, 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 from .base import Action
class Run(Action): class Run(Action):
"""Run a script or command on a target.""" async def call(self, *args, **kwargs):
def __init__(self, *args, image=None, **kwargs): image = self.kwargs.get('image', None)
super().__init__(**kwargs) if not image:
self.args = args return await self.exec(*self.args, **self.kwargs)
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
if isinstance(image, Buildah): if isinstance(image, Buildah):
breakpoint()
result = await self.action(image, *args, **kwargs) result = await self.action(image, *args, **kwargs)
return await Docker( return await Docker(
-3
View File
@@ -4,9 +4,6 @@ from .base import Action
class Service(Action): class Service(Action):
"""
Manage a systemd service.
"""
def __init__(self, *names, state=None): def __init__(self, *names, state=None):
self.state = state or 'started' self.state = state or 'started'
self.names = names 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: shlax yourfile.py: to list actions you have declared.
- Target: a target host and protocol shlax yourfile.py <action>: to execute a given action
- Action: execute a shlax action #!/usr/bin/env shlax: when making yourfile.py an executable.
- Strategy: defines how to apply actions on targets (scripted only) '''
Shlax executes mostly in 3 ways: import asyncio
- 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 cli2 import cli2
import copy
import inspect import inspect
import importlib import importlib
import glob import glob
import os import os
import sys import sys
from .actions.base import Action from .exceptions import *
from .exceptions import ShlaxException, WrongResult from .shlaxfile import Shlaxfile
from .strategies import Script from .targets import Localhost
class ConsoleScript(cli2.ConsoleScript): class ConsoleScript(cli2.ConsoleScript):
class Parser(cli2.Parser): def __call__(self, *args, **kwargs):
def __init__(self, *args, **kwargs): self.shlaxfile = None
self.targets = dict() shlaxfile = sys.argv.pop(1) if len(sys.argv) > 1 else ''
super().__init__(*args, **kwargs) 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): self.shlaxfile = Shlaxfile()
if '=' not in arg and '@' in arg: self.shlaxfile.parse(shlaxfile)
if '://' in arg: if 'main' in self.shlaxfile.actions:
kind, spec = arg.split('://') action = self.shlaxfile.actions['main']
else: for name, child in self.shlaxfile.actions['main'].menu.items():
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[name] = cli2.Callable( self[name] = cli2.Callable(
name, name,
self.action(value), child.callable(),
doc=type(value).__doc__,
options={ options={
option: cli2.Option(option, **cfg) k: cli2.Option(name=k, **v)
for option, cfg in value.options.items() for k, v in action.options.items()
} },
color=getattr(action, 'color', cli2.YELLOW),
) )
#self[name] = value for name, action in self.shlaxfile.actions.items():
#elif callable(value) and getattr(value, '__name__', '').startswith('test_'): self[name] = cli2.Callable(
# self.tests[value.__name__] = value name,
action.callable(),
#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),
options={ options={
option: cli2.Option(option, **cfg) k: cli2.Option(name=k, **v)
for option, cfg in value.options.items() for k, v in action.options.items()
} },
) color=getattr(action, 'color', cli2.YELLOW),
doc=inspect.getdoc(getattr(action, name, None)) or action._doc,
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()
}
) )
else: else:
for name, step in value.steps().items(): from shlax import repo
if isinstance(step, Action): path = repo.__path__._path[0]
self[modname][name] = cli2.Callable( for shlaxfile in glob.glob(os.path.join(path, '*.py')):
modname, name = shlaxfile.split('/')[-1].split('.')[0]
self.action(step), mod = importlib.import_module('shlax.repo.' + name)
doc=inspect.getdoc(step), self[name] = cli2.Callable(name, mod)
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()
}
)
return super().__call__() return super().__call__(*args, **kwargs)
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
def call(self, command): def call(self, command):
kwargs = copy.copy(self.parser.funckwargs)
kwargs.update(self.parser.options)
try: try:
return super().call(command) return command(*self.parser.funcargs, **kwargs)
except WrongResult as e: except WrongResult as e:
print(e) print(e)
self.exit_code = e.proc.rc self.exit_code = e.proc.rc
@@ -243,4 +82,5 @@ class ConsoleScript(cli2.ConsoleScript):
print(e) print(e)
self.exit_code = 1 self.exit_code = 1
cli = ConsoleScript(__doc__).add_module('shlax.cli') 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] 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 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.out
msg += '\n' + proc.err msg += '\n' + proc.err
+5 -5
View File
@@ -104,7 +104,7 @@ class Output:
self.colors['purplebold'], self.colors['purplebold'],
'! TEST ', '! TEST ',
self.colors['reset'], self.colors['reset'],
action.colorized(self.colors), action.colorized(),
'\n', '\n',
])) ]))
@@ -114,7 +114,7 @@ class Output:
self.colors['bluebold'], self.colors['bluebold'],
'+ CLEAN ', '+ CLEAN ',
self.colors['reset'], self.colors['reset'],
action.colorized(self.colors), action.colorized(),
'\n', '\n',
])) ]))
@@ -124,7 +124,7 @@ class Output:
self.colors['orangebold'], self.colors['orangebold'],
'⚠ START ', '⚠ START ',
self.colors['reset'], self.colors['reset'],
action.colorized(self.colors), action.colorized(),
'\n', '\n',
])) ]))
@@ -134,7 +134,7 @@ class Output:
self.colors['greenbold'], self.colors['greenbold'],
'✔ SUCCESS ', '✔ SUCCESS ',
self.colors['reset'], self.colors['reset'],
action.colorized(self.colors) if hasattr(action, 'colorized') else str(action), action.colorized() if hasattr(action, 'colorized') else str(action),
'\n', '\n',
])) ]))
@@ -144,6 +144,6 @@ class Output:
self.colors['redbold'], self.colors['redbold'],
'✘ FAIL ', '✘ FAIL ',
self.colors['reset'], self.colors['reset'],
action.colorized(self.colors) if hasattr(action, 'colorized') else str(action), action.colorized() if hasattr(action, 'colorized') else str(action),
'\n', '\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 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.output = output or Output()
self.cmd = ' '.join(args) self.cmd = ' '.join(args)
self.args = args self.args = args
@@ -86,7 +87,7 @@ class Proc:
if self.called: if self.called:
raise Exception('Already called: ' + self.cmd) 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) self.output.cmd(self.cmd)
if self.test: if self.test:
+11 -5
View File
@@ -3,15 +3,11 @@
Manage a traefik container maintained by Shlax community. Manage a traefik container maintained by Shlax community.
""" """
from shlax.shortcuts import * from shlax import *
main = Docker( main = Docker(
name='traefik', name='traefik',
image='traefik:v2.0.0', image='traefik:v2.0.0',
install=Htpasswd(
'./htpasswd', 'root', doc='Install root user in ./htpasswd'
),
networks=['web'], networks=['web'],
command=[ command=[
'--entrypoints.web.address=:80', '--entrypoints.web.address=:80',
@@ -32,4 +28,14 @@ main = Docker(
'traefik.http.routers.traefik.service=api@internal', 'traefik.http.routers.traefik.service=api@internal',
'traefik.http.routers.traefik.entrypoints=web', '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) spec.loader.exec_module(mod)
for name, value in mod.__dict__.items(): for name, value in mod.__dict__.items():
if isinstance(value, Action): if isinstance(value, Action):
value.__name__ = name value.name = name
self.actions[name] = value self.actions[name] = value
elif callable(value) and getattr(value, '__name__', '').startswith('test_'): elif callable(value) and getattr(value, '__name__', '').startswith('test_'):
self.tests[value.__name__] = value 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): class Docker(Localhost):
"""Manage a docker container.""" contextualize = Localhost.contextualize + ['mnt', 'ctr', 'mount']
default_steps = ['install', 'up']
contextualize = ['image', 'home']
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.image = kwargs.get('image', 'alpine') self.image = kwargs.get('image', 'alpine')
@@ -53,28 +51,33 @@ class Docker(Localhost):
# raises=False # raises=False
# ) # )
# ).out.split('\n')[0] # ).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(): if step('rm'):
await self.exec('docker', 'rm', '-f', self.name) await self.rm(*args, **kwargs)
if step('down') and self.name:
await self.exec('docker', 'down', '-f', self.name)
if step('up'): if step('up'):
if await self.exists(): await self.up(*args, **kwargs)
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
return await super().call(*args, **kwargs) return await super().call(*args, **kwargs)
async def exists(self): async def rm(self, *args, **kwargs):
proc = await self.exec( return await self.exec('docker', 'rm', '-f', self.name)
'docker', 'ps', '-aq', '--filter',
'name=' + self.name, async def down(self, *args, **kwargs):
raises=False """Remove instance, except persistent data if any"""
) if self.name:
return bool(proc.out.strip()) 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): async def copy(self, *args):
src = args[:-1] src = args[:-1]
@@ -94,18 +97,3 @@ class Docker(Localhost):
procs.append(self.exec(*args)) procs.append(self.exec(*args))
return await asyncio.gather(*procs) 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 return args, kwargs
async def exec(self, *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) args, kwargs = self.shargs(*args, **kwargs)
proc = await Proc(*args, **kwargs)() proc = await Proc(*args, **kwargs)()
if kwargs.get('wait', True): if kwargs.get('wait', True):
+1 -1
View File
@@ -12,7 +12,7 @@ build = Buildah(
'quay.io/podman/stable', 'quay.io/podman/stable',
Packages('python38', 'buildah', 'unzip', 'findutils', 'python3-yaml', upgrade=False), Packages('python38', 'buildah', 'unzip', 'findutils', 'python3-yaml', upgrade=False),
Async( Async(
# dancing for pip on centos python3.8 # python3.8 on centos with pip dance ...
Run(''' Run('''
curl -o setuptools.zip https://files.pythonhosted.org/packages/42/3e/2464120172859e5d103e5500315fb5555b1e908c0dacc73d80d35a9480ca/setuptools-45.1.0.zip curl -o setuptools.zip https://files.pythonhosted.org/packages/42/3e/2464120172859e5d103e5500315fb5555b1e908c0dacc73d80d35a9480ca/setuptools-45.1.0.zip
unzip setuptools.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)