You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
217 lines
7.2 KiB
217 lines
7.2 KiB
#!/usr/bin/env python3 |
|
|
|
import sys |
|
import argparse |
|
import docker |
|
import dockerpty |
|
import io |
|
import json |
|
import getpass |
|
import platform |
|
import os |
|
import re |
|
import xdg.BaseDirectory |
|
import configparser |
|
import subprocess |
|
from pprint import pprint |
|
|
|
|
|
def natural_sortkey(string): |
|
tokenize = re.compile(r'(\d+)|(\D+)').findall |
|
return tuple(int(num) if num else alpha for num, alpha in tokenize(string)) |
|
|
|
def read_ini(user_confname, osname=''): |
|
ini_list = [ 'runon.conf', '.runon.conf', os.path.join(xdg.BaseDirectory.xdg_config_home, 'runon', 'runon.conf'), '/etc/runon/runon.conf' ] |
|
defaults = { |
|
'osname': osname, |
|
'user': getpass.getuser() |
|
} |
|
if user_confname: |
|
ini_list.insert(0, user_confname) |
|
ini = configparser.ConfigParser(defaults=defaults, interpolation=configparser.ExtendedInterpolation()) |
|
ini.read(ini_list) |
|
return ini |
|
|
|
def list_osnames(user_confname): |
|
ini = read_ini(user_confname) |
|
return ini.sections() |
|
|
|
def load_config(user_confname, osname): |
|
ini = read_ini(user_confname, osname) |
|
if not ini.has_section(osname): |
|
print('ERROR: cannot find configuration for distribution "{}"'.format(osname)) |
|
sys.exit(1) |
|
conf = {} |
|
fields = [ 'dockerfile', 'pkginstall', 'packages', 'environment', 'binds', 'user', 'osname' ] |
|
try: |
|
for f in fields: |
|
conf[f] = ini.get(osname, f) |
|
except configparser.NoOptionError as e: |
|
print('ERROR: {}'.format(e)) |
|
sys.exit(1) |
|
for f in [ 'dockerfile', 'environment', 'binds' ]: |
|
conf[f] = [ i for i in conf[f].split('\n') if i ] |
|
for f in [ 'packages' ]: |
|
conf[f] = conf[f].split() |
|
return conf |
|
|
|
|
|
def make_osname_link(binpath, osname): |
|
link = os.path.join(os.path.dirname(binpath), osname) |
|
try: |
|
os.symlink('runon', link) |
|
except FileExistsError: |
|
pass |
|
|
|
def make_image_name(osname): |
|
user = getpass.getuser() |
|
name = 'runon-{}-{}'.format(osname, user) |
|
return name |
|
|
|
|
|
def build_image(client, conf, update, verbose): |
|
packages = conf['packages'] |
|
dockerfile = conf['dockerfile'] |
|
pkginstall = conf['pkginstall'] |
|
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): |
|
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) |
|
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') |
|
parser.epilog = '(c) 2021 Gilles Grandou <gilles@grandou.net>' |
|
|
|
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"') |
|
parser.add_argument('command', nargs='*', default=None, |
|
help = 'command to execute') |
|
|
|
args = parser.parse_args() |
|
if osname: |
|
args.osname = osname |
|
|
|
if args.osname == 'list': |
|
osnames = list_osnames(args.config) |
|
print('Available distributions:') |
|
for o in sorted(osnames, key=natural_sortkey): |
|
print(' {}'.format(o)) |
|
print() |
|
if args.link: |
|
for o in osnames: |
|
make_osname_link(sys.argv[0], args.osname) |
|
return 0 |
|
|
|
client = docker.from_env() |
|
conf = load_config(args.config, args.osname) |
|
if args.link: |
|
make_osname_link(sys.argv[0], args.osname) |
|
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 |
|
|
|
|
|
if __name__ == '__main__': |
|
ret = main() |
|
sys.exit(ret)
|
|
|