#!/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 < [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 Cluster name in cluster-context/madagascar.json (default: madagascar) --node Restrict to one node. Can be repeated. --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 "$@"