4 # Copyright 2025 xs <xs@inda.re>
6 # Permission to use, copy, modify, and/or distribute this software for any
7 # purpose with or without fee is hereby granted, provided that the above
8 # copyright notice and this permission notice appear in all copies.
10 # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
11 # REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
12 # FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
13 # INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
14 # LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
15 # OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
16 # PERFORMANCE OF THIS SOFTWARE.
18 # shellcheck disable=SC2046,SC2031
20 # Global configuration
21 # ====================
24 : "${HOSTNAME:=$(uname -n)}"
26 # shellcheck disable=SC2031
30 # Here is the default configuration
33 : "${R7_GROUP_ALLPREFIX:=no}"
34 : "${R7_GROUP_SUMMARY:=no}"
35 : "${R7_GROUP_EXITONERROR:=no}"
36 : "${R7_PARALLEL:=no}"
37 : "${R7_PREFIX:=install state doc}"
38 : "${R7_SHOWOUTPUT:=no}"
39 : "${R7_WORKDIR:=$PWD}"
40 : "${R7_SITE_CONFIG:=$R7_WORKDIR/site-config}"
41 : "${R7_OUTPUTDIR:=$R7_WORKDIR/_output}"
42 : "${SSH_CONFIG_DIR:=$R7_WORKDIR/.ssh}"
43 : "${SSH_CONFIG_FILE:=$R7_WORKDIR/.ssh/config}"
44 : "${SSH_CONTROL_DIR:=$SSH_CONFIG_DIR/control}"
45 : "${SSH_IDENTITY_FILE:=$R7_WORKDIR/.ssh/id_ed25519_r7}"
46 : "${SSH_KNOWN_HOSTS:=$R7_WORKDIR/.ssh/known_hosts}"
47 : "${SSH_CONNECT_TIMEOUT:=3}"
51 # Validate environment
52 if test "$R7_DEBUG" = yes; then
53 set | grep -e '^R7_' -e '^SSH' 1>&2
56 for param in "$R7_DEBUG" "$R7_DRYRUN" "$R7_PARALLEL"; do
59 *) _fail "Invalid parameter value: '$param'" ;;
63 R7_WORKDIR_tmp=$(_resolv_dir "$R7_WORKDIR")
64 test -d "$R7_WORKDIR_tmp" || _error "Working directory not found: '$R7_WORKDIR'"
65 R7_WORKDIR=$R7_WORKDIR_tmp
66 R7_RUNDIR=$R7_WORKDIR/_run
67 R7_OUTPUTDIR=$R7_WORKDIR/_output
69 R7_SITE_CONFIG_tmp=$(_resolv_file "$R7_SITE_CONFIG")
70 test -f "$R7_SITE_CONFIG_tmp" || _error "Site configuration not found: '$R7_SITE_CONFIG'"
71 R7_SITE_CONFIG=$R7_SITE_CONFIG_tmp
73 SSH_CONFIG_FILE_tmp=$(_resolv_file "$SSH_CONFIG_FILE")
74 test -f "$SSH_CONFIG_FILE_tmp" || _error "Unable to read SSH configuration: '$SSH_CONFIG_FILE'"
75 SSH_CONFIG_FILE=$SSH_CONFIG_FILE_tmp
77 SSH_IDENTITY_FILE_tmp=$(_resolv_file "$SSH_IDENTITY_FILE")
78 test -f "$SSH_IDENTITY_FILE_tmp" || _error "Unable to read SSH identity file: '$SSH_IDENTITY_FILE'"
79 SSH_IDENTITY_FILE=$SSH_IDENTITY_FILE_tmp
82 unset R7_WORKDIR_tmp R7_SITE_CONFIG_tmp SSH_CONFIG_FILE_tmp SSH_IDENTITY_FILE_tmp
89 printf "FAILURE: %s\n" "$*" >&2
94 printf "ERROR: %s\n" "$*" >&2
98 test "$R7_DEBUG" = yes && printf %s\\n "${*:-$(cat -)}" >&2
102 if test "$R7_ENV_READY" = yes; then
103 cd "$R7_WORKDIR" || return 2
104 _trace_id_last=$(trace_id_last)
105 if test -n "$_trace_id_last"; then
106 printf -- "- (%s) - in %s the %s\n" \
107 "$_trace_id_last" "$(basename "$R7_WORKDIR")" "$(_epoch "$_trace_id_last")"
108 nodes_active | sed 's/^/ - (active) /'
121 usage: $0 [options] [command [arguments...]]
123 -h Show usage and exit
126 -F SSH client configuration file
127 -w Work directory where to find ./nodes and ./groups
128 -r Run directory, default to ./_run
129 -o Output directory, default to ./_output
130 -c Site configuration file, default to ./site-config
131 -n Dry run, prevent any host modification
132 -e Exit the group execution on the error function
133 -p Run each host in parallel per group call
134 -O Show output interactively
136 -a Select all available prefixes for a group command
137 -s Output-dump and summary are applied after a group command
140 run-config Run the site configuration
141 group Deploy specified group to hosts
142 output-dump Dump the run directory to the output directory
143 output Consult the output
144 summary Summary of errors and changes
145 digraph-host Produce digraph host
146 digraph-ssh Produce SSH IDs digraph
147 import Import TARGET host in current nodes directory
148 info Default command showing active nodes and summary
149 init Initialize a new work directory
152 prefix [prefix...] # Define the prefix to be executed in order
153 group groupname ['.*'|hostname...] # Deploy specified group to hosts
158 # shellcheck disable=SC2086
160 test -n "$ZSH_VERSION" && {
161 echo >&2 "ZSH is unsupported. Exiting."
164 while getopts 'P:F:r:o:i:w:c:npdseaOh' arg; do
172 _debug "R7 DEBUG $VERSION"
174 i) SSH_IDENTITY_FILE=$OPTARG ;;
175 F) SSH_CONFIG_FILE=$OPTARG ;;
176 w) R7_WORKDIR=$OPTARG ;;
177 r) R7_RUNDIR=$OPTARG ;;
178 o) R7_OUTPUTDIR=$OPTARG ;;
179 s) R7_GROUP_SUMMARY=yes ;;
180 a) R7_GROUP_ALLPREFIX=yes ;;
181 e) R7_GROUP_EXITONERROR=yes ;;
182 c) R7_SITE_CONFIG=$OPTARG ;;
187 p) R7_PARALLEL=yes ;;
192 O) R7_SHOWOUTPUT=yes ;;
195 echo 1>&2 "Unknown option '$arg'"
200 shift $((OPTIND - 1))
213 rc | run-config) run_config ;;
216 trace_id=$(_trace_id_new)
217 if test "$R7_GROUP_ALLPREFIX" = yes; then
218 prefix $(group_prefixes "$1")
223 if test "$R7_GROUP_SUMMARY" = yes; then
237 outdump | output-dump) output_dump ;;
252 echo 1>&2 "Unknown command '$1'"
256 *) prefix $R7_PREFIX ;;
263 if test -d "$1"; then
270 if test -f "$1"; then
271 cd "${1%/*}" || return 2
272 printf %s "$PWD/${1##*/}"
276 # shellcheck disable=SC2030
278 _debug _WORKDIR_INIT "$PWD" "$R7_WORKDIR"
279 if test -n "$R7_WORKDIR"; then
280 mkdir -p "$R7_WORKDIR" || _fail "Unable to create directory: '$R7_WORKDIR'"
281 cd "$R7_WORKDIR" || _fail "Unable to cd to directory: '$R7_WORKDIR'"
287 mkdir -p .ssh/control
288 : "${SSH_CONFIG_FILE:=$R7_WORKDIR/.ssh/config}"
289 : "${SSH_IDENTITY_FILE:=$R7_WORKDIR/.ssh/id_ed25519_r7}"
290 R7_SITE_CONFIG=./site-config
291 if ! test -f "$R7_SITE_CONFIG"; then
292 cat >"$R7_SITE_CONFIG" <<-eof
293 # Example r7 site configuration
297 if ! test -f "$SSH_CONFIG_FILE"; then
298 cat >"$SSH_CONFIG_FILE" <<eof
299 # Example SSH client configuration
306 #ControlPath $R7_WORKDIR/.ssh/control/%C
307 ControlPath $R7_WORKDIR/.ssh/control/%r@%h:%p
309 IdentityFile $SSH_IDENTITY_FILE
310 StrictHostKeyChecking yes
312 UserKnownHostsFile $R7_WORKDIR/.ssh/known_hosts
324 touch "${SSH_IDENTITY_FILE}" "${SSH_IDENTITY_FILE}.pub"
325 touch .ssh/known_hosts
327 find "$PWD" -exec printf -- "\t- %s\n" {} +
335 /usr/bin/ssh "-oConnectTimeout=$SSH_CONNECT_TIMEOUT" -F "$SSH_CONFIG_FILE" "$@"
338 alias ssh-debug=ssh_debug
344 alias ssh-known-hosts=ssh_known_hosts
346 /usr/bin/ssh-keygen -lv -f "${SSH_KNOWN_HOSTS:-$R7_WORKDIR/.ssh/known_hosts}"
349 alias ssh-authorized-keys-reset=ssh_authorized_keys_reset
350 alias ssh-authreset=ssh_authorized_keys_reset
351 ssh_authorized_keys_reset() {
352 ssh -o BatchMode=no "$@" "
355 touch ~/.ssh/authorized_keys
356 chmod 600 ~/.ssh/authorized_keys
357 echo '$(cat "$SSH_IDENTITY_FILE.pub")' >~/.ssh/authorized_keys
361 alias ssh-accept-new=ssh_accept_new
363 ssh -o StrictHostKeyChecking=accept-new "$@" :
366 alias ssh-control-master-clean=ssh_control_master_clean
367 ssh_control_master_clean() {
368 find "$SSH_CONTROL_DIR" -type f -exec rm {} \;
371 alias ssh-users=ssh_users
373 cd "$R7_WORKDIR/nodes" || _fail "Unable to cd to '$R7_WORKDIR/nodes'."
374 find ./* -type f -name "authorized_keys_*" |
376 sort | while read -r keyfile; do
378 grep -v "^#" "$R7_WORKDIR/nodes/$keyfile"
382 alias ssh-ids=ssh_ids
384 ssh_users | grep -v ^- | sort -u
387 alias ssh-authorized-keys=ssh_authorized_keys
388 alias ssh-auth=ssh_authorized_keys
389 ssh_authorized_keys() {
390 ssh_users | grep -e ^- | sed -e 's/^- /nodes\//' -e 's/:$//'
393 alias ssh-ids-hosts=ssh_ids_hosts
394 # shellcheck disable=SC2046
396 ssh_ids | awk '{ print $3 }' |
398 grep -e "$id" $(ssh_authorized_keys)
399 done | while IFS="$IFS/:" read -r _ hostname _ keyfile _ _ comment; do
400 printf '"%s" -> "%s" -> "%s"\n' "${comment#* }" "${keyfile#authorized_keys_}" "$hostname"
407 ssh-add "$SSH_IDENTITY_FILE"
411 test "$R7_DRYRUN" = yes && return 0
412 _debug "COPY $1 $2 BEGIN"
415 if r=$(ssh "$node" command -v openrsync); then
416 if openrsync -V 2>/dev/null; then
417 _debug "COPY $1 $2 USE OPENRSYNC"
418 openrsync -e "/usr/bin/ssh -oConnectTimeout=$SSH_CONNECT_TIMEOUT -F $SSH_CONFIG_FILE" \
421 -a "$1" "$2" && return 0
424 if r=$(ssh "$node" command -v rsync); then
425 if rsync -V 1>/dev/null; then
426 _debug "COPY $1 $2 USE RSYNC"
427 rsync -e "/usr/bin/ssh -oConnectTimeout=$SSH_CONNECT_TIMEOUT -F $SSH_CONFIG_FILE" \
430 -aq "$1" "$2" && return 0
433 _debug "COPY $1 $2 USE SCP"
435 ssh "$node" rm -fr "$dir/$1" 2>/dev/null
436 /usr/bin/scp "-oConnectTimeout=$SSH_CONNECT_TIMEOUT" -qF "$SSH_CONFIG_FILE" -r "$1" "$2"
439 # Deployment functions
440 # ====================
442 _trace_id_new() { date +%s; }
445 cd "$R7_WORKDIR" || return 2
446 test -d ./nodes || return 0
447 find nodes -type d -maxdepth 1 | tail -n +2 | awk -F/ '{ print $2 }'
450 alias nodes-active=nodes_active
451 alias nodesact=nodes_active
453 test "$R7_DRYRUN" = yes && {
457 if test -s "$R7_WORKDIR/.active_nodes"; then
458 cat "$R7_WORKDIR/.active_nodes"
460 touch "$R7_WORKDIR/.active_nodes"
461 for node in $(nodes); do
462 _debug "ACTIVE $node"
463 if ssh "$node" :; then
464 printf "%s\\n" "$node" >>"$R7_WORKDIR/.active_nodes"
467 cat "$R7_WORKDIR/.active_nodes"
471 alias run-config=run_config
473 # shellcheck disable=SC1090,SC2031
475 trace_id=$(_trace_id_new)
476 # shellcheck disable=SC1091
477 _debug LOADING "$R7_SITE_CONFIG"
478 sh -n "$R7_SITE_CONFIG" && . "$R7_SITE_CONFIG"
481 # Inherits $target from group()
482 # shellcheck disable=SC2031
484 _debug "DEPLOY $new_name $target"
485 cd "$R7_WORKDIR" || exit 2
486 stem="groups/$new_name"
487 nodedir="nodes/$target"
488 test "$R7_DRYRUN" = yes || {
489 if test -d "$stem.dir"; then
490 ssh "$target" 'test -d /tmp/r7 || { umask 027; mkdir -p /tmp/r7; }'
491 copy "$stem.dir" "$target:/tmp/r7" || return 3
493 if test -d "$nodedir"; then
494 test -f /tmp/r7/"$trace_id.$target" || {
495 ssh "$target" 'test -d /tmp/r7 || { umask 027; mkdir -p /tmp/r7; }'
496 copy "$nodedir" "$target:/tmp/r7" || return 3
500 if test -f "$stem"; then
501 if _payload "$stem" | sh -n; then
502 _debug SYNTAX OK "$stem"
503 test "$R7_DRYRUN" = yes || {
504 _payload "$stem" | ssh 2>&1 "$target" /bin/sh - |
505 _collect "$R7_RUNDIR/$trace_id.$target.$new_name"
508 _debug SYNTAX KO "$stem"
509 echo 1>&2 "syntax error in '$stem', skipping..."
516 set -f -- $(printf -- "-e %s " "$@")
517 nodes_active | grep "$@"
520 # shellcheck disable=SC2030
522 test $# -eq 0 && return 0
523 cd "$R7_WORKDIR" || return 2
525 test "$R7_DRYRUN" = yes || {
526 mkdir -p "$R7_RUNDIR" || return 2
527 cd "$R7_RUNDIR" || return 2
529 _debug "CREATED $R7_RUNDIR"
531 _debug "PROCESSING GROUP $new_name"
533 # shellcheck disable=SC2046
535 for target in $(members "$@"); do
549 grep 2>/dev/null -e "^.*()" "$R7_WORKDIR/groups/$1" | cut -d\( -f1 | cut -d_ -f1 | uniq
552 alias group-functions=group_functions
553 alias groupfuncs=group_functions
555 if test "$R7_GROUP_ALLPREFIX" = yes; then
556 grep 2>/dev/null -e "^.*()" "$R7_WORKDIR/groups/$1" | cut -d\( -f1
559 for pre in $prefix; do
560 grep 2>/dev/null -e "^${pre}_.*()" "$R7_WORKDIR/groups/$1" | cut -d\( -f1
567 export prefix R7_PREFIX
571 cd "$R7_WORKDIR" || return 2
572 grep -e "^$2_.*()" "$1" |
573 cut -d\( -f1 | while read -r function; do
574 _debug "CALL $1 $function"
575 echo trace "${1#*/}" "$function" START
576 echo "_ret=; $function || { _ret=\$?; : ;}"
577 echo trace "${1#*/}" "$function" END "\${_ret:-0}"
578 test "$R7_DRYRUN" = yes && {
579 echo 1>&2 "- (dryrun) $target:$new_name::$function"
585 echo "trace_id=$trace_id"
586 echo "groupname=$new_name"
587 echo "exitonerror=$R7_GROUP_EXITONERROR"
590 echo ":: $trace_id $(nodename) $*" 1>&2
594 trace ERROR "$*" 1>&2
595 test "$exitonerror" = yes && {
596 case $_ret in (0) _ret=1 ;; esac
602 hostname=${hostname:-$(uname -n)}
603 echo "${hostname%%.*}"
606 atom=/tmp/r7/$(nodename)/$1
607 test -f "$atom" && return 0
608 test -d "$atom" && return 0
612 echo "/tmp/r7/$(nodename)"
615 echo "/tmp/r7/$groupname.dir"
619 error missing source argument
622 source "$(test -f "$1" && printf %s "$PWD/${1##"$PWD"}")"
625 node "$1" && cd "$(nodedir)" && _presource "$1"
628 cd "$(groupdir)" && _presource "$1"
635 trace SOURCE FAILED "$1"
639 install() ( cd /tmp/r7 && r7_install "$@" ; )
640 groupinstall() ( cd "$(groupdir)" && r7_install "$@" ; )
641 nodeinstall() ( cd "$(nodedir)" && r7_install "$@" ; )
644 ls -l "$1" | grep -v total | awk '{ print $1 " " $3 " " $4 }'
648 before=$(_getperm "$1")
649 if test -n "$owner"; then
650 chown "$owner" "$1" || {
655 if test -n "$mode"; then
656 chmod "$mode" "$1" || {
661 after=$(_getperm "$1")
662 if test "$before" != "$after"; then
663 trace PERMISSIONS CHANGE "$before" TO "$after" ON "$1"
672 { error copying "$source" to "$2"; return 2 ; }
677 while getopts "o:m:" arg; do
684 shift $((OPTIND - 1))
687 source_dirname=${1%/*}
688 if ! test -e "$source"; then
689 error "$source" not found
693 */) test -d "$target" || {
694 error "$target" directory not found
698 if test -e "$target"; then
699 cmp -s "$source" "$target"
703 if test -d "$target"; then
704 new_filename="$target/${source##$source_dirname/}"
705 cmp -s "$source" "$new_filename"
707 destination_is=directory
709 if test -e "$source"; then
710 if test -d "$target"; then
711 destination_is=directory
719 case $destination_is in
721 new_filename="$target/${source##$source_dirname/}"
722 _install "$source" "$new_filename"
725 _install "$source" "$target"
731 installdir() { r7_installdir "$@" ; }
732 groupinstalldir() ( cd "$(groupdir)" && r7_installdir "$@" ; )
733 nodeinstalldir() ( cd "$(nodedir)" && r7_installdir "$@" ; )
735 while getopts "o:m:O:M:" arg; do
737 o) fileowner=$OPTARG ;;
738 m) filenode=$OPTARG ;;
739 O) dirowner=$OPTARG ;;
740 M) dirmode=$OPTARG ;;
744 shift $((OPTIND - 1))
747 source_basename=${source##*/}
748 source_dirname=$PWD/${source%%/*}
749 target_basename=${target##*/}
750 test -d "$source" || {
751 error "$source" directory not found
754 for dir in $(find "$source" -type d); do
757 cd "$target" || return 2
758 mkdir -p "$newdir" || { error unable to create "$newdir"; return 2 ; }
759 owner=$dirowner mode=$dirmode _setperm "$1"
762 for file in $(find "$source" -type f); do
764 r7_install -o "$fileowner" -m "$filemode" "$file" "$target/$newfile"
768 cd /tmp/r7 && umask 027
773 trace CONNECTED $(date +%s)
776 _debug "PREFIX $R7_PREFIX"
777 for run in $R7_PREFIX; do
781 trace DISCONNECT $(date +%s)
785 # Interactive output function
786 # ===========================
788 # shellcheck disable=SC2086,SC2030
790 tee -a "$1" | while read -r line; do
794 # Traces from payload
804 save_group=$group save_function=$function
806 if test "$R7_SHOWOUTPUT" = yes; then
807 echo "- (output) $host:$group::$function"
814 0 | 30 | 31) echo "- (ok) $host:$group::$function" ;;
815 *) echo "! (error $endcode) $host:$group::$function" ;;
819 # Traces from functions
823 SOURCE) echo "+ (source) $host:$save_group::$save_function" ;;
824 DIFF) echo "+ (diff) $host:$save_group::$save_function" ;;
825 PERMISSIONS) echo "+ (permissions) $host:$save_group::$save_function" ;;
826 ERROR) echo "! (error) $host:$save_group::$save_function" ;;
827 EXIT) echo "! (exit) $host:$save_group::$save_function" ;;
834 if test "$R7_SHOWOUTPUT" = yes; then
835 test "$in_function" -eq 1 &&
836 printf "%s\\n" "$line"
841 # Text output functions
842 # =====================
844 alias output-dump=output_dump
845 alias outdump=output_dump
847 set -e -- $(output_latest)
848 test $# -eq 0 && return 0
849 cd "$R7_WORKDIR" || _fail "Unable to cd to workdir: $R7_WORKDIR"
850 test "$R7_DRYRUN" = yes || {
851 mkdir -p "$R7_OUTPUTDIR"
852 awk -voutput_dir="$R7_OUTPUTDIR" '
861 if ($4 == "CONNECTED") {
865 system("mkdir -p " output_dir "/" hostname)
868 $2 == trace_id && $6 == "START" {
872 output_node = output_dir "/" hostname "/" groupname "/" funcname
873 output_status = output_node "/status"
874 output_output = output_node "/output"
875 system("rm -fr " output_node)
876 system("mkdir -p " output_node)
879 $2 == trace_id && $4 == "EXIT" {
881 system("echo " $5 " >" output_status)
883 $2 == trace_id && $6 == "END" {
885 system("echo " $7 " >" output_status)
887 in_function_block == 1 {
888 print > output_output
890 $2 == trace_id && $4 == "DISCONNECT" {
891 timestamp_disconnect=$5
892 difference=timestamp_disconnect - timestamp_connect
893 output_duration = output_dir "/" hostname "/" groupname "/duration"
894 system("echo " difference " >" output_duration)
902 OpenBSD | FreeBSD | NetBSD | Darwin)
909 perl -e "print scalar localtime($1)"
915 alias output-latest=output_latest
916 alias outlatest=output_latest
920 # shellcheck disable=SC2012
921 ls 2>/dev/null -lt1 "$R7_RUNDIR/" | while read -r file; do
926 test -z "$latest" && return 0
927 latest_date=$(_epoch "$latest")
928 if test -n "$latest_date"; then
929 find "$R7_RUNDIR" -type f -name "$latest.*"
934 cd "$R7_OUTPUTDIR" 2>/dev/null || return 2
935 find ./* -type f -name 'output'
939 cd "$R7_OUTPUTDIR" 2>/dev/null || return 2
940 find ./* -type f -name 'status'
944 cd "$R7_OUTPUTDIR" 2>/dev/null || return 2
945 find ./* -type f -name 'duration'
949 column -t 2>/dev/null ||
953 alias output-status=output_status
954 alias outstat=output_status
956 cd "$R7_OUTPUTDIR" || return 2
957 echo HOST GROUP FUNCTION STATUS
959 while read -r status; do
960 echo "$status" | while IFS=/ read -r _ host groupname funcname _; do
961 echo "$host" "$groupname" "$funcname" "$(cat "$status")"
967 alias output-duration=output_duration
968 alias outdur=output_duration
970 cd "$R7_OUTPUTDIR" || return 2
971 echo HOST GROUP DURATION
973 while read -r duration; do
974 echo "$duration" | while IFS=/ read -r _ host groupname _; do
975 echo "$host" "$groupname" "$(cat "$duration")"
982 # shellcheck disable=SC2254
984 cd "$R7_OUTPUTDIR" || return 2
985 _output_files | while read -r output; do
986 echo "$output" | while IFS=/ read -r _ host groupname funcname _; do
987 case $host in ${1:-*})
988 case $groupname in ${2:-*})
989 case $funcname in ${3:-*})
990 echo "- $host" "$groupname" "$funcname"
992 sed 's/^/ /' "$R7_OUTPUTDIR/$output"
1004 alias trace-id-last=trace_id_last
1005 alias lastid=trace_id_last
1007 output_latest | while read -r output; do
1008 output=${output##*/}
1009 output=${output%%.*}
1015 # _output/tools0/r7test/r7test_diff/output::: 1748408055 tools0 DIFF /tmp/difftest r7test.dir/difftest
1016 alias output-diffs=output_diffs
1017 alias outdiffs=output_diffs
1018 # shellcheck disable=SC2048,SC2086
1020 trace_id=$(trace_id_last)
1021 cd "$R7_OUTPUTDIR" || return 0
1022 grep -r "^:: ${trace_id:-} .* DIFF" | uniq | while read -r line; do
1024 output_file=${1%%:::}
1028 host=$1 groupname=$2 funcname=$3
1030 echo "- (diff) -" "in" "$host" "$groupname" "$funcname" "($trace_id)"
1032 sed 's/^/ /' "$R7_OUTPUTDIR/$host/$groupname/$funcname/output" | grep -v "^ :: $trace_id $host"
1038 # :: 1748415578 tools0 PERMISSIONS CHANGE -rw-r-xr-x root wheel -> -rw-r--r-- root wheel on /tmp/difftest
1039 alias output-permissions=output_permissions
1040 alias outperm=output_permissions
1041 # shellcheck disable=SC2048,SC2086
1042 output_permissions() (
1043 trace_id=$(trace_id_last)
1044 cd "$R7_OUTPUTDIR" || return 0
1045 grep -r "^:: ${trace_id:-} .* PERMISSIONS" | while read -r line; do
1047 output_file=${1%%:::}
1051 host=$1 groupname=$2 funcname=$3
1053 echo "- (permissions) -" "in" "$host" "$groupname" "$funcname" "($trace_id)"
1055 grep -e "^:: ${trace_id:-} .* PERMISSIONS CHANGE" "$R7_OUTPUTDIR/$host/$groupname/$funcname/output" |
1056 while read -r _ _ _ _ _ mode owner group _ mode2 owner2 group2 _ filename; do
1057 echo " - $filename from $mode $owner:$group to $mode2 $owner2:$group2"
1064 alias output-errors=output_errors
1065 alias outerr=output_errors
1067 trace_id=$(trace_id_last)
1068 output_status | while read -r host groupname funcname status; do
1070 STATUS | 0 | 30 | 31) continue ;;
1072 echo "- (error $status) -" "in" "$host" "$groupname" "$funcname" "($trace_id)"
1073 if test -f "$R7_OUTPUTDIR/$host/$groupname/$funcname/output"; then
1075 sed 's/^/ /' "$R7_OUTPUTDIR/$host/$groupname/$funcname/output"
1083 test -d "$R7_OUTPUTDIR" || return 0
1089 test -n "$summary_out" &&
1090 printf "%s\n" "$summary_out"
1097 # Used in the digraph generation because inside a heredoc
1098 # context, memebers don't work as expected (OKSH).
1099 # shellcheck disable=SC2048,SC2086
1103 nodes | grep -e '.*'
1107 set -f -- $(printf -- "-e %s " "$@")
1111 alias digraph-host=digraph_host
1112 # shellcheck disable=SC2086
1114 _debug "DIGRAPH HOST"
1119 graph [overlap="false", rankdir=LR, mode=KK]
1122 for node in $(nodes); do echo "\"$node\" [peripheries=2 color=blue]"; done
1123 grep 2>/dev/null "^group" "$R7_SITE_CONFIG" | while read -r _ groupname nodes; do
1124 echo "\"$groupname\" [color=orange]"
1125 for node in $(_members2 $nodes); do
1126 echo "\"$node\" -> \"$groupname\""
1128 for funct in $(group_functions "$groupname"); do
1129 echo "\"$groupname::$funct\" [color=red]"
1130 echo "\"$groupname\" -> \"$groupname::$funct\""
1138 alias digraph-ssh-id-hosts=digraph_ssh_id_hosts
1139 digraph_ssh_id_hosts() {
1141 digraph ssh_id_hosts {
1144 graph [overlap="false"]
1155 pandoc -t html -f markdown+smart "$@"
1159 summary | while read -r line; do
1163 printf %s\\n "$line"
1169 cat "$R7_OUTPUTDIR/$6/$7/$8/output"
1180 <meta charset="UTF-8">
1181 <meta name="viewport" content="width=device-width, initial-scale=1.0">
1182 <title>inda.re report</title>
1183 <style type="text/css" media="screen">
1184 html { margin: 4%; }
1189 padding-right: 50px;
1191 padding-bottom: 50px;
1193 overflow-wrap: break-word;
1194 text-rendering: optimizeLegibility;
1195 font-kerning: normal;
1198 text-align: justify;
1199 text-justify: inter-word;
1201 @media screen and (min-width: 1000px) {
1207 @media (prefers-color-scheme: dark) {
1212 --buildhead: #474749;
1221 background-color: #1E1F21;
1231 <li><a href="#Documentation">Documentation</a></li>
1232 <li><a href="#Summary">Summary</a></li>
1236 <section id="Documentation">
1239 <section id="Summary">
1257 cd "$R7_WORKDIR" || _fail "Unable to cd to '$R7_WORKDIR'"
1259 etc_files_to_import='
1279 etc_mail_files_to_import='
1284 etc_ssh_files_to_import='
1295 var_postgresql_files='
1297 data/postgresql.conf
1299 if test -n "$target"; then
1300 echo >&2 "-> Importing target host: $target"
1301 case $(ssh "$target" uname) in OpenBSD) : ;; *)
1302 _error "Platform unsupported or host unavailable."
1306 _OpenBSD_import_ssh_authorized_keys
1307 _OpenBSD_import_etc_if_exists
1308 _OpenBSD_import_etc_ssh_if_exists
1309 _OpenBSD_import_etc_mail_if_exists
1310 _OpenBSD_import_unbound_if_enabled
1311 _OpenBSD_import_nsd_if_enabled
1312 _OpenBSD_import_postgresql_if_installed
1313 echo >&2 "-> done: $target"
1315 echo >&2 "usage: $0 target-host"
1319 _OpenBSD_import_ssh_authorized_keys() {
1321 ssh "$target" getent passwd | awk -F: '$3 >= 1000 { print $1 }' |
1325 for user in root $other_users; do
1326 # shellcheck disable=SC2029
1327 home=$(ssh "$target" getent passwd "$user" | awk -F: '{ print $6 }')
1328 authorized_keys="$home/.ssh/authorized_keys"
1329 ssh "$target" test -f "$authorized_keys" || continue
1330 mkdir -p "nodes/$target/ssh/"
1331 echo >&2 "--> Importing $authorized_keys"
1332 #_import_copy "root@$target:$authorized_keys" "nodes/$target/ssh/authorized_keys_$user"
1333 scp "root@$target:$authorized_keys" "nodes/$target/ssh/authorized_keys_$user"
1337 _OpenBSD_import_etc_if_exists() {
1338 mkdir -p nodes/"$target"
1339 interfaces=$(ssh "$target" 'cd /etc && ls -1 hostname.*')
1340 echo >&2 "--> Importing /etc"
1341 for file in $etc_files_to_import $interfaces; do
1342 ssh "$target" test -f "'/etc/$file'" &&
1343 _import_copy "$target:/etc/$file" "nodes/$target/"
1347 _OpenBSD_import_etc_ssh_if_exists() {
1348 mkdir -p nodes/"$target"
1349 echo >&2 "--> Importing /etc/ssh"
1350 for file in $etc_ssh_files_to_import; do
1351 ssh "$target" test -f "'/etc/$file'" &&
1352 _import_copy "$target:/etc/$file" "nodes/$target/ssh"
1356 _OpenBSD_import_etc_mail_if_exists() {
1357 mkdir -p nodes/"$target"
1358 echo >&2 "--> Importing /etc/ssh"
1359 for file in $etc_mail_files_to_import; do
1360 ssh "$target" test -f "'/etc/$file'" &&
1361 _import_copy "$target:/etc/$file" "nodes/$target/mail"
1365 _OpenBSD_import_unbound_if_enabled() {
1366 ssh "$target" rcctl check unbound >/dev/null && {
1367 echo >&2 "--> Importing /var/unbound"
1368 for file in $var_unbound_files; do
1370 mkdir -p "nodes/$target/unbound/$prefix"
1371 _import_copy "$target:/var/unbound/$file" "nodes/$target/unbound/$prefix/"
1377 _OpenBSD_import_nsd_if_enabled() {
1378 ssh "$target" rcctl check nsd >/dev/null && {
1379 echo >&2 "--> Import /var/nsd"
1380 for file in $var_nsd_files; do
1382 mkdir -p "nodes/$target/nsd/$prefix"
1383 _import_copy "$target:/var/nsd/$file" "nodes/$target/nsd/$prefix/"
1389 _OpenBSD_import_postgresql_if_installed() {
1390 ssh "$target" postgres -V 2>/dev/null | grep -q PostgreSQL && {
1391 echo >&2 "--> Importing /var/postgresql"
1392 for file in $var_postgresql_files; do
1394 mkdir -p "nodes/$target/postgresql/$prefix"
1395 _import_copy "$target:/var/postgresql/$file" "nodes/$target/postgresql/$prefix/"
1397 echo >&2 "-- Importing /etc/login.conf.d/postgresql"
1398 mkdir -p "nodes/$target/login.conf.d"
1399 _import_copy "$target:/etc/login.conf.d/postgresql" "nodes/$target/login.conf.d/"
1405 _debug "IMPORT $1 $2 USE OPENRSYNC"
1406 openrsync -e "/usr/bin/ssh -oConnectTimeout=$SSH_CONNECT_TIMEOUT -F $SSH_CONFIG_FILE" \
1407 --rsync-path="/usr/bin/openrsync" \
1408 -a "$1" "$2" && return 0