feat(backend): Implement remaining TODOs
1. resources/store.go: Implement sorting in Query.Execute() - Added sortResources function with support for common fields - Supports: name, type, status, cpu, memory, disk, last_seen - Both ascending and descending order supported 2. ai/service.go: Implement hasAgentForTarget properly - Now maps target to specific agent based on hostname/node - Uses ResourceProvider lookup for container→host mapping - Supports cluster peer routing for Proxmox clusters - Properly handles single-agent vs multi-agent scenarios
This commit is contained in:
parent
c5ac33e8c6
commit
3d8a523971
2 changed files with 138 additions and 5 deletions
|
|
@ -1564,16 +1564,81 @@ func (s *Service) logRemediation(req ExecuteRequest, command, output string, suc
|
|||
Msg("Logged remediation action to operational memory")
|
||||
}
|
||||
|
||||
// hasAgentForTarget checks if we have an agent connection for the given target
|
||||
// hasAgentForTarget checks if we have an agent connection for the given target.
|
||||
// This uses the same routing logic as command execution to determine if the target
|
||||
// can be reached, including cluster peer routing for Proxmox clusters.
|
||||
func (s *Service) hasAgentForTarget(req ExecuteRequest) bool {
|
||||
if s.agentServer == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// For now, just check if any agent is connected
|
||||
// TODO: Map target to specific agent based on hostname/node
|
||||
agents := s.agentServer.GetConnectedAgents()
|
||||
return len(agents) > 0
|
||||
if len(agents) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// For host targets with no specific context, any agent will do
|
||||
if req.TargetType == "host" && len(req.Context) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
// Try to determine the target node from the request context
|
||||
// This mirrors the logic in routeToAgent
|
||||
targetNode := ""
|
||||
|
||||
// Check context fields for the target node
|
||||
hostFields := []string{"node", "host", "guest_node", "hostname", "host_name", "target_host"}
|
||||
for _, field := range hostFields {
|
||||
if value, ok := req.Context[field].(string); ok && value != "" {
|
||||
targetNode = strings.ToLower(value)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// If no target node found in context, try the ResourceProvider
|
||||
if targetNode == "" {
|
||||
s.mu.RLock()
|
||||
rp := s.resourceProvider
|
||||
s.mu.RUnlock()
|
||||
|
||||
if rp != nil {
|
||||
resourceName := ""
|
||||
if name, ok := req.Context["containerName"].(string); ok && name != "" {
|
||||
resourceName = name
|
||||
} else if name, ok := req.Context["name"].(string); ok && name != "" {
|
||||
resourceName = name
|
||||
} else if name, ok := req.Context["guestName"].(string); ok && name != "" {
|
||||
resourceName = name
|
||||
}
|
||||
|
||||
if resourceName != "" {
|
||||
if host := rp.FindContainerHost(resourceName); host != "" {
|
||||
targetNode = strings.ToLower(host)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we still don't have a target node, check for single agent scenario
|
||||
if targetNode == "" {
|
||||
// For unknown targets, we need at least one agent
|
||||
// The actual routing will determine which one to use
|
||||
return len(agents) >= 1
|
||||
}
|
||||
|
||||
// Check if we have a direct agent match or a cluster peer
|
||||
for _, agent := range agents {
|
||||
if strings.EqualFold(agent.Hostname, targetNode) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Try cluster peer routing (for Proxmox clusters)
|
||||
if peerAgentID := s.findClusterPeerAgent(targetNode, agents); peerAgentID != "" {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// getTools returns the available tools for AI
|
||||
|
|
|
|||
|
|
@ -739,7 +739,10 @@ func (q *ResourceQuery) Execute() []Resource {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: Implement sorting
|
||||
// Apply sorting if specified
|
||||
if q.sortBy != "" {
|
||||
sortResources(results, q.sortBy, q.sortDesc)
|
||||
}
|
||||
|
||||
// Apply pagination
|
||||
if q.offset > 0 {
|
||||
|
|
@ -831,6 +834,71 @@ func (q *ResourceQuery) matches(r *Resource) bool {
|
|||
return true
|
||||
}
|
||||
|
||||
// sortResources sorts a slice of resources by the given field.
|
||||
// Supported fields: name, type, status, cpu, memory, disk, last_seen
|
||||
func sortResources(resources []Resource, field string, desc bool) {
|
||||
if len(resources) < 2 {
|
||||
return
|
||||
}
|
||||
|
||||
// Simple bubble sort for consistency (stable sort)
|
||||
// For production, could use sort.Slice with a compare function
|
||||
for i := 0; i < len(resources)-1; i++ {
|
||||
for j := i + 1; j < len(resources); j++ {
|
||||
shouldSwap := false
|
||||
|
||||
switch strings.ToLower(field) {
|
||||
case "name":
|
||||
if desc {
|
||||
shouldSwap = resources[i].EffectiveDisplayName() < resources[j].EffectiveDisplayName()
|
||||
} else {
|
||||
shouldSwap = resources[i].EffectiveDisplayName() > resources[j].EffectiveDisplayName()
|
||||
}
|
||||
case "type":
|
||||
if desc {
|
||||
shouldSwap = string(resources[i].Type) < string(resources[j].Type)
|
||||
} else {
|
||||
shouldSwap = string(resources[i].Type) > string(resources[j].Type)
|
||||
}
|
||||
case "status":
|
||||
if desc {
|
||||
shouldSwap = string(resources[i].Status) < string(resources[j].Status)
|
||||
} else {
|
||||
shouldSwap = string(resources[i].Status) > string(resources[j].Status)
|
||||
}
|
||||
case "cpu":
|
||||
if desc {
|
||||
shouldSwap = resources[i].CPUPercent() < resources[j].CPUPercent()
|
||||
} else {
|
||||
shouldSwap = resources[i].CPUPercent() > resources[j].CPUPercent()
|
||||
}
|
||||
case "memory", "mem":
|
||||
if desc {
|
||||
shouldSwap = resources[i].MemoryPercent() < resources[j].MemoryPercent()
|
||||
} else {
|
||||
shouldSwap = resources[i].MemoryPercent() > resources[j].MemoryPercent()
|
||||
}
|
||||
case "disk":
|
||||
if desc {
|
||||
shouldSwap = resources[i].DiskPercent() < resources[j].DiskPercent()
|
||||
} else {
|
||||
shouldSwap = resources[i].DiskPercent() > resources[j].DiskPercent()
|
||||
}
|
||||
case "last_seen", "lastseen":
|
||||
if desc {
|
||||
shouldSwap = resources[i].LastSeen.Before(resources[j].LastSeen)
|
||||
} else {
|
||||
shouldSwap = resources[i].LastSeen.After(resources[j].LastSeen)
|
||||
}
|
||||
}
|
||||
|
||||
if shouldSwap {
|
||||
resources[i], resources[j] = resources[j], resources[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Cross-Platform Analysis Methods
|
||||
// These methods support AI queries like "what's using the most CPU"
|
||||
|
|
|
|||
Loading…
Reference in a new issue