mirror of
https://github.com/DeBrosOfficial/orama.git
synced 2026-03-17 03:33:01 +00:00
Replace sshpass password auth with RootWallet SSH keys
Replaces plaintext password-based SSH authentication (sshpass) across the entire Go CLI with wallet-derived ed25519 keys via RootWallet. - Add `rw vault ssh agent-load` command to RootWallet CLI for SSH agent forwarding in push fanout - Create wallet.go bridge: PrepareNodeKeys resolves keys from `rw vault ssh get --priv`, writes temp PEMs (0600), zero-overwrites on cleanup - Remove Password field from Node struct, update config parser to new 3-field format (env|user@host|role) - Remove all sshpass branches from inspector/ssh.go and remotessh/ssh.go, require SSHKey on all SSH paths - Add WithAgentForward() option to RunSSHStreaming for hub fanout - Add PrepareNodeKeys + defer cleanup to all 7 entry points: inspect, monitor, push, upgrade, clean, recover, install - Update push fanout to use SSH agent forwarding instead of sshpass on hub - Delete install/ssh.go duplicate, replace with remotessh calls - Create nodes.conf from remote-nodes.conf (topology only, no secrets) - Update all config defaults and help text from remote-nodes.conf to nodes.conf - Use StrictHostKeyChecking=accept-new consistently everywhere
This commit is contained in:
parent
f0d2621199
commit
6898f47e2e
8
go.mod
8
go.mod
@ -20,6 +20,10 @@ require (
|
||||
github.com/miekg/dns v1.1.70
|
||||
github.com/multiformats/go-multiaddr v0.16.0
|
||||
github.com/olric-data/olric v0.7.0
|
||||
github.com/pion/interceptor v0.1.40
|
||||
github.com/pion/rtcp v1.2.15
|
||||
github.com/pion/turn/v4 v4.0.2
|
||||
github.com/pion/webrtc/v4 v4.1.2
|
||||
github.com/rqlite/gorqlite v0.0.0-20250609141355-ac86a4a1c9a8
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/stretchr/testify v1.11.1
|
||||
@ -123,11 +127,9 @@ require (
|
||||
github.com/pion/dtls/v2 v2.2.12 // indirect
|
||||
github.com/pion/dtls/v3 v3.0.6 // indirect
|
||||
github.com/pion/ice/v4 v4.0.10 // indirect
|
||||
github.com/pion/interceptor v0.1.40 // indirect
|
||||
github.com/pion/logging v0.2.3 // indirect
|
||||
github.com/pion/mdns/v2 v2.0.7 // indirect
|
||||
github.com/pion/randutil v0.1.0 // indirect
|
||||
github.com/pion/rtcp v1.2.15 // indirect
|
||||
github.com/pion/rtp v1.8.19 // indirect
|
||||
github.com/pion/sctp v1.8.39 // indirect
|
||||
github.com/pion/sdp/v3 v3.0.13 // indirect
|
||||
@ -136,8 +138,6 @@ require (
|
||||
github.com/pion/stun/v3 v3.0.0 // indirect
|
||||
github.com/pion/transport/v2 v2.2.10 // indirect
|
||||
github.com/pion/transport/v3 v3.0.7 // indirect
|
||||
github.com/pion/turn/v4 v4.0.2 // indirect
|
||||
github.com/pion/webrtc/v4 v4.1.2 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_golang v1.23.0 // indirect
|
||||
|
||||
@ -61,7 +61,7 @@ func ShowHelp() {
|
||||
fmt.Printf("Subcommands:\n")
|
||||
fmt.Printf(" status - Show cluster node status (RQLite + Olric)\n")
|
||||
fmt.Printf(" Options:\n")
|
||||
fmt.Printf(" --all - SSH into all nodes from remote-nodes.conf (TODO)\n")
|
||||
fmt.Printf(" --all - SSH into all nodes from nodes.conf (TODO)\n")
|
||||
fmt.Printf(" health - Run cluster health checks\n")
|
||||
fmt.Printf(" rqlite <subcommand> - RQLite-specific commands\n")
|
||||
fmt.Printf(" status - Show detailed Raft state for local node\n")
|
||||
|
||||
@ -34,7 +34,7 @@ func init() {
|
||||
Cmd.PersistentFlags().StringVar(&flagEnv, "env", "", "Environment: devnet, testnet, mainnet (required)")
|
||||
Cmd.PersistentFlags().BoolVar(&flagJSON, "json", false, "Machine-readable JSON output")
|
||||
Cmd.PersistentFlags().StringVar(&flagNode, "node", "", "Filter to specific node host/IP")
|
||||
Cmd.PersistentFlags().StringVar(&flagConfig, "config", "scripts/remote-nodes.conf", "Path to remote-nodes.conf")
|
||||
Cmd.PersistentFlags().StringVar(&flagConfig, "config", "scripts/nodes.conf", "Path to nodes.conf")
|
||||
Cmd.MarkPersistentFlagRequired("env")
|
||||
|
||||
Cmd.AddCommand(liveCmd)
|
||||
|
||||
@ -9,6 +9,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/DeBrosOfficial/network/pkg/cli/remotessh"
|
||||
"github.com/DeBrosOfficial/network/pkg/inspector"
|
||||
// Import checks package so init() registers the checkers
|
||||
_ "github.com/DeBrosOfficial/network/pkg/inspector/checks"
|
||||
@ -49,7 +50,7 @@ func HandleInspectCommand(args []string) {
|
||||
|
||||
fs := flag.NewFlagSet("inspect", flag.ExitOnError)
|
||||
|
||||
configPath := fs.String("config", "scripts/remote-nodes.conf", "Path to remote-nodes.conf")
|
||||
configPath := fs.String("config", "scripts/nodes.conf", "Path to nodes.conf")
|
||||
env := fs.String("env", "", "Environment to inspect (devnet, testnet)")
|
||||
subsystem := fs.String("subsystem", "all", "Subsystem to inspect (rqlite,olric,ipfs,dns,wg,system,network,anyone,all)")
|
||||
format := fs.String("format", "table", "Output format (table, json)")
|
||||
@ -98,6 +99,14 @@ func HandleInspectCommand(args []string) {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Prepare wallet-derived SSH keys
|
||||
cleanup, err := remotessh.PrepareNodeKeys(nodes)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error preparing SSH keys: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
// Parse subsystems
|
||||
var subsystems []string
|
||||
if *subsystem != "all" {
|
||||
|
||||
@ -8,6 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/DeBrosOfficial/network/pkg/cli/production/report"
|
||||
"github.com/DeBrosOfficial/network/pkg/cli/remotessh"
|
||||
"github.com/DeBrosOfficial/network/pkg/inspector"
|
||||
)
|
||||
|
||||
@ -34,6 +35,13 @@ func CollectOnce(ctx context.Context, cfg CollectorConfig) (*ClusterSnapshot, er
|
||||
return nil, fmt.Errorf("no nodes found for env %q", cfg.Env)
|
||||
}
|
||||
|
||||
// Prepare wallet-derived SSH keys
|
||||
cleanup, err := remotessh.PrepareNodeKeys(nodes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("prepare SSH keys: %w", err)
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
timeout := cfg.Timeout
|
||||
if timeout == 0 {
|
||||
timeout = 30 * time.Second
|
||||
@ -87,7 +95,7 @@ func collectNodeReport(ctx context.Context, node inspector.Node, timeout time.Du
|
||||
return cs
|
||||
}
|
||||
|
||||
// Enrich with node metadata from remote-nodes.conf
|
||||
// Enrich with node metadata from nodes.conf
|
||||
if rpt.Hostname == "" {
|
||||
rpt.Hostname = node.Host
|
||||
}
|
||||
|
||||
@ -63,6 +63,12 @@ func execute(flags *Flags) error {
|
||||
return err
|
||||
}
|
||||
|
||||
cleanup, err := remotessh.PrepareNodeKeys(nodes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
if flags.Node != "" {
|
||||
nodes = remotessh.FilterByIP(nodes, flags.Node)
|
||||
if len(nodes) == 0 {
|
||||
|
||||
@ -7,6 +7,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/DeBrosOfficial/network/pkg/cli/remotessh"
|
||||
"github.com/DeBrosOfficial/network/pkg/inspector"
|
||||
)
|
||||
|
||||
@ -14,39 +15,72 @@ import (
|
||||
// It uploads the source archive, extracts it on the VPS, and runs
|
||||
// the actual install command remotely.
|
||||
type RemoteOrchestrator struct {
|
||||
flags *Flags
|
||||
node inspector.Node
|
||||
flags *Flags
|
||||
node inspector.Node
|
||||
cleanup func()
|
||||
}
|
||||
|
||||
// NewRemoteOrchestrator creates a new remote orchestrator.
|
||||
// It resolves SSH credentials and checks prerequisites.
|
||||
// Resolves SSH credentials via wallet-derived keys and checks prerequisites.
|
||||
func NewRemoteOrchestrator(flags *Flags) (*RemoteOrchestrator, error) {
|
||||
if flags.VpsIP == "" {
|
||||
return nil, fmt.Errorf("--vps-ip is required\nExample: orama install --vps-ip 1.2.3.4 --nameserver --domain orama-testnet.network")
|
||||
}
|
||||
|
||||
// Resolve SSH credentials
|
||||
node, err := resolveSSHCredentials(flags.VpsIP)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve SSH credentials: %w", err)
|
||||
// Try to find this IP in nodes.conf for the correct user
|
||||
user := resolveUser(flags.VpsIP)
|
||||
|
||||
node := inspector.Node{
|
||||
User: user,
|
||||
Host: flags.VpsIP,
|
||||
Role: "node",
|
||||
}
|
||||
|
||||
// Prepare wallet-derived SSH key
|
||||
nodes := []inspector.Node{node}
|
||||
cleanup, err := remotessh.PrepareNodeKeys(nodes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to prepare SSH key: %w\nEnsure you've run: rw vault ssh add %s/%s", err, flags.VpsIP, user)
|
||||
}
|
||||
// PrepareNodeKeys modifies nodes in place
|
||||
node = nodes[0]
|
||||
|
||||
return &RemoteOrchestrator{
|
||||
flags: flags,
|
||||
node: node,
|
||||
flags: flags,
|
||||
node: node,
|
||||
cleanup: cleanup,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// resolveUser looks up the SSH user for a VPS IP from nodes.conf.
|
||||
// Falls back to "root" if not found.
|
||||
func resolveUser(vpsIP string) string {
|
||||
confPath := remotessh.FindNodesConf()
|
||||
if confPath != "" {
|
||||
nodes, err := inspector.LoadNodes(confPath)
|
||||
if err == nil {
|
||||
for _, n := range nodes {
|
||||
if n.Host == vpsIP {
|
||||
return n.User
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return "root"
|
||||
}
|
||||
|
||||
// Execute runs the remote install process.
|
||||
// If a binary archive exists locally, uploads and extracts it on the VPS
|
||||
// so Phase2b auto-detects pre-built mode. Otherwise, source must already
|
||||
// be present on the VPS.
|
||||
func (r *RemoteOrchestrator) Execute() error {
|
||||
defer r.cleanup()
|
||||
|
||||
fmt.Printf("Installing on %s via SSH (%s@%s)...\n\n", r.flags.VpsIP, r.node.User, r.node.Host)
|
||||
|
||||
// Try to upload a binary archive if one exists locally
|
||||
if err := r.uploadBinaryArchive(); err != nil {
|
||||
fmt.Printf(" ⚠️ Binary archive upload skipped: %v\n", err)
|
||||
fmt.Printf(" Binary archive upload skipped: %v\n", err)
|
||||
fmt.Printf(" Proceeding with source mode (source must already be on VPS)\n\n")
|
||||
}
|
||||
|
||||
@ -71,7 +105,7 @@ func (r *RemoteOrchestrator) uploadBinaryArchive() error {
|
||||
|
||||
// Upload to /tmp/ on VPS
|
||||
remoteTmp := "/tmp/" + filepath.Base(archivePath)
|
||||
if err := uploadFile(r.node, archivePath, remoteTmp); err != nil {
|
||||
if err := remotessh.UploadFile(r.node, archivePath, remoteTmp); err != nil {
|
||||
return fmt.Errorf("failed to upload archive: %w", err)
|
||||
}
|
||||
|
||||
@ -79,7 +113,7 @@ func (r *RemoteOrchestrator) uploadBinaryArchive() error {
|
||||
fmt.Printf("Extracting archive on VPS...\n")
|
||||
extractCmd := fmt.Sprintf("%smkdir -p /opt/orama && tar xzf %s -C /opt/orama && rm -f %s && cp /opt/orama/bin/orama /usr/local/bin/orama && chmod +x /usr/local/bin/orama && echo ' ✓ Archive extracted, CLI installed'",
|
||||
r.sudoPrefix(), remoteTmp, remoteTmp)
|
||||
if err := runSSHStreaming(r.node, extractCmd); err != nil {
|
||||
if err := remotessh.RunSSHStreaming(r.node, extractCmd); err != nil {
|
||||
return fmt.Errorf("failed to extract archive on VPS: %w", err)
|
||||
}
|
||||
|
||||
@ -118,7 +152,7 @@ func (r *RemoteOrchestrator) findLocalArchive() string {
|
||||
// runRemoteInstall executes `orama install` on the VPS.
|
||||
func (r *RemoteOrchestrator) runRemoteInstall() error {
|
||||
cmd := r.buildRemoteCommand()
|
||||
return runSSHStreaming(r.node, cmd)
|
||||
return remotessh.RunSSHStreaming(r.node, cmd)
|
||||
}
|
||||
|
||||
// buildRemoteCommand constructs the `sudo orama install` command string
|
||||
|
||||
@ -1,153 +0,0 @@
|
||||
package install
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/DeBrosOfficial/network/pkg/inspector"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
const sourceArchivePath = "/tmp/network-source.tar.gz"
|
||||
|
||||
// resolveSSHCredentials finds SSH credentials for the given VPS IP.
|
||||
// First checks remote-nodes.conf, then prompts interactively.
|
||||
func resolveSSHCredentials(vpsIP string) (inspector.Node, error) {
|
||||
confPath := findRemoteNodesConf()
|
||||
if confPath != "" {
|
||||
nodes, err := inspector.LoadNodes(confPath)
|
||||
if err == nil {
|
||||
for _, n := range nodes {
|
||||
if n.Host == vpsIP {
|
||||
// Expand ~ in SSH key path
|
||||
if n.SSHKey != "" && strings.HasPrefix(n.SSHKey, "~") {
|
||||
home, _ := os.UserHomeDir()
|
||||
n.SSHKey = filepath.Join(home, n.SSHKey[1:])
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Not found in config — prompt interactively
|
||||
return promptSSHCredentials(vpsIP), nil
|
||||
}
|
||||
|
||||
// findRemoteNodesConf searches for the remote-nodes.conf file.
|
||||
func findRemoteNodesConf() string {
|
||||
candidates := []string{
|
||||
"scripts/remote-nodes.conf",
|
||||
"../scripts/remote-nodes.conf",
|
||||
"network/scripts/remote-nodes.conf",
|
||||
}
|
||||
for _, c := range candidates {
|
||||
if _, err := os.Stat(c); err == nil {
|
||||
return c
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// promptSSHCredentials asks the user for SSH credentials interactively.
|
||||
func promptSSHCredentials(vpsIP string) inspector.Node {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
|
||||
fmt.Printf("\nSSH credentials for %s\n", vpsIP)
|
||||
fmt.Print(" SSH user (default: ubuntu): ")
|
||||
user, _ := reader.ReadString('\n')
|
||||
user = strings.TrimSpace(user)
|
||||
if user == "" {
|
||||
user = "ubuntu"
|
||||
}
|
||||
|
||||
fmt.Print(" SSH password: ")
|
||||
passwordBytes, err := term.ReadPassword(int(os.Stdin.Fd()))
|
||||
fmt.Println() // newline after hidden input
|
||||
if err != nil {
|
||||
// Fall back to plain read if terminal is not available
|
||||
password, _ := reader.ReadString('\n')
|
||||
return inspector.Node{
|
||||
User: user,
|
||||
Host: vpsIP,
|
||||
Password: strings.TrimSpace(password),
|
||||
}
|
||||
}
|
||||
password := string(passwordBytes)
|
||||
|
||||
return inspector.Node{
|
||||
User: user,
|
||||
Host: vpsIP,
|
||||
Password: password,
|
||||
}
|
||||
}
|
||||
|
||||
// uploadFile copies a local file to a remote host via SCP.
|
||||
func uploadFile(node inspector.Node, localPath, remotePath string) error {
|
||||
dest := fmt.Sprintf("%s@%s:%s", node.User, node.Host, remotePath)
|
||||
|
||||
var cmd *exec.Cmd
|
||||
if node.SSHKey != "" {
|
||||
cmd = exec.Command("scp",
|
||||
"-o", "StrictHostKeyChecking=no",
|
||||
"-o", "ConnectTimeout=10",
|
||||
"-i", node.SSHKey,
|
||||
localPath, dest,
|
||||
)
|
||||
} else {
|
||||
if _, err := exec.LookPath("sshpass"); err != nil {
|
||||
return fmt.Errorf("sshpass not found — install it: brew install hudochenkov/sshpass/sshpass")
|
||||
}
|
||||
cmd = exec.Command("sshpass", "-p", node.Password,
|
||||
"scp",
|
||||
"-o", "StrictHostKeyChecking=no",
|
||||
"-o", "ConnectTimeout=10",
|
||||
localPath, dest,
|
||||
)
|
||||
}
|
||||
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("SCP failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// runSSHStreaming executes a command on a remote host via SSH,
|
||||
// streaming stdout/stderr to the local terminal in real-time.
|
||||
func runSSHStreaming(node inspector.Node, command string) error {
|
||||
var cmd *exec.Cmd
|
||||
if node.SSHKey != "" {
|
||||
cmd = exec.Command("ssh",
|
||||
"-o", "StrictHostKeyChecking=no",
|
||||
"-o", "ConnectTimeout=10",
|
||||
"-i", node.SSHKey,
|
||||
fmt.Sprintf("%s@%s", node.User, node.Host),
|
||||
command,
|
||||
)
|
||||
} else {
|
||||
cmd = exec.Command("sshpass", "-p", node.Password,
|
||||
"ssh",
|
||||
"-o", "StrictHostKeyChecking=no",
|
||||
"-o", "ConnectTimeout=10",
|
||||
fmt.Sprintf("%s@%s", node.User, node.Host),
|
||||
command,
|
||||
)
|
||||
}
|
||||
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdin = os.Stdin // Allow password prompts from remote sudo
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("SSH command failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -72,6 +72,13 @@ func execute(flags *Flags) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Prepare wallet-derived SSH keys
|
||||
cleanup, err := remotessh.PrepareNodeKeys(nodes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
// Filter to single node if specified
|
||||
if flags.Node != "" {
|
||||
nodes = remotessh.FilterByIP(nodes, flags.Node)
|
||||
@ -86,6 +93,11 @@ func execute(flags *Flags) error {
|
||||
return pushDirect(archivePath, nodes)
|
||||
}
|
||||
|
||||
// Load keys into ssh-agent for fanout forwarding
|
||||
if err := remotessh.LoadAgentKeys(nodes); err != nil {
|
||||
return fmt.Errorf("load agent keys for fanout: %w", err)
|
||||
}
|
||||
|
||||
return pushFanout(archivePath, nodes)
|
||||
}
|
||||
|
||||
@ -111,7 +123,7 @@ func pushDirect(archivePath string, nodes []inspector.Node) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// pushFanout uploads to a hub node, then fans out to all others via server-to-server SCP.
|
||||
// pushFanout uploads to a hub node, then fans out to all others via agent forwarding.
|
||||
func pushFanout(archivePath string, nodes []inspector.Node) error {
|
||||
hub := remotessh.PickHubNode(nodes)
|
||||
remotePath := "/tmp/" + filepath.Base(archivePath)
|
||||
@ -127,7 +139,7 @@ func pushFanout(archivePath string, nodes []inspector.Node) error {
|
||||
}
|
||||
fmt.Printf(" ✓ hub %s done\n\n", hub.Host)
|
||||
|
||||
// Step 2: Fan out from hub to remaining nodes in parallel
|
||||
// Step 2: Fan out from hub to remaining nodes in parallel (via agent forwarding)
|
||||
remaining := make([]inspector.Node, 0, len(nodes)-1)
|
||||
for _, n := range nodes {
|
||||
if n.Host != hub.Host {
|
||||
@ -150,11 +162,11 @@ func pushFanout(archivePath string, nodes []inspector.Node) error {
|
||||
go func(idx int, target inspector.Node) {
|
||||
defer wg.Done()
|
||||
|
||||
// SCP from hub to target, then extract
|
||||
scpCmd := fmt.Sprintf("sshpass -p '%s' scp -o StrictHostKeyChecking=no -o ConnectTimeout=10 -o PreferredAuthentications=password -o PubkeyAuthentication=no %s %s@%s:%s",
|
||||
target.Password, remotePath, target.User, target.Host, remotePath)
|
||||
// SCP from hub to target (agent forwarding serves the key)
|
||||
scpCmd := fmt.Sprintf("scp -o StrictHostKeyChecking=accept-new -o ConnectTimeout=10 %s %s@%s:%s",
|
||||
remotePath, target.User, target.Host, remotePath)
|
||||
|
||||
if err := remotessh.RunSSHStreaming(hub, scpCmd); err != nil {
|
||||
if err := remotessh.RunSSHStreaming(hub, scpCmd, remotessh.WithAgentForward()); err != nil {
|
||||
errors[idx] = fmt.Errorf("fanout to %s failed: %w", target.Host, err)
|
||||
return
|
||||
}
|
||||
@ -196,16 +208,17 @@ func extractOnNode(node inspector.Node, remotePath string) error {
|
||||
}
|
||||
|
||||
// extractOnNodeVia extracts the archive on a target node by SSHing through the hub.
|
||||
// Uses agent forwarding so the hub can authenticate to the target.
|
||||
func extractOnNodeVia(hub, target inspector.Node, remotePath string) error {
|
||||
sudo := remotessh.SudoPrefix(target)
|
||||
extractCmd := fmt.Sprintf("%smkdir -p /opt/orama && %star xzf %s -C /opt/orama && %srm -f %s",
|
||||
sudo, sudo, remotePath, sudo, remotePath)
|
||||
|
||||
// SSH from hub to target to extract
|
||||
sshCmd := fmt.Sprintf("sshpass -p '%s' ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 -o PreferredAuthentications=password -o PubkeyAuthentication=no %s@%s '%s'",
|
||||
target.Password, target.User, target.Host, extractCmd)
|
||||
// SSH from hub to target to extract (agent forwarding serves the key)
|
||||
sshCmd := fmt.Sprintf("ssh -o StrictHostKeyChecking=accept-new -o ConnectTimeout=10 %s@%s '%s'",
|
||||
target.User, target.Host, extractCmd)
|
||||
|
||||
return remotessh.RunSSHStreaming(hub, sshCmd)
|
||||
return remotessh.RunSSHStreaming(hub, sshCmd, remotessh.WithAgentForward())
|
||||
}
|
||||
|
||||
// findNewestArchive finds the newest binary archive in /tmp/.
|
||||
|
||||
@ -70,6 +70,12 @@ func execute(flags *Flags) error {
|
||||
return err
|
||||
}
|
||||
|
||||
cleanup, err := remotessh.PrepareNodeKeys(nodes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
// Find leader node
|
||||
leaderNodes := remotessh.FilterByIP(nodes, flags.Leader)
|
||||
if len(leaderNodes) == 0 {
|
||||
|
||||
@ -25,6 +25,12 @@ func (r *RemoteUpgrader) Execute() error {
|
||||
return err
|
||||
}
|
||||
|
||||
cleanup, err := remotessh.PrepareNodeKeys(nodes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
// Filter to single node if specified
|
||||
if r.flags.NodeFilter != "" {
|
||||
nodes = remotessh.FilterByIP(nodes, r.flags.NodeFilter)
|
||||
|
||||
@ -4,24 +4,23 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/DeBrosOfficial/network/pkg/inspector"
|
||||
)
|
||||
|
||||
// FindRemoteNodesConf searches for the remote-nodes.conf file
|
||||
// FindNodesConf searches for the nodes.conf file
|
||||
// in common locations relative to the current directory or project root.
|
||||
func FindRemoteNodesConf() string {
|
||||
func FindNodesConf() string {
|
||||
candidates := []string{
|
||||
"scripts/remote-nodes.conf",
|
||||
"../scripts/remote-nodes.conf",
|
||||
"network/scripts/remote-nodes.conf",
|
||||
"scripts/nodes.conf",
|
||||
"../scripts/nodes.conf",
|
||||
"network/scripts/nodes.conf",
|
||||
}
|
||||
|
||||
// Also check from home dir
|
||||
home, _ := os.UserHomeDir()
|
||||
if home != "" {
|
||||
candidates = append(candidates, filepath.Join(home, ".orama", "remote-nodes.conf"))
|
||||
candidates = append(candidates, filepath.Join(home, ".orama", "nodes.conf"))
|
||||
}
|
||||
|
||||
for _, c := range candidates {
|
||||
@ -32,11 +31,12 @@ func FindRemoteNodesConf() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// LoadEnvNodes loads all nodes for a given environment from remote-nodes.conf.
|
||||
// LoadEnvNodes loads all nodes for a given environment from nodes.conf.
|
||||
// SSHKey fields are NOT set — caller must call PrepareNodeKeys() after this.
|
||||
func LoadEnvNodes(env string) ([]inspector.Node, error) {
|
||||
confPath := FindRemoteNodesConf()
|
||||
confPath := FindNodesConf()
|
||||
if confPath == "" {
|
||||
return nil, fmt.Errorf("remote-nodes.conf not found (checked scripts/, ../scripts/, network/scripts/)")
|
||||
return nil, fmt.Errorf("nodes.conf not found (checked scripts/, ../scripts/, network/scripts/)")
|
||||
}
|
||||
|
||||
nodes, err := inspector.LoadNodes(confPath)
|
||||
@ -49,14 +49,6 @@ func LoadEnvNodes(env string) ([]inspector.Node, error) {
|
||||
return nil, fmt.Errorf("no nodes found for environment %q in %s", env, confPath)
|
||||
}
|
||||
|
||||
// Expand ~ in SSH key paths
|
||||
home, _ := os.UserHomeDir()
|
||||
for i := range filtered {
|
||||
if filtered[i].SSHKey != "" && strings.HasPrefix(filtered[i].SSHKey, "~") {
|
||||
filtered[i].SSHKey = filepath.Join(home, filtered[i].SSHKey[1:])
|
||||
}
|
||||
}
|
||||
|
||||
return filtered, nil
|
||||
}
|
||||
|
||||
|
||||
@ -8,31 +8,34 @@ import (
|
||||
"github.com/DeBrosOfficial/network/pkg/inspector"
|
||||
)
|
||||
|
||||
// SSHOption configures SSH command behavior.
|
||||
type SSHOption func(*sshOptions)
|
||||
|
||||
type sshOptions struct {
|
||||
agentForward bool
|
||||
}
|
||||
|
||||
// WithAgentForward enables SSH agent forwarding (-A flag).
|
||||
// Used by push fanout so the hub can reach targets via the forwarded agent.
|
||||
func WithAgentForward() SSHOption {
|
||||
return func(o *sshOptions) { o.agentForward = true }
|
||||
}
|
||||
|
||||
// UploadFile copies a local file to a remote host via SCP.
|
||||
// Requires node.SSHKey to be set (via PrepareNodeKeys).
|
||||
func UploadFile(node inspector.Node, localPath, remotePath string) error {
|
||||
if node.SSHKey == "" {
|
||||
return fmt.Errorf("no SSH key for %s (call PrepareNodeKeys first)", node.Name())
|
||||
}
|
||||
|
||||
dest := fmt.Sprintf("%s@%s:%s", node.User, node.Host, remotePath)
|
||||
|
||||
var cmd *exec.Cmd
|
||||
if node.SSHKey != "" {
|
||||
cmd = exec.Command("scp",
|
||||
"-o", "StrictHostKeyChecking=no",
|
||||
"-o", "ConnectTimeout=10",
|
||||
"-i", node.SSHKey,
|
||||
localPath, dest,
|
||||
)
|
||||
} else {
|
||||
if _, err := exec.LookPath("sshpass"); err != nil {
|
||||
return fmt.Errorf("sshpass not found — install it: brew install hudochenkov/sshpass/sshpass")
|
||||
}
|
||||
cmd = exec.Command("sshpass", "-p", node.Password,
|
||||
"scp",
|
||||
"-o", "StrictHostKeyChecking=no",
|
||||
"-o", "ConnectTimeout=10",
|
||||
"-o", "PreferredAuthentications=password",
|
||||
"-o", "PubkeyAuthentication=no",
|
||||
localPath, dest,
|
||||
)
|
||||
}
|
||||
cmd := exec.Command("scp",
|
||||
"-o", "StrictHostKeyChecking=accept-new",
|
||||
"-o", "ConnectTimeout=10",
|
||||
"-i", node.SSHKey,
|
||||
localPath, dest,
|
||||
)
|
||||
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
@ -45,28 +48,28 @@ func UploadFile(node inspector.Node, localPath, remotePath string) error {
|
||||
|
||||
// RunSSHStreaming executes a command on a remote host via SSH,
|
||||
// streaming stdout/stderr to the local terminal in real-time.
|
||||
func RunSSHStreaming(node inspector.Node, command string) error {
|
||||
var cmd *exec.Cmd
|
||||
if node.SSHKey != "" {
|
||||
cmd = exec.Command("ssh",
|
||||
"-o", "StrictHostKeyChecking=no",
|
||||
"-o", "ConnectTimeout=10",
|
||||
"-i", node.SSHKey,
|
||||
fmt.Sprintf("%s@%s", node.User, node.Host),
|
||||
command,
|
||||
)
|
||||
} else {
|
||||
cmd = exec.Command("sshpass", "-p", node.Password,
|
||||
"ssh",
|
||||
"-o", "StrictHostKeyChecking=no",
|
||||
"-o", "ConnectTimeout=10",
|
||||
"-o", "PreferredAuthentications=password",
|
||||
"-o", "PubkeyAuthentication=no",
|
||||
fmt.Sprintf("%s@%s", node.User, node.Host),
|
||||
command,
|
||||
)
|
||||
// Requires node.SSHKey to be set (via PrepareNodeKeys).
|
||||
func RunSSHStreaming(node inspector.Node, command string, opts ...SSHOption) error {
|
||||
if node.SSHKey == "" {
|
||||
return fmt.Errorf("no SSH key for %s (call PrepareNodeKeys first)", node.Name())
|
||||
}
|
||||
|
||||
var cfg sshOptions
|
||||
for _, o := range opts {
|
||||
o(&cfg)
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"-o", "StrictHostKeyChecking=accept-new",
|
||||
"-o", "ConnectTimeout=10",
|
||||
"-i", node.SSHKey,
|
||||
}
|
||||
if cfg.agentForward {
|
||||
args = append(args, "-A")
|
||||
}
|
||||
args = append(args, fmt.Sprintf("%s@%s", node.User, node.Host), command)
|
||||
|
||||
cmd := exec.Command("ssh", args...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdin = os.Stdin
|
||||
|
||||
158
pkg/cli/remotessh/wallet.go
Normal file
158
pkg/cli/remotessh/wallet.go
Normal file
@ -0,0 +1,158 @@
|
||||
package remotessh
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/DeBrosOfficial/network/pkg/inspector"
|
||||
)
|
||||
|
||||
// PrepareNodeKeys resolves wallet-derived SSH keys for all nodes.
|
||||
// Calls `rw vault ssh get <host>/<user> --priv` for each unique host/user,
|
||||
// writes PEMs to temp files, and sets node.SSHKey for each node.
|
||||
//
|
||||
// The nodes slice is modified in place — each node.SSHKey is set to
|
||||
// the path of the temporary key file.
|
||||
//
|
||||
// Returns a cleanup function that zero-overwrites and removes all temp files.
|
||||
// Caller must defer cleanup().
|
||||
func PrepareNodeKeys(nodes []inspector.Node) (cleanup func(), err error) {
|
||||
rw, err := rwBinary()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create temp dir for all keys
|
||||
tmpDir, err := os.MkdirTemp("", "orama-ssh-")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create temp dir: %w", err)
|
||||
}
|
||||
|
||||
// Track resolved keys by host/user to avoid duplicate rw calls
|
||||
keyPaths := make(map[string]string) // "host/user" → temp file path
|
||||
var allKeyPaths []string
|
||||
|
||||
for i := range nodes {
|
||||
key := nodes[i].Host + "/" + nodes[i].User
|
||||
if existing, ok := keyPaths[key]; ok {
|
||||
nodes[i].SSHKey = existing
|
||||
continue
|
||||
}
|
||||
|
||||
// Call rw to get the private key PEM
|
||||
pem, err := resolveWalletKey(rw, nodes[i].Host, nodes[i].User)
|
||||
if err != nil {
|
||||
// Cleanup any keys already written before returning error
|
||||
cleanupKeys(tmpDir, allKeyPaths)
|
||||
return nil, fmt.Errorf("resolve key for %s: %w", nodes[i].Name(), err)
|
||||
}
|
||||
|
||||
// Write PEM to temp file with restrictive perms
|
||||
keyFile := filepath.Join(tmpDir, fmt.Sprintf("id_%d", i))
|
||||
if err := os.WriteFile(keyFile, []byte(pem), 0600); err != nil {
|
||||
cleanupKeys(tmpDir, allKeyPaths)
|
||||
return nil, fmt.Errorf("write key for %s: %w", nodes[i].Name(), err)
|
||||
}
|
||||
|
||||
keyPaths[key] = keyFile
|
||||
allKeyPaths = append(allKeyPaths, keyFile)
|
||||
nodes[i].SSHKey = keyFile
|
||||
}
|
||||
|
||||
cleanup = func() {
|
||||
cleanupKeys(tmpDir, allKeyPaths)
|
||||
}
|
||||
return cleanup, nil
|
||||
}
|
||||
|
||||
// LoadAgentKeys loads SSH keys for the given nodes into the system ssh-agent.
|
||||
// Used by push fanout to enable agent forwarding.
|
||||
// Calls `rw vault ssh agent-load <host1/user1> <host2/user2> ...`
|
||||
func LoadAgentKeys(nodes []inspector.Node) error {
|
||||
rw, err := rwBinary()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Deduplicate host/user pairs
|
||||
seen := make(map[string]bool)
|
||||
var targets []string
|
||||
for _, n := range nodes {
|
||||
key := n.Host + "/" + n.User
|
||||
if seen[key] {
|
||||
continue
|
||||
}
|
||||
seen[key] = true
|
||||
targets = append(targets, key)
|
||||
}
|
||||
|
||||
if len(targets) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
args := append([]string{"vault", "ssh", "agent-load"}, targets...)
|
||||
cmd := exec.Command(rw, args...)
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdout = os.Stderr // info messages go to stderr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("rw vault ssh agent-load failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// resolveWalletKey calls `rw vault ssh get <host>/<user> --priv`
|
||||
// and returns the PEM string. Requires an active rw session.
|
||||
func resolveWalletKey(rw string, host, user string) (string, error) {
|
||||
target := host + "/" + user
|
||||
cmd := exec.Command(rw, "vault", "ssh", "get", target, "--priv")
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
stderr := strings.TrimSpace(string(exitErr.Stderr))
|
||||
if strings.Contains(stderr, "No SSH entry") {
|
||||
return "", fmt.Errorf("no vault SSH entry for %s — run: rw vault ssh add %s", target, target)
|
||||
}
|
||||
if strings.Contains(stderr, "not unlocked") || strings.Contains(stderr, "session") {
|
||||
return "", fmt.Errorf("wallet is locked — run: rw unlock")
|
||||
}
|
||||
return "", fmt.Errorf("%s", stderr)
|
||||
}
|
||||
return "", fmt.Errorf("rw command failed: %w", err)
|
||||
}
|
||||
pem := string(out)
|
||||
if !strings.Contains(pem, "BEGIN OPENSSH PRIVATE KEY") {
|
||||
return "", fmt.Errorf("rw returned invalid key for %s", target)
|
||||
}
|
||||
return pem, nil
|
||||
}
|
||||
|
||||
// rwBinary returns the path to the `rw` binary.
|
||||
// Checks RW_PATH env var first, then PATH.
|
||||
func rwBinary() (string, error) {
|
||||
if p := os.Getenv("RW_PATH"); p != "" {
|
||||
if _, err := os.Stat(p); err == nil {
|
||||
return p, nil
|
||||
}
|
||||
return "", fmt.Errorf("RW_PATH=%q not found", p)
|
||||
}
|
||||
|
||||
p, err := exec.LookPath("rw")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("rw not found in PATH — install rootwallet CLI: https://github.com/DeBrosOfficial/rootwallet")
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// cleanupKeys zero-overwrites and removes all key files, then removes the temp dir.
|
||||
func cleanupKeys(tmpDir string, keyPaths []string) {
|
||||
zeros := make([]byte, 512)
|
||||
for _, p := range keyPaths {
|
||||
_ = os.WriteFile(p, zeros, 0600) // zero-overwrite
|
||||
_ = os.Remove(p)
|
||||
}
|
||||
_ = os.Remove(tmpDir)
|
||||
}
|
||||
@ -13,7 +13,6 @@ func makeNode(host, role string) inspector.Node {
|
||||
Environment: "devnet",
|
||||
User: "ubuntu",
|
||||
Host: host,
|
||||
Password: "test",
|
||||
Role: role,
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,14 +7,13 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Node represents a remote node parsed from remote-nodes.conf.
|
||||
// Node represents a remote node parsed from nodes.conf.
|
||||
type Node struct {
|
||||
Environment string // devnet, testnet
|
||||
User string // SSH user
|
||||
Host string // IP or hostname
|
||||
Password string // SSH password
|
||||
Role string // node, nameserver-ns1, nameserver-ns2, nameserver-ns3
|
||||
SSHKey string // optional path to SSH key
|
||||
SSHKey string // populated at runtime by PrepareNodeKeys()
|
||||
}
|
||||
|
||||
// Name returns a short display name for the node (user@host).
|
||||
@ -27,8 +26,8 @@ func (n Node) IsNameserver() bool {
|
||||
return strings.HasPrefix(n.Role, "nameserver")
|
||||
}
|
||||
|
||||
// LoadNodes parses a remote-nodes.conf file into a slice of Nodes.
|
||||
// Format: environment|user@host|password|role|ssh_key (ssh_key optional)
|
||||
// LoadNodes parses a nodes.conf file into a slice of Nodes.
|
||||
// Format: environment|user@host|role
|
||||
func LoadNodes(path string) ([]Node, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
@ -46,20 +45,14 @@ func LoadNodes(path string) ([]Node, error) {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.SplitN(line, "|", 5)
|
||||
if len(parts) < 4 {
|
||||
return nil, fmt.Errorf("line %d: expected at least 4 pipe-delimited fields, got %d", lineNum, len(parts))
|
||||
parts := strings.SplitN(line, "|", 4)
|
||||
if len(parts) < 3 {
|
||||
return nil, fmt.Errorf("line %d: expected 3 pipe-delimited fields (env|user@host|role), got %d", lineNum, len(parts))
|
||||
}
|
||||
|
||||
env := parts[0]
|
||||
userHost := parts[1]
|
||||
password := parts[2]
|
||||
role := parts[3]
|
||||
|
||||
var sshKey string
|
||||
if len(parts) == 5 {
|
||||
sshKey = parts[4]
|
||||
}
|
||||
role := parts[2]
|
||||
|
||||
// Parse user@host
|
||||
at := strings.LastIndex(userHost, "@")
|
||||
@ -73,9 +66,7 @@ func LoadNodes(path string) ([]Node, error) {
|
||||
Environment: env,
|
||||
User: user,
|
||||
Host: host,
|
||||
Password: password,
|
||||
Role: role,
|
||||
SSHKey: sshKey,
|
||||
})
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
|
||||
@ -8,9 +8,9 @@ import (
|
||||
|
||||
func TestLoadNodes(t *testing.T) {
|
||||
content := `# Comment line
|
||||
devnet|ubuntu@1.2.3.4|pass123|node
|
||||
devnet|ubuntu@1.2.3.5|pass456|node
|
||||
devnet|ubuntu@5.6.7.8|pass789|nameserver-ns1|/path/to/key
|
||||
devnet|ubuntu@1.2.3.4|node
|
||||
devnet|ubuntu@1.2.3.5|node
|
||||
devnet|ubuntu@5.6.7.8|nameserver-ns1
|
||||
`
|
||||
path := writeTempFile(t, content)
|
||||
|
||||
@ -33,34 +33,28 @@ devnet|ubuntu@5.6.7.8|pass789|nameserver-ns1|/path/to/key
|
||||
if n.Host != "1.2.3.4" {
|
||||
t.Errorf("node[0].Host = %q, want 1.2.3.4", n.Host)
|
||||
}
|
||||
if n.Password != "pass123" {
|
||||
t.Errorf("node[0].Password = %q, want pass123", n.Password)
|
||||
}
|
||||
if n.Role != "node" {
|
||||
t.Errorf("node[0].Role = %q, want node", n.Role)
|
||||
}
|
||||
if n.SSHKey != "" {
|
||||
t.Errorf("node[0].SSHKey = %q, want empty", n.SSHKey)
|
||||
t.Errorf("node[0].SSHKey = %q, want empty (set at runtime)", n.SSHKey)
|
||||
}
|
||||
|
||||
// Third node with SSH key
|
||||
// Third node with nameserver role
|
||||
n3 := nodes[2]
|
||||
if n3.Role != "nameserver-ns1" {
|
||||
t.Errorf("node[2].Role = %q, want nameserver-ns1", n3.Role)
|
||||
}
|
||||
if n3.SSHKey != "/path/to/key" {
|
||||
t.Errorf("node[2].SSHKey = %q, want /path/to/key", n3.SSHKey)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadNodes_EmptyLines(t *testing.T) {
|
||||
content := `
|
||||
# Full line comment
|
||||
|
||||
devnet|ubuntu@1.2.3.4|pass|node
|
||||
devnet|ubuntu@1.2.3.4|node
|
||||
|
||||
# Another comment
|
||||
devnet|ubuntu@1.2.3.5|pass|node
|
||||
devnet|ubuntu@1.2.3.5|node
|
||||
`
|
||||
path := writeTempFile(t, content)
|
||||
|
||||
@ -78,8 +72,8 @@ func TestLoadNodes_InvalidFormat(t *testing.T) {
|
||||
name string
|
||||
content string
|
||||
}{
|
||||
{"too few fields", "devnet|ubuntu@1.2.3.4|pass\n"},
|
||||
{"no @ in userhost", "devnet|localhost|pass|node\n"},
|
||||
{"too few fields", "devnet|ubuntu@1.2.3.4\n"},
|
||||
{"no @ in userhost", "devnet|localhost|node\n"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
@ -31,7 +31,7 @@ func (r SSHResult) OK() bool {
|
||||
}
|
||||
|
||||
// RunSSH executes a command on a remote node via SSH with retry on connection failure.
|
||||
// Uses sshpass for password auth, falls back to -i for key-based auth.
|
||||
// Requires node.SSHKey to be set (via PrepareNodeKeys).
|
||||
// The -n flag is used to prevent SSH from reading stdin.
|
||||
func RunSSH(ctx context.Context, node Node, command string) SSHResult {
|
||||
var result SSHResult
|
||||
@ -76,30 +76,23 @@ func RunSSH(ctx context.Context, node Node, command string) SSHResult {
|
||||
func runSSHOnce(ctx context.Context, node Node, command string) SSHResult {
|
||||
start := time.Now()
|
||||
|
||||
var args []string
|
||||
if node.SSHKey != "" {
|
||||
// Key-based auth
|
||||
args = []string{
|
||||
"ssh", "-n",
|
||||
"-o", "StrictHostKeyChecking=no",
|
||||
"-o", "ConnectTimeout=10",
|
||||
"-o", "BatchMode=yes",
|
||||
"-i", node.SSHKey,
|
||||
fmt.Sprintf("%s@%s", node.User, node.Host),
|
||||
command,
|
||||
}
|
||||
} else {
|
||||
// Password auth via sshpass
|
||||
args = []string{
|
||||
"sshpass", "-p", node.Password,
|
||||
"ssh", "-n",
|
||||
"-o", "StrictHostKeyChecking=no",
|
||||
"-o", "ConnectTimeout=10",
|
||||
fmt.Sprintf("%s@%s", node.User, node.Host),
|
||||
command,
|
||||
if node.SSHKey == "" {
|
||||
return SSHResult{
|
||||
Duration: 0,
|
||||
Err: fmt.Errorf("no SSH key for %s (call PrepareNodeKeys first)", node.Name()),
|
||||
}
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"ssh", "-n",
|
||||
"-o", "StrictHostKeyChecking=accept-new",
|
||||
"-o", "ConnectTimeout=10",
|
||||
"-o", "BatchMode=yes",
|
||||
"-i", node.SSHKey,
|
||||
fmt.Sprintf("%s@%s", node.User, node.Host),
|
||||
command,
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, args[0], args[1:]...)
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
@ -130,8 +123,6 @@ func runSSHOnce(ctx context.Context, node Node, command string) SSHResult {
|
||||
// isSSHConnectionError returns true if the failure looks like an SSH connection
|
||||
// problem (timeout, refused, network unreachable) rather than a remote command error.
|
||||
func isSSHConnectionError(r SSHResult) bool {
|
||||
// sshpass exit code 5 = invalid/incorrect password (not retriable)
|
||||
// sshpass exit code 6 = host key verification failed (not retriable)
|
||||
// SSH exit code 255 = SSH connection error (retriable)
|
||||
if r.ExitCode == 255 {
|
||||
return true
|
||||
|
||||
42
scripts/nodes.conf
Normal file
42
scripts/nodes.conf
Normal file
@ -0,0 +1,42 @@
|
||||
# Orama Network node topology
|
||||
# Format: environment|user@host|role
|
||||
# Auth: wallet-derived SSH keys (rw vault ssh)
|
||||
#
|
||||
# environment: devnet, testnet
|
||||
# role: node, nameserver-ns1, nameserver-ns2, nameserver-ns3
|
||||
|
||||
# --- Devnet nameservers ---
|
||||
devnet|ubuntu@57.129.7.232|nameserver-ns1
|
||||
devnet|ubuntu@57.131.41.160|nameserver-ns2
|
||||
devnet|ubuntu@51.38.128.56|nameserver-ns3
|
||||
|
||||
# --- Devnet nodes ---
|
||||
devnet|ubuntu@144.217.162.62|node
|
||||
devnet|ubuntu@51.83.128.181|node
|
||||
devnet|ubuntu@144.217.160.15|node
|
||||
devnet|root@46.250.241.133|node
|
||||
devnet|root@109.123.229.231|node
|
||||
devnet|ubuntu@144.217.162.143|node
|
||||
devnet|ubuntu@144.217.163.114|node
|
||||
devnet|root@109.123.239.61|node
|
||||
devnet|root@217.76.56.2|node
|
||||
devnet|ubuntu@198.244.150.237|node
|
||||
devnet|root@154.38.187.158|node
|
||||
|
||||
# --- Testnet nameservers ---
|
||||
testnet|ubuntu@51.195.109.238|nameserver-ns1
|
||||
testnet|ubuntu@57.131.41.159|nameserver-ns1
|
||||
testnet|ubuntu@51.38.130.69|nameserver-ns1
|
||||
|
||||
# --- Testnet nodes ---
|
||||
testnet|root@178.212.35.184|node
|
||||
testnet|root@62.72.44.87|node
|
||||
testnet|ubuntu@51.178.84.172|node
|
||||
testnet|ubuntu@135.125.175.236|node
|
||||
testnet|ubuntu@57.128.223.149|node
|
||||
testnet|root@38.242.221.178|node
|
||||
testnet|root@194.61.28.7|node
|
||||
testnet|root@83.171.248.66|node
|
||||
testnet|ubuntu@141.227.165.168|node
|
||||
testnet|ubuntu@141.227.165.154|node
|
||||
testnet|ubuntu@141.227.156.51|node
|
||||
Loading…
x
Reference in New Issue
Block a user