Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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/
104 changes: 104 additions & 0 deletions docs/macos-local-demo.md
Original file line number Diff line number Diff line change
@@ -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.
232 changes: 232 additions & 0 deletions scripts/run-local-cluster-macos.sh
Original file line number Diff line number Diff line change
@@ -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 <<USAGE
Usage: $0 [--start|--stop|--status] [--count N] [--base-port PORT] [--no-bootstrap]

Runs multiple local macOS Expanso Edge agents without VMs. Each node gets its
own data directory, tmux session, API port, log file, and PID file.

Defaults:
count: $COUNT
base port: $BASE_PORT (node 01 listens on localhost:$((BASE_PORT + 1)))
name: $DEMO_NAME
tmp dir: $TMP_DIR

Environment overrides:
EXPANSO_EDGE_CLUSTER_COUNT
EXPANSO_EDGE_CLUSTER_BASE_PORT
EXPANSO_EDGE_CLUSTER_NAME
EXPANSO_EDGE_CLUSTER_TMP_DIR
USAGE
}

while [[ $# -gt 0 ]]; do
case "$1" in
--start)
MODE="start"
shift
;;
--stop)
MODE="stop"
shift
;;
--status)
MODE="status"
shift
;;
--count)
COUNT="${2:?missing value for --count}"
shift 2
;;
--base-port)
BASE_PORT="${2:?missing value for --base-port}"
shift 2
;;
--no-bootstrap)
BOOTSTRAP=0
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "error: unknown argument: $1" >&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
Loading