From ea061db51f6e296a8fe52a491b5b27534cf67ca2 Mon Sep 17 00:00:00 2001 From: jpic Date: Thu, 20 Feb 2020 13:18:53 +0100 Subject: [PATCH] wip --- setup.py | 2 +- shlax/__init__.py | 7 -- shlax/actions/__init__.py | 7 -- shlax/actions/base.py | 182 +++++++++++++++++---------------- shlax/actions/copy.py | 1 + shlax/actions/htpasswd.py | 22 ++-- shlax/actions/packages.py | 13 ++- shlax/actions/pip.py | 2 + shlax/actions/run.py | 6 +- shlax/actions/service.py | 3 + shlax/cli.py | 204 ++++++++++++++++++++++++++----------- shlax/contrib/gitlab.py | 37 ------- shlax/output.py | 10 +- shlax/play.py | 9 ++ shlax/repo/traefik.py | 16 +-- shlax/result.py | 7 ++ shlax/shlaxfile.py | 2 +- shlax/shortcuts.py | 14 +++ shlax/strategies/test.py | 7 ++ shlax/targets/__init__.py | 4 - shlax/targets/docker.py | 58 ++++++----- shlax/targets/localhost.py | 4 +- shlaxfile.py | 2 +- 23 files changed, 356 insertions(+), 263 deletions(-) delete mode 100644 shlax/__init__.py delete mode 100644 shlax/contrib/gitlab.py create mode 100644 shlax/play.py create mode 100644 shlax/result.py create mode 100644 shlax/shortcuts.py create mode 100644 shlax/strategies/test.py diff --git a/setup.py b/setup.py index 1eed12a..b15babf 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name='shlax', versioning='dev', setup_requires='setupmeta', - install_requires=['cli2>=1.1.6'], + install_requires=['cli2'], extras_require=dict( full=[ 'pyyaml', diff --git a/shlax/__init__.py b/shlax/__init__.py deleted file mode 100644 index 0021973..0000000 --- a/shlax/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -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 diff --git a/shlax/actions/__init__.py b/shlax/actions/__init__.py index f3514b0..e69de29 100644 --- a/shlax/actions/__init__.py +++ b/shlax/actions/__init__.py @@ -1,7 +0,0 @@ -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 diff --git a/shlax/actions/base.py b/shlax/actions/base.py index b09fec2..51b2ad8 100644 --- a/shlax/actions/base.py +++ b/shlax/actions/base.py @@ -6,9 +6,28 @@ 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 = { @@ -26,26 +45,12 @@ class Action: ), ) - def __init__(self, *args, doc=None, **kwargs): + def __init__(self, *args, **kwargs): self.args = args - self.kwargs = kwargs - 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 + for key, value in kwargs.items(): + setattr(self, key, value) + if isinstance(value, Action): + getattr(self, key).shlaxstep = True def actions_filter(self, results, f=None, **filters): if f: @@ -100,77 +105,72 @@ class Action: add(self) return self.actions_filter(children, f, **filters) - 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}') + async def __call__(self, *targets, **options): + if not targets: + from ..targets.localhost import Localhost + targets = [Localhost()] - async def call(self, *args, **kwargs): - print(f'{self}.call(*args, **kwargs) not implemented') - sys.exit(1) + output = Output(regexp=self.regexps, debug=True) + 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) + try: + await getattr(action, step)() + except Exception as e: + output.fail(action, e) + action.result.status = 'fail' + proc = getattr(e, 'proc', None) + if proc: + result = proc.rc + else: + raise + else: + output.success(action) + result.status = 'success' + finally: + clean = getattr(action, 'clean', None) + if clean: + output.clean(action) + await clean(target) - 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: - result = await self.call(*args, **kwargs) - except Exception as e: - self.output_fail(e) - self.status = 'fail' - proc = getattr(e, 'proc', None) - if proc: - result = proc.rc - else: - raise - else: - self.output_success() - if self.status == 'running': - self.status = 'success' - finally: - clean = getattr(self, 'clean', None) - if clean: - self.output.clean(self) - await clean(*args, **kwargs) - return result - - 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) + return results def __repr__(self): - return ' '.join([type(self).__name__] + list(self.args) + [ + return ' '.join([type(self).__name__] + [ f'{k}={v}' - for k, v in self.kwargs.items() + 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) ]) - def colorized(self): + def colorized(self, colors): return ' '.join([ - self.output.colors['pink1'] + colors['pink1'] + type(self).__name__ - + 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']]) + + '.' + + 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']]) def callable(self): from ..targets import Localhost @@ -199,6 +199,7 @@ 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: @@ -209,17 +210,26 @@ 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 - def bind(self, *args): - clone = deepcopy(self) - clone.call_args = args - return clone + @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) + ) + } diff --git a/shlax/actions/copy.py b/shlax/actions/copy.py index c325f03..cbdace0 100644 --- a/shlax/actions/copy.py +++ b/shlax/actions/copy.py @@ -2,5 +2,6 @@ from .base import Action class Copy(Action): + """Copy files or directories to target.""" async def call(self, *args, **kwargs): await self.copy(*self.args) diff --git a/shlax/actions/htpasswd.py b/shlax/actions/htpasswd.py index 54d6964..9f251c8 100644 --- a/shlax/actions/htpasswd.py +++ b/shlax/actions/htpasswd.py @@ -6,14 +6,22 @@ from .base import Action class Htpasswd(Action): - def __init__(self, path, user, *args, **kwargs): - self.path = path - self.user = user - super().__init__(*args, **kwargs) + """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', + } - async def call(self, *args, **kwargs): + def __init__(self, user, path, **kwargs): + self.user = user + self.path = path + super().__init__(**kwargs) + + async def apply(self): found = False - htpasswd = await self.exec('cat', self.path, raises=False) + htpasswd = await self.target.exec( + 'cat', self.path, raises=False) if htpasswd.rc == 0: for line in htpasswd.out.split('\n'): if line.startswith(self.user + ':'): @@ -26,4 +34,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.exec(f'echo {line} >> {self.path}') + await self.target.exec(f'echo {line} >> {self.path}') diff --git a/shlax/actions/packages.py b/shlax/actions/packages.py index fc11109..a7ff83c 100644 --- a/shlax/actions/packages.py +++ b/shlax/actions/packages.py @@ -12,14 +12,13 @@ from .base import Action class Packages(Action): """ - The Packages visitor wraps around the container's package manager. + Package manager abstract layer with caching. 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', @@ -106,12 +105,12 @@ class Packages(Action): print(f'{self.container.name} | Waiting for update ...') await asyncio.sleep(1) - async def call(self, *args, **kwargs): - cached = getattr(self, '_pagkages_mgr', None) + async def apply(self): + cached = getattr(self.target, 'pkgmgr', None) if cached: self.mgr = cached else: - mgr = await self.which(*self.mgrs.keys()) + mgr = await self.target.which(*self.mgrs.keys()) if mgr: self.mgr = mgr[0].split('/')[-1] @@ -122,7 +121,7 @@ class Packages(Action): if not getattr(self, '_packages_upgraded', None): await self.update() if self.kwargs.get('upgrade', True): - await self.rexec(self.cmds['upgrade']) + await self.target.exec(self.cmds['upgrade'], user='root') self._packages_upgraded = True packages = [] @@ -136,7 +135,7 @@ class Packages(Action): else: packages.append(package) - await self.rexec(*self.cmds['install'].split(' ') + packages) + await self.target.exec(*self.cmds['install'].split(' ') + packages, user='root') async def apk_setup(self): cachedir = os.path.join(self.cache_root, self.mgr) diff --git a/shlax/actions/pip.py b/shlax/actions/pip.py index c74815c..3ef9887 100644 --- a/shlax/actions/pip.py +++ b/shlax/actions/pip.py @@ -5,6 +5,8 @@ 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) diff --git a/shlax/actions/run.py b/shlax/actions/run.py index e3f8730..e30f252 100644 --- a/shlax/actions/run.py +++ b/shlax/actions/run.py @@ -1,14 +1,14 @@ -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.""" async def call(self, *args, **kwargs): image = self.kwargs.get('image', None) if not image: return await self.exec(*self.args, **self.kwargs) + from ..targets.buildah import Buildah + from ..targets.docker import Docker if isinstance(image, Buildah): breakpoint() result = await self.action(image, *args, **kwargs) diff --git a/shlax/actions/service.py b/shlax/actions/service.py index 3561c64..1fe3184 100644 --- a/shlax/actions/service.py +++ b/shlax/actions/service.py @@ -4,6 +4,9 @@ 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 diff --git a/shlax/cli.py b/shlax/cli.py index 283c2f8..b8080db 100644 --- a/shlax/cli.py +++ b/shlax/cli.py @@ -1,80 +1,165 @@ -''' -shlax is a micro-framework to orchestrate commands. +""" +Shlax automation tool manual - shlax yourfile.py: to list actions you have declared. - shlax yourfile.py : to execute a given action - #!/usr/bin/env shlax: when making yourfile.py an executable. -''' +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 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 asyncio -import cli2 import copy +import cli2 import inspect import importlib import glob import os import sys -from .exceptions import * -from .shlaxfile import Shlaxfile -from .targets import Localhost +from .actions.base import Action +from .exceptions import ShlaxException, WrongResult +from .strategies import Script class ConsoleScript(cli2.ConsoleScript): - 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__ + class Parser(cli2.Parser): + def __init__(self, *args, **kwargs): + self.targets = dict() + super().__init__(*args, **kwargs) - self.shlaxfile = Shlaxfile() - self.shlaxfile.parse(shlaxfile) - self._doc = inspect.getdoc(mod) - 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, - child.callable(), - options={ - k: cli2.Option(name=k, **v) - for k, v in action.options.items() - }, - color=getattr(action, 'color', cli2.YELLOW), - ) - for name, action in self.shlaxfile.actions.items(): - self[name] = cli2.Callable( - name, - action.callable(), - options={ - 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, - ) + 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(sys.argv) > 1 and os.path.exists(sys.argv[1]): + pass else: - 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) + scripts = glob.glob(os.path.join( + os.path.dirname(__file__), 'actions', '*.py')) + for script in scripts: + modname = script.split('/')[-1].replace('.py', '') + mod = importlib.import_module('shlax.actions.' + modname) + for key, value in mod.__dict__.items(): + if key.lower() != modname: + continue + break + self[modname] = cli2.Callable( + modname, self.action_class(value)) - return super().__call__(*args, **kwargs) + 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) + else: + for name, method in value.steps().items(): + self[modname][name] = cli2.Callable( + modname, self.action(value), + doc=inspect.getdoc(method) + ) + else: + if len(value.steps()) == 1: + self[modname][key] = cli2.Callable( + modname, self.action(value), doc=doc) + else: + self[modname][key] = cli2.Group('steps') + for step in value.steps(): + self[modname][key][step] = cli2.Callable( + modname, self.action(value), doc='lol') + + 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) + 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] + return cb def call(self, command): - kwargs = copy.copy(self.parser.funckwargs) - kwargs.update(self.parser.options) try: - return command(*self.parser.funcargs, **kwargs) + return super().call(command) except WrongResult as e: print(e) self.exit_code = e.proc.rc @@ -82,5 +167,4 @@ class ConsoleScript(cli2.ConsoleScript): print(e) self.exit_code = 1 - cli = ConsoleScript(__doc__).add_module('shlax.cli') diff --git a/shlax/contrib/gitlab.py b/shlax/contrib/gitlab.py deleted file mode 100644 index 000efc0..0000000 --- a/shlax/contrib/gitlab.py +++ /dev/null @@ -1,37 +0,0 @@ -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__ diff --git a/shlax/output.py b/shlax/output.py index 9a8955f..530fcfb 100644 --- a/shlax/output.py +++ b/shlax/output.py @@ -104,7 +104,7 @@ class Output: self.colors['purplebold'], '! TEST ', self.colors['reset'], - action.colorized(), + action.colorized(self.colors), '\n', ])) @@ -114,7 +114,7 @@ class Output: self.colors['bluebold'], '+ CLEAN ', self.colors['reset'], - action.colorized(), + action.colorized(self.colors), '\n', ])) @@ -124,7 +124,7 @@ class Output: self.colors['orangebold'], '⚠ START ', self.colors['reset'], - action.colorized(), + action.colorized(self.colors), '\n', ])) @@ -134,7 +134,7 @@ class Output: self.colors['greenbold'], '✔ SUCCESS ', self.colors['reset'], - action.colorized() if hasattr(action, 'colorized') else str(action), + action.colorized(self.colors) if hasattr(action, 'colorized') else str(action), '\n', ])) @@ -144,6 +144,6 @@ class Output: self.colors['redbold'], '✘ FAIL ', self.colors['reset'], - action.colorized() if hasattr(action, 'colorized') else str(action), + action.colorized(self.colors) if hasattr(action, 'colorized') else str(action), '\n', ])) diff --git a/shlax/play.py b/shlax/play.py new file mode 100644 index 0000000..5006bf5 --- /dev/null +++ b/shlax/play.py @@ -0,0 +1,9 @@ + +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 = diff --git a/shlax/repo/traefik.py b/shlax/repo/traefik.py index eccab07..23acaeb 100755 --- a/shlax/repo/traefik.py +++ b/shlax/repo/traefik.py @@ -3,11 +3,15 @@ Manage a traefik container maintained by Shlax community. """ -from shlax import * +from shlax.shortcuts 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', @@ -28,14 +32,4 @@ 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') diff --git a/shlax/result.py b/shlax/result.py new file mode 100644 index 0000000..2b15130 --- /dev/null +++ b/shlax/result.py @@ -0,0 +1,7 @@ + + +class Result: + def __init__(self, action, target): + self.action = action + self.target = target + self.status = 'pending' diff --git a/shlax/shlaxfile.py b/shlax/shlaxfile.py index 0bb95fb..7d4c6da 100644 --- a/shlax/shlaxfile.py +++ b/shlax/shlaxfile.py @@ -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 diff --git a/shlax/shortcuts.py b/shlax/shortcuts.py new file mode 100644 index 0000000..02989c0 --- /dev/null +++ b/shlax/shortcuts.py @@ -0,0 +1,14 @@ +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 + + diff --git a/shlax/strategies/test.py b/shlax/strategies/test.py new file mode 100644 index 0000000..c84fc4a --- /dev/null +++ b/shlax/strategies/test.py @@ -0,0 +1,7 @@ +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) diff --git a/shlax/targets/__init__.py b/shlax/targets/__init__.py index 3f7353f..e69de29 100644 --- a/shlax/targets/__init__.py +++ b/shlax/targets/__init__.py @@ -1,4 +0,0 @@ -from .buildah import Buildah -from .docker import Docker -from .localhost import Localhost -from .ssh import Ssh diff --git a/shlax/targets/docker.py b/shlax/targets/docker.py index f97d24d..ef1c68c 100644 --- a/shlax/targets/docker.py +++ b/shlax/targets/docker.py @@ -7,7 +7,8 @@ from .localhost import Localhost class Docker(Localhost): - contextualize = Localhost.contextualize + ['mnt', 'ctr', 'mount'] + """Manage a docker container.""" + default_steps = ['install', 'up'] def __init__(self, *args, **kwargs): self.image = kwargs.get('image', 'alpine') @@ -51,33 +52,29 @@ class Docker(Localhost): # raises=False # ) # ).out.split('\n')[0] + if step('install') and 'install' in self.kwargs: + breakpoint() + await self.action(self.kwargs['install'], *args, **kwargs) - if step('rm'): - await self.rm(*args, **kwargs) - - if step('down') and self.name: - await self.exec('docker', 'down', '-f', self.name) + if step('rm') and await self.exists(): + await self.exec('docker', 'rm', '-f', self.name) if step('up'): - await self.up(*args, **kwargs) + 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 return await super().call(*args, **kwargs) - 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 exists(self): + proc = await self.exec( + 'docker', 'ps', '-aq', '--filter', + 'name=' + self.name, + raises=False + ) + return bool(proc.out.strip()) async def copy(self, *args): src = args[:-1] @@ -97,3 +94,18 @@ 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 diff --git a/shlax/targets/localhost.py b/shlax/targets/localhost.py index 829e78f..01a4db1 100644 --- a/shlax/targets/localhost.py +++ b/shlax/targets/localhost.py @@ -35,9 +35,7 @@ class Localhost(Script): return args, kwargs async def exec(self, *args, **kwargs): - if 'debug' not in kwargs: - kwargs['debug'] = getattr(self, 'call_kwargs', {}).get('debug', False) - kwargs.setdefault('output', self.output) + kwargs['output'] = self.output args, kwargs = self.shargs(*args, **kwargs) proc = await Proc(*args, **kwargs)() if kwargs.get('wait', True): diff --git a/shlaxfile.py b/shlaxfile.py index 3921ed5..41eee8f 100755 --- a/shlaxfile.py +++ b/shlaxfile.py @@ -12,7 +12,7 @@ build = Buildah( 'quay.io/podman/stable', Packages('python38', 'buildah', 'unzip', 'findutils', 'python3-yaml', upgrade=False), Async( - # python3.8 on centos with pip dance ... + # dancing for pip on centos python3.8 Run(''' curl -o setuptools.zip https://files.pythonhosted.org/packages/42/3e/2464120172859e5d103e5500315fb5555b1e908c0dacc73d80d35a9480ca/setuptools-45.1.0.zip unzip setuptools.zip