mrrl-containers

MRRL version of container scripts
git clone https://ccx.te2000.cz/git/mrrl-containers
Log | Files | Refs

commit b25e95c970f937c160442d08fe61be7ad3e5f0e9
parent 2a751866c53d8ddc59f689b1396e7424ab9a2a7f
Author: Jan Pobrislo <ccx@te2000.cz>
Date:   Fri, 19 Sep 2025 00:05:32 +0000

Add WIP service generator

Diffstat:
Asbin/ns_run_svc_gen | 611+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 611 insertions(+), 0 deletions(-)

diff --git a/sbin/ns_run_svc_gen b/sbin/ns_run_svc_gen @@ -0,0 +1,611 @@ +#!/bin/zsh +setopt no_unset warn_create_global extended_glob +zmodload zsh/system || exit $? +zmodload -m -F zsh/files b:zf_\* || exit $? +typeset -g scriptname=$0 + +typeset -g container_image_dir=/mnt/volumes/containers/systems +typeset -g container_user_dir=/mnt/volumes/containers/user +typeset -g service_script_dir=${0:P:h:h}/service_scripts + +# message helpers +typeset -g hl_fatal hl_warn hl_reset +if (( $terminfo[colors] >= 8 )); then + hl_fatal='%F{red}%B'; hl_fatal=${(%)hl_fatal} + hl_warn='%F{yellow}%B'; hl_warn=${(%)hl_warn} + hl_reset='%b%f'; hl_reset=${(%)hl_reset} +fi + +err_msg() { + local first=$1 + shift + printf >&2 '%s%s%s%s\n' "$hl_fatal" "$first" "$hl_reset" "$*" +} + +warn_msg() { + local first=$1 + shift + printf >&2 '%s%s%s%s\n' "$hl_warn" "$first" "$hl_reset" "$*" +} + +# helper that prints out stack, error message and exits +die_ret() { + set +x + local ret n + ret=$1 + shift + print -r - >&2 "${hl_fatal}Fatal$hl_reset error occurend in:" + for n in {${#funcfiletrace}..1}; do + printf >&2 '%d> %s (%s)\n' $n "$funcfiletrace[$n]" "$functrace[$n]" + done + printf >&2 '%s\n' "${hl_fatal}*$hl_reset $^@" + exit $ret +} + +# useful exit aliases +die() { + set +x + die_ret 1 "$@" +} +die100() { # 100: wrong usage + set +x + die_ret 100 "$@" +} +die101() { # 101: can't happen / internal error + set +x + die_ret 101 "$@" +} +die102() { # 102: config / data file error + set +x + die_ret 102 "$@" +} +die111() { # 111: system call failed / transient error + set +x + die_ret 111 "$@" +} + +-() { # Run command and die on nonzero exitcode + "$@" || die_ret $? "command failed with exitcode $?: ${(j: :)${(q)@}}" +} + +die_usage() { + die100 "$@" "usage: ns_run_svc_gen TODO" +} + +# --- + +typeset -g config_section_regex='(mountpoints|profile/[-a-zA-Z0-9][-.a-zA-Z0-9]*)' +typeset -g mountpoint_name_regex='[-a-zA-Z0-9][-.a-zA-Z0-9]*' +typeset -g seccomp_profile_regex='[-a-zA-Z0-9][-.a-zA-Z0-9]*' + +typeset -g config_current_section +typeset -gA config_values config_sections +config_section() { + local match mbegin mend # for regex match + [[ $1 =~ "$config_section_regex" ]] || + die102 "invalid config section: ${(qqq)1}" + config_current_section=$1 + config_sections[$1]=1 +} +config_assign() { + local key=$config_current_section/$1 + if (($+config_values[$key])); then + die102 "Attempting to redefine $key" \ + "from ${(qqq)config_values[$key]}" \ + " to ${(qqq)2}" + fi + config_values[$key]=$2 + config_check $key +} +config_append() { + local key=$config_current_section/$1 + if (($+config_values[$key])); then + config_values[$key]+=$'\0'$2 + else + config_values[$key]=$2 + fi + config_check $key +} +# typeset -f -t config_section config_assign config_append +config_check() { + local value + local match mbegin mend # for regex match + value=$config_values[$1] + if [[ $config_current_section == mountpoints ]]; then + [[ ${1:t} =~ "$mountpoint_name_regex" ]] || + die102 "invalid mountpoint name: ${(qqq)1:t}" + return 0 + fi + case ${1:t} in + (seccomp_profile) + [[ $value =~ "$seccomp_profile_regex" ]] || + die102 "invalid seccomp profile: ${(qqq)value}" + ;; + (container_type) + case $value in + (generic|ephemeral|alsa|xsession|xorg) ;; + (*) die102 "unknown container type: ${(qqq)value}" ;; + esac + ;; + (el_pid1) ;; + (el_prepare) ;; + (*) die102 "unsupported parameter ${(qqq)1:t} in section ${(qqq)config_current_section}";; + esac +} + +parse_config() { + local line lineno=1 + while IFS= read -r "$@" line; do + line=${line//#[ ]##} # strip leading whitespace + case $line in + ('#'*) ;; # comment + (';'*) ;; # comment + ('') ;; # empty + (\[[^=]##\]) # section + config_section ${${line%\]}#\[} + ;; + ([^\]\[=+]##=*) # assign + config_assign ${line%%=*} ${line#*=} + ;; + ([^\]\[=+]##+=*) # append + config_append ${line%%+=*} ${line#*+=} + ;; + (*) # error + die102 "invalid config line $lineno: ${(qqq)line}" + ;; + esac + ((lineno++)) + done +} + +# --- + +typeset -g cdefs_current_section +typeset -gA cdefs_values cdefs_sections + +typeset -g container_name_regex='[-a-zA-Z0-9]+' +typeset -g image_name_regex='[-a-zA-Z0-9][-.a-zA-Z0-9]*' + +cdefs_section() { + [[ -n $cdefs_current_section ]] && + - cdef_validate_section + local match mbegin mend # for regex match + [[ $1 =~ "$container_name_regex" ]] || + die102 "invalid container name: ${(qqq)1}" + cdefs_current_section=$1 + cdefs_sections[$1]=1 +} +cdef_validate_section() { + (($+cdefs_values[$cdefs_current_section/profile])) || + die102 "container ${(qqq)cdefs_current_section} missing definition of \"profile\"" + (($+cdefs_values[$cdefs_current_section/image])) || + die102 "container ${(qqq)cdefs_current_section} missing definition of \"image\"" +} +cdefs_check() { + local value + local match mbegin mend # for regex match + value=$cdefs_values[$1] + case ${1:t} in + (profile) + (($+config_sections[profile/$value])) || + die102 "undefined container profile: ${(qqq)value}" + ;; + (image) + [[ $value =~ "$image_name_regex" ]] || + die102 "invalid container image: ${(qqq)value}" + ;; + (mount_rw) + local mtp + for mtp in "${(0@)value}"; do + (($+config_values[mountpoints/$mtp])) || + die102 "undefined mountpoint: ${(qqq)mtp}" + done + ;; + (mount_ro) + local mtp + for mtp in "${(0@)value}"; do + (($+config_values[mountpoints/$mtp])) || + die102 "undefined mountpoint: ${(qqq)mtp}" + done + ;; + (*) die102 "unsupported parameter ${(qqq)1:t} in section ${(qqq)cdefs_current_section}";; + esac +} +cdefs_assign() { + if [[ $1 = image_prefix ]]; then + cdefs_assign image $2$cdefs_current_section + return $? + fi + local key=$cdefs_current_section/$1 + if (($+cdefs_values[$key])); then + die102 "Attempting to redefine $key" \ + "from ${(qqq)cdefs_values[$key]}" \ + " to ${(qqq)2}" + fi + cdefs_values[$key]=$2 + cdefs_check $key +} +cdefs_append() { + local key=$cdefs_current_section/$1 + if (($+cdefs_values[$key])); then + cdefs_values[$key]+=$'\0'$2 + else + cdefs_values[$key]=$2 + fi + cdefs_check $key +} +# typeset -f -t cdefs_section cdefs_assign cdefs_append + +parse_cdefs() { + local line lineno=1 + while IFS= read -r "$@" line; do + line=${line//#[ ]##} # strip leading whitespace + case $line in + ('#'*) ;; # comment + (';'*) ;; # comment + ('') ;; # empty + (\[[^=]##\]) # section + cdefs_section ${${line%\]}#\[} + ;; + ([^\]\[=+]##=*) # assign + cdefs_assign ${line%%=*} ${line#*=} + ;; + ([^\]\[=+]##+=*) # append + cdefs_append ${line%%+=*} ${line#*+=} + ;; + (*) # error + die102 "invalid cdefs line $lineno: ${(qqq)line}" + ;; + esac + ((lineno++)) + done +} + +# service builder + +sv_start() { + (($+sv_name)) && \ + die100 "sv_start: previous service definition ${(qqq)sv_name} wasn't ended properly" + (( $# != 1 )) && die100 "sv_start: incorrect arguments:" "${(qqq)@}" + typeset -g sv_name=$1 + [[ $sv_name == */* ]] && die100 "sv_start: invalid service name: ${(qqq)1}" + typeset -g sv_dir=$build/$1 + - mkdir -p $sv_dir + - touch $sv_dir/down +} + +sv_cond_file() { + local varname=sv_${1}_lines + if (( ${#${(P)varname}} == 0 )); then + unset "$varname" + return 0 + fi + [[ -e $sv_dir/$1 ]] && die "error: $sv_dir/$1 already exists" + - sv_write_lines $1 "${(P@)varname}" + if [[ $1 == (run|finish) ]]; then + - chmod +x $sv_dir/$1 + fi + unset "$varname" +} +#typeset -f -t sv_cond_file + +sv_end() { + (($+sv_name)) || die100 "$0: no service definition started" + (($+sv_dir)) || die100 "$0: inconsistent state for service ${(qqq)sv_name}" + (( $# != 0 )) && die100 "$0: incorrect arguments:" "${(qqq)@}" + + #if ! [[ -L $scandir/$sv_name ]]; then + # new_services+=( $scandir/$sv_name ) + # # - s6-mkfifodir $sv_dir/event + # - s6-svlink -t 3000 $scandir $sv_dir + # # TODO: chmod + #fi + unset sv_name sv_dir +} + +sv_link_command() { + (($+sv_name)) || die100 "$0: no service definition started" + (($+sv_dir)) || die100 "$0: inconsistent state for service ${(qqq)sv_name}" + (( $# != 2 )) && die100 "$0: incorrect arguments:" "${(qqq)@}" + + local script_path=$sv_dir/$1 + - s6-ln -s -f -n ${commands[$2]} $script_path +} + +sv_write_lines() { + (($+sv_name)) || die100 "$0: no service definition started" + (($+sv_dir)) || die100 "$0: inconsistent state for service ${(qqq)sv_name}" + (( $# < 2 )) && die100 "$0: incorrect arguments:" "${(qqq)@}" + + local file_path=$sv_dir/$1 + local tmp_path=${file_path:h}/.new.${file_path:t} + shift + printf '%s\n' >$tmp_path "$@" || \ + die111 "Error writing to ${(qqq)tmp_path}" + - s6-rename $tmp_path $file_path +} + +sv_el_script() { + (($+sv_name)) || die100 "$0: no service definition started" + (($+sv_dir)) || die100 "$0: inconsistent state for service ${(qqq)sv_name}" + + local file_path=$sv_dir/$1 + local tmp_path=${file_path:h}/.new.${file_path:t} + shift + printf '%s\n' >$tmp_path "#!$commands[execlineb] -P" "$@" || \ + die111 "Error writing to ${(qqq)tmp_path}" + - chmod +x $tmp_path + - s6-rename $tmp_path $file_path +} + +sv_notification_fd() { + (($+sv_name)) || die100 "$0: no service definition started" + (($+sv_dir)) || die100 "$0: inconsistent state for service ${(qqq)sv_name}" + (( $# != 1 )) && die100 "$0: incorrect arguments:" "${(qqq)@}" + + - sv_write_lines notification-fd $1 +} + +sv_env() { + (($+sv_name)) || die100 "$0: no service definition started" + (($+sv_dir)) || die100 "$0: inconsistent state for service ${(qqq)sv_name}" + (( $# < 2 )) && die100 "$0: incorrect arguments:" "${(qqq)@}" + + - mkdir -p $sv_dir/env + local k v + for k v in "$@"; do + [[ $k == */* ]] && die100 "$0: invalid env variable ${(qqq)k}" + - sv_write_lines env/$k "$v" + done +} + +sv_mkdir() { + (($+sv_name)) || die100 "$0: no service definition started" + (($+sv_dir)) || die100 "$0: inconsistent state for service ${(qqq)sv_name}" + (( $# < 1 )) && die100 "$0: incorrect arguments" + + - mkdir -p $sv_dir/${^@} +} + +sv_rm() { + (($+sv_name)) || die100 "$0: no service definition started" + (($+sv_dir)) || die100 "$0: inconsistent state for service ${(qqq)sv_name}" + (( $# != 1 )) && die100 "$0: incorrect arguments" + + if [[ -e $sv_dir/$1 ]]; then + - rm -r $sv_dir/$1 + fi +} + +sv_log() { + (($+sv_name)) || die100 "$0: no service definition started" + (($+sv_dir)) || die100 "$0: inconsistent state for service ${(qqq)sv_name}" + (( $# != 0 )) && die100 "$0: incorrect arguments" + + - sv_mkdir log log/env + - printf '%s' $sv_name >$sv_dir/log/env/sv_name + - sv_el_script log/run \ + 's6-envdir env' \ + 'importas -i sv_name sv_name' \ + 'if { mkdir -p /run/log/${sv_name} }' \ + 'pipeline { s6-log -b -- 1 n10 s10240000 t /run/log/${sv_name} } awk "{print ENVIRON[\"sv_name\"]\": \"$0}"' +} + +# end service builder + +erase_previous() { + local s rundir + for s in $build/*(N); do + if (($+new_services[${s:t}])); then + else + rundir=$scandir/${s:t} + if [[ ${rundir:P} != ${s:P} ]]; then + die102 "mismatched service symlink:" \ + "${(qqq)rundir} -> ${(qqq)rundir:P} != ${(qqq)s:P}" + fi + - s6-svunlink -t 1000 $scandir ${s:t} + - warn_msg 'rm ' $s + - rm -rv $s + fi + done +} + +sv_common_finish() { + local -a lines=( + 'fdmove -c 2 1' \ + 'backtick -E up { s6-svstat -o up ../'${(qqq)sv_name_server}' }' + 'backtick -E wantedup { s6-svstat -o wantedup ../'${(qqq)sv_name_server}' }' + ## down this service if server is neither up nor wanted up + 'if -n { if { $up } $wantedup }' + # 'ifelse { eltest true == $up -o true == $wantedup } { }' + 's6-svc -d .' + ) + - sv_el_script finish $lines +} + + +write_sv_definitions() { + typeset -g build + build=/tmp/user_containers.$USER + local c + for c in "${(@k)cdefs_sections}"; do + - container_sv $c + done +} + +container_sv() { + local p + local container_type seccomp_profile image + local user userdir rundir src dst rw src_esc dst_esc mnt_dirs + local -a el_pid1 el_prepare bind_mounts + p=profile/${cdefs_values[$1/profile]:-default} + image=${cdefs_values[$1/image]} + container_type=${config_values[$p/container_type]:-generic} + seccomp_profile=${config_values[$p/seccomp_profile]:-default} + (($+config_values[$p/el_pid1])) && + el_pid1=( "${(0@)config_values[$p/el_pid1]}" ) + (($+config_values[$p/el_prepare])) && + el_prepare=( "${(0@)config_values[$p/el_prepare]}" ) + + user=ccx + userdir=$container_user_dir/$user/$1 + rundir=/run/containers/$1.$user + bind_mounts=( + $container_image_dir/$image + $userdir/root + ro + + $userdir/home + $userdir/root/home + rw + + $rundir/run + $userdir/root/run + rw + + $rundir/tmp + $userdir/root/tmp + rw + + $rundir/mnt + $userdir/root/mnt + rw + ) + (($+cdefs_values[$1/mount_ro])) && for dst in "${(0@)cdefs_values[$1/mount_ro]}"; do + bind_mounts+=( $config_values[mountpoints/$dst] $userdir/root/mnt/$dst ro ) + mnt_dirs+=( $dst ) + done + (($+cdefs_values[$1/mount_rw])) && for dst in "${(0@)cdefs_values[$1/mount_rw]}"; do + bind_mounts+=( $config_values[mountpoints/$dst] $userdir/root/mnt/$dst rw ) + mnt_dirs+=( $dst ) + done + for src dst rw in "$bind_mounts[@]"; do + #fstab+=( $src$'\t'$dst$'\tnone\tbind,'$rw$',nosuid,nodev,slave\t0 0' ) + src_esc=\"${${src//\\/\\\\}//\"/\\\"}\" + dst_esc=\"${${dst//\\/\\\\}//\"/\\\"}\" + el_prepare+=( + "if { s6-mount -o bind,$rw,nodev,nosuid,slave $src_esc $dst_esc }" + "if { s6-mount -o remount,bind,$rw,nodev,nosuid . $dst_esc }" + ) + done + + - sv_start container.$1.$user + - s6-ln -s -f -n $service_script_dir/generic/run $sv_dir/run + - s6-ln -s -f -n $service_script_dir/generic/finish $sv_dir/finish + - sv_mkdir data + + if (($#el_pid1)); then + sv_el_script data/pid1_exec "${(0@)el_pid1}" + else + sv_rm data/pid1_exec + fi + + if (($#el_prepare)); then + sv_el_script data/prepare_chroot "${(0@)el_prepare}" + else + sv_rm data/prepare_chroot + fi + + - sv_env CONTAINER_NAME $1 + - sv_env CONTAINER_USER $user + - sv_env CONTAINER_CAPS '' + - sv_env CONTAINER_MNT_DIRS "ns $mnt_dirs" + - sv_env CONTAINER_SECCOMP_PROFILE $seccomp_profile + + - sv_end +} +#typeset -f -t container_sv + +# --- + +main.ns_run_svc_gen() { + - parse_config <<EOF +[profile/network] +seccomp_profile=default + +[profile/default] +seccomp_profile=default +el_pid1+=unshare -n # make new network namespace +el_pid1+=if { ip addr add 127.0.0.1/8 dev lo } +el_pid1+=if { ip addr add ::1/128 dev lo } +el_pid1+=if { ip link set lo up } + +[profile/dev] +seccomp_profile=ptrace +el_pid1+=unshare -n # make new network namespace +el_pid1+=if { ip addr add 127.0.0.1/8 dev lo } +el_pid1+=if { ip addr add ::1/128 dev lo } +el_pid1+=if { ip link set lo up } + +[profile/audio] +seccomp_profile=default +el_pid1+=unshare -n # make new network namespace +el_pid1+=if { ip addr add 127.0.0.1/8 dev lo } +el_pid1+=if { ip addr add ::1/128 dev lo } +el_pid1+=if { ip link set lo up } +el_prepare+=if { mount -o bind,ro /dev/snd dev/snd } +el_prepare+=mount -t sysfs sysfs sys # maybe not necessary? + +[profile/pthbs] +seccomp_profile=build +el_pid1+=unshare -n # make new network namespace +el_pid1+=if { ip addr add 127.0.0.1/8 dev lo } +el_pid1+=if { ip addr add ::1/128 dev lo } +el_pid1+=if { ip link set lo up } +el_pid1+=zsh -c "ulimit -hn 16384 && exec \"$@\"" -- + +[profile/usb] +seccomp_profile=default +el_pid1+=unshare -n # make new network namespace +el_pid1+=if { ip addr add 127.0.0.1/8 dev lo } +el_pid1+=if { ip addr add ::1/128 dev lo } +el_pid1+=if { ip link set lo up } +el_prepare+=if { mount -o bind,ro /dev/bus/usb dev/bus/usb } +el_prepare+=mount -t sysfs sysfs sys + +[profile/xpra] +seccomp_profile=xpra +container_type=ephemeral +el_pid1+=unshare -n # make new network namespace +el_pid1+=if { ip addr add 127.0.0.1/8 dev lo } +el_pid1+=if { ip addr add ::1/128 dev lo } +el_pid1+=if { ip link set lo up } + +[profile/openssh] +seccomp_profile=setuidgid +el_pid1+=zsh -c "ulimit -hn 16384 && exec \"$@\"" -- + +[mountpoints] +init=/home/ccx/bzr/container-user-init +ccx-bzr=/home/ccx/bzr +ccx-dotfiles=/home/ccx/bzr/container-dotfiles +ccx-scripts=/home/ccx/bzr/container-scripts +ccx-password-store=/home/ccx/bzr/password-store +ccx-development=/home/ccx/development +ccx-baregit=/home/ccx/baregit +rcm-devops=/mnt/volumes/containers/user/ccx/git/home/ccx/git/rcm-devops +ccx-task=/home/ccx/task +pthbs=/usr/src/pthbs +mrrl=/usr/src/mrrl +audio=/mnt/volumes/audio +video=/mnt/volumes/video +photos=/mnt/volumes/photos +versions=/versions +mail-te2000.cz-ccx=/home/ccx/mail/te2000.cz/ccx +mail-disroot.org-ccx=/home/ccx/mail/disroot.org/ccx +mail-recombee.com-jan.pobrislo=/home/ccx/mail/recombee.com/jan.pobrislo +EOF + local key + for key in "${(ko@)config_values}"; do + - printf >&2 "conf: %s=%s\n" $key ${(j:; :)${(qqq0@)config_values[$key]}} + done + - parse_cdefs <ccx.cdefs + for key in "${(ko@)cdefs_values}"; do + - printf >&2 "cdef: %s=%s\n" $key ${(j:; :)${(qqq0@)cdefs_values[$key]}} + done + - write_sv_definitions +} +#typeset -f -t main.ns_run_svc_gen + +main.ns_run_svc_gen "$@" +# vim: ft=zsh noet ts=4 sts=4 sw=4