6 Commits
Author SHA1 Message Date
jpic ea061db51f wip 2020-02-20 13:18:53 +01:00
jpic 2db971234a Trying to get somewhere with traefik module 2020-02-17 01:00:59 +01:00
jpic 41ec8db301 Magic command line 2020-02-16 21:34:41 +01:00
jpic 33c37f8e44 wip 2020-02-16 20:54:12 +01:00
jpic f5ab14d383 Trying to enrich shlax command in a scalable fashion 2020-02-16 20:53:49 +01:00
jpic 97255866f8 Remove old code 2020-02-16 20:14:20 +01:00
24 changed files with 478 additions and 301 deletions
-7
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
-6
View File
@@ -1,6 +0,0 @@
from .copy import Copy
from .packages import Packages # noqa
from .base import Action # noqa
from .run import Run # noqa
from .pip import Pip
from .service import Service
+97 -73
View File
@@ -1,3 +1,4 @@
from copy import deepcopy
import functools import functools
import inspect import inspect
import importlib import importlib
@@ -5,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 = {
@@ -27,16 +47,10 @@ class Action:
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.args = args self.args = args
self.kwargs = kwargs for key, value in kwargs.items():
setattr(self, key, value)
@property if isinstance(value, Action):
def context(self): getattr(self, key).shlaxstep = True
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:
@@ -91,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
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): return results
kwargs.setdefault('regexps', self.regexps)
return Output(**kwargs)
async def __call__(self, *args, **kwargs):
self.call_args = args
self.call_kwargs = 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)
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
@@ -190,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:
@@ -200,12 +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'):
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 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)
)
}
+1
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)
+37
View File
@@ -0,0 +1,37 @@
import hashlib
import secrets
import string
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
self.path = path
super().__init__(**kwargs)
async def apply(self):
found = 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 + ':'):
found = True
break
if not found:
self.password = ''.join(secrets.choice(
string.ascii_letters + string.digits
) 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}')
+6 -7
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)
+2
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)
+3 -3
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)
+3
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
+149 -125
View File
@@ -1,140 +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 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
async def runall(*args, **kwargs):
for name, action in cli.shlaxfile.actions.items():
await Localhost(action)(*args, **kwargs)
@cli2.option('debug', alias='d', help='Display debug output.')
async def test(*args, **kwargs):
"""Run podctl test over a bunch of paths."""
report = []
for arg in args:
candidates = [
os.path.join(os.getcwd(), arg, 'pod.py'),
os.path.join(os.getcwd(), arg, 'pod_test.py'),
]
for candidate in candidates:
if not os.path.exists(candidate):
continue
podfile = Podfile.factory(candidate)
# disable push
for name, container in podfile.containers.items():
commit = container.visitor('commit')
if commit:
commit.push = False
output.print(
'\n\x1b[1;38;5;160;48;5;118m BUILD START \x1b[0m'
+ ' ' + podfile.path + '\n'
)
old_exit_code = console_script.exit_code
console_script.exit_code = 0
try:
await podfile.pod.script('build')()
except Exception as e:
report.append(('build ' + candidate, False))
continue
if console_script.exit_code != 0:
report.append(('build ' + candidate, False))
continue
console_script.exit_code = old_exit_code
for name, test in podfile.tests.items():
name = '::'.join([podfile.path, name])
output.print(
'\n\x1b[1;38;5;160;48;5;118m TEST START \x1b[0m'
+ ' ' + name + '\n'
)
try:
await test(podfile.pod)
except Exception as e:
report.append((name, False))
output.print('\x1b[1;38;5;15;48;5;196m TEST FAIL \x1b[0m' + name)
else:
report.append((name, True))
output.print('\x1b[1;38;5;200;48;5;44m TEST SUCCESS \x1b[0m' + name)
output.print('\n')
print('\n')
for name, success in report:
if success:
output.print('\n\x1b[1;38;5;200;48;5;44m TEST SUCCESS \x1b[0m' + name)
else:
output.print('\n\x1b[1;38;5;15;48;5;196m TEST FAIL \x1b[0m' + name)
print('\n')
success = [*filter(lambda i: i[1], report)]
failures = [*filter(lambda i: not i[1], report)]
output.print(
'\n\x1b[1;38;5;200;48;5;44m TEST TOTAL: \x1b[0m'
+ str(len(report))
)
if success:
output.print(
'\n\x1b[1;38;5;200;48;5;44m TEST SUCCESS: \x1b[0m'
+ str(len(success))
)
if failures:
output.print(
'\n\x1b[1;38;5;15;48;5;196m TEST FAIL: \x1b[0m'
+ str(len(failures))
)
if failures:
console_script.exit_code = 1
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 os.path.exists(shlaxfile.split('::')[0]): super().__init__(*args, **kwargs)
self.shlaxfile = Shlaxfile()
self.shlaxfile.parse(shlaxfile) def append(self, arg):
for name, action in self.shlaxfile.actions.items(): if '=' not in arg and '@' in arg:
self[name] = cli2.Callable( if '://' in arg:
name, kind, spec = arg.split('://')
action.callable(), else:
options={ kind = 'ssh'
k: cli2.Option(name=k, **v) spec = arg
for k, v in action.options.items()
}, mod = importlib.import_module('shlax.targets.' + kind)
color=getattr(action, 'color', cli2.YELLOW), target = getattr(mod, kind.capitalize())(spec)
) self.targets[str(target)] = target
return super().__call__(*args, **kwargs) else:
super().append(arg)
def __call__(self):
if len(sys.argv) > 1 and os.path.exists(sys.argv[1]):
pass
else:
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))
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): 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
@@ -142,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')
-37
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__
+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(), 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
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 =
+35
View File
@@ -0,0 +1,35 @@
#!/usr/bin/env shlax
"""
Manage a traefik container maintained by Shlax community.
"""
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',
'--providers.docker',
'--api',
],
ports=[
'80:80',
'443:443',
],
volumes=[
'/var/run/docker.sock:/var/run/docker.sock:ro',
'/etc/traefik/acme/:/etc/traefik/acme/',
'/etc/traefik/htpasswd:/htpasswd:ro',
],
labels=[
'traefik.http.routers.traefik.rule=Host(`{{ url.split("/")[2] }}`)',
'traefik.http.routers.traefik.service=api@internal',
'traefik.http.routers.traefik.entrypoints=web',
],
)
+7
View File
@@ -0,0 +1,7 @@
class Result:
def __init__(self, action, target):
self.action = action
self.target = target
self.status = 'pending'
+2 -1
View File
@@ -16,10 +16,11 @@ 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
self.paths.append(path) self.paths.append(path)
@property @property
+14
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
+23 -6
View File
@@ -1,14 +1,31 @@
import os import os
from .script import Script from .script import Script
from ..image import Image
class Container(Script): class Container(Script):
async def call(self, *args, **kwargs): """
if not args or 'build' in args: Wolcome to crazy container control cli
await self.kwargs['build'](**kwargs)
self.image = self.kwargs['build'].image
if not args or 'test' in args: Such wow
"""
def __init__(self, *args, **kwargs):
kwargs.setdefault('start', dict())
super().__init__(*args, **kwargs)
async def call(self, *args, **kwargs):
if step('build'):
await self.kwargs['build'](**kwargs)
self.image = self.kwargs['build'].image
else:
self.image = kwargs.get('image', 'alpine')
if isinstance(self.image, str):
self.image = Image(self.image)
if step('install'):
await self.install(*args, **kwargs)
if step('test'):
self.output.test(self) self.output.test(self)
await self.action('Docker', await self.action('Docker',
*self.kwargs['test'].actions, *self.kwargs['test'].actions,
@@ -17,7 +34,7 @@ class Container(Script):
workdir='/app', workdir='/app',
)(**kwargs) )(**kwargs)
if not args or 'push' in args: if step('push'):
await self.image.push(action=self) await self.image.push(action=self)
#name = kwargs.get('name', os.getcwd()).split('/')[-1] #name = kwargs.get('name', os.getcwd()).split('/')[-1]
+11 -4
View File
@@ -14,16 +14,17 @@ class Actions(list):
self.append(action) self.append(action)
def append(self, value): def append(self, value):
value = copy.deepcopy(value) action = copy.deepcopy(value)
value.parent = self.owner action.parent = self.owner
value.status = 'pending' action.status = 'pending'
super().append(value) super().append(action)
class Script(Action): class Script(Action):
contextualize = ['shargs', 'exec', 'rexec', 'env', 'which', 'copy'] contextualize = ['shargs', 'exec', 'rexec', 'env', 'which', 'copy']
def __init__(self, *actions, **kwargs): def __init__(self, *actions, **kwargs):
self.home = kwargs.pop('home', os.getcwd())
super().__init__(**kwargs) super().__init__(**kwargs)
self.actions = Actions(self, actions) self.actions = Actions(self, actions)
@@ -32,3 +33,9 @@ class Script(Action):
result = await action(*args, **kwargs) result = await action(*args, **kwargs)
if action.status != 'success': if action.status != 'success':
break break
def pollute(self, gbls):
for name, script in self.kwargs.items():
if not isinstance(script, Script):
continue
gbls[name] = script
+7
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)
-4
View File
@@ -1,4 +0,0 @@
from .buildah import Buildah
from .docker import Docker
from .localhost import Localhost
from .ssh import Ssh
+53 -18
View File
@@ -7,14 +7,17 @@ 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')
self.name = kwargs.get('name', os.getcwd().split('/')[-1])
if not isinstance(self.image, Image): if not isinstance(self.image, Image):
self.image = Image(self.image) self.image = Image(self.image)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.context['ctr'] = None
def shargs(self, *args, daemon=False, **kwargs): def shargs(self, *args, daemon=False, **kwargs):
if args[0] == 'docker': if args[0] == 'docker':
@@ -26,9 +29,9 @@ class Docker(Localhost):
args, kwargs = super().shargs(*args, **kwargs) args, kwargs = super().shargs(*args, **kwargs)
if self.context['ctr']: if self.name:
executor = 'exec' executor = 'exec'
extra = [self.context['ctr']] extra = [self.name]
return [self.kwargs.get('docker', 'docker'), executor, '-t'] + extra + list(args), kwargs return [self.kwargs.get('docker', 'docker'), executor, '-t'] + extra + list(args), kwargs
executor = 'run' executor = 'run'
@@ -39,23 +42,40 @@ class Docker(Localhost):
return [self.kwargs.get('docker', 'docker'), executor, '-t'] + extra + [str(self.image)] + list(args), kwargs return [self.kwargs.get('docker', 'docker'), executor, '-t'] + extra + [str(self.image)] + list(args), kwargs
async def call(self, *args, **kwargs): async def call(self, *args, **kwargs):
name = kwargs.get('name', os.getcwd()).split('/')[-1] def step(step):
self.context['ctr'] = ( return not args or step in args
await self.exec(
'docker', 'ps', '-aq', '--filter',
'name=' + name,
raises=False
)
).out.split('\n')[0]
if 'recreate' in args and self.context['ctr']: # self.name = (
await self.exec('docker', 'rm', '-f', self.context['ctr']) # await self.exec(
self.context['ctr'] = None # 'docker', 'ps', '-aq', '--filter',
# 'name=' + self.name,
# raises=False
# )
# ).out.split('\n')[0]
if step('install') and 'install' in self.kwargs:
breakpoint()
await self.action(self.kwargs['install'], *args, **kwargs)
if self.context['ctr']: if step('rm') and await self.exists():
self.context['ctr'] = (await self.exec('docker', 'start', name)).out await self.exec('docker', 'rm', '-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
return await super().call(*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 copy(self, *args): async def copy(self, *args):
src = args[:-1] src = args[:-1]
dst = args[-1] dst = args[-1]
@@ -70,7 +90,22 @@ class Docker(Localhost):
else: else:
args = ['docker', 'copy', self.ctr, s, dst] args = ['docker', 'copy', self.ctr, s, dst]
''' '''
args = ['docker', 'cp', s, self.context['ctr'] + ':' + dst] args = ['docker', 'cp', s, self.name + ':' + dst]
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
+13 -4
View File
@@ -8,6 +8,11 @@ from ..strategies.script import Script
class Localhost(Script): class Localhost(Script):
root = '/' root = '/'
contextualize = Script.contextualize + ['home']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.home = kwargs.pop('home', os.getcwd())
def shargs(self, *args, **kwargs): def shargs(self, *args, **kwargs):
user = kwargs.pop('user', None) user = kwargs.pop('user', None)
@@ -30,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):
@@ -46,6 +49,9 @@ class Localhost(Script):
async def env(self, name): async def env(self, name):
return (await self.exec('echo $' + name)).out return (await self.exec('echo $' + name)).out
async def exists(self, *paths):
proc = await self.exec('type ' + ' '.join(cmd), raises=False)
async def which(self, *cmd): async def which(self, *cmd):
""" """
Return the first path to the cmd in the container. Return the first path to the cmd in the container.
@@ -61,7 +67,10 @@ class Localhost(Script):
return result return result
async def copy(self, *args): async def copy(self, *args):
args = ['cp', '-ra'] + list(args) if args[-1].startswith('./'):
args = list(args)
args[-1] = self.home + '/' + args[-1][2:]
args = ['cp', '-rua'] + list(args)
return await self.exec(*args) return await self.exec(*args)
async def mount(self, *dirs): async def mount(self, *dirs):
+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(
# 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