Files
k8scd/nix-infra-machine/cli
2026-03-17 21:00:30 +01:00

503 lines
14 KiB
Bash
Executable File

#!/usr/bin/env bash
WORK_DIR=${WORK_DIR:-"$(dirname "$0")"}
NIX_INFRA=${NIX_INFRA:-"nix-infra"}
NIXOS_VERSION=${NIXOS_VERSION:-"25.11"}
SSH_KEY=${SSH_KEY:-"nixinfra-machine"}
SSH_EMAIL=${SSH_EMAIL:-"your-email@example.com"}
SECRETS_PWD=${SECRETS_PWD:-"my_secrets_password"}
LOCATION=${LOCATION:-"nbg1"}
MACHINE_TYPE=${MACHINE_TYPE:-"cx23"}
ENV=${ENV:-"$WORK_DIR/.env"}
read -r -d '' __help_text__ <<EOF || true
nix-infra-machine CLI
=====================
Usage: $0 <command> [options]
Commands:
create <nodes> Provision and initialize machines
update <nodes> Update node configuration and deploy apps
upgrade <nodes> Upgrade NixOS version on nodes
rollback <nodes> Rollback to previous NixOS configuration
destroy Destroy machines
--target=<nodes> Nodes to destroy
ssh <node> SSH into a node
cmd Run command on node(s)
--target=<nodes> Target node(s)
<command> Command to execute
action Run app module action
--target=<node> Target node
<module> App module name
<cmd> Action command to run
port-forward Forward port from remote node to local
--target=<node> Target node to forward from
--port-mapping=<l:r> Port mapping as local:remote
claude Launch Claude with nix-infra-machine-mcp
Options:
--env=<file> Environment file (default: .env)
--target=<nodes> Target node(s) for commands
--node-module=<file> Node module file (default: node_types/standalone_machine.nix)
--port-mapping=<l:r> Port mapping as local:remote (for port-forward)
Examples:
# Create a new machine
$0 create --env=.env node001
# Create multiple machines
$0 create --env=.env node001 node002 node003
# SSH into a machine
$0 ssh --env=.env node001
# Run a command on a machine
$0 cmd --env=.env --target=node001 "systemctl status nginx"
# Update configuration
$0 update --env=.env node001
# Destroy machines
$0 destroy --env=.env --target="node001 node002"
# Launch Claude with dev MCP
$0 claude-dev --env=.env
EOF
if [[ "create upgrade rollback destroy update ssh cmd action port-forward claude claude-dev" == *"$1"* ]]; then
CMD="$1"
shift
else
echo "$__help_text__"
exit 1
fi
for i in "$@"; do
case $i in
--help)
echo "$__help_text__"
exit 0
;;
--env=*)
ENV="${i#*=}"
shift
;;
--target=*)
TARGET="${i#*=}"
shift
;;
--port-mapping=*)
PORT_MAPPING="${i#*=}"
shift
;;
--node-module=*)
NODE_MODULE="${i#*=}"
shift
;;
*)
REST="$@"
;;
esac
done
# Read the environment file if provided
if [ "$ENV" != "" ] && [ -f "$ENV" ]; then
source "$ENV"
fi
# Check for nix-infra CLI
if [ "$NIX_INFRA" = "nix-infra" ] && ! command -v nix-infra >/dev/null 2>&1; then
echo "The 'nix-infra' cli is required for this script to work."
echo "Visit https://github.com/jhsware/nix-infra for instructions on installation"
exit 1
fi
# A Hetzner Cloud token is required
if [ -z "$HCLOUD_TOKEN" ]; then
echo "Missing env-var HCLOUD_TOKEN. Load through .env-file that is specified through --env."
exit 1
fi
# ============================================================================
# Validation helpers
# ============================================================================
check_required_vars() {
local missing=""
for var in "$@"; do
if [ -z "${!var}" ]; then
missing="$missing $var"
fi
done
if [ -n "$missing" ]; then
die "Missing required environment variables:$missing
Load through .env-file specified with --env option."
fi
}
check_required_args() {
local name="$1"
local value="$2"
if [ -z "$value" ]; then
die "Missing required argument: $name"
fi
}
# ============================================================================
# Helper Functions
# ============================================================================
printTime() {
local _start=$1; local _end=$2; local _secs=$((_end-_start))
printf '%02dh:%02dm:%02ds' $((_secs/3600)) $((_secs%3600/60)) $((_secs%60))
}
destroyNodes() {
$NIX_INFRA fleet destroy -d "$WORK_DIR" --env="$ENV" --batch \
--target="$TARGET"
}
cleanupOnFail() {
if [ $1 -ne 0 ]; then
echo "$2"
destroyNodes
exit 1
fi
}
# ============================================================================
# Interactive Commands
# ============================================================================
if [ "$CMD" = "ssh" ]; then
if [ -z "$REST" ]; then
echo "Usage: $0 ssh --env=.env <node>"
exit 1
fi
$NIX_INFRA fleet ssh -d "$WORK_DIR" --env="$ENV" --target="$REST"
exit 0
fi
if [ "$CMD" = "cmd" ]; then
if [ -z "$TARGET" ] || [ -z "$REST" ]; then
echo "Usage: $0 cmd --env=.env --target=<node> <command>"
exit 1
fi
$NIX_INFRA fleet cmd -d "$WORK_DIR" --env="$ENV" --target="$TARGET" "$REST"
exit 0
fi
if [ "$CMD" = "action" ]; then
if [ -z "$TARGET" ] || [ -z "$REST" ]; then
echo "Usage: $0 action --env=.env --target=<node> <module> <cmd>"
exit 1
fi
read -r module action_cmd <<< "$REST"
$NIX_INFRA fleet action -d "$WORK_DIR" --env="$ENV" --target="$TARGET" --app-module="$module" \
--cmd="$action_cmd"
exit 0
fi
if [ "$CMD" = "port-forward" ]; then
if [ -z "$TARGET" ] || [ -z "$PORT_MAPPING" ]; then
echo "Usage: $0 port-forward --env=.env --target=<node> --port-mapping=<local:remote>"
exit 1
fi
OLD_IFS=$IFS
IFS=: read LOCAL_PORT REMOTE_PORT <<< "$PORT_MAPPING"
IFS=$OLD_IFS
$NIX_INFRA fleet port-forward -d "$WORK_DIR" --env="$ENV" \
--target="$TARGET" \
--local-port="$LOCAL_PORT" \
--remote-port="$REMOTE_PORT"
exit 0
fi
# ============================================================================
# Fleet Management Commands
# ============================================================================
if [ "$CMD" = "destroy" ]; then
if [ -z "$TARGET" ]; then
echo "Usage: $0 destroy --env=.env --target=\"<node1> <node2> ...\""
exit 1
fi
destroyNodes
exit 0
fi
if [ "$CMD" = "update" ]; then
if [ -z "$REST" ]; then
echo "Usage: $0 update --env=.env <node1> <node2> ..."
exit 1
fi
NODE_MODULE=${NODE_MODULE:-"node_types/standalone_machine.nix"}
$NIX_INFRA fleet update -d "$WORK_DIR" --env="$ENV" --batch \
--nixos-version="$NIXOS_VERSION" \
--target="$REST" \
--node-module="$NODE_MODULE" \
--no-rebuild
$NIX_INFRA fleet deploy-apps -d "$WORK_DIR" --env="$ENV" --batch \
--target="$REST"
$NIX_INFRA fleet cmd -d "$WORK_DIR" --env="$ENV" --target="$REST" "nixos-rebuild switch"
exit 0
fi
if [ "$CMD" = "upgrade" ]; then
if [ -z "$REST" ]; then
echo "Usage: $0 upgrade --env=$ENV [--nixos-version='$NIXOS_VERSION'] [node1 node2 ...]"
exit 1
fi
# (cd "$WORK_DIR" && git fetch origin && git reset --hard origin/$(git branch --show-current))
$NIX_INFRA cluster upgrade-nixos -d "$WORK_DIR" --env="$ENV" --batch \
--nixos-version="$NIXOS_VERSION" \
--target="$REST"
exit 0
fi
if [ "$CMD" = "rollback" ]; then
if [ -z "$REST" ]; then
echo "Usage: $0 rollback --env=.env <node1> <node2> ..."
exit 1
fi
$NIX_INFRA fleet cmd -d "$WORK_DIR" --env="$ENV" --target="$REST" "nixos-rebuild switch --rollback"
exit 0
fi
# ============================================================================
# Create Command - Provision and Initialize Machines
# ============================================================================
if [ "$CMD" = "create" ]; then
if [ -z "$REST" ]; then
echo "Usage: $0 create --env=.env <node1> <node2> ..."
exit 1
fi
TARGET="$REST"
_start=$(date +%s)
# Initialize if not already done (check for ssh directory)
if [ ! -d "$WORK_DIR/ssh" ]; then
echo "Initializing nix-infra..."
$NIX_INFRA init -d "$WORK_DIR" --env="$ENV" --no-cert-auth --batch
fi
# Add SSH key to agent
ssh-add "$WORK_DIR/ssh/$SSH_KEY" 2>/dev/null || true
echo "*** Provisioning NixOS $NIXOS_VERSION ***"
$NIX_INFRA fleet provision -d "$WORK_DIR" --env="$ENV" --batch \
--nixos-version="$NIXOS_VERSION" \
--ssh-key="$SSH_KEY" \
--location="$LOCATION" \
--machine-type="$MACHINE_TYPE" \
--node-names="$TARGET"
cleanupOnFail $? "WARNING: Provisioning failed! Cleaning up..."
_provision=$(date +%s)
echo "*** Initializing machines ***"
NODE_MODULE=${NODE_MODULE:-"node_types/standalone_machine.nix"}
$NIX_INFRA fleet init-machine -d "$WORK_DIR" --env="$ENV" --batch \
--nixos-version="$NIXOS_VERSION" \
--target="$TARGET" \
--node-module="$NODE_MODULE"
$NIX_INFRA fleet cmd -d "$WORK_DIR" --env="$ENV" --target="$TARGET" "nixos-rebuild switch --fast"
_init_nodes=$(date +%s)
echo "*** Deploying apps ***"
$NIX_INFRA fleet deploy-apps -d "$WORK_DIR" --env="$ENV" --batch \
--target="$TARGET"
$NIX_INFRA fleet cmd -d "$WORK_DIR" --env="$ENV" --target="$TARGET" "nixos-rebuild switch --fast"
_end=$(date +%s)
echo ""
echo "=========================================="
printf '+ provision %s\n' "$(printTime $_start $_provision)"
printf '+ init nodes %s\n' "$(printTime $_provision $_init_nodes)"
printf '+ deploy apps %s\n' "$(printTime $_init_nodes $_end)"
printf '= TOTAL %s\n' "$(printTime $_start $_end)"
echo "=========================================="
echo "Done!"
exit 0
fi
if [ "$CMD" = "claude" ]; then
if [ -z "$SSH_KEY" ] || [ -z "$HCLOUD_TOKEN" ]; then
echo "You need a .env file in the working directory with SSH_KEY and HCLOUD_TOKEN defined" >&2
exit 1
fi
if ! type nix-infra-machine-mcp &> /dev/null; then
echo "nix-infra-machine-mcp is not installed" >&2
exit 1
fi
# Detect OS and set paths accordingly
case "$(uname -s)" in
Darwin)
path_to_claude="$HOME/Library/Application Support/Claude"
claude_bin="/Applications/Claude.app/Contents/MacOS/Claude"
;;
Linux)
path_to_claude="${XDG_CONFIG_HOME:-$HOME/.config}/Claude"
claude_bin="claude"
;;
*)
echo "Unsupported operating system: $(uname -s)" >&2
exit 1
;;
esac
if [ ! -d "$path_to_claude" ]; then
echo "You do not have Claude Application Support files in your user. Start the app once manually and retry." >&2
exit 1
fi
if [ -f "$path_to_claude/claude_desktop_config.json" ]; then
cp -f "$path_to_claude/claude_desktop_config.json" "$path_to_claude/claude_desktop_config.json.nix-infra.bak"
fi
# Create mcp configuration for Claude
cat > "$path_to_claude/claude_desktop_config.json" << EOF
{
"mcpServers": {
"nix-infra-machine-mcp": {
"command": "nix-infra-machine-mcp"
}
}
}
EOF
# Run claude
"$claude_bin" 2>&1
# Reset mcp configuration for Claude
if [ -f "$path_to_claude/claude_desktop_config.json.nix-infra.bak" ]; then
cp -f "$path_to_claude/claude_desktop_config.json.nix-infra.bak" "$path_to_claude/claude_desktop_config.json"
fi
fi
# ============================================================================
# MCP Support
# ============================================================================
if [ "$CMD" = "claude" ] || [ "$CMD" = "claude-dev" ]; then
check_required_vars SSH_KEY HCLOUD_TOKEN
# Detect OS and set paths accordingly
case "$(uname -s)" in
Darwin)
path_to_claude="$HOME/Library/Application Support/Claude"
claude_bin="/Applications/Claude.app/Contents/MacOS/Claude"
;;
Linux)
path_to_claude="${XDG_CONFIG_HOME:-$HOME/.config}/Claude"
claude_bin="claude"
;;
*)
die "Unsupported operating system: $(uname -s)"
;;
esac
if [ ! -d "$path_to_claude" ]; then
die "You do not have Claude Application Support files in your user. Start the app once manually and retry."
fi
# ============================================================================
# Command: claude
# ============================================================================
if [ "$CMD" = "claude" ]; then
if ! command -v nix-infra-cluster-mcp &>/dev/null; then
die "nix-infra-cluster-mcp is not installed"
fi
# Backup existing config
if [ -f "$path_to_claude/claude_desktop_config.json" ]; then
cp -f "$path_to_claude/claude_desktop_config.json" "$path_to_claude/claude_desktop_config.json.nix-infra.bak"
fi
# Create mcp configuration for Claude
cat >"$path_to_claude/claude_desktop_config.json" <<EOF
{
"mcpServers": {
"nix-infra-cluster-mcp": {
"command": "nix-infra-cluster-mcp"
}
}
}
EOF
# Run claude
"$claude_bin" 2>&1
# Reset mcp configuration for Claude
if [ -f "$path_to_claude/claude_desktop_config.json.nix-infra.bak" ]; then
cp -f "$path_to_claude/claude_desktop_config.json.nix-infra.bak" "$path_to_claude/claude_desktop_config.json"
fi
exit 0
fi
# ============================================================================
# Command: claude-dev
# ============================================================================
if [ "$CMD" = "claude-dev" ]; then
# if ! command -v nix-infra-dev-mcp &>/dev/null; then
# die "nix-infra-dev-mcp is not installed"
# fi
# Backup existing config
if [ -f "$path_to_claude/claude_desktop_config.json" ]; then
cp -f "$path_to_claude/claude_desktop_config.json" "$path_to_claude/claude_desktop_config.json.nix-infra.bak"
fi
# Create mcp configuration for Claude
cat >"$path_to_claude/claude_desktop_config.json" <<EOF
{
"mcpServers": {
"nix-infra-dev-mcp": {
"command": "nix-infra-dev-mcp"
}
}
}
EOF
# Run claude
"$claude_bin" 2>&1
# Reset mcp configuration for Claude
if [ -f "$path_to_claude/claude_desktop_config.json.nix-infra.bak" ]; then
cp -f "$path_to_claude/claude_desktop_config.json.nix-infra.bak" "$path_to_claude/claude_desktop_config.json"
fi
exit 0
fi
fi
# If we get here, command wasn't handled
die "Unknown command: $CMD"