feat: add guided unified installer
This commit is contained in:
parent
7583b1317c
commit
1db48b9665
4 changed files with 369 additions and 112 deletions
|
|
@ -10,19 +10,25 @@ readonly INNER_ASSET="ochenstarik-server-monitor-manager.sh"
|
||||||
RELEASE_TAG="${SMM_TAG:-$DEFAULT_RELEASE_TAG}"
|
RELEASE_TAG="${SMM_TAG:-$DEFAULT_RELEASE_TAG}"
|
||||||
REPOSITORY="${SMM_REPOSITORY:-$DEFAULT_REPOSITORY}"
|
REPOSITORY="${SMM_REPOSITORY:-$DEFAULT_REPOSITORY}"
|
||||||
CACHE_DIR="${SMM_CACHE_DIR:-${XDG_CACHE_HOME:-$HOME/.cache}/server-monitor-manager}"
|
CACHE_DIR="${SMM_CACHE_DIR:-${XDG_CACHE_HOME:-$HOME/.cache}/server-monitor-manager}"
|
||||||
|
interactive=0
|
||||||
|
node_code=""
|
||||||
|
node_control_url=""
|
||||||
|
node_hub_endpoint=""
|
||||||
|
node_ca_fingerprint=""
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
cat <<'USAGE'
|
cat <<'USAGE'
|
||||||
Usage:
|
Usage:
|
||||||
|
smm-setup.sh
|
||||||
smm-setup.sh [--tag TAG] [--repository OWNER/REPO] COMMAND [ARG...]
|
smm-setup.sh [--tag TAG] [--repository OWNER/REPO] COMMAND [ARG...]
|
||||||
|
|
||||||
Convenience installation commands:
|
With no arguments, an interactive terminal guides a complete Hub or Node
|
||||||
|
installation. Non-interactive commands remain available:
|
||||||
install-hub PUBLIC_HOST [HTTPS_PORT] [WG_PORT]
|
install-hub PUBLIC_HOST [HTTPS_PORT] [WG_PORT]
|
||||||
install-node
|
install-node
|
||||||
|
|
||||||
Other commands are passed to the verified ochenstarik-server-monitor-manager.sh
|
Other commands are passed to the verified ochenstarik-server-monitor-manager.sh
|
||||||
asset from the selected immutable GitHub release. Use -- before a command to
|
asset. Use -- before a command to force pass-through. Common commands:
|
||||||
force pass-through. Common bootstrap commands:
|
|
||||||
install-agent | install-control | uninstall-agent | uninstall-control
|
install-agent | install-control | uninstall-agent | uninstall-control
|
||||||
backup-create | backup-restore | version
|
backup-create | backup-restore | version
|
||||||
|
|
||||||
|
|
@ -33,159 +39,270 @@ Environment overrides:
|
||||||
USAGE
|
USAGE
|
||||||
}
|
}
|
||||||
|
|
||||||
die() {
|
no_tty_help() {
|
||||||
printf '%s: %s\n' "$PROGRAM_NAME" "$*" >&2
|
cat >&2 <<'HELP'
|
||||||
exit 1
|
smm-setup: interactive installation requires a terminal on stdin and stdout.
|
||||||
|
For automation, choose an explicit command:
|
||||||
|
sudo ./smm-setup.sh install-hub PUBLIC_HOST [HTTPS_PORT] [WG_PORT]
|
||||||
|
sudo ./smm-setup.sh install-node
|
||||||
|
Run ./smm-setup.sh --help for pass-through commands.
|
||||||
|
HELP
|
||||||
}
|
}
|
||||||
|
|
||||||
require_command() {
|
die() { printf '%s: %s\n' "$PROGRAM_NAME" "$*" >&2; exit 1; }
|
||||||
command -v "$1" >/dev/null 2>&1 || die "required command is unavailable: $1"
|
require_command() { command -v "$1" >/dev/null 2>&1 || die "required command is unavailable: $1"; }
|
||||||
|
|
||||||
|
validate_port() {
|
||||||
|
[[ "$1" =~ ^[0-9]{1,5}$ ]] && (( 10#$1 >= 1 && 10#$1 <= 65535 )) \
|
||||||
|
|| die "invalid port: $1"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validate_public_host() {
|
||||||
|
[[ "$1" != *:* && "$1" =~ ^[A-Za-z0-9]([A-Za-z0-9.-]{0,251}[A-Za-z0-9])?$ ]] \
|
||||||
|
|| die "invalid public address: $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
prompt_default() {
|
||||||
|
local prompt="$1" default="$2" value
|
||||||
|
read -r -p "$prompt [$default]: " value
|
||||||
|
printf '%s' "${value:-$default}"
|
||||||
|
}
|
||||||
|
|
||||||
|
confirm() {
|
||||||
|
local prompt="$1" answer
|
||||||
|
read -r -p "$prompt [y/N]: " answer
|
||||||
|
[[ "$answer" == "y" || "$answer" == "Y" || "$answer" == "yes" || "$answer" == "YES" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
os_value() {
|
||||||
|
local key="$1"
|
||||||
|
[[ -r /etc/os-release ]] || return 0
|
||||||
|
sed -n "s/^${key}=//p" /etc/os-release | head -n1 | sed 's/^"//;s/"$//'
|
||||||
|
}
|
||||||
|
|
||||||
|
detect_public_address() {
|
||||||
|
command -v curl >/dev/null 2>&1 || return 0
|
||||||
|
curl -fsS --max-time 5 https://api.ipify.org 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
role_status() {
|
||||||
|
local role="$1" binary
|
||||||
|
case "$role" in
|
||||||
|
Hub) binary=/usr/local/lib/ochenstarik-server-monitor-manager/control/ochenstarik-smm-control ;;
|
||||||
|
Node) binary=/usr/local/lib/ochenstarik-server-monitor-manager/agent/ochenstarik-smm-agent ;;
|
||||||
|
esac
|
||||||
|
[[ -x "$binary" ]] && printf 'installed' || printf 'not installed'
|
||||||
|
}
|
||||||
|
|
||||||
|
show_machine() {
|
||||||
|
local distro version architecture public_address
|
||||||
|
distro="$(os_value NAME)"
|
||||||
|
version="$(os_value VERSION_ID)"
|
||||||
|
architecture="$(uname -m)"
|
||||||
|
public_address="$(detect_public_address)"
|
||||||
|
printf '\nServer Monitor Manager setup\n'
|
||||||
|
printf ' System: %s %s\n' "${distro:-unknown Linux}" "${version:-unknown}"
|
||||||
|
printf ' Architecture: %s\n' "$architecture"
|
||||||
|
printf ' Public address: %s\n' "${public_address:-not detected}"
|
||||||
|
printf ' Hub: %s\n' "$(role_status Hub)"
|
||||||
|
printf ' Node: %s\n\n' "$(role_status Node)"
|
||||||
|
DETECTED_PUBLIC_ADDRESS="$public_address"
|
||||||
|
}
|
||||||
|
|
||||||
|
install_dependencies() {
|
||||||
|
local item missing=()
|
||||||
|
for item in curl sha256sum mktemp openssl base64 install; do
|
||||||
|
command -v "$item" >/dev/null 2>&1 || missing+=("$item")
|
||||||
|
done
|
||||||
|
if (( ${#missing[@]} > 0 )); then
|
||||||
|
[[ ${EUID:-$(id -u)} -eq 0 ]] || die "run with sudo to install dependencies: ${missing[*]}"
|
||||||
|
command -v apt-get >/dev/null 2>&1 || die "install required commands manually: ${missing[*]}"
|
||||||
|
printf 'Installing required system packages...\n'
|
||||||
|
DEBIAN_FRONTEND=noninteractive apt-get update
|
||||||
|
DEBIAN_FRONTEND=noninteractive apt-get install -y ca-certificates curl coreutils openssl
|
||||||
|
fi
|
||||||
|
printf 'Dependencies are ready. The verified bootstrap will provision cosign if needed.\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
base64url_decode() {
|
||||||
|
local encoded="$1" padding
|
||||||
|
[[ -n "$encoded" && "$encoded" =~ ^[A-Za-z0-9_-]+$ ]] || return 1
|
||||||
|
encoded="${encoded//-/+}"
|
||||||
|
encoded="${encoded//_/\/}"
|
||||||
|
case $(( ${#encoded} % 4 )) in
|
||||||
|
0) padding="" ;; 2) padding="==" ;; 3) padding="=" ;; *) return 1 ;;
|
||||||
|
esac
|
||||||
|
printf '%s%s' "$encoded" "$padding" | base64 --decode 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
inspect_node_code() {
|
||||||
|
local code="$1" parts=() ca_file hub_key node_address mesh_network part
|
||||||
|
local control_url_pattern='^https://(\[[0-9A-Fa-f:.]+\]|[A-Za-z0-9.-]+)(:[0-9]{1,5})?/?$'
|
||||||
|
IFS='.' read -r -a parts <<<"$code"
|
||||||
|
[[ ${#parts[@]} -eq 9 && "${parts[0]}" == "SMMNODE2" ]] \
|
||||||
|
|| die "invalid SMMNODE2 code: expected exactly nine segments"
|
||||||
|
for part in "${parts[@]:1}"; do [[ -n "$part" ]] || die "invalid SMMNODE2 code: empty segment"; done
|
||||||
|
node_control_url="$(base64url_decode "${parts[1]}")" || die "invalid SMMNODE2 Control URL encoding"
|
||||||
|
node_hub_endpoint="$(base64url_decode "${parts[5]}")" || die "invalid SMMNODE2 Hub endpoint encoding"
|
||||||
|
hub_key="$(base64url_decode "${parts[6]}")" || die "invalid SMMNODE2 Hub key encoding"
|
||||||
|
node_address="$(base64url_decode "${parts[7]}")" || die "invalid SMMNODE2 node address encoding"
|
||||||
|
mesh_network="$(base64url_decode "${parts[8]}")" || die "invalid SMMNODE2 network encoding"
|
||||||
|
[[ "$node_control_url" =~ $control_url_pattern ]] || die "invalid Control URL in SMMNODE2 code"
|
||||||
|
[[ "$node_hub_endpoint" =~ ^[A-Za-z0-9.-]+:[0-9]{1,5}$ ]] || die "invalid Hub endpoint in SMMNODE2 code"
|
||||||
|
validate_port "${node_hub_endpoint##*:}"
|
||||||
|
[[ "$hub_key" =~ ^[A-Za-z0-9+/]{43}=$ ]] || die "invalid Hub public key in SMMNODE2 code"
|
||||||
|
[[ "$node_address" =~ ^10\.77\.0\.([2-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-4])$ ]] \
|
||||||
|
|| die "invalid mesh address in SMMNODE2 code"
|
||||||
|
[[ "$mesh_network" == "10.77.0.0/24" ]] || die "invalid mesh network in SMMNODE2 code"
|
||||||
|
ca_file="$(mktemp -t smm-setup-ca.XXXXXXXX.crt)"
|
||||||
|
chmod 0600 "$ca_file"
|
||||||
|
if ! base64url_decode "${parts[2]}" >"$ca_file" \
|
||||||
|
|| ! openssl x509 -in "$ca_file" -noout >/dev/null 2>&1; then
|
||||||
|
rm -f -- "$ca_file"
|
||||||
|
die "invalid or tampered CA certificate in SMMNODE2 code"
|
||||||
|
fi
|
||||||
|
node_ca_fingerprint="$(openssl x509 -in "$ca_file" -noout -fingerprint -sha256 | cut -d= -f2)"
|
||||||
|
rm -f -- "$ca_file"
|
||||||
|
}
|
||||||
|
|
||||||
|
choose_interactive_action() {
|
||||||
|
local role host https_port wg_port existing default_host
|
||||||
|
show_machine
|
||||||
|
read -r -p 'Choose role: 1) Hub 2) Node: ' role
|
||||||
|
case "${role,,}" in
|
||||||
|
1|hub)
|
||||||
|
action=install-hub; existing="$(role_status Hub)"
|
||||||
|
default_host="${DETECTED_PUBLIC_ADDRESS:-hub.example.com}"
|
||||||
|
[[ -n "$default_host" ]] || default_host=hub.example.com
|
||||||
|
host="$(prompt_default 'Public IPv4 address or DNS name' "$default_host")"
|
||||||
|
https_port="$(prompt_default 'Control HTTPS port' 7443)"
|
||||||
|
wg_port="$(prompt_default 'WireGuard UDP port' 51820)"
|
||||||
|
validate_public_host "$host"; validate_port "$https_port"; validate_port "$wg_port"
|
||||||
|
action_args=("$host" "$https_port" "$wg_port")
|
||||||
|
;;
|
||||||
|
2|node)
|
||||||
|
action=install-node; existing="$(role_status Node)"
|
||||||
|
read -r -s -p 'Paste SMMNODE2 code: ' node_code; printf '\n'
|
||||||
|
[[ -n "$node_code" ]] || die "SMMNODE2 code is empty"
|
||||||
|
action_args=()
|
||||||
|
;;
|
||||||
|
*) die "choose Hub (1) or Node (2)" ;;
|
||||||
|
esac
|
||||||
|
if [[ "$existing" == "installed" ]] && ! confirm "This role is already installed. Reinstall or update it?"; then
|
||||||
|
printf 'No changes were made.\n'; exit 0
|
||||||
|
fi
|
||||||
|
install_dependencies
|
||||||
|
if [[ "$action" == install-node ]]; then
|
||||||
|
inspect_node_code "$node_code"
|
||||||
|
printf '\nNode enrollment details:\n Control: %s\n WireGuard Hub: %s\n CA SHA-256: %s\n' \
|
||||||
|
"$node_control_url" "$node_hub_endpoint" "$node_ca_fingerprint"
|
||||||
|
confirm 'Do these Hub and CA fingerprint values match the operator application?' \
|
||||||
|
|| die "installation cancelled: Hub identity was not confirmed"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
original_count=$#
|
||||||
pass_through=0
|
pass_through=0
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
--tag)
|
--tag) [[ $# -ge 2 ]] || die '--tag requires a value'; RELEASE_TAG="$2"; shift 2 ;;
|
||||||
[[ $# -ge 2 ]] || die '--tag requires a value'
|
--repository) [[ $# -ge 2 ]] || die '--repository requires a value'; REPOSITORY="$2"; shift 2 ;;
|
||||||
RELEASE_TAG="$2"
|
-h|--help) usage; exit 0 ;;
|
||||||
shift 2
|
--) pass_through=1; shift; break ;;
|
||||||
;;
|
-*) die "unknown option: $1" ;;
|
||||||
--repository)
|
*) break ;;
|
||||||
[[ $# -ge 2 ]] || die '--repository requires a value'
|
|
||||||
REPOSITORY="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
-h|--help)
|
|
||||||
usage
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
--)
|
|
||||||
pass_through=1
|
|
||||||
shift
|
|
||||||
break
|
|
||||||
;;
|
|
||||||
-*)
|
|
||||||
die "unknown option: $1"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
break
|
|
||||||
;;
|
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
[[ $# -gt 0 ]] || {
|
action_args=()
|
||||||
usage >&2
|
action=""
|
||||||
exit 2
|
if (( original_count == 0 )); then
|
||||||
}
|
[[ -t 0 && -t 1 ]] || { no_tty_help; exit 2; }
|
||||||
[[ "$RELEASE_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.-]+)?$ ]] \
|
interactive=1
|
||||||
|| die "invalid release tag: $RELEASE_TAG"
|
choose_interactive_action
|
||||||
[[ "$REPOSITORY" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]] \
|
else
|
||||||
|| die "invalid repository: $REPOSITORY"
|
[[ $# -gt 0 ]] || die "a command is required after options"
|
||||||
|
action="$1"; shift; action_args=("$@")
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ "$RELEASE_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.-]+)?$ ]] || die "invalid release tag: $RELEASE_TAG"
|
||||||
|
[[ "$REPOSITORY" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]] || die "invalid repository: $REPOSITORY"
|
||||||
|
|
||||||
action="$1"
|
|
||||||
shift
|
|
||||||
if (( pass_through == 0 )); then
|
if (( pass_through == 0 )); then
|
||||||
case "$action" in
|
case "$action" in
|
||||||
install-hub)
|
install-hub) [[ ${#action_args[@]} -ge 1 && ${#action_args[@]} -le 3 ]] || die "install-hub requires PUBLIC_HOST [HTTPS_PORT] [WG_PORT]" ;;
|
||||||
[[ $# -ge 1 && $# -le 3 ]] \
|
install-node) [[ ${#action_args[@]} -eq 0 ]] || die "install-node takes no arguments" ;;
|
||||||
|| die "install-hub requires PUBLIC_HOST [HTTPS_PORT] [WG_PORT]"
|
|
||||||
;;
|
|
||||||
install-node)
|
|
||||||
[[ $# -eq 0 ]] || die "install-node takes no arguments"
|
|
||||||
;;
|
|
||||||
esac
|
esac
|
||||||
fi
|
fi
|
||||||
|
|
||||||
require_command curl
|
require_command curl; require_command sha256sum; require_command mktemp
|
||||||
require_command sha256sum
|
|
||||||
require_command mktemp
|
|
||||||
|
|
||||||
release_base="https://github.com/$REPOSITORY/releases/download/$RELEASE_TAG"
|
release_base="https://github.com/$REPOSITORY/releases/download/$RELEASE_TAG"
|
||||||
cache_release="$CACHE_DIR/$REPOSITORY/$RELEASE_TAG"
|
cache_release="$CACHE_DIR/$REPOSITORY/$RELEASE_TAG"
|
||||||
cached_script="$cache_release/$INNER_ASSET"
|
cached_script="$cache_release/$INNER_ASSET"
|
||||||
cached_checksum="$cache_release/$INNER_ASSET.sha256"
|
cached_checksum="$cache_release/$INNER_ASSET.sha256"
|
||||||
mkdir -p "$cache_release"
|
mkdir -p "$cache_release"
|
||||||
|
|
||||||
temporary_directory="$(mktemp -d -t smm-setup.XXXXXXXX)"
|
temporary_directory="$(mktemp -d -t smm-setup.XXXXXXXX)"
|
||||||
trap 'rm -rf -- "$temporary_directory"' EXIT
|
trap 'rm -rf -- "$temporary_directory"' EXIT
|
||||||
|
|
||||||
curl -fsSL "$release_base/$INNER_ASSET" -o "$temporary_directory/$INNER_ASSET"
|
download() {
|
||||||
curl -fsSL "$release_base/$INNER_ASSET.sha256" -o "$temporary_directory/$INNER_ASSET.sha256"
|
local url="$1" destination="$2"
|
||||||
|
if (( interactive == 1 )); then
|
||||||
(
|
printf 'Downloading %s\n' "${url##*/}"
|
||||||
cd "$temporary_directory"
|
curl -fL --progress-bar "$url" -o "$destination"
|
||||||
sha256sum -c "$INNER_ASSET.sha256" >/dev/null
|
else
|
||||||
) || die "checksum verification failed for $RELEASE_TAG/$INNER_ASSET"
|
curl -fsSL "$url" -o "$destination"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
download "$release_base/$INNER_ASSET" "$temporary_directory/$INNER_ASSET"
|
||||||
|
download "$release_base/$INNER_ASSET.sha256" "$temporary_directory/$INNER_ASSET.sha256"
|
||||||
|
( cd "$temporary_directory"; sha256sum -c "$INNER_ASSET.sha256" >/dev/null ) \
|
||||||
|
|| die "checksum verification failed for $RELEASE_TAG/$INNER_ASSET"
|
||||||
install -m 0755 "$temporary_directory/$INNER_ASSET" "$cached_script"
|
install -m 0755 "$temporary_directory/$INNER_ASSET" "$cached_script"
|
||||||
install -m 0644 "$temporary_directory/$INNER_ASSET.sha256" "$cached_checksum"
|
install -m 0644 "$temporary_directory/$INNER_ASSET.sha256" "$cached_checksum"
|
||||||
|
|
||||||
download_required_asset() {
|
download_required_asset() {
|
||||||
local asset="$1"
|
local asset="$1"
|
||||||
if ! curl -fsSL "$release_base/$asset" -o "$temporary_directory/$asset"; then
|
download "$release_base/$asset" "$temporary_directory/$asset" || die "required release asset is unavailable: $asset"
|
||||||
case "$asset" in
|
|
||||||
server-monitor-manager-manifest.json|server-monitor-manager-manifest.sig|server-monitor-manager-manifest.pem)
|
|
||||||
die "required signed-release asset is unavailable: $asset"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
die "required release asset is unavailable: $asset"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
download_platform_release() {
|
download_platform_release() {
|
||||||
local platform archive_asset asset
|
local platform archive_asset asset
|
||||||
case "$(uname -m)" in
|
case "$(uname -m)" in x86_64) platform="linux-x64" ;; aarch64|arm64) platform="linux-arm64" ;; *) die "unsupported architecture: $(uname -m)" ;; esac
|
||||||
x86_64) platform="linux-x64" ;;
|
|
||||||
aarch64|arm64) platform="linux-arm64" ;;
|
|
||||||
*) die "unsupported architecture: $(uname -m)" ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
archive_asset="server-monitor-manager-$platform.tar.gz"
|
archive_asset="server-monitor-manager-$platform.tar.gz"
|
||||||
for asset in \
|
for asset in "$archive_asset" "$archive_asset.sha256" server-monitor-manager-manifest.json \
|
||||||
"$archive_asset" \
|
server-monitor-manager-manifest.sig server-monitor-manager-manifest.pem; do
|
||||||
"$archive_asset.sha256" \
|
|
||||||
server-monitor-manager-manifest.json \
|
|
||||||
server-monitor-manager-manifest.sig \
|
|
||||||
server-monitor-manager-manifest.pem; do
|
|
||||||
download_required_asset "$asset"
|
download_required_asset "$asset"
|
||||||
done
|
done
|
||||||
|
( cd "$temporary_directory"; sha256sum -c "$archive_asset.sha256" >/dev/null ) \
|
||||||
(
|
|| die "checksum verification failed for $RELEASE_TAG/$archive_asset"
|
||||||
cd "$temporary_directory"
|
|
||||||
sha256sum -c "$archive_asset.sha256" >/dev/null
|
|
||||||
) || die "checksum verification failed for $RELEASE_TAG/$archive_asset"
|
|
||||||
downloaded_archive="$temporary_directory/$archive_asset"
|
downloaded_archive="$temporary_directory/$archive_asset"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (( pass_through == 1 )); then
|
if (( pass_through == 1 )); then exec "$cached_script" "$action" "${action_args[@]}"; fi
|
||||||
exec "$cached_script" "$action" "$@"
|
|
||||||
fi
|
|
||||||
|
|
||||||
case "$action" in
|
case "$action" in
|
||||||
install-hub)
|
install-hub)
|
||||||
download_platform_release
|
download_platform_release; archive="$downloaded_archive"
|
||||||
archive="$downloaded_archive"
|
public_host="${action_args[0]}"; https_port="${action_args[1]:-}"; wg_port="${action_args[2]:-}"
|
||||||
public_host="$1"
|
if [[ -n "$https_port" ]]; then "$cached_script" install-control "$archive" "$public_host" "$https_port"; else "$cached_script" install-control "$archive" "$public_host"; fi
|
||||||
https_port="${2:-}"
|
if [[ -n "$wg_port" ]]; then "$cached_script" mesh-init "$public_host" "$wg_port"; else "$cached_script" mesh-init "$public_host"; fi
|
||||||
wg_port="${3:-}"
|
if (( interactive == 1 )); then
|
||||||
if [[ -n "$https_port" ]]; then
|
printf '\nHub installation is complete. Insert this device registration code into the operator application:\n'
|
||||||
"$cached_script" install-control "$archive" "$public_host" "$https_port"
|
"$cached_script" control-device-code operator
|
||||||
else
|
printf 'Hub CA SHA-256 fingerprint (verify it in the application):\n'
|
||||||
"$cached_script" install-control "$archive" "$public_host"
|
"$cached_script" control-ca-fingerprint
|
||||||
fi
|
fi
|
||||||
if [[ -n "$wg_port" ]]; then
|
|
||||||
exec "$cached_script" mesh-init "$public_host" "$wg_port"
|
|
||||||
fi
|
|
||||||
exec "$cached_script" mesh-init "$public_host"
|
|
||||||
;;
|
;;
|
||||||
install-node)
|
install-node)
|
||||||
download_platform_release
|
download_platform_release; archive="$downloaded_archive"
|
||||||
archive="$downloaded_archive"
|
if (( interactive == 1 )); then
|
||||||
|
SMM_ENROLL_CODE="$node_code" SMM_ACCEPT_CA_FINGERPRINT=1 "$cached_script" install-node "$archive"
|
||||||
|
node_code=""
|
||||||
|
printf '\nNode installation is complete. Insert the SMMPEER1 code printed above into the operator application for this Node.\n'
|
||||||
|
else
|
||||||
exec "$cached_script" install-node "$archive"
|
exec "$cached_script" install-node "$archive"
|
||||||
|
fi
|
||||||
;;
|
;;
|
||||||
*)
|
*) exec "$cached_script" "$action" "${action_args[@]}" ;;
|
||||||
exec "$cached_script" "$action" "$@"
|
|
||||||
;;
|
|
||||||
esac
|
esac
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,19 @@ sha256sum -c smm-setup.sh.sha256
|
||||||
chmod 700 smm-setup.sh
|
chmod 700 smm-setup.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
На Hub установка Control и инициализация Mesh выполняются одной командой. Порты можно не указывать: по умолчанию используются HTTPS `7443` и WireGuard `51820`.
|
Для обычной установки запустите один и тот же файл без аргументов на каждой машине:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo ./smm-setup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Интерактивный режим показывает дистрибутив, архитектуру, определённый публичный адрес и уже установленные роли, затем предлагает выбрать Hub или Node. Для Hub он предлагает публичный адрес и порты HTTPS `7443`/WireGuard `51820`. Для Node он принимает готовый `SMMNODE2`, показывает извлечённые адрес Hub и SHA-256 fingerprint CA и продолжает только после подтверждения оператора. Загрузки сопровождаются индикатором; системные зависимости и закреплённый `cosign` устанавливаются и проверяются до использования.
|
||||||
|
|
||||||
|
После установки Hub установщик печатает код регистрации устройства `SMMDEV1-...` и fingerprint CA — оба значения предназначены для подключения приложения оператора. После установки Node он печатает `SMMPEER1...`; вставьте этот код в карточку соответствующего Node в приложении оператора.
|
||||||
|
|
||||||
|
Повторный запуск распознаёт установленную роль и просит явно подтвердить переустановку/обновление. Если stdin или stdout не являются терминалом, режим без аргументов завершается с подсказкой и ничего не меняет.
|
||||||
|
|
||||||
|
Для автоматизации прежние команды сохранены. На Hub установка Control и инициализация Mesh выполняются одной командой:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo ./smm-setup.sh install-hub hub.example.com 7443 51820
|
sudo ./smm-setup.sh install-hub hub.example.com 7443 51820
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,16 @@ bootstrap="$root/deploy/ochenstarik-server-monitor-manager.sh"
|
||||||
helper="$root/deploy/ochenstarik-smm-policy-apply"
|
helper="$root/deploy/ochenstarik-smm-policy-apply"
|
||||||
emergency="$root/deploy/ochenstarik-smm-emergency"
|
emergency="$root/deploy/ochenstarik-smm-emergency"
|
||||||
acceptance="$root/tests/acceptance/three-server-mesh.sh"
|
acceptance="$root/tests/acceptance/three-server-mesh.sh"
|
||||||
|
unified_setup="$root/deploy/smm-setup.sh"
|
||||||
|
unified_test="$root/tests/bootstrap/test-unified-installer.sh"
|
||||||
|
|
||||||
|
bash -n "$unified_setup" "$unified_test"
|
||||||
|
if command -v shellcheck >/dev/null 2>&1; then
|
||||||
|
shellcheck --severity=error "$unified_setup" "$unified_test"
|
||||||
|
fi
|
||||||
|
if [[ "$(uname -s)" != MINGW* ]]; then
|
||||||
|
bash "$unified_test"
|
||||||
|
fi
|
||||||
|
|
||||||
grep -Fq 'listing="$(/usr/sbin/nft -a list chain' "$helper" || {
|
grep -Fq 'listing="$(/usr/sbin/nft -a list chain' "$helper" || {
|
||||||
printf '%s\n' "policy status probe must fail closed when nftables cannot be inspected" >&2
|
printf '%s\n' "policy status probe must fail closed when nftables cannot be inspected" >&2
|
||||||
|
|
|
||||||
118
tests/bootstrap/test-unified-installer.sh
Normal file
118
tests/bootstrap/test-unified-installer.sh
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
IFS=$'\n\t'
|
||||||
|
|
||||||
|
root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
setup="$root/deploy/smm-setup.sh"
|
||||||
|
fixture="$(mktemp -d -t smm-unified-installer.XXXXXXXX)"
|
||||||
|
trap 'rm -rf -- "$fixture"' EXIT
|
||||||
|
mkdir -p "$fixture/bin" "$fixture/release" "$fixture/cache"
|
||||||
|
|
||||||
|
cat >"$fixture/release/ochenstarik-server-monitor-manager.sh" <<'INNER'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
case "$1" in
|
||||||
|
install-control) printf 'FAKE_INSTALL_CONTROL=%s,%s\n' "$3" "$4" ;;
|
||||||
|
mesh-init) printf 'FAKE_MESH_INIT=%s,%s\n' "$2" "$3" ;;
|
||||||
|
control-device-code) [[ "$2" == operator ]]; printf '%s\n' 'SMMDEV1-test-device-code' ;;
|
||||||
|
control-ca-fingerprint) printf '%s\n' 'AA:BB:CC:DD' ;;
|
||||||
|
install-node)
|
||||||
|
[[ -n "${SMM_ENROLL_CODE:-}" && "${SMM_ACCEPT_CA_FINGERPRINT:-}" == 1 ]]
|
||||||
|
printf '%s\n' 'SMMPEER1.test-node.test-address.test-key'
|
||||||
|
;;
|
||||||
|
*) printf 'PASSTHROUGH=%s\n' "$*" ;;
|
||||||
|
esac
|
||||||
|
INNER
|
||||||
|
chmod 0755 "$fixture/release/ochenstarik-server-monitor-manager.sh"
|
||||||
|
(
|
||||||
|
cd "$fixture/release"
|
||||||
|
sha256sum ochenstarik-server-monitor-manager.sh >ochenstarik-server-monitor-manager.sh.sha256
|
||||||
|
printf '%s' archive >server-monitor-manager-linux-x64.tar.gz
|
||||||
|
sha256sum server-monitor-manager-linux-x64.tar.gz >server-monitor-manager-linux-x64.tar.gz.sha256
|
||||||
|
printf '%s' manifest >server-monitor-manager-manifest.json
|
||||||
|
printf '%s' signature >server-monitor-manager-manifest.sig
|
||||||
|
printf '%s' certificate >server-monitor-manager-manifest.pem
|
||||||
|
)
|
||||||
|
|
||||||
|
cat >"$fixture/bin/curl" <<'CURL'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
destination=""
|
||||||
|
url=""
|
||||||
|
while (( $# > 0 )); do
|
||||||
|
case "$1" in
|
||||||
|
-o) destination="$2"; shift 2 ;;
|
||||||
|
http*) url="$1"; shift ;;
|
||||||
|
*) shift ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
if [[ "$url" == https://api.ipify.org ]]; then
|
||||||
|
printf '%s' '203.0.113.10'
|
||||||
|
elif [[ -n "$destination" ]]; then
|
||||||
|
cp "$FIXTURE_RELEASE/${url##*/}" "$destination"
|
||||||
|
else
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
CURL
|
||||||
|
chmod 0755 "$fixture/bin/curl"
|
||||||
|
|
||||||
|
openssl req -x509 -newkey rsa:2048 -nodes -days 1 -subj /CN=fixture-ca \
|
||||||
|
-keyout "$fixture/ca.key" -out "$fixture/ca.crt" >/dev/null 2>&1
|
||||||
|
|
||||||
|
b64url() { base64 -w 0 | tr '+/' '-_' | tr -d '='; }
|
||||||
|
control_part="$(printf '%s' 'https://hub.example:7443' | b64url)"
|
||||||
|
ca_part="$(b64url <"$fixture/ca.crt")"
|
||||||
|
node_part="$(printf '%s' 'fixture-node' | b64url)"
|
||||||
|
token_part="$(printf '%s' 'fixture-token' | b64url)"
|
||||||
|
endpoint_part="$(printf '%s' 'hub.example:51820' | b64url)"
|
||||||
|
hub_key_part="$(printf '%s' 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=' | b64url)"
|
||||||
|
address_part="$(printf '%s' '10.77.0.2' | b64url)"
|
||||||
|
network_part="$(printf '%s' '10.77.0.0/24' | b64url)"
|
||||||
|
valid_code="SMMNODE2.$control_part.$ca_part.$node_part.$token_part.$endpoint_part.$hub_key_part.$address_part.$network_part"
|
||||||
|
|
||||||
|
run_tty() {
|
||||||
|
local input="$1" output="$2"
|
||||||
|
if ! printf '%b' "$input" | script -qefc \
|
||||||
|
"env PATH='$fixture/bin:/usr/bin:/bin' FIXTURE_RELEASE='$fixture/release' SMM_CACHE_DIR='$fixture/cache' bash '$setup'" \
|
||||||
|
/dev/null >"$output" 2>&1; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
if bash "$setup" </dev/null >"$fixture/no-tty.out" 2>&1; then
|
||||||
|
printf '%s\n' 'no-tty interactive invocation unexpectedly succeeded' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
grep -Fq 'interactive installation requires a terminal' "$fixture/no-tty.out"
|
||||||
|
grep -Fq 'install-hub PUBLIC_HOST' "$fixture/no-tty.out"
|
||||||
|
|
||||||
|
run_tty $'1\n\n\n\n' "$fixture/hub.out"
|
||||||
|
grep -Fq 'System:' "$fixture/hub.out"
|
||||||
|
grep -Fq 'Public address: 203.0.113.10' "$fixture/hub.out"
|
||||||
|
grep -Fq 'FAKE_INSTALL_CONTROL=203.0.113.10,7443' "$fixture/hub.out"
|
||||||
|
grep -Fq 'FAKE_MESH_INIT=203.0.113.10,51820' "$fixture/hub.out"
|
||||||
|
grep -Fq 'SMMDEV1-test-device-code' "$fixture/hub.out"
|
||||||
|
grep -Fq 'AA:BB:CC:DD' "$fixture/hub.out"
|
||||||
|
|
||||||
|
run_tty "2\n$valid_code\ny\n" "$fixture/node.out"
|
||||||
|
grep -Fq 'Control: https://hub.example:7443' "$fixture/node.out"
|
||||||
|
grep -Fq 'WireGuard Hub: hub.example:51820' "$fixture/node.out"
|
||||||
|
grep -Fq 'CA SHA-256:' "$fixture/node.out"
|
||||||
|
grep -Fq 'SMMPEER1.test-node.test-address.test-key' "$fixture/node.out"
|
||||||
|
grep -Fq 'operator application' "$fixture/node.out"
|
||||||
|
|
||||||
|
tampered_ca_part="$(printf '%s' 'not-a-certificate' | b64url)"
|
||||||
|
tampered_code="SMMNODE2.$control_part.$tampered_ca_part.$node_part.$token_part.$endpoint_part.$hub_key_part.$address_part.$network_part"
|
||||||
|
for bad_code in "${valid_code%.*}" "$valid_code.extra" "$tampered_code"; do
|
||||||
|
if run_tty "2\n$bad_code\n" "$fixture/bad.out"; then
|
||||||
|
printf '%s\n' 'corrupt SMMNODE2 code unexpectedly succeeded' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
grep -Eq 'invalid|tampered' "$fixture/bad.out"
|
||||||
|
done
|
||||||
|
|
||||||
|
PATH="$fixture/bin:$PATH" FIXTURE_RELEASE="$fixture/release" SMM_CACHE_DIR="$fixture/cache" \
|
||||||
|
bash "$setup" version >"$fixture/noninteractive.out"
|
||||||
|
grep -Fq 'PASSTHROUGH=version' "$fixture/noninteractive.out"
|
||||||
|
|
||||||
|
printf '%s\n' 'UNIFIED_INSTALLER=PASS'
|
||||||
Loading…
Reference in a new issue