🎭 CHORUS now contains full BZZZ functionality adapted for containers Core systems ported: - P2P networking (libp2p with DHT and PubSub) - Task coordination (COOEE protocol) - HMMM collaborative reasoning - SHHH encryption and security - SLURP admin election system - UCXL content addressing - UCXI server integration - Hypercore logging system - Health monitoring and graceful shutdown - License validation with KACHING Container adaptations: - Environment variable configuration (no YAML files) - Container-optimized logging to stdout/stderr - Auto-generated agent IDs for container deployments - Docker-first architecture All proven BZZZ P2P protocols, AI integration, and collaboration features are now available in containerized form. Next: Build and test container deployment. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
2476 lines
77 KiB
Go
2476 lines
77 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"golang.org/x/crypto/ssh"
|
|
"chorus.services/bzzz/pkg/config"
|
|
"chorus.services/bzzz/pkg/security"
|
|
"chorus.services/bzzz/repository"
|
|
)
|
|
|
|
// SetupManager handles the initial configuration setup for BZZZ
|
|
type SetupManager struct {
|
|
configPath string
|
|
factory repository.ProviderFactory
|
|
validator *security.SecurityValidator
|
|
}
|
|
|
|
// NewSetupManager creates a new setup manager
|
|
func NewSetupManager(configPath string) *SetupManager {
|
|
return &SetupManager{
|
|
configPath: configPath,
|
|
factory: &repository.DefaultProviderFactory{},
|
|
validator: security.NewSecurityValidator(),
|
|
}
|
|
}
|
|
|
|
// 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{}
|
|
|
|
// SECURITY: Validate all input parameters with zero-trust approach
|
|
if err := s.validator.ValidateSSHConnectionRequest(ip, username, password, privateKey, port); err != nil {
|
|
result.Success = false
|
|
result.Error = fmt.Sprintf("Security validation failed: %s", err.Error())
|
|
return result, nil
|
|
}
|
|
|
|
// Set default port if not provided
|
|
if port == 0 {
|
|
port = 22
|
|
}
|
|
|
|
// SSH client config with flexible authentication
|
|
var authMethods []ssh.AuthMethod
|
|
var authErrors []string
|
|
|
|
if privateKey != "" {
|
|
// Try private key authentication first
|
|
if signer, err := ssh.ParsePrivateKey([]byte(privateKey)); err == nil {
|
|
authMethods = append(authMethods, ssh.PublicKeys(signer))
|
|
} else {
|
|
authErrors = append(authErrors, fmt.Sprintf("Invalid SSH private key: %v", err))
|
|
}
|
|
}
|
|
if password != "" {
|
|
// Add password authentication
|
|
authMethods = append(authMethods, ssh.Password(password))
|
|
}
|
|
|
|
if len(authMethods) == 0 {
|
|
result.Success = false
|
|
result.Error = fmt.Sprintf("No valid authentication methods available. Errors: %v", strings.Join(authErrors, "; "))
|
|
return result, nil
|
|
}
|
|
|
|
config := &ssh.ClientConfig{
|
|
User: username,
|
|
Auth: authMethods,
|
|
HostKeyCallback: ssh.InsecureIgnoreHostKey(), // For setup phase
|
|
Timeout: 10 * time.Second,
|
|
}
|
|
|
|
// Connect to SSH with detailed error reporting
|
|
address := fmt.Sprintf("%s:%d", ip, port)
|
|
client, err := ssh.Dial("tcp", address, config)
|
|
if err != nil {
|
|
result.Success = false
|
|
|
|
// Provide specific error messages based on error type
|
|
if strings.Contains(err.Error(), "connection refused") {
|
|
result.Error = fmt.Sprintf("SSH connection refused to %s:%d - SSH service may not be running or port blocked", ip, port)
|
|
} else if strings.Contains(err.Error(), "permission denied") {
|
|
result.Error = fmt.Sprintf("SSH authentication failed for user '%s' on %s:%d - check username/password/key", username, ip, port)
|
|
} else if strings.Contains(err.Error(), "no route to host") {
|
|
result.Error = fmt.Sprintf("No network route to host %s - check IP address and network connectivity", ip)
|
|
} else if strings.Contains(err.Error(), "timeout") {
|
|
result.Error = fmt.Sprintf("SSH connection timeout to %s:%d - host may be unreachable or SSH service slow", ip, port)
|
|
} else {
|
|
result.Error = fmt.Sprintf("SSH connection failed to %s@%s:%d - %v", username, ip, port, 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 []DeploymentStep `json:"steps,omitempty"`
|
|
RollbackLog []string `json:"rollback_log,omitempty"`
|
|
SystemInfo *DeploymentSystemInfo `json:"system_info,omitempty"`
|
|
}
|
|
|
|
// DeploymentStep represents a single deployment step with detailed status
|
|
type DeploymentStep struct {
|
|
Name string `json:"name"`
|
|
Status string `json:"status"` // "pending", "running", "success", "failed"
|
|
Command string `json:"command,omitempty"`
|
|
Output string `json:"output,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
Duration string `json:"duration,omitempty"`
|
|
Verified bool `json:"verified"`
|
|
}
|
|
|
|
// DeployServiceToMachine deploys BZZZ service to a remote machine with full verification
|
|
func (s *SetupManager) DeployServiceToMachine(ip string, privateKey string, username string, password string, port int, config interface{}) (*DeploymentResult, error) {
|
|
result := &DeploymentResult{
|
|
Steps: []DeploymentStep{},
|
|
RollbackLog: []string{},
|
|
}
|
|
|
|
// SECURITY: Validate all input parameters with zero-trust approach
|
|
if err := s.validator.ValidateSSHConnectionRequest(ip, username, password, privateKey, port); err != nil {
|
|
result.Success = false
|
|
result.Error = fmt.Sprintf("Security validation failed: %s", err.Error())
|
|
return result, nil
|
|
}
|
|
|
|
// Set default port if not provided
|
|
if port == 0 {
|
|
port = 22
|
|
}
|
|
|
|
// SSH client config with flexible authentication
|
|
var authMethods []ssh.AuthMethod
|
|
var authErrors []string
|
|
|
|
if privateKey != "" {
|
|
// Try private key authentication first
|
|
if signer, err := ssh.ParsePrivateKey([]byte(privateKey)); err == nil {
|
|
authMethods = append(authMethods, ssh.PublicKeys(signer))
|
|
} else {
|
|
authErrors = append(authErrors, fmt.Sprintf("Invalid SSH private key: %v", err))
|
|
}
|
|
}
|
|
if password != "" {
|
|
// Add password authentication
|
|
authMethods = append(authMethods, ssh.Password(password))
|
|
}
|
|
|
|
if len(authMethods) == 0 {
|
|
result.Success = false
|
|
result.Error = fmt.Sprintf("No valid authentication methods available. Errors: %v", strings.Join(authErrors, "; "))
|
|
return result, nil
|
|
}
|
|
|
|
sshConfig := &ssh.ClientConfig{
|
|
User: username,
|
|
Auth: authMethods,
|
|
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
|
Timeout: 30 * time.Second,
|
|
}
|
|
|
|
// Connect to SSH with detailed error reporting
|
|
address := fmt.Sprintf("%s:%d", ip, port)
|
|
client, err := ssh.Dial("tcp", address, sshConfig)
|
|
if err != nil {
|
|
result.Success = false
|
|
|
|
// Provide specific error messages based on error type
|
|
if strings.Contains(err.Error(), "connection refused") {
|
|
result.Error = fmt.Sprintf("SSH connection refused to %s:%d - SSH service may not be running or port blocked", ip, port)
|
|
} else if strings.Contains(err.Error(), "permission denied") {
|
|
result.Error = fmt.Sprintf("SSH authentication failed for user '%s' on %s:%d - check username/password/key", username, ip, port)
|
|
} else if strings.Contains(err.Error(), "no route to host") {
|
|
result.Error = fmt.Sprintf("No network route to host %s - check IP address and network connectivity", ip)
|
|
} else if strings.Contains(err.Error(), "timeout") {
|
|
result.Error = fmt.Sprintf("SSH connection timeout to %s:%d - host may be unreachable or SSH service slow", ip, port)
|
|
} else {
|
|
result.Error = fmt.Sprintf("SSH connection failed to %s@%s:%d - %v", username, ip, port, err)
|
|
}
|
|
return result, nil
|
|
}
|
|
defer client.Close()
|
|
|
|
s.addStep(result, "SSH Connection", "success", "", "SSH connection established successfully", "", true)
|
|
|
|
// Execute deployment steps with verification
|
|
steps := []func(*ssh.Client, interface{}, string, *DeploymentResult) error{
|
|
s.verifiedPreDeploymentCheck,
|
|
s.verifiedStopExistingServices,
|
|
s.verifiedCopyBinary,
|
|
s.verifiedDeployConfiguration,
|
|
s.verifiedConfigureFirewall,
|
|
s.verifiedCreateSystemdService,
|
|
s.verifiedStartService,
|
|
s.verifiedPostDeploymentTest,
|
|
}
|
|
|
|
for _, step := range steps {
|
|
if err := step(client, config, password, result); err != nil {
|
|
result.Success = false
|
|
result.Error = err.Error()
|
|
s.performRollbackWithPassword(client, password, result)
|
|
return result, nil
|
|
}
|
|
}
|
|
|
|
result.Success = true
|
|
return result, nil
|
|
}
|
|
|
|
// addStep adds a deployment step to the result with timing information
|
|
func (s *SetupManager) addStep(result *DeploymentResult, name, status, command, output, error string, verified bool) {
|
|
step := DeploymentStep{
|
|
Name: name,
|
|
Status: status,
|
|
Command: command,
|
|
Output: output,
|
|
Error: error,
|
|
Verified: verified,
|
|
Duration: "", // Will be filled by the calling function if needed
|
|
}
|
|
result.Steps = append(result.Steps, step)
|
|
}
|
|
|
|
// executeSSHCommand executes a command via SSH and returns output, error
|
|
func (s *SetupManager) executeSSHCommand(client *ssh.Client, command string) (string, error) {
|
|
session, err := client.NewSession()
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to create SSH session: %w", err)
|
|
}
|
|
defer session.Close()
|
|
|
|
var stdout, stderr strings.Builder
|
|
session.Stdout = &stdout
|
|
session.Stderr = &stderr
|
|
|
|
err = session.Run(command)
|
|
output := stdout.String()
|
|
if stderr.Len() > 0 {
|
|
output += "\n[STDERR]: " + stderr.String()
|
|
}
|
|
|
|
return output, err
|
|
}
|
|
|
|
// executeSudoCommand executes a command with sudo using the provided password, or tries passwordless sudo if no password
|
|
func (s *SetupManager) executeSudoCommand(client *ssh.Client, password string, command string) (string, error) {
|
|
// SECURITY: Sanitize command to prevent injection
|
|
safeCommand := s.validator.SanitizeForCommand(command)
|
|
if safeCommand != command {
|
|
return "", fmt.Errorf("command contained unsafe characters and was sanitized: original='%s', safe='%s'", command, safeCommand)
|
|
}
|
|
|
|
if password != "" {
|
|
// SECURITY: Use here-document to avoid password exposure in process list
|
|
// This keeps the password out of command line arguments and process lists
|
|
escapedPassword := strings.ReplaceAll(password, "'", "'\"'\"'")
|
|
secureCommand := fmt.Sprintf(`sudo -S %s <<'BZZZ_EOF'
|
|
%s
|
|
BZZZ_EOF`, safeCommand, escapedPassword)
|
|
|
|
return s.executeSSHCommand(client, secureCommand)
|
|
} else {
|
|
// Try passwordless sudo
|
|
sudoCommand := fmt.Sprintf("sudo -n %s", safeCommand)
|
|
return s.executeSSHCommand(client, sudoCommand)
|
|
}
|
|
}
|
|
|
|
// DeploymentSystemInfo holds information about the target system for deployment
|
|
type DeploymentSystemInfo struct {
|
|
OS string `json:"os"` // linux, darwin, freebsd, etc.
|
|
Distro string `json:"distro"` // ubuntu, centos, debian, etc.
|
|
ServiceMgr string `json:"service_mgr"` // systemd, sysv, openrc, launchd
|
|
Architecture string `json:"architecture"` // x86_64, arm64, etc.
|
|
BinaryPath string `json:"binary_path"` // Where to install binary
|
|
ServicePath string `json:"service_path"` // Where to install service file
|
|
}
|
|
|
|
// detectSystemInfo detects target system information
|
|
func (s *SetupManager) detectSystemInfo(client *ssh.Client) (*DeploymentSystemInfo, error) {
|
|
info := &DeploymentSystemInfo{}
|
|
|
|
// Detect OS
|
|
osOutput, err := s.executeSSHCommand(client, "uname -s")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to detect OS: %v", err)
|
|
}
|
|
info.OS = strings.ToLower(strings.TrimSpace(osOutput))
|
|
|
|
// Detect architecture
|
|
archOutput, err := s.executeSSHCommand(client, "uname -m")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to detect architecture: %v", err)
|
|
}
|
|
info.Architecture = strings.TrimSpace(archOutput)
|
|
|
|
// Detect distribution (Linux only)
|
|
if info.OS == "linux" {
|
|
if distroOutput, err := s.executeSSHCommand(client, "cat /etc/os-release 2>/dev/null | grep '^ID=' | cut -d= -f2 | tr -d '\"' || echo 'unknown'"); err == nil {
|
|
info.Distro = strings.TrimSpace(distroOutput)
|
|
}
|
|
}
|
|
|
|
// Detect service manager and set paths
|
|
if err := s.detectServiceManager(client, info); err != nil {
|
|
return nil, fmt.Errorf("failed to detect service manager: %v", err)
|
|
}
|
|
|
|
return info, nil
|
|
}
|
|
|
|
// detectServiceManager detects the service manager and sets appropriate paths
|
|
func (s *SetupManager) detectServiceManager(client *ssh.Client, info *DeploymentSystemInfo) error {
|
|
switch info.OS {
|
|
case "linux":
|
|
// Check for systemd
|
|
if _, err := s.executeSSHCommand(client, "which systemctl"); err == nil {
|
|
if pidOutput, err := s.executeSSHCommand(client, "ps -p 1 -o comm="); err == nil && strings.Contains(pidOutput, "systemd") {
|
|
info.ServiceMgr = "systemd"
|
|
info.ServicePath = "/etc/systemd/system"
|
|
info.BinaryPath = "/usr/local/bin"
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// Check for OpenRC
|
|
if _, err := s.executeSSHCommand(client, "which rc-service"); err == nil {
|
|
info.ServiceMgr = "openrc"
|
|
info.ServicePath = "/etc/init.d"
|
|
info.BinaryPath = "/usr/local/bin"
|
|
return nil
|
|
}
|
|
|
|
// Check for SysV init
|
|
if _, err := s.executeSSHCommand(client, "ls /etc/init.d/ 2>/dev/null"); err == nil {
|
|
info.ServiceMgr = "sysv"
|
|
info.ServicePath = "/etc/init.d"
|
|
info.BinaryPath = "/usr/local/bin"
|
|
return nil
|
|
}
|
|
|
|
return fmt.Errorf("unsupported service manager on Linux")
|
|
|
|
case "darwin":
|
|
info.ServiceMgr = "launchd"
|
|
info.ServicePath = "/Library/LaunchDaemons"
|
|
info.BinaryPath = "/usr/local/bin"
|
|
return nil
|
|
|
|
case "freebsd":
|
|
info.ServiceMgr = "rc"
|
|
info.ServicePath = "/usr/local/etc/rc.d"
|
|
info.BinaryPath = "/usr/local/bin"
|
|
return nil
|
|
|
|
default:
|
|
return fmt.Errorf("unsupported operating system: %s", info.OS)
|
|
}
|
|
}
|
|
|
|
// verifiedPreDeploymentCheck checks system requirements and existing installations
|
|
func (s *SetupManager) verifiedPreDeploymentCheck(client *ssh.Client, config interface{}, password string, result *DeploymentResult) error {
|
|
stepName := "Pre-deployment Check"
|
|
s.addStep(result, stepName, "running", "", "", "", false)
|
|
|
|
// Detect system information
|
|
sysInfo, err := s.detectSystemInfo(client)
|
|
if err != nil {
|
|
s.updateLastStep(result, "failed", "system detection", "", fmt.Sprintf("System detection failed: %v", err), false)
|
|
return fmt.Errorf("system detection failed: %v", err)
|
|
}
|
|
|
|
// Store system info for other steps to use
|
|
result.SystemInfo = sysInfo
|
|
|
|
// Check for existing BZZZ processes (informational only - cleanup step will handle)
|
|
output, err := s.executeSSHCommand(client, "ps aux | grep bzzz | grep -v grep || echo 'No BZZZ processes found'")
|
|
if err != nil {
|
|
s.updateLastStep(result, "failed", "process check", output, fmt.Sprintf("Failed to check processes: %v", err), false)
|
|
return fmt.Errorf("pre-deployment check failed: %v", err)
|
|
}
|
|
|
|
// Log existing processes but don't fail - cleanup step will handle this
|
|
var processStatus string
|
|
if !strings.Contains(output, "No BZZZ processes found") {
|
|
processStatus = "Existing BZZZ processes detected (will be stopped in cleanup step)"
|
|
} else {
|
|
processStatus = "No existing BZZZ processes detected"
|
|
}
|
|
|
|
// Check for existing systemd services
|
|
output2, _ := s.executeSSHCommand(client, "systemctl status bzzz 2>/dev/null || echo 'No BZZZ service'")
|
|
|
|
// Check system requirements
|
|
output3, _ := s.executeSSHCommand(client, "uname -a && free -m && df -h /tmp")
|
|
|
|
combinedOutput := fmt.Sprintf("Process status: %s\n\nProcess details:\n%s\n\nService check:\n%s\n\nSystem info:\n%s", processStatus, output, output2, output3)
|
|
s.updateLastStep(result, "success", "", combinedOutput, "", true)
|
|
return nil
|
|
}
|
|
|
|
// verifiedStopExistingServices stops any existing BZZZ services
|
|
func (s *SetupManager) verifiedStopExistingServices(client *ssh.Client, config interface{}, password string, result *DeploymentResult) error {
|
|
stepName := "Stop & Remove Existing Services"
|
|
s.addStep(result, stepName, "running", "", "", "", false)
|
|
|
|
// Stop systemd service if exists
|
|
cmd1 := "systemctl stop bzzz 2>/dev/null || echo 'No systemd service to stop'"
|
|
output1, _ := s.executeSudoCommand(client, password, cmd1)
|
|
|
|
// Disable systemd service if exists - separate command for better error tracking
|
|
cmd2a := "systemctl disable bzzz 2>/dev/null || echo 'No systemd service to disable'"
|
|
output2a, _ := s.executeSudoCommand(client, password, cmd2a)
|
|
|
|
// Remove service files
|
|
cmd2b := "rm -f /etc/systemd/system/bzzz.service ~/.config/systemd/user/bzzz.service 2>/dev/null || echo 'No service file to remove'"
|
|
output2b, _ := s.executeSudoCommand(client, password, cmd2b)
|
|
|
|
// Kill any remaining processes
|
|
cmd3 := "pkill -f bzzz || echo 'No processes to kill'"
|
|
output3, _ := s.executeSSHCommand(client, cmd3)
|
|
|
|
// Remove old binaries from standard locations
|
|
cmd4 := "rm -f /usr/local/bin/bzzz ~/bin/bzzz ~/bzzz 2>/dev/null || echo 'No old binaries to remove'"
|
|
output4, _ := s.executeSudoCommand(client, password, cmd4)
|
|
|
|
// Reload systemd after changes
|
|
cmd5 := "systemctl daemon-reload 2>/dev/null || echo 'Systemd reload completed'"
|
|
output5, _ := s.executeSudoCommand(client, password, cmd5)
|
|
|
|
// Verify no processes remain
|
|
output6, err := s.executeSSHCommand(client, "ps aux | grep bzzz | grep -v grep || echo 'All BZZZ processes stopped'")
|
|
if err != nil {
|
|
combinedOutput := fmt.Sprintf("Stop service:\n%s\n\nDisable service:\n%s\n\nRemove service files:\n%s\n\nKill processes:\n%s\n\nRemove binaries:\n%s\n\nReload systemd:\n%s\n\nVerification:\n%s",
|
|
output1, output2a, output2b, output3, output4, output5, output6)
|
|
s.updateLastStep(result, "failed", "cleanup verification", combinedOutput, fmt.Sprintf("Failed verification: %v", err), false)
|
|
return fmt.Errorf("failed to verify process cleanup: %v", err)
|
|
}
|
|
|
|
if !strings.Contains(output6, "All BZZZ processes stopped") {
|
|
combinedOutput := fmt.Sprintf("Stop service:\n%s\n\nDisable service:\n%s\n\nRemove service files:\n%s\n\nKill processes:\n%s\n\nRemove binaries:\n%s\n\nReload systemd:\n%s\n\nVerification:\n%s",
|
|
output1, output2a, output2b, output3, output4, output5, output6)
|
|
s.updateLastStep(result, "failed", "process verification", combinedOutput, "BZZZ processes still running after cleanup", false)
|
|
return fmt.Errorf("failed to stop all BZZZ processes")
|
|
}
|
|
|
|
combinedOutput := fmt.Sprintf("Stop service:\n%s\n\nDisable service:\n%s\n\nRemove service files:\n%s\n\nKill processes:\n%s\n\nRemove binaries:\n%s\n\nReload systemd:\n%s\n\nVerification:\n%s",
|
|
output1, output2a, output2b, output3, output4, output5, output6)
|
|
s.updateLastStep(result, "success", "stop + cleanup + verify", combinedOutput, "", true)
|
|
return nil
|
|
}
|
|
|
|
// updateLastStep updates the last step in the result
|
|
func (s *SetupManager) updateLastStep(result *DeploymentResult, status, command, output, error string, verified bool) {
|
|
if len(result.Steps) > 0 {
|
|
lastStep := &result.Steps[len(result.Steps)-1]
|
|
lastStep.Status = status
|
|
if command != "" {
|
|
lastStep.Command = command
|
|
}
|
|
if output != "" {
|
|
lastStep.Output = output
|
|
}
|
|
if error != "" {
|
|
lastStep.Error = error
|
|
}
|
|
lastStep.Verified = verified
|
|
}
|
|
}
|
|
|
|
// performRollbackWithPassword attempts to undo changes made during failed deployment using password
|
|
func (s *SetupManager) performRollbackWithPassword(client *ssh.Client, password string, result *DeploymentResult) {
|
|
result.RollbackLog = append(result.RollbackLog, "Starting rollback procedure...")
|
|
|
|
// Stop any services we might have started
|
|
if output, err := s.executeSudoCommand(client, password, "systemctl stop bzzz 2>/dev/null || echo 'No service to stop'"); err == nil {
|
|
result.RollbackLog = append(result.RollbackLog, "Stopped service: "+output)
|
|
}
|
|
|
|
// Remove systemd service
|
|
if output, err := s.executeSudoCommand(client, password, "systemctl disable bzzz 2>/dev/null; rm -f /etc/systemd/system/bzzz.service 2>/dev/null || echo 'No service file to remove'"); err == nil {
|
|
result.RollbackLog = append(result.RollbackLog, "Removed service: "+output)
|
|
}
|
|
|
|
// Remove binary
|
|
if output, err := s.executeSudoCommand(client, password, "rm -f /usr/local/bin/bzzz 2>/dev/null || echo 'No binary to remove'"); err == nil {
|
|
result.RollbackLog = append(result.RollbackLog, "Removed binary: "+output)
|
|
}
|
|
|
|
// Reload systemd
|
|
if output, err := s.executeSudoCommand(client, password, "systemctl daemon-reload"); err == nil {
|
|
result.RollbackLog = append(result.RollbackLog, "Reloaded systemd: "+output)
|
|
}
|
|
}
|
|
|
|
// performRollback attempts to rollback any changes made during failed deployment
|
|
func (s *SetupManager) performRollback(client *ssh.Client, result *DeploymentResult) {
|
|
result.RollbackLog = append(result.RollbackLog, "Starting rollback procedure...")
|
|
|
|
// Stop any services we might have started
|
|
if output, err := s.executeSSHCommand(client, "sudo -n systemctl stop bzzz 2>/dev/null || echo 'No service to stop'"); err == nil {
|
|
result.RollbackLog = append(result.RollbackLog, "Stopped service: "+output)
|
|
}
|
|
|
|
// Remove binaries we might have copied
|
|
if output, err := s.executeSSHCommand(client, "rm -f ~/bzzz /usr/local/bin/bzzz 2>/dev/null || echo 'No binaries to remove'"); err == nil {
|
|
result.RollbackLog = append(result.RollbackLog, "Removed binaries: "+output)
|
|
}
|
|
|
|
result.RollbackLog = append(result.RollbackLog, "Rollback completed")
|
|
}
|
|
|
|
// verifiedCopyBinary copies BZZZ binary and verifies installation
|
|
func (s *SetupManager) verifiedCopyBinary(client *ssh.Client, config interface{}, password string, result *DeploymentResult) error {
|
|
stepName := "Copy Binary"
|
|
s.addStep(result, stepName, "running", "", "", "", false)
|
|
|
|
// Copy binary using existing function but with verification
|
|
if err := s.copyBinaryToMachineWithPassword(client, password); err != nil {
|
|
s.updateLastStep(result, "failed", "scp binary", "", err.Error(), false)
|
|
return fmt.Errorf("binary copy failed: %v", err)
|
|
}
|
|
|
|
// Verify binary was copied and is executable
|
|
checkCmd := "ls -la /usr/local/bin/bzzz ~/bin/bzzz 2>/dev/null || echo 'Binary not found in expected locations'"
|
|
output, err := s.executeSSHCommand(client, checkCmd)
|
|
if err != nil {
|
|
s.updateLastStep(result, "failed", checkCmd, output, fmt.Sprintf("Verification failed: %v", err), false)
|
|
return fmt.Errorf("binary verification failed: %v", err)
|
|
}
|
|
|
|
// Verify binary can execute (note: BZZZ doesn't have --version flag, use --help)
|
|
versionCmd := "timeout 3s /usr/local/bin/bzzz --help 2>&1 | head -n1 || timeout 3s ~/bin/bzzz --help 2>&1 | head -n1 || echo 'Binary not executable'"
|
|
versionOutput, _ := s.executeSSHCommand(client, versionCmd)
|
|
|
|
combinedOutput := fmt.Sprintf("File check:\n%s\n\nBinary test:\n%s", output, versionOutput)
|
|
|
|
if strings.Contains(output, "Binary not found") {
|
|
s.updateLastStep(result, "failed", checkCmd, combinedOutput, "Binary not found in expected locations", false)
|
|
return fmt.Errorf("binary installation verification failed")
|
|
}
|
|
|
|
s.updateLastStep(result, "success", "scp + verify", combinedOutput, "", true)
|
|
return nil
|
|
}
|
|
|
|
// verifiedDeployConfiguration deploys configuration and verifies correctness
|
|
func (s *SetupManager) verifiedDeployConfiguration(client *ssh.Client, config interface{}, password string, result *DeploymentResult) error {
|
|
stepName := "Deploy Configuration"
|
|
s.addStep(result, stepName, "running", "", "", "", false)
|
|
|
|
// Generate and deploy configuration using existing function
|
|
if err := s.generateAndDeployConfig(client, "remote-host", config); err != nil {
|
|
s.updateLastStep(result, "failed", "deploy config", "", err.Error(), false)
|
|
return fmt.Errorf("configuration deployment failed: %v", err)
|
|
}
|
|
|
|
// Verify configuration file was created and is valid YAML
|
|
verifyCmd := "ls -la ~/.bzzz/config.yaml && echo '--- Config Preview ---' && head -20 ~/.bzzz/config.yaml"
|
|
output, err := s.executeSSHCommand(client, verifyCmd)
|
|
if err != nil {
|
|
s.updateLastStep(result, "failed", verifyCmd, output, fmt.Sprintf("Config verification failed: %v", err), false)
|
|
return fmt.Errorf("configuration verification failed: %v", err)
|
|
}
|
|
|
|
// Check if config contains expected sections for complex config structure
|
|
if !strings.Contains(output, "agent:") || !strings.Contains(output, "whoosh_api:") || !strings.Contains(output, "ai:") {
|
|
s.updateLastStep(result, "failed", verifyCmd, output, "Configuration missing required sections", false)
|
|
return fmt.Errorf("configuration incomplete - missing required sections")
|
|
}
|
|
|
|
s.updateLastStep(result, "success", "deploy + verify config", output, "", true)
|
|
return nil
|
|
}
|
|
|
|
// verifiedConfigureFirewall configures firewall and verifies rules
|
|
func (s *SetupManager) verifiedConfigureFirewall(client *ssh.Client, config interface{}, password string, result *DeploymentResult) error {
|
|
stepName := "Configure Firewall"
|
|
s.addStep(result, stepName, "running", "", "", "", false)
|
|
|
|
// Configure firewall using existing function
|
|
if err := s.configureFirewall(client, config); err != nil {
|
|
s.updateLastStep(result, "failed", "configure firewall", "", err.Error(), false)
|
|
return fmt.Errorf("firewall configuration failed: %v", err)
|
|
}
|
|
|
|
// Verify firewall rules (this is informational, not critical)
|
|
verifyCmd := "ufw status 2>/dev/null || firewall-cmd --list-ports 2>/dev/null || echo 'Firewall status unavailable'"
|
|
output, _ := s.executeSudoCommand(client, password, verifyCmd)
|
|
|
|
s.updateLastStep(result, "success", "configure + verify firewall", output, "", true)
|
|
return nil
|
|
}
|
|
|
|
// verifiedCreateSystemdService creates systemd service and verifies configuration
|
|
func (s *SetupManager) verifiedCreateSystemdService(client *ssh.Client, config interface{}, password string, result *DeploymentResult) error {
|
|
stepName := "Create SystemD Service"
|
|
s.addStep(result, stepName, "running", "", "", "", false)
|
|
|
|
// Create systemd service using password-based sudo
|
|
if err := s.createSystemdServiceWithPassword(client, config, password); err != nil {
|
|
s.updateLastStep(result, "failed", "create service", "", err.Error(), false)
|
|
return fmt.Errorf("systemd service creation failed: %v", err)
|
|
}
|
|
|
|
// Verify service file was created and contains correct paths
|
|
verifyCmd := "systemctl cat bzzz"
|
|
output, err := s.executeSudoCommand(client, password, verifyCmd)
|
|
if err != nil {
|
|
// Try to check if the service file exists another way
|
|
checkCmd := "ls -la /etc/systemd/system/bzzz.service"
|
|
checkOutput, checkErr := s.executeSudoCommand(client, password, checkCmd)
|
|
if checkErr != nil {
|
|
s.updateLastStep(result, "failed", verifyCmd, output, fmt.Sprintf("Service verification failed: %v. Service file check also failed: %v", err, checkErr), false)
|
|
return fmt.Errorf("systemd service verification failed: %v", err)
|
|
}
|
|
s.updateLastStep(result, "warning", verifyCmd, checkOutput, "Service file exists but systemctl cat failed, continuing", false)
|
|
}
|
|
|
|
// Verify service can be enabled
|
|
enableCmd := "systemctl enable bzzz"
|
|
enableOutput, enableErr := s.executeSudoCommand(client, password, enableCmd)
|
|
if enableErr != nil {
|
|
combinedOutput := fmt.Sprintf("Service file:\n%s\n\nEnable attempt:\n%s", output, enableOutput)
|
|
s.updateLastStep(result, "failed", enableCmd, combinedOutput, fmt.Sprintf("Failed to enable service: %v", enableErr), false)
|
|
return fmt.Errorf("failed to enable systemd service: %v", enableErr)
|
|
}
|
|
|
|
combinedOutput := fmt.Sprintf("Service file:\n%s\n\nService enabled:\n%s", output, enableOutput)
|
|
s.updateLastStep(result, "success", "create + enable service", combinedOutput, "", true)
|
|
return nil
|
|
}
|
|
|
|
// verifiedStartService starts the service and verifies it's running properly
|
|
func (s *SetupManager) verifiedStartService(client *ssh.Client, config interface{}, password string, result *DeploymentResult) error {
|
|
stepName := "Start Service"
|
|
s.addStep(result, stepName, "running", "", "", "", false)
|
|
|
|
// Check if auto-start is enabled
|
|
configMap, ok := config.(map[string]interface{})
|
|
if !ok || configMap["autoStart"] != true {
|
|
s.updateLastStep(result, "success", "", "Auto-start disabled, skipping service start", "", true)
|
|
return nil
|
|
}
|
|
|
|
// Pre-flight checks before starting service
|
|
s.addStep(result, "Pre-Start Checks", "running", "", "", "", false)
|
|
|
|
// Check if config file exists and is readable by the service user
|
|
configCheck := "ls -la /home/*/bzzz/config.yaml 2>/dev/null || echo 'Config file not found'"
|
|
configOutput, _ := s.executeSSHCommand(client, configCheck)
|
|
|
|
// Check if binary is executable
|
|
binCheck := "ls -la /usr/local/bin/bzzz"
|
|
binOutput, _ := s.executeSudoCommand(client, password, binCheck)
|
|
|
|
preflightInfo := fmt.Sprintf("Binary check:\n%s\n\nConfig check:\n%s", binOutput, configOutput)
|
|
s.updateLastStep(result, "success", "pre-flight", preflightInfo, "Pre-start checks completed", false)
|
|
|
|
// Start the service
|
|
startCmd := "systemctl start bzzz"
|
|
startOutput, err := s.executeSudoCommand(client, password, startCmd)
|
|
if err != nil {
|
|
// Get detailed error information
|
|
statusCmd := "systemctl status bzzz"
|
|
statusOutput, _ := s.executeSudoCommand(client, password, statusCmd)
|
|
|
|
logsCmd := "journalctl -u bzzz --no-pager -n 20"
|
|
logsOutput, _ := s.executeSudoCommand(client, password, logsCmd)
|
|
|
|
// Combine all error information
|
|
detailedError := fmt.Sprintf("Start command output:\n%s\n\nService status:\n%s\n\nRecent logs:\n%s",
|
|
startOutput, statusOutput, logsOutput)
|
|
|
|
s.updateLastStep(result, "failed", startCmd, detailedError, fmt.Sprintf("Failed to start service: %v", err), false)
|
|
return fmt.Errorf("failed to start systemd service: %v", err)
|
|
}
|
|
|
|
// Wait for service to fully initialize (BZZZ needs time to start all subsystems)
|
|
time.Sleep(8 * time.Second)
|
|
|
|
// Verify service is running
|
|
statusCmd := "systemctl status bzzz"
|
|
statusOutput, _ := s.executeSSHCommand(client, statusCmd)
|
|
|
|
// Check if service is active
|
|
if !strings.Contains(statusOutput, "active (running)") {
|
|
// Get detailed logs to understand why service failed
|
|
logsCmd := "journalctl -u bzzz --no-pager -n 20"
|
|
logsOutput, _ := s.executeSudoCommand(client, password, logsCmd)
|
|
|
|
// Check if config file exists and is readable
|
|
configCheckCmd := "ls -la ~/.bzzz/config.yaml && head -5 ~/.bzzz/config.yaml"
|
|
configCheckOutput, _ := s.executeSSHCommand(client, configCheckCmd)
|
|
|
|
combinedOutput := fmt.Sprintf("Start attempt:\n%s\n\nStatus check:\n%s\n\nRecent logs:\n%s\n\nConfig check:\n%s",
|
|
startOutput, statusOutput, logsOutput, configCheckOutput)
|
|
s.updateLastStep(result, "failed", startCmd, combinedOutput, "Service failed to reach running state", false)
|
|
return fmt.Errorf("service is not running after start attempt")
|
|
}
|
|
|
|
combinedOutput := fmt.Sprintf("Service started:\n%s\n\nStatus verification:\n%s", startOutput, statusOutput)
|
|
s.updateLastStep(result, "success", startCmd+" + verify", combinedOutput, "", true)
|
|
return nil
|
|
}
|
|
|
|
// verifiedPostDeploymentTest performs final verification that deployment is functional
|
|
func (s *SetupManager) verifiedPostDeploymentTest(client *ssh.Client, config interface{}, password string, result *DeploymentResult) error {
|
|
stepName := "Post-deployment Test"
|
|
s.addStep(result, stepName, "running", "", "", "", false)
|
|
|
|
// Test 1: Verify binary is executable
|
|
// Note: BZZZ binary doesn't have --version flag, so just check if it's executable and can start help
|
|
versionCmd := "if pgrep -f bzzz >/dev/null; then echo 'BZZZ process running'; else timeout 3s /usr/local/bin/bzzz --help 2>&1 | head -n1 || timeout 3s ~/bin/bzzz --help 2>&1 | head -n1 || echo 'Binary not executable'; fi"
|
|
versionOutput, _ := s.executeSSHCommand(client, versionCmd)
|
|
|
|
// Test 2: Verify service status
|
|
serviceCmd := "systemctl status bzzz --no-pager"
|
|
serviceOutput, _ := s.executeSSHCommand(client, serviceCmd)
|
|
|
|
// Test 3: Wait for API to be ready, then check if setup API is responding
|
|
// Poll for API readiness with timeout (up to 15 seconds)
|
|
var apiOutput string
|
|
apiReady := false
|
|
for i := 0; i < 15; i++ {
|
|
apiCmd := "curl -s -m 2 http://localhost:8090/api/setup/required 2>/dev/null"
|
|
output, err := s.executeSSHCommand(client, apiCmd)
|
|
if err == nil && !strings.Contains(output, "Connection refused") && !strings.Contains(output, "timeout") {
|
|
apiOutput = fmt.Sprintf("API ready (after %ds): %s", i+1, output)
|
|
apiReady = true
|
|
break
|
|
}
|
|
if i < 14 { // Don't sleep on the last iteration
|
|
time.Sleep(1 * time.Second)
|
|
}
|
|
}
|
|
if !apiReady {
|
|
apiOutput = "API not responding after 15s timeout"
|
|
}
|
|
|
|
// Test 4: Verify configuration is readable
|
|
configCmd := "test -r ~/.bzzz/config.yaml && echo 'Config readable' || echo 'Config not readable'"
|
|
configOutput, _ := s.executeSSHCommand(client, configCmd)
|
|
|
|
combinedOutput := fmt.Sprintf("Binary test:\n%s\n\nService test:\n%s\n\nAPI test:\n%s\n\nConfig test:\n%s",
|
|
versionOutput, serviceOutput, apiOutput, configOutput)
|
|
|
|
// Determine if tests passed and provide detailed failure information
|
|
// Binary test passes if BZZZ is running OR if help command succeeded
|
|
binaryFailed := strings.Contains(versionOutput, "Binary not executable") && !strings.Contains(versionOutput, "BZZZ process running")
|
|
configFailed := strings.Contains(configOutput, "Config not readable")
|
|
|
|
if binaryFailed || configFailed {
|
|
var failures []string
|
|
if binaryFailed {
|
|
failures = append(failures, "Binary not executable or accessible")
|
|
}
|
|
if configFailed {
|
|
failures = append(failures, "Config file not readable")
|
|
}
|
|
|
|
failureMsg := fmt.Sprintf("Tests failed: %s", strings.Join(failures, ", "))
|
|
s.updateLastStep(result, "failed", "post-deployment tests", combinedOutput, failureMsg, false)
|
|
return fmt.Errorf("post-deployment verification failed: %s", failureMsg)
|
|
}
|
|
|
|
s.updateLastStep(result, "success", "comprehensive verification", combinedOutput, "", true)
|
|
return nil
|
|
}
|
|
|
|
// copyBinaryToMachineWithPassword copies the BZZZ binary to remote machine using SCP protocol with sudo password
|
|
func (s *SetupManager) copyBinaryToMachineWithPassword(client *ssh.Client, password string) 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()
|
|
|
|
// Try to move to /usr/local/bin with sudo (with or without password), fall back to user bin if needed
|
|
var sudoCmd string
|
|
if password == "" {
|
|
// Try passwordless sudo first
|
|
sudoCmd = "sudo -n mv ~/bzzz /usr/local/bin/bzzz && sudo -n chmod +x /usr/local/bin/bzzz"
|
|
} else {
|
|
// Use password sudo
|
|
escapedPassword := strings.ReplaceAll(password, "'", "'\"'\"'")
|
|
sudoCmd = fmt.Sprintf("echo '%s' | sudo -S mv ~/bzzz /usr/local/bin/bzzz && echo '%s' | sudo -S chmod +x /usr/local/bin/bzzz",
|
|
escapedPassword, escapedPassword)
|
|
}
|
|
|
|
if err := session.Run(sudoCmd); 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
|
|
}
|
|
|
|
// copyBinaryToMachine copies the BZZZ binary to remote machine using SCP protocol (passwordless sudo)
|
|
func (s *SetupManager) copyBinaryToMachine(client *ssh.Client) error {
|
|
return s.copyBinaryToMachineWithPassword(client, "")
|
|
}
|
|
|
|
// createSystemdServiceWithPassword creates systemd service file using password sudo
|
|
func (s *SetupManager) createSystemdServiceWithPassword(client *ssh.Client, config interface{}, password string) 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())
|
|
}
|
|
}
|
|
|
|
// Get the actual username for the service
|
|
session, err = client.NewSession()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer session.Close()
|
|
|
|
var userBuilder strings.Builder
|
|
session.Stdout = &userBuilder
|
|
if err := session.Run("whoami"); err != nil {
|
|
return fmt.Errorf("failed to get username: %w", err)
|
|
}
|
|
username := strings.TrimSpace(userBuilder.String())
|
|
|
|
// Create service file with actual username
|
|
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 /home/%s/.bzzz/config.yaml
|
|
Restart=always
|
|
RestartSec=10
|
|
User=%s
|
|
Group=%s
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
`, binaryPath, username, username, username)
|
|
|
|
// Create service file in temp location first, then move with sudo
|
|
createCmd := fmt.Sprintf("cat > /tmp/bzzz.service << 'EOF'\n%sEOF", serviceFile)
|
|
if _, err := s.executeSSHCommand(client, createCmd); err != nil {
|
|
return fmt.Errorf("failed to create temp service file: %w", err)
|
|
}
|
|
|
|
// Move to systemd directory using password sudo
|
|
moveCmd := "mv /tmp/bzzz.service /etc/systemd/system/bzzz.service"
|
|
if _, err := s.executeSudoCommand(client, password, moveCmd); err != nil {
|
|
return fmt.Errorf("failed to install system service file: %w", err)
|
|
}
|
|
|
|
// Reload systemd to recognize new service
|
|
reloadCmd := "systemctl daemon-reload"
|
|
if _, err := s.executeSudoCommand(client, password, reloadCmd); err != nil {
|
|
return fmt.Errorf("failed to reload systemd: %w", err)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// GenerateConfigForMachine generates the YAML configuration for a specific machine (for download/inspection)
|
|
func (s *SetupManager) GenerateConfigForMachine(machineIP string, config interface{}) (string, error) {
|
|
// Extract configuration from the setup data
|
|
configMap, ok := config.(map[string]interface{})
|
|
if !ok {
|
|
return "", fmt.Errorf("invalid configuration format: expected map[string]interface{}, got %T: %+v", config, config)
|
|
}
|
|
|
|
// Use machine IP to determine hostname (simplified)
|
|
hostname := strings.ReplaceAll(machineIP, ".", "-")
|
|
|
|
// 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 that matches the Go struct layout
|
|
configYAML := fmt.Sprintf(`# BZZZ Configuration for %s
|
|
whoosh_api:
|
|
base_url: "https://whoosh.home.deepblack.cloud"
|
|
timeout: 30s
|
|
retry_count: 3
|
|
|
|
agent:
|
|
id: "%s-agent"
|
|
capabilities: ["general", "reasoning", "task-coordination"]
|
|
poll_interval: 30s
|
|
max_tasks: 3
|
|
models: ["phi3", "llama3.1"]
|
|
specialization: "general_developer"
|
|
model_selection_webhook: "https://n8n.home.deepblack.cloud/webhook/model-selection"
|
|
default_reasoning_model: "phi3"
|
|
sandbox_image: "registry.home.deepblack.cloud/bzzz-sandbox:latest"
|
|
role: ""
|
|
system_prompt: ""
|
|
reports_to: []
|
|
expertise: []
|
|
deliverables: []
|
|
collaboration:
|
|
preferred_message_types: []
|
|
auto_subscribe_to_roles: []
|
|
auto_subscribe_to_expertise: []
|
|
response_timeout_seconds: 0
|
|
max_collaboration_depth: 0
|
|
escalation_threshold: 0
|
|
custom_topic_subscriptions: []
|
|
|
|
github:
|
|
token_file: ""
|
|
user_agent: "Bzzz-P2P-Agent/1.0"
|
|
timeout: 30s
|
|
rate_limit: true
|
|
assignee: ""
|
|
|
|
p2p:
|
|
service_tag: "bzzz-peer-discovery"
|
|
bzzz_topic: "bzzz/coordination/v1"
|
|
hmmm_topic: "hmmm/meta-discussion/v1"
|
|
discovery_timeout: 10s
|
|
escalation_webhook: "https://n8n.home.deepblack.cloud/webhook-test/human-escalation"
|
|
escalation_keywords: ["stuck", "help", "human", "escalate", "clarification needed", "manual intervention"]
|
|
conversation_limit: 10
|
|
|
|
logging:
|
|
level: "info"
|
|
format: "text"
|
|
output: "stdout"
|
|
structured: false
|
|
|
|
slurp:
|
|
enabled: true
|
|
base_url: ""
|
|
api_key: ""
|
|
timeout: 30s
|
|
retry_count: 3
|
|
max_concurrent_requests: 10
|
|
request_queue_size: 100
|
|
|
|
v2:
|
|
enabled: false
|
|
protocol_version: "2.0.0"
|
|
uri_resolution:
|
|
cache_ttl: 5m0s
|
|
max_peers_per_result: 5
|
|
default_strategy: "best_match"
|
|
resolution_timeout: 30s
|
|
dht:
|
|
enabled: false
|
|
bootstrap_peers: []
|
|
mode: "auto"
|
|
protocol_prefix: "/bzzz"
|
|
bootstrap_timeout: 30s
|
|
discovery_interval: 1m0s
|
|
auto_bootstrap: false
|
|
semantic_addressing:
|
|
enable_wildcards: true
|
|
default_agent: "any"
|
|
default_role: "any"
|
|
default_project: "any"
|
|
enable_role_hierarchy: true
|
|
feature_flags:
|
|
uri_protocol: false
|
|
semantic_addressing: false
|
|
dht_discovery: false
|
|
advanced_resolution: false
|
|
|
|
ucxl:
|
|
enabled: false
|
|
server:
|
|
port: 8081
|
|
base_path: "/bzzz"
|
|
enabled: true
|
|
resolution:
|
|
cache_ttl: 5m0s
|
|
enable_wildcards: true
|
|
max_results: 50
|
|
storage:
|
|
type: "filesystem"
|
|
directory: "/tmp/bzzz-ucxl-storage"
|
|
max_size: 104857600
|
|
p2p_integration:
|
|
enable_announcement: true
|
|
enable_discovery: true
|
|
announcement_topic: "bzzz/ucxl/announcement/v1"
|
|
discovery_timeout: 30s
|
|
|
|
security:
|
|
admin_key_shares:
|
|
threshold: 3
|
|
total_shares: 5
|
|
election_config:
|
|
heartbeat_timeout: 5s
|
|
discovery_timeout: 30s
|
|
election_timeout: 15s
|
|
max_discovery_attempts: 6
|
|
discovery_backoff: 5s
|
|
minimum_quorum: 3
|
|
consensus_algorithm: "raft"
|
|
split_brain_detection: true
|
|
conflict_resolution: "highest_uptime"
|
|
key_rotation_days: 90
|
|
audit_logging: true
|
|
audit_path: ".bzzz/security-audit.log"
|
|
|
|
ai:
|
|
ollama:
|
|
endpoint: "http://192.168.1.27:11434"
|
|
timeout: 30s
|
|
models: ["phi3", "llama3.1"]
|
|
openai:
|
|
api_key: ""
|
|
endpoint: "https://api.openai.com/v1"
|
|
timeout: 30s
|
|
`, hostname, hostname)
|
|
|
|
return configYAML, nil
|
|
}
|
|
|
|
// GenerateConfigForMachineSimple generates a simple BZZZ configuration that matches the working config structure
|
|
// REVENUE CRITICAL: This method now properly processes license data to enable revenue protection
|
|
func (s *SetupManager) GenerateConfigForMachineSimple(machineIP string, config interface{}) (string, error) {
|
|
// CRITICAL FIX: Extract license data from setup configuration - this was being ignored!
|
|
// This fix enables revenue protection by ensuring license data is saved in configuration
|
|
configMap, ok := config.(map[string]interface{})
|
|
if !ok {
|
|
return "", fmt.Errorf("invalid configuration format: expected map[string]interface{}, got %T", config)
|
|
}
|
|
|
|
// Use machine IP to determine hostname (simplified)
|
|
hostname := strings.ReplaceAll(machineIP, ".", "-")
|
|
|
|
// REVENUE CRITICAL: Extract license data from setup configuration
|
|
// This ensures license data collected during setup is actually saved in the configuration
|
|
var licenseData map[string]interface{}
|
|
if license, exists := configMap["license"]; exists {
|
|
if licenseMap, ok := license.(map[string]interface{}); ok {
|
|
licenseData = licenseMap
|
|
}
|
|
}
|
|
|
|
// Validate license data exists - FAIL CLOSED DESIGN
|
|
if licenseData == nil {
|
|
return "", fmt.Errorf("REVENUE PROTECTION: License data missing from setup configuration - BZZZ cannot be deployed without valid licensing")
|
|
}
|
|
|
|
// Extract required license fields with validation
|
|
email, _ := licenseData["email"].(string)
|
|
licenseKey, _ := licenseData["licenseKey"].(string)
|
|
orgName, _ := licenseData["organizationName"].(string)
|
|
|
|
if email == "" || licenseKey == "" {
|
|
return "", fmt.Errorf("REVENUE PROTECTION: Email and license key are required - cannot deploy BZZZ without valid licensing")
|
|
}
|
|
|
|
// Generate unique cluster ID for license binding (prevents license sharing across clusters)
|
|
clusterID := fmt.Sprintf("cluster-%s-%d", hostname, time.Now().Unix())
|
|
|
|
// Generate YAML configuration with FULL license integration for revenue protection
|
|
configYAML := fmt.Sprintf(`# BZZZ Configuration for %s - REVENUE PROTECTED
|
|
# Generated at %s with license validation
|
|
whoosh_api:
|
|
base_url: "https://whoosh.home.deepblack.cloud"
|
|
api_key: ""
|
|
timeout: 30s
|
|
retry_count: 3
|
|
|
|
agent:
|
|
id: "%s-agent"
|
|
capabilities: ["general"]
|
|
poll_interval: 30s
|
|
max_tasks: 2
|
|
models: []
|
|
specialization: ""
|
|
model_selection_webhook: ""
|
|
default_reasoning_model: ""
|
|
sandbox_image: ""
|
|
role: ""
|
|
system_prompt: ""
|
|
reports_to: []
|
|
expertise: []
|
|
deliverables: []
|
|
collaboration:
|
|
preferred_message_types: []
|
|
auto_subscribe_to_roles: []
|
|
auto_subscribe_to_expertise: []
|
|
response_timeout_seconds: 0
|
|
max_collaboration_depth: 0
|
|
escalation_threshold: 0
|
|
custom_topic_subscriptions: []
|
|
|
|
github:
|
|
token_file: ""
|
|
user_agent: "BZZZ-Agent/1.0"
|
|
timeout: 30s
|
|
rate_limit: true
|
|
assignee: ""
|
|
|
|
p2p:
|
|
service_tag: "bzzz-peer-discovery"
|
|
bzzz_topic: "bzzz/coordination/v1"
|
|
hmmm_topic: "hmmm/meta-discussion/v1"
|
|
discovery_timeout: 10s
|
|
escalation_webhook: ""
|
|
escalation_keywords: []
|
|
conversation_limit: 10
|
|
|
|
logging:
|
|
level: "info"
|
|
format: "text"
|
|
output: "stdout"
|
|
structured: false
|
|
|
|
slurp:
|
|
enabled: false
|
|
base_url: ""
|
|
api_key: ""
|
|
timeout: 30s
|
|
retry_count: 3
|
|
max_concurrent_requests: 10
|
|
request_queue_size: 100
|
|
|
|
v2:
|
|
enabled: false
|
|
protocol_version: "2.0.0"
|
|
uri_resolution:
|
|
cache_ttl: 5m0s
|
|
max_peers_per_result: 5
|
|
default_strategy: "best_match"
|
|
resolution_timeout: 30s
|
|
dht:
|
|
enabled: false
|
|
bootstrap_peers: []
|
|
mode: "auto"
|
|
protocol_prefix: "/bzzz"
|
|
bootstrap_timeout: 30s
|
|
discovery_interval: 1m0s
|
|
auto_bootstrap: false
|
|
semantic_addressing:
|
|
enable_wildcards: true
|
|
default_agent: "any"
|
|
default_role: "any"
|
|
default_project: "any"
|
|
enable_role_hierarchy: true
|
|
feature_flags:
|
|
uri_protocol: false
|
|
semantic_addressing: false
|
|
dht_discovery: false
|
|
advanced_resolution: false
|
|
|
|
ucxl:
|
|
enabled: false
|
|
server:
|
|
port: 8081
|
|
base_path: "/bzzz"
|
|
enabled: false
|
|
resolution:
|
|
cache_ttl: 5m0s
|
|
enable_wildcards: true
|
|
max_results: 50
|
|
storage:
|
|
type: "filesystem"
|
|
directory: "/tmp/bzzz-ucxl-storage"
|
|
max_size: 104857600
|
|
p2p_integration:
|
|
enable_announcement: false
|
|
enable_discovery: false
|
|
announcement_topic: "bzzz/ucxl/announcement/v1"
|
|
discovery_timeout: 30s
|
|
|
|
security:
|
|
admin_key_shares:
|
|
threshold: 3
|
|
total_shares: 5
|
|
election_config:
|
|
heartbeat_timeout: 5s
|
|
discovery_timeout: 30s
|
|
election_timeout: 15s
|
|
max_discovery_attempts: 6
|
|
discovery_backoff: 5s
|
|
minimum_quorum: 3
|
|
consensus_algorithm: "raft"
|
|
split_brain_detection: true
|
|
conflict_resolution: "highest_uptime"
|
|
key_rotation_days: 90
|
|
audit_logging: false
|
|
audit_path: ""
|
|
|
|
ai:
|
|
ollama:
|
|
endpoint: ""
|
|
timeout: 30s
|
|
models: []
|
|
openai:
|
|
api_key: ""
|
|
endpoint: "https://api.openai.com/v1"
|
|
timeout: 30s
|
|
|
|
# REVENUE CRITICAL: License configuration enables revenue protection
|
|
license:
|
|
email: "%s"
|
|
license_key: "%s"
|
|
organization_name: "%s"
|
|
cluster_id: "%s"
|
|
cluster_name: "%s-cluster"
|
|
kaching_url: "https://kaching.chorus.services"
|
|
heartbeat_minutes: 60
|
|
grace_period_hours: 24
|
|
last_validated: "%s"
|
|
validation_token: ""
|
|
license_type: ""
|
|
max_nodes: 0
|
|
expires_at: "0001-01-01T00:00:00Z"
|
|
is_active: true
|
|
`, hostname, time.Now().Format(time.RFC3339), email, licenseKey, orgName, clusterID, hostname, time.Now().Format(time.RFC3339))
|
|
|
|
return configYAML, nil
|
|
}
|
|
|
|
// generateAndDeployConfig generates node-specific config.yaml and deploys it
|
|
func (s *SetupManager) generateAndDeployConfig(client *ssh.Client, nodeIP string, config interface{}) error {
|
|
// 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())
|
|
|
|
// Generate YAML configuration using the shared method
|
|
configYAML, err := s.GenerateConfigForMachineSimple(hostname, config)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to generate config: %w", err)
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// ValidateOllamaEndpoint tests if an Ollama endpoint is accessible and returns available models
|
|
func (s *SetupManager) ValidateOllamaEndpoint(endpoint string) (bool, []string, error) {
|
|
if endpoint == "" {
|
|
return false, nil, fmt.Errorf("endpoint cannot be empty")
|
|
}
|
|
|
|
// Ensure endpoint has proper format
|
|
if !strings.HasPrefix(endpoint, "http://") && !strings.HasPrefix(endpoint, "https://") {
|
|
endpoint = "http://" + endpoint
|
|
}
|
|
|
|
// Create HTTP client with timeout
|
|
client := &http.Client{
|
|
Timeout: 10 * time.Second,
|
|
}
|
|
|
|
// Test connection to /api/tags endpoint
|
|
apiURL := strings.TrimRight(endpoint, "/") + "/api/tags"
|
|
resp, err := client.Get(apiURL)
|
|
if err != nil {
|
|
return false, nil, fmt.Errorf("failed to connect to Ollama API: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return false, nil, fmt.Errorf("Ollama API returned status %d", resp.StatusCode)
|
|
}
|
|
|
|
// Parse the response to get available models
|
|
var tagsResponse struct {
|
|
Models []struct {
|
|
Name string `json:"name"`
|
|
} `json:"models"`
|
|
}
|
|
|
|
if err := json.NewDecoder(resp.Body).Decode(&tagsResponse); err != nil {
|
|
return false, nil, fmt.Errorf("failed to decode Ollama response: %w", err)
|
|
}
|
|
|
|
// Extract model names
|
|
var models []string
|
|
for _, model := range tagsResponse.Models {
|
|
models = append(models, model.Name)
|
|
}
|
|
|
|
return true, models, nil
|
|
} |