diff --git a/.env.example b/.env.example index 1d2e96b..d7f9e01 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,3 @@ -EXPANSO_API_KEY=example +EXPANSO_CLI_API_KEY=example +EXPANSO_CLI_ENDPOINT=https://your-network.us2.cloud.expanso.io:9010 +EXPANSO_EDGE_BOOTSTRAP_TOKEN=exp_bk_example diff --git a/.gitignore b/.gitignore index d60f370..fa5e0cb 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ # Secrets: real values + the local overlay inventory. # Copy deploy/secrets.example -> deploy/secrets and fill it in. .env +.local/ deploy/secrets/ diff --git a/docs/macos-local-demo.md b/docs/macos-local-demo.md new file mode 100644 index 0000000..1ec5faf --- /dev/null +++ b/docs/macos-local-demo.md @@ -0,0 +1,104 @@ +# macOS Local Demo + +Use this path when you want to run an Expanso Edge agent directly on a Mac, +without Proxmox, Docker, or a Linux VM. + +## Prerequisites + +- macOS arm64 or amd64. +- `expanso-edge` installed at `/usr/local/bin/expanso-edge`, or set + `EXPANSO_EDGE_BIN`. +- `EXPANSO_EDGE_BOOTSTRAP_TOKEN` in `.env` for first-time bootstrap. +- Local edge credentials at `~/.expanso/edge/auth/credentials.creds`, or at + `$EXPANSO_EDGE_DATA_DIR/auth/credentials.creds` if you set a custom data + directory. + +If credentials are missing, bootstrap once: + +```bash +./scripts/run-local-macos.sh --bootstrap +``` + +## Run + +Start the local agent in the foreground: + +```bash +./scripts/run-local-macos.sh --verbose +``` + +The process stays attached to the terminal and prints connection logs. Press +Ctrl-C to stop it. + +To check prerequisites without starting the agent: + +```bash +./scripts/run-local-macos.sh --check +``` + +To keep it running in the background after the terminal exits: + +```bash +./scripts/run-local-macos.sh --background --verbose +tail -f /tmp/jetpack-expanso/edge.log +``` + +Background mode uses `tmux` so the agent keeps the pseudo-terminal it expects. + +Stop the background process with: + +```bash +./scripts/run-local-macos.sh --stop-background +``` + +## Five Local Nodes + +For a quick five-node Expanso demo on this Mac, you do not need Proxmox, +Docker, or a Linux VM. Run five isolated local agents instead: + +```bash +./scripts/run-local-cluster-macos.sh --start +./scripts/run-local-cluster-macos.sh --status +``` + +The cluster helper uses separate credentials and state for each node: + +- node 01: `.local/edge`, `localhost:9011`, tmux session + `demo-jetpack-expanso-edge` +- node 02: `.local/edge-02`, `localhost:9012`, tmux session + `demo-jetpack-expanso-edge-02` +- node 03: `.local/edge-03`, `localhost:9013`, tmux session + `demo-jetpack-expanso-edge-03` +- node 04: `.local/edge-04`, `localhost:9014`, tmux session + `demo-jetpack-expanso-edge-04` +- node 05: `.local/edge-05`, `localhost:9015`, tmux session + `demo-jetpack-expanso-edge-05` + +Stop all five local agents with: + +```bash +./scripts/run-local-cluster-macos.sh --stop +``` + +This is a control-plane cluster demo, not a hardware isolation test. The nodes +appear separately to Expanso but share the same Mac CPU, memory, disk, and +hostname. Use lightweight ARM Linux VMs only when you need OS-level isolation. + +## Verify + +Use the Expanso CLI endpoint for the same control plane. The endpoint host +matches the `network_id` in `.local/edge/config.d/50-connection.yaml`. + +```bash +set -a +source .env +set +a + +expanso-cli node list \ + --endpoint "$EXPANSO_CLI_ENDPOINT" \ + --api-key "$EXPANSO_CLI_API_KEY" \ + --wide +``` + +A connected local Mac node should show the installed agent version and node +name. The five-node helper should show five separate node names. diff --git a/scripts/run-local-cluster-macos.sh b/scripts/run-local-cluster-macos.sh new file mode 100755 index 0000000..5cf2c1e --- /dev/null +++ b/scripts/run-local-cluster-macos.sh @@ -0,0 +1,232 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +RUN_LOCAL="$SCRIPT_DIR/run-local-macos.sh" + +COUNT="${EXPANSO_EDGE_CLUSTER_COUNT:-5}" +BASE_PORT="${EXPANSO_EDGE_CLUSTER_BASE_PORT:-9010}" +DEMO_NAME="${EXPANSO_EDGE_CLUSTER_NAME:-$(basename "$REPO_ROOT")}" +TMP_DIR="${EXPANSO_EDGE_CLUSTER_TMP_DIR:-/tmp/$DEMO_NAME}" +MODE="start" +BOOTSTRAP=1 + +usage() { + cat <&2 + usage >&2 + exit 1 + ;; + esac +done + +if ! [[ "$COUNT" =~ ^[0-9]+$ ]] || [[ "$COUNT" -lt 1 ]]; then + echo "error: --count must be a positive integer" >&2 + exit 1 +fi + +if ! [[ "$BASE_PORT" =~ ^[0-9]+$ ]] || [[ "$BASE_PORT" -lt 1 ]]; then + echo "error: --base-port must be a positive integer" >&2 + exit 1 +fi + +node_label() { + printf "%02d" "$1" +} + +node_data_dir() { + local label="$1" + if [[ "$label" == "01" ]]; then + printf "%s/.local/edge" "$REPO_ROOT" + else + printf "%s/.local/edge-%s" "$REPO_ROOT" "$label" + fi +} + +node_session() { + local label="$1" + if [[ "$label" == "01" ]]; then + printf "%s-edge" "$DEMO_NAME" + else + printf "%s-edge-%s" "$DEMO_NAME" "$label" + fi +} + +node_log_file() { + local label="$1" + if [[ "$label" == "01" ]]; then + printf "%s/edge.log" "$TMP_DIR" + else + printf "%s/edge-%s.log" "$TMP_DIR" "$label" + fi +} + +node_pid_file() { + local label="$1" + if [[ "$label" == "01" ]]; then + printf "%s/edge.pid" "$TMP_DIR" + else + printf "%s/edge-%s.pid" "$TMP_DIR" "$label" + fi +} + +node_name() { + local label="$1" + if [[ "$label" == "01" ]]; then + printf "%s-local" "$DEMO_NAME" + else + printf "%s-node-%s" "$DEMO_NAME" "$label" + fi +} + +start_node() { + local index="$1" + local label data_dir session log_file pid_file port name + + label="$(node_label "$index")" + data_dir="$(node_data_dir "$label")" + session="$(node_session "$label")" + log_file="$(node_log_file "$label")" + pid_file="$(node_pid_file "$label")" + port=$((BASE_PORT + index)) + name="$(node_name "$label")" + + echo "Starting node $label: name=$name api=localhost:$port data_dir=$data_dir" + if [[ "$BOOTSTRAP" == "1" && ! -f "$data_dir/auth/credentials.creds" ]]; then + EXPANSO_EDGE_DATA_DIR="$data_dir" "$RUN_LOCAL" --bootstrap + fi + + if [[ ! -f "$data_dir/auth/credentials.creds" ]]; then + echo "error: node $label is missing credentials at $data_dir/auth/credentials.creds" >&2 + echo "Set EXPANSO_EDGE_BOOTSTRAP_TOKEN or rerun without --no-bootstrap." >&2 + exit 1 + fi + + EXPANSO_EDGE_DATA_DIR="$data_dir" \ + EXPANSO_EDGE_TMUX_SESSION="$session" \ + EXPANSO_EDGE_LOG="$log_file" \ + EXPANSO_EDGE_PID="$pid_file" \ + "$RUN_LOCAL" --background --verbose --name "$name" --api-listen "localhost:$port" +} + +stop_node() { + local index="$1" + local label data_dir session log_file pid_file + + label="$(node_label "$index")" + data_dir="$(node_data_dir "$label")" + session="$(node_session "$label")" + log_file="$(node_log_file "$label")" + pid_file="$(node_pid_file "$label")" + + EXPANSO_EDGE_DATA_DIR="$data_dir" \ + EXPANSO_EDGE_TMUX_SESSION="$session" \ + EXPANSO_EDGE_LOG="$log_file" \ + EXPANSO_EDGE_PID="$pid_file" \ + "$RUN_LOCAL" --stop-background +} + +status_node() { + local index="$1" + local label data_dir session log_file pid_file port state pid node_id + + label="$(node_label "$index")" + data_dir="$(node_data_dir "$label")" + session="$(node_session "$label")" + log_file="$(node_log_file "$label")" + pid_file="$(node_pid_file "$label")" + port=$((BASE_PORT + index)) + state="stopped" + pid="-" + node_id="-" + + if command -v tmux >/dev/null 2>&1 && tmux has-session -t "$session" 2>/dev/null; then + state="running" + fi + + if [[ -f "$pid_file" ]]; then + pid="$(cat "$pid_file")" + if ! ps -p "$pid" >/dev/null 2>&1; then + pid="$pid?" + fi + fi + + if [[ -f "$log_file" ]]; then + node_id="$(tr '\r' '\n' < "$log_file" | grep -E 'node_id=|nodeID=' | tail -n 1 | sed -E 's/.*(node_id|nodeID)=([^ ]+).*/\2/' || true)" + if [[ -z "$node_id" ]]; then + node_id="-" + fi + fi + + printf "%-5s %-8s %-8s %-16s %-40s %s\n" "$label" "$state" "$pid" "localhost:$port" "$node_id" "$data_dir" +} + +case "$MODE" in + start) + for ((i = 1; i <= COUNT; i++)); do + start_node "$i" + done + ;; + stop) + for ((i = 1; i <= COUNT; i++)); do + stop_node "$i" + done + ;; + status) + printf "%-5s %-8s %-8s %-16s %-40s %s\n" "NODE" "STATE" "PID" "API" "NODE_ID" "DATA_DIR" + for ((i = 1; i <= COUNT; i++)); do + status_node "$i" + done + ;; +esac diff --git a/scripts/run-local-macos.sh b/scripts/run-local-macos.sh new file mode 100755 index 0000000..ec28f9b --- /dev/null +++ b/scripts/run-local-macos.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +DOTENV_PATH="${EXPANSO_DOTENV:-$REPO_ROOT/.env}" + +if [[ -f "$DOTENV_PATH" ]]; then + set -a + # shellcheck disable=SC1090 + source "$DOTENV_PATH" + set +a +fi + +EDGE_BIN="${EXPANSO_EDGE_BIN:-/usr/local/bin/expanso-edge}" +DATA_DIR="${EXPANSO_EDGE_DATA_DIR:-$HOME/.expanso/edge}" +CREDS_FILE="$DATA_DIR/auth/credentials.creds" +PID_FILE="${EXPANSO_EDGE_PID:-/tmp/jetpack-expanso/edge.pid}" +LOG_FILE="${EXPANSO_EDGE_LOG:-/tmp/jetpack-expanso/edge.log}" +TMUX_SESSION="${EXPANSO_EDGE_TMUX_SESSION:-jetpack-expanso-edge}" + +if [[ "$(uname -s)" != "Darwin" ]]; then + echo "error: this helper is for macOS only" >&2 + exit 1 +fi + +if [[ ! -x "$EDGE_BIN" ]]; then + echo "error: expanso-edge not found at $EDGE_BIN" >&2 + echo "Install it with the Expanso installer, or set EXPANSO_EDGE_BIN." >&2 + exit 1 +fi + +require_credentials() { + if [[ -f "$CREDS_FILE" ]]; then + return 0 + fi + + echo "error: local edge credentials not found at $CREDS_FILE" >&2 + echo "Bootstrap first with: $0 --bootstrap" >&2 + exit 1 +} + +echo "Starting expanso-edge from $EDGE_BIN" +echo "Using data directory $DATA_DIR" + +if [[ "${1:-}" == "--check" ]]; then + require_credentials + echo "Local macOS edge prerequisites OK." + exit 0 +fi + +if [[ "${1:-}" == "--bootstrap" ]]; then + if [[ -z "${EXPANSO_EDGE_BOOTSTRAP_TOKEN:-}" ]]; then + echo "error: EXPANSO_EDGE_BOOTSTRAP_TOKEN is not set" >&2 + exit 1 + fi + + bootstrap_cmd=("$EDGE_BIN" bootstrap --data-dir "$DATA_DIR" --token "$EXPANSO_EDGE_BOOTSTRAP_TOKEN") + if [[ -n "${EXPANSO_EDGE_BOOTSTRAP_URL:-}" ]]; then + bootstrap_cmd+=(--url "$EXPANSO_EDGE_BOOTSTRAP_URL") + fi + + "${bootstrap_cmd[@]}" + exit 0 +fi + +if [[ "${1:-}" == "--stop-background" ]]; then + if command -v tmux >/dev/null 2>&1 && tmux has-session -t "$TMUX_SESSION" 2>/dev/null; then + tmux kill-session -t "$TMUX_SESSION" + echo "Stopped tmux session $TMUX_SESSION." + fi + rm -f "$PID_FILE" + exit 0 +fi + +if [[ "${1:-}" == "--background" ]]; then + require_credentials + shift + if ! command -v tmux >/dev/null 2>&1; then + echo "error: tmux is required for background mode" >&2 + exit 1 + fi + + mkdir -p "$(dirname "$LOG_FILE")" + mkdir -p "$(dirname "$PID_FILE")" + tmux kill-session -t "$TMUX_SESSION" 2>/dev/null || true + + tmux_cmd="exec $(printf '%q' "$EDGE_BIN") run --data-dir $(printf '%q' "$DATA_DIR")" + for arg in "$@"; do + tmux_cmd="$tmux_cmd $(printf '%q' "$arg")" + done + + : > "$LOG_FILE" + tmux new-session -d -s "$TMUX_SESSION" "$tmux_cmd" + tmux pipe-pane -t "$TMUX_SESSION" -o "cat >> $(printf '%q' "$LOG_FILE")" + tmux list-panes -t "$TMUX_SESSION" -F "#{pane_pid}" > "$PID_FILE" + echo "Started tmux session $TMUX_SESSION with expanso-edge PID $(cat "$PID_FILE")." + echo "Logs: $LOG_FILE" + exit 0 +fi + +require_credentials +echo "Logs stay attached to this terminal. Press Ctrl-C to stop." + +exec "$EDGE_BIN" run --data-dir "$DATA_DIR" "$@"