package proxmox import ( "fmt" "log" "sort" "sync" "time" ) // Service manages Proxmox operations type Service struct { client *Client nodeCache map[string]*NodeStats cacheMu sync.RWMutex config Config } // Config holds Proxmox configuration type Config struct { BaseURL string `json:"base_url"` Username string `json:"username"` Password string `json:"password"` TokenID string `json:"token_id"` Token string `json:"token"` } // NewService creates a new Proxmox service func NewService(config Config) *Service { var client *Client if config.TokenID != "" && config.Token != "" { client = NewClientWithToken(config.BaseURL, config.TokenID, config.Token) } else { client = NewClient(config.BaseURL, config.Username, config.Password) } return &Service{ client: client, nodeCache: make(map[string]*NodeStats), config: config, } } // GetClusterStatus returns the overall cluster status func (s *Service) GetClusterStatus() (*ClusterInfo, error) { // This would require additional API endpoints for cluster info // For now, return basic cluster information nodes, err := s.client.GetNodes() if err != nil { return nil, fmt.Errorf("failed to get cluster nodes: %w", err) } activeNodes := 0 for _, node := range nodes { if node.Status == "online" { activeNodes++ } } return &ClusterInfo{ Name: "containr-cluster", Version: "7.x", // This should be dynamically retrieved Nodes: len(nodes), Quorate: activeNodes > 0, }, nil } // GetAllNodes returns all nodes with their current status func (s *Service) GetAllNodes() ([]Node, error) { return s.client.GetNodes() } // GetNodeStats returns detailed statistics for a specific node func (s *Service) GetNodeStats(nodeName string) (*NodeStats, error) { s.cacheMu.RLock() if stats, exists := s.nodeCache[nodeName]; exists { s.cacheMu.RUnlock() return stats, nil } s.cacheMu.RUnlock() // Fetch fresh data nodes, err := s.client.GetNodes() if err != nil { return nil, fmt.Errorf("failed to get nodes: %w", err) } var targetNode *Node for _, node := range nodes { if node.Node == nodeName { targetNode = &node break } } if targetNode == nil { return nil, fmt.Errorf("node %s not found", nodeName) } stats := &NodeStats{ Node: targetNode.Node, Status: targetNode.Status, CPU: targetNode.CPU, MemoryTotal: targetNode.MaxMemory, MemoryUsed: targetNode.MemoryUsed, MemoryFree: targetNode.MaxMemory - targetNode.MemoryUsed, DiskTotal: targetNode.MaxDisk, DiskUsed: targetNode.DiskUsed, DiskFree: targetNode.MaxDisk - targetNode.DiskUsed, Uptime: targetNode.Uptime, LastUpdate: time.Now(), } // Update cache s.cacheMu.Lock() s.nodeCache[nodeName] = stats s.cacheMu.Unlock() return stats, nil } // GetAllVMs returns all VMs across all nodes func (s *Service) GetAllVMs() ([]VM, error) { nodes, err := s.client.GetNodes() if err != nil { return nil, fmt.Errorf("failed to get nodes: %w", err) } var allVMs []VM for _, node := range nodes { if node.Status == "online" { vms, err := s.client.GetVMs(node.Node) if err != nil { log.Printf("Failed to get VMs for node %s: %v", node.Node, err) continue } allVMs = append(allVMs, vms...) } } return allVMs, nil } // GetAllContainers returns all containers across all nodes func (s *Service) GetAllContainers() ([]Container, error) { nodes, err := s.client.GetNodes() if err != nil { return nil, fmt.Errorf("failed to get nodes: %w", err) } var allContainers []Container for _, node := range nodes { if node.Status == "online" { containers, err := s.client.GetContainers(node.Node) if err != nil { log.Printf("Failed to get containers for node %s: %v", node.Node, err) continue } allContainers = append(allContainers, containers...) } } return allContainers, nil } // CreateServiceVM creates a new VM optimized for running services func (s *Service) CreateServiceVM(nodeName string, config ServiceVMConfig) (*VM, error) { if nodeName == "" { resolvedNode, err := s.SelectBestNodeForWorkload(config.Memory, config.Cores) if err != nil { return nil, err } nodeName = resolvedNode } // Find the next available VMID vmid, err := s.getNextAvailableVMID(nodeName) if err != nil { return nil, fmt.Errorf("failed to get next VMID: %w", err) } vmConfig := VMConfig{ VMID: vmid, Name: config.Name, Memory: config.Memory, Cores: config.Cores, DiskSize: config.DiskSize, Storage: config.Storage, NetworkBridge: config.NetworkBridge, Template: config.Template, } taskID, err := s.client.CreateVM(nodeName, vmConfig) if err != nil { return nil, fmt.Errorf("failed to create VM: %w", err) } log.Printf("VM creation started with task ID: %s", taskID) // Wait for VM to be created and get its status time.Sleep(5 * time.Second) // Give Proxmox time to process vms, err := s.client.GetVMs(nodeName) if err != nil { return nil, fmt.Errorf("failed to get VM status after creation: %w", err) } for _, vm := range vms { if vm.VMID == vmid { return &vm, nil } } return nil, fmt.Errorf("VM %d not found after creation", vmid) } // CreateServiceContainer creates a new LXC container optimized for running services func (s *Service) CreateServiceContainer(nodeName string, config ServiceContainerConfig) (*Container, error) { if nodeName == "" { resolvedNode, err := s.SelectBestNodeForWorkload(config.Memory, config.Cores) if err != nil { return nil, err } nodeName = resolvedNode } // Find the next available VMID vmid, err := s.getNextAvailableVMID(nodeName) if err != nil { return nil, fmt.Errorf("failed to get next VMID: %w", err) } containerConfig := ContainerConfig{ VMID: vmid, Hostname: config.Hostname, Memory: config.Memory, Cores: config.Cores, DiskSize: config.DiskSize, Storage: config.Storage, NetworkBridge: config.NetworkBridge, Template: config.Template, } taskID, err := s.client.CreateContainer(nodeName, containerConfig) if err != nil { return nil, fmt.Errorf("failed to create container: %w", err) } log.Printf("Container creation started with task ID: %s", taskID) // Wait for container to be created and get its status time.Sleep(5 * time.Second) // Give Proxmox time to process containers, err := s.client.GetContainers(nodeName) if err != nil { return nil, fmt.Errorf("failed to get container status after creation: %w", err) } for _, container := range containers { if container.VMID == vmid { return &container, nil } } return nil, fmt.Errorf("Container %d not found after creation", vmid) } // StartInstance starts a VM or container func (s *Service) StartInstance(nodeName string, vmid int, instanceType string) error { switch instanceType { case "qemu": return s.client.StartVM(nodeName, vmid) case "lxc": return s.client.StartContainer(nodeName, vmid) default: return fmt.Errorf("unknown instance type: %s", instanceType) } } // StopInstance stops a VM or container func (s *Service) StopInstance(nodeName string, vmid int, instanceType string) error { switch instanceType { case "qemu": return s.client.StopVM(nodeName, vmid) case "lxc": return s.client.StopContainer(nodeName, vmid) default: return fmt.Errorf("unknown instance type: %s", instanceType) } } // DeleteInstance deletes a VM or container func (s *Service) DeleteInstance(nodeName string, vmid int, instanceType string) error { switch instanceType { case "qemu": return s.client.DeleteVM(nodeName, vmid) case "lxc": return s.client.DeleteContainer(nodeName, vmid) default: return fmt.Errorf("unknown instance type: %s", instanceType) } } // GetInstanceStatus returns the status of a VM or container func (s *Service) GetInstanceStatus(nodeName string, vmid int, instanceType string) (interface{}, error) { switch instanceType { case "qemu": return s.client.GetVMStatus(nodeName, vmid) case "lxc": return s.client.GetContainerStatus(nodeName, vmid) default: return nil, fmt.Errorf("unknown instance type: %s", instanceType) } } // FindNodeForVM resolves a VM's node by searching all online nodes. func (s *Service) FindNodeForVM(vmid int) (string, error) { nodes, err := s.client.GetNodes() if err != nil { return "", fmt.Errorf("failed to get nodes: %w", err) } for _, node := range nodes { if node.Status != "online" { continue } vms, err := s.client.GetVMs(node.Node) if err != nil { log.Printf("failed to inspect VMs on node %s: %v", node.Node, err) continue } for _, vm := range vms { if vm.VMID == vmid { return node.Node, nil } } } return "", fmt.Errorf("VM %d not found", vmid) } // FindNodeForContainer resolves a container's node by searching all online nodes. func (s *Service) FindNodeForContainer(vmid int) (string, error) { nodes, err := s.client.GetNodes() if err != nil { return "", fmt.Errorf("failed to get nodes: %w", err) } for _, node := range nodes { if node.Status != "online" { continue } containers, err := s.client.GetContainers(node.Node) if err != nil { log.Printf("failed to inspect containers on node %s: %v", node.Node, err) continue } for _, container := range containers { if container.VMID == vmid { return node.Node, nil } } } return "", fmt.Errorf("container %d not found", vmid) } // SelectBestNodeForWorkload picks an online node with enough free memory and lowest CPU. func (s *Service) SelectBestNodeForWorkload(requiredMemoryMB int, requiredCores int) (string, error) { nodes, err := s.client.GetNodes() if err != nil { return "", fmt.Errorf("failed to get nodes: %w", err) } type candidate struct { nodeName string cpu float64 freeMemoryByte int } candidates := make([]candidate, 0, len(nodes)) requiredMemoryBytes := requiredMemoryMB * 1024 * 1024 for _, node := range nodes { if node.Status != "online" { continue } _ = requiredCores // Core data is not exposed by this Proxmox endpoint. freeMemory := node.MaxMemory - node.MemoryUsed if requiredMemoryBytes > 0 && freeMemory < requiredMemoryBytes { continue } candidates = append(candidates, candidate{ nodeName: node.Node, cpu: node.CPU, freeMemoryByte: freeMemory, }) } if len(candidates) == 0 { return "", fmt.Errorf("no online nodes have sufficient resources") } sort.Slice(candidates, func(i, j int) bool { if candidates[i].cpu != candidates[j].cpu { return candidates[i].cpu < candidates[j].cpu } if candidates[i].freeMemoryByte != candidates[j].freeMemoryByte { return candidates[i].freeMemoryByte > candidates[j].freeMemoryByte } return candidates[i].nodeName < candidates[j].nodeName }) return candidates[0].nodeName, nil } // getNextAvailableVMID finds the next available VM ID on the specified node func (s *Service) getNextAvailableVMID(nodeName string) (int, error) { vms, err := s.client.GetVMs(nodeName) if err != nil { return 0, err } containers, err := s.client.GetContainers(nodeName) if err != nil { return 0, err } usedIDs := make(map[int]bool) for _, vm := range vms { usedIDs[vm.VMID] = true } for _, container := range containers { usedIDs[container.VMID] = true } // Start from 1000 and find the first available ID for vmid := 1000; vmid < 9999; vmid++ { if !usedIDs[vmid] { return vmid, nil } } return 0, fmt.Errorf("no available VM IDs found") } // ServiceVMConfig represents configuration for creating a service VM type ServiceVMConfig struct { Name string `json:"name"` Memory int `json:"memory"` Cores int `json:"cores"` DiskSize int `json:"disk_size"` // in GB Storage string `json:"storage"` NetworkBridge string `json:"network_bridge"` Template string `json:"template"` } // ServiceContainerConfig represents configuration for creating a service container type ServiceContainerConfig struct { Hostname string `json:"hostname"` Memory int `json:"memory"` Cores int `json:"cores"` DiskSize int `json:"disk_size"` // in GB Storage string `json:"storage"` NetworkBridge string `json:"network_bridge"` Template string `json:"template"` } // GetResourceUsage returns resource usage across the cluster func (s *Service) GetResourceUsage() (map[string]interface{}, error) { nodes, err := s.client.GetNodes() if err != nil { return nil, fmt.Errorf("failed to get nodes: %w", err) } var totalCPU, usedCPU float64 var totalMemory, usedMemory, totalDisk, usedDisk int64 var onlineNodes int for _, node := range nodes { if node.Status == "online" { onlineNodes++ totalCPU += 1.0 // Assuming 1 CPU per node for simplicity usedCPU += node.CPU totalMemory += int64(node.MaxMemory) usedMemory += int64(node.MemoryUsed) totalDisk += int64(node.MaxDisk) usedDisk += int64(node.DiskUsed) } } return map[string]interface{}{ "total_nodes": len(nodes), "online_nodes": onlineNodes, "cpu_usage": map[string]interface{}{ "total": totalCPU, "used": usedCPU, "free": totalCPU - usedCPU, }, "memory_usage": map[string]interface{}{ "total": totalMemory, "used": usedMemory, "free": totalMemory - usedMemory, }, "disk_usage": map[string]interface{}{ "total": totalDisk, "used": usedDisk, "free": totalDisk - usedDisk, }, }, nil } // ValidateConnection tests the connection to Proxmox func (s *Service) ValidateConnection() error { _, err := s.client.GetNodes() if err != nil { return fmt.Errorf("failed to connect to Proxmox: %w", err) } return nil } // GetAvailableTemplates returns a list of available VM and container templates func (s *Service) GetAvailableTemplates(nodeName string) (map[string]interface{}, error) { vms, err := s.client.GetVMs(nodeName) if err != nil { return nil, fmt.Errorf("failed to get VMs: %w", err) } containers, err := s.client.GetContainers(nodeName) if err != nil { return nil, fmt.Errorf("failed to get containers: %w", err) } var vmTemplates []VMTemplate for _, vm := range vms { if vm.Template { vmTemplates = append(vmTemplates, VMTemplate{ VMID: vm.VMID, Name: vm.Name, Node: vm.Node, Storage: "local", // This should be dynamically retrieved CPU: 2, // Default values Memory: 2048, DiskSize: 20, }) } } var containerTemplates []ContainerTemplate for _, container := range containers { if container.Template { containerTemplates = append(containerTemplates, ContainerTemplate{ VMID: container.VMID, Name: container.Name, Node: container.Node, Storage: "local", // This should be dynamically retrieved CPU: 1, Memory: 512, DiskSize: 8, OSTemplate: "ubuntu-22.04-standard", // Default template }) } } return map[string]interface{}{ "vm_templates": vmTemplates, "container_templates": containerTemplates, }, nil }