Blob


1 #!/bin/sh
2 # ISC License
3 #
4 # Copyright 2025 xs <xs@inda.re>
5 #
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.
9 #
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 # ====================
23 : "${ZSH_VERSION:=}"
24 R7_DEBUG=no
25 VERSION=0.2.0
26 _init_pwd="$PWD"
28 # shellcheck disable=SC2031
29 _init_env() {
30 _debug _INIT_ENV
31 cd "$_init_pwd" || _fail "Unable to cd to '$_init_pwd'"
33 # Here is the default configuration
34 : "${R7_DEBUG:=no}"
35 : "${R7_DRYRUN:=no}"
36 : "${R7_GROUP_ALLPREFIX:=no}"
37 : "${R7_GROUP_SUMMARY:=no}"
38 : "${R7_GROUP_EXITONERROR:=no}"
39 : "${R7_PARALLEL:=no}"
40 : "${R7_PREFIX:=install state doc}"
41 : "${R7_SHOWOUTPUT:=no}"
42 : "${R7_WORKDIR:=$_init_pwd}"
43 : "${R7_SITE_CONFIG:=$R7_WORKDIR/site-config}"
44 : "${R7_OUTPUTDIR:=$R7_WORKDIR/_output}"
45 : "${SSH_CONFIG_DIR:=$R7_WORKDIR/.ssh}"
46 : "${SSH_CONFIG_FILE:=$R7_WORKDIR/.ssh/config}"
47 : "${SSH_CONTROL_DIR:=$SSH_CONFIG_DIR/control}"
48 : "${SSH_IDENTITY_FILE:=$R7_WORKDIR/.ssh/id_ed25519_r7}"
49 : "${SSH_KNOWN_HOSTS:=$R7_WORKDIR/.ssh/known_hosts}"
50 : "${SSH_CONNECT_TIMEOUT:=3}"
52 _debug ENVIRONMENT
54 # Validate environment
55 if test "$R7_DEBUG" = yes; then
56 set | grep -e '^R7_' -e '^SSH' 1>&2
57 fi
59 for param in "$R7_DEBUG" "$R7_DRYRUN" "$R7_PARALLEL"; do
60 case $param in
61 yes | no) : ;;
62 *) _fail "Invalid parameter value: '$param'" ;;
63 esac
64 done
66 R7_WORKDIR_tmp=$(_resolv_dir "$R7_WORKDIR")
67 test -d "$R7_WORKDIR_tmp" || _error "Working directory not found: '$R7_WORKDIR'"
68 R7_WORKDIR=$R7_WORKDIR_tmp
69 R7_RUNDIR=$R7_WORKDIR/_run
70 R7_OUTPUTDIR=$R7_WORKDIR/_output
72 R7_SITE_CONFIG_tmp=$(_resolv_file "$R7_SITE_CONFIG")
73 test -f "$R7_SITE_CONFIG_tmp" || _error "Site configuration not found: '$R7_SITE_CONFIG'"
74 R7_SITE_CONFIG=$R7_SITE_CONFIG_tmp
76 SSH_CONFIG_FILE_tmp=$(_resolv_file "$SSH_CONFIG_FILE")
77 test -f "$SSH_CONFIG_FILE_tmp" || _error "Unable to read SSH configuration: '$SSH_CONFIG_FILE'"
78 SSH_CONFIG_FILE=$SSH_CONFIG_FILE_tmp
80 SSH_IDENTITY_FILE_tmp=$(_resolv_file "$SSH_IDENTITY_FILE")
81 test -f "$SSH_IDENTITY_FILE_tmp" || _error "Unable to read SSH identity file: '$SSH_IDENTITY_FILE'"
82 SSH_IDENTITY_FILE=$SSH_IDENTITY_FILE_tmp
84 R7_ENV_READY=yes
85 unset R7_WORKDIR_tmp R7_SITE_CONFIG_tmp SSH_CONFIG_FILE_tmp SSH_IDENTITY_FILE_tmp
86 }
88 # Interface
89 # =========
91 _fail() {
92 printf "FAILURE: %s\n" "$*" >&2
93 exit 99
94 }
96 _error() {
97 printf "ERROR: %s\n" "$*" >&2
98 }
100 _debug() {
101 test "$R7_DEBUG" = yes && printf %s\\n "${*:-$(cat -)}" >&2
104 info() {
105 if test "$R7_ENV_READY" = yes; then
106 cd "$R7_WORKDIR" || return 2
107 _trace_id_last=$(trace_id_last)
108 if test -n "$_trace_id_last"; then
109 printf -- "- (%s) - in %s the %s\n" \
110 "$_trace_id_last" "$(basename "$R7_WORKDIR")" "$(_epoch "$_trace_id_last")"
111 nodes_active | sed 's/^/ - (active) /'
112 echo
113 fi
114 output_dump
115 summary
116 fi
117 return 0
120 alias help=usage
121 usage() {
122 cat <<eof
123 version: $VERSION
124 usage: $0 [options] [command [arguments...]]
125 usage options:
126 -h Show usage and exit
127 -d Debug
128 -i SSH identity file
129 -F SSH client configuration file
130 -w Work directory where to find ./nodes and ./groups
131 -r Run directory, default to ./_run
132 -o Output directory, default to ./_output
133 -c Site configuration file, default to ./site-config
134 -n Dry run, prevent any host modification
135 -e Exit the group execution on the error function
136 -p Run each host in parallel per group call
137 -O Show output interactively
138 -P Prefix, can contain whole function names
139 -a Select all available prefixes for a group command
140 -s Output-dump and summary are applied after a group command
142 commands:
143 run-config Run the site configuration
144 group Deploy specified group to hosts
145 output-dump Dump the run directory to the output directory
146 output Consult the output
147 summary Summary of errors and changes
148 digraph-host Produce digraph host
149 digraph-ssh Produce SSH IDs digraph
150 import Import TARGET host in current nodes directory
151 info Default command showing active nodes and summary
152 init Initialize a new work directory
154 site-config format:
155 prefix [prefix...] # Define the prefix to be executed in order
156 group groupname ['.*'|hostname...] # Deploy specified group to hosts
158 eof
161 # shellcheck disable=SC2086
162 _main() {
163 test -n "$ZSH_VERSION" && {
164 echo >&2 "ZSH is unsupported. Exiting."
165 return 3
167 while getopts 'P:F:r:o:i:w:c:npdseaOh' arg; do
168 case $arg in
169 h)
170 usage
171 return 0
172 ;;
173 d)
174 R7_DEBUG=yes
175 _debug "R7 DEBUG $VERSION"
176 ;;
177 i) SSH_IDENTITY_FILE=$OPTARG ;;
178 F) SSH_CONFIG_FILE=$OPTARG ;;
179 w) R7_WORKDIR=$OPTARG ;;
180 r) R7_RUNDIR=$OPTARG ;;
181 o) R7_OUTPUTDIR=$OPTARG ;;
182 s) R7_GROUP_SUMMARY=yes ;;
183 a) R7_GROUP_ALLPREFIX=yes ;;
184 e) R7_GROUP_EXITONERROR=yes ;;
185 c) R7_SITE_CONFIG=$OPTARG ;;
186 n)
187 R7_DRYRUN=yes
188 _debug "MODE DRYRUN"
189 ;;
190 p) R7_PARALLEL=yes ;;
191 P)
192 R7_PREFIX=$OPTARG
193 prefix $R7_PREFIX
194 ;;
195 O) R7_SHOWOUTPUT=yes ;;
196 *)
197 usage
198 echo 1>&2 "Unknown option '$arg'"
199 exit 2
200 ;;
201 esac
202 done
203 shift $((OPTIND - 1))
205 case $1 in
206 init | workdir-init)
207 shift
208 _workdir_init "$@"
209 exit $?
210 ;;
211 esac
213 _init_env
215 case $1 in
216 rc | run-config) run_config ;;
217 group)
218 shift
219 trace_id=$(_trace_id_new)
220 if test "$R7_GROUP_ALLPREFIX" = yes; then
221 prefix $(group_prefixes "$1")
222 else
223 prefix $R7_PREFIX
224 fi
225 group "$@"
226 if test "$R7_GROUP_SUMMARY" = yes; then
227 output_dump
228 summary
229 fi
230 ;;
231 info) info ;;
232 import)
233 shift
234 import "$@"
235 ;;
236 out | output)
237 shift
238 output "$@"
239 ;;
240 outdump | output-dump) output_dump ;;
241 summary) summary ;;
242 digraph-host)
243 prefix $R7_PREFIX
244 digraph_host
245 ;;
246 digraph-ssh)
247 digraph_ssh_id_hosts
248 ;;
249 html) html "$@" ;;
250 '') cd "$R7_WORKDIR" && info ;;
251 *)
252 case $0 in
253 -) exit 33 ;;
254 *r7)
255 echo 1>&2 "Unknown command '$1'"
256 usage
257 exit 2
258 ;;
259 *) prefix $R7_PREFIX ;;
260 esac
261 ;;
262 esac
265 _resolv_dir() {
266 if test -d "$1"; then
267 cd "$1" || return 2
268 printf %s "$PWD"
269 fi
272 _resolv_file() {
273 if test -f "$1"; then
274 cd "${1%/*}" || return 2
275 printf %s "$PWD/${1##*/}"
276 fi
279 # shellcheck disable=SC2030
280 _workdir_init() (
281 _debug _WORKDIR_INIT "$PWD" "$R7_WORKDIR"
282 if test -n "$R7_WORKDIR"; then
283 mkdir -p "$R7_WORKDIR" || _fail "Unable to create directory: '$R7_WORKDIR'"
284 cd "$R7_WORKDIR" || _fail "Unable to cd to directory: '$R7_WORKDIR'"
285 R7_WORKDIR=$PWD
286 fi
287 R7_WORKDIR=$PWD
288 mkdir -p groups
289 mkdir -p nodes
290 mkdir -p .ssh/control
291 : "${SSH_CONFIG_FILE:=$R7_WORKDIR/.ssh/config}"
292 : "${SSH_IDENTITY_FILE:=$R7_WORKDIR/.ssh/id_ed25519_r7}"
293 R7_SITE_CONFIG=./site-config
294 if ! test -f "$R7_SITE_CONFIG"; then
295 cat >"$R7_SITE_CONFIG" <<-eof
296 # Example r7 site configuration
297 group common '.*'
298 eof
299 fi
300 if ! test -f "$SSH_CONFIG_FILE"; then
301 cat >"$SSH_CONFIG_FILE" <<eof
302 # Example SSH client configuration
303 Host *
304 AddKeysToAgent yes
305 BatchMode yes
306 ConnectTimeout 3
307 ConnectionAttempts 1
308 ControlMaster auto
309 #ControlPath $R7_WORKDIR/.ssh/control/%C
310 ControlPath $R7_WORKDIR/.ssh/control/%r@%h:%p
311 ControlPersist 2m
312 IdentityFile $SSH_IDENTITY_FILE
313 StrictHostKeyChecking yes
314 User root
315 UserKnownHostsFile $R7_WORKDIR/.ssh/known_hosts
316 eof
317 fi
318 touch "${SSH_IDENTITY_FILE}" "${SSH_IDENTITY_FILE}.pub"
319 touch .ssh/known_hosts
320 echo Created:
321 find "$PWD" -exec printf -- "\t- %s\n" {} +
324 # SSH functions
325 # =============
327 ssh() (
328 _debug "SSH $*"
329 /usr/bin/ssh "-oConnectTimeout=$SSH_CONNECT_TIMEOUT" -F "$SSH_CONFIG_FILE" "$@"
332 alias ssh-debug=ssh_debug
333 ssh_debug() {
334 _debug "SSH $*"
335 ssh -vvv "$@"
338 alias ssh-known-hosts=ssh_known_hosts
339 ssh_known_hosts() {
340 /usr/bin/ssh-keygen -lv -f "${SSH_KNOWN_HOSTS:-$R7_WORKDIR/.ssh/known_hosts}"
343 alias ssh-authorized-keys-reset=ssh_authorized_keys_reset
344 alias ssh-authreset=ssh_authorized_keys_reset
345 ssh_authorized_keys_reset() {
346 ssh -o BatchMode=no "$@" "
347 mkdir -p ~/.ssh
348 chmod 700 ~/.ssh
349 touch ~/.ssh/authorized_keys
350 chmod 600 ~/.ssh/authorized_keys
351 echo '$(cat "$SSH_IDENTITY_FILE.pub")' >~/.ssh/authorized_keys
355 alias ssh-accept-new=ssh_accept_new
356 ssh_accept_new() {
357 ssh -F "$SSH_CONFIG_FILE" -o StrictHostKeyChecking=accept-new "$@" :
360 alias ssh-control-master-clean=ssh_control_master_clean
361 ssh_control_master_clean() {
362 find "$SSH_CONTROL_DIR" -type f -exec rm {} \;
365 alias ssh-users=ssh_users
366 ssh_users() {
367 cd "$R7_WORKDIR/nodes" || _fail "Unable to cd to '$R7_WORKDIR/nodes'."
368 find ./* -type f -name "authorized_keys_*" |
369 sed 's;^./;;' |
370 sort | while read -r keyfile; do
371 echo "- $keyfile:"
372 grep -v "^#" "$R7_WORKDIR/nodes/$keyfile"
373 done
376 alias ssh-ids=ssh_ids
377 ssh_ids() {
378 ssh_users | grep -v ^- | sort -u
381 alias ssh-authorized-keys=ssh_authorized_keys
382 alias ssh-auth=ssh_authorized_keys
383 ssh_authorized_keys() {
384 ssh_users | grep -e ^- | sed -e 's/^- /nodes\//' -e 's/:$//'
387 alias ssh-ids-hosts=ssh_ids_hosts
388 # shellcheck disable=SC2046
389 ssh_ids_hosts() {
390 ssh_ids | awk '{ print $3 }' |
391 while read -r id; do
392 grep -e "$id" $(ssh_authorized_keys)
393 done | while IFS="$IFS/:" read -r _ hostname _ keyfile _ _ comment; do
394 printf '"%s" -> "%s" -> "%s"\n' "${comment#* }" "${keyfile#authorized_keys_}" "$hostname"
395 done | sort
398 unlock() {
399 eval "$(ssh-agent)"
400 ssh-add "$SSH_IDENTITY_FILE"
403 copy() (
404 test "$R7_DRYRUN" = yes && return 0
405 _debug "COPY $1 $2 BEGIN"
406 node=${2%%:*}
407 dir=${2##*:}
408 if r=$(ssh "$node" command -v openrsync); then
409 if openrsync -V 2>/dev/null; then
410 _debug "COPY $1 $2 USE OPENRSYNC"
411 openrsync -e "/usr/bin/ssh -oConnectTimeout=$SSH_CONNECT_TIMEOUT -F $SSH_CONFIG_FILE" \
412 --rsync-path="$r" \
413 --delete \
414 -a "$1" "$2" && return 0
415 fi
416 fi
417 if r=$(ssh "$node" command -v rsync); then
418 if rsync -V 1>/dev/null; then
419 _debug "COPY $1 $2 USE RSYNC"
420 rsync -e "/usr/bin/ssh -oConnectTimeout=$SSH_CONNECT_TIMEOUT -F $SSH_CONFIG_FILE" \
421 --rsync-path="$r" \
422 --delete \
423 -aq "$1" "$2" && return 0
424 fi
425 fi
426 _debug "COPY $1 $2 USE SCP"
427 dir=${dir:-/tmp/r7}
428 ssh "$node" rm -fr "$dir/$1" 2>/dev/null
429 scp "-oConnectTimeout=$SSH_CONNECT_TIMEOUT" -qF "$SSH_CONFIG_FILE" -r "$1" "$2" && return 0
430 _fail "Unable to copy '$1' to '$2'."
433 # Deployment functions
434 # ====================
436 _trace_id_new() { date +%s; }
438 nodes() (
439 cd "$R7_WORKDIR" || return 2
440 test -d ./nodes || return 0
441 find nodes -type d -maxdepth 1 | tail -n +2 | awk -F/ '{ print $2 }'
444 alias nodes-active=nodes_active
445 alias nodesact=nodes_active
446 nodes_active() (
447 test "$R7_DRYRUN" = yes && {
448 nodes
449 return 0
451 if test -s "$R7_WORKDIR/.active_nodes"; then
452 cat "$R7_WORKDIR/.active_nodes"
453 else
454 touch "$R7_WORKDIR/.active_nodes"
455 for node in $(nodes); do
456 _debug "ACTIVE $node"
457 if ssh "$node" :; then
458 printf "%s\\n" "$node" >>"$R7_WORKDIR/.active_nodes"
459 fi
460 done
461 cat "$R7_WORKDIR/.active_nodes"
462 fi
465 alias run-config=run_config
466 alias rc=run_config
467 # shellcheck disable=SC1090,SC2031
468 run_config() {
469 trace_id=$(_trace_id_new)
470 # shellcheck disable=SC1091
471 _debug LOADING "$R7_SITE_CONFIG"
472 sh -n "$R7_SITE_CONFIG" && . "$R7_SITE_CONFIG"
475 # Inherits $target from group()
476 # shellcheck disable=SC2031
477 _deploy() (
478 _debug "DEPLOY $new_name $target"
479 cd "$R7_WORKDIR" || exit 2
480 stem="groups/$new_name"
481 nodedir="nodes/$target"
482 test "$R7_DRYRUN" = yes || {
483 if test -d "$stem.dir"; then
484 ssh "$target" /bin/sh -c 'umask 027; mkdir -p /tmp/r7'
485 copy "$stem.dir" "$target:/tmp/r7" || return 3
486 fi
487 if test -d "$nodedir"; then
488 test -f /tmp/r7/"$trace_id.$target" || {
489 ssh "$target" /bin/sh -c 'umask 027; mkdir -p /tmp/r7'
490 copy "$nodedir" "$target:/tmp/r7" || return 3
492 fi
494 if test -f "$stem"; then
495 if _payload "$stem" | sh -n; then
496 _debug SYNTAX OK "$stem"
497 test "$R7_DRYRUN" = yes || {
498 _payload "$stem" | ssh 2>&1 "$target" /bin/sh - |
499 _collect "$R7_RUNDIR/$trace_id.$target.$new_name"
501 else
502 _debug SYNTAX KO "$stem"
503 echo 1>&2 "syntax error in '$stem', skipping..."
504 return 3
505 fi
506 fi
509 members() (
510 set -f -- $(printf -- "-e %s " "$@")
511 nodes_active | grep "$@"
514 # shellcheck disable=SC2030
515 group() (
516 test $# -eq 0 && return 0
517 cd "$R7_WORKDIR" || return 2
518 umask 027
519 test "$R7_DRYRUN" = yes || {
520 mkdir -p "$R7_RUNDIR" || return 2
521 cd "$R7_RUNDIR" || return 2
523 _debug "CREATED $R7_RUNDIR"
524 new_name=$1
525 _debug "PROCESSING GROUP $new_name"
526 shift
527 # shellcheck disable=SC2046
528 set -fm
529 for target in $(members "$@"); do
530 case $R7_PARALLEL in
531 yes)
532 _deploy &
533 ;;
534 *)
535 _deploy
536 ;;
537 esac
538 done
539 wait
542 group_prefixes() (
543 grep 2>/dev/null -e "^.*()" "$R7_WORKDIR/groups/$1" | cut -d\( -f1 | cut -d_ -f1 | uniq
546 alias group-functions=group_functions
547 alias groupfuncs=group_functions
548 group_functions() (
549 if test "$R7_GROUP_ALLPREFIX" = yes; then
550 grep 2>/dev/null -e "^[^#].*()" "$R7_WORKDIR/groups/$1" | cut -d\( -f1
551 return 0
552 fi
553 for pre in $prefix; do
554 grep 2>/dev/null -e "^${pre}.*()" "$R7_WORKDIR/groups/$1" | cut -d\( -f1
555 done
558 alias group-functions-all=group_functions_all
559 alias groupfuncsall=group_functions_all
560 group_functions_all() (
561 set -- $(cd "$R7_WORKDIR/groups" && find ./* -type f -maxdepth 0)
562 for file; do
563 group_functions "${file##./}" | while read -r func; do
564 printf %s::%s\\n "${file##./}" "$func"
565 done
566 done
569 prefix() {
570 prefix="$*"
571 R7_PREFIX=$prefix
572 export prefix R7_PREFIX
575 _call() (
576 cd "$R7_WORKDIR" || return 2
577 grep -e "^$2.*()" "$1" |
578 cut -d\( -f1 | while read -r function; do
579 _debug "CALL $1 $function"
580 echo trace "${1#*/}" "$function" START
581 echo "_ret=; $function || { _ret=\$?; : ;}"
582 echo trace "${1#*/}" "$function" END "\${_ret:-0}"
583 test "$R7_DRYRUN" = yes && {
584 echo 1>&2 "- (dryrun) $target:$new_name::$function"
586 done
589 _payload() (
590 echo "hostname=$target"
591 echo "trace_id=$trace_id"
592 echo "groupname=$new_name"
593 echo "exitonerror=$R7_GROUP_EXITONERROR"
594 cat <<-'eof'
595 trace() {
596 echo ":: $trace_id $(nodename) $*" 1>&2
598 error() {
599 _ret=$?
600 trace ERROR "$*" 1>&2
601 test "$exitonerror" = yes && {
602 case $_ret in (0) _ret=1 ;; esac
603 trace EXIT $_ret
604 exit $_ret
607 nodename() (
608 hostname=${hostname:-$(uname -n)}
609 echo "${hostname%%.*}"
611 node() (
612 atom=/tmp/r7/$(nodename)/$1
613 test -f "$atom" && return 0
614 test -d "$atom" && return 0
615 return 31
617 nodedir() {
618 echo "/tmp/r7/$(nodename)"
620 groupdir() (
621 echo "/tmp/r7/$groupname.dir"
623 _source() {
624 if sh -n "$1"; then
625 trace SOURCE "$1"
626 . "$1"
627 else
628 trace SOURCE FAILED "$1"
629 return 2
630 fi
632 fsource() {
633 test $# -gt 0 || {
634 error missing source argument
635 return 2
637 _source "$(test -f "$1" && printf %s "$PWD/${1##"$PWD"}")"
639 nodesource() {
640 node "$1" && cd "$(nodedir)" && fsource "$1"
642 groupsource() {
643 cd "$(groupdir)" && _fsource "$1"
645 install() ( cd /tmp/r7 && r7_install "$@" ; )
646 groupinstall() ( cd "$(groupdir)" && r7_install "$@" ; )
647 nodeinstall() ( cd "$(nodedir)" && r7_install "$@" ; )
648 _getperm() {
649 test -f "$1" && {
650 ls -l "$1" | grep -v total | awk '{ print $1 " " $3 " " $4 }'
653 _setperm() {
654 before=$(_getperm "$1")
655 if test -n "$owner"; then
656 chown "$owner" "$1" || {
657 error chown "$1"
658 return 2
660 fi
661 if test -n "$mode"; then
662 chmod "$mode" "$1" || {
663 error chmod "$1"
664 return 2
666 fi
667 after=$(_getperm "$1")
668 if test "$before" != "$after"; then
669 trace PERMISSIONS CHANGE "$before" TO "$after" ON "$1"
670 fi
672 _install() {
673 test -e "$2" && {
674 trace DIFF
675 diff -U2 "$2" "$1"
677 cp "$1" "$2" ||
678 { error copying "$source" to "$2"; return 2 ; }
679 _setperm "$2"
680 return 0
682 r7_install() (
683 while getopts "o:m:" arg; do
684 case $arg in
685 o) owner=$OPTARG ;;
686 m) mode=$OPTARG ;;
687 *) : ;;
688 esac
689 done
690 shift $((OPTIND - 1))
691 source=$1
692 target=$2
693 source_dirname=${1%/*}
694 if ! test -e "$source"; then
695 error "$source" not found
696 return 2
697 fi
698 case $target in
699 */) test -d "$target" || {
700 error "$target" directory not found
701 return 2
702 } ;;
703 esac
704 if test -e "$target"; then
705 cmp -s "$source" "$target"
706 ret=$?
707 destination_is=file
708 fi
709 if test -d "$target"; then
710 new_filename="$target/${source##$source_dirname/}"
711 cmp -s "$source" "$new_filename"
712 ret=$?
713 destination_is=directory
714 fi
715 if test -e "$source"; then
716 if test -d "$target"; then
717 destination_is=directory
718 else
719 destination_is=file
720 fi
721 fi
722 case "$ret" in
723 0) return 30 ;;
724 *)
725 case $destination_is in
726 directory)
727 new_filename="$target/${source##$source_dirname/}"
728 _install "$source" "$new_filename"
729 ;;
730 file)
731 _install "$source" "$target"
732 ;;
733 esac
734 ;;
735 esac
737 installdir() { r7_installdir "$@" ; }
738 groupinstalldir() ( cd "$(groupdir)" && r7_installdir "$@" ; )
739 nodeinstalldir() ( cd "$(nodedir)" && r7_installdir "$@" ; )
740 r7_installdir() (
741 while getopts "o:m:O:M:" arg; do
742 case $arg in
743 o) fileowner=$OPTARG ;;
744 m) filenode=$OPTARG ;;
745 O) dirowner=$OPTARG ;;
746 M) dirmode=$OPTARG ;;
747 *) : ;;
748 esac
749 done
750 shift $((OPTIND - 1))
751 source=$1
752 target=$2
753 source_basename=${source##*/}
754 source_dirname=$PWD/${source%%/*}
755 target_basename=${target##*/}
756 test -d "$source" || {
757 error "$source" directory not found
758 return 2
760 for dir in $(find "$source" -type d); do
761 newdir=${dir#./}
763 cd "$target" || return 2
764 mkdir -p "$newdir" || { error unable to create "$newdir"; return 2 ; }
765 owner=$dirowner mode=$dirmode _setperm "$1"
767 done
768 for file in $(find "$source" -type f); do
769 newfile=${file#./}
770 r7_install -o "$fileowner" -m "$filemode" "$file" "$target/$newfile"
771 done
773 template() (
774 while getopts "s:" arg; do
775 case $arg in
776 s) source=$OPTARG ;;
777 *) : ;;
778 esac
779 done
780 shift $((OPTIND - 1))
781 set -a
782 test -n "$source" && sh -n "$source" && set -a && . "./$source" && set +a
783 for file; do
784 sh > "${file%%.tpl}" <<-.
785 cat <<-..
786 $(cat "$file")
787 ..
789 done
791 grouptemplate() ( cd "$(groupdir)" && template "$@" ; )
792 nodetemplate() ( cd "$(nodedir)" && template "$@" ; )
793 set -e
794 cd /tmp/r7 && umask 027
795 set +e
796 nodename=$(nodename)
797 nodedir=$(nodedir)
798 groupdir=$(groupdir)
799 trace CONNECTED $(date +%s)
800 exec 1>&2
801 eof
802 cat "$1"
803 _debug "PREFIX $R7_PREFIX"
804 for run in $R7_PREFIX; do
805 _call "$1" "$run"
806 done
807 cat <<-'eof'
808 trace DISCONNECT $(date +%s)
809 eof
812 # Interactive output function
813 # ===========================
815 # shellcheck disable=SC2086,SC2030
816 _collect() (
817 set +u
818 tee -a "$1" | while read -r line; do
819 set -- $line
820 case $line in
821 ::*)
822 # Traces from payload
823 trace_id=$2
824 host=$3
825 group=$4
826 function=$5
827 status=$6
828 endcode=$7
829 # Traces from _call
830 case $status in
831 START)
832 save_group=$group save_function=$function
833 in_function=1
834 if test "$R7_SHOWOUTPUT" = yes; then
835 echo "- (output) $host:$group::$function"
836 fi
837 continue
838 ;;
839 END)
840 in_function=0
841 case $endcode in
842 0 | 30 | 31) echo "- (ok) $host:$group::$function" ;;
843 *) echo "! (error $endcode) $host:$group::$function" ;;
844 esac
845 ;;
846 esac
847 # Traces from functions
848 case $2 in
849 "$trace_id")
850 case $4 in
851 SOURCE) echo "+ (source) $host:$save_group::$save_function" ;;
852 DIFF) echo "+ (diff) $host:$save_group::$save_function" ;;
853 PERMISSIONS) echo "+ (permissions) $host:$save_group::$save_function" ;;
854 ERROR) echo "! (error) $host:$save_group::$save_function" ;;
855 EXIT) echo "! (exit) $host:$save_group::$save_function" ;;
856 esac
857 ;;
858 esac
859 ;;
860 esac
862 if test "$R7_SHOWOUTPUT" = yes; then
863 test "$in_function" -eq 1 &&
864 printf "%s\\n" "$line"
865 fi
866 done
869 # Text output functions
870 # =====================
872 alias output-dump=output_dump
873 alias outdump=output_dump
874 output_dump() (
875 set -e -- $(output_latest)
876 test $# -eq 0 && return 0
877 cd "$R7_WORKDIR" || _fail "Unable to cd to workdir: $R7_WORKDIR"
878 test "$R7_DRYRUN" = yes || {
879 mkdir -p "$R7_OUTPUTDIR"
880 awk -voutput_dir="$R7_OUTPUTDIR" '
881 BEGIN {
882 trace_id=""
883 hostname=""
884 funcname=""
885 groupname=""
886 in_function_block=0
888 /^:: [0-9].*/ {
889 if ($4 == "CONNECTED") {
890 trace_id=$2
891 hostname=$3
892 timestamp_connect=$5
893 system("mkdir -p " output_dir "/" hostname)
896 $2 == trace_id && $6 == "START" {
897 groupname=$4
898 funcname=$5
899 in_function_block=1
900 output_node = output_dir "/" hostname "/" groupname "/" funcname
901 output_status = output_node "/status"
902 output_output = output_node "/output"
903 system("rm -fr " output_node)
904 system("mkdir -p " output_node)
905 next
907 $2 == trace_id && $4 == "EXIT" {
908 in_function_block=0
909 system("echo " $5 " >" output_status)
911 $2 == trace_id && $6 == "END" {
912 in_function_block=0
913 system("echo " $7 " >" output_status)
915 in_function_block == 1 {
916 print > output_output
918 $2 == trace_id && $4 == "DISCONNECT" {
919 timestamp_disconnect=$5
920 difference=timestamp_disconnect - timestamp_connect
921 output_duration = output_dir "/" hostname "/" groupname "/duration"
922 system("echo " difference " >" output_duration)
924 ' "$@"
928 _epoch() (
929 case $(uname) in
930 OpenBSD | FreeBSD | NetBSD | Darwin)
931 date -r "$1"
932 ;;
933 Linux)
934 date -d "@$1"
935 ;;
936 *)
937 perl -e "print scalar localtime($1)"
938 ;;
939 esac
943 alias output-latest=output_latest
944 alias outlatest=output_latest
945 output_latest() (
946 set -e
947 latest=$(
948 # shellcheck disable=SC2012
949 ls 2>/dev/null -lt1 "$R7_RUNDIR/" | while read -r file; do
950 echo "${file%%.*}"
951 break
952 done
954 test -z "$latest" && return 0
955 latest_date=$(_epoch "$latest")
956 if test -n "$latest_date"; then
957 find "$R7_RUNDIR" -type f -name "$latest.*"
958 fi
961 _output_files() (
962 cd "$R7_OUTPUTDIR" 2>/dev/null || return 2
963 find ./* -type f -name 'output'
966 _status_files() (
967 cd "$R7_OUTPUTDIR" 2>/dev/null || return 2
968 find ./* -type f -name 'status'
971 _duration_files() (
972 cd "$R7_OUTPUTDIR" 2>/dev/null || return 2
973 find ./* -type f -name 'duration'
976 _column() {
977 column -t 2>/dev/null ||
978 cat -
981 alias output-status=output_status
982 alias outstat=output_status
983 output_status() (
984 cd "$R7_OUTPUTDIR" || return 2
985 echo HOST GROUP FUNCTION STATUS
986 _status_files | {
987 while read -r status; do
988 echo "$status" | while IFS=/ read -r _ host groupname funcname _; do
989 echo "$host" "$groupname" "$funcname" "$(cat "$status")"
990 done
991 done
992 } | _column
995 alias output-duration=output_duration
996 alias outdur=output_duration
997 output_duration() (
998 cd "$R7_OUTPUTDIR" || return 2
999 echo HOST GROUP DURATION
1000 _duration_files | {
1001 while read -r duration; do
1002 echo "$duration" | while IFS=/ read -r _ host groupname _; do
1003 echo "$host" "$groupname" "$(cat "$duration")"
1004 done
1005 done
1006 } | _column
1009 alias out=output
1010 # shellcheck disable=SC2254
1011 output() (
1012 cd "$R7_OUTPUTDIR" || return 2
1013 _output_files | while read -r output; do
1014 echo "$output" | while IFS=/ read -r _ host groupname funcname _; do
1015 case $host in ${1:-*})
1016 case $groupname in ${2:-*})
1017 case $funcname in ${3:-*})
1018 echo "- $host" "$groupname" "$funcname"
1019 echo
1020 sed 's/^/ /' "$R7_OUTPUTDIR/$output"
1021 echo
1023 esac
1025 esac
1027 esac
1028 done
1029 done
1032 alias trace-id-last=trace_id_last
1033 alias lastid=trace_id_last
1034 trace_id_last() {
1035 output_latest | while read -r output; do
1036 output=${output##*/}
1037 output=${output%%.*}
1038 echo "$output"
1039 break
1040 done
1043 # _output/tools0/r7test/r7test_diff/output::: 1748408055 tools0 DIFF /tmp/difftest r7test.dir/difftest
1044 alias output-diffs=output_diffs
1045 alias outdiffs=output_diffs
1046 # shellcheck disable=SC2048,SC2086
1047 output_diffs() (
1048 trace_id=$(trace_id_last)
1049 cd "$R7_OUTPUTDIR" || return 0
1050 grep -r "^:: ${trace_id:-} .* DIFF" | uniq | while read -r line; do
1051 set -- $line
1052 output_file=${1%%:::}
1053 OIFS=$IFS
1054 IFS=/
1055 set -- $output_file
1056 host=$1 groupname=$2 funcname=$3
1057 IFS=$OIFS
1058 echo "- (diff) -" "in" "$host" "$groupname" "$funcname" "($trace_id)"
1059 echo
1060 sed 's/^/ /' "$R7_OUTPUTDIR/$host/$groupname/$funcname/output" | grep -v "^ :: $trace_id $host"
1061 echo
1062 done || return 0
1063 return 0
1066 # :: 1748415578 tools0 PERMISSIONS CHANGE -rw-r-xr-x root wheel -> -rw-r--r-- root wheel on /tmp/difftest
1067 alias output-permissions=output_permissions
1068 alias outperm=output_permissions
1069 # shellcheck disable=SC2048,SC2086
1070 output_permissions() (
1071 trace_id=$(trace_id_last)
1072 cd "$R7_OUTPUTDIR" || return 0
1073 grep -r "^:: ${trace_id:-} .* PERMISSIONS" | while read -r line; do
1074 set -- $line
1075 output_file=${1%%:::}
1076 OIFS=$IFS
1077 IFS=/
1078 set -- $output_file
1079 host=$1 groupname=$2 funcname=$3
1080 IFS=$OIFS
1081 echo "- (permissions) -" "in" "$host" "$groupname" "$funcname" "($trace_id)"
1082 echo
1083 grep -e "^:: ${trace_id:-} .* PERMISSIONS CHANGE" "$R7_OUTPUTDIR/$host/$groupname/$funcname/output" |
1084 while read -r _ _ _ _ _ mode owner group _ mode2 owner2 group2 _ filename; do
1085 echo " - $filename from $mode $owner:$group to $mode2 $owner2:$group2"
1086 done
1087 echo
1088 done
1089 return 0
1092 alias output-errors=output_errors
1093 alias outerr=output_errors
1094 output_errors() (
1095 trace_id=$(trace_id_last)
1096 output_status | while read -r host groupname funcname status; do
1097 case $status in
1098 STATUS | 0 | 30 | 31) continue ;;
1099 esac
1100 echo "- (error $status) -" "in" "$host" "$groupname" "$funcname" "($trace_id)"
1101 if test -f "$R7_OUTPUTDIR/$host/$groupname/$funcname/output"; then
1102 echo
1103 sed 's/^/ /' "$R7_OUTPUTDIR/$host/$groupname/$funcname/output"
1104 echo
1106 done
1107 return 0
1110 summary() (
1111 test -d "$R7_OUTPUTDIR" || return 0
1112 summary_out=$(
1113 output_errors
1114 output_diffs
1115 output_permissions
1117 test -n "$summary_out" &&
1118 printf "%s\n" "$summary_out"
1119 return 0
1122 # Digraph functions
1123 # =================
1125 # Used in the digraph generation because inside a heredoc
1126 # context, memebers don't work as expected (OKSH).
1127 # shellcheck disable=SC2048,SC2086
1128 _members2() (
1129 case $1 in
1130 "'.*'")
1131 nodes | grep -e '.*'
1132 return 0
1134 esac
1135 set -f -- $(printf -- "-e %s " "$@")
1136 nodes | grep $*
1139 alias digraph-host=digraph_host
1140 # shellcheck disable=SC2086
1141 digraph_host() (
1142 _debug "DIGRAPH HOST"
1143 cat <<-eof
1144 digraph host {
1145 layout="dot"
1146 splines="true"
1147 graph [overlap="false", rankdir=LR, mode=KK]
1149 set -f
1150 for node in $(nodes); do echo "\"$node\" [peripheries=2 color=blue]"; done
1151 grep 2>/dev/null "^group" "$R7_SITE_CONFIG" | while read -r _ groupname nodes; do
1152 echo "\"$groupname\" [color=orange]"
1153 for node in $(_members2 $nodes); do
1154 echo "\"$node\" -> \"$groupname\""
1155 done
1156 for funct in $(group_functions "$groupname"); do
1157 echo "\"$groupname::$funct\" [color=red]"
1158 echo "\"$groupname\" -> \"$groupname::$funct\""
1159 done
1160 done
1163 eof
1166 alias digraph-ssh-id-hosts=digraph_ssh_id_hosts
1167 digraph_ssh_id_hosts() {
1168 cat <<-eof
1169 digraph ssh_id_hosts {
1170 layout="dot"
1171 splines="true"
1172 graph [overlap="false"]
1173 $(ssh_ids_hosts)
1175 eof
1178 # Html
1179 # ====
1180 # TODO: WIP
1182 # shellcheck disable=SC2120
1183 md() {
1184 pandoc -t html -f markdown+smart "$@"
1187 _doc() {
1188 output \* \* doc\* | grep ^- | while read -r _ host group function; do
1189 cat "$R7_OUTPUTDIR/$host/$group/$function/output"
1190 echo
1191 done
1194 _summary() {
1195 summary | while read -r line; do
1196 set -- $line
1197 case $1 in
1199 printf %s\\n "$line"
1201 *) continue ;;
1202 esac
1203 echo
1204 echo '```'
1205 cat "$R7_OUTPUTDIR/$6/$7/$8/output"
1206 echo '```'
1207 done
1210 html() {
1211 _debug HTML
1212 cat <<-eof
1213 <!DOCTYPE html>
1214 <html lang="en">
1215 <head>
1216 <meta charset="UTF-8">
1217 <meta name="viewport" content="width=device-width,initial-scale=1">
1218 <title>${PWD##*/}</title>
1219 <style>
1220 body{margin:1rem;background:#f5f7fa;color:#333;font:14px/1.5 Consolas,monospace}
1221 .container{max-width:800px;margin:0 auto;padding:1.5rem;background:#fff;box-shadow:0 1px 4px rgba(0,0,0,0.05);border-radius:3px}
1222 header{border-bottom:1px solid #ddd;margin-bottom:1rem;padding-bottom:.5rem}
1223 header h1{margin:0;font-size:1.4rem;font-weight:400}
1224 header time{font-size:.85rem;color:#666}
1225 .summary{border-bottom:1px dashed #eee;padding:.5rem 0}
1226 .summary:last-child{border:none;padding:0}
1227 .timestamp{display:block;font-size:.8rem;color:#555;margin-bottom:.25rem}
1228 </style>
1229 </head>
1230 <body>
1231 <div class="container">
1232 <header>
1233 <h1>Summary</h1>
1234 </header>
1235 <section class="summary">
1236 $(_summary | md)
1237 </section>
1238 <section class="summary">
1239 $(_doc | md)
1240 </section>
1241 </div>
1242 </body>
1243 </html>
1244 eof
1245 return 0
1248 # Importers
1249 # =========
1251 import() {
1252 cd "$R7_WORKDIR" || _fail "Unable to cd to '$R7_WORKDIR'"
1253 target=$1
1254 if test -n "$target"; then
1255 echo >&2 "-> Importing target host: $target"
1256 case $(ssh "$target" uname) in
1257 Linux) _Linux_import ;;
1258 SunOS) _SunOS_import ;;
1259 OpenBSD) _OpenBSD_import ;;
1260 FreeBSD) _FreeBSD_import ;;
1261 NetBSD) _NetBSD_import ;;
1262 DragonFly) _DragonFly_import ;;
1263 Haiku) _Haiku_import ;;
1264 QNX) _QNX_import ;;
1265 Minix) _Minix_import ;;
1267 _error "Platform unsupported or host unavailable."
1268 return 3
1270 esac
1271 echo >&2 "-> done: $target"
1272 else
1273 echo >&2 "usage: $0 target-host"
1277 _import_copy() (
1278 src=$1
1279 dst=$2
1280 _debug "COPY $src $dst"
1281 ssh "$target" command -v rsync >/dev/null && {
1282 _debug "IMPORT $1 $2 USE RSYNC"
1283 rsync -e "/usr/bin/ssh -oConnectTimeout=$SSH_CONNECT_TIMEOUT -F $SSH_CONFIG_FILE" \
1284 -a "$src" "$dst" && return 0
1286 _openrsync=$(ssh "$target" command -v openrsync) && {
1287 _debug "IMPORT $1 $2 USE OPENRSYNC"
1288 openrsync -e "/usr/bin/ssh -oConnectTimeout=$SSH_CONNECT_TIMEOUT -F $SSH_CONFIG_FILE" \
1289 --rsync-path="$_openrsync" \
1290 -a "$src" "$dst" && return 0
1292 _debug "IMPORT $1 $2 USE SCP"
1293 scp "-oConnectTimeout=$SSH_CONNECT_TIMEOUT" -qF "$SSH_CONFIG_FILE" -r "$src" "$dst" && return 0
1294 _fail "IMPORT $1 $2 FAILED"
1297 _import_ssh_authorized_keys_old() (
1298 set +e
1299 other_users=$(
1300 ssh "$target" getent passwd | awk -F: '$3 >= 1000 { print $1 }' |
1301 grep -v nobody
1304 for user in root $other_users; do
1305 # shellcheck disable=SC2029
1306 home=$(ssh "$target" getent passwd "$user" | awk -F: '{ print $6 }')
1307 authorized_keys="$home/.ssh/authorized_keys"
1308 ssh "$target" test -f "$authorized_keys" || continue
1309 mkdir -p "nodes/$target/ssh/"
1310 echo >&2 "--> Importing $authorized_keys"
1311 _import_copy "$target:$authorized_keys" "nodes/$target/ssh/authorized_keys_$user"
1312 done
1315 _import_ssh_authorized_keys() (
1316 set +e
1317 other_users=$(
1318 ssh "$target" cat /etc/passwd | awk -F: '$3 >= 1000 { print $1 }' | grep -v nobody
1320 for user in root $other_users; do
1321 home=$(
1322 ssh "$target" cat /etc/passwd | awk -F: -v user="$user" '$1 == user { print $6 }'
1324 authorized_keys="$home/.ssh/authorized_keys"
1325 if ! ssh -n "$target" test -f "$authorized_keys"; then
1326 continue
1328 mkdir -p "nodes/$target/ssh/"
1329 echo >&2 "--> Importing $authorized_keys for user='$user'"
1330 _import_copy "$target:$authorized_keys" "nodes/$target/ssh/authorized_keys_$user"
1331 done
1334 _import_etc_if_exists() {
1335 mkdir -p nodes/"$target"
1336 echo >&2 "--> Importing /etc"
1337 for file in $etc_files_to_import; do
1338 ssh "$target" test -f "'/etc/$file'" &&
1339 _import_copy "$target:/etc/$file" "nodes/$target"
1340 done
1343 _import_etc_ssh_if_exists() {
1344 etc_ssh_files_to_import='
1345 ssh/sshd_config
1346 ssh/ssh_config
1348 mkdir -p nodes/"$target/ssh"
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"
1353 done
1356 _import_modprobe() {
1357 if ssh "$target" test -d /etc/modprobe.d; then
1358 _content=$(ssh "$target" ls -A /etc/modprobe.d/)
1359 test -z "$_content" && return 0
1360 echo >&2 "--> Importing /etc/modprobe.d"
1361 mkdir -p nodes/"$target/modprobe.d"
1362 _import_copy "$target:/etc/modprobe.d/*" "nodes/$target/modprobe.d"
1366 _QNX_import() {
1367 etc_files_to_import='
1368 build.date
1370 _import_etc_if_exists
1371 echo >&2 "--> Importing /etc/settings"
1372 mkdir -p "nodes/$target/settings"
1373 _import_copy "$target:/etc/settings" "nodes/$target/settings"
1374 echo >&2 "--> Importing /system/etc/ssh"
1375 mkdir -p "nodes/$target/ssh"
1376 _import_copy "$target:/system/etc/ssh/sshd_config" "nodes/$target/ssh"
1377 _import_ssh_authorized_keys
1380 _Minix_import() {
1381 etc_files_to_import='
1382 fstab
1383 inet.conf
1384 profile
1385 resolv.conf
1386 shells
1387 rc.conf
1389 _import_etc_if_exists
1390 echo >&2 "--> Importing /usr/pkg/etc/ssh"
1391 mkdir -p nodes/"$target/ssh"
1392 _import_copy "$target:/usr/pkg/etc/ssh/sshd_config" "nodes/$target/ssh"
1393 _import_copy "$target:/usr/pkg/etc/ssh/ssh_config" "nodes/$target/ssh"
1394 echo >&2 "--> Importing /usr/pkg/etc/pkgin"
1395 mkdir -p nodes/"$target/pkgin"
1396 _import_copy "$target:/usr/pkg/etc/pkgin/repositories.conf" "nodes/$target/pkgin"
1399 _Haiku_import() {
1400 etc_files_to_import='
1401 inputrc
1402 profile
1404 _import_etc_if_exists
1405 _import_copy "$target:/boot/system/settings/network/hosts" "nodes/$target/"
1406 _import_copy "$target:/boot/system/settings/network/resolv.conf" "nodes/$target/"
1407 _import_copy "$target:/boot/system/settings/package-repositories" "nodes/$target/"
1408 echo >&2 "--> Importing /boot/system/settings/ssh"
1409 mkdir -p "nodes/$target/ssh"
1410 _import_copy "$target:/boot/system/settings/ssh/sshd_config" "nodes/$target/ssh"
1411 _import_copy "$target:/boot/system/settings/ssh/ssh_config" "nodes/$target/ssh"
1412 _import_copy "$target:/boot/home/config/settings/ssh/authorized_keys" \
1413 "nodes/$target/ssh/authorized_keys_user"
1416 _Linux_import() {
1417 if _Linux_is_debian_based; then
1418 _Linux_import_debian
1419 elif _Linux_is_rhel_based; then
1420 _Linux_import_rhel
1421 elif _Linux_is_suse_based; then
1422 _Linux_import_suse
1423 elif _Linux_is_gentoo_based; then
1424 _Linux_import_gentoo
1425 elif _Linux_is_arch_based; then
1426 _Linux_import_arch
1427 elif _Linux_is_artix; then
1428 _Linux_import_artix
1429 elif ssh "$target" "grep -q 'ID=alpine' /etc/os-release"; then
1430 _Linux_import_alpine
1431 elif ssh "$target" "grep -q 'ID=crux' /etc/os-release"; then
1432 _Linux_import_crux
1433 elif ssh "$target" "grep -q 'ID=\"void\"' /etc/os-release"; then
1434 _Linux_import_void
1435 elif ssh "$target" "grep -q 'ID=guix' /etc/os-release"; then
1436 _Linux_import_guix
1437 else
1438 echo >&2 "Unsupported Linux distribution."
1442 _Linux_is_debian_based() (
1443 ssh "$target" "grep -q 'ID=debian' /etc/os-release || \
1444 grep -q 'ID_LIKE=.*debian' /etc/os-release || \
1445 grep -qiE 'Debian|Ubuntu|Linux Mint' /etc/issue || \
1446 test -f /etc/debian_version || test -d /etc/apt || command -v apt-get >/dev/null 2>&1" 2>/dev/null
1449 _Linux_is_rhel_based() (
1450 ssh "$target" "grep -qiE 'ID=\"?(rhel|fedora|centos|ol|scientific|rocky|eurolinux)\"?' /etc/os-release || \
1451 grep -qiE 'ID_LIKE=\"?.*(rhel|fedora).*\"?' /etc/os-release || \
1452 grep -qiE 'Red Hat|CentOS|Fedora|Scientific Linux|Oracle Linux' /etc/issue || \
1453 test -f /etc/redhat-release || test -f /etc/system-release || test -f /etc/centos-release || \
1454 test -f /etc/fedora-release || test -f /etc/oracle-release || \
1455 (test -f /etc/os-release && grep -qi 'rhel' /etc/os-release) || test -d /etc/yum.repos.d" 2>/dev/null
1458 _Linux_is_gentoo_based() (
1459 ssh "$target" "test -f /etc/gentoo-release || \
1460 grep -q 'ID=gentoo' /etc/os-release || \
1461 grep -q 'ID=funtoo' /etc/os-release || \
1462 grep -qi 'Gentoo' /etc/issue || \
1463 command -v emerge >/dev/null 2>&1" 2>/dev/null
1466 _Linux_is_suse_based() (
1467 ssh "$target" "test -f /etc/os-release && grep -Eqi \
1468 'suse|opensuse' /etc/os-release || \
1469 command -v zypper >/dev/null || \
1470 command -v yast2 >/dev/null" 2>/dev/null
1473 _Linux_is_arch_based() (
1474 ssh "$target" "test -f /etc/os-release && grep -Eqi \
1475 'arch|manjaro|endeavouros|arcolinux|parabola' /etc/os-release || \
1476 command -v pacman >/dev/null || \
1477 command -v makepkg >/dev/null" 2>/dev/null
1480 _Linux_is_artix() (
1481 ssh "$target" "test -f /etc/os-release && grep -Eqi \
1482 'artix|hyperbola' /etc/os-release || \
1483 command -v pacman >/dev/null || \
1484 command -v makepkg >/dev/null" 2>/dev/null
1487 _Linux_import_gentoo() {
1488 etc_files_to_import='
1489 crontab
1490 doas.conf
1491 environment
1492 fstab
1493 hostname
1494 hosts
1495 hosts.denv
1496 issue
1497 locale.gen
1498 machine-id
1499 motd
1500 nsswitch.conf
1501 pam.conf
1502 profile
1503 shells
1504 sudo.conf
1505 sudoers
1506 sysctl.conf
1508 _import_etc_if_exists
1509 _import_etc_ssh_if_exists
1510 echo >&2 "--> Importing /etc/portage"
1511 mkdir -p "nodes/$target/portage"
1512 _import_copy "$target:/etc/portage/" "nodes/$target/portage/"
1513 _import_ssh_authorized_keys
1514 _import_modprobe
1517 _Linux_import_alpine() {
1518 etc_files_to_import='
1519 crontab
1520 doas.conf
1521 environment
1522 fstab
1523 hostname
1524 hosts
1525 hosts.denv
1526 issue
1527 motd
1528 nsswitch.conf
1529 pam.conf
1530 profile
1531 rc.conf
1532 resolv.conf
1533 shells
1534 sysctl.conf
1536 _import_etc_if_exists
1537 _import_etc_ssh_if_exists
1538 echo >&2 "--> Importing /etc/apk"
1539 mkdir -p "nodes/$target/apk"
1540 _import_copy "$target:/etc/apk/repositories" "nodes/$target/apk/"
1541 _import_ssh_authorized_keys
1542 _import_modprobe
1545 _Linux_import_rhel() {
1546 etc_files_to_import='
1547 crontab
1548 environment
1549 fstab
1550 hostname
1551 hosts
1552 hosts.denv
1553 issue
1554 locale.gen
1555 machine-id
1556 motd
1557 nsswitch.conf
1558 pam.conf
1559 profile
1560 resolv.conf
1561 shells
1562 sudo.conf
1563 sudoers
1565 _import_etc_if_exists
1566 _import_etc_ssh_if_exists
1567 echo >&2 "--> Importing /etc/yum.repos.d"
1568 mkdir -p "nodes/$target/yum.repos.d"
1569 _import_copy "$target:/etc/yum.repos.d/" "nodes/$target/yum.repos.d/"
1570 _import_ssh_authorized_keys
1571 _import_modprobe
1574 _Linux_import_debian() {
1575 etc_files_to_import='
1576 crontab
1577 environment
1578 fstab
1579 hostname
1580 hosts
1581 hosts.denv
1582 issue
1583 locale.gen
1584 machine-id
1585 nsswitch.conf
1586 pam.conf
1587 profile
1588 resolv.conf
1589 shells
1590 sudo.conf
1591 sudoers
1593 _import_etc_if_exists
1594 echo >&2 "--> Importing /etc/apt"
1595 mkdir -p "nodes/$target/apt"
1596 _import_copy "$target:/etc/apt/sources.list" "nodes/$target/apt"
1597 _import_copy "$target:/etc/apt/sources.list.d" "nodes/$target/apt"
1598 echo >&2 "--> Importing /etc/network"
1599 mkdir -p "nodes/$target/network/"
1600 _import_copy "$target:/etc/network/interfaces" "nodes/$target/network/"
1601 _import_copy "$target:/etc/network/interfaces.d" "nodes/$target/network/"
1602 _import_etc_ssh_if_exists
1603 _import_ssh_authorized_keys
1604 _import_modprobe
1608 _Linux_import_suse() {
1609 etc_files_to_import='
1610 fstab
1611 hosts
1612 profile
1613 sudo.conf
1614 sudoers
1615 sysctl.conf
1616 environment
1617 exports
1618 chrony.conf
1620 _import_etc_if_exists
1621 _import_etc_ssh_if_exists
1622 _import_ssh_authorized_keys
1623 _import_modprobe
1626 _Linux_import_arch() {
1627 etc_files_to_import='
1628 environment
1629 hostname
1630 profile
1631 arch-release
1632 issue
1633 hosts
1634 pacman.conf
1635 sudo.conf
1636 sudoers
1637 fstab
1639 _import_etc_if_exists
1640 _import_etc_ssh_if_exists
1641 _import_ssh_authorized_keys
1642 _import_modprobe
1645 _Linux_import_artix() {
1646 _Linux_import_arch
1649 _Linux_import_guix() {
1650 etc_files_to_import='
1651 config.scm
1652 environment
1653 fstab
1654 hostname
1655 hosts
1656 issue
1657 nsswitch.conf
1658 profile
1659 resolv.conf
1660 shells
1661 sudoers
1663 _import_etc_if_exists
1664 _import_etc_ssh_if_exists
1665 _import_ssh_authorized_keys
1668 _Linux_import_void() {
1669 etc_files_to_import='\
1670 fstab
1671 dhcpcd.conf
1672 environment
1673 hosts
1674 profile
1675 rc.conf
1676 nsswitch.conf
1677 shells
1678 sudoers
1679 sudo.conf
1680 sysctl.conf
1681 resolv.conf
1683 _import_etc_if_exists
1684 _import_etc_ssh_if_exists
1685 _import_ssh_authorized_keys
1686 _import_modprobe
1689 _Linux_import_crux() {
1690 etc_files_to_import='
1691 crontab
1692 hosts
1693 resolv.conf
1694 environment
1695 fstab
1696 locale.gen
1697 hosts
1698 profile
1699 rc.conf
1700 rc.local
1701 syslog.conf
1702 sysctl.conf
1704 _import_etc_if_exists
1705 _import_etc_ssh_if_exists
1706 _import_ssh_authorized_keys
1707 _import_modprobe
1710 _SunOS_import() {
1711 etc_files_to_import='
1712 crontab
1713 environment
1714 fstab
1715 hostname
1716 hosts
1717 hosts.denv
1718 issue
1719 locale.gen
1720 motd
1721 nodename
1722 nsswitch.conf
1723 pam.conf
1724 profile
1725 profile
1726 resolv.conf
1727 rsyslog.conf
1728 shells
1729 sudo.conf
1730 sudoers
1732 _import_etc_if_exists
1733 _import_etc_ssh_if_exists
1734 _import_ssh_authorized_keys
1737 _FreeBSD_import() {
1738 etc_files_to_import='
1739 crontab
1740 fbtab
1741 fstab
1742 hostname
1743 hosts
1744 login.conf
1745 nsswitch.conf
1746 ntp.conf
1747 profile
1748 resolv.conf
1749 shells
1750 sysctl.conf
1751 syslog.conf
1753 _import_etc_if_exists
1754 echo >&2 "--> Importing /etc/pkg"
1755 _import_copy "$target:/etc/pkg" "nodes/$target/"
1756 _import_etc_ssh_if_exists
1757 _import_ssh_authorized_keys
1760 _DragonFly_import() {
1761 etc_files_to_import='
1762 crontab
1763 fstab
1764 hosts
1765 login.conf
1766 pf.conf
1767 resolv.conf
1768 shells
1769 sysctl.conf
1770 syslog.conf
1772 _import_etc_if_exists
1773 _import_etc_ssh_if_exists
1774 _import_ssh_authorized_keys
1777 _NetBSD_import() {
1778 etc_files_to_import='
1779 fstab
1780 hosts
1781 locate.conf
1782 pf.conf
1783 resolv.conf
1784 shells
1785 sysctl.conf
1786 syslog.conf
1787 usermgmt.conf
1788 wscons.conf
1789 rc.local
1791 _import_etc_if_exists
1792 _import_etc_ssh_if_exists
1793 _import_ssh_authorized_keys
1796 _OpenBSD_import() {
1797 etc_files_to_import='
1798 acme-client.conf
1799 boot.conf
1800 dhcpcd.conf
1801 dhcpd.conf
1802 doas.conf
1803 fstab
1804 gotd.conf
1805 gotwebd.conf
1806 hosts
1807 httpd.conf
1808 installurl
1809 locate.rc
1810 login.conf
1811 mygate
1812 myname
1813 newsyslog.conf
1814 syslog.conf
1815 ntpd.conf
1816 pf.conf
1817 resolv.conf
1818 sysctl.conf
1819 unwind.conf
1820 usermgt.conf
1821 rc.conf.local
1823 etc_ssh_files_to_import='
1824 ssh/sshd_config
1825 ssh/ssh_config
1827 var_unbound_files='
1828 db/named.root
1829 etc/unbound.conf
1831 var_nsd_files='
1832 etc/nsd.conf
1834 var_postgresql_files='
1835 data/pg_hba.conf
1836 data/postgresql.conf
1838 _import_ssh_authorized_keys
1839 _OpenBSD_import_etc_if_exists
1840 _import_etc_ssh_if_exists
1841 _OpenBSD_import_etc_mail_if_exists
1842 _OpenBSD_import_unbound_if_enabled
1843 _OpenBSD_import_nsd_if_enabled
1844 _OpenBSD_import_postgresql_if_installed
1847 _OpenBSD_import_etc_if_exists() {
1848 mkdir -p nodes/"$target"
1849 interfaces=$(ssh "$target" 'cd /etc && ls -1 hostname.*')
1850 echo >&2 "--> Importing /etc"
1851 for file in $etc_files_to_import $interfaces; do
1852 ssh "$target" test -f "'/etc/$file'" &&
1853 _import_copy "$target:/etc/$file" "nodes/$target/"
1854 done
1857 _OpenBSD_import_etc_mail_if_exists() {
1858 etc_mail_files_to_import='
1859 mail/aliases
1860 mail/smtpd.conf
1861 mail/spamd.conf
1863 mkdir -p nodes/"$target"
1864 echo >&2 "--> Importing /etc/mail"
1865 for file in $etc_mail_files_to_import; do
1866 ssh "$target" test -f "'/etc/$file'" &&
1867 _import_copy "$target:/etc/$file" "nodes/$target/mail"
1868 done
1871 _OpenBSD_import_unbound_if_enabled() {
1872 ssh "$target" rcctl check unbound >/dev/null && {
1873 echo >&2 "--> Importing /var/unbound"
1874 for file in $var_unbound_files; do
1875 prefix=${file%%/*}
1876 mkdir -p "nodes/$target/unbound/$prefix"
1877 _import_copy "$target:/var/unbound/$file" "nodes/$target/unbound/$prefix/"
1878 done
1879 return 0
1883 _OpenBSD_import_nsd_if_enabled() {
1884 ssh "$target" rcctl check nsd >/dev/null && {
1885 echo >&2 "--> Importing /var/nsd"
1886 for file in $var_nsd_files; do
1887 prefix=${file%%/*}
1888 mkdir -p "nodes/$target/nsd/$prefix"
1889 _import_copy "$target:/var/nsd/$file" "nodes/$target/nsd/$prefix/"
1890 done
1891 return 0
1895 _OpenBSD_import_postgresql_if_installed() {
1896 ssh "$target" postgres -V 2>/dev/null | grep -q PostgreSQL && {
1897 echo >&2 "--> Importing /var/postgresql"
1898 for file in $var_postgresql_files; do
1899 prefix=${file%%/*}
1900 mkdir -p "nodes/$target/postgresql/$prefix"
1901 _import_copy "$target:/var/postgresql/$file" "nodes/$target/postgresql/$prefix/"
1902 done
1903 echo >&2 "--> Importing /etc/login.conf.d/postgresql"
1904 mkdir -p "nodes/$target/login.conf.d"
1905 _import_copy "$target:/etc/login.conf.d/postgresql" "nodes/$target/login.conf.d/"
1906 return 0
1910 # Main
1911 # ====
1913 _main "${@:-}"