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")
|
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 {
|
func (s *Service) hasAgentForTarget(req ExecuteRequest) bool {
|
||||||
if s.agentServer == nil {
|
if s.agentServer == nil {
|
||||||
return false
|
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()
|
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
|
// 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
|
// Apply pagination
|
||||||
if q.offset > 0 {
|
if q.offset > 0 {
|
||||||
|
|
@ -831,6 +834,71 @@ func (q *ResourceQuery) matches(r *Resource) bool {
|
||||||
return true
|
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
|
// Cross-Platform Analysis Methods
|
||||||
// These methods support AI queries like "what's using the most CPU"
|
// These methods support AI queries like "what's using the most CPU"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue