ns_run_svc_gen (16642B)
1 #!/bin/zsh 2 setopt no_unset warn_create_global extended_glob 3 zmodload zsh/system || exit $? 4 zmodload -m -F zsh/files b:zf_\* || exit $? 5 typeset -g scriptname=$0 6 7 typeset -g container_image_dir=/mnt/volumes/containers/systems 8 typeset -g container_user_dir=/mnt/volumes/containers/user 9 typeset -g service_script_dir=${0:P:h:h}/service_scripts 10 11 # message helpers 12 typeset -g hl_fatal hl_warn hl_reset 13 if (( $terminfo[colors] >= 8 )); then 14 hl_fatal='%F{red}%B'; hl_fatal=${(%)hl_fatal} 15 hl_warn='%F{yellow}%B'; hl_warn=${(%)hl_warn} 16 hl_reset='%b%f'; hl_reset=${(%)hl_reset} 17 fi 18 19 err_msg() { 20 local first=$1 21 shift 22 printf >&2 '%s%s%s%s\n' "$hl_fatal" "$first" "$hl_reset" "$*" 23 } 24 25 warn_msg() { 26 local first=$1 27 shift 28 printf >&2 '%s%s%s%s\n' "$hl_warn" "$first" "$hl_reset" "$*" 29 } 30 31 # helper that prints out stack, error message and exits 32 die_ret() { 33 set +x 34 local ret n 35 ret=$1 36 shift 37 print -r - >&2 "${hl_fatal}Fatal$hl_reset error occurend in:" 38 for n in {${#funcfiletrace}..1}; do 39 printf >&2 '%d> %s (%s)\n' $n "$funcfiletrace[$n]" "$functrace[$n]" 40 done 41 printf >&2 '%s\n' "${hl_fatal}*$hl_reset $^@" 42 exit $ret 43 } 44 45 # useful exit aliases 46 die() { 47 set +x 48 die_ret 1 "$@" 49 } 50 die100() { # 100: wrong usage 51 set +x 52 die_ret 100 "$@" 53 } 54 die101() { # 101: can't happen / internal error 55 set +x 56 die_ret 101 "$@" 57 } 58 die102() { # 102: config / data file error 59 set +x 60 die_ret 102 "$@" 61 } 62 die111() { # 111: system call failed / transient error 63 set +x 64 die_ret 111 "$@" 65 } 66 67 -() { # Run command and die on nonzero exitcode 68 "$@" || die_ret $? "command failed with exitcode $?: ${(j: :)${(q)@}}" 69 } 70 71 die_usage() { 72 die100 "$@" "usage: ns_run_svc_gen TODO" 73 } 74 75 # --- 76 77 typeset -g config_section_regex='(mountpoints|profile/[-a-zA-Z0-9][-.a-zA-Z0-9]*)' 78 typeset -g mountpoint_name_regex='[-a-zA-Z0-9][-.a-zA-Z0-9]*' 79 typeset -g seccomp_profile_regex='[-a-zA-Z0-9][-.a-zA-Z0-9]*' 80 81 typeset -g config_current_section 82 typeset -gA config_values config_sections 83 config_section() { 84 local match mbegin mend # for regex match 85 [[ $1 =~ "$config_section_regex" ]] || 86 die102 "invalid config section: ${(qqq)1}" 87 config_current_section=$1 88 config_sections[$1]=1 89 } 90 config_assign() { 91 local key=$config_current_section/$1 92 if (($+config_values[$key])); then 93 die102 "Attempting to redefine $key" \ 94 "from ${(qqq)config_values[$key]}" \ 95 " to ${(qqq)2}" 96 fi 97 config_values[$key]=$2 98 config_check $key 99 } 100 config_append() { 101 local key=$config_current_section/$1 102 if (($+config_values[$key])); then 103 config_values[$key]+=$'\0'$2 104 else 105 config_values[$key]=$2 106 fi 107 config_check $key 108 } 109 # typeset -f -t config_section config_assign config_append 110 config_check() { 111 local value 112 local match mbegin mend # for regex match 113 value=$config_values[$1] 114 if [[ $config_current_section == mountpoints ]]; then 115 [[ ${1:t} =~ "$mountpoint_name_regex" ]] || 116 die102 "invalid mountpoint name: ${(qqq)1:t}" 117 return 0 118 fi 119 case ${1:t} in 120 (seccomp_profile) 121 [[ $value =~ "$seccomp_profile_regex" ]] || 122 die102 "invalid seccomp profile: ${(qqq)value}" 123 ;; 124 (container_type) 125 case $value in 126 (generic|ephemeral|alsa|xsession|xorg) ;; 127 (*) die102 "unknown container type: ${(qqq)value}" ;; 128 esac 129 ;; 130 (el_pid1) ;; 131 (el_prepare) ;; 132 (*) die102 "unsupported parameter ${(qqq)1:t} in section ${(qqq)config_current_section}";; 133 esac 134 } 135 136 parse_config() { 137 local line lineno=1 138 while IFS= read -r "$@" line; do 139 line=${line//#[ ]##} # strip leading whitespace 140 case $line in 141 ('#'*) ;; # comment 142 (';'*) ;; # comment 143 ('') ;; # empty 144 (\[[^=]##\]) # section 145 config_section ${${line%\]}#\[} 146 ;; 147 ([^\]\[=+]##=*) # assign 148 config_assign ${line%%=*} ${line#*=} 149 ;; 150 ([^\]\[=+]##+=*) # append 151 config_append ${line%%+=*} ${line#*+=} 152 ;; 153 (*) # error 154 die102 "invalid config line $lineno: ${(qqq)line}" 155 ;; 156 esac 157 ((lineno++)) 158 done 159 } 160 161 # --- 162 163 typeset -g cdefs_current_section 164 typeset -gA cdefs_values cdefs_sections 165 166 typeset -g container_name_regex='[-a-zA-Z0-9]+' 167 typeset -g image_name_regex='[-a-zA-Z0-9][-.a-zA-Z0-9]*' 168 169 cdefs_section() { 170 [[ -n $cdefs_current_section ]] && 171 - cdef_validate_section 172 local match mbegin mend # for regex match 173 [[ $1 =~ "$container_name_regex" ]] || 174 die102 "invalid container name: ${(qqq)1}" 175 cdefs_current_section=$1 176 cdefs_sections[$1]=1 177 } 178 cdef_validate_section() { 179 (($+cdefs_values[$cdefs_current_section/profile])) || 180 die102 "container ${(qqq)cdefs_current_section} missing definition of \"profile\"" 181 (($+cdefs_values[$cdefs_current_section/image])) || 182 die102 "container ${(qqq)cdefs_current_section} missing definition of \"image\"" 183 } 184 cdefs_check() { 185 local value 186 local match mbegin mend # for regex match 187 value=$cdefs_values[$1] 188 case ${1:t} in 189 (profile) 190 (($+config_sections[profile/$value])) || 191 die102 "undefined container profile: ${(qqq)value}" 192 ;; 193 (image) 194 [[ $value =~ "$image_name_regex" ]] || 195 die102 "invalid container image: ${(qqq)value}" 196 ;; 197 (mount_rw) 198 local mtp 199 for mtp in "${(0@)value}"; do 200 (($+config_values[mountpoints/$mtp])) || 201 die102 "undefined mountpoint: ${(qqq)mtp}" 202 done 203 ;; 204 (mount_ro) 205 local mtp 206 for mtp in "${(0@)value}"; do 207 (($+config_values[mountpoints/$mtp])) || 208 die102 "undefined mountpoint: ${(qqq)mtp}" 209 done 210 ;; 211 (*) die102 "unsupported parameter ${(qqq)1:t} in section ${(qqq)cdefs_current_section}";; 212 esac 213 } 214 cdefs_assign() { 215 if [[ $1 = image_prefix ]]; then 216 cdefs_assign image $2$cdefs_current_section 217 return $? 218 fi 219 local key=$cdefs_current_section/$1 220 if (($+cdefs_values[$key])); then 221 die102 "Attempting to redefine $key" \ 222 "from ${(qqq)cdefs_values[$key]}" \ 223 " to ${(qqq)2}" 224 fi 225 cdefs_values[$key]=$2 226 cdefs_check $key 227 } 228 cdefs_append() { 229 local key=$cdefs_current_section/$1 230 if (($+cdefs_values[$key])); then 231 cdefs_values[$key]+=$'\0'$2 232 else 233 cdefs_values[$key]=$2 234 fi 235 cdefs_check $key 236 } 237 # typeset -f -t cdefs_section cdefs_assign cdefs_append 238 239 parse_cdefs() { 240 local line lineno=1 241 while IFS= read -r "$@" line; do 242 line=${line//#[ ]##} # strip leading whitespace 243 case $line in 244 ('#'*) ;; # comment 245 (';'*) ;; # comment 246 ('') ;; # empty 247 (\[[^=]##\]) # section 248 cdefs_section ${${line%\]}#\[} 249 ;; 250 ([^\]\[=+]##=*) # assign 251 cdefs_assign ${line%%=*} ${line#*=} 252 ;; 253 ([^\]\[=+]##+=*) # append 254 cdefs_append ${line%%+=*} ${line#*+=} 255 ;; 256 (*) # error 257 die102 "invalid cdefs line $lineno: ${(qqq)line}" 258 ;; 259 esac 260 ((lineno++)) 261 done 262 } 263 264 # service builder 265 266 sv_start() { 267 (($+sv_name)) && \ 268 die100 "sv_start: previous service definition ${(qqq)sv_name} wasn't ended properly" 269 (( $# != 1 )) && die100 "sv_start: incorrect arguments:" "${(qqq)@}" 270 typeset -g sv_name=$1 271 [[ $sv_name == */* ]] && die100 "sv_start: invalid service name: ${(qqq)1}" 272 typeset -g sv_dir=$build/$1 273 - mkdir -p $sv_dir 274 - touch $sv_dir/down 275 } 276 277 sv_cond_file() { 278 local varname=sv_${1}_lines 279 if (( ${#${(P)varname}} == 0 )); then 280 unset "$varname" 281 return 0 282 fi 283 [[ -e $sv_dir/$1 ]] && die "error: $sv_dir/$1 already exists" 284 - sv_write_lines $1 "${(P@)varname}" 285 if [[ $1 == (run|finish) ]]; then 286 - chmod +x $sv_dir/$1 287 fi 288 unset "$varname" 289 } 290 #typeset -f -t sv_cond_file 291 292 sv_end() { 293 (($+sv_name)) || die100 "$0: no service definition started" 294 (($+sv_dir)) || die100 "$0: inconsistent state for service ${(qqq)sv_name}" 295 (( $# != 0 )) && die100 "$0: incorrect arguments:" "${(qqq)@}" 296 297 #if ! [[ -L $scandir/$sv_name ]]; then 298 # new_services+=( $scandir/$sv_name ) 299 # # - s6-mkfifodir $sv_dir/event 300 # - s6-svlink -t 3000 $scandir $sv_dir 301 # # TODO: chmod 302 #fi 303 unset sv_name sv_dir 304 } 305 306 sv_link_command() { 307 (($+sv_name)) || die100 "$0: no service definition started" 308 (($+sv_dir)) || die100 "$0: inconsistent state for service ${(qqq)sv_name}" 309 (( $# != 2 )) && die100 "$0: incorrect arguments:" "${(qqq)@}" 310 311 local script_path=$sv_dir/$1 312 - s6-ln -s -f -n ${commands[$2]} $script_path 313 } 314 315 sv_write_lines() { 316 (($+sv_name)) || die100 "$0: no service definition started" 317 (($+sv_dir)) || die100 "$0: inconsistent state for service ${(qqq)sv_name}" 318 (( $# < 2 )) && die100 "$0: incorrect arguments:" "${(qqq)@}" 319 320 local file_path=$sv_dir/$1 321 local tmp_path=${file_path:h}/.new.${file_path:t} 322 shift 323 printf '%s\n' >$tmp_path "$@" || \ 324 die111 "Error writing to ${(qqq)tmp_path}" 325 - s6-rename $tmp_path $file_path 326 } 327 328 sv_el_script() { 329 (($+sv_name)) || die100 "$0: no service definition started" 330 (($+sv_dir)) || die100 "$0: inconsistent state for service ${(qqq)sv_name}" 331 332 local file_path=$sv_dir/$1 333 local tmp_path=${file_path:h}/.new.${file_path:t} 334 shift 335 printf '%s\n' >$tmp_path "#!$commands[execlineb] -P" "$@" || \ 336 die111 "Error writing to ${(qqq)tmp_path}" 337 - chmod +x $tmp_path 338 - s6-rename $tmp_path $file_path 339 } 340 341 sv_notification_fd() { 342 (($+sv_name)) || die100 "$0: no service definition started" 343 (($+sv_dir)) || die100 "$0: inconsistent state for service ${(qqq)sv_name}" 344 (( $# != 1 )) && die100 "$0: incorrect arguments:" "${(qqq)@}" 345 346 - sv_write_lines notification-fd $1 347 } 348 349 sv_env() { 350 (($+sv_name)) || die100 "$0: no service definition started" 351 (($+sv_dir)) || die100 "$0: inconsistent state for service ${(qqq)sv_name}" 352 (( $# < 2 )) && die100 "$0: incorrect arguments:" "${(qqq)@}" 353 354 - mkdir -p $sv_dir/env 355 local k v 356 for k v in "$@"; do 357 [[ $k == */* ]] && die100 "$0: invalid env variable ${(qqq)k}" 358 - sv_write_lines env/$k "$v" 359 done 360 } 361 362 sv_mkdir() { 363 (($+sv_name)) || die100 "$0: no service definition started" 364 (($+sv_dir)) || die100 "$0: inconsistent state for service ${(qqq)sv_name}" 365 (( $# < 1 )) && die100 "$0: incorrect arguments" 366 367 - mkdir -p $sv_dir/${^@} 368 } 369 370 sv_rm() { 371 (($+sv_name)) || die100 "$0: no service definition started" 372 (($+sv_dir)) || die100 "$0: inconsistent state for service ${(qqq)sv_name}" 373 (( $# != 1 )) && die100 "$0: incorrect arguments" 374 375 if [[ -e $sv_dir/$1 ]]; then 376 - rm -r $sv_dir/$1 377 fi 378 } 379 380 sv_log() { 381 (($+sv_name)) || die100 "$0: no service definition started" 382 (($+sv_dir)) || die100 "$0: inconsistent state for service ${(qqq)sv_name}" 383 (( $# != 0 )) && die100 "$0: incorrect arguments" 384 385 - sv_mkdir log log/env 386 - printf '%s' $sv_name >$sv_dir/log/env/sv_name 387 - sv_el_script log/run \ 388 's6-envdir env' \ 389 'importas -i sv_name sv_name' \ 390 'if { mkdir -p /run/log/${sv_name} }' \ 391 'pipeline { s6-log -b -- 1 n10 s10240000 t /run/log/${sv_name} } awk "{print ENVIRON[\"sv_name\"]\": \"$0}"' 392 } 393 394 # end service builder 395 396 erase_previous() { 397 local s rundir 398 for s in $build/*(N); do 399 if (($+new_services[${s:t}])); then 400 else 401 rundir=$scandir/${s:t} 402 if [[ ${rundir:P} != ${s:P} ]]; then 403 die102 "mismatched service symlink:" \ 404 "${(qqq)rundir} -> ${(qqq)rundir:P} != ${(qqq)s:P}" 405 fi 406 - s6-svunlink -t 1000 $scandir ${s:t} 407 - warn_msg 'rm ' $s 408 - rm -rv $s 409 fi 410 done 411 } 412 413 sv_common_finish() { 414 local -a lines=( 415 'fdmove -c 2 1' \ 416 'backtick -E up { s6-svstat -o up ../'${(qqq)sv_name_server}' }' 417 'backtick -E wantedup { s6-svstat -o wantedup ../'${(qqq)sv_name_server}' }' 418 ## down this service if server is neither up nor wanted up 419 'if -n { if { $up } $wantedup }' 420 # 'ifelse { eltest true == $up -o true == $wantedup } { }' 421 's6-svc -d .' 422 ) 423 - sv_el_script finish $lines 424 } 425 426 427 write_sv_definitions() { 428 typeset -g build 429 build=/tmp/user_containers.$USER 430 local c 431 for c in "${(@k)cdefs_sections}"; do 432 - container_sv $c 433 done 434 } 435 436 container_sv() { 437 local p 438 local container_type seccomp_profile image 439 local user userdir rundir src dst rw src_esc dst_esc mnt_dirs 440 local -a el_pid1 el_prepare bind_mounts 441 p=profile/${cdefs_values[$1/profile]:-default} 442 image=${cdefs_values[$1/image]} 443 container_type=${config_values[$p/container_type]:-generic} 444 seccomp_profile=${config_values[$p/seccomp_profile]:-default} 445 (($+config_values[$p/el_pid1])) && 446 el_pid1=( "${(0@)config_values[$p/el_pid1]}" ) 447 (($+config_values[$p/el_prepare])) && 448 el_prepare=( "${(0@)config_values[$p/el_prepare]}" ) 449 450 user=ccx 451 userdir=$container_user_dir/$user/$1 452 rundir=/run/containers/$1.$user 453 bind_mounts=( 454 $container_image_dir/$image 455 $userdir/root 456 ro 457 458 $userdir/home 459 $userdir/root/home 460 rw 461 462 $rundir/run 463 $userdir/root/run 464 rw 465 466 $rundir/tmp 467 $userdir/root/tmp 468 rw 469 470 $rundir/mnt 471 $userdir/root/mnt 472 rw 473 ) 474 (($+cdefs_values[$1/mount_ro])) && for dst in "${(0@)cdefs_values[$1/mount_ro]}"; do 475 bind_mounts+=( $config_values[mountpoints/$dst] $userdir/root/mnt/$dst ro ) 476 mnt_dirs+=( $dst ) 477 done 478 (($+cdefs_values[$1/mount_rw])) && for dst in "${(0@)cdefs_values[$1/mount_rw]}"; do 479 bind_mounts+=( $config_values[mountpoints/$dst] $userdir/root/mnt/$dst rw ) 480 mnt_dirs+=( $dst ) 481 done 482 for src dst rw in "$bind_mounts[@]"; do 483 #fstab+=( $src$'\t'$dst$'\tnone\tbind,'$rw$',nosuid,nodev,slave\t0 0' ) 484 src_esc=\"${${src//\\/\\\\}//\"/\\\"}\" 485 dst_esc=\"${${dst//\\/\\\\}//\"/\\\"}\" 486 el_prepare+=( 487 "if { s6-mount -o bind,$rw,nodev,nosuid,slave $src_esc $dst_esc }" 488 "if { s6-mount -o remount,bind,$rw,nodev,nosuid . $dst_esc }" 489 ) 490 done 491 492 - sv_start container.$1.$user 493 - s6-ln -s -f -n $service_script_dir/generic/run $sv_dir/run 494 - s6-ln -s -f -n $service_script_dir/generic/finish $sv_dir/finish 495 - sv_mkdir data 496 497 if (($#el_pid1)); then 498 sv_el_script data/pid1_exec "${(0@)el_pid1}" 499 else 500 sv_rm data/pid1_exec 501 fi 502 503 if (($#el_prepare)); then 504 sv_el_script data/prepare_chroot "${(0@)el_prepare}" 505 else 506 sv_rm data/prepare_chroot 507 fi 508 509 - sv_env CONTAINER_NAME $1 510 - sv_env CONTAINER_USER $user 511 - sv_env CONTAINER_CAPS '' 512 - sv_env CONTAINER_MNT_DIRS "ns $mnt_dirs" 513 - sv_env CONTAINER_SECCOMP_PROFILE $seccomp_profile 514 515 - sv_end 516 } 517 #typeset -f -t container_sv 518 519 # --- 520 521 main.ns_run_svc_gen() { 522 - parse_config <<EOF 523 [profile/network] 524 seccomp_profile=default 525 526 [profile/default] 527 seccomp_profile=default 528 el_pid1+=unshare -n # make new network namespace 529 el_pid1+=if { ip addr add 127.0.0.1/8 dev lo } 530 el_pid1+=if { ip addr add ::1/128 dev lo } 531 el_pid1+=if { ip link set lo up } 532 533 [profile/dev] 534 seccomp_profile=ptrace 535 el_pid1+=unshare -n # make new network namespace 536 el_pid1+=if { ip addr add 127.0.0.1/8 dev lo } 537 el_pid1+=if { ip addr add ::1/128 dev lo } 538 el_pid1+=if { ip link set lo up } 539 540 [profile/audio] 541 seccomp_profile=default 542 el_pid1+=unshare -n # make new network namespace 543 el_pid1+=if { ip addr add 127.0.0.1/8 dev lo } 544 el_pid1+=if { ip addr add ::1/128 dev lo } 545 el_pid1+=if { ip link set lo up } 546 el_prepare+=if { mount -o bind,ro /dev/snd dev/snd } 547 el_prepare+=mount -t sysfs sysfs sys # maybe not necessary? 548 549 [profile/pthbs] 550 seccomp_profile=build 551 el_pid1+=unshare -n # make new network namespace 552 el_pid1+=if { ip addr add 127.0.0.1/8 dev lo } 553 el_pid1+=if { ip addr add ::1/128 dev lo } 554 el_pid1+=if { ip link set lo up } 555 el_pid1+=zsh -c "ulimit -hn 16384 && exec \"$@\"" -- 556 557 [profile/usb] 558 seccomp_profile=default 559 el_pid1+=unshare -n # make new network namespace 560 el_pid1+=if { ip addr add 127.0.0.1/8 dev lo } 561 el_pid1+=if { ip addr add ::1/128 dev lo } 562 el_pid1+=if { ip link set lo up } 563 el_prepare+=if { mount -o bind,ro /dev/bus/usb dev/bus/usb } 564 el_prepare+=mount -t sysfs sysfs sys 565 566 [profile/xpra] 567 seccomp_profile=xpra 568 container_type=ephemeral 569 el_pid1+=unshare -n # make new network namespace 570 el_pid1+=if { ip addr add 127.0.0.1/8 dev lo } 571 el_pid1+=if { ip addr add ::1/128 dev lo } 572 el_pid1+=if { ip link set lo up } 573 574 [profile/openssh] 575 seccomp_profile=setuidgid 576 el_pid1+=zsh -c "ulimit -hn 16384 && exec \"$@\"" -- 577 578 [mountpoints] 579 init=/home/ccx/bzr/container-user-init 580 ccx-bzr=/home/ccx/bzr 581 ccx-dotfiles=/home/ccx/bzr/container-dotfiles 582 ccx-scripts=/home/ccx/bzr/container-scripts 583 ccx-password-store=/home/ccx/bzr/password-store 584 ccx-development=/home/ccx/development 585 ccx-baregit=/home/ccx/baregit 586 rcm-devops=/mnt/volumes/containers/user/ccx/git/home/ccx/git/rcm-devops 587 ccx-task=/home/ccx/task 588 pthbs=/usr/src/pthbs 589 mrrl=/usr/src/mrrl 590 audio=/mnt/volumes/audio 591 video=/mnt/volumes/video 592 photos=/mnt/volumes/photos 593 versions=/versions 594 mail-te2000.cz-ccx=/home/ccx/mail/te2000.cz/ccx 595 mail-disroot.org-ccx=/home/ccx/mail/disroot.org/ccx 596 mail-recombee.com-jan.pobrislo=/home/ccx/mail/recombee.com/jan.pobrislo 597 EOF 598 local key 599 for key in "${(ko@)config_values}"; do 600 - printf >&2 "conf: %s=%s\n" $key ${(j:; :)${(qqq0@)config_values[$key]}} 601 done 602 - parse_cdefs <ccx.cdefs 603 for key in "${(ko@)cdefs_values}"; do 604 - printf >&2 "cdef: %s=%s\n" $key ${(j:; :)${(qqq0@)cdefs_values[$key]}} 605 done 606 - write_sv_definitions 607 } 608 #typeset -f -t main.ns_run_svc_gen 609 610 main.ns_run_svc_gen "$@" 611 # vim: ft=zsh noet ts=4 sts=4 sw=4