This commit is contained in:
Gilles Grandou 2023-12-02 18:16:13 +01:00
parent 541b5bf453
commit 2320c3e37b
7 changed files with 347 additions and 369 deletions

134
install
View File

@ -1,103 +1,41 @@
#!/usr/bin/bash
#!/bin/bash
local_bin_dir=~/local/bin
local_config_dir=~/.config/runon
system_bin_dir=/usr/local/bin
system_config_dir=/etc/runon
srcdir=$(dirname $(readlink -f $0))
venvdir=~/.local/lib/runon
bindir=~/.local/bin
configdir=~/.config/runon
op=install
bin_dir=$local_bin_dir
config_dir=$local_config_dir
function usage
{
echo "$0 [local|system] [-u|--uninstall] [<dest>]"
echo
echo " local install for local user, by default"
echo " system install for all users"
echo " dev install in dev mode (create links to source)"
echo " -u, --uninstall local or system uninstall"
echo " <dest> destination dir for binary"
echo
echo "default local paths:"
echo " binary: $local_bin_dir"
echo " config: $local_config_dir"
echo
echo "default system paths:"
echo " binary: $system_bin_dir"
echo " config: $system_config_dir"
echo
}
function do_exec
{
echo "$@"
"$@"
}
while [ $# -gt 0 ]; do
case $1 in
local)
bin_dir=$local_bin_dir
config_dir=$local_config_dir
;;
system)
bin_dir=$system_bin_dir
config_dir=$system_config_dir
;;
dev)
bin_dir=$local_bin_dir
config_dir=$local_config_dir
op=installdev
;;
-u)
op=uninstall
;;
--uninstall)
op=uninstall
;;
-h*)
usage
exit 0
;;
*) bin_dir=$1
;;
esac
shift
done
bin_dir=$(realpath $bin_dir)
config_dir=$(realpath $config_dir)
if [[ ":$PATH:" != *":$bin_dir:"* ]]; then
echo "WARNING: $bin_dir is not in your PATH, runon will not be automatically found."
echo
if [ "$1" == "--dev" ]; then
editable=--editable
devmode=1
fi
set -e
if [ "$op" = "install" ]; then
do_exec install -d $bin_dir
do_exec install -d $config_dir
#rm -f $bin_dir/runon
#rm -f $config_dir/runon
do_exec install runon $bin_dir
do_exec install runon.conf $config_dir
elif [ "$op" = "installdev" ]; then
do_exec install -d $bin_dir
do_exec install -d $config_dir
do_exec ln -s -f $(realpath runon) $bin_dir/
do_exec ln -s -f $(realpath runon.conf) $config_dir/
elif [ "$op" = "uninstall" ]; then
# find all symlinks targetting to runon, and remove them
find $bin_dir -type l | while read l; do
if [ "$(readlink $l)" = "runon" ]; then
do_exec rm $l
fi
done
do_exec rm -f $bin_dir/runon
do_exec rm -f $config_dir/runon.conf
test -d $bin_dir && do_exec rmdir --parents --ignore-fail-on-non-empty $bin_dir 2> /dev/null
test -d $config_dir && do_exec rmdir --parents --ignore-fail-on-non-empty $config_dir 2> /dev/null
if [ ! -d $venvdir/bin/activate ]; then
echo "create virtualenv $venvdir..."
python3 -m venv $venvdir
fi
source $venvdir/bin/activate
echo "populate $venvdir..."
python3 -m pip install --upgrade pip
python3 -m pip install wheel
python3 -m pip install $editable $srcdir
echo "create links in $bindir..."
mkdir -p $bindir
ln -sf $venvdir/bin/runon $bindir/
echo "install base config in $configdir..."
mkdir -p $configdir
if [ -n "$devmode" ]; then
ln -s $srcdir/runon.default.yaml $configdir/
else
cp -p $srcdir/runon.default.yaml $configdir/
fi
echo "done."
if [[ ":$PATH:" != *":$(readlink -f $bindir):"* ]]; then
echo ""
echo "WARNING: $bindir is not in your PATH"
fi

View File

