This commit is contained in:
jpic 2020-02-20 13:18:53 +01:00
parent 2db971234a
commit ea061db51f
23 changed files with 356 additions and 263 deletions

View File

@ -5,7 +5,7 @@ setup(
name='shlax', name='shlax',
versioning='dev', versioning='dev',
setup_requires='setupmeta', setup_requires='setupmeta',
install_requires=['cli2>=1.1.6'], install_requires=['cli2'],
extras_require=dict( extras_require=dict(
full=[ full=[
'pyyaml', 'pyyaml',

View File

@ -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

View File

@ -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

View File

@ -6,9 +6,28 @@ 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 = {
@ -26,26 +45,12 @@ class Action:
), ),
) )
def __init__(self, *args, doc=None, **kwargs): def __init__(self, *args, **kwargs):
self.args = args self.args = args
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:
@ -100,77 +105,72 @@ class Action:
add(self) add(self)
return self.actions_filter(children, f, **filters) return self.actions_filter(children, f, **filters)
def __getattr__(self, name): async def __call__(self, *targets, **options):
for a in self.parents() + self.sibblings() + self.children(): if not targets:
if name in a.contextualize: from ..targets.localhost import Localhost
return getattr(a, name) targets = [Localhost()]
raise AttributeError(f'{type(self).__name__} has no {name}')
async def call(self, *args, **kwargs): output = Output(regexp=self.regexps, debug=True)
print(f'{self}.call(*args, **kwargs) not implemented') results = []
sys.exit(1) for target in targets:
target.output = output
def output_factory(self, *args, **kwargs): if len(targets) > 1:
kwargs.setdefault('regexps', self.regexps) output.prefix = target
return Output(**kwargs) from copy import deepcopy
action = deepcopy(self)
async def __call__(self, *args, **kwargs): action.target = target
self.call_args = list(self.call_args) + list(args) result = Result(action, target)
self.call_kwargs.update(kwargs) results.append(result)
self.output = self.output_factory(*args, **kwargs) action.result = result
self.output_start() action.output = output
self.status = 'running' 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:
result = await self.call(*args, **kwargs) await getattr(action, step)()
except Exception as e: except Exception as e:
self.output_fail(e) output.fail(action, e)
self.status = 'fail' action.result.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:
self.output_success() output.success(action)
if self.status == 'running': result.status = 'success'
self.status = 'success'
finally: finally:
clean = getattr(self, 'clean', None) clean = getattr(action, 'clean', None)
if clean: if clean:
self.output.clean(self) output.clean(action)
await clean(*args, **kwargs) await clean(target)
return result
def output_start(self): return results
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__] + list(self.args) + [ return ' '.join([type(self).__name__] + [
f'{k}={v}' 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([ return ' '.join([
self.output.colors['pink1'] colors['pink1']
+ type(self).__name__ + type(self).__name__
+ self.output.colors['yellow'] + '.'
] + list(self.args) + [ + self.step
f'{self.output.colors["blue"]}{k}{self.output.colors["gray"]}={self.output.colors["green2"]}{v}' + colors['yellow']
for k, v in self.kwargs_output().items() ] + [
] + [self.output.colors['reset']]) 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): def callable(self):
from ..targets import Localhost from ..targets import Localhost
@ -199,6 +199,7 @@ 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:
@ -209,17 +210,26 @@ 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'):
break
p.parent = parent p.parent = parent
break
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
def bind(self, *args): @class_or_instance_method
clone = deepcopy(self) def steps(self):
clone.call_args = args return {
return clone 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)
)
}

View File

@ -2,5 +2,6 @@ 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)

View File

@ -6,14 +6,22 @@ from .base import Action
class Htpasswd(Action): class Htpasswd(Action):
def __init__(self, path, user, *args, **kwargs): """Ensure a user is present in an htpasswd file."""
self.path = path display_variables = ('user', 'path')
self.user = user regexps = {
super().__init__(*args, **kwargs) 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 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: 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 + ':'):
@ -26,4 +34,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.exec(f'echo {line} >> {self.path}') await self.target.exec(f'echo {line} >> {self.path}')

View File

@ -12,14 +12,13 @@ from .base import Action
class Packages(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 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',
@ -106,12 +105,12 @@ class Packages(Action):
print(f'{self.container.name} | Waiting for update ...') print(f'{self.container.name} | Waiting for update ...')
await asyncio.sleep(1) await asyncio.sleep(1)
async def call(self, *args, **kwargs): async def apply(self):
cached = getattr(self, '_pagkages_mgr', None) cached = getattr(self.target, 'pkgmgr', None)
if cached: if cached:
self.mgr = cached self.mgr = cached
else: else:
mgr = await self.which(*self.mgrs.keys()) mgr = await self.target.which(*self.mgrs.keys())
if mgr: if mgr:
self.mgr = mgr[0].split('/')[-1] self.mgr = mgr[0].split('/')[-1]
@ -122,7 +121,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.rexec(self.cmds['upgrade']) await self.target.exec(self.cmds['upgrade'], user='root')
self._packages_upgraded = True self._packages_upgraded = True
packages = [] packages = []
@ -136,7 +135,7 @@ class Packages(Action):
else: else:
packages.append(package) 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): async def apk_setup(self):
cachedir = os.path.join(self.cache_root, self.mgr) cachedir = os.path.join(self.cache_root, self.mgr)

View File

@ -5,6 +5,8 @@ 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)

View File

@ -1,14 +1,14 @@
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): async def call(self, *args, **kwargs):
image = self.kwargs.get('image', None) image = self.kwargs.get('image', None)
if not image: if not image:
return await self.exec(*self.args, **self.kwargs) return await self.exec(*self.args, **self.kwargs)
from ..targets.buildah import Buildah
from ..targets.docker import Docker
if isinstance(image, Buildah): if isinstance(image, Buildah):
breakpoint() breakpoint()
result = await self.action(image, *args, **kwargs) result = await self.action(image, *args, **kwargs)

