From f3f4a84762c6cfe84d741ded194828a43618afd1 Mon Sep 17 00:00:00 2001 From: anonpenguin23 Date: Sat, 28 Mar 2026 14:30:55 +0200 Subject: [PATCH] feat(cli): add push command and improve node setup - Add `orama push` command to upload and extract binary archives to nodes - Update `node setup` to pass operator metadata and auto-configure environments - Improve SSH configuration and node registration logic --- core/cmd/cli/root.go | 2 + core/pkg/cli/cmd/node/setup.go | 1 + core/pkg/cli/cmd/pushcmd/push.go | 152 ++++++++++++++++++ core/pkg/cli/cmd/sshcmd/ssh.go | 1 + core/pkg/cli/production/install/flags.go | 10 ++ .../cli/production/install/orchestrator.go | 5 + core/pkg/cli/production/setup/command.go | 68 ++++++-- core/pkg/cli/remotessh/ssh.go | 4 +- core/pkg/config/node_config.go | 3 + core/pkg/environments/production/config.go | 10 +- .../environments/production/orchestrator.go | 10 ++ core/pkg/environments/templates/node.yaml | 9 ++ core/pkg/environments/templates/render.go | 5 + core/pkg/inspector/ssh.go | 1 + core/pkg/node/dns_registration.go | 14 +- website/src/docs/operator/getting-started.mdx | 116 ++++++------- website/src/docs/operator/node-management.mdx | 43 ++++- website/src/docs/operator/node-setup.mdx | 25 ++- website/src/docs/operator/upgrades.mdx | 31 +++- 19 files changed, 422 insertions(+), 88 deletions(-) create mode 100644 core/pkg/cli/cmd/pushcmd/push.go diff --git a/core/cmd/cli/root.go b/core/cmd/cli/root.go index 2ed928a..8401311 100644 --- a/core/cmd/cli/root.go +++ b/core/cmd/cli/root.go @@ -19,6 +19,7 @@ import ( "github.com/DeBrosOfficial/network/pkg/cli/cmd/namespacecmd" "github.com/DeBrosOfficial/network/pkg/cli/cmd/node" "github.com/DeBrosOfficial/network/pkg/cli/cmd/nodescmd" + "github.com/DeBrosOfficial/network/pkg/cli/cmd/pushcmd" "github.com/DeBrosOfficial/network/pkg/cli/cmd/rolloutcmd" "github.com/DeBrosOfficial/network/pkg/cli/cmd/sandboxcmd" "github.com/DeBrosOfficial/network/pkg/cli/cmd/sshcmd" @@ -97,6 +98,7 @@ and interacting with the Orama distributed network.`, // Unified node management commands rootCmd.AddCommand(nodescmd.Cmd) + rootCmd.AddCommand(pushcmd.Cmd) rootCmd.AddCommand(rolloutcmd.Cmd) rootCmd.AddCommand(statuscmd.Cmd) rootCmd.AddCommand(sshcmd.Cmd) diff --git a/core/pkg/cli/cmd/node/setup.go b/core/pkg/cli/cmd/node/setup.go index b5131e9..899924b 100644 --- a/core/pkg/cli/cmd/node/setup.go +++ b/core/pkg/cli/cmd/node/setup.go @@ -40,6 +40,7 @@ func init() { setupCmd.Flags().StringVar(&setupOpts.User, "user", "root", "SSH user on the VPS") setupCmd.Flags().StringVar(&setupOpts.Password, "password", "", "One-time password for initial SSH access") setupCmd.Flags().StringVar(&setupOpts.BaseDomain, "base-domain", "", "Base domain for the network") + setupCmd.Flags().StringVar(&setupOpts.Gateway, "gateway", "", "Gateway URL for invite tokens (e.g., http://1.2.3.4)") setupCmd.Flags().BoolVar(&setupOpts.Genesis, "genesis", false, "Create a new cluster (first node)") setupCmd.Flags().BoolVar(&setupOpts.AnyoneRelay, "anyone-relay", false, "Run as Anyone relay operator") setupCmd.MarkFlagRequired("ip") diff --git a/core/pkg/cli/cmd/pushcmd/push.go b/core/pkg/cli/cmd/pushcmd/push.go new file mode 100644 index 0000000..48bdb61 --- /dev/null +++ b/core/pkg/cli/cmd/pushcmd/push.go @@ -0,0 +1,152 @@ +package pushcmd + +import ( + "fmt" + "os" + "path/filepath" + "sort" + + "github.com/DeBrosOfficial/network/pkg/cli" + "github.com/DeBrosOfficial/network/pkg/cli/noderesolver" + "github.com/DeBrosOfficial/network/pkg/cli/remotessh" + "github.com/DeBrosOfficial/network/pkg/inspector" + "github.com/spf13/cobra" +) + +var ( + envFlag string + ipFlag string + userFlag string +) + +// Cmd is the top-level "push" command — upload binary archive to nodes. +var Cmd = &cobra.Command{ + Use: "push", + Short: "Push binary archive to your nodes", + Long: `Upload the pre-built binary archive to nodes and extract it. + +Use --ip to push to a single node, or omit it to push to all nodes +in the active environment. + +Examples: + orama push --ip 1.2.3.4 # Push to one node + orama push --ip 1.2.3.4 --user ubuntu # Push with specific SSH user + orama push --env devnet # Push to all devnet nodes + orama push --env devnet --ip 1.2.3.4 # Push to one devnet node`, + RunE: func(cmd *cobra.Command, args []string) error { + archivePath := findNewestArchive() + if archivePath == "" { + return fmt.Errorf("no binary archive found in /tmp/ (run `orama build` first)") + } + info, err := os.Stat(archivePath) + if err != nil { + return fmt.Errorf("stat archive: %w", err) + } + fmt.Printf("Archive: %s (%s)\n", filepath.Base(archivePath), formatBytes(info.Size())) + + var nodes []inspector.Node + + if ipFlag != "" { + // Single node push + user := userFlag + if user == "" { + user = "root" + } + vaultTarget := fmt.Sprintf("%s/%s", ipFlag, user) + env := envFlag + if env == "" { + active, err := cli.GetActiveEnvironment() + if err == nil { + env = active.Name + } + } + if env == "sandbox" { + vaultTarget = "sandbox/root" + } + nodes = []inspector.Node{{ + Host: ipFlag, + User: user, + VaultTarget: vaultTarget, + Environment: env, + }} + } else { + // All nodes in environment + env := envFlag + if env == "" { + active, err := cli.GetActiveEnvironment() + if err != nil { + return fmt.Errorf("no --ip or --env specified and no active environment") + } + env = active.Name + } + + resolved, err := noderesolver.ResolveNodes(env) + if err != nil { + return fmt.Errorf("failed to resolve nodes: %w", err) + } + if len(resolved) == 0 { + return fmt.Errorf("no nodes found for environment %q", env) + } + nodes = resolved + } + + // Prepare SSH keys + cleanup, err := remotessh.PrepareNodeKeys(nodes) + if err != nil { + return fmt.Errorf("failed to prepare SSH keys: %w", err) + } + defer cleanup() + + fmt.Printf("Pushing to %d node(s)...\n\n", len(nodes)) + + remotePath := "/tmp/" + filepath.Base(archivePath) + extractCmd := fmt.Sprintf("sudo bash -c 'mkdir -p /opt/orama && tar xzf %s -C /opt/orama && rm -f %s && /opt/orama/bin/orama version'", remotePath, remotePath) + + for _, n := range nodes { + fmt.Printf(" %s: uploading...", n.Host) + if err := remotessh.UploadFile(n, archivePath, remotePath); err != nil { + fmt.Printf(" FAILED (%v)\n", err) + continue + } + fmt.Printf(" extracting...") + if err := remotessh.RunSSHStreaming(n, extractCmd); err != nil { + fmt.Printf(" FAILED (%v)\n", err) + continue + } + fmt.Println(" OK") + } + + fmt.Println("\nPush complete") + return nil + }, +} + +func init() { + Cmd.Flags().StringVar(&envFlag, "env", "", "Target environment (default: active)") + Cmd.Flags().StringVar(&ipFlag, "ip", "", "Push to a single node by IP") + Cmd.Flags().StringVar(&userFlag, "user", "", "SSH user (default: root)") +} + +func findNewestArchive() string { + matches, _ := filepath.Glob("/tmp/orama-*-linux-*.tar.gz") + if len(matches) == 0 { + return "" + } + sort.Slice(matches, func(i, j int) bool { + fi, _ := os.Stat(matches[i]) + fj, _ := os.Stat(matches[j]) + if fi == nil || fj == nil { + return false + } + return fi.ModTime().After(fj.ModTime()) + }) + return matches[0] +} + +func formatBytes(b int64) string { + const mb = 1024 * 1024 + if b >= mb { + return fmt.Sprintf("%.1f MB", float64(b)/float64(mb)) + } + return fmt.Sprintf("%d KB", b/1024) +} diff --git a/core/pkg/cli/cmd/sshcmd/ssh.go b/core/pkg/cli/cmd/sshcmd/ssh.go index 448e29e..c5955b3 100644 --- a/core/pkg/cli/cmd/sshcmd/ssh.go +++ b/core/pkg/cli/cmd/sshcmd/ssh.go @@ -78,6 +78,7 @@ func sshInto(node inspector.Node) error { sshCmd := exec.Command(sshBin, "-i", keyPath, "-o", "StrictHostKeyChecking=accept-new", + "-o", "IdentitiesOnly=yes", fmt.Sprintf("%s@%s", node.User, node.Host), ) sshCmd.Stdin = os.Stdin diff --git a/core/pkg/cli/production/install/flags.go b/core/pkg/cli/production/install/flags.go index 50b844e..d3a360d 100644 --- a/core/pkg/cli/production/install/flags.go +++ b/core/pkg/cli/production/install/flags.go @@ -43,6 +43,11 @@ type Flags struct { AnyoneFamily string // Comma-separated fingerprints of other relays you operate AnyoneBandwidth int // Percentage of VPS bandwidth for relay (default: 30, 0=unlimited) AnyoneAccounting int // Monthly data cap for relay in GB (0=unlimited) + + // Operator metadata (set by orama node setup, written to node.yaml for registration) + SSHUser string // SSH user for remote management + Environment string // Environment name (devnet, testnet, etc.) + OperatorWallet string // Operator wallet address } // ParseFlags parses install command flags @@ -90,6 +95,11 @@ func ParseFlags(args []string) (*Flags, error) { fs.IntVar(&flags.AnyoneBandwidth, "anyone-bandwidth", 30, "Limit relay to N% of VPS bandwidth (0=unlimited, runs speedtest)") fs.IntVar(&flags.AnyoneAccounting, "anyone-accounting", 0, "Monthly data cap for relay in GB (0=unlimited)") + // Operator metadata (set by orama node setup) + fs.StringVar(&flags.SSHUser, "ssh-user", "", "SSH user for remote management") + fs.StringVar(&flags.Environment, "environment", "", "Environment name (devnet, testnet, etc.)") + fs.StringVar(&flags.OperatorWallet, "operator-wallet", "", "Operator wallet address") + if err := fs.Parse(args); err != nil { if err == flag.ErrHelp { return nil, err diff --git a/core/pkg/cli/production/install/orchestrator.go b/core/pkg/cli/production/install/orchestrator.go index 04a4054..58f0f0d 100644 --- a/core/pkg/cli/production/install/orchestrator.go +++ b/core/pkg/cli/production/install/orchestrator.go @@ -68,6 +68,11 @@ func NewOrchestrator(flags *Flags) (*Orchestrator, error) { setup.SetAnyoneClient(true) } + // Set operator metadata (from orama node setup) + setup.SSHUser = flags.SSHUser + setup.Environment = flags.Environment + setup.OperatorWallet = flags.OperatorWallet + validator := NewValidator(flags, oramaDir) return &Orchestrator{ diff --git a/core/pkg/cli/production/setup/command.go b/core/pkg/cli/production/setup/command.go index f2408ed..6c08aad 100644 --- a/core/pkg/cli/production/setup/command.go +++ b/core/pkg/cli/production/setup/command.go @@ -38,7 +38,8 @@ type Options struct { User string // SSH user (default: "root") Password string // One-time password for initial SSH access BaseDomain string - Genesis bool // If true, create a new cluster instead of joining + Gateway string // Gateway URL to use for invite tokens (overrides env config) + Genesis bool // If true, create a new cluster instead of joining AnyoneRelay bool } @@ -154,6 +155,25 @@ func Run(opts Options) error { return fmt.Errorf("install failed: %w", err) } + // 9. After genesis install, update the environment gateway URL to this node's IP. + // This allows subsequent `node setup` calls to find the gateway automatically. + if opts.Genesis && opts.Env != "" { + gatewayURL := fmt.Sprintf("http://%s", opts.IP) + desc := fmt.Sprintf("%s (genesis: %s)", opts.Env, opts.IP) + if err := cli.AddEnvironment(opts.Env, gatewayURL, desc); err != nil { + fmt.Fprintf(os.Stderr, " Warning: failed to update environment: %v\n", err) + } else { + if err := cli.SwitchEnvironment(opts.Env); err != nil { + fmt.Fprintf(os.Stderr, " Warning: failed to switch environment: %v\n", err) + } + fmt.Printf(" Environment %q updated: gateway → %s\n", opts.Env, gatewayURL) + fmt.Printf("\n To join more nodes, first authenticate:\n") + fmt.Printf(" orama auth login\n") + fmt.Printf(" Then:\n") + fmt.Printf(" orama node setup --ip --password '' --env %s --base-domain %s\n", opts.Env, opts.BaseDomain) + } + } + fmt.Printf("\n Node %s setup complete!\n", opts.IP) return nil } @@ -176,6 +196,8 @@ func installPublicKey(ip, user, password, pubKey string) error { "ssh", "-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=10", + "-o", "PreferredAuthentications=password", + "-o", "PubkeyAuthentication=no", fmt.Sprintf("%s@%s", user, ip), cmd, } @@ -212,22 +234,38 @@ func buildInstallCommand(opts Options, node inspector.Node, agentClient *rwagent parts = append(parts, "--anyone-client") } - if !opts.Genesis { - // Get gateway URL and invite token - env := opts.Env - if env == "" { - active, err := cli.GetActiveEnvironment() - if err != nil { - return "", fmt.Errorf("failed to get active environment: %w", err) - } - env = active.Name - } + // Pass operator metadata so the node registers with correct values + if opts.User != "" { + parts = append(parts, "--ssh-user", opts.User) + } + if opts.Env != "" { + parts = append(parts, "--environment", opts.Env) + } - envConfig, err := cli.GetEnvironmentByName(env) - if err != nil { - return "", fmt.Errorf("environment %q not found: %w", env, err) + // Get wallet address for operator tagging + ctx := context.Background() + if addrData, err := agentClient.GetAddress(ctx, "evm"); err == nil && addrData.Address != "" { + parts = append(parts, "--operator-wallet", addrData.Address) + } + + if !opts.Genesis { + // Determine gateway URL for invite token request + gatewayURL := opts.Gateway + if gatewayURL == "" { + env := opts.Env + if env == "" { + active, err := cli.GetActiveEnvironment() + if err != nil { + return "", fmt.Errorf("failed to get active environment: %w", err) + } + env = active.Name + } + envConfig, err := cli.GetEnvironmentByName(env) + if err != nil { + return "", fmt.Errorf("environment %q not found (use --gateway to specify directly): %w", env, err) + } + gatewayURL = envConfig.GatewayURL } - gatewayURL := envConfig.GatewayURL // Request invite token via operator API token, err := requestInviteToken(gatewayURL) diff --git a/core/pkg/cli/remotessh/ssh.go b/core/pkg/cli/remotessh/ssh.go index 3ce5157..73bcd4f 100644 --- a/core/pkg/cli/remotessh/ssh.go +++ b/core/pkg/cli/remotessh/ssh.go @@ -42,7 +42,7 @@ func UploadFile(node inspector.Node, localPath, remotePath string, opts ...SSHOp dest := fmt.Sprintf("%s@%s:%s", node.User, node.Host, remotePath) - args := []string{"-o", "ConnectTimeout=10", "-i", node.SSHKey} + args := []string{"-o", "ConnectTimeout=10", "-o", "IdentitiesOnly=yes", "-i", node.SSHKey} if cfg.noHostKeyCheck { args = append([]string{"-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null"}, args...) } else { @@ -73,7 +73,7 @@ func RunSSHStreaming(node inspector.Node, command string, opts ...SSHOption) err o(&cfg) } - args := []string{"-o", "ConnectTimeout=10", "-i", node.SSHKey} + args := []string{"-o", "ConnectTimeout=10", "-o", "IdentitiesOnly=yes", "-i", node.SSHKey} if cfg.noHostKeyCheck { args = append([]string{"-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null"}, args...) } else { diff --git a/core/pkg/config/node_config.go b/core/pkg/config/node_config.go index a23ffcc..ab070c9 100644 --- a/core/pkg/config/node_config.go +++ b/core/pkg/config/node_config.go @@ -7,4 +7,7 @@ type NodeConfig struct { DataDir string `yaml:"data_dir"` // Data directory MaxConnections int `yaml:"max_connections"` // Maximum peer connections Domain string `yaml:"domain"` // Domain for this node (e.g., node-1.orama.network) + SSHUser string `yaml:"ssh_user,omitempty"` // SSH user for remote management + Environment string `yaml:"environment,omitempty"` // Environment name (devnet, testnet, etc.) + OperatorWallet string `yaml:"operator_wallet,omitempty"` // Operator wallet address } diff --git a/core/pkg/environments/production/config.go b/core/pkg/environments/production/config.go index cb80560..2eaa530 100644 --- a/core/pkg/environments/production/config.go +++ b/core/pkg/environments/production/config.go @@ -20,7 +20,10 @@ import ( // ConfigGenerator manages generation of node, gateway, and service configs type ConfigGenerator struct { - oramaDir string + oramaDir string + SSHUser string // Operator metadata + Environment string + OperatorWallet string } // NewConfigGenerator creates a new config generator @@ -192,6 +195,11 @@ func (cg *ConfigGenerator) GenerateNodeConfig(peerAddresses []string, vpsIP stri // HTTPS is still used for client-facing gateway traffic via autocert // TLS can be enabled manually later if needed for inter-node encryption + // Operator metadata (set by orama node setup via --ssh-user, --environment, --operator-wallet) + data.SSHUser = cg.SSHUser + data.Environment = cg.Environment + data.OperatorWallet = cg.OperatorWallet + return templates.RenderNodeConfig(data) } diff --git a/core/pkg/environments/production/orchestrator.go b/core/pkg/environments/production/orchestrator.go index 7458c75..4a3ace8 100644 --- a/core/pkg/environments/production/orchestrator.go +++ b/core/pkg/environments/production/orchestrator.go @@ -53,6 +53,11 @@ type ProductionSetup struct { serviceController *SystemdController binaryInstaller *BinaryInstaller NodePeerID string // Captured during Phase3 for later display + + // Operator metadata (from --ssh-user, --environment, --operator-wallet flags) + SSHUser string + Environment string + OperatorWallet string } // ReadBranchPreference reads the stored branch preference from disk @@ -599,6 +604,11 @@ func (ps *ProductionSetup) Phase4GenerateConfigs(peerAddresses []string, vpsIP s ps.logf("Phase 4: Generating configurations...") } + // Propagate operator metadata to config generator + ps.configGenerator.SSHUser = ps.SSHUser + ps.configGenerator.Environment = ps.Environment + ps.configGenerator.OperatorWallet = ps.OperatorWallet + // Node config (unified architecture) nodeConfig, err := ps.configGenerator.GenerateNodeConfig(peerAddresses, vpsIP, joinAddress, domain, baseDomain, enableHTTPS) if err != nil { diff --git a/core/pkg/environments/templates/node.yaml b/core/pkg/environments/templates/node.yaml index e44e9da..8559e0f 100644 --- a/core/pkg/environments/templates/node.yaml +++ b/core/pkg/environments/templates/node.yaml @@ -5,6 +5,15 @@ node: data_dir: "{{.DataDir}}" max_connections: 50 domain: "{{.Domain}}" +{{- if .SSHUser}} + ssh_user: "{{.SSHUser}}" +{{- end}} +{{- if .Environment}} + environment: "{{.Environment}}" +{{- end}} +{{- if .OperatorWallet}} + operator_wallet: "{{.OperatorWallet}}" +{{- end}} database: data_dir: "{{.DataDir}}/rqlite" diff --git a/core/pkg/environments/templates/render.go b/core/pkg/environments/templates/render.go index d867955..135085e 100644 --- a/core/pkg/environments/templates/render.go +++ b/core/pkg/environments/templates/render.go @@ -41,6 +41,11 @@ type NodeConfigData struct { NodeKey string // Path to X.509 private key for node-to-node communication NodeCACert string // Path to CA certificate (optional) NodeNoVerify bool // Skip certificate verification (for self-signed certs) + + // Operator metadata — written to dns_nodes during registration + SSHUser string // SSH user for remote management + Environment string // Environment name (devnet, testnet, etc.) + OperatorWallet string // Operator wallet address } // GatewayConfigData holds parameters for gateway.yaml rendering diff --git a/core/pkg/inspector/ssh.go b/core/pkg/inspector/ssh.go index f73d2f0..f277b17 100644 --- a/core/pkg/inspector/ssh.go +++ b/core/pkg/inspector/ssh.go @@ -88,6 +88,7 @@ func runSSHOnce(ctx context.Context, node Node, command string) SSHResult { "-o", "StrictHostKeyChecking=accept-new", "-o", "ConnectTimeout=10", "-o", "BatchMode=yes", + "-o", "IdentitiesOnly=yes", "-i", node.SSHKey, fmt.Sprintf("%s@%s", node.User, node.Host), command, diff --git a/core/pkg/node/dns_registration.go b/core/pkg/node/dns_registration.go index b8fb870..3b1572b 100644 --- a/core/pkg/node/dns_registration.go +++ b/core/pkg/node/dns_registration.go @@ -44,21 +44,29 @@ func (n *Node) registerDNSNode(ctx context.Context) error { // Determine region (defaulting to "local" for now, could be from cloud metadata in future) region := "local" + // Read optional metadata from node config + sshUser := n.config.Node.SSHUser + environment := n.config.Node.Environment + operatorWallet := n.config.Node.OperatorWallet + // Insert or update node record query := ` - INSERT INTO dns_nodes (id, ip_address, internal_ip, region, status, last_seen, created_at, updated_at) - VALUES (?, ?, ?, ?, 'active', datetime('now'), datetime('now'), datetime('now')) + INSERT INTO dns_nodes (id, ip_address, internal_ip, region, status, ssh_user, environment, operator_wallet, last_seen, created_at, updated_at) + VALUES (?, ?, ?, ?, 'active', ?, ?, ?, datetime('now'), datetime('now'), datetime('now')) ON CONFLICT(id) DO UPDATE SET ip_address = excluded.ip_address, internal_ip = excluded.internal_ip, region = excluded.region, status = 'active', + ssh_user = COALESCE(NULLIF(excluded.ssh_user, ''), dns_nodes.ssh_user), + environment = COALESCE(NULLIF(excluded.environment, ''), dns_nodes.environment), + operator_wallet = COALESCE(NULLIF(excluded.operator_wallet, ''), dns_nodes.operator_wallet), last_seen = datetime('now'), updated_at = datetime('now') ` db := n.rqliteAdapter.GetSQLDB() - _, err = rqlite.SafeExecContext(db, ctx, query, nodeID, ipAddress, internalIP, region) + _, err = rqlite.SafeExecContext(db, ctx, query, nodeID, ipAddress, internalIP, region, sshUser, environment, operatorWallet) if err != nil { return fmt.Errorf("failed to register DNS node: %w", err) } diff --git a/website/src/docs/operator/getting-started.mdx b/website/src/docs/operator/getting-started.mdx index d58b7f2..7c4b3e7 100644 --- a/website/src/docs/operator/getting-started.mdx +++ b/website/src/docs/operator/getting-started.mdx @@ -48,79 +48,87 @@ The CLI is used both for initial node setup and ongoing management. All node ope To become an operator, you need: 1. A VPS meeting the hardware requirements above -2. An invite token (generated by an existing node in the cluster) -3. The `orama` binary on your VPS +2. The `orama` CLI on your local machine +3. RootWallet desktop app (for SSH key management and wallet authentication) -### Step 1: Get an Invite Token +### Setting Up Your First Node (Genesis) -An existing cluster node generates a single-use invite token: +The first node creates a new cluster. Run this from your **local machine** (not the VPS): ```bash -# On an existing node (requires sudo) -sudo orama node invite --expiry 24h -``` - -Tokens are single-use and expire after the specified duration (default: 1 hour). - -### Step 2: Install the Node - -**Genesis node (first node, creates a new cluster):** - -```bash -sudo orama node install \ - --vps-ip \ - --domain \ +orama node setup \ + --ip \ + --password '' \ + --env devnet \ --base-domain \ - --nameserver + --role nameserver \ + --genesis ``` -**Nameserver node (joins existing cluster):** +This single command: +1. Creates an SSH key in your RootWallet vault +2. Installs the SSH key on the VPS (using the password once) +3. Uploads the binary archive +4. Installs and starts all services +5. Updates your local environment config to point to this node + +After genesis, authenticate with the new cluster: ```bash -sudo orama node install \ - --join http:// --token \ - --vps-ip \ - --domain \ +orama auth login +``` + +### Adding More Nodes + +Once the genesis node is running and you're authenticated, add more nodes with one command: + +```bash +# Add a nameserver node +orama node setup \ + --ip \ + --password '' \ + --env devnet \ --base-domain \ - --nameserver -``` + --role nameserver -**Regular node (joins existing cluster, domain is auto-generated):** - -```bash -sudo orama node install \ - --join http:// --token \ - --vps-ip \ +# Add a regular node +orama node setup \ + --ip \ + --password '' \ + --env devnet \ --base-domain -``` -**Regular node with Anyone relay:** - -```bash -sudo orama node install \ - --join http:// --token \ - --vps-ip \ +# Add a node with Anyone relay +orama node setup \ + --ip \ + --password '' \ + --env devnet \ --base-domain \ - --anyone-relay \ - --anyone-nickname \ - --anyone-wallet \ - --anyone-contact "" + --anyone-relay ``` -### Install Flags +The setup command automatically: +- Creates a unique SSH key for the node in RootWallet +- Installs the key on the VPS (password is used once, then discarded) +- Requests an invite token from the cluster (using your wallet authentication) +- Uploads binaries and runs the full installation +- Joins the node to the cluster + +The `--password` flag is only needed for the initial SSH key installation. After that, all SSH access uses the RootWallet key. + +### Setup Flags | Flag | Required | Description | |------|----------|-------------| -| `--vps-ip` | Yes | Public IP address of this VPS | -| `--base-domain` | Yes | Base domain for deployment routing (e.g., `orama-devnet.network`) | -| `--domain` | No | Domain for HTTPS. Auto-generated for non-nameserver nodes (e.g., `node-a3f8k2.orama-devnet.network`) | -| `--nameserver` | No | Make this node a nameserver (runs CoreDNS + Caddy) | -| `--join` | No | URL of an existing node to join (e.g., `http://1.2.3.4`) | -| `--token` | No | Invite token from `orama node invite` on an existing node | -| `--force` | No | Force reconfiguration even if already installed | -| `--dry-run` | No | Show what would be done without making changes | -| `--skip-checks` | No | Skip minimum resource checks (RAM/CPU) | -| `--skip-firewall` | No | Skip UFW firewall setup (for users who manage their own firewall) | +| `--ip` | Yes | Public IP address of the VPS | +| `--password` | No | One-time VPS password for SSH key installation | +| `--env` | No | Target environment (default: active environment) | +| `--base-domain` | Yes | Base domain for the network (e.g., `orama-devnet.network`) | +| `--role` | No | `node` (default) or `nameserver` | +| `--user` | No | SSH user (default: `root`) | +| `--gateway` | No | Gateway URL override for invite tokens | +| `--genesis` | No | Create a new cluster (first node only) | +| `--anyone-relay` | No | Run as Anyone relay operator | #### Anyone Relay Flags diff --git a/website/src/docs/operator/node-management.mdx b/website/src/docs/operator/node-management.mdx index e0c9d3f..7c21b47 100644 --- a/website/src/docs/operator/node-management.mdx +++ b/website/src/docs/operator/node-management.mdx @@ -98,13 +98,46 @@ For detailed monitoring with alerts, use the full monitor: orama monitor report --env devnet ``` +## Pushing Binary Updates + +Push a new binary archive to nodes without reinstalling: + +```bash +# Build the archive first +orama build + +# Push to a single node +orama push --ip 1.2.3.4 + +# Push to all nodes in an environment +orama push --env devnet +``` + +## Adding New Nodes + +Add a new VPS to your cluster with one command: + +```bash +orama node setup --ip --password '' --env devnet --base-domain orama-devnet.network +``` + +This creates an SSH key in RootWallet, installs it on the VPS, uploads binaries, requests an invite token, and joins the node to the cluster. The `--password` is used once for initial SSH access — after that, RootWallet handles authentication. + +For the first node in a new cluster, add `--genesis`: + +```bash +orama node setup --ip --password '' --env devnet \ + --base-domain orama-devnet.network --role nameserver --genesis +``` + ## How Node Ownership Works -When you install a node (via `orama node install` with an invite token), the network records your wallet address as the node's operator. This happens automatically through the invite token system: +When you set up a node via `orama node setup`, the CLI: -1. You generate an invite token (authenticated with your wallet) -2. The new node joins using that token -3. The network tags the node with your wallet address from the token +1. Authenticates with the cluster using your wallet (via RootWallet) +2. Requests an invite token tagged with your wallet address +3. The new node joins using that token +4. The network records your wallet as the node's operator This means `orama nodes` can query the network for "all nodes owned by my wallet" — no local files to maintain. @@ -124,8 +157,10 @@ The legacy `nodes.conf` continues to work as a fallback — you can migrate at y | Command | Description | |---------|-------------| +| `orama node setup --ip ` | Set up a new node (SSH key + install + join) | | `orama nodes [--env]` | List your nodes | | `orama ssh [--env]` | SSH into a node | | `orama status [--env] [--json]` | Health check all nodes | +| `orama push [--ip ] [--env]` | Push binary archive to nodes | | `orama rollout [--env] [--delay]` | Rolling upgrade all nodes | | `orama node migrate-conf [--env]` | Migrate nodes.conf to wallet-based tracking | diff --git a/website/src/docs/operator/node-setup.mdx b/website/src/docs/operator/node-setup.mdx index 22d9e15..de48e0f 100644 --- a/website/src/docs/operator/node-setup.mdx +++ b/website/src/docs/operator/node-setup.mdx @@ -2,7 +2,30 @@ This guide walks you through the full process of setting up an Orama node on a fresh VPS — from system requirements through installation, verification, and day-to-day lifecycle management. -> **Tip:** After installation, manage your nodes with the unified CLI commands: `orama nodes`, `orama ssh`, `orama status`, `orama rollout`. See [Node Management](/docs/operator/node-management) for details. +## Quick Start + +The fastest way to set up a node is with `orama node setup` from your local machine: + +```bash +# First node (creates cluster) +orama node setup --ip --password '' --env devnet \ + --base-domain orama-devnet.network --role nameserver --genesis + +# Authenticate with the new cluster +orama auth login + +# Add more nodes (invite token handled automatically) +orama node setup --ip --password '' --env devnet \ + --base-domain orama-devnet.network +``` + +This handles everything: SSH key management (via RootWallet), binary upload, invite tokens, and installation. See [Getting Started](/docs/operator/getting-started) for the full walkthrough. + +> **Tip:** After installation, manage your nodes with: `orama nodes`, `orama ssh`, `orama status`, `orama rollout`, `orama push`. See [Node Management](/docs/operator/node-management) for details. + +## Manual Setup + +The sections below describe the manual installation process for advanced users or environments where `orama node setup` is not suitable. ## System Requirements diff --git a/website/src/docs/operator/upgrades.mdx b/website/src/docs/operator/upgrades.mdx index f186876..d65d170 100644 --- a/website/src/docs/operator/upgrades.mdx +++ b/website/src/docs/operator/upgrades.mdx @@ -147,19 +147,34 @@ orama node rollout --env testnet --no-build --yes --delay 60 --- -## Push Command +## Push Command (Recommended) -The `orama node push` command uploads the binary to nodes without restarting services. Useful for pre-staging a binary before a maintenance window. +The `orama push` command uploads the binary archive to nodes without restarting services. Useful for pre-staging a binary before a maintenance window. Uses RootWallet for SSH authentication. ```bash -# Push to all nodes in an environment -orama node push --env devnet - # Push to a single node -orama node push --env testnet --node 1.2.3.4 +orama push --ip 1.2.3.4 -# Upload directly to each node (no hub fanout) -orama node push --env testnet --direct +# Push to all nodes in an environment +orama push --env devnet + +# Push with a specific SSH user +orama push --ip 1.2.3.4 --user ubuntu +``` + +| Flag | Default | Description | +|------|---------|-------------| +| `--ip` | | Push to a single node by IP | +| `--env` | active env | Push to all nodes in environment | +| `--user` | `root` | SSH user | + +### Legacy Push + +The `orama node push` command is still available and uses `nodes.conf`: + +```bash +orama node push --env devnet +orama node push --env testnet --node 1.2.3.4 ``` | Flag | Default | Description |