10 Commits
Author SHA1 Message Date
jpic 6b15838059 Guess who's going out of the rabbithole 2020-04-18 20:46:47 +02:00
jpic 85e11755f7 wip 2020-04-18 19:52:22 +02:00
jpic ef7656ddea wip 2020-04-18 17:33:52 +02:00
jpic d16a761241 tests 2020-03-04 02:14:54 +01:00
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
31 changed files with 652 additions and 310 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
+108 -72
View File
@@ -1,3 +1,4 @@
from copy import deepcopy
import functools
import inspect
import importlib
@@ -5,9 +6,27 @@ 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 = {
@@ -23,20 +42,21 @@ class Action:
'''.strip(),
immediate=True,
),
verbose=dict(
alias='v',
default=False,
help='Verbose, like -d=visit,cmd,out',
immediate=True,
),
)
def __init__(self, *args, **kwargs):
self.args = args
self.kwargs = kwargs
@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:
@@ -91,77 +111,78 @@ 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='cmd,visit,out' if options['verbose'] else options['debug'],
)
results = []
for target in targets:
target.output = output
if len(targets) > 1:
output.prefix = target
from copy import deepcopy
action = deepcopy(self)
action.target = target
result = Result(action, target)
results.append(result)
action.result = result
action.output = output
for step in options.get('steps', None) or self.default_steps:
if step not in action.steps():
print(f'Failed to find {type(action).__name__}.{step}')
continue
action.step = step
output.start(action)
try:
if isinstance(getattr(action, step), Action):
await getattr(action, step)(**options)
else:
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 = 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)
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
@@ -190,6 +211,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:
@@ -200,12 +222,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
@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):
"""Copy files or directories to target."""
async def call(self, *args, **kwargs):
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}')
+8 -9
View File
@@ -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',
@@ -95,7 +94,7 @@ class Packages(Action):
f.write(str(os.getpid()))
try:
await self.rexec(self.cmds['update'])
await self.target.rexec(self.cmds['update'])
finally:
os.unlink(lockfile)
@@ -103,15 +102,15 @@ class Packages(Action):
f.write(str(now))
else:
while os.path.exists(lockfile):
print(f'{self.container.name} | Waiting for update ...')
print(f'{self.target} | Waiting for {lockfile} ...')
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)
+2
View File
@@ -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)
+14 -8
View File
@@ -1,16 +1,22 @@
from ..targets.buildah import Buildah
from ..targets.docker import Docker
from .base import Action
class Run(Action):
async def call(self, *args, **kwargs):
image = self.kwargs.get('image', None)
if not image:
return await self.exec(*self.args, **self.kwargs)
"""Run a script or command on a target."""
def __init__(self, *args, image=None, **kwargs):
super().__init__(**kwargs)
self.args = args
self.kwargs = kwargs
self.image = image
async def apply(self):
if not self.image:
return await self.target.exec(*self.args, **self.kwargs)
from ..targets.buildah import Buildah
from ..targets.docker import Docker
if isinstance(image, Buildah):
breakpoint()
result = await self.action(image, *args, **kwargs)
return await Docker(
+3
View File
@@ -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
+223 -123
View File
@@ -1,140 +1,241 @@
'''
shlax is a micro-framework to orchestrate commands.
"""
Shlax automation tool manual
shlax yourfile.py: to list actions you have declared.
shlax yourfile.py <action>: to execute a given action
#!/usr/bin/env shlax: when making yourfile.py an executable.
'''
Shlax 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
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
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 os.path.exists(shlaxfile.split('::')[0]):
self.shlaxfile = Shlaxfile()
self.shlaxfile.parse(shlaxfile)
for name, action in self.shlaxfile.actions.items():
self[name] = cli2.Callable(
name,
action.callable(),
class Parser(cli2.Parser):
def __init__(self, *args, **kwargs):
self.targets = dict()
super().__init__(*args, **kwargs)
def append(self, arg):
if '=' not in arg and '@' in arg:
if '://' in arg:
kind, spec = arg.split('://')
else:
kind = 'ssh'
spec = arg
mod = importlib.import_module('shlax.targets.' + kind)
target = getattr(mod, kind.capitalize())(spec)
self.targets[str(target)] = target
else:
super().append(arg)
def __call__(self):
if len(self.argv) > 1 and os.path.exists(self.argv[1]):
self.argv = sys.argv[1:]
spec = importlib.util.spec_from_file_location('shlaxfile', sys.argv[1])
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
self.doc = (inspect.getdoc(mod) or '').split("\n")[0]
for name, value in mod.__dict__.items():
if isinstance(value, Action):
self[name] = cli2.Callable(
name,
self.action(value),
doc=type(value).__doc__,
options={
option: cli2.Option(option, **cfg)
for option, cfg in value.options.items()
}
)
#self[name] = value
#elif callable(value) and getattr(value, '__name__', '').startswith('test_'):
# self.tests[value.__name__] = value
#modname = sys.argv[1].split('/')[-1].replace('.py', '')
#mod = importlib.import_module('shlax.actions.' + modname)
else:
scripts = glob.glob(os.path.join(
os.path.dirname(__file__), 'actions', '*.py'))
for script in scripts:
modname = script.split('/')[-1].replace('.py', '')
if modname == '__init__':
continue
mod = importlib.import_module('shlax.actions.' + modname)
for key, value in mod.__dict__.items():
if key == '__builtins__':
continue
if key.lower() != modname:
continue
break
self[modname] = cli2.Callable(
modname,
self.action_class(value),
options={
k: cli2.Option(name=k, **v)
for k, v in action.options.items()
},
color=getattr(action, 'color', cli2.YELLOW),
option: cli2.Option(option, **cfg)
for option, cfg in value.options.items()
}
)
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,
options={
option: cli2.Option(option, **cfg)
for option, cfg in value.options.items()
}
)
else:
for name, step in value.steps().items():
if isinstance(step, Action):
self[modname][name] = cli2.Callable(
modname,
self.action(step),
doc=inspect.getdoc(step),
options={
option: cli2.Option(option, **cfg)
for option, cfg in value.options.items()
}
)
else:
# should be a method, just clone the
# original action and replace default_steps
action = copy.deepcopy(value)
action.default_steps = [name]
self[modname][name] = cli2.Callable(
modname,
self.action(action),
doc=inspect.getdoc(step),
options={
option: cli2.Option(option, **cfg)
for option, cfg in value.options.items()
}
)
else:
if len(value.steps()) == 1:
self[modname][key] = cli2.Callable(
modname,
self.action(value),
doc=doc,
options={
option: cli2.Option(option, **cfg)
for option, cfg in value.options.items()
}
)
else:
self[modname][key] = cli2.Group('steps')
for step in value.steps():
self[modname][key][step] = cli2.Callable(
modname,
self.action(value),
doc='lol',
options={
option: cli2.Option(option, **cfg)
for option, cfg in value.options.items()
}
)
return super().__call__()
def action(self, action):
async def cb(*args, **kwargs):
options = dict(steps=args)
options.update(self.parser.options)
# UnboundLocalError: local variable 'action' referenced before assignment
# ??? gotta be missing something, commenting meanwhile
# action = copy.deepcopy(action)
return await action(*self.parser.targets, **options)
cb.__name__ = type(action).__name__
return cb
def action_class(self, action_class):
async def cb(*args, **kwargs):
argspec = inspect.getfullargspec(action_class)
required = argspec.args[1:]
missing = []
for i, name in enumerate(required):
if len(args) - 1 <= i:
continue
if name in kwargs:
continue
missing.append(name)
if missing:
if not args:
print('No args provided after action name ' + action_class.__name__.lower())
print('Required arguments: ' + ', '.join(argspec.args[1:]))
if args:
print('Provided: ' + ', '.join(args))
print('Missing arguments: ' + ', '.join(missing))
print('Try to just add args on the command line separated with a space')
print(inspect.getdoc(action_class))
example = 'Example: shlax action '
example += action_class.__name__.lower()
if args:
example += ' ' + ' '.join(args)
example += ' ' + ' '.join(missing)
print(example)
return
_args = []
steps = []
for arg in args:
if arg in action_class.steps():
steps.append(arg)
else:
_args.append(arg)
options = dict(steps=steps)
'''
varargs = argspec.varargs
if varargs:
extra = args[len(argspec.args) - 1:]
args = args[:len(argspec.args) - 1]
options = dict(steps=extra)
else:
extra = args[len(argspec.args) - 1:]
args = args[:len(argspec.args) - 1]
options = dict(steps=extra)
'''
options.update(self.parser.options)
return await action_class(*_args, **kwargs)(*self.parser.targets, **options)
cb.__doc__ = (inspect.getdoc(action_class) or '').split("\n")[0]
cb.__name__ = action_class.__name__
return cb
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
@@ -142,5 +243,4 @@ class ConsoleScript(cli2.ConsoleScript):
print(e)
self.exit_code = 1
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__
+2 -2
View File
@@ -12,10 +12,10 @@ class WrongResult(ShlaxException):
msg = f'FAIL exit with {proc.rc} ' + proc.args[0]
if not proc.debug or 'cmd' not in str(proc.debug):
if not proc.output.debug or 'cmd' not in str(proc.output.debug):
msg += '\n' + proc.cmd
if not proc.debug or 'out' not in str(proc.debug):
if not proc.output.debug or 'out' not in str(proc.output.debug):
msg += '\n' + proc.out
msg += '\n' + proc.err
+5 -5
View File
@@ -104,7 +104,7 @@ class Output:
self.colors['purplebold'],
'! TEST ',
self.colors['reset'],
action.colorized(),
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',
]))
+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 =
+2 -3
View File
@@ -54,8 +54,7 @@ class Proc:
"""
test = False
def __init__(self, *args, prefix=None, raises=True, debug=None, output=None):
self.debug = debug if not self.test else False
def __init__(self, *args, prefix=None, raises=True, output=None):
self.output = output or Output()
self.cmd = ' '.join(args)
self.args = args
@@ -87,7 +86,7 @@ class Proc:
if self.called:
raise Exception('Already called: ' + self.cmd)
if self.debug is True or 'cmd' in str(self.debug):
if 'cmd' in str(self.output.debug):
self.output.cmd(self.cmd)
if self.test:
+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)
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
self.paths.append(path)
@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
from .script import Script
from ..image import Image
class Container(Script):
async def call(self, *args, **kwargs):
if not args or 'build' in args:
await self.kwargs['build'](**kwargs)
self.image = self.kwargs['build'].image
"""
Wolcome to crazy container control cli
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)
await self.action('Docker',
*self.kwargs['test'].actions,
@@ -17,7 +34,7 @@ class Container(Script):
workdir='/app',
)(**kwargs)
if not args or 'push' in args:
if step('push'):
await self.image.push(action=self)
#name = kwargs.get('name', os.getcwd()).split('/')[-1]
+11 -4
View File
@@ -14,16 +14,17 @@ class Actions(list):
self.append(action)
def append(self, value):
value = copy.deepcopy(value)
value.parent = self.owner
value.status = 'pending'
super().append(value)
action = copy.deepcopy(value)
action.parent = self.owner
action.status = 'pending'
super().append(action)
class Script(Action):
contextualize = ['shargs', 'exec', 'rexec', 'env', 'which', 'copy']
def __init__(self, *actions, **kwargs):
self.home = kwargs.pop('home', os.getcwd())
super().__init__(**kwargs)
self.actions = Actions(self, actions)
@@ -32,3 +33,9 @@ class Script(Action):
result = await action(*args, **kwargs)
if action.status != 'success':
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,18 @@ from .localhost import Localhost
class Docker(Localhost):
contextualize = Localhost.contextualize + ['mnt', 'ctr', 'mount']
"""Manage a docker container."""
default_steps = ['install', 'up']
contextualize = ['image', 'home']
def __init__(self, *args, **kwargs):
self.image = kwargs.get('image', 'alpine')
self.name = kwargs.get('name', os.getcwd().split('/')[-1])
if not isinstance(self.image, Image):
self.image = Image(self.image)
super().__init__(*args, **kwargs)
self.context['ctr'] = None
def shargs(self, *args, daemon=False, **kwargs):
if args[0] == 'docker':
@@ -26,9 +30,9 @@ class Docker(Localhost):
args, kwargs = super().shargs(*args, **kwargs)
if self.context['ctr']:
if self.name:
executor = 'exec'
extra = [self.context['ctr']]
extra = [self.name]
return [self.kwargs.get('docker', 'docker'), executor, '-t'] + extra + list(args), kwargs
executor = 'run'
@@ -39,23 +43,39 @@ class Docker(Localhost):
return [self.kwargs.get('docker', 'docker'), executor, '-t'] + extra + [str(self.image)] + list(args), kwargs
async def call(self, *args, **kwargs):
name = kwargs.get('name', os.getcwd()).split('/')[-1]
self.context['ctr'] = (
await self.exec(
'docker', 'ps', '-aq', '--filter',
'name=' + name,
raises=False
)
).out.split('\n')[0]
def step(step):
return not args or step in args
if 'recreate' in args and self.context['ctr']:
await self.exec('docker', 'rm', '-f', self.context['ctr'])
self.context['ctr'] = None
# self.name = (
# await self.exec(
# 'docker', 'ps', '-aq', '--filter',
# 'name=' + self.name,
# raises=False
# )
# ).out.split('\n')[0]
if step('install') and 'install' in self.kwargs:
await self.action(self.kwargs['install'], *args, **kwargs)
if self.context['ctr']:
self.context['ctr'] = (await self.exec('docker', 'start', name)).out
if step('rm') and await self.exists():
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)
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]
dst = args[-1]
@@ -70,7 +90,22 @@ class Docker(Localhost):
else:
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))
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):
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):
user = kwargs.pop('user', None)
@@ -30,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):
@@ -46,6 +49,9 @@ class Localhost(Script):
async def env(self, name):
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):
"""
Return the first path to the cmd in the container.
@@ -61,7 +67,10 @@ class Localhost(Script):
return result
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)
async def mount(self, *dirs):
+1 -1
View File
@@ -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
+7
View File
@@ -0,0 +1,7 @@
from shlax.cli import ConsoleScript
def test_parser():
parser = ConsoleScript.Parser(['@host'])
parser.parse()
assert parser.targets['host'] == Ssh('host')
+7
View File
@@ -0,0 +1,7 @@
from shlax.cli import ConsoleScript
def test_parser():
parser = ConsoleScript.Parser(['@host'])
parser.parse()
assert parser.targets['host'] == Ssh('host')
+5
View File
@@ -0,0 +1,5 @@
from shlax.play import Play
def test_play_call():
+47
View File
@@ -0,0 +1,47 @@
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
@@ -0,0 +1,6 @@
import os
import sys
import pytest
if not os.getenv('CI'):
pytest.skip('Please run with ./shlaxfile.py test', allow_module_level=True)