View File

@ -4,6 +4,9 @@ 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

View File

@ -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 is built mostly around 3 moving pieces:
shlax yourfile.py <action>: to execute a given action - Target: a target host and protocol
#!/usr/bin/env shlax: when making yourfile.py an executable. - 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 copy
import cli2
import inspect import inspect
import importlib import importlib
import glob import glob
import os import os
import sys import sys
from .exceptions import * from .actions.base import Action
from .shlaxfile import Shlaxfile from .exceptions import ShlaxException, WrongResult
from .targets import Localhost from .strategies import Script
class ConsoleScript(cli2.ConsoleScript): class ConsoleScript(cli2.ConsoleScript):
def __call__(self, *args, **kwargs): class Parser(cli2.Parser):
self.shlaxfile = None def __init__(self, *args, **kwargs):
shlaxfile = sys.argv.pop(1) if len(sys.argv) > 1 else '' self.targets = dict()
if shlaxfile: super().__init__(*args, **kwargs)
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.shlaxfile = Shlaxfile() def append(self, arg):
self.shlaxfile.parse(shlaxfile) if '=' not in arg and '@' in arg:
self._doc = inspect.getdoc(mod) 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'
self[name] = cli2.Callable( spec = arg
name,
child.callable(), mod = importlib.import_module('shlax.targets.' + kind)
options={ target = getattr(mod, kind.capitalize())(spec)
k: cli2.Option(name=k, **v) self.targets[str(target)] = target
for k, v in action.options.items() else:
}, super().append(arg)
color=getattr(action, 'color', cli2.YELLOW),
) def __call__(self):
for name, action in self.shlaxfile.actions.items(): if len(sys.argv) > 1 and os.path.exists(sys.argv[1]):
self[name] = cli2.Callable( pass
name, else:
action.callable(), scripts = glob.glob(os.path.join(
options={ os.path.dirname(__file__), 'actions', '*.py'))
k: cli2.Option(name=k, **v) for script in scripts:
for k, v in action.options.items() modname = script.split('/')[-1].replace('.py', '')
}, mod = importlib.import_module('shlax.actions.' + modname)
color=getattr(action, 'color', cli2.YELLOW), for key, value in mod.__dict__.items():
doc=inspect.getdoc(getattr(action, name, None)) or action._doc, if key.lower() != modname:
continue
break
self[modname] = cli2.Callable(
modname, self.action_class(value))
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: else:
from shlax import repo if len(value.steps()) == 1:
path = repo.__path__._path[0] self[modname][key] = cli2.Callable(
for shlaxfile in glob.glob(os.path.join(path, '*.py')): modname, self.action(value), doc=doc)
name = shlaxfile.split('/')[-1].split('.')[0] else:
mod = importlib.import_module('shlax.repo.' + name) self[modname][key] = cli2.Group('steps')
self[name] = cli2.Callable(name, mod) for step in value.steps():
self[modname][key][step] = cli2.Callable(
modname, self.action(value), doc='lol')
return super().__call__(*args, **kwargs) 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): def call(self, command):
kwargs = copy.copy(self.parser.funckwargs)
kwargs.update(self.parser.options)
try: try:
return command(*self.parser.funcargs, **kwargs) return super().call(command)
except WrongResult as e: except WrongResult as e:
print(e) print(e)
self.exit_code = e.proc.rc self.exit_code = e.proc.rc
@ -82,5 +167,4 @@ 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')

