Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save megamen32/5bce58d71973d70b64e8f220053368c0 to your computer and use it in GitHub Desktop.

Select an option

Save megamen32/5bce58d71973d70b64e8f220053368c0 to your computer and use it in GitHub Desktop.
# --- all: run command or script on local + many hosts (bash-only) ------------
all() {
# ---- defaults / env ----
local parallel=${ALL_PARALLEL:-1} # 1=parallel remotes
local run_local=${ALL_LOCAL:-1} # 1=run local too
local local_parallel=${ALL_LOCAL_PARALLEL:-1} # 1=local concurrent with remotes
local timeout=${ALL_TIMEOUT:-8}
local pty=${ALL_PTY:-0}
local sudo_n=${ALL_SUDO_N:-1}
local prefix=${ALL_PREFIX:-1} # kept for backward compatibility
local show_ok=${ALL_SHOW_OK:-0} # 1=print OK lines in summary
# suppress bash job-control noise
set +m 2>/dev/null || true
set +o notify 2>/dev/null || true
_hdr() { printf '===== %s =====\n' "$1"; }
# expand "~" at beginning (supports ~ and ~/ ; ~user/ optional)
_expand_path() {
local p="$1"
case "$p" in
"~") printf '%s\n' "$HOME" ;;
"~/"*) printf '%s\n' "$HOME/${p:2}" ;;
"~"[^/]*/*)
local u="${p%%/*}"; u="${u:1}"
local rest="${p#*/}"
local uh
uh="$(getent passwd "$u" 2>/dev/null | awk -F: '{print $6}' | head -n1)"
[[ -n "$uh" ]] && printf '%s\n' "$uh/$rest" || printf '%s\n' "$p"
;;
*) printf '%s\n' "$p" ;;
esac
}
# ---- hosts ----
local -a hosts
if [[ -n "${ALL_HOSTS-}" ]]; then
read -r -a hosts <<<"$ALL_HOSTS"
else
hosts=(192.168.2.5 192.168.2.75)
fi
# ---- ssh opts ----
local -a ssh_opts=()
if [[ -n "${ALL_SSH_OPTS-}" ]]; then
read -r -a ssh_opts <<<"$ALL_SSH_OPTS"
fi
local -a base_ssh=(ssh -o BatchMode=yes -o ConnectTimeout="$timeout")
(( pty )) && base_ssh+=( -tt )
base_ssh+=( "${ssh_opts[@]}" )
# ---- mode parse ----
local mode="argv" # argv|file|stdin
local script_path="" script_blob=""
local -a argv=()
if (( $# == 0 )); then
if ! -t 0; then
mode="stdin"
script_blob="$(cat)"
else
echo "Usage:" >&2
echo " all <command...>" >&2
echo " all @/path/to/script.sh" >&2
echo " all <<'SH' ... SH" >&2
echo " cat script.sh | all" >&2
echo "Env: ALL_HOSTS=\"h1 h2\" ALL_PARALLEL=1 ALL_TIMEOUT=8 ALL_SSH_OPTS=\"...\" ALL_PTY=1 ALL_SUDO_N=1 ALL_LOCAL=1" >&2
return 1
fi
else
if [[ "$1" == @* ]]; then
mode="file"
script_path="$(_expand_path "${1#@}")"
[[ -f "$script_path" ]] || { echo "ERROR: script file not found: $script_path" >&2; return 2; }
script_blob="$(cat "$script_path")"
shift || true
(( $# == 0 )) || { echo "ERROR: extra args after @file are not supported" >&2; return 2; }
else
mode="argv"
argv=( "$@" )
if (( sudo_n )) && [[ "${argv[0]}" == "sudo" ]]; then
case " ${argv[*]} " in
*" sudo -n "*|*" sudo -S "*|*" sudo -A "* ) : ;;
* ) argv=( sudo -n "${argv[@]:1}" ) ;;
esac
fi
fi
fi
# ---- build remote cmd (argv mode) ----
local remote_cmd="" bash_lc=""
if [[ "$mode" == "argv" ]]; then
remote_cmd="$(printf '%q ' "${argv[@]}")"
bash_lc="$(printf '%q' "$remote_cmd")"
fi
# ---- runners ----
_run_local() {
if [[ "$mode" == "argv" ]]; then
"${argv[@]}"
else
bash -s <<<"$script_blob"
fi
}
_run_remote() {
local host="$1"
if [[ "$mode" == "argv" ]]; then
"${base_ssh[@]}" "$host" "bash -lc $bash_lc"
else
printf '%s' "$script_blob" | "${base_ssh[@]}" "$host" "bash -s"
fi
}
_print_block() {
local title="$1" rc="$2" file="$3" status
if (( rc == 0 )); then
status="OK"
else
status="FAIL:$rc"
fi
printf '\n===== %s [%s] =====\n' "$title" "$status"
if [[ -s "$file" ]]; then
cat "$file"
else
printf '(no output)\n'
fi
}
# ---- exec ----
local tmpdir
tmpdir="$(mktemp -d "${TMPDIR:-/tmp}/all.XXXXXX")" || return 1
trap 'rm -rf "$tmpdir"' RETURN
local -a pids=() names=() titles=() files=() rcs=()
local rc any_fail=0
_spawn() {
local name="$1" title="$2" out="$3"; shift 3
local pid
{ ( "$@" >"$out" 2>&1 ) & pid=$!; } 2>/dev/null
pids+=( "$pid" )
names+=( "$name" )
titles+=( "$title" )
files+=( "$out" )
}
if (( parallel )); then
if (( run_local && local_parallel )); then
_spawn "LOCAL" "LOCAL ($(hostname))" "$tmpdir/LOCAL.log" _run_local
fi
local host
for host in "${hosts[@]}"; do
_spawn "$host" "$host" "$tmpdir/$host.log" _run_remote "$host"
done
local i
for i in "${!pids[@]}"; do
wait "${pids[$i]}"; rc=$?
rcs+=( "$rc" )
(( rc != 0 )) && any_fail=1
done
if (( run_local && ! local_parallel )); then
names+=( "LOCAL" )
titles+=( "LOCAL ($(hostname))" )
files+=( "$tmpdir/LOCAL.log" )
_run_local >"${files[-1]}" 2>&1; rc=$?
rcs+=( "$rc" )
(( rc != 0 )) && any_fail=1
fi
else
if (( run_local )); then
names+=( "LOCAL" )
titles+=( "LOCAL ($(hostname))" )
files+=( "$tmpdir/LOCAL.log" )
_run_local >"${files[-1]}" 2>&1; rc=$?
rcs+=( "$rc" )
(( rc != 0 )) && any_fail=1
fi
local host
for host in "${hosts[@]}"; do
names+=( "$host" )
titles+=( "$host" )
files+=( "$tmpdir/$host.log" )
_run_remote "$host" >"${files[-1]}" 2>&1; rc=$?
rcs+=( "$rc" )
(( rc != 0 )) && any_fail=1
done
fi
# ---- output blocks ----
local i
for i in "${!names[@]}"; do
_print_block "${titles[$i]}" "${rcs[$i]}" "${files[$i]}"
done
# ---- summary ----
local name
for i in "${!names[@]}"; do
rc="${rcs[$i]}"
name="${names[$i]}"
if (( rc != 0 )); then
printf 'FAIL (%s): exit=%s\n' "$name" "$rc" >&2
elif (( show_ok )); then
printf 'OK (%s)\n' "$name"
fi
done
(( any_fail )) && return 1
return 0
}
# --- helpers for your ansible-sync project -----------------------------------
all_add() {
local path="$1"
[[ -n "$path" ]] || { echo "Usage: all_add /absolute/path"; return 2; }
[[ "$path" == /* ]] || { echo "ERROR: use absolute path"; return 2; }
[[ -e "$path" ]] || { echo "ERROR: not found: $path"; return 2; }
local proj="$HOME/ansible-sync"
[[ -d "$proj" ]] || { echo "ERROR: $proj not found"; return 2; }
echo "$path" >> "$proj/sync_paths.txt"
echo "Registered: $path"
( cd "$proj" && ./bin_sync.sh )
}
all_deploy() {
local proj="$HOME/ansible-sync"
[[ -d "$proj" ]] || { echo "ERROR: $proj not found"; return 2; }
( cd "$proj" && ./bin_sync.sh )
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment