diff --git a/cmd/spinifex/cmd/admin.go b/cmd/spinifex/cmd/admin.go index 07ad80382..ac36d91ec 100644 --- a/cmd/spinifex/cmd/admin.go +++ b/cmd/spinifex/cmd/admin.go @@ -334,7 +334,6 @@ func init() { adminInitCmd.Flags().String("external-gateway", "", "WAN gateway IP (auto-detected from default route)") adminInitCmd.Flags().String("gateway-ip", "", "OVN gateway router's external IP for SNAT (default: pool range_start for pool mode, required for nat mode without DHCP)") adminInitCmd.Flags().Int("external-prefix-len", 24, "External pool subnet prefix length (auto-detected)") - adminInitCmd.Flags().Bool("no-external", false, "Disable external networking (overlay-only, no internet for VMs)") adminInitCmd.Flags().Bool("gpu-passthrough", false, "Enable VFIO GPU passthrough (sets gpu_passthrough = true in daemon config)") adminInitCmd.Flags().Bool("ipsec", true, "Encrypt intra-AZ Geneve via OVN native IPsec (cluster-wide); disable only for trusted single-rack lab") @@ -1018,7 +1017,6 @@ func runAdminInit(cmd *cobra.Command, args []string) { externalGateway, _ := cmd.Flags().GetString("external-gateway") externalPrefixLen, _ := cmd.Flags().GetInt("external-prefix-len") gatewayIP, _ := cmd.Flags().GetString("gateway-ip") - noExternal, _ := cmd.Flags().GetBool("no-external") gpuPassthrough, _ := cmd.Flags().GetBool("gpu-passthrough") ipsecEnabled, _ := cmd.Flags().GetBool("ipsec") @@ -1051,59 +1049,56 @@ func runAdminInit(cmd *cobra.Command, args []string) { // Auto-detect network topology var poolStart, poolEnd string var detectedNet *admin.DetectedNetwork - if !noExternal { - detected, err := admin.DetectNetwork() - if err != nil { - fmt.Fprintf(os.Stderr, "āš ļø Network auto-detection failed: %v\n", err) - fmt.Fprintf(os.Stderr, " Use --no-external to skip, or specify flags manually.\n") + detected, err := admin.DetectNetwork() + if err != nil { + fmt.Fprintf(os.Stderr, "āš ļø Network auto-detection failed: %v\n", err) + fmt.Fprintf(os.Stderr, " Use --external-mode=nat for outbound-only VMs on a non-bridgeable uplink, or specify --external-* flags manually.\n") + } else { + detectedNet = detected + + // Print detected topology + fmt.Println("\nšŸ” Detected network topology:") + fmt.Printf(" %-14s %-18s %-20s %-16s %s\n", "Interface", "IP", "Subnet", "Gateway", "Role") + for _, iface := range detected.Interfaces { + gw := "—" + if iface.Gateway != "" { + gw = iface.Gateway + } + fmt.Printf(" %-14s %-18s %-20s %-16s %s\n", iface.Name, iface.IP, iface.Subnet, gw, strings.ToUpper(iface.Role)) + } + if detected.LANCount == 0 { + fmt.Println("\n Mode: single-NIC (veth-bridged external)") } else { - detectedNet = detected + fmt.Printf("\n Mode: %d LAN + 1 WAN (veth-bridged external)\n", detected.LANCount) + } - // Print detected topology - fmt.Println("\nšŸ” Detected network topology:") - fmt.Printf(" %-14s %-18s %-20s %-16s %s\n", "Interface", "IP", "Subnet", "Gateway", "Role") - for _, iface := range detected.Interfaces { - gw := "—" - if iface.Gateway != "" { - gw = iface.Gateway - } - fmt.Printf(" %-14s %-18s %-20s %-16s %s\n", iface.Name, iface.IP, iface.Subnet, gw, strings.ToUpper(iface.Role)) + // Apply auto-detected values when flags not explicitly set + if detected.WAN != nil { + if externalIface == "" { + externalIface = detected.WAN.Name } - if detected.LANCount == 0 { - fmt.Println("\n Mode: single-NIC (veth-bridged external)") - } else { - fmt.Printf("\n Mode: %d LAN + 1 WAN (veth-bridged external)\n", detected.LANCount) + if externalGateway == "" { + externalGateway = detected.WAN.Gateway + } + if !cmd.Flags().Changed("external-prefix-len") { + externalPrefixLen = detected.WAN.PrefixLen } - // Apply auto-detected values when flags not explicitly set - if detected.WAN != nil { - if externalIface == "" { - externalIface = detected.WAN.Name - } - if externalGateway == "" { - externalGateway = detected.WAN.Gateway - } - if !cmd.Flags().Changed("external-prefix-len") { - externalPrefixLen = detected.WAN.PrefixLen - } - - // Default mode: always "pool". Source defaults to "static"; if - // --external-pool is omitted the validator below will error with - // a SuggestPoolRange hint. - if externalMode == "" && !cmd.Flags().Changed("external-mode") { - if isNonBridgeableUplink(detected.WAN.Name) { - fmt.Fprintf(os.Stderr, "\nāŒ Detected WAN interface %s cannot be bridged (WiFi/cellular/PPP).\n", detected.WAN.Name) - fmt.Fprintf(os.Stderr, " Use routed NAT mode instead (outbound-only VM networking):\n") - fmt.Fprintf(os.Stderr, " ./scripts/setup-ovn.sh --management --nat-uplink\n") - fmt.Fprintf(os.Stderr, " spx admin init --external-mode=nat\n") - os.Exit(1) - } - externalMode = "pool" + // Default mode: always "pool". Source defaults to "static"; if + // --external-pool is omitted the validator below will error with + // a SuggestPoolRange hint. + if externalMode == "" && !cmd.Flags().Changed("external-mode") { + if isNonBridgeableUplink(detected.WAN.Name) { + fmt.Fprintf(os.Stderr, "\nāŒ Detected WAN interface %s cannot be bridged (WiFi/cellular/PPP).\n", detected.WAN.Name) + fmt.Fprintf(os.Stderr, " Use routed NAT mode instead (outbound-only VM networking):\n") + fmt.Fprintf(os.Stderr, " ./scripts/setup-ovn.sh --management --nat-uplink\n") + fmt.Fprintf(os.Stderr, " spx admin init --external-mode=nat\n") + os.Exit(1) } + externalMode = "pool" } } } - // Validate external networking flags if externalMode != "" && externalMode != "pool" && externalMode != "nat" { fmt.Fprintf(os.Stderr, "āŒ Error: --external-mode must be 'pool', 'nat', or empty, got: %s\n", externalMode) @@ -1209,8 +1204,8 @@ func runAdminInit(cmd *cobra.Command, args []string) { os.Exit(1) } - // Resolve the off-host advertise IP. DetectNetwork may have been skipped - // when --no-external is set; detect lazily if we need the WAN IP. + // Resolve the off-host advertise IP. DetectNetwork may have failed earlier; + // detect lazily if we still need the WAN IP. if advertiseFlag == "" && (bindIP == "0.0.0.0" || bindIP == "127.0.0.1") && detectedNet == nil { if d, derr := admin.DetectNetwork(); derr == nil { detectedNet = d diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index 1e1f06047..c75a02c20 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -54,7 +54,7 @@ Operational commands for inspecting cluster state. These fan out NATS requests t | Command | Flags | Description | |---------|-------|-------------| -| `spx admin init` | `--nodes`, `--node`, `--bind`, `--port`, `--region`, `--az`, `--cluster-name`, `--cluster-bind`, `--cluster-routes`, `--predastore-nodes`, `--services`, `--formation-timeout`, `--token-ttl`, `--force`, `--external-mode` (`pool` \| `nat` routed NAT for non-bridgeable uplinks, single-node; pair with `setup-ovn.sh --nat-uplink`; add `--external-pool start-end --external-gateway ` or `--external-source=dhcp [--external-bind-bridge ]` for a public pool with full EIP support; note: system instances (ECS/EKS/load-balancer agents) are unsupported in nat v1 — they require `pool` mode), `--no-external` | Generates root IAM credentials (AKIA-prefixed access key + secret) → creates master.key (AES-256, 32 bytes, 0600) → writes bootstrap.json (consumed on first start) → generates CA + server TLS certificates → generates join token (written to `join-token` file, displayed in join command) → creates NATS config with auth token → writes spinifex.toml, awsgw.toml, predastore.toml → configures AWS CLI `spx` profile → creates directory structure under `~/spinifex/`. `--force` on an already-initialized node is idempotent for crypto: it preserves the existing keys (master.key, predastore/viperblock encryption keys), system + admin credentials, and CA, and refreshes only the config files and the CA-signed server certificate (so a changed bind IP / SANs is picked up without breaking already-joined nodes or CA-baked AMIs). A genuine clean slate requires removing the data dirs first. | +| `spx admin init` | `--nodes`, `--node`, `--bind`, `--port`, `--region`, `--az`, `--cluster-name`, `--cluster-bind`, `--cluster-routes`, `--predastore-nodes`, `--services`, `--formation-timeout`, `--token-ttl`, `--force`, `--external-mode` (`pool` \| `nat` routed NAT for non-bridgeable uplinks, single-node; pair with `setup-ovn.sh --nat-uplink`; add `--external-pool start-end --external-gateway ` or `--external-source=dhcp [--external-bind-bridge ]` for a public pool with full EIP support; note: system instances (ECS/EKS/load-balancer agents) are unsupported in nat v1 — they require `pool` mode) | Generates root IAM credentials (AKIA-prefixed access key + secret) → creates master.key (AES-256, 32 bytes, 0600) → writes bootstrap.json (consumed on first start) → generates CA + server TLS certificates → generates join token (written to `join-token` file, displayed in join command) → creates NATS config with auth token → writes spinifex.toml, awsgw.toml, predastore.toml → configures AWS CLI `spx` profile → creates directory structure under `~/spinifex/`. `--force` on an already-initialized node is idempotent for crypto: it preserves the existing keys (master.key, predastore/viperblock encryption keys), system + admin credentials, and CA, and refreshes only the config files and the CA-signed server certificate (so a changed bind IP / SANs is picked up without breaking already-joined nodes or CA-baked AMIs). A genuine clean slate requires removing the data dirs first. | | `spx admin join` | `--host` (required), `--node` (required), `--token` (required), `--bind`, `--port`, `--region`, `--az`, `--cluster-bind`, `--cluster-routes`, `--data-dir`, `--services` | Connects to leader node with join token (Authorization: Bearer header) → retrieves cluster configuration → configures local node to join cluster and participate in distributed operations | ### Version