View File

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

View File

@ -104,7 +104,7 @@ class Output:
self.colors['purplebold'], self.colors['purplebold'],
'! TEST ', '! TEST ',
self.colors['reset'], self.colors['reset'],
action.colorized(), action.colorized(self.colors),
'\n', '\n',
])) ]))
@ -114,7 +114,7 @@ class Output:
self.colors['bluebold'], self.colors['bluebold'],
'+ CLEAN ', '+ CLEAN ',
self.colors['reset'], self.colors['reset'],
action.colorized(), action.colorized(self.colors),
'\n', '\n',
])) ]))
@ -124,7 +124,7 @@ class Output:
self.colors['orangebold'], self.colors['orangebold'],
'⚠ START ', '⚠ START ',
self.colors['reset'], self.colors['reset'],
action.colorized(), action.colorized(self.colors),
'\n', '\n',
])) ]))
@ -134,7 +134,7 @@ class Output:
self.colors['greenbold'], self.colors['greenbold'],
'✔ SUCCESS ', '✔ SUCCESS ',
self.colors['reset'], self.colors['reset'],
action.colorized() if hasattr(action, 'colorized') else str(action), action.colorized(self.colors) 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() if hasattr(action, 'colorized') else str(action), action.colorized(self.colors) if hasattr(action, 'colorized') else str(action),
'\n', '\n',
])) ]))

9
shlax/play.py Normal file
View File

@ -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 =

View File

@ -3,11 +3,15 @@
Manage a traefik container maintained by Shlax community. Manage a traefik container maintained by Shlax community.
""" """
from shlax import * from shlax.shortcuts 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',
@ -28,14 +32,4 @@ 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
shlax/result.py Normal file
View File

@ -0,0 +1,7 @@
class Result:
def __init__(self, action, target):
self.action = action
self.target = target
self.status = 'pending'

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
shlax/shortcuts.py Normal file
View File

@ -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

7
shlax/strategies/test.py Normal file
View File

@ -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)

View File

@ -1,4 +0,0 @@
from .buildah import Buildah
from .docker import Docker
from .localhost import Localhost
from .ssh import Ssh

View File

@ -7,7 +7,8 @@ from .localhost import Localhost
class Docker(Localhost): class Docker(Localhost):
contextualize = Localhost.contextualize + ['mnt', 'ctr', 'mount'] """Manage a docker container."""
default_steps = ['install', 'up']
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.image = kwargs.get('image', 'alpine') self.image = kwargs.get('image', 'alpine')
@ -51,33 +52,29 @@ class Docker(Localhost):
# raises=False # raises=False
# ) # )
# ).out.split('\n')[0] # ).out.split('\n')[0]
if step('install') and 'install' in self.kwargs:
breakpoint()
await self.action(self.kwargs['install'], *args, **kwargs)
if step('rm'): if step('rm') and await self.exists():
await self.rm(*args, **kwargs) await self.exec('docker', 'rm', '-f', self.name)
if step('down') and self.name:
await self.exec('docker', 'down', '-f', self.name)
if step('up'): 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) return await super().call(*args, **kwargs)
async def rm(self, *args, **kwargs): async def exists(self):
return await self.exec('docker', 'rm', '-f', self.name) proc = await self.exec(
'docker', 'ps', '-aq', '--filter',
async def down(self, *args, **kwargs): 'name=' + self.name,
"""Remove instance, except persistent data if any""" raises=False
if self.name: )
self.name = (await self.exec('docker', 'start', self.name)).out return bool(proc.out.strip())
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]
@ -97,3 +94,18 @@ 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

View File

@ -35,9 +35,7 @@ class Localhost(Script):
return args, kwargs return args, kwargs
async def exec(self, *args, **kwargs): async def exec(self, *args, **kwargs):
if 'debug' not in kwargs: kwargs['output'] = self.output
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):

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(
# python3.8 on centos with pip dance ... # dancing for pip on centos python3.8
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