Compare commits

...

10 Commits

Author SHA1 Message Date
Gilles Grandou ee903975e9 wip 2023-12-03 22:48:17 +01:00
Gilles Grandou e345d47b8d wip 2023-12-03 21:43:14 +01:00
Gilles Grandou 8624bc5bfb wip 2023-12-03 21:31:37 +01:00
Gilles Grandou 5b2f567824 wip 2023-12-03 21:20:21 +01:00
Gilles Grandou e3d2c356ba wip 2023-12-03 21:20:04 +01:00
Gilles Grandou 60202fff83 wip 2023-12-03 20:07:29 +01:00
Gilles Grandou 151c9beeb0 wip 2023-12-03 19:32:46 +01:00
Gilles Grandou 93e79f3118 wip 2023-12-03 18:36:27 +01:00
Gilles Grandou 449940238b wip 2023-12-02 19:31:01 +01:00
Gilles Grandou fd2e795eb1 wip 2023-12-02 19:25:46 +01:00
7 changed files with 391 additions and 36 deletions

1
.gitignore vendored
View File

@ -1,4 +1,5 @@
*.egg-info
*.swp
__pycache__
build
venv

35
install Executable file
View File

@ -0,0 +1,35 @@
#!/bin/bash
srcdir=$(dirname $(readlink -f $0))
venvdir=~/.local/lib/runon
bindir=~/.local/bin
configdir=~/.config/runon
if [ "$1" == "--dev" ]; then
editable=--editable
fi
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 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
cp -p $srcdir/runon.default.yaml $configdir/
echo "done."
if [[ ":$PATH:" != *":$(readlink -f $bindir):"* ]]; then
echo ""
echo "WARNING: $bindir is not in your PATH"
fi

119
runon.default.yaml Normal file
View File

@ -0,0 +1,119 @@
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
- build-essential
- git
- libgtk2.0
- libgtk-3-0
- bash-completion
binds:
- /etc/timezone:ro
- /etc/localtime:ro
- "{home}"
environment:
- USER
- DISPLAY
- TERM
- debian_chroot={osname}
debian10:
<<: *debian_base
image: debian:10
debian11:
<<: *debian_base
image: debian:11
debian12:
<<: *debian_base
image: debian:12
ubuntu20.04:
<<: *debian_base
image: ubuntu:20.04
ubuntu22.04:
<<: *debian_base
image: ubuntu22.04
rh_base: &rh_base
dockerfile:
- RUN dnf install -y dnf-plugins-core
- RUN dnf config-manager -y --set-enabled powertools
- RUN dnf install -y sudo
- RUN echo "Defaults lecture = never" >> /etc/sudoers
- RUN echo "ALL ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
pkginstall:
"RUN dnf install -y {}"
packages:
- xterm
- xorg-x11-apps
- xkeyboard-config
- git
- glibc-devel
- gtk2
- gtk3
- bash-completion
- python3
binds:
- /etc/timezone:ro
- /etc/localtime:ro
- "{home}"
environment:
- USER
- DISPLAY
- TERM
- debian_chroot={osname}
centos7:
<<: *rh_base
image: centos:7
dockerfile:
- RUN yum install -y dnf-plugins-core
- RUN yum install -y sudo
- RUN echo "Defaults lecture = never" >> /etc/sudoers
- RUN echo "ALL ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
pkginstall:
"RUN yum install -y {}"
rocky8:
<<: *rh_base
image: rockylinux:8
dockerfile:
- RUN dnf install -y dnf-plugins-core
- RUN dnf config-manager -y --set-enabled powertools
- RUN dnf install -y sudo
- RUN echo "Defaults lecture = never" >> /etc/sudoers
- RUN echo "ALL ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
rocky9:
<<: *rh_base
image: rockylinux:9
dockerfile:
- RUN dnf install -y dnf-plugins-core
- RUN dnf install -y sudo
- RUN echo "Defaults lecture = never" >> /etc/sudoers
- RUN echo "ALL ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
packages:
- xterm
- xkeyboard-config
- git
- glibc-devel
- gtk2
- gtk3
- bash-completion
- python3

View File

@ -1,35 +0,0 @@
import os
import argparse
def main():
osname = None
run_name = os.path.basename(os.sys.argv[0])
if run_name == "runon":
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')
args = parser.parse_args()
if osname:
args.osname = osname
print(args)

216
runon/runon.py Normal file
View File

@ -0,0 +1,216 @@
import sys
import os
import argparse
import getpass
import pathlib
import xdg_base_dirs
import yaml
import json
import datetime
import pytz
import subprocess
from pprint import pprint
def read_yaml(conf_name):
yaml_list = [
'runon.yaml',
'.runon.yaml',
os.path.join(xdg_base_dirs.xdg_config_home(), 'runon', 'runon.yaml')
os.path.join(xdg_base_dirs.xdg_config_home(), 'runon', 'runon.default.yaml')
]
if conf_name:
yaml_list.insert(0, conf_name)
for yaml_file in yaml_list:
if not os.path.exists(yaml_file):
continue
try:
with open(yaml_file, 'r') as file:
conf = yaml.safe_load(file)
conf['stamp'] = datetime.datetime.fromtimestamp(os.path.getmtime(yaml_file), tz=pytz.UTC)
return conf
except yaml.YAMLError as e:
print(f'ERROR: bad configuration file:')
print(e)
sys.exit(1)
return None
def list_osnames(user_confname):
conf = read_yaml(user_confname)
osnames = []
for key in conf:
if (type(conf[key]) is dict) and conf[key].get('dockerfile'):
osnames.append(key)
return osnames
def load_config(conf_name, osname):
user_vars = {
'osname': osname,
'user': getpass.getuser(),
'uid': os.getuid(),
'home': pathlib.Path.home(),
}
conf = read_yaml(conf_name)
osconf = conf.get(osname)
osconf['stamp'] = conf.get('stamp')
if not osconf:
print(f"ERROR: cannot find configuration for distribution {osname}")
sys.exit(1)
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_base_dirs.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')
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):
print(' {}'.format(o))
print()
return 0
if args.link:
make_osname_link(sys.argv[0], args.osname)
conf = load_config(args.config, args.osname)
image_name = build_image(conf, args.update, args.verbose)
ret = run_image(image_name, conf, args.command, args.verbose)
return ret
#

View File

@ -9,10 +9,14 @@ setup(
packages = find_packages(),
python_requires = ">=3.6",
install_requires = [
"pathlib == 1.0.1",
"xdg_base_dirs == 6.0.1",
"pyyaml == 6.0.1",
"pytz",
],
entry_points={
"console_scripts": [
"runon=runon:main",
"runon=runon.runon:main",
],
}
)

15
uninstall Executable file
View File

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