1 contributor
#!/bin/bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
CONFIG_PATH="${ROOT_DIR}/cluster-context/madagascar.json"
CLUSTER_NAME="madagascar"
COMMAND="install"
REMOTE_USER="root"
DRY_RUN=0
PROJECT_NAME=""
PROJECT_DIR=""
DEPLOY_MODE=""
NODE_FILTERS=()
TARGETS=()
usage() {
cat <<EOF
Usage: $0 <project> [command] [options]
Commands:
install Deploy/install project on selected nodes (default)
uninstall Remove project from selected nodes
status Query project status on selected nodes (deploy.sh projects only)
start Start services on selected nodes (deploy.sh projects only)
restart Restart services on selected nodes (deploy.sh projects only)
stop Stop services on selected nodes (deploy.sh projects only)
Options:
--cluster <name> Cluster name in cluster-context/madagascar.json (default: madagascar)
--node <name|ip> Restrict to one node. Can be repeated.
--user <user> Remote SSH user for setup.sh projects (default: root)
--dry-run Show resolved targets and commands without executing
-h, --help Show this help
Examples:
$0 pve-guests-state
$0 pve-guests-state install --node ebony
$0 autoNAS install
$0 autoSMART status --node 192.168.2.92
EOF
}
require_config() {
if [[ ! -f "${CONFIG_PATH}" ]]; then
echo "ERROR: missing cluster config: ${CONFIG_PATH}" >&2
exit 1
fi
}
require_project() {
PROJECT_DIR="${ROOT_DIR}/projects/${PROJECT_NAME}"
if [[ ! -d "${PROJECT_DIR}" ]]; then
echo "ERROR: unknown project: ${PROJECT_NAME}" >&2
exit 1
fi
if [[ -x "${PROJECT_DIR}/setup.sh" ]]; then
DEPLOY_MODE="setup"
return
fi
if [[ -x "${PROJECT_DIR}/deploy.sh" ]]; then
DEPLOY_MODE="deploy"
return
fi
echo "ERROR: project ${PROJECT_NAME} has neither setup.sh nor deploy.sh" >&2
exit 1
}
load_targets() {
local entry=""
TARGETS=()
while IFS= read -r entry; do
[[ -n "${entry}" ]] && TARGETS+=("${entry}")
done < <(
jq -r --arg cluster "${CLUSTER_NAME}" '
.clusters[$cluster].nodes
| to_entries[]
| .key + "\t" + ((.value.ip // .value.wan.vmbr443.address // empty) | split("/")[0])
' "${CONFIG_PATH}"
)
if [[ ${#TARGETS[@]} -eq 0 ]]; then
echo "ERROR: no targets found for cluster ${CLUSTER_NAME}" >&2
exit 1
fi
}
match_filter() {
local filter="$1"
local node_name="$2"
local node_ip="$3"
[[ "${filter}" == "${node_name}" || "${filter}" == "${node_ip}" ]]
}
filter_targets() {
local filtered=()
local entry=""
local filter=""
local node_name=""
local node_ip=""
if [[ ${#NODE_FILTERS[@]} -eq 0 ]]; then
return
fi
for entry in "${TARGETS[@]}"; do
node_name="${entry%%$'\t'*}"
node_ip="${entry#*$'\t'}"
for filter in "${NODE_FILTERS[@]}"; do
if match_filter "${filter}" "${node_name}" "${node_ip}"; then
filtered+=("${entry}")
break
fi
done
done
TARGETS=("${filtered[@]}")
if [[ ${#TARGETS[@]} -eq 0 ]]; then
echo "ERROR: no targets matched the provided --node filters" >&2
exit 1
fi
}
run_setup_project() {
local node_name="$1"
local node_ip="$2"
local cmd=()
case "${COMMAND}" in
install)
cmd=(bash "${PROJECT_DIR}/setup.sh" --user "${REMOTE_USER}" "${node_ip}")
;;
uninstall)
cmd=(bash "${PROJECT_DIR}/setup.sh" --user "${REMOTE_USER}" --uninstall "${node_ip}")
;;
*)
echo "ERROR: command ${COMMAND} is not supported for setup.sh-only projects" >&2
exit 1
;;
esac
echo "==> ${PROJECT_NAME}: ${COMMAND} on ${node_name} (${node_ip})"
if [[ "${DRY_RUN}" -eq 1 ]]; then
printf 'DRY-RUN:'
printf ' %q' "${cmd[@]}"
echo
return
fi
(cd "${PROJECT_DIR}" && "${cmd[@]}")
}
run_deploy_project() {
local node_name="$1"
local node_ip="$2"
local cmd=(bash "${PROJECT_DIR}/deploy.sh" "${COMMAND}" "${node_ip}")
echo "==> ${PROJECT_NAME}: ${COMMAND} on ${node_name} (${node_ip})"
if [[ "${DRY_RUN}" -eq 1 ]]; then
printf 'DRY-RUN:'
printf ' %q' "${cmd[@]}"
echo
return
fi
(cd "${PROJECT_DIR}" && "${cmd[@]}")
}
parse_args() {
if [[ $# -lt 1 ]]; then
usage
exit 1
fi
PROJECT_NAME="$1"
shift
if [[ $# -gt 0 ]]; then
case "$1" in
install|uninstall|status|start|restart|stop)
COMMAND="$1"
shift
;;
esac
fi
while [[ $# -gt 0 ]]; do
case "$1" in
--cluster)
CLUSTER_NAME="$2"
shift 2
;;
--node)
NODE_FILTERS+=("$2")
shift 2
;;
--user)
REMOTE_USER="$2"
shift 2
;;
--dry-run)
DRY_RUN=1
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "ERROR: unknown option: $1" >&2
usage
exit 1
;;
esac
done
}
main() {
parse_args "$@"
require_config
require_project
load_targets
filter_targets
echo "Project: ${PROJECT_NAME}"
echo "Mode: ${DEPLOY_MODE}"
echo "Command: ${COMMAND}"
echo "Cluster: ${CLUSTER_NAME}"
echo "Targets:"
printf ' %s\n' "${TARGETS[@]}"
echo
local entry=""
local node_name=""
local node_ip=""
for entry in "${TARGETS[@]}"; do
node_name="${entry%%$'\t'*}"
node_ip="${entry#*$'\t'}"
if [[ "${DEPLOY_MODE}" == "setup" ]]; then
run_setup_project "${node_name}" "${node_ip}"
else
run_deploy_project "${node_name}" "${node_ip}"
fi
echo
done
}
main "$@"