runon/runon/runon.py

234 lines
7.3 KiB
Python
Raw Normal View History

2021-04-25 18:09:29 +02:00
import sys
import argparse
import docker
import dockerpty
import io
import getpass
2023-12-04 22:39:30 +01:00
import pathlib
2021-04-25 18:09:29 +02:00
import platform
import os
import re
import xdg.BaseDirectory
2023-12-04 22:39:30 +01:00
import yaml
2021-04-25 18:09:29 +02:00
import subprocess
from pprint import pprint
2023-12-04 22:39:30 +01:00
def find_config_file(user_conf):
conf_list = [
'runon.yaml',
'.runon.yaml',
os.path.join(xdg.BaseDirectory.xdg_config_home, 'runon', 'runon.yaml'),
os.path.join(xdg.BaseDirectory.xdg_config_home, 'runon', 'runon.default.yaml')
]
if user_conf:
conf_list = [ user_conf ]
for conf in conf_list:
if os.path.exists(conf):
return conf
return None
def read_yaml(conf_file):
2021-04-25 18:09:29 +02:00
try:
2023-12-04 22:39:30 +01:00
with open(conf_file, 'r') as file:
conf = yaml.safe_load(file)
return conf
except yaml.YAMLError as e:
print(f'ERROR: bad configuration file:')
print(e)
2021-04-25 18:09:29 +02:00
sys.exit(1)
return conf
2023-12-04 22:39:30 +01:00
def list_osnames(conf_file):
conf = read_yaml(conf_file)
osnames = []
for key in conf:
if (type(conf[key]) is dict) and conf[key].get('image'):
osnames.append(key)
return osnames
def load_config(conf_file, osname):
user_vars = {
'osname': osname,
'user': getpass.getuser(),
'uid': os.getuid(),
'home': pathlib.Path.home(),
}
conf = read_yaml(conf_file)
osconf = conf.get(osname)
if not osconf:
print(f"ERROR: cannot find configuration for distribution {osname}")
sys.exit(1)
osconf['osname'] = osname
for k in [ 'dockerfile', 'packages', 'environment', 'binds' ]:
if osconf.get(k):
osconf[k] = [ s.format(**user_vars) for s in osconf[k]]
return osconf
2021-04-25 18:09:29 +02:00
def make_osname_link(binpath, osname):
link = os.path.join(os.path.dirname(binpath), osname)
try:
os.symlink('runon', link)
except FileExistsError:
pass
2021-04-25 18:09:29 +02:00
def make_image_name(osname):
user = getpass.getuser()
name = 'runon-{}-{}'.format(osname, user)
return name
def build_image(client, conf, update, verbose):
2023-12-04 22:39:30 +01:00
image = conf.get('image')
2021-04-25 18:09:29 +02:00
packages = conf['packages']
dockerfile = conf['dockerfile']
pkginstall = conf['pkginstall']
2023-12-04 22:39:30 +01:00
if image:
dockerfile.insert(0, 'FROM {}'.format(image))
2021-04-25 18:09:29 +02:00
for p in packages:
dockerfile.append(pkginstall.format(p))
tag = make_image_name(conf['osname'])
try:
if verbose:
# fallback to external command 'docker build' as there is
# no way to follow the build progress with API.
print('Building image {} ...'.format(tag))
cmd = ['docker', 'build']
if update:
cmd.append('--no-cache')
cmd += ['-t', tag, '-']
ret = subprocess.run(cmd,
input='\n'.join(dockerfile).encode('utf-8'),
stderr=subprocess.STDOUT, check=True)
image = client.images.get(tag)
else:
with io.BytesIO('\n'.join(dockerfile).encode('utf-8')) as fd:
image, logs = client.images.build(tag=tag, fileobj=fd, rm=True, nocache=update)
if verbose:
print('Built image {} / {}'.format(image.tags[0], image.short_id))
for l in logs:
print(l.get('stream', '').strip('\n'))
except (docker.errors.BuildError, KeyboardInterrupt, subprocess.CalledProcessError, docker.errors.ImageNotFound) as e:
print('Build Error: {}'.format(e))
print()
print('with dockerfile:')
for line in dockerfile:
print(' {}'.format(line))
sys.exit(1)
return image
def create_container(client, image, conf, command, verbose):
volumes = {}
environment = {}
for mnt in conf['binds']:
mnt = mnt.split(':')
if mnt[-1] in ['ro','rw']:
mode = mnt[-1]
del mnt[-1]
else:
mode = 'rw'
mnt = mnt[:2]
bind = mnt[-1]
vol = mnt[0]
volumes[vol] = { 'bind': bind, 'mode': mode }
hostname = platform.node()
for v in conf['environment']:
e = v.split('=')
if len(e) == 1:
e.append(os.getenv(e[0]))
environment[e[0]] = e[1]
#environment['debian_chroot']=conf['osname']
user='{}:{}'.format(os.getuid(), os.getgid())
pwd=os.getcwd()
container = client.containers.create(image, command,
detach=False, stdin_open=True, tty=True,
auto_remove=True,
hostname=hostname,
volumes=volumes,
environment=environment,
user=user,
network_mode='host',
working_dir=pwd
)
return container
def run_container(client, container):
2021-04-30 11:32:39 +02:00
try:
dockerpty.start(client.api, container.id)
container.reload() # to update attrs fields
except docker.errors.APIError as e:
print('ERROR: {}'.format(e))
sys.exit(1)
2021-04-25 18:09:29 +02:00
ret = container.attrs['State']['ExitCode']
return ret
def main():
osname = None
run_name = os.path.basename(os.sys.argv[0])
if run_name == 'runos':
pass
elif run_name.startswith('runon'):
m = re.match('runon[-_]?(.*)$', run_name)
if m:
osname = m[1]
else:
osname = run_name
parser = argparse.ArgumentParser()
if osname:
parser.description = 'run commands on "{}" distribution'.format(osname)
else:
parser.description = 'run commands on any distribution'
parser.add_argument('osname',
help = 'distribution name to run on, '
'"list" to dump all available distributions')
2021-05-01 16:23:11 +02:00
parser.epilog = '(c) 2021 Gilles Grandou <gilles@grandou.net>'
2021-04-25 18:09:29 +02:00
parser.add_argument('-v', '--verbose', action='store_true',
help='verbose output')
parser.add_argument('-c', '--config',
help='specify config file')
parser.add_argument('-u', '--update', action='store_true',
help='force image update')
parser.add_argument('-l', '--link', action='store_true',
help='create a symlink to call "osname" as a shortcut to "runon osname"')
2021-04-25 18:09:29 +02:00
parser.add_argument('command', nargs='*', default=None,
help = 'command to execute')
args = parser.parse_args()
if osname:
args.osname = osname
2023-12-04 22:39:30 +01:00
conf_file = find_config_file(args.config)
if not conf_file:
print('ERROR: config file not found')
sys.exit(1)
if args.osname == 'list':
2023-12-04 22:39:30 +01:00
osnames = list_osnames(conf_file)
print('Available distributions:')
2023-12-04 22:39:30 +01:00
for o in sorted(osnames):
print(' {}'.format(o))
print()
return 0
if args.link:
make_osname_link(sys.argv[0], args.osname)
2023-12-04 22:39:30 +01:00
conf = load_config(conf_file, args.osname)
client = docker.from_env()
2021-04-25 18:09:29 +02:00
image = build_image(client, conf, args.update, args.verbose)
container = create_container(client, image, conf, args.command, args.verbose)
ret = run_container(client, container)
return ret