@ -1,121 +0,0 @@
[DEFAULT]
environment =
HOME
USER
DISPLAY
TERM
debian_chroot=${osname}
binds =
/etc/timezone:ro
/etc/localtime:ro
/etc/passwd:ro
/etc/group:ro
/etc/shadow:ro
/tmp/.X11-unix:ro
/home/${user}
[centos7]
dockerfile =
FROM centos:7
RUN yum install sudo -y
RUN echo "Defaults lecture = never" >> /etc/sudoers
RUN echo "ALL ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
RUN yum group install -y "Development Tools"
pkginstall = RUN yum install {} -y
packages = xterm vim-X11 git python3 bash-completion
[rocky8]
dockerfile =
FROM rockylinux:8
RUN yum install sudo -y
RUN echo "Defaults lecture = never" >> /etc/sudoers
RUN echo "ALL ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
pkginstall = RUN yum install {} -y
packages = xterm vim-X11 git python3 bash-completion
[rocky9]
dockerfile =
FROM rockylinux:9
RUN dnf install sudo -y
RUN echo "Defaults lecture = never" >> /etc/sudoers
RUN echo "ALL ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
pkginstall = RUN dnf install {} -y
packages = xterm vim-X11 git python3 bash-completion
[debian9]
dockerfile =
FROM debian:9
ARG DEBIAN_FRONTEND=noninteractive
RUN echo "deb http://archive.debian.org/debian stretch main" > /etc/apt/sources.list
RUN apt-get update
RUN apt-get -y upgrade
RUN apt install -y --allow-downgrades libnettle6=3.3-1+b2 # default libnettle6 conflicts with libgtk-3.0
RUN apt-get -y install sudo
RUN echo "Defaults lecture = never" >> /etc/sudoers
RUN echo "ALL ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
pkginstall = RUN apt-get -y install {}
packages = xterm x11-apps vim-gtk3 git build-essential python3 bash-completion
[debian10]
dockerfile =
FROM debian:10
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update
RUN apt-get -y install apt-utils
RUN apt-get -y upgrade
RUN apt-get -y install sudo
RUN echo "Defaults lecture = never" >> /etc/sudoers
RUN echo "ALL ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
pkginstall = RUN apt-get -y install {}
packages = xterm x11-apps vim-gtk3 git build-essential python3 bash-completion
[debian11]
dockerfile =
FROM debian:11
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update
RUN apt-get -y install apt-utils
RUN apt-get -y install sudo
RUN echo "Defaults lecture = never" >> /etc/sudoers
RUN echo "ALL ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
pkginstall = RUN apt-get -y install {}
packages = xterm x11-apps vim-gtk3 git build-essential python3 bash-completion
[debian12]
dockerfile =
FROM debian:12
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update
RUN apt-get -y install apt-utils
RUN apt-get -y install sudo
RUN echo "Defaults lecture = never" >> /etc/sudoers
RUN echo "ALL ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
pkginstall = RUN apt-get -y install {}
packages = xterm x11-apps vim-gtk3 git build-essential python3 bash-completion
[ubuntu20.04]
dockerfile =
FROM ubuntu:20.04
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update
RUN apt-get -y install apt-utils
RUN apt-get -y install sudo
RUN echo "Defaults lecture = never" >> /etc/sudoers
RUN echo "ALL ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
pkginstall = RUN apt-get -y install {}
packages = xterm x11-apps vim-gtk3 git build-essential python3 bash-completion
[ubuntu22.04]
dockerfile =
FROM ubuntu:22.04
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update
RUN apt-get -y install apt-utils
RUN apt-get -y install sudo
RUN echo "Defaults lecture = never" >> /etc/sudoers
RUN echo "ALL ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
pkginstall = RUN apt-get -y install {}
packages = xterm x11-apps vim-gtk3 git build-essential python3 bash-completion

107
runon.default.yaml Normal file
View File

@ -0,0 +1,107 @@
debian_base: &debian_base
dockerfile:
- ARG DEBIAN_FRONTEND=noninteractive
- RUN apt-get update
- RUN apt-get -y install apt-utils
- RUN apt-get -y upgrade
- RUN apt-get -y install sudo
- RUN echo "Defaults lecture = never" >> /etc/sudoers
- RUN echo "ALL ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
pkginstall:
"RUN apt-get -y install {}"
packages:
- xterm
- x11-apps
- vim-gtk3
- git
- build-essential
- python3
- bash-completion
binds:
- /etc/timezone:ro
- /etc/localtime:ro
- "{home}"
environment:
- USER
- DISPLAY
- TERM
- container={osname}
debian9:
<<: *debian_base
image: docker.io/debian:9
dockerfile:
- ARG DEBIAN_FRONTEND=noninteractive
- RUN echo "deb http://archive.debian.org/debian stretch main" > /etc/apt/sources.list
- RUN apt-get update
- RUN apt-get -y upgrade
- RUN apt install -y --allow-downgrades libnettle6=3.3-1+b2 # default libnettle6 conflicts with libgtk-3.0
- RUN apt-get -y install sudo
- RUN echo "Defaults lecture = never" >> /etc/sudoers
- RUN echo "ALL ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
debian10:
<<: *debian_base
image: docker.io/debian:10
debian11:
<<: *debian_base
image: docker.io/debian:11
debian12:
<<: *debian_base
image: docker.io/debian:12
ubuntu20.04:
<<: *debian_base
image: docker.io/ubuntu:20.04
ubuntu22.04:
<<: *debian_base
image: docker.io/ubuntu:22.04
rh_base: &rh_base
dockerfile:
- RUN dnf install -y sudo
- RUN echo "Defaults lecture = never" >> /etc/sudoers
- RUN echo "ALL ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
- RUN dnf group install -y "Development Tools"
pkginstall:
"RUN dnf install -y {}"
packages:
- xterm
- vim-X11
- git
- python3
- bash-completion
binds:
- /etc/timezone:ro
- /etc/localtime:ro
- "{home}"
environment:
- USER
- DISPLAY
- TERM
- container={osname}
centos7:
<<: *rh_base
image: docker.io/centos:7
dockerfile:
- RUN yum install -y sudo
- RUN echo "Defaults lecture = never" >> /etc/sudoers
- RUN echo "ALL ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
- RUN yum group install -y "Development Tools"
pkginstall:
"RUN yum install -y {}"
rocky8:
<<: *rh_base
image: docker.io/rockylinux:8
rocky9:
<<: *rh_base
image: docker.io/rockylinux:9

