commit b25e95c970f937c160442d08fe61be7ad3e5f0e9
parent 2a751866c53d8ddc59f689b1396e7424ab9a2a7f
Author: Jan Pobrislo <ccx@te2000.cz>
Date: Fri, 19 Sep 2025 00:05:32 +0000
Add WIP service generator
Diffstat:
| A | sbin/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