mirror of
https://github.com/DeBrosOfficial/orama.git
synced 2026-05-01 07:14:13 +00:00
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:
parent
ab1be4105c
commit
f3f4a84762
@ -19,6 +19,7 @@ import (
|
|||||||
"github.com/DeBrosOfficial/network/pkg/cli/cmd/namespacecmd"
|
"github.com/DeBrosOfficial/network/pkg/cli/cmd/namespacecmd"
|
||||||
"github.com/DeBrosOfficial/network/pkg/cli/cmd/node"
|
"github.com/DeBrosOfficial/network/pkg/cli/cmd/node"
|
||||||
"github.com/DeBrosOfficial/network/pkg/cli/cmd/nodescmd"
|
"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/rolloutcmd"
|
||||||
"github.com/DeBrosOfficial/network/pkg/cli/cmd/sandboxcmd"
|
"github.com/DeBrosOfficial/network/pkg/cli/cmd/sandboxcmd"
|
||||||
"github.com/DeBrosOfficial/network/pkg/cli/cmd/sshcmd"
|
"github.com/DeBrosOfficial/network/pkg/cli/cmd/sshcmd"
|
||||||
@ -97,6 +98,7 @@ and interacting with the Orama distributed network.`,
|
|||||||
|
|
||||||
// Unified node management commands
|
// Unified node management commands
|
||||||
rootCmd.AddCommand(nodescmd.Cmd)
|
rootCmd.AddCommand(nodescmd.Cmd)
|
||||||
|
rootCmd.AddCommand(pushcmd.Cmd)
|
||||||
rootCmd.AddCommand(rolloutcmd.Cmd)
|
rootCmd.AddCommand(rolloutcmd.Cmd)
|
||||||
rootCmd.AddCommand(statuscmd.Cmd)
|
rootCmd.AddCommand(statuscmd.Cmd)
|
||||||
rootCmd.AddCommand(sshcmd.Cmd)
|
rootCmd.AddCommand(sshcmd.Cmd)
|
||||||
|
|||||||
@ -40,6 +40,7 @@ func init() {
|
|||||||
setupCmd.Flags().StringVar(&setupOpts.User, "user", "root", "SSH user on the VPS")
|
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.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.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.Genesis, "genesis", false, "Create a new cluster (first node)")
|
||||||
setupCmd.Flags().BoolVar(&setupOpts.AnyoneRelay, "anyone-relay", false, "Run as Anyone relay operator")
|
setupCmd.Flags().BoolVar(&setupOpts.AnyoneRelay, "anyone-relay", false, "Run as Anyone relay operator")
|
||||||
setupCmd.MarkFlagRequired("ip")
|
setupCmd.MarkFlagRequired("ip")
|
||||||
|
|||||||
152
core/pkg/cli/cmd/pushcmd/push.go
Normal file
152
core/pkg/cli/cmd/pushcmd/push.go
Normal 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)
|
||||||
|
}
|
||||||
@ -78,6 +78,7 @@ func sshInto(node inspector.Node) error {
|
|||||||
sshCmd := exec.Command(sshBin,
|
sshCmd := exec.Command(sshBin,
|
||||||
"-i", keyPath,
|
"-i", keyPath,
|
||||||
"-o", "StrictHostKeyChecking=accept-new",
|
"-o", "StrictHostKeyChecking=accept-new",
|
||||||
|
"-o", "IdentitiesOnly=yes",
|
||||||
fmt.Sprintf("%s@%s", node.User, node.Host),
|
fmt.Sprintf("%s@%s", node.User, node.Host),
|
||||||
)
|
)
|
||||||
sshCmd.Stdin = os.Stdin
|
sshCmd.Stdin = os.Stdin
|
||||||
|
|||||||
@ -43,6 +43,11 @@ type Flags struct {
|
|||||||
AnyoneFamily string // Comma-separated fingerprints of other relays you operate
|
AnyoneFamily string // Comma-separated fingerprints of other relays you operate
|
||||||
AnyoneBandwidth int // Percentage of VPS bandwidth for relay (default: 30, 0=unlimited)
|
AnyoneBandwidth int // Percentage of VPS bandwidth for relay (default: 30, 0=unlimited)
|
||||||
AnyoneAccounting int // Monthly data cap for relay in GB (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
|
// 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.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)")
|
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 := fs.Parse(args); err != nil {
|
||||||
if err == flag.ErrHelp {
|
if err == flag.ErrHelp {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@ -68,6 +68,11 @@ func NewOrchestrator(flags *Flags) (*Orchestrator, error) {
|
|||||||
setup.SetAnyoneClient(true)
|
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)
|
validator := NewValidator(flags, oramaDir)
|
||||||
|
|
||||||
return &Orchestrator{
|
return &Orchestrator{
|
||||||
|
|||||||
@ -38,7 +38,8 @@ type Options struct {
|
|||||||
User string // SSH user (default: "root")
|
User string // SSH user (default: "root")
|
||||||
Password string // One-time password for initial SSH access
|
Password string // One-time password for initial SSH access
|
||||||
BaseDomain string
|
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
|
AnyoneRelay bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -154,6 +155,25 @@ func Run(opts Options) error {
|
|||||||
return fmt.Errorf("install failed: %w", err)
|
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)
|
fmt.Printf("\n Node %s setup complete!\n", opts.IP)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -176,6 +196,8 @@ func installPublicKey(ip, user, password, pubKey string) error {
|
|||||||
"ssh",
|
"ssh",
|
||||||
"-o", "StrictHostKeyChecking=no",
|
"-o", "StrictHostKeyChecking=no",
|
||||||
"-o", "ConnectTimeout=10",
|
"-o", "ConnectTimeout=10",
|
||||||
|
"-o", "PreferredAuthentications=password",
|
||||||
|
"-o", "PubkeyAuthentication=no",
|
||||||
fmt.Sprintf("%s@%s", user, ip),
|
fmt.Sprintf("%s@%s", user, ip),
|
||||||
cmd,
|
cmd,
|
||||||
}
|
}
|
||||||
@ -212,22 +234,38 @@ func buildInstallCommand(opts Options, node inspector.Node, agentClient *rwagent
|
|||||||
parts = append(parts, "--anyone-client")
|
parts = append(parts, "--anyone-client")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !opts.Genesis {
|
// Pass operator metadata so the node registers with correct values
|
||||||
// Get gateway URL and invite token
|
if opts.User != "" {
|
||||||
env := opts.Env
|
parts = append(parts, "--ssh-user", opts.User)
|
||||||
if env == "" {
|
}
|
||||||
active, err := cli.GetActiveEnvironment()
|
if opts.Env != "" {
|
||||||
if err != nil {
|
parts = append(parts, "--environment", opts.Env)
|
||||||
return "", fmt.Errorf("failed to get active environment: %w", err)
|
}
|
||||||
}
|
|
||||||
env = active.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
envConfig, err := cli.GetEnvironmentByName(env)
|
// Get wallet address for operator tagging
|
||||||
if err != nil {
|
ctx := context.Background()
|
||||||
return "", fmt.Errorf("environment %q not found: %w", env, err)
|
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
|
// Request invite token via operator API
|
||||||
token, err := requestInviteToken(gatewayURL)
|
token, err := requestInviteToken(gatewayURL)
|
||||||
|
|||||||
@ -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)
|
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 {
|
if cfg.noHostKeyCheck {
|
||||||
args = append([]string{"-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null"}, args...)
|
args = append([]string{"-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null"}, args...)
|
||||||
} else {
|
} else {
|
||||||
@ -73,7 +73,7 @@ func RunSSHStreaming(node inspector.Node, command string, opts ...SSHOption) err
|
|||||||
o(&cfg)
|
o(&cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
args := []string{"-o", "ConnectTimeout=10", "-i", node.SSHKey}
|
args := []string{"-o", "ConnectTimeout=10", "-o", "IdentitiesOnly=yes", "-i", node.SSHKey}
|
||||||
if cfg.noHostKeyCheck {
|
if cfg.noHostKeyCheck {
|
||||||
args = append([]string{"-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null"}, args...)
|
args = append([]string{"-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null"}, args...)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -7,4 +7,7 @@ type NodeConfig struct {
|
|||||||
DataDir string `yaml:"data_dir"` // Data directory
|
DataDir string `yaml:"data_dir"` // Data directory
|
||||||
MaxConnections int `yaml:"max_connections"` // Maximum peer connections
|
MaxConnections int `yaml:"max_connections"` // Maximum peer connections
|
||||||
Domain string `yaml:"domain"` // Domain for this node (e.g., node-1.orama.network)
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,7 +20,10 @@ import (
|
|||||||
|
|
||||||
// ConfigGenerator manages generation of node, gateway, and service configs
|
// ConfigGenerator manages generation of node, gateway, and service configs
|
||||||
type ConfigGenerator struct {
|
type ConfigGenerator struct {
|
||||||
oramaDir string
|
oramaDir string
|
||||||
|
SSHUser string // Operator metadata
|
||||||
|
Environment string
|
||||||
|
OperatorWallet string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewConfigGenerator creates a new config generator
|
// 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
|
// HTTPS is still used for client-facing gateway traffic via autocert
|
||||||
// TLS can be enabled manually later if needed for inter-node encryption
|
// 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)
|
return templates.RenderNodeConfig(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -53,6 +53,11 @@ type ProductionSetup struct {
|
|||||||
serviceController *SystemdController
|
serviceController *SystemdController
|
||||||
binaryInstaller *BinaryInstaller
|
binaryInstaller *BinaryInstaller
|
||||||
NodePeerID string // Captured during Phase3 for later display
|
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
|
// 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...")
|
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)
|
// Node config (unified architecture)
|
||||||
nodeConfig, err := ps.configGenerator.GenerateNodeConfig(peerAddresses, vpsIP, joinAddress, domain, baseDomain, enableHTTPS)
|
nodeConfig, err := ps.configGenerator.GenerateNodeConfig(peerAddresses, vpsIP, joinAddress, domain, baseDomain, enableHTTPS)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -5,6 +5,15 @@ node:
|
|||||||
data_dir: "{{.DataDir}}"
|
data_dir: "{{.DataDir}}"
|
||||||
max_connections: 50
|
max_connections: 50
|
||||||
domain: "{{.Domain}}"
|
domain: "{{.Domain}}"
|
||||||
|
{{- if .SSHUser}}
|
||||||
|
ssh_user: "{{.SSHUser}}"
|
||||||
|
{{- end}}
|
||||||
|
{{- if .Environment}}
|
||||||
|
environment: "{{.Environment}}"
|
||||||
|
{{- end}}
|
||||||
|
{{- if .OperatorWallet}}
|
||||||
|
operator_wallet: "{{.OperatorWallet}}"
|
||||||
|
{{- end}}
|
||||||
|
|
||||||
database:
|
database:
|
||||||
data_dir: "{{.DataDir}}/rqlite"
|
data_dir: "{{.DataDir}}/rqlite"
|
||||||
|
|||||||
@ -41,6 +41,11 @@ type NodeConfigData struct {
|
|||||||
NodeKey string // Path to X.509 private key for node-to-node communication
|
NodeKey string // Path to X.509 private key for node-to-node communication
|
||||||
NodeCACert string // Path to CA certificate (optional)
|
NodeCACert string // Path to CA certificate (optional)
|
||||||
NodeNoVerify bool // Skip certificate verification (for self-signed certs)
|
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
|
// GatewayConfigData holds parameters for gateway.yaml rendering
|
||||||
|
|||||||
@ -88,6 +88,7 @@ func runSSHOnce(ctx context.Context, node Node, command string) SSHResult {
|
|||||||
"-o", "StrictHostKeyChecking=accept-new",
|
"-o", "StrictHostKeyChecking=accept-new",
|
||||||
"-o", "ConnectTimeout=10",
|
"-o", "ConnectTimeout=10",
|
||||||
"-o", "BatchMode=yes",
|
"-o", "BatchMode=yes",
|
||||||
|
"-o", "IdentitiesOnly=yes",
|
||||||
"-i", node.SSHKey,
|
"-i", node.SSHKey,
|
||||||
fmt.Sprintf("%s@%s", node.User, node.Host),
|
fmt.Sprintf("%s@%s", node.User, node.Host),
|
||||||
command,
|
command,
|
||||||
|
|||||||
@ -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)
|
// Determine region (defaulting to "local" for now, could be from cloud metadata in future)
|
||||||
region := "local"
|
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
|
// Insert or update node record
|
||||||
query := `
|
query := `
|
||||||
INSERT INTO dns_nodes (id, ip_address, internal_ip, region, status, last_seen, created_at, updated_at)
|
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'))
|
VALUES (?, ?, ?, ?, 'active', ?, ?, ?, datetime('now'), datetime('now'), datetime('now'))
|
||||||
ON CONFLICT(id) DO UPDATE SET
|
ON CONFLICT(id) DO UPDATE SET
|
||||||
ip_address = excluded.ip_address,
|
ip_address = excluded.ip_address,
|
||||||
internal_ip = excluded.internal_ip,
|
internal_ip = excluded.internal_ip,
|
||||||
region = excluded.region,
|
region = excluded.region,
|
||||||
status = 'active',
|
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'),
|
last_seen = datetime('now'),
|
||||||
updated_at = datetime('now')
|
updated_at = datetime('now')
|
||||||
`
|
`
|
||||||
|
|
||||||
db := n.rqliteAdapter.GetSQLDB()
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to register DNS node: %w", err)
|
return fmt.Errorf("failed to register DNS node: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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:
|
To become an operator, you need:
|
||||||
|
|
||||||
1. A VPS meeting the hardware requirements above
|
1. A VPS meeting the hardware requirements above
|
||||||
2. An invite token (generated by an existing node in the cluster)
|
2. The `orama` CLI on your local machine
|
||||||
3. The `orama` binary on your VPS
|
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
|
```bash
|
||||||
# On an existing node (requires sudo)
|
orama node setup \
|
||||||
sudo orama node invite --expiry 24h
|
--ip <VPS_IP> \
|
||||||
```
|
--password '<VPS_PASSWORD>' \
|
||||||
|
--env devnet \
|
||||||
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> \
|
|
||||||
--base-domain <BASE_DOMAIN> \
|
--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
|
```bash
|
||||||
sudo orama node install \
|
orama auth login
|
||||||
--join http://<GENESIS_IP> --token <TOKEN> \
|
```
|
||||||
--vps-ip <PUBLIC_IP> \
|
|
||||||
--domain <BASE_DOMAIN> \
|
### 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> \
|
--base-domain <BASE_DOMAIN> \
|
||||||
--nameserver
|
--role nameserver
|
||||||
```
|
|
||||||
|
|
||||||
**Regular node (joins existing cluster, domain is auto-generated):**
|
# Add a regular node
|
||||||
|
orama node setup \
|
||||||
```bash
|
--ip <VPS_IP> \
|
||||||
sudo orama node install \
|
--password '<VPS_PASSWORD>' \
|
||||||
--join http://<GENESIS_IP> --token <TOKEN> \
|
--env devnet \
|
||||||
--vps-ip <PUBLIC_IP> \
|
|
||||||
--base-domain <BASE_DOMAIN>
|
--base-domain <BASE_DOMAIN>
|
||||||
```
|
|
||||||
|
|
||||||
**Regular node with Anyone relay:**
|
# Add a node with Anyone relay
|
||||||
|
orama node setup \
|
||||||
```bash
|
--ip <VPS_IP> \
|
||||||
sudo orama node install \
|
--password '<VPS_PASSWORD>' \
|
||||||
--join http://<GENESIS_IP> --token <TOKEN> \
|
--env devnet \
|
||||||
--vps-ip <PUBLIC_IP> \
|
|
||||||
--base-domain <BASE_DOMAIN> \
|
--base-domain <BASE_DOMAIN> \
|
||||||
--anyone-relay \
|
--anyone-relay
|
||||||
--anyone-nickname <NAME> \
|
|
||||||
--anyone-wallet <ETH_ADDRESS> \
|
|
||||||
--anyone-contact "<EMAIL_OR_TELEGRAM>"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 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 |
|
| Flag | Required | Description |
|
||||||
|------|----------|-------------|
|
|------|----------|-------------|
|
||||||
| `--vps-ip` | Yes | Public IP address of this VPS |
|
| `--ip` | Yes | Public IP address of the VPS |
|
||||||
| `--base-domain` | Yes | Base domain for deployment routing (e.g., `orama-devnet.network`) |
|
| `--password` | No | One-time VPS password for SSH key installation |
|
||||||
| `--domain` | No | Domain for HTTPS. Auto-generated for non-nameserver nodes (e.g., `node-a3f8k2.orama-devnet.network`) |
|
| `--env` | No | Target environment (default: active environment) |
|
||||||
| `--nameserver` | No | Make this node a nameserver (runs CoreDNS + Caddy) |
|
| `--base-domain` | Yes | Base domain for the network (e.g., `orama-devnet.network`) |
|
||||||
| `--join` | No | URL of an existing node to join (e.g., `http://1.2.3.4`) |
|
| `--role` | No | `node` (default) or `nameserver` |
|
||||||
| `--token` | No | Invite token from `orama node invite` on an existing node |
|
| `--user` | No | SSH user (default: `root`) |
|
||||||
| `--force` | No | Force reconfiguration even if already installed |
|
| `--gateway` | No | Gateway URL override for invite tokens |
|
||||||
| `--dry-run` | No | Show what would be done without making changes |
|
| `--genesis` | No | Create a new cluster (first node only) |
|
||||||
| `--skip-checks` | No | Skip minimum resource checks (RAM/CPU) |
|
| `--anyone-relay` | No | Run as Anyone relay operator |
|
||||||
| `--skip-firewall` | No | Skip UFW firewall setup (for users who manage their own firewall) |
|
|
||||||
|
|
||||||
#### Anyone Relay Flags
|
#### Anyone Relay Flags
|
||||||
|
|
||||||
|
|||||||
@ -98,13 +98,46 @@ For detailed monitoring with alerts, use the full monitor:
|
|||||||
orama monitor report --env devnet
|
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
|
## 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)
|
1. Authenticates with the cluster using your wallet (via RootWallet)
|
||||||
2. The new node joins using that token
|
2. Requests an invite token tagged with your wallet address
|
||||||
3. The network tags the node with your wallet address from the token
|
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.
|
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 |
|
| Command | Description |
|
||||||
|---------|-------------|
|
|---------|-------------|
|
||||||
|
| `orama node setup --ip <ip>` | Set up a new node (SSH key + install + join) |
|
||||||
| `orama nodes [--env]` | List your nodes |
|
| `orama nodes [--env]` | List your nodes |
|
||||||
| `orama ssh <ip> [--env]` | SSH into a node |
|
| `orama ssh <ip> [--env]` | SSH into a node |
|
||||||
| `orama status [--env] [--json]` | Health check all nodes |
|
| `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 rollout [--env] [--delay]` | Rolling upgrade all nodes |
|
||||||
| `orama node migrate-conf [--env]` | Migrate nodes.conf to wallet-based tracking |
|
| `orama node migrate-conf [--env]` | Migrate nodes.conf to wallet-based tracking |
|
||||||
|
|||||||
@ -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.
|
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
|
## System Requirements
|
||||||
|
|
||||||
|
|||||||
@ -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
|
```bash
|
||||||
# Push to all nodes in an environment
|
|
||||||
orama node push --env devnet
|
|
||||||
|
|
||||||
# Push to a single node
|
# 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)
|
# Push to all nodes in an environment
|
||||||
orama node push --env testnet --direct
|
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 |
|
| Flag | Default | Description |
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user