Files
bzzz/api/setup_manager.go
anthonyrawlins c177363a19 Save current BZZZ config-ui state before CHORUS branding update
🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-19 00:19:00 +10:00

1398 lines
36 KiB
Go

package api
import (
"context"
"fmt"
"net"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
"sync"
"time"
"golang.org/x/crypto/ssh"
"chorus.services/bzzz/pkg/config"
"chorus.services/bzzz/repository"
)
// SetupManager handles the initial configuration setup for BZZZ
type SetupManager struct {
configPath string
factory repository.ProviderFactory
}
// NewSetupManager creates a new setup manager
func NewSetupManager(configPath string) *SetupManager {
return &SetupManager{
configPath: configPath,
factory: &repository.DefaultProviderFactory{},
}
}
// IsSetupRequired checks if initial setup is needed
func (s *SetupManager) IsSetupRequired() bool {
// Check if config file exists and is valid
if _, err := os.Stat(s.configPath); os.IsNotExist(err) {
return true
}
// Try to load and validate existing config
cfg, err := config.LoadConfig(s.configPath)
if err != nil {
return true
}
// Check if essential configuration is present
return cfg.Agent.ID == "" || cfg.WHOOSHAPI.BaseURL == ""
}
// SystemInfo holds system detection information
type SystemInfo struct {
OS string `json:"os"`
Architecture string `json:"architecture"`
CPUCores int `json:"cpu_cores"`
Memory int64 `json:"memory_mb"`
GPUs []GPUInfo `json:"gpus"`
Network NetworkInfo `json:"network"`
Storage StorageInfo `json:"storage"`
Docker DockerInfo `json:"docker"`
}
// GPUInfo holds GPU detection information
type GPUInfo struct {
Name string `json:"name"`
Memory string `json:"memory"`
Driver string `json:"driver"`
Type string `json:"type"` // nvidia, amd, intel
}
// NetworkInfo holds network configuration
type NetworkInfo struct {
Hostname string `json:"hostname"`
Interfaces []string `json:"interfaces"`
PublicIP string `json:"public_ip,omitempty"`
PrivateIPs []string `json:"private_ips"`
DockerBridge string `json:"docker_bridge,omitempty"`
}
// StorageInfo holds storage information
type StorageInfo struct {
TotalSpace int64 `json:"total_space_gb"`
FreeSpace int64 `json:"free_space_gb"`
MountPath string `json:"mount_path"`
}
// DockerInfo holds Docker environment information
type DockerInfo struct {
Available bool `json:"available"`
Version string `json:"version,omitempty"`
ComposeAvailable bool `json:"compose_available"`
SwarmMode bool `json:"swarm_mode"`
}
// DetectSystemInfo performs comprehensive system detection
func (s *SetupManager) DetectSystemInfo() (*SystemInfo, error) {
info := &SystemInfo{
OS: runtime.GOOS,
Architecture: runtime.GOARCH,
CPUCores: runtime.NumCPU(),
}
// Detect memory
if memory, err := s.detectMemory(); err == nil {
info.Memory = memory
}
// Detect GPUs
if gpus, err := s.detectGPUs(); err == nil {
info.GPUs = gpus
}
// Detect network configuration
if network, err := s.detectNetwork(); err == nil {
info.Network = network
}
// Detect storage
if storage, err := s.detectStorage(); err == nil {
info.Storage = storage
}
// Detect Docker
if docker, err := s.detectDocker(); err == nil {
info.Docker = docker
}
return info, nil
}
// detectMemory detects system memory
func (s *SetupManager) detectMemory() (int64, error) {
switch runtime.GOOS {
case "linux":
// Read from /proc/meminfo
content, err := os.ReadFile("/proc/meminfo")
if err != nil {
return 0, err
}
lines := strings.Split(string(content), "\n")
for _, line := range lines {
if strings.HasPrefix(line, "MemTotal:") {
parts := strings.Fields(line)
if len(parts) >= 2 {
kb, err := strconv.ParseInt(parts[1], 10, 64)
if err == nil {
return kb / 1024, nil // Convert KB to MB
}
}
}
}
case "darwin":
// Use sysctl on macOS
cmd := exec.Command("sysctl", "-n", "hw.memsize")
output, err := cmd.Output()
if err == nil {
bytes, err := strconv.ParseInt(strings.TrimSpace(string(output)), 10, 64)
if err == nil {
return bytes / (1024 * 1024), nil // Convert bytes to MB
}
}
}
return 0, fmt.Errorf("memory detection not supported on %s", runtime.GOOS)
}
// detectGPUs detects available GPUs
func (s *SetupManager) detectGPUs() ([]GPUInfo, error) {
var gpus []GPUInfo
// Try NVIDIA GPUs first
if nvidiaGPUs, err := s.detectNVIDIAGPUs(); err == nil {
gpus = append(gpus, nvidiaGPUs...)
}
// Try AMD GPUs
if amdGPUs, err := s.detectAMDGPUs(); err == nil {
gpus = append(gpus, amdGPUs...)
}
// Try Intel GPUs (basic detection)
if intelGPUs, err := s.detectIntelGPUs(); err == nil {
gpus = append(gpus, intelGPUs...)
}
return gpus, nil
}
// detectNVIDIAGPUs detects NVIDIA GPUs using nvidia-smi
func (s *SetupManager) detectNVIDIAGPUs() ([]GPUInfo, error) {
var gpus []GPUInfo
// Check if nvidia-smi is available
cmd := exec.Command("nvidia-smi", "--query-gpu=name,memory.total,driver_version", "--format=csv,noheader,nounits")
output, err := cmd.Output()
if err != nil {
return nil, err
}
lines := strings.Split(strings.TrimSpace(string(output)), "\n")
for _, line := range lines {
if line == "" {
continue
}
parts := strings.Split(line, ", ")
if len(parts) >= 3 {
gpu := GPUInfo{
Name: strings.TrimSpace(parts[0]),
Memory: strings.TrimSpace(parts[1]) + " MB",
Driver: strings.TrimSpace(parts[2]),
Type: "nvidia",
}
gpus = append(gpus, gpu)
}
}
return gpus, nil
}
// detectAMDGPUs detects AMD GPUs
func (s *SetupManager) detectAMDGPUs() ([]GPUInfo, error) {
var gpus []GPUInfo
// Try rocm-smi for AMD GPUs
cmd := exec.Command("rocm-smi", "--showproductname", "--showmeminfo", "vram")
output, err := cmd.Output()
if err != nil {
return nil, err
}
// Parse rocm-smi output (simplified)
if strings.Contains(string(output), "GPU") {
gpu := GPUInfo{
Name: "AMD GPU (detected)",
Memory: "Unknown",
Driver: "ROCm",
Type: "amd",
}
gpus = append(gpus, gpu)
}
return gpus, nil
}
// detectIntelGPUs detects Intel integrated GPUs
func (s *SetupManager) detectIntelGPUs() ([]GPUInfo, error) {
var gpus []GPUInfo
switch runtime.GOOS {
case "linux":
// Check for Intel GPU in /sys/class/drm
if _, err := os.Stat("/sys/class/drm/card0"); err == nil {
// Basic Intel GPU detection
gpu := GPUInfo{
Name: "Intel Integrated Graphics",
Memory: "Shared",
Driver: "i915",
Type: "intel",
}
gpus = append(gpus, gpu)
}
}
return gpus, nil
}
// detectNetwork detects network configuration
func (s *SetupManager) detectNetwork() (NetworkInfo, error) {
info := NetworkInfo{
PrivateIPs: []string{},
}
// Get hostname
if hostname, err := os.Hostname(); err == nil {
info.Hostname = hostname
}
// Detect network interfaces (simplified)
switch runtime.GOOS {
case "linux":
cmd := exec.Command("ip", "addr", "show")
output, err := cmd.Output()
if err == nil {
s.parseLinuxNetworkInfo(string(output), &info)
}
case "darwin":
cmd := exec.Command("ifconfig")
output, err := cmd.Output()
if err == nil {
s.parseDarwinNetworkInfo(string(output), &info)
}
}
return info, nil
}
// parseLinuxNetworkInfo parses Linux network info from ip command
func (s *SetupManager) parseLinuxNetworkInfo(output string, info *NetworkInfo) {
lines := strings.Split(output, "\n")
var currentInterface string
for _, line := range lines {
line = strings.TrimSpace(line)
// Interface line
if strings.Contains(line, ": ") && !strings.HasPrefix(line, "inet") {
parts := strings.Split(line, ":")
if len(parts) >= 2 {
currentInterface = strings.TrimSpace(parts[1])
if currentInterface != "lo" { // Skip loopback
info.Interfaces = append(info.Interfaces, currentInterface)
}
}
}
// IP address line
if strings.HasPrefix(line, "inet ") && currentInterface != "lo" {
parts := strings.Fields(line)
if len(parts) >= 2 {
ip := strings.Split(parts[1], "/")[0]
if s.isPrivateIP(ip) {
info.PrivateIPs = append(info.PrivateIPs, ip)
}
}
}
}
}
// parseDarwinNetworkInfo parses macOS network info from ifconfig
func (s *SetupManager) parseDarwinNetworkInfo(output string, info *NetworkInfo) {
lines := strings.Split(output, "\n")
var currentInterface string
for _, line := range lines {
// Interface line
if !strings.HasPrefix(line, "\t") && strings.Contains(line, ":") {
parts := strings.Split(line, ":")
if len(parts) >= 1 {
currentInterface = strings.TrimSpace(parts[0])
if currentInterface != "lo0" { // Skip loopback
info.Interfaces = append(info.Interfaces, currentInterface)
}
}
}
// IP address line
if strings.Contains(line, "inet ") && currentInterface != "lo0" {
parts := strings.Fields(strings.TrimSpace(line))
if len(parts) >= 2 {
ip := parts[1]
if s.isPrivateIP(ip) {
info.PrivateIPs = append(info.PrivateIPs, ip)
}
}
}
}
}
// isPrivateIP checks if an IP address is private
func (s *SetupManager) isPrivateIP(ip string) bool {
return strings.HasPrefix(ip, "192.168.") ||
strings.HasPrefix(ip, "10.") ||
strings.HasPrefix(ip, "172.16.") ||
strings.HasPrefix(ip, "172.17.") ||
strings.HasPrefix(ip, "172.18.") ||
strings.HasPrefix(ip, "172.19.") ||
strings.HasPrefix(ip, "172.20.") ||
strings.HasPrefix(ip, "172.21.") ||
strings.HasPrefix(ip, "172.22.") ||
strings.HasPrefix(ip, "172.23.") ||
strings.HasPrefix(ip, "172.24.") ||
strings.HasPrefix(ip, "172.25.") ||
strings.HasPrefix(ip, "172.26.") ||
strings.HasPrefix(ip, "172.27.") ||
strings.HasPrefix(ip, "172.28.") ||
strings.HasPrefix(ip, "172.29.") ||
strings.HasPrefix(ip, "172.30.") ||
strings.HasPrefix(ip, "172.31.")
}
// detectStorage detects storage information
func (s *SetupManager) detectStorage() (StorageInfo, error) {
info := StorageInfo{
MountPath: "/",
}
// Get current working directory for storage detection
wd, err := os.Getwd()
if err != nil {
wd = "/"
}
info.MountPath = wd
switch runtime.GOOS {
case "linux", "darwin":
cmd := exec.Command("df", "-BG", wd)
output, err := cmd.Output()
if err == nil {
lines := strings.Split(string(output), "\n")
if len(lines) >= 2 {
fields := strings.Fields(lines[1])
if len(fields) >= 4 {
// Parse total and available space
if total, err := strconv.ParseInt(strings.TrimSuffix(fields[1], "G"), 10, 64); err == nil {
info.TotalSpace = total
}
if free, err := strconv.ParseInt(strings.TrimSuffix(fields[3], "G"), 10, 64); err == nil {
info.FreeSpace = free
}
}
}
}
}
return info, nil
}
// detectDocker detects Docker environment
func (s *SetupManager) detectDocker() (DockerInfo, error) {
info := DockerInfo{}
// Check if docker command is available
cmd := exec.Command("docker", "--version")
output, err := cmd.Output()
if err == nil {
info.Available = true
info.Version = strings.TrimSpace(string(output))
}
// Check if docker compose is available (modern Docker includes compose as subcommand)
cmd = exec.Command("docker", "compose", "version")
if err := cmd.Run(); err == nil {
info.ComposeAvailable = true
} else {
// Fallback to legacy docker-compose for older systems
cmd = exec.Command("docker-compose", "--version")
if err := cmd.Run(); err == nil {
info.ComposeAvailable = true
}
}
// Check if Docker is in swarm mode
if info.Available {
cmd = exec.Command("docker", "info", "--format", "{{.Swarm.LocalNodeState}}")
output, err := cmd.Output()
if err == nil && strings.TrimSpace(string(output)) == "active" {
info.SwarmMode = true
}
}
return info, nil
}
// RepositoryConfig holds repository configuration for setup
type RepositoryConfig struct {
Provider string `json:"provider"`
BaseURL string `json:"baseURL,omitempty"`
AccessToken string `json:"accessToken"`
Owner string `json:"owner"`
Repository string `json:"repository"`
}
// ValidateRepositoryConfig validates repository configuration
func (s *SetupManager) ValidateRepositoryConfig(repoConfig *RepositoryConfig) error {
if repoConfig.Provider == "" {
return fmt.Errorf("provider is required")
}
if repoConfig.AccessToken == "" {
return fmt.Errorf("access token is required")
}
if repoConfig.Owner == "" {
return fmt.Errorf("owner is required")
}
if repoConfig.Repository == "" {
return fmt.Errorf("repository is required")
}
// Validate provider-specific requirements
switch strings.ToLower(repoConfig.Provider) {
case "gitea":
if repoConfig.BaseURL == "" {
return fmt.Errorf("base_url is required for Gitea")
}
case "github":
// GitHub uses default URL
if repoConfig.BaseURL == "" {
repoConfig.BaseURL = "https://api.github.com"
}
default:
return fmt.Errorf("unsupported provider: %s", repoConfig.Provider)
}
// Test connection to repository
return s.testRepositoryConnection(repoConfig)
}
// testRepositoryConnection tests connection to the repository
func (s *SetupManager) testRepositoryConnection(repoConfig *RepositoryConfig) error {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
config := &repository.Config{
Provider: repoConfig.Provider,
BaseURL: repoConfig.BaseURL,
AccessToken: repoConfig.AccessToken,
Owner: repoConfig.Owner,
Repository: repoConfig.Repository,
TaskLabel: "bzzz-task",
InProgressLabel: "in-progress",
CompletedLabel: "completed",
BaseBranch: "main",
BranchPrefix: "bzzz/",
}
provider, err := s.factory.CreateProvider(ctx, config)
if err != nil {
return fmt.Errorf("failed to create provider: %w", err)
}
// Try to list tasks to test connection
_, err = provider.ListAvailableTasks()
if err != nil {
return fmt.Errorf("failed to connect to repository: %w", err)
}
return nil
}
// SetupConfig holds the complete setup configuration
type SetupConfig struct {
AgentID string `json:"agent_id"`
Capabilities []string `json:"capabilities"`
Models []string `json:"models"`
Repository *RepositoryConfig `json:"repository"`
Network map[string]interface{} `json:"network"`
Storage map[string]interface{} `json:"storage"`
Security map[string]interface{} `json:"security"`
}
// SaveConfiguration saves the setup configuration to file
func (s *SetupManager) SaveConfiguration(setupConfig *SetupConfig) error {
// Create configuration directory if it doesn't exist
configDir := filepath.Dir(s.configPath)
if err := os.MkdirAll(configDir, 0755); err != nil {
return fmt.Errorf("failed to create config directory: %w", err)
}
// Load default configuration
cfg, err := config.LoadConfig("")
if err != nil {
// If loading fails, we'll create a minimal config
cfg = &config.Config{}
}
// Apply setup configuration
if setupConfig.AgentID != "" {
cfg.Agent.ID = setupConfig.AgentID
}
if len(setupConfig.Capabilities) > 0 {
cfg.Agent.Capabilities = setupConfig.Capabilities
}
if len(setupConfig.Models) > 0 {
cfg.Agent.Models = setupConfig.Models
}
// Configure repository if provided
if setupConfig.Repository != nil {
// This would integrate with the existing repository configuration
// For now, we'll store it in a way that can be used by the main application
}
// Save configuration to file
if err := config.SaveConfig(cfg, s.configPath); err != nil {
return fmt.Errorf("failed to save configuration: %w", err)
}
return nil
}
// GetSupportedProviders returns list of supported repository providers
func (s *SetupManager) GetSupportedProviders() []string {
return s.factory.SupportedProviders()
}
// Machine represents a discovered network machine
type Machine struct {
IP string `json:"ip"`
Hostname string `json:"hostname"`
OS string `json:"os,omitempty"`
OSVersion string `json:"os_version,omitempty"`
SystemInfo map[string]interface{} `json:"system_info,omitempty"`
}
// DiscoverNetworkMachines scans the network subnet for available machines
func (s *SetupManager) DiscoverNetworkMachines(subnet string, sshKey string) ([]Machine, error) {
var machines []Machine
// Parse CIDR subnet
_, ipNet, err := net.ParseCIDR(subnet)
if err != nil {
return nil, fmt.Errorf("invalid subnet: %w", err)
}
// Create a semaphore to limit concurrent goroutines (max 50 for faster scanning)
sem := make(chan struct{}, 50)
var wg sync.WaitGroup
var mu sync.Mutex
// Context for early termination when we have enough results
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Generate list of IPs to scan
var ips []string
// Start from the network address
ip := make(net.IP, len(ipNet.IP))
copy(ip, ipNet.IP.Mask(ipNet.Mask))
for ipNet.Contains(ip) {
// Skip network and broadcast addresses
ipStr := ip.String()
if !ip.Equal(ipNet.IP) && !isBroadcast(ip, ipNet) {
ips = append(ips, ipStr)
}
// Increment IP
inc(ip)
// Limit total IPs to scan (avoid scanning entire /16)
if len(ips) >= 254 {
break
}
}
// Scan IPs with limited concurrency
for _, targetIP := range ips {
// Check if context is cancelled (early termination)
select {
case <-ctx.Done():
break
default:
}
wg.Add(1)
go func(ip string) {
defer wg.Done()
// Check context again
select {
case <-ctx.Done():
return
default:
}
// Acquire semaphore
sem <- struct{}{}
defer func() { <-sem }()
// Quick ping test with shorter timeout
pingCtx, pingCancel := context.WithTimeout(ctx, 500*time.Millisecond)
defer pingCancel()
cmd := exec.CommandContext(pingCtx, "ping", "-c", "1", "-W", "300", ip)
if err := cmd.Run(); err == nil {
// Machine is pingable, try to get more info
machine := Machine{
IP: ip,
Hostname: s.getHostname(ip),
}
mu.Lock()
machines = append(machines, machine)
// Stop early if we have enough machines
if len(machines) >= 20 {
mu.Unlock()
cancel() // Signal other goroutines to stop
return
}
mu.Unlock()
}
}(targetIP)
}
wg.Wait()
return machines, nil
}
// inc increments IP address
func inc(ip net.IP) {
for j := len(ip) - 1; j >= 0; j-- {
ip[j]++
if ip[j] > 0 {
break
}
}
}
// isBroadcast checks if IP is the broadcast address for the network
func isBroadcast(ip net.IP, ipNet *net.IPNet) bool {
if ip == nil || ipNet == nil {
return false
}
// Calculate broadcast address
broadcast := make(net.IP, len(ip))
copy(broadcast, ipNet.IP.Mask(ipNet.Mask))
// Set all host bits to 1
for i := range broadcast {
broadcast[i] |= ^ipNet.Mask[i]
}
return ip.Equal(broadcast)
}
// getHostname attempts to resolve hostname for IP
func (s *SetupManager) getHostname(ip string) string {
names, err := net.LookupAddr(ip)
if err != nil || len(names) == 0 {
return "Unknown"
}
return strings.TrimSuffix(names[0], ".")
}
// SSHTestResult represents the result of SSH connection test
type SSHTestResult struct {
Success bool `json:"success"`
Error string `json:"error,omitempty"`
OS string `json:"os,omitempty"`
OSVersion string `json:"os_version,omitempty"`
SystemInfo map[string]interface{} `json:"system_info,omitempty"`
}
// TestSSHConnection tests SSH connectivity and gathers system info
func (s *SetupManager) TestSSHConnection(ip string, privateKey string, username string, password string, port int) (*SSHTestResult, error) {
result := &SSHTestResult{}
// Validate required parameters
if username == "" {
result.Success = false
result.Error = "SSH username is required"
return result, nil
}
if password == "" {
result.Success = false
result.Error = "SSH password is required"
return result, nil
}
// Set default port if not provided
if port == 0 {
port = 22
}
// SSH client config with password authentication only
config := &ssh.ClientConfig{
User: username,
Auth: []ssh.AuthMethod{
ssh.Password(password),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(), // For setup phase
Timeout: 10 * time.Second,
}
// Connect to SSH with exact credentials provided - no fallbacks
address := fmt.Sprintf("%s:%d", ip, port)
client, err := ssh.Dial("tcp", address, config)
if err != nil {
result.Success = false
result.Error = fmt.Sprintf("SSH connection failed for %s@%s: %v", username, address, err)
return result, nil
}
defer client.Close()
result.Success = true
// Gather system information
session, err := client.NewSession()
if err == nil {
defer session.Close()
// Get OS info
if output, err := session.Output("uname -s"); err == nil {
result.OS = strings.TrimSpace(string(output))
}
// Get OS version
session, _ = client.NewSession()
if output, err := session.Output("lsb_release -d 2>/dev/null || cat /etc/os-release | head -1"); err == nil {
result.OSVersion = strings.TrimSpace(string(output))
}
session.Close()
// Get basic system info
session, _ = client.NewSession()
if output, err := session.Output("nproc && free -m | grep Mem | awk '{print $2}' && df -h / | tail -1 | awk '{print $4}'"); err == nil {
lines := strings.Split(strings.TrimSpace(string(output)), "\n")
systemInfo := make(map[string]interface{})
if len(lines) >= 3 {
if cpu, err := strconv.Atoi(lines[0]); err == nil {
systemInfo["cpu"] = cpu
}
if mem, err := strconv.Atoi(lines[1]); err == nil {
systemInfo["memory"] = mem / 1024 // Convert MB to GB
}
systemInfo["disk"] = lines[2]
}
result.SystemInfo = systemInfo
}
session.Close()
}
return result, nil
}
// DeploymentResult represents the result of service deployment
type DeploymentResult struct {
Success bool `json:"success"`
Error string `json:"error,omitempty"`
Steps []string `json:"steps,omitempty"`
}
// DeployServiceToMachine deploys BZZZ service to a remote machine
func (s *SetupManager) DeployServiceToMachine(ip string, privateKey string, username string, password string, port int, config interface{}) (*DeploymentResult, error) {
result := &DeploymentResult{
Steps: []string{},
}
// Validate required parameters
if username == "" {
result.Success = false
result.Error = "SSH username is required"
return result, nil
}
if password == "" {
result.Success = false
result.Error = "SSH password is required"
return result, nil
}
// Set default port if not provided
if port == 0 {
port = 22
}
// SSH client config with password authentication only
sshConfig := &ssh.ClientConfig{
User: username,
Auth: []ssh.AuthMethod{
ssh.Password(password),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
Timeout: 30 * time.Second,
}
// Connect to SSH with exact credentials provided - no fallbacks
address := fmt.Sprintf("%s:%d", ip, port)
client, err := ssh.Dial("tcp", address, sshConfig)
if err != nil {
result.Success = false
result.Error = fmt.Sprintf("SSH connection failed for %s@%s: %v", username, address, err)
return result, nil
}
defer client.Close()
result.Steps = append(result.Steps, "✅ SSH connection established")
// Copy BZZZ binary
if err := s.copyBinaryToMachine(client); err != nil {
result.Success = false
result.Error = fmt.Sprintf("Failed to copy binary: %v", err)
return result, nil
}
result.Steps = append(result.Steps, "✅ BZZZ binary copied")
// Generate and deploy configuration
if err := s.generateAndDeployConfig(client, ip, config); err != nil {
result.Success = false
result.Error = fmt.Sprintf("Failed to deploy configuration: %v", err)
return result, nil
}
result.Steps = append(result.Steps, "✅ Configuration deployed")
// Configure firewall
if err := s.configureFirewall(client, config); err != nil {
result.Success = false
result.Error = fmt.Sprintf("Failed to configure firewall: %v", err)
return result, nil
}
result.Steps = append(result.Steps, "✅ Firewall configured")
// Create systemd service
if err := s.createSystemdService(client, config); err != nil {
result.Success = false
result.Error = fmt.Sprintf("Failed to create service: %v", err)
return result, nil
}
result.Steps = append(result.Steps, "✅ SystemD service created")
// Start service if auto-start is enabled
configMap, ok := config.(map[string]interface{})
if ok && configMap["autoStart"] == true {
if err := s.startService(client); err != nil {
result.Success = false
result.Error = fmt.Sprintf("Failed to start service: %v", err)
return result, nil
}
result.Steps = append(result.Steps, "✅ BZZZ service started")
}
result.Success = true
return result, nil
}
// copyBinaryToMachine copies the BZZZ binary to remote machine using SCP protocol
func (s *SetupManager) copyBinaryToMachine(client *ssh.Client) error {
// Read current binary
binaryPath, err := os.Executable()
if err != nil {
return err
}
binaryData, err := os.ReadFile(binaryPath)
if err != nil {
return err
}
// SCP protocol implementation
// Create SCP session
session, err := client.NewSession()
if err != nil {
return err
}
defer session.Close()
// Set up pipes
stdin, err := session.StdinPipe()
if err != nil {
return err
}
defer stdin.Close()
stdout, err := session.StdoutPipe()
if err != nil {
return err
}
// Start SCP receive command on remote host
remotePath := "~/bzzz"
go func() {
defer stdin.Close()
// Send SCP header: C<mode> <size> <filename>\n
header := fmt.Sprintf("C0755 %d bzzz\n", len(binaryData))
stdin.Write([]byte(header))
// Wait for acknowledgment
response := make([]byte, 1)
stdout.Read(response)
if response[0] != 0 {
return
}
// Send file content
stdin.Write(binaryData)
// Send final null byte
stdin.Write([]byte{0})
}()
// Execute SCP receive command
cmd := fmt.Sprintf("scp -t %s", remotePath)
if err := session.Run(cmd); err != nil {
return fmt.Errorf("failed to copy binary via SCP: %w", err)
}
// Make the binary executable
session, err = client.NewSession()
if err != nil {
return err
}
defer session.Close()
if err := session.Run("chmod +x ~/bzzz"); err != nil {
return fmt.Errorf("failed to make binary executable: %w", err)
}
// Try to move to /usr/local/bin with sudo, fall back to user bin if needed
session, err = client.NewSession()
if err != nil {
return err
}
defer session.Close()
// First try passwordless sudo
if err := session.Run("sudo -n mv ~/bzzz /usr/local/bin/bzzz && sudo -n chmod +x /usr/local/bin/bzzz"); err != nil {
// If sudo fails, create user bin directory and install there
session, err = client.NewSession()
if err != nil {
return err
}
defer session.Close()
// Create ~/bin directory and add to PATH if it doesn't exist
if err := session.Run("mkdir -p ~/bin && mv ~/bzzz ~/bin/bzzz && chmod +x ~/bin/bzzz"); err != nil {
return fmt.Errorf("failed to install binary to ~/bin: %w", err)
}
// Add ~/bin to PATH in .bashrc if not already there
session, err = client.NewSession()
if err != nil {
return err
}
defer session.Close()
session.Run("grep -q 'export PATH=\"$HOME/bin:$PATH\"' ~/.bashrc || echo 'export PATH=\"$HOME/bin:$PATH\"' >> ~/.bashrc")
}
return nil
}
// createSystemdService creates systemd service file
func (s *SetupManager) createSystemdService(client *ssh.Client, config interface{}) error {
// Determine the correct binary path
session, err := client.NewSession()
if err != nil {
return err
}
defer session.Close()
var stdout strings.Builder
session.Stdout = &stdout
// Check where the binary was installed
binaryPath := "/usr/local/bin/bzzz"
if err := session.Run("test -f /usr/local/bin/bzzz"); err != nil {
// If not in /usr/local/bin, it should be in ~/bin
session, err = client.NewSession()
if err != nil {
return err
}
defer session.Close()
session.Stdout = &stdout
if err := session.Run("echo $HOME/bin/bzzz"); err == nil {
binaryPath = strings.TrimSpace(stdout.String())
}
}
// Create service file that works for both system and user services
serviceFile := fmt.Sprintf(`[Unit]
Description=BZZZ P2P Task Coordination System
Documentation=https://chorus.services/docs/bzzz
After=network.target
[Service]
Type=simple
ExecStart=%s --config %%h/.bzzz/config.yaml
Restart=always
RestartSec=10
Environment=HOME=%%h
[Install]
WantedBy=default.target
`, binaryPath)
// Create service file using a more robust approach
session, err = client.NewSession()
if err != nil {
return err
}
defer session.Close()
// Create service file in temp location first, then move with sudo
cmd := fmt.Sprintf("cat > /tmp/bzzz.service << 'EOF'\n%sEOF", serviceFile)
if err := session.Run(cmd); err != nil {
return fmt.Errorf("failed to create temp service file: %w", err)
}
// Try to install as system service first, fall back to user service
session, err = client.NewSession()
if err != nil {
return err
}
defer session.Close()
// Try passwordless sudo for system service
if err := session.Run("sudo -n mv /tmp/bzzz.service /etc/systemd/system/bzzz.service"); err != nil {
// Sudo failed, create user-level service instead
session, err = client.NewSession()
if err != nil {
return err
}
defer session.Close()
// Create user systemd directory and install service there
if err := session.Run("mkdir -p ~/.config/systemd/user && mv /tmp/bzzz.service ~/.config/systemd/user/bzzz.service"); err != nil {
return fmt.Errorf("failed to install user service file: %w", err)
}
// Reload user systemd and enable service
session, err = client.NewSession()
if err != nil {
return err
}
defer session.Close()
if err := session.Run("systemctl --user daemon-reload && systemctl --user enable bzzz"); err != nil {
return fmt.Errorf("failed to enable user bzzz service: %w", err)
}
// Enable lingering so user services start at boot
session, err = client.NewSession()
if err != nil {
return err
}
defer session.Close()
session.Run("sudo -n loginctl enable-linger $(whoami) 2>/dev/null || true")
} else {
// System service installation succeeded, continue with system setup
session, err = client.NewSession()
if err != nil {
return err
}
defer session.Close()
if err := session.Run("sudo -n useradd -r -s /bin/false bzzz 2>/dev/null || true"); err != nil {
return fmt.Errorf("failed to create bzzz user: %w", err)
}
session, err = client.NewSession()
if err != nil {
return err
}
defer session.Close()
if err := session.Run("sudo -n mkdir -p /opt/bzzz && sudo -n chown bzzz:bzzz /opt/bzzz"); err != nil {
return fmt.Errorf("failed to create bzzz directory: %w", err)
}
// Reload systemd and enable service
session, err = client.NewSession()
if err != nil {
return err
}
defer session.Close()
if err := session.Run("sudo -n systemctl daemon-reload && sudo -n systemctl enable bzzz"); err != nil {
return fmt.Errorf("failed to enable bzzz service: %w", err)
}
}
return nil
}
// startService starts the BZZZ service (system or user level)
func (s *SetupManager) startService(client *ssh.Client) error {
session, err := client.NewSession()
if err != nil {
return err
}
defer session.Close()
// Try system service first, fall back to user service
if err := session.Run("sudo -n systemctl start bzzz"); err != nil {
// Try user service instead
session, err = client.NewSession()
if err != nil {
return err
}
defer session.Close()
return session.Run("systemctl --user start bzzz")
}
return nil
}
// generateAndDeployConfig generates node-specific config.yaml and deploys it
func (s *SetupManager) generateAndDeployConfig(client *ssh.Client, nodeIP string, config interface{}) error {
// Extract configuration from the setup data
configMap, ok := config.(map[string]interface{})
if !ok {
// Log the actual type and value for debugging
return fmt.Errorf("invalid configuration format: expected map[string]interface{}, got %T: %+v", config, config)
}
// Get hostname for unique agent ID
session, err := client.NewSession()
if err != nil {
return err
}
defer session.Close()
var stdout strings.Builder
session.Stdout = &stdout
if err := session.Run("hostname"); err != nil {
return fmt.Errorf("failed to get hostname: %w", err)
}
hostname := strings.TrimSpace(stdout.String())
// Extract ports from configuration
ports := map[string]interface{}{
"api": 8080,
"mcp": 3000,
"webui": 8080,
"p2p": 7000,
}
// Override with configured ports if available
if portsConfig, exists := configMap["ports"]; exists {
if portsMap, ok := portsConfig.(map[string]interface{}); ok {
for key, value := range portsMap {
ports[key] = value
}
}
}
// Extract security configuration
securityConfig := map[string]interface{}{
"cluster_secret": "default-secret",
}
if security, exists := configMap["security"]; exists {
if securityMap, ok := security.(map[string]interface{}); ok {
if secret, exists := securityMap["clusterSecret"]; exists {
securityConfig["cluster_secret"] = secret
}
}
}
// Generate YAML configuration
configYAML := fmt.Sprintf(`# BZZZ Configuration for %s
agent:
id: "%s-agent"
name: "%s Agent"
# Network configuration
network:
listen_ip: "0.0.0.0"
ports:
api: %v
mcp: %v
webui: %v
p2p: %v
# Security configuration
security:
cluster_secret: "%v"
# Storage configuration
storage:
data_dir: "~/.bzzz/data"
# Logging configuration
logging:
level: "info"
file: "~/.bzzz/logs/bzzz.log"
`, hostname, hostname, hostname, ports["api"], ports["mcp"], ports["webui"], ports["p2p"], securityConfig["cluster_secret"])
// Create configuration directory
session, err = client.NewSession()
if err != nil {
return err
}
defer session.Close()
if err := session.Run("mkdir -p ~/.bzzz ~/.bzzz/data ~/.bzzz/logs"); err != nil {
return fmt.Errorf("failed to create config directories: %w", err)
}
// Deploy configuration file
session, err = client.NewSession()
if err != nil {
return err
}
defer session.Close()
stdin, err := session.StdinPipe()
if err != nil {
return err
}
go func() {
defer stdin.Close()
stdin.Write([]byte(configYAML))
}()
if err := session.Run("cat > ~/.bzzz/config.yaml"); err != nil {
return fmt.Errorf("failed to deploy config file: %w", err)
}
return nil
}
// configureFirewall configures firewall rules for BZZZ ports
func (s *SetupManager) configureFirewall(client *ssh.Client, config interface{}) error {
// Extract ports from configuration
configMap, ok := config.(map[string]interface{})
if !ok {
return fmt.Errorf("invalid configuration format in firewall: expected map[string]interface{}, got %T: %+v", config, config)
}
ports := []string{"22"} // Always include SSH
// Add BZZZ ports
if portsConfig, exists := configMap["ports"]; exists {
if portsMap, ok := portsConfig.(map[string]interface{}); ok {
for _, value := range portsMap {
if portStr := fmt.Sprintf("%v", value); portStr != "" {
ports = append(ports, portStr)
}
}
}
}
// Detect firewall system and configure rules
session, err := client.NewSession()
if err != nil {
return err
}
defer session.Close()
// Try ufw first (Ubuntu/Debian)
if err := session.Run("which ufw > /dev/null 2>&1"); err == nil {
return s.configureUFW(client, ports)
}
// Try firewalld (RHEL/CentOS/Fedora)
session, err = client.NewSession()
if err != nil {
return err
}
defer session.Close()
if err := session.Run("which firewall-cmd > /dev/null 2>&1"); err == nil {
return s.configureFirewalld(client, ports)
}
// If no firewall detected, that's okay - just log it
return nil
}
// configureUFW configures UFW firewall rules
func (s *SetupManager) configureUFW(client *ssh.Client, ports []string) error {
for _, port := range ports {
session, err := client.NewSession()
if err != nil {
return err
}
defer session.Close()
// Try with sudo, ignore failures for non-sudo users
cmd := fmt.Sprintf("sudo -n ufw allow %s 2>/dev/null || true", port)
session.Run(cmd)
}
return nil
}
// configureFirewalld configures firewalld rules
func (s *SetupManager) configureFirewalld(client *ssh.Client, ports []string) error {
for _, port := range ports {
session, err := client.NewSession()
if err != nil {
return err
}
defer session.Close()
// Try with sudo, ignore failures for non-sudo users
cmd := fmt.Sprintf("sudo -n firewall-cmd --permanent --add-port=%s/tcp 2>/dev/null || true", port)
session.Run(cmd)
}
// Reload firewall rules
session, err := client.NewSession()
if err != nil {
return err
}
defer session.Close()
session.Run("sudo -n firewall-cmd --reload 2>/dev/null || true")
return nil
}