231 lines
7.1 KiB
Python
Executable File
231 lines
7.1 KiB
Python
Executable File
import sys
|
|
import os
|
|
import argparse
|
|
import getpass
|
|
import pathlib
|
|
import xdg.BaseDirectory
|
|
import yaml
|
|
import datetime
|
|
import pytz
|
|
import subprocess
|
|
from pprint import pprint
|
|
|
|
|
|
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):
|
|
try:
|
|
with open(conf_file, 'r') as file:
|
|
conf = yaml.safe_load(file)
|
|
conf['stamp'] = datetime.datetime.fromtimestamp(os.path.getmtime(conf_file), tz=pytz.UTC)
|
|
return conf
|
|
except yaml.YAMLError as e:
|
|
print(f'ERROR: bad configuration file:')
|
|
print(e)
|
|
sys.exit(1)
|
|
return conf
|
|
|
|
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['stamp'] = conf.get('stamp')
|
|
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
|
|
|
|
|
|
def make_osname_link(binpath, osname):
|
|
link = os.path.join(os.path.dirname(binpath), osname)
|
|
try:
|
|
os.symlink('runon', link)
|
|
except FileExistsError:
|
|
pass
|
|
|
|
|
|
def build_image(conf, update, verbose):
|
|
osname = conf.get('osname')
|
|
image_name = 'runon-{}'.format(osname)
|
|
cache_dir = os.path.join(xdg.BaseDirectory.xdg_cache_home, 'runon')
|
|
cache_file = os.path.join(cache_dir, image_name)
|
|
|
|
if not update and os.path.exists(cache_file):
|
|
ts_image = datetime.datetime.fromtimestamp(os.path.getmtime(cache_file), tz=pytz.UTC)
|
|
ts_conf = conf.get('stamp')
|
|
if verbose:
|
|
print('config: {}'.format(ts_conf))
|
|
print('image: {}'.format(ts_image))
|
|
if ts_image and ts_image > ts_conf:
|
|
if verbose:
|
|
print('image: {} up-to-date'.format(image_name))
|
|
return image_name
|
|
|
|
image = conf.get('image')
|
|
dockerfile = conf.get('dockerfile')
|
|
pkginstall = conf.get('pkginstall')
|
|
packages = conf.get('packages')
|
|
if not dockerfile:
|
|
dockerfile = []
|
|
if image:
|
|
dockerfile.insert(0, 'FROM {}'.format(image))
|
|
if packages:
|
|
for p in packages:
|
|
dockerfile.append(pkginstall.format(p))
|
|
|
|
if verbose:
|
|
print('Dockerfile:')
|
|
for l in dockerfile:
|
|
print(' * {}'.format(l))
|
|
print('Building image {} ...'.format(image_name))
|
|
cmd = ['podman', 'build']
|
|
if update:
|
|
cmd.append('--no-cache')
|
|
cmd += ['-t', image_name, '-']
|
|
ret = subprocess.run(cmd,
|
|
input='\n'.join(dockerfile).encode('utf-8'),
|
|
stderr=subprocess.STDOUT, check=True)
|
|
|
|
if not os.path.exists(cache_dir):
|
|
os.mkdir(cache_dir)
|
|
with open(cache_file, 'w') as file:
|
|
if verbose:
|
|
print('cache: {}'.format(cache_file))
|
|
file.write('')
|
|
|
|
return image_name
|
|
|
|
|
|
def run_image(name, conf, command, verbose):
|
|
volumes = {
|
|
}
|
|
environment = {}
|
|
if conf.get('binds'):
|
|
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]
|
|
if os.path.exists(vol):
|
|
volumes[vol] = { 'bind': bind, 'mode': mode }
|
|
if conf.get('environment'):
|
|
for v in conf['environment']:
|
|
e = v.split('=')
|
|
if len(e) == 1:
|
|
e.append(os.getenv(e[0]))
|
|
environment[e[0]] = e[1]
|
|
|
|
cmd = ['podman', 'run', '--rm', '--interactive', '--tty', '--userns=keep-id', '--net=host' ]
|
|
for e in environment:
|
|
cmd += [ '-e', '{}={}'.format(e, environment[e]) ]
|
|
for v in volumes:
|
|
cmd += [ '-v', ':'.join([v, volumes[v]['bind'], volumes[v]['mode']]) ]
|
|
cmd += [ '--workdir', os.getcwd() ]
|
|
cmd += [ name ]
|
|
if command:
|
|
cmd += command
|
|
if verbose:
|
|
print('executing: {}\n'.format(' '.join(cmd)))
|
|
ret = subprocess.run(cmd)
|
|
return ret.returncode
|
|
|
|
|
|
def main():
|
|
osname = None
|
|
run_name = os.path.basename(os.sys.argv[0])
|
|
if run_name == "runon":
|
|
pass
|
|
elif run_name.startswith("runon_"):
|
|
osname = run_name[len("runon_"):]
|
|
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, '
|
|
'"edit" to open the current config file in a text editor.')
|
|
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
|
|
|
|
conf_file = find_config_file(args.config)
|
|
if not conf_file:
|
|
print('ERROR: config file not found')
|
|
sys.exit(1)
|
|
|
|
if args.osname == 'list':
|
|
osnames = list_osnames(conf_file)
|
|
print('Available distributions:')
|
|
for o in sorted(osnames):
|
|
print(' {}'.format(o))
|
|
print()
|
|
return 0
|
|
elif args.osname == 'edit':
|
|
cmd = [ 'xdg-open', conf_file ]
|
|
ret = subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL);
|
|
return 0
|
|
|
|
if args.link:
|
|
make_osname_link(sys.argv[0], args.osname)
|
|
|
|
conf = load_config(conf_file, args.osname)
|
|
image_name = build_image(conf, args.update, args.verbose)
|
|
ret = run_image(image_name, conf, args.command, args.verbose)
|
|
return ret
|
|
|