mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-19 10:33:29 +00:00
chore: remove remaining gitignored files from tracking
- analyze_coverage.py (local coverage analysis script) - coverage_summary.txt (coverage output) - mock.env (environment file)
This commit is contained in:
parent
a53aa387f3
commit
23ff4d1337
11 changed files with 136 additions and 6019 deletions
|
|
@ -1,56 +0,0 @@
|
|||
|
||||
import sys
|
||||
import os
|
||||
|
||||
def parse_coverage(filename):
|
||||
if not os.path.exists(filename):
|
||||
print(f"File {filename} not found")
|
||||
return
|
||||
|
||||
package_stmts = {}
|
||||
package_covered = {}
|
||||
|
||||
with open(filename, 'r') as f:
|
||||
lines = f.readlines()
|
||||
|
||||
current_mode = ""
|
||||
for line in lines:
|
||||
if line.startswith("mode:"):
|
||||
current_mode = line.split()[1]
|
||||
continue
|
||||
|
||||
parts = line.strip().split(':')
|
||||
if len(parts) != 2:
|
||||
continue
|
||||
|
||||
file_path = parts[0]
|
||||
# Package is directory of file_path
|
||||
package_name = os.path.dirname(file_path)
|
||||
|
||||
metrics = parts[1].split()
|
||||
if len(metrics) != 3:
|
||||
continue
|
||||
|
||||
# start_end = metrics[0]
|
||||
num_stmts = int(metrics[1])
|
||||
count = int(metrics[2])
|
||||
|
||||
package_stmts[package_name] = package_stmts.get(package_name, 0) + num_stmts
|
||||
if count > 0:
|
||||
package_covered[package_name] = package_covered.get(package_name, 0) + num_stmts
|
||||
|
||||
results = []
|
||||
for pkg, total in package_stmts.items():
|
||||
covered = package_covered.get(pkg, 0)
|
||||
percent = (covered / total) * 100 if total > 0 else 0
|
||||
results.append((pkg, percent, covered, total))
|
||||
|
||||
# Sort by percentage (ascending)
|
||||
results.sort(key=lambda x: x[1])
|
||||
|
||||
print("Package Coverage Report (Bottom 20):")
|
||||
for pkg, pct, cov, tot in results[:20]:
|
||||
print(f"{pct:6.2f}% ({cov}/{tot}) {pkg}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
parse_coverage("coverage.out")
|
||||
3873
coverage_summary.txt
3873
coverage_summary.txt
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -1,96 +0,0 @@
|
|||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// registerControlToolsConsolidated registers the consolidated pulse_control tool
|
||||
func (e *PulseToolExecutor) registerControlToolsConsolidated() {
|
||||
e.registry.Register(RegisteredTool{
|
||||
Definition: Tool{
|
||||
Name: "pulse_control",
|
||||
Description: `Control Proxmox VMs/LXC containers or execute WRITE commands on infrastructure.
|
||||
|
||||
IMPORTANT: For READ operations (grep, cat, tail, logs, ps, status checks), use pulse_read instead.
|
||||
This tool is for WRITE operations that modify state.
|
||||
|
||||
Types:
|
||||
- guest: Start, stop, restart, shutdown, or delete VMs and LXC containers
|
||||
- command: Execute commands that MODIFY state (restart services, write files, etc.)
|
||||
|
||||
USE pulse_control FOR:
|
||||
- Guest control: start/stop/restart/delete VMs and LXCs
|
||||
- Service management: systemctl restart, service start/stop
|
||||
- Package management: apt install, yum update
|
||||
- File modification: echo > file, sed -i, rm, mv, cp
|
||||
|
||||
DO NOT use pulse_control for:
|
||||
- Reading logs → use pulse_read action=exec or action=logs
|
||||
- Checking status → use pulse_read action=exec
|
||||
- Reading files → use pulse_read action=file
|
||||
- Finding files → use pulse_read action=find
|
||||
|
||||
Examples:
|
||||
- Restart VM: type="guest", guest_id="101", action="restart"
|
||||
- Restart service: type="command", command="systemctl restart nginx", target_host="webserver"
|
||||
|
||||
For Docker container control, use pulse_docker.
|
||||
Note: Delete requires the guest to be stopped first.`,
|
||||
InputSchema: InputSchema{
|
||||
Type: "object",
|
||||
Properties: map[string]PropertySchema{
|
||||
"type": {
|
||||
Type: "string",
|
||||
Description: "Control type: guest or command",
|
||||
Enum: []string{"guest", "command"},
|
||||
},
|
||||
"guest_id": {
|
||||
Type: "string",
|
||||
Description: "For guest: VMID or name",
|
||||
},
|
||||
"action": {
|
||||
Type: "string",
|
||||
Description: "For guest: start, stop, shutdown, restart, delete",
|
||||
Enum: []string{"start", "stop", "shutdown", "restart", "delete"},
|
||||
},
|
||||
"command": {
|
||||
Type: "string",
|
||||
Description: "For command type: the shell command to execute",
|
||||
},
|
||||
"target_host": {
|
||||
Type: "string",
|
||||
Description: "For command type: hostname to run command on",
|
||||
},
|
||||
"run_on_host": {
|
||||
Type: "boolean",
|
||||
Description: "For command type: run on host (default true)",
|
||||
},
|
||||
"force": {
|
||||
Type: "boolean",
|
||||
Description: "For guest stop: force stop without graceful shutdown",
|
||||
},
|
||||
},
|
||||
Required: []string{"type"},
|
||||
},
|
||||
},
|
||||
Handler: func(ctx context.Context, exec *PulseToolExecutor, args map[string]interface{}) (CallToolResult, error) {
|
||||
return exec.executeControl(ctx, args)
|
||||
},
|
||||
RequireControl: true,
|
||||
})
|
||||
}
|
||||
|
||||
// executeControl routes to the appropriate control handler based on type
|
||||
// Handler functions are implemented in tools_control.go
|
||||
func (e *PulseToolExecutor) executeControl(ctx context.Context, args map[string]interface{}) (CallToolResult, error) {
|
||||
controlType, _ := args["type"].(string)
|
||||
switch controlType {
|
||||
case "guest":
|
||||
return e.executeControlGuest(ctx, args)
|
||||
case "command":
|
||||
return e.executeRunCommand(ctx, args)
|
||||
default:
|
||||
return NewErrorResult(fmt.Errorf("unknown type: %s. Use: guest, command", controlType)), nil
|
||||
}
|
||||
}
|
||||
|
|
@ -3,507 +3,85 @@ package tools
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// getCommandContext returns information about how to run commands on a resource.
|
||||
// This helps the AI understand what commands to use with pulse_control.
|
||||
type CommandContext struct {
|
||||
// How to run commands: "direct" (agent on resource), "via_host" (agent on parent host)
|
||||
Method string `json:"method"`
|
||||
// The target_host value to use with pulse_control
|
||||
TargetHost string `json:"target_host"`
|
||||
// Example command pattern (what to pass to pulse_control)
|
||||
Example string `json:"example"`
|
||||
// For containers running inside this resource (e.g., Docker in LXC)
|
||||
NestedExample string `json:"nested_example,omitempty"`
|
||||
}
|
||||
|
||||
// getCLIAccessPattern returns context about the resource type.
|
||||
// Does NOT prescribe how to access - the AI should determine that based on available agents.
|
||||
func getCLIAccessPattern(resourceType, hostID, resourceID string) string {
|
||||
switch resourceType {
|
||||
case "lxc":
|
||||
return fmt.Sprintf("LXC container on Proxmox node '%s' (VMID %s)", hostID, resourceID)
|
||||
case "vm":
|
||||
return fmt.Sprintf("VM on Proxmox node '%s' (VMID %s)", hostID, resourceID)
|
||||
case "docker":
|
||||
return fmt.Sprintf("Docker container '%s' on host '%s'", resourceID, hostID)
|
||||
case "host":
|
||||
return fmt.Sprintf("Host '%s'", hostID)
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// commonServicePaths contains typical log/config paths for well-known services
|
||||
// These are fallbacks when discovery doesn't find specific paths
|
||||
var commonServicePaths = map[string]struct {
|
||||
LogPaths []string
|
||||
ConfigPaths []string
|
||||
DataPaths []string
|
||||
}{
|
||||
"jellyfin": {
|
||||
LogPaths: []string{"/var/log/jellyfin/", "/config/log/"},
|
||||
ConfigPaths: []string{"/etc/jellyfin/", "/config/"},
|
||||
DataPaths: []string{"/var/lib/jellyfin/", "/config/data/"},
|
||||
},
|
||||
"plex": {
|
||||
LogPaths: []string{"/var/lib/plexmediaserver/Library/Application Support/Plex Media Server/Logs/"},
|
||||
ConfigPaths: []string{"/var/lib/plexmediaserver/Library/Application Support/Plex Media Server/"},
|
||||
DataPaths: []string{"/var/lib/plexmediaserver/"},
|
||||
},
|
||||
"sonarr": {
|
||||
LogPaths: []string{"/config/logs/"},
|
||||
ConfigPaths: []string{"/config/"},
|
||||
DataPaths: []string{"/config/"},
|
||||
},
|
||||
"radarr": {
|
||||
LogPaths: []string{"/config/logs/"},
|
||||
ConfigPaths: []string{"/config/"},
|
||||
DataPaths: []string{"/config/"},
|
||||
},
|
||||
"prowlarr": {
|
||||
LogPaths: []string{"/config/logs/"},
|
||||
ConfigPaths: []string{"/config/"},
|
||||
DataPaths: []string{"/config/"},
|
||||
},
|
||||
"lidarr": {
|
||||
LogPaths: []string{"/config/logs/"},
|
||||
ConfigPaths: []string{"/config/"},
|
||||
DataPaths: []string{"/config/"},
|
||||
},
|
||||
"postgresql": {
|
||||
LogPaths: []string{"/var/log/postgresql/", "/var/lib/postgresql/data/log/"},
|
||||
ConfigPaths: []string{"/etc/postgresql/", "/var/lib/postgresql/data/"},
|
||||
DataPaths: []string{"/var/lib/postgresql/data/"},
|
||||
},
|
||||
"mysql": {
|
||||
LogPaths: []string{"/var/log/mysql/", "/var/lib/mysql/"},
|
||||
ConfigPaths: []string{"/etc/mysql/"},
|
||||
DataPaths: []string{"/var/lib/mysql/"},
|
||||
},
|
||||
"mariadb": {
|
||||
LogPaths: []string{"/var/log/mysql/", "/var/lib/mysql/"},
|
||||
ConfigPaths: []string{"/etc/mysql/"},
|
||||
DataPaths: []string{"/var/lib/mysql/"},
|
||||
},
|
||||
"nginx": {
|
||||
LogPaths: []string{"/var/log/nginx/"},
|
||||
ConfigPaths: []string{"/etc/nginx/"},
|
||||
DataPaths: []string{"/var/www/"},
|
||||
},
|
||||
"homeassistant": {
|
||||
LogPaths: []string{"/config/home-assistant.log"},
|
||||
ConfigPaths: []string{"/config/"},
|
||||
DataPaths: []string{"/config/"},
|
||||
},
|
||||
"frigate": {
|
||||
LogPaths: []string{"/config/logs/"},
|
||||
ConfigPaths: []string{"/config/"},
|
||||
DataPaths: []string{"/media/frigate/"},
|
||||
},
|
||||
"redis": {
|
||||
LogPaths: []string{"/var/log/redis/"},
|
||||
ConfigPaths: []string{"/etc/redis/"},
|
||||
DataPaths: []string{"/var/lib/redis/"},
|
||||
},
|
||||
"mongodb": {
|
||||
LogPaths: []string{"/var/log/mongodb/"},
|
||||
ConfigPaths: []string{"/etc/mongod.conf"},
|
||||
DataPaths: []string{"/var/lib/mongodb/"},
|
||||
},
|
||||
"grafana": {
|
||||
LogPaths: []string{"/var/log/grafana/"},
|
||||
ConfigPaths: []string{"/etc/grafana/"},
|
||||
DataPaths: []string{"/var/lib/grafana/"},
|
||||
},
|
||||
"prometheus": {
|
||||
LogPaths: []string{"/var/log/prometheus/"},
|
||||
ConfigPaths: []string{"/etc/prometheus/"},
|
||||
DataPaths: []string{"/var/lib/prometheus/"},
|
||||
},
|
||||
"influxdb": {
|
||||
LogPaths: []string{"/var/log/influxdb/"},
|
||||
ConfigPaths: []string{"/etc/influxdb/"},
|
||||
DataPaths: []string{"/var/lib/influxdb/"},
|
||||
},
|
||||
}
|
||||
|
||||
// getCommonServicePaths returns fallback paths for a service type
|
||||
func getCommonServicePaths(serviceType string) (logPaths, configPaths, dataPaths []string) {
|
||||
// Normalize service type (lowercase, remove version numbers)
|
||||
normalized := strings.ToLower(serviceType)
|
||||
// Try to match against known services
|
||||
for key, paths := range commonServicePaths {
|
||||
if strings.Contains(normalized, key) {
|
||||
return paths.LogPaths, paths.ConfigPaths, paths.DataPaths
|
||||
}
|
||||
}
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
// registerDiscoveryTools registers AI-powered infrastructure discovery tools
|
||||
func (e *PulseToolExecutor) registerDiscoveryTools() {
|
||||
// registerDiscoveryToolsConsolidated registers the consolidated pulse_discovery tool
|
||||
func (e *PulseToolExecutor) registerDiscoveryToolsConsolidated() {
|
||||
e.registry.Register(RegisteredTool{
|
||||
Definition: Tool{
|
||||
Name: "pulse_get_discovery",
|
||||
Description: `Get AI-discovered information about a specific resource (VM, LXC container, Docker container, or host).
|
||||
Name: "pulse_discovery",
|
||||
Description: `Get deep AI-discovered information about services (log paths, config locations, service details).
|
||||
|
||||
Returns: JSON with service type, version, config paths, data paths, CLI access command, and discovered facts.
|
||||
Actions:
|
||||
- get: Trigger discovery and get detailed info for a specific resource. Use this when you need deep context about a container/VM (where logs are, config paths, service details). Requires resource_type, resource_id, and host_id - use pulse_query action="search" first if you don't know these.
|
||||
- list: Search existing discoveries only. Will NOT find resources that haven't been discovered yet. Use action="get" to trigger discovery for new resources.
|
||||
|
||||
Use when: You need detailed context about a resource before proposing remediation commands, or when investigating what services are running on infrastructure.
|
||||
Workflow for investigating applications:
|
||||
1. Use pulse_query action="search" to find the resource by name
|
||||
2. Use pulse_discovery action="get" with the resource details to get deep context (log paths, config locations)
|
||||
3. Use pulse_control type="command" to run commands (check logs, query app state, etc.)
|
||||
|
||||
The discovery includes:
|
||||
- Service type and version (e.g., "Frigate NVR v0.13.2")
|
||||
- Configuration file locations
|
||||
- Data/storage paths
|
||||
- CLI access command (e.g., "pct exec 101 -- <command>")
|
||||
- Discovered facts (ports, GPUs, connected services, etc.)
|
||||
- User-added notes
|
||||
|
||||
This information is critical for proposing correct remediation commands that match the actual service configuration.`,
|
||||
Examples:
|
||||
- Trigger discovery: action="get", resource_type="docker", resource_id="jellyfin", host_id="docker-host-1"
|
||||
- Search existing: action="list", service_type="postgresql"`,
|
||||
InputSchema: InputSchema{
|
||||
Type: "object",
|
||||
Properties: map[string]PropertySchema{
|
||||
"action": {
|
||||
Type: "string",
|
||||
Description: "Discovery action: get or list",
|
||||
Enum: []string{"get", "list"},
|
||||
},
|
||||
"resource_type": {
|
||||
Type: "string",
|
||||
Description: "Type of resource: 'vm', 'lxc', 'docker', or 'host'",
|
||||
Description: "For get: resource type (vm, lxc, docker, host)",
|
||||
Enum: []string{"vm", "lxc", "docker", "host"},
|
||||
},
|
||||
"resource_id": {
|
||||
Type: "string",
|
||||
Description: "Resource identifier (VMID for VM/LXC, container name for Docker, hostname for host)",
|
||||
Description: "For get: resource identifier (VMID, container name, hostname)",
|
||||
},
|
||||
"host_id": {
|
||||
Type: "string",
|
||||
Description: "Node/host where the resource runs. For VM/LXC: the PVE node from 'node' field. For Docker: the Docker host. For host type: same as resource_id.",
|
||||
Description: "For get: node/host where resource runs",
|
||||
},
|
||||
},
|
||||
Required: []string{"resource_type", "resource_id", "host_id"},
|
||||
},
|
||||
},
|
||||
Handler: func(ctx context.Context, exec *PulseToolExecutor, args map[string]interface{}) (CallToolResult, error) {
|
||||
return exec.executeGetDiscovery(ctx, args)
|
||||
},
|
||||
})
|
||||
|
||||
e.registry.Register(RegisteredTool{
|
||||
Definition: Tool{
|
||||
Name: "pulse_list_discoveries",
|
||||
Description: `List all AI-discovered infrastructure information with optional filtering.
|
||||
|
||||
Returns: JSON array of discoveries with service types, versions, and summaries.
|
||||
|
||||
Use when: You need an overview of what services are running across infrastructure, or want to find specific service types.
|
||||
|
||||
Filters:
|
||||
- type: Filter by resource type (vm, lxc, docker, host)
|
||||
- host: Filter by host/node ID
|
||||
- service_type: Filter by discovered service type (e.g., "frigate", "postgresql")`,
|
||||
InputSchema: InputSchema{
|
||||
Type: "object",
|
||||
Properties: map[string]PropertySchema{
|
||||
"type": {
|
||||
Type: "string",
|
||||
Description: "Optional: Filter by resource type",
|
||||
Description: "For list: filter by resource type",
|
||||
Enum: []string{"vm", "lxc", "docker", "host"},
|
||||
},
|
||||
"host": {
|
||||
Type: "string",
|
||||
Description: "Optional: Filter by host/node ID",
|
||||
Description: "For list: filter by host/node ID",
|
||||
},
|
||||
"service_type": {
|
||||
Type: "string",
|
||||
Description: "Optional: Filter by discovered service type",
|
||||
Description: "For list: filter by service type (e.g., frigate, postgresql)",
|
||||
},
|
||||
"limit": {
|
||||
Type: "integer",
|
||||
Description: "Maximum number of results (default: 50)",
|
||||
Description: "For list: maximum results (default: 50)",
|
||||
},
|
||||
},
|
||||
Required: []string{"action"},
|
||||
},
|
||||
},
|
||||
Handler: func(ctx context.Context, exec *PulseToolExecutor, args map[string]interface{}) (CallToolResult, error) {
|
||||
return exec.executeListDiscoveries(ctx, args)
|
||||
return exec.executeDiscovery(ctx, args)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (e *PulseToolExecutor) executeGetDiscovery(ctx context.Context, args map[string]interface{}) (CallToolResult, error) {
|
||||
if e.discoveryProvider == nil {
|
||||
return NewTextResult("Discovery service not available."), nil
|
||||
// executeDiscovery routes to the appropriate discovery handler based on action
|
||||
// Handler functions are implemented in tools_discovery.go
|
||||
func (e *PulseToolExecutor) executeDiscovery(ctx context.Context, args map[string]interface{}) (CallToolResult, error) {
|
||||
action, _ := args["action"].(string)
|
||||
switch action {
|
||||
case "get":
|
||||
return e.executeGetDiscovery(ctx, args)
|
||||
case "list":
|
||||
return e.executeListDiscoveries(ctx, args)
|
||||
default:
|
||||
return NewErrorResult(fmt.Errorf("unknown action: %s. Use: get, list", action)), nil
|
||||
}
|
||||
|
||||
resourceType, _ := args["resource_type"].(string)
|
||||
resourceID, _ := args["resource_id"].(string)
|
||||
hostID, _ := args["host_id"].(string)
|
||||
|
||||
if resourceType == "" {
|
||||
return NewErrorResult(fmt.Errorf("resource_type is required")), nil
|
||||
}
|
||||
if resourceID == "" {
|
||||
return NewErrorResult(fmt.Errorf("resource_id is required")), nil
|
||||
}
|
||||
if hostID == "" {
|
||||
return NewErrorResult(fmt.Errorf("host_id is required - use the 'node' field from search or get_resource results")), nil
|
||||
}
|
||||
|
||||
// For LXC and VM types, resourceID should be a numeric VMID.
|
||||
// If a name was passed, try to resolve it to a VMID.
|
||||
if (resourceType == "lxc" || resourceType == "vm") && e.stateProvider != nil {
|
||||
if _, err := strconv.Atoi(resourceID); err != nil {
|
||||
// Not a number - try to resolve the name to a VMID
|
||||
state := e.stateProvider.GetState()
|
||||
resolved := false
|
||||
|
||||
if resourceType == "lxc" {
|
||||
for _, c := range state.Containers {
|
||||
if strings.EqualFold(c.Name, resourceID) && c.Node == hostID {
|
||||
resourceID = fmt.Sprintf("%d", c.VMID)
|
||||
resolved = true
|
||||
break
|
||||
}
|
||||
}
|
||||
} else if resourceType == "vm" {
|
||||
for _, vm := range state.VMs {
|
||||
if strings.EqualFold(vm.Name, resourceID) && vm.Node == hostID {
|
||||
resourceID = fmt.Sprintf("%d", vm.VMID)
|
||||
resolved = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !resolved {
|
||||
return NewErrorResult(fmt.Errorf("could not resolve resource name '%s' to a VMID on host '%s'", resourceID, hostID)), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// First try to get existing discovery
|
||||
discovery, err := e.discoveryProvider.GetDiscoveryByResource(resourceType, hostID, resourceID)
|
||||
if err != nil {
|
||||
return NewErrorResult(fmt.Errorf("failed to get discovery: %w", err)), nil
|
||||
}
|
||||
|
||||
// Compute CLI access pattern (always useful, even if discovery fails)
|
||||
cliAccess := getCLIAccessPattern(resourceType, hostID, resourceID)
|
||||
|
||||
// If no discovery exists, trigger one
|
||||
if discovery == nil {
|
||||
discovery, err = e.discoveryProvider.TriggerDiscovery(ctx, resourceType, hostID, resourceID)
|
||||
if err != nil {
|
||||
// Even on failure, provide cli_access so AI can investigate manually
|
||||
return NewJSONResult(map[string]interface{}{
|
||||
"found": false,
|
||||
"resource_type": resourceType,
|
||||
"resource_id": resourceID,
|
||||
"host_id": hostID,
|
||||
"cli_access": cliAccess,
|
||||
"message": fmt.Sprintf("Discovery failed: %v", err),
|
||||
"hint": "Use pulse_control with type='command' to investigate. Try checking /var/log/ for logs.",
|
||||
}), nil
|
||||
}
|
||||
}
|
||||
|
||||
if discovery == nil {
|
||||
// No discovery but provide cli_access for manual investigation
|
||||
return NewJSONResult(map[string]interface{}{
|
||||
"found": false,
|
||||
"resource_type": resourceType,
|
||||
"resource_id": resourceID,
|
||||
"host_id": hostID,
|
||||
"cli_access": cliAccess,
|
||||
"message": "Discovery returned no data. The resource may not be accessible.",
|
||||
"hint": "Use pulse_control with type='command' to investigate. Try listing /var/log/ or checking running processes.",
|
||||
}), nil
|
||||
}
|
||||
|
||||
// Use fallback cli_access if discovery didn't provide one
|
||||
responseCLIAccess := discovery.CLIAccess
|
||||
if responseCLIAccess == "" {
|
||||
responseCLIAccess = cliAccess
|
||||
}
|
||||
|
||||
// Use fallback paths for known services if discovery didn't find specific ones
|
||||
responseConfigPaths := discovery.ConfigPaths
|
||||
responseDataPaths := discovery.DataPaths
|
||||
var responseLogPaths []string
|
||||
|
||||
if discovery.ServiceType != "" {
|
||||
fallbackLogPaths, fallbackConfigPaths, fallbackDataPaths := getCommonServicePaths(discovery.ServiceType)
|
||||
if len(responseConfigPaths) == 0 && len(fallbackConfigPaths) > 0 {
|
||||
responseConfigPaths = fallbackConfigPaths
|
||||
}
|
||||
if len(responseDataPaths) == 0 && len(fallbackDataPaths) > 0 {
|
||||
responseDataPaths = fallbackDataPaths
|
||||
}
|
||||
if len(fallbackLogPaths) > 0 {
|
||||
responseLogPaths = fallbackLogPaths
|
||||
}
|
||||
}
|
||||
|
||||
// Return the discovery information
|
||||
response := map[string]interface{}{
|
||||
"found": true,
|
||||
"id": discovery.ID,
|
||||
"resource_type": discovery.ResourceType,
|
||||
"resource_id": discovery.ResourceID,
|
||||
"host_id": discovery.HostID,
|
||||
"hostname": discovery.Hostname,
|
||||
"service_type": discovery.ServiceType,
|
||||
"service_name": discovery.ServiceName,
|
||||
"service_version": discovery.ServiceVersion,
|
||||
"category": discovery.Category,
|
||||
"cli_access": responseCLIAccess,
|
||||
"config_paths": responseConfigPaths,
|
||||
"data_paths": responseDataPaths,
|
||||
"confidence": discovery.Confidence,
|
||||
"discovered_at": discovery.DiscoveredAt,
|
||||
"updated_at": discovery.UpdatedAt,
|
||||
}
|
||||
|
||||
// Add log paths if we have them (from fallback or discovery)
|
||||
if len(responseLogPaths) > 0 {
|
||||
response["log_paths"] = responseLogPaths
|
||||
}
|
||||
|
||||
// Add facts if present
|
||||
if len(discovery.Facts) > 0 {
|
||||
facts := make([]map[string]string, 0, len(discovery.Facts))
|
||||
for _, f := range discovery.Facts {
|
||||
facts = append(facts, map[string]string{
|
||||
"category": f.Category,
|
||||
"key": f.Key,
|
||||
"value": f.Value,
|
||||
})
|
||||
}
|
||||
response["facts"] = facts
|
||||
}
|
||||
|
||||
// Add user notes if present
|
||||
if discovery.UserNotes != "" {
|
||||
response["user_notes"] = discovery.UserNotes
|
||||
}
|
||||
|
||||
// Add AI reasoning for context
|
||||
if discovery.AIReasoning != "" {
|
||||
response["ai_reasoning"] = discovery.AIReasoning
|
||||
}
|
||||
|
||||
// Add listening ports if present
|
||||
if len(discovery.Ports) > 0 {
|
||||
ports := make([]map[string]interface{}, 0, len(discovery.Ports))
|
||||
for _, p := range discovery.Ports {
|
||||
port := map[string]interface{}{
|
||||
"port": p.Port,
|
||||
"protocol": p.Protocol,
|
||||
}
|
||||
if p.Process != "" {
|
||||
port["process"] = p.Process
|
||||
}
|
||||
if p.Address != "" {
|
||||
port["address"] = p.Address
|
||||
}
|
||||
ports = append(ports, port)
|
||||
}
|
||||
response["ports"] = ports
|
||||
}
|
||||
|
||||
return NewJSONResult(response), nil
|
||||
}
|
||||
|
||||
func (e *PulseToolExecutor) executeListDiscoveries(_ context.Context, args map[string]interface{}) (CallToolResult, error) {
|
||||
if e.discoveryProvider == nil {
|
||||
return NewTextResult("Discovery service not available."), nil
|
||||
}
|
||||
|
||||
filterType, _ := args["type"].(string)
|
||||
filterHost, _ := args["host"].(string)
|
||||
filterServiceType, _ := args["service_type"].(string)
|
||||
limit := intArg(args, "limit", 50)
|
||||
|
||||
var discoveries []*ResourceDiscoveryInfo
|
||||
var err error
|
||||
|
||||
// Get discoveries based on filters
|
||||
if filterType != "" {
|
||||
discoveries, err = e.discoveryProvider.ListDiscoveriesByType(filterType)
|
||||
} else if filterHost != "" {
|
||||
discoveries, err = e.discoveryProvider.ListDiscoveriesByHost(filterHost)
|
||||
} else {
|
||||
discoveries, err = e.discoveryProvider.ListDiscoveries()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return NewErrorResult(fmt.Errorf("failed to list discoveries: %w", err)), nil
|
||||
}
|
||||
|
||||
// Filter by service type if specified
|
||||
if filterServiceType != "" {
|
||||
filtered := make([]*ResourceDiscoveryInfo, 0)
|
||||
filterLower := strings.ToLower(filterServiceType)
|
||||
for _, d := range discoveries {
|
||||
if strings.Contains(strings.ToLower(d.ServiceType), filterLower) ||
|
||||
strings.Contains(strings.ToLower(d.ServiceName), filterLower) {
|
||||
filtered = append(filtered, d)
|
||||
}
|
||||
}
|
||||
discoveries = filtered
|
||||
}
|
||||
|
||||
// Apply limit
|
||||
if len(discoveries) > limit {
|
||||
discoveries = discoveries[:limit]
|
||||
}
|
||||
|
||||
// Build response
|
||||
results := make([]map[string]interface{}, 0, len(discoveries))
|
||||
for _, d := range discoveries {
|
||||
result := map[string]interface{}{
|
||||
"id": d.ID,
|
||||
"resource_type": d.ResourceType,
|
||||
"resource_id": d.ResourceID,
|
||||
"host_id": d.HostID,
|
||||
"hostname": d.Hostname,
|
||||
"service_type": d.ServiceType,
|
||||
"service_name": d.ServiceName,
|
||||
"service_version": d.ServiceVersion,
|
||||
"category": d.Category,
|
||||
"cli_access": d.CLIAccess,
|
||||
"confidence": d.Confidence,
|
||||
"updated_at": d.UpdatedAt,
|
||||
}
|
||||
|
||||
// Add key facts count
|
||||
if len(d.Facts) > 0 {
|
||||
result["facts_count"] = len(d.Facts)
|
||||
}
|
||||
|
||||
// Add ports count
|
||||
if len(d.Ports) > 0 {
|
||||
result["ports_count"] = len(d.Ports)
|
||||
}
|
||||
|
||||
results = append(results, result)
|
||||
}
|
||||
|
||||
response := map[string]interface{}{
|
||||
"discoveries": results,
|
||||
"total": len(results),
|
||||
}
|
||||
|
||||
if filterType != "" {
|
||||
response["filter_type"] = filterType
|
||||
}
|
||||
if filterHost != "" {
|
||||
response["filter_host"] = filterHost
|
||||
}
|
||||
if filterServiceType != "" {
|
||||
response["filter_service_type"] = filterServiceType
|
||||
}
|
||||
|
||||
return NewJSONResult(response), nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,87 +0,0 @@
|
|||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// registerDiscoveryToolsConsolidated registers the consolidated pulse_discovery tool
|
||||
func (e *PulseToolExecutor) registerDiscoveryToolsConsolidated() {
|
||||
e.registry.Register(RegisteredTool{
|
||||
Definition: Tool{
|
||||
Name: "pulse_discovery",
|
||||
Description: `Get deep AI-discovered information about services (log paths, config locations, service details).
|
||||
|
||||
Actions:
|
||||
- get: Trigger discovery and get detailed info for a specific resource. Use this when you need deep context about a container/VM (where logs are, config paths, service details). Requires resource_type, resource_id, and host_id - use pulse_query action="search" first if you don't know these.
|
||||
- list: Search existing discoveries only. Will NOT find resources that haven't been discovered yet. Use action="get" to trigger discovery for new resources.
|
||||
|
||||
Workflow for investigating applications:
|
||||
1. Use pulse_query action="search" to find the resource by name
|
||||
2. Use pulse_discovery action="get" with the resource details to get deep context (log paths, config locations)
|
||||
3. Use pulse_control type="command" to run commands (check logs, query app state, etc.)
|
||||
|
||||
Examples:
|
||||
- Trigger discovery: action="get", resource_type="docker", resource_id="jellyfin", host_id="docker-host-1"
|
||||
- Search existing: action="list", service_type="postgresql"`,
|
||||
InputSchema: InputSchema{
|
||||
Type: "object",
|
||||
Properties: map[string]PropertySchema{
|
||||
"action": {
|
||||
Type: "string",
|
||||
Description: "Discovery action: get or list",
|
||||
Enum: []string{"get", "list"},
|
||||
},
|
||||
"resource_type": {
|
||||
Type: "string",
|
||||
Description: "For get: resource type (vm, lxc, docker, host)",
|
||||
Enum: []string{"vm", "lxc", "docker", "host"},
|
||||
},
|
||||
"resource_id": {
|
||||
Type: "string",
|
||||
Description: "For get: resource identifier (VMID, container name, hostname)",
|
||||
},
|
||||
"host_id": {
|
||||
Type: "string",
|
||||
Description: "For get: node/host where resource runs",
|
||||
},
|
||||
"type": {
|
||||
Type: "string",
|
||||
Description: "For list: filter by resource type",
|
||||
Enum: []string{"vm", "lxc", "docker", "host"},
|
||||
},
|
||||
"host": {
|
||||
Type: "string",
|
||||
Description: "For list: filter by host/node ID",
|
||||
},
|
||||
"service_type": {
|
||||
Type: "string",
|
||||
Description: "For list: filter by service type (e.g., frigate, postgresql)",
|
||||
},
|
||||
"limit": {
|
||||
Type: "integer",
|
||||
Description: "For list: maximum results (default: 50)",
|
||||
},
|
||||
},
|
||||
Required: []string{"action"},
|
||||
},
|
||||
},
|
||||
Handler: func(ctx context.Context, exec *PulseToolExecutor, args map[string]interface{}) (CallToolResult, error) {
|
||||
return exec.executeDiscovery(ctx, args)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// executeDiscovery routes to the appropriate discovery handler based on action
|
||||
// Handler functions are implemented in tools_discovery.go
|
||||
func (e *PulseToolExecutor) executeDiscovery(ctx context.Context, args map[string]interface{}) (CallToolResult, error) {
|
||||
action, _ := args["action"].(string)
|
||||
switch action {
|
||||
case "get":
|
||||
return e.executeGetDiscovery(ctx, args)
|
||||
case "list":
|
||||
return e.executeListDiscoveries(ctx, args)
|
||||
default:
|
||||
return NewErrorResult(fmt.Errorf("unknown action: %s. Use: get, list", action)), nil
|
||||
}
|
||||
}
|
||||
|
|
@ -5,325 +5,60 @@ import (
|
|||
"fmt"
|
||||
)
|
||||
|
||||
// registerPMGTools registers Proxmox Mail Gateway query tools
|
||||
func (e *PulseToolExecutor) registerPMGTools() {
|
||||
// registerPMGToolsConsolidated registers the consolidated pulse_pmg tool
|
||||
func (e *PulseToolExecutor) registerPMGToolsConsolidated() {
|
||||
e.registry.Register(RegisteredTool{
|
||||
Definition: Tool{
|
||||
Name: "pulse_get_pmg_status",
|
||||
Description: `Get Proxmox Mail Gateway instance status and health.
|
||||
Name: "pulse_pmg",
|
||||
Description: `Query Proxmox Mail Gateway status and statistics.
|
||||
|
||||
Returns: JSON with instances array containing id, name, host, status, version, and nodes (with status, role, uptime, load).
|
||||
Types:
|
||||
- status: Instance status and health (nodes, uptime, load)
|
||||
- mail_stats: Mail flow statistics (counts, spam, virus, bounces)
|
||||
- queues: Mail queue status (active, deferred, hold)
|
||||
- spam: Spam quarantine statistics and score distribution
|
||||
|
||||
Use when: User asks about mail gateway status, PMG health, or mail server infrastructure.`,
|
||||
Examples:
|
||||
- Get status: type="status"
|
||||
- Get specific instance: type="status", instance="pmg01"
|
||||
- Get mail stats: type="mail_stats"
|
||||
- Get queue status: type="queues"
|
||||
- Get spam stats: type="spam"`,
|
||||
InputSchema: InputSchema{
|
||||
Type: "object",
|
||||
Properties: map[string]PropertySchema{
|
||||
"type": {
|
||||
Type: "string",
|
||||
Description: "PMG query type",
|
||||
Enum: []string{"status", "mail_stats", "queues", "spam"},
|
||||
},
|
||||
"instance": {
|
||||
Type: "string",
|
||||
Description: "Optional: specific PMG instance name or ID",
|
||||
},
|
||||
},
|
||||
Required: []string{"type"},
|
||||
},
|
||||
},
|
||||
Handler: func(ctx context.Context, exec *PulseToolExecutor, args map[string]interface{}) (CallToolResult, error) {
|
||||
return exec.executeGetPMGStatus(ctx, args)
|
||||
},
|
||||
})
|
||||
|
||||
e.registry.Register(RegisteredTool{
|
||||
Definition: Tool{
|
||||
Name: "pulse_get_mail_stats",
|
||||
Description: `Get mail flow statistics (counts, spam, virus, bounces).
|
||||
|
||||
Returns: JSON with mail statistics including total in/out, spam in/out, virus counts, bounces, greylist count, and average processing time.
|
||||
|
||||
Use when: User asks about mail flow, email statistics, spam counts, or virus detections.`,
|
||||
InputSchema: InputSchema{
|
||||
Type: "object",
|
||||
Properties: map[string]PropertySchema{
|
||||
"instance": {
|
||||
Type: "string",
|
||||
Description: "Optional: specific PMG instance name or ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Handler: func(ctx context.Context, exec *PulseToolExecutor, args map[string]interface{}) (CallToolResult, error) {
|
||||
return exec.executeGetMailStats(ctx, args)
|
||||
},
|
||||
})
|
||||
|
||||
e.registry.Register(RegisteredTool{
|
||||
Definition: Tool{
|
||||
Name: "pulse_get_mail_queues",
|
||||
Description: `Get mail queue status (active, deferred, hold).
|
||||
|
||||
Returns: JSON with queue status for each node including active, deferred, hold, incoming counts and oldest message age.
|
||||
|
||||
Use when: User asks about mail queues, deferred messages, mail delivery issues, or queue backlogs.`,
|
||||
InputSchema: InputSchema{
|
||||
Type: "object",
|
||||
Properties: map[string]PropertySchema{
|
||||
"instance": {
|
||||
Type: "string",
|
||||
Description: "Optional: specific PMG instance name or ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Handler: func(ctx context.Context, exec *PulseToolExecutor, args map[string]interface{}) (CallToolResult, error) {
|
||||
return exec.executeGetMailQueues(ctx, args)
|
||||
},
|
||||
})
|
||||
|
||||
e.registry.Register(RegisteredTool{
|
||||
Definition: Tool{
|
||||
Name: "pulse_get_spam_stats",
|
||||
Description: `Get spam quarantine statistics and distribution.
|
||||
|
||||
Returns: JSON with quarantine counts (spam, virus, attachment, blacklisted) and spam score distribution buckets.
|
||||
|
||||
Use when: User asks about spam statistics, quarantine status, or spam score distribution.`,
|
||||
InputSchema: InputSchema{
|
||||
Type: "object",
|
||||
Properties: map[string]PropertySchema{
|
||||
"instance": {
|
||||
Type: "string",
|
||||
Description: "Optional: specific PMG instance name or ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Handler: func(ctx context.Context, exec *PulseToolExecutor, args map[string]interface{}) (CallToolResult, error) {
|
||||
return exec.executeGetSpamStats(ctx, args)
|
||||
return exec.executePMG(ctx, args)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (e *PulseToolExecutor) executeGetPMGStatus(_ context.Context, args map[string]interface{}) (CallToolResult, error) {
|
||||
if e.stateProvider == nil {
|
||||
return NewTextResult("State provider not available."), nil
|
||||
// executePMG routes to the appropriate PMG handler based on type
|
||||
func (e *PulseToolExecutor) executePMG(ctx context.Context, args map[string]interface{}) (CallToolResult, error) {
|
||||
pmgType, _ := args["type"].(string)
|
||||
switch pmgType {
|
||||
case "status":
|
||||
return e.executeGetPMGStatus(ctx, args)
|
||||
case "mail_stats":
|
||||
return e.executeGetMailStats(ctx, args)
|
||||
case "queues":
|
||||
return e.executeGetMailQueues(ctx, args)
|
||||
case "spam":
|
||||
return e.executeGetSpamStats(ctx, args)
|
||||
default:
|
||||
return NewErrorResult(fmt.Errorf("unknown type: %s. Use: status, mail_stats, queues, spam", pmgType)), nil
|
||||
}
|
||||
|
||||
instanceFilter, _ := args["instance"].(string)
|
||||
|
||||
state := e.stateProvider.GetState()
|
||||
|
||||
if len(state.PMGInstances) == 0 {
|
||||
return NewTextResult("No Proxmox Mail Gateway instances found. PMG monitoring may not be configured."), nil
|
||||
}
|
||||
|
||||
var instances []PMGInstanceSummary
|
||||
for _, pmg := range state.PMGInstances {
|
||||
if instanceFilter != "" && pmg.ID != instanceFilter && pmg.Name != instanceFilter {
|
||||
continue
|
||||
}
|
||||
|
||||
var nodes []PMGNodeSummary
|
||||
for _, node := range pmg.Nodes {
|
||||
nodes = append(nodes, PMGNodeSummary{
|
||||
Name: node.Name,
|
||||
Status: node.Status,
|
||||
Role: node.Role,
|
||||
Uptime: node.Uptime,
|
||||
LoadAvg: node.LoadAvg,
|
||||
})
|
||||
}
|
||||
|
||||
instances = append(instances, PMGInstanceSummary{
|
||||
ID: pmg.ID,
|
||||
Name: pmg.Name,
|
||||
Host: pmg.Host,
|
||||
Status: pmg.Status,
|
||||
Version: pmg.Version,
|
||||
Nodes: nodes,
|
||||
})
|
||||
}
|
||||
|
||||
if len(instances) == 0 && instanceFilter != "" {
|
||||
return NewTextResult(fmt.Sprintf("PMG instance '%s' not found.", instanceFilter)), nil
|
||||
}
|
||||
|
||||
// Ensure non-nil slices
|
||||
for i := range instances {
|
||||
if instances[i].Nodes == nil {
|
||||
instances[i].Nodes = []PMGNodeSummary{}
|
||||
}
|
||||
}
|
||||
|
||||
response := PMGStatusResponse{
|
||||
Instances: instances,
|
||||
Total: len(instances),
|
||||
}
|
||||
|
||||
return NewJSONResult(response), nil
|
||||
}
|
||||
|
||||
func (e *PulseToolExecutor) executeGetMailStats(_ context.Context, args map[string]interface{}) (CallToolResult, error) {
|
||||
if e.stateProvider == nil {
|
||||
return NewTextResult("State provider not available."), nil
|
||||
}
|
||||
|
||||
instanceFilter, _ := args["instance"].(string)
|
||||
|
||||
state := e.stateProvider.GetState()
|
||||
|
||||
if len(state.PMGInstances) == 0 {
|
||||
return NewTextResult("No Proxmox Mail Gateway instances found. PMG monitoring may not be configured."), nil
|
||||
}
|
||||
|
||||
// If filtering, find that specific instance
|
||||
for _, pmg := range state.PMGInstances {
|
||||
if instanceFilter != "" && pmg.ID != instanceFilter && pmg.Name != instanceFilter {
|
||||
continue
|
||||
}
|
||||
|
||||
if pmg.MailStats == nil {
|
||||
if instanceFilter != "" {
|
||||
return NewTextResult(fmt.Sprintf("No mail statistics available for PMG instance '%s'.", instanceFilter)), nil
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
response := MailStatsResponse{
|
||||
Instance: pmg.Name,
|
||||
Stats: PMGMailStatsSummary{
|
||||
Timeframe: pmg.MailStats.Timeframe,
|
||||
TotalIn: pmg.MailStats.CountIn,
|
||||
TotalOut: pmg.MailStats.CountOut,
|
||||
SpamIn: pmg.MailStats.SpamIn,
|
||||
SpamOut: pmg.MailStats.SpamOut,
|
||||
VirusIn: pmg.MailStats.VirusIn,
|
||||
VirusOut: pmg.MailStats.VirusOut,
|
||||
BouncesIn: pmg.MailStats.BouncesIn,
|
||||
BouncesOut: pmg.MailStats.BouncesOut,
|
||||
BytesIn: pmg.MailStats.BytesIn,
|
||||
BytesOut: pmg.MailStats.BytesOut,
|
||||
GreylistCount: pmg.MailStats.GreylistCount,
|
||||
RBLRejects: pmg.MailStats.RBLRejects,
|
||||
AverageProcessTimeMs: pmg.MailStats.AverageProcessTimeMs,
|
||||
},
|
||||
}
|
||||
|
||||
return NewJSONResult(response), nil
|
||||
}
|
||||
|
||||
if instanceFilter != "" {
|
||||
return NewTextResult(fmt.Sprintf("PMG instance '%s' not found.", instanceFilter)), nil
|
||||
}
|
||||
|
||||
return NewTextResult("No mail statistics available from any PMG instance."), nil
|
||||
}
|
||||
|
||||
func (e *PulseToolExecutor) executeGetMailQueues(_ context.Context, args map[string]interface{}) (CallToolResult, error) {
|
||||
if e.stateProvider == nil {
|
||||
return NewTextResult("State provider not available."), nil
|
||||
}
|
||||
|
||||
instanceFilter, _ := args["instance"].(string)
|
||||
|
||||
state := e.stateProvider.GetState()
|
||||
|
||||
if len(state.PMGInstances) == 0 {
|
||||
return NewTextResult("No Proxmox Mail Gateway instances found. PMG monitoring may not be configured."), nil
|
||||
}
|
||||
|
||||
// Collect queue data from all instances (or filtered instance)
|
||||
for _, pmg := range state.PMGInstances {
|
||||
if instanceFilter != "" && pmg.ID != instanceFilter && pmg.Name != instanceFilter {
|
||||
continue
|
||||
}
|
||||
|
||||
var queues []PMGQueueSummary
|
||||
for _, node := range pmg.Nodes {
|
||||
if node.QueueStatus != nil {
|
||||
queues = append(queues, PMGQueueSummary{
|
||||
Node: node.Name,
|
||||
Active: node.QueueStatus.Active,
|
||||
Deferred: node.QueueStatus.Deferred,
|
||||
Hold: node.QueueStatus.Hold,
|
||||
Incoming: node.QueueStatus.Incoming,
|
||||
Total: node.QueueStatus.Total,
|
||||
OldestAgeSeconds: node.QueueStatus.OldestAge,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if len(queues) == 0 {
|
||||
if instanceFilter != "" {
|
||||
return NewTextResult(fmt.Sprintf("No queue data available for PMG instance '%s'.", instanceFilter)), nil
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
response := MailQueuesResponse{
|
||||
Instance: pmg.Name,
|
||||
Queues: queues,
|
||||
}
|
||||
|
||||
return NewJSONResult(response), nil
|
||||
}
|
||||
|
||||
if instanceFilter != "" {
|
||||
return NewTextResult(fmt.Sprintf("PMG instance '%s' not found.", instanceFilter)), nil
|
||||
}
|
||||
|
||||
return NewTextResult("No mail queue data available from any PMG instance."), nil
|
||||
}
|
||||
|
||||
func (e *PulseToolExecutor) executeGetSpamStats(_ context.Context, args map[string]interface{}) (CallToolResult, error) {
|
||||
if e.stateProvider == nil {
|
||||
return NewTextResult("State provider not available."), nil
|
||||
}
|
||||
|
||||
instanceFilter, _ := args["instance"].(string)
|
||||
|
||||
state := e.stateProvider.GetState()
|
||||
|
||||
if len(state.PMGInstances) == 0 {
|
||||
return NewTextResult("No Proxmox Mail Gateway instances found. PMG monitoring may not be configured."), nil
|
||||
}
|
||||
|
||||
for _, pmg := range state.PMGInstances {
|
||||
if instanceFilter != "" && pmg.ID != instanceFilter && pmg.Name != instanceFilter {
|
||||
continue
|
||||
}
|
||||
|
||||
quarantine := PMGQuarantineSummary{}
|
||||
if pmg.Quarantine != nil {
|
||||
quarantine = PMGQuarantineSummary{
|
||||
Spam: pmg.Quarantine.Spam,
|
||||
Virus: pmg.Quarantine.Virus,
|
||||
Attachment: pmg.Quarantine.Attachment,
|
||||
Blacklisted: pmg.Quarantine.Blacklisted,
|
||||
Total: pmg.Quarantine.Spam + pmg.Quarantine.Virus + pmg.Quarantine.Attachment + pmg.Quarantine.Blacklisted,
|
||||
}
|
||||
}
|
||||
|
||||
var distribution []PMGSpamBucketSummary
|
||||
for _, bucket := range pmg.SpamDistribution {
|
||||
distribution = append(distribution, PMGSpamBucketSummary{
|
||||
Score: bucket.Score,
|
||||
Count: bucket.Count,
|
||||
})
|
||||
}
|
||||
|
||||
response := SpamStatsResponse{
|
||||
Instance: pmg.Name,
|
||||
Quarantine: quarantine,
|
||||
Distribution: distribution,
|
||||
}
|
||||
|
||||
if response.Distribution == nil {
|
||||
response.Distribution = []PMGSpamBucketSummary{}
|
||||
}
|
||||
|
||||
return NewJSONResult(response), nil
|
||||
}
|
||||
|
||||
if instanceFilter != "" {
|
||||
return NewTextResult(fmt.Sprintf("PMG instance '%s' not found.", instanceFilter)), nil
|
||||
}
|
||||
|
||||
return NewTextResult("No spam statistics available from any PMG instance."), nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,64 +0,0 @@
|
|||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// registerPMGToolsConsolidated registers the consolidated pulse_pmg tool
|
||||
func (e *PulseToolExecutor) registerPMGToolsConsolidated() {
|
||||
e.registry.Register(RegisteredTool{
|
||||
Definition: Tool{
|
||||
Name: "pulse_pmg",
|
||||
Description: `Query Proxmox Mail Gateway status and statistics.
|
||||
|
||||
Types:
|
||||
- status: Instance status and health (nodes, uptime, load)
|
||||
- mail_stats: Mail flow statistics (counts, spam, virus, bounces)
|
||||
- queues: Mail queue status (active, deferred, hold)
|
||||
- spam: Spam quarantine statistics and score distribution
|
||||
|
||||
Examples:
|
||||
- Get status: type="status"
|
||||
- Get specific instance: type="status", instance="pmg01"
|
||||
- Get mail stats: type="mail_stats"
|
||||
- Get queue status: type="queues"
|
||||
- Get spam stats: type="spam"`,
|
||||
InputSchema: InputSchema{
|
||||
Type: "object",
|
||||
Properties: map[string]PropertySchema{
|
||||
"type": {
|
||||
Type: "string",
|
||||
Description: "PMG query type",
|
||||
Enum: []string{"status", "mail_stats", "queues", "spam"},
|
||||
},
|
||||
"instance": {
|
||||
Type: "string",
|
||||
Description: "Optional: specific PMG instance name or ID",
|
||||
},
|
||||
},
|
||||
Required: []string{"type"},
|
||||
},
|
||||
},
|
||||
Handler: func(ctx context.Context, exec *PulseToolExecutor, args map[string]interface{}) (CallToolResult, error) {
|
||||
return exec.executePMG(ctx, args)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// executePMG routes to the appropriate PMG handler based on type
|
||||
func (e *PulseToolExecutor) executePMG(ctx context.Context, args map[string]interface{}) (CallToolResult, error) {
|
||||
pmgType, _ := args["type"].(string)
|
||||
switch pmgType {
|
||||
case "status":
|
||||
return e.executeGetPMGStatus(ctx, args)
|
||||
case "mail_stats":
|
||||
return e.executeGetMailStats(ctx, args)
|
||||
case "queues":
|
||||
return e.executeGetMailQueues(ctx, args)
|
||||
case "spam":
|
||||
return e.executeGetSpamStats(ctx, args)
|
||||
default:
|
||||
return NewErrorResult(fmt.Errorf("unknown type: %s. Use: status, mail_stats, queues, spam", pmgType)), nil
|
||||
}
|
||||
}
|
||||
10
mock.env
10
mock.env
|
|
@ -1,10 +0,0 @@
|
|||
# Mock Mode Configuration
|
||||
PULSE_MOCK_MODE=false
|
||||
PULSE_MOCK_NODES=7
|
||||
PULSE_MOCK_VMS_PER_NODE=5
|
||||
PULSE_MOCK_LXCS_PER_NODE=8
|
||||
PULSE_MOCK_DOCKER_HOSTS=3
|
||||
PULSE_MOCK_DOCKER_CONTAINERS=12
|
||||
PULSE_MOCK_RANDOM_METRICS=true
|
||||
PULSE_MOCK_STOPPED_PERCENT=20
|
||||
PULSE_LICENSE_PUBLIC_KEY="OzbVzmg+TaSGt0eWzDVpn0QkqhOzJqUbOFvSF3AmuRU="
|
||||
Loading…
Add table
Add a link
Reference in a new issue