run command on any linux distribution, transparently using docker containers
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.
 
 
runon/runon

181 lines
6.0 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 load_config(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)
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_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')
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('command', nargs='*', default=None,
help = 'command to execute')
args = parser.parse_args()
if osname:
args.osname = osname
client = docker.from_env()
conf = load_config(args.config, 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)