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
This commit is contained in:
anonpenguin23 2026-03-28 14:30:55 +02:00
parent ab1be4105c
commit f3f4a84762
19 changed files with 422 additions and 88 deletions

View File

@ -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)

View File

@ -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")

View File

@ -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)
}

View File

@ -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

View File

@ -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

View File

@ -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{

View File

@ -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 <IP> --password '<PASS>' --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)

View File

@ -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 {

View File

@ -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
}

View File

@ -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)
}

View File

@ -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 {

View File

@ -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"

View File

@ -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

View File

@ -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,

View File

@ -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)
}

View File

@ -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 <PUBLIC_IP> \
--domain <BASE_DOMAIN> \
orama node setup \
--ip <VPS_IP> \
--password '<VPS_PASSWORD>' \
--env devnet \
--base-domain <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://<GENESIS_IP> --token <TOKEN> \
--vps-ip <PUBLIC_IP> \
--domain <BASE_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 <VPS_IP> \
--password '<VPS_PASSWORD>' \
--env devnet \
--base-domain <BASE_DOMAIN> \
--nameserver
```
--role nameserver
**Regular node (joins existing cluster, domain is auto-generated):**
```bash
sudo orama node install \
--join http://<GENESIS_IP> --token <TOKEN> \
--vps-ip <PUBLIC_IP> \
# Add a regular node
orama node setup \
--ip <VPS_IP> \
--password '<VPS_PASSWORD>' \
--env devnet \
--base-domain <BASE_DOMAIN>
```
**Regular node with Anyone relay:**
```bash
sudo orama node install \
--join http://<GENESIS_IP> --token <TOKEN> \
--vps-ip <PUBLIC_IP> \
# Add a node with Anyone relay
orama node setup \
--ip <VPS_IP> \
--password '<VPS_PASSWORD>' \
--env devnet \
--base-domain <BASE_DOMAIN> \
--anyone-relay \
--anyone-nickname <NAME> \
--anyone-wallet <ETH_ADDRESS> \
--anyone-contact "<EMAIL_OR_TELEGRAM>"
--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

View File

@ -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 <IP> --password '<PASS>' --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 <IP> --password '<PASS>' --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 <ip>` | Set up a new node (SSH key + install + join) |
| `orama nodes [--env]` | List your nodes |
| `orama ssh <ip> [--env]` | SSH into a node |
| `orama status [--env] [--json]` | Health check all nodes |
| `orama push [--ip <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 |

View File

@ -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 <IP> --password '<PASS>' --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 <IP> --password '<PASS>' --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

View File

@ -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 |