runon/runon/runon.py

231 lines
7.1 KiB
Python
Raw Permalink Normal View History

2021-04-25 18:09:29 +02:00
import sys
import os
2021-04-25 18:09:29 +02:00
import argparse
import getpass
2023-12-04 22:39:30 +01:00
import pathlib
2021-04-25 18:09:29 +02:00
import xdg.BaseDirectory
2023-12-04 22:39:30 +01:00
import yaml
import datetime
import pytz
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)
conf['stamp'] = datetime.datetime.fromtimestamp(os.path.getmtime(conf_file), tz=pytz.UTC)
2023-12-04 22:39:30 +01:00
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['stamp'] = conf.get('stamp')
2023-12-04 22:39:30 +01:00
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 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
2023-12-04 22:39:30 +01:00
image = conf.get('image')
dockerfile = conf.get('dockerfile')
pkginstall = conf.get('pkginstall')
packages = conf.get('packages')
if not dockerfile:
dockerfile = []
2023-12-04 22:39:30 +01:00
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:
2021-04-25 18:09:29 +02:00
if verbose:
print('cache: {}'.format(cache_file))
file.write('')
return image_name
2021-04-25 18:09:29 +02:00
def run_image(name, conf, command, verbose):
volumes = {
}
2021-04-25 18:09:29 +02:00
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
2021-04-25 18:09:29 +02:00
def main():
osname = None
run_name = os.path.basename(os.sys.argv[0])
if run_name == "runon":
2021-04-25 18:09:29 +02:00
pass
elif run_name.startswith("runon_"):
osname = run_name[len("runon_"):]
2021-04-25 18:09:29 +02:00
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, '
2023-12-04 23:46:19 +01:00
'"list" to dump all available distributions, '
'"edit" to open the current config file in a text editor.')
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
2023-12-04 23:46:19 +01:00
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)
2023-12-04 22:39:30 +01:00
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)
2021-04-25 18:09:29 +02:00
return ret