View File

@ -1,60 +1,73 @@
#!/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 argparse
import getpass
import pathlib
import xdg.BaseDirectory
import configparser
import yaml
import json
import datetime
import pytz
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 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_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' ]
def read_yaml(conf_file):
try:
for f in fields:
conf[f] = ini.get(osname, f)
except configparser.NoOptionError as e:
print('ERROR: {}'.format(e))
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)
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 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)
@ -63,106 +76,104 @@ def make_osname_link(binpath, osname):
except FileExistsError:
pass
def make_image_name(osname):
user = getpass.getuser()
name = 'runon-{}-{}'.format(osname, user)
return name
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)
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 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:
# 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)
print('config: {}'.format(ts_conf))
print('image: {}'.format(ts_image))
if ts_image and ts_image > ts_conf:
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
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 create_container(client, image, conf, command, verbose):
volumes = {}
def run_image(name, 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()
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]
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
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 == 'runos':
if run_name == "runon":
pass
elif run_name.startswith('runon'):
m = re.match('runon[-_]?(.*)$', run_name)
if m:
osname = m[1]
elif run_name.startswith("runon_"):
osname = run_name[len("runon_"):]
else:
osname = run_name
@ -173,7 +184,8 @@ def main():
parser.description = 'run commands on any distribution'
parser.add_argument('osname',
help = 'distribution name to run on, '
'"list" to dump all available distributions')
'"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',
@ -191,27 +203,28 @@ def main():
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(args.config)
osnames = list_osnames(conf_file)
print('Available distributions:')
for o in sorted(osnames, key=natural_sortkey):
for o in sorted(osnames):
print(' {}'.format(o))
print()
if args.link:
for o in osnames:
make_osname_link(sys.argv[0], args.osname)
return 0
elif args.osname == 'edit':
cmd = [ 'xdg-open', conf_file ]
ret = subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL);
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)
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
if __name__ == '__main__':
ret = main()
sys.exit(ret)
#

View File

@ -9,10 +9,10 @@ setup(
packages = find_packages(),
python_requires = ">=3.6",
install_requires = [
"docker",
"dockerpty",
"pathlib",
"pyxdg",
"pyyaml",
"pytz",
],
entry_points={
"console_scripts": [

16
tests.sh Executable file
View File

@ -0,0 +1,16 @@
#!/bin/bash -e
cat /etc/os-release
echo
set -x
sudo id
python3 --version
g++ --version | head -1
gvim --version | head -1
xterm -e /bin/bash -c "sleep 1"
date
set +x
echo "[OK]"

25
uninstall Executable file
View File

@ -0,0 +1,25 @@
#!/bin/bash
srcdir=$(dirname $(readlink -f $0))
venvdir=~/.local/lib/runon
bindir=~/.local/bin
configdir=~/.config/runon
if [ -e $venvdir/bin/runon ]; then
echo "remove links from $bindir"
find -L $bindir -samefile $bindir/runon -exec rm -v {} \;
find -L $bindir -samefile $venvdir/bin/runon -exec rm -v {} \;
fi
if [ -d $venvdir ]; then
echo "remove virtualenv $venvdir"
rm -rf $venvdir
fi
if [ -e $configdir ]; then
echo "remove configs"
rm -vf $configdir/runon.default.yaml
rmdir -v $configdir || true
fi
echo "done."