Pulse/internal/ai/tools/tools_storage.go

1174 lines
31 KiB
Go

package tools
import (
"context"
"encoding/json"
"fmt"
"strings"
)
// registerStorageTools registers the pulse_storage tool
func (e *PulseToolExecutor) registerStorageTools() {
e.registry.Register(RegisteredTool{
Definition: Tool{
Name: "pulse_storage",
Description: `Query storage pools, backups, snapshots, Ceph, replication, RAID, and disk health. Use the "type" parameter to select what to query.`,
InputSchema: InputSchema{
Type: "object",
Properties: map[string]PropertySchema{
"type": {
Type: "string",
Description: "Storage type to query",
Enum: []string{"pools", "backups", "backup_tasks", "snapshots", "ceph", "ceph_details", "replication", "pbs_jobs", "raid", "disk_health", "resource_disks"},
},
"storage_id": {
Type: "string",
Description: "Filter by storage ID (for pools)",
},
"resource_id": {
Type: "string",
Description: "Filter by VM/container ID (for backups, snapshots, resource_disks)",
},
"guest_id": {
Type: "string",
Description: "Filter by guest ID (for snapshots, backup_tasks)",
},
"vm_id": {
Type: "string",
Description: "Filter by VM ID (for replication)",
},
"instance": {
Type: "string",
Description: "Filter by Proxmox/PBS instance",
},
"node": {
Type: "string",
Description: "Filter by node name",
},
"host": {
Type: "string",
Description: "Filter by host (for raid, ceph_details)",
},
"cluster": {
Type: "string",
Description: "Filter by Ceph cluster name",
},
"job_type": {
Type: "string",
Description: "Filter PBS jobs by type: backup, sync, verify, prune, garbage",
Enum: []string{"backup", "sync", "verify", "prune", "garbage"},
},
"state": {
Type: "string",
Description: "Filter RAID arrays by state: clean, degraded, rebuilding",
},
"status": {
Type: "string",
Description: "Filter backup tasks by status: ok, error",
},
"resource_type": {
Type: "string",
Description: "Filter by type: vm or lxc (for resource_disks)",
},
"min_usage": {
Type: "number",
Description: "Only show resources with disk usage above this percentage (for resource_disks)",
},
"limit": {
Type: "integer",
Description: "Maximum number of results (default: 100)",
},
"offset": {
Type: "integer",
Description: "Number of results to skip",
},
},
Required: []string{"type"},
},
},
Handler: func(ctx context.Context, exec *PulseToolExecutor, args map[string]interface{}) (CallToolResult, error) {
return exec.executeStorage(ctx, args)
},
})
}
// executeStorage routes to the appropriate storage handler based on type
func (e *PulseToolExecutor) executeStorage(ctx context.Context, args map[string]interface{}) (CallToolResult, error) {
storageType, _ := args["type"].(string)
switch storageType {
case "pools":
return e.executeListStorage(ctx, args)
case "backups":
return e.executeListBackups(ctx, args)
case "backup_tasks":
return e.executeListBackupTasks(ctx, args)
case "snapshots":
return e.executeListSnapshots(ctx, args)
case "ceph":
return e.executeGetCephStatus(ctx, args)
case "ceph_details":
return e.executeGetHostCephDetails(ctx, args)
case "replication":
return e.executeGetReplication(ctx, args)
case "pbs_jobs":
return e.executeListPBSJobs(ctx, args)
case "raid":
return e.executeGetHostRAIDStatus(ctx, args)
case "disk_health":
return e.executeGetDiskHealth(ctx, args)
case "resource_disks":
return e.executeGetResourceDisks(ctx, args)
default:
return NewErrorResult(fmt.Errorf("unknown type: %s. Use: pools, backups, backup_tasks, snapshots, ceph, ceph_details, replication, pbs_jobs, raid, disk_health, resource_disks", storageType)), nil
}
}
func (e *PulseToolExecutor) executeListBackups(_ context.Context, args map[string]interface{}) (CallToolResult, error) {
resourceID, _ := args["resource_id"].(string)
limit := intArg(args, "limit", 100)
offset := intArg(args, "offset", 0)
if e.backupProvider == nil {
return NewTextResult("Backup information not available."), nil
}
backups := e.backupProvider.GetBackups()
pbsInstances := e.backupProvider.GetPBSInstances()
response := BackupsResponse{}
// PBS Backups
count := 0
for _, b := range backups.PBS {
if resourceID != "" && b.VMID != resourceID {
continue
}
if count < offset {
count++
continue
}
if len(response.PBS) >= limit {
break
}
response.PBS = append(response.PBS, PBSBackupSummary{
VMID: b.VMID,
BackupType: b.BackupType,
BackupTime: b.BackupTime,
Instance: b.Instance,
Datastore: b.Datastore,
SizeGB: float64(b.Size) / (1024 * 1024 * 1024),
Verified: b.Verified,
Protected: b.Protected,
})
count++
}
// PVE Backups
count = 0
for _, b := range backups.PVE.StorageBackups {
if resourceID != "" && string(rune(b.VMID)) != resourceID {
continue
}
if count < offset {
count++
continue
}
if len(response.PVE) >= limit {
break
}
response.PVE = append(response.PVE, PVEBackupSummary{
VMID: b.VMID,
BackupTime: b.Time,
SizeGB: float64(b.Size) / (1024 * 1024 * 1024),
Storage: b.Storage,
})
count++
}
// PBS Servers
for _, pbs := range pbsInstances {
server := PBSServerSummary{
Name: pbs.Name,
Host: pbs.Host,
Status: pbs.Status,
}
for _, ds := range pbs.Datastores {
server.Datastores = append(server.Datastores, DatastoreSummary{
Name: ds.Name,
UsagePercent: ds.Usage,
FreeGB: float64(ds.Free) / (1024 * 1024 * 1024),
})
}
response.PBSServers = append(response.PBSServers, server)
}
// Recent tasks
for _, t := range backups.PVE.BackupTasks {
if len(response.RecentTasks) >= 20 {
break
}
response.RecentTasks = append(response.RecentTasks, BackupTaskSummary{
VMID: t.VMID,
Node: t.Node,
Status: t.Status,
StartTime: t.StartTime,
})
}
// Ensure non-nil slices
if response.PBS == nil {
response.PBS = []PBSBackupSummary{}
}
if response.PVE == nil {
response.PVE = []PVEBackupSummary{}
}
if response.PBSServers == nil {
response.PBSServers = []PBSServerSummary{}
}
if response.RecentTasks == nil {
response.RecentTasks = []BackupTaskSummary{}
}
return NewJSONResult(response), nil
}
func (e *PulseToolExecutor) executeListStorage(_ context.Context, args map[string]interface{}) (CallToolResult, error) {
storageID, _ := args["storage_id"].(string)
limit := intArg(args, "limit", 100)
offset := intArg(args, "offset", 0)
if e.storageProvider == nil {
return NewTextResult("Storage information not available."), nil
}
storage := e.storageProvider.GetStorage()
cephClusters := e.storageProvider.GetCephClusters()
response := StorageResponse{}
// Storage pools
count := 0
for _, s := range storage {
if storageID != "" && s.ID != storageID && s.Name != storageID {
continue
}
if count < offset {
count++
continue
}
if len(response.Pools) >= limit {
break
}
pool := StoragePoolSummary{
ID: s.ID,
Name: s.Name,
Node: s.Node,
Instance: s.Instance,
Nodes: s.Nodes,
Type: s.Type,
Status: s.Status,
Enabled: s.Enabled,
Active: s.Active,
Path: s.Path,
UsagePercent: s.Usage,
UsedGB: float64(s.Used) / (1024 * 1024 * 1024),
TotalGB: float64(s.Total) / (1024 * 1024 * 1024),
FreeGB: float64(s.Free) / (1024 * 1024 * 1024),
Content: s.Content,
Shared: s.Shared,
}
if s.ZFSPool != nil {
pool.ZFS = &ZFSPoolSummary{
Name: s.ZFSPool.Name,
State: s.ZFSPool.State,
ReadErrors: s.ZFSPool.ReadErrors,
WriteErrors: s.ZFSPool.WriteErrors,
ChecksumErrors: s.ZFSPool.ChecksumErrors,
Scan: s.ZFSPool.Scan,
}
}
response.Pools = append(response.Pools, pool)
count++
}
// Ceph clusters
for _, c := range cephClusters {
response.CephClusters = append(response.CephClusters, CephClusterSummary{
Name: c.Name,
Health: c.Health,
HealthMessage: c.HealthMessage,
UsagePercent: c.UsagePercent,
UsedTB: float64(c.UsedBytes) / (1024 * 1024 * 1024 * 1024),
TotalTB: float64(c.TotalBytes) / (1024 * 1024 * 1024 * 1024),
NumOSDs: c.NumOSDs,
NumOSDsUp: c.NumOSDsUp,
NumOSDsIn: c.NumOSDsIn,
NumMons: c.NumMons,
NumMgrs: c.NumMgrs,
})
}
// Ensure non-nil slices
if response.Pools == nil {
response.Pools = []StoragePoolSummary{}
}
if response.CephClusters == nil {
response.CephClusters = []CephClusterSummary{}
}
return NewJSONResult(response), nil
}
func (e *PulseToolExecutor) executeGetDiskHealth(_ context.Context, _ map[string]interface{}) (CallToolResult, error) {
if e.diskHealthProvider == nil && e.storageProvider == nil {
return NewTextResult("Disk health information not available."), nil
}
response := DiskHealthResponse{
Hosts: []HostDiskHealth{},
}
// SMART and RAID data from host agents
if e.diskHealthProvider != nil {
hosts := e.diskHealthProvider.GetHosts()
for _, host := range hosts {
hostHealth := HostDiskHealth{
Hostname: host.Hostname,
}
// SMART data
for _, disk := range host.Sensors.SMART {
hostHealth.SMART = append(hostHealth.SMART, SMARTDiskSummary{
Device: disk.Device,
Model: disk.Model,
Health: disk.Health,
Temperature: disk.Temperature,
})
}
// RAID arrays
for _, raid := range host.RAID {
hostHealth.RAID = append(hostHealth.RAID, RAIDArraySummary{
Device: raid.Device,
Level: raid.Level,
State: raid.State,
ActiveDevices: raid.ActiveDevices,
WorkingDevices: raid.WorkingDevices,
FailedDevices: raid.FailedDevices,
SpareDevices: raid.SpareDevices,
RebuildPercent: raid.RebuildPercent,
})
}
// Ceph from agent
if host.Ceph != nil {
hostHealth.Ceph = &CephStatusSummary{
Health: host.Ceph.Health.Status,
NumOSDs: host.Ceph.OSDMap.NumOSDs,
NumOSDsUp: host.Ceph.OSDMap.NumUp,
NumOSDsIn: host.Ceph.OSDMap.NumIn,
NumPGs: host.Ceph.PGMap.NumPGs,
UsagePercent: host.Ceph.PGMap.UsagePercent,
}
}
// Only add if there's data
if len(hostHealth.SMART) > 0 || len(hostHealth.RAID) > 0 || hostHealth.Ceph != nil {
// Ensure non-nil slices
if hostHealth.SMART == nil {
hostHealth.SMART = []SMARTDiskSummary{}
}
if hostHealth.RAID == nil {
hostHealth.RAID = []RAIDArraySummary{}
}
response.Hosts = append(response.Hosts, hostHealth)
}
}
}
return NewJSONResult(response), nil
}
// executeGetCephStatus returns Ceph cluster status
func (e *PulseToolExecutor) executeGetCephStatus(_ context.Context, args map[string]interface{}) (CallToolResult, error) {
clusterFilter, _ := args["cluster"].(string)
state := e.stateProvider.GetState()
if len(state.CephClusters) == 0 {
return NewTextResult("No Ceph clusters found. Ceph may not be configured or data is not yet available."), nil
}
type CephSummary struct {
Name string `json:"name"`
Health string `json:"health"`
Details map[string]interface{} `json:"details,omitempty"`
}
var results []CephSummary
for _, cluster := range state.CephClusters {
if clusterFilter != "" && cluster.Name != clusterFilter {
continue
}
summary := CephSummary{
Name: cluster.Name,
Health: cluster.Health,
Details: make(map[string]interface{}),
}
// Add relevant details
if cluster.HealthMessage != "" {
summary.Details["health_message"] = cluster.HealthMessage
}
if cluster.NumOSDs > 0 {
summary.Details["osd_count"] = cluster.NumOSDs
summary.Details["osds_up"] = cluster.NumOSDsUp
summary.Details["osds_in"] = cluster.NumOSDsIn
summary.Details["osds_down"] = cluster.NumOSDs - cluster.NumOSDsUp
}
if cluster.NumMons > 0 {
summary.Details["monitors"] = cluster.NumMons
}
if cluster.TotalBytes > 0 {
summary.Details["total_bytes"] = cluster.TotalBytes
summary.Details["used_bytes"] = cluster.UsedBytes
summary.Details["available_bytes"] = cluster.AvailableBytes
summary.Details["usage_percent"] = cluster.UsagePercent
}
if len(cluster.Pools) > 0 {
summary.Details["pools"] = cluster.Pools
}
results = append(results, summary)
}
if len(results) == 0 && clusterFilter != "" {
return NewTextResult(fmt.Sprintf("Ceph cluster '%s' not found.", clusterFilter)), nil
}
output, _ := json.MarshalIndent(results, "", " ")
return NewTextResult(string(output)), nil
}
// executeGetReplication returns replication job status
func (e *PulseToolExecutor) executeGetReplication(_ context.Context, args map[string]interface{}) (CallToolResult, error) {
vmFilter, _ := args["vm_id"].(string)
state := e.stateProvider.GetState()
if len(state.ReplicationJobs) == 0 {
return NewTextResult("No replication jobs found. Replication may not be configured."), nil
}
type ReplicationSummary struct {
ID string `json:"id"`
GuestID int `json:"guest_id"`
GuestName string `json:"guest_name,omitempty"`
GuestType string `json:"guest_type,omitempty"`
SourceNode string `json:"source_node,omitempty"`
TargetNode string `json:"target_node"`
Schedule string `json:"schedule,omitempty"`
Status string `json:"status"`
LastSync string `json:"last_sync,omitempty"`
NextSync string `json:"next_sync,omitempty"`
LastDuration string `json:"last_duration,omitempty"`
Error string `json:"error,omitempty"`
}
var results []ReplicationSummary
for _, job := range state.ReplicationJobs {
if vmFilter != "" && fmt.Sprintf("%d", job.GuestID) != vmFilter {
continue
}
summary := ReplicationSummary{
ID: job.ID,
GuestID: job.GuestID,
GuestName: job.GuestName,
GuestType: job.GuestType,
SourceNode: job.SourceNode,
TargetNode: job.TargetNode,
Schedule: job.Schedule,
Status: job.Status,
}
if job.LastSyncTime != nil {
summary.LastSync = job.LastSyncTime.Format("2006-01-02 15:04:05")
}
if job.NextSyncTime != nil {
summary.NextSync = job.NextSyncTime.Format("2006-01-02 15:04:05")
}
if job.LastSyncDurationHuman != "" {
summary.LastDuration = job.LastSyncDurationHuman
}
if job.Error != "" {
summary.Error = job.Error
}
results = append(results, summary)
}
if len(results) == 0 && vmFilter != "" {
return NewTextResult(fmt.Sprintf("No replication jobs found for VM %s.", vmFilter)), nil
}
output, _ := json.MarshalIndent(results, "", " ")
return NewTextResult(string(output)), nil
}
// containsAny checks if s contains any of the substrings (case-insensitive)
func containsAny(s string, substrs ...string) bool {
lower := strings.ToLower(s)
for _, sub := range substrs {
if strings.Contains(lower, strings.ToLower(sub)) {
return true
}
}
return false
}
func (e *PulseToolExecutor) executeListSnapshots(_ context.Context, args map[string]interface{}) (CallToolResult, error) {
if e.stateProvider == nil {
return NewTextResult("State provider not available."), nil
}
guestIDFilter, _ := args["guest_id"].(string)
instanceFilter, _ := args["instance"].(string)
limit := intArg(args, "limit", 100)
offset := intArg(args, "offset", 0)
state := e.stateProvider.GetState()
// Build VM name map for enrichment
vmNames := make(map[int]string)
for _, vm := range state.VMs {
vmNames[vm.VMID] = vm.Name
}
for _, ct := range state.Containers {
vmNames[ct.VMID] = ct.Name
}
var snapshots []SnapshotSummary
filteredCount := 0
count := 0
for _, snap := range state.PVEBackups.GuestSnapshots {
// Apply filters
if guestIDFilter != "" && fmt.Sprintf("%d", snap.VMID) != guestIDFilter {
continue
}
if instanceFilter != "" && snap.Instance != instanceFilter {
continue
}
filteredCount++
// Apply pagination
if count < offset {
count++
continue
}
if len(snapshots) >= limit {
count++
continue
}
snapshots = append(snapshots, SnapshotSummary{
ID: snap.ID,
VMID: snap.VMID,
VMName: vmNames[snap.VMID],
Type: snap.Type,
Node: snap.Node,
Instance: snap.Instance,
SnapshotName: snap.Name,
Description: snap.Description,
Time: snap.Time,
VMState: snap.VMState,
SizeBytes: snap.SizeBytes,
})
count++
}
if snapshots == nil {
snapshots = []SnapshotSummary{}
}
response := SnapshotsResponse{
Snapshots: snapshots,
Total: len(state.PVEBackups.GuestSnapshots),
Filtered: filteredCount,
}
return NewJSONResult(response), nil
}
func (e *PulseToolExecutor) executeListPBSJobs(_ context.Context, args map[string]interface{}) (CallToolResult, error) {
if e.backupProvider == nil {
return NewTextResult("Backup provider not available."), nil
}
instanceFilter, _ := args["instance"].(string)
jobTypeFilter, _ := args["job_type"].(string)
pbsInstances := e.backupProvider.GetPBSInstances()
if len(pbsInstances) == 0 {
return NewTextResult("No PBS instances found. PBS monitoring may not be configured."), nil
}
var jobs []PBSJobSummary
for _, pbs := range pbsInstances {
if instanceFilter != "" && pbs.ID != instanceFilter && pbs.Name != instanceFilter {
continue
}
// Backup jobs
if jobTypeFilter == "" || jobTypeFilter == "backup" {
for _, job := range pbs.BackupJobs {
jobs = append(jobs, PBSJobSummary{
ID: job.ID,
Type: "backup",
Store: job.Store,
Status: job.Status,
LastRun: job.LastBackup,
NextRun: job.NextRun,
Error: job.Error,
VMID: job.VMID,
})
}
}
// Sync jobs
if jobTypeFilter == "" || jobTypeFilter == "sync" {
for _, job := range pbs.SyncJobs {
jobs = append(jobs, PBSJobSummary{
ID: job.ID,
Type: "sync",
Store: job.Store,
Status: job.Status,
LastRun: job.LastSync,
NextRun: job.NextRun,
Error: job.Error,
Remote: job.Remote,
})
}
}
// Verify jobs
if jobTypeFilter == "" || jobTypeFilter == "verify" {
for _, job := range pbs.VerifyJobs {
jobs = append(jobs, PBSJobSummary{
ID: job.ID,
Type: "verify",
Store: job.Store,
Status: job.Status,
LastRun: job.LastVerify,
NextRun: job.NextRun,
Error: job.Error,
})
}
}
// Prune jobs
if jobTypeFilter == "" || jobTypeFilter == "prune" {
for _, job := range pbs.PruneJobs {
jobs = append(jobs, PBSJobSummary{
ID: job.ID,
Type: "prune",
Store: job.Store,
Status: job.Status,
LastRun: job.LastPrune,
NextRun: job.NextRun,
Error: job.Error,
})
}
}
// Garbage jobs
if jobTypeFilter == "" || jobTypeFilter == "garbage" {
for _, job := range pbs.GarbageJobs {
jobs = append(jobs, PBSJobSummary{
ID: job.ID,
Type: "garbage",
Store: job.Store,
Status: job.Status,
LastRun: job.LastGarbage,
NextRun: job.NextRun,
Error: job.Error,
RemovedBytes: job.RemovedBytes,
})
}
}
}
if jobs == nil {
jobs = []PBSJobSummary{}
}
response := PBSJobsResponse{
Instance: instanceFilter,
Jobs: jobs,
Total: len(jobs),
}
return NewJSONResult(response), nil
}
func (e *PulseToolExecutor) executeListBackupTasks(_ context.Context, args map[string]interface{}) (CallToolResult, error) {
if e.stateProvider == nil {
return NewTextResult("State provider not available."), nil
}
instanceFilter, _ := args["instance"].(string)
guestIDFilter, _ := args["guest_id"].(string)
statusFilter, _ := args["status"].(string)
limit := intArg(args, "limit", 50)
state := e.stateProvider.GetState()
// Build VM name map
vmNames := make(map[int]string)
for _, vm := range state.VMs {
vmNames[vm.VMID] = vm.Name
}
for _, ct := range state.Containers {
vmNames[ct.VMID] = ct.Name
}
var tasks []BackupTaskDetail
filteredCount := 0
for _, task := range state.PVEBackups.BackupTasks {
// Apply filters
if instanceFilter != "" && task.Instance != instanceFilter {
continue
}
if guestIDFilter != "" && fmt.Sprintf("%d", task.VMID) != guestIDFilter {
continue
}
if statusFilter != "" && !strings.EqualFold(task.Status, statusFilter) {
continue
}
filteredCount++
if len(tasks) >= limit {
continue
}
tasks = append(tasks, BackupTaskDetail{
ID: task.ID,
VMID: task.VMID,
VMName: vmNames[task.VMID],
Node: task.Node,
Instance: task.Instance,
Type: task.Type,
Status: task.Status,
StartTime: task.StartTime,
EndTime: task.EndTime,
SizeBytes: task.Size,
Error: task.Error,
})
}
if tasks == nil {
tasks = []BackupTaskDetail{}
}
response := BackupTasksListResponse{
Tasks: tasks,
Total: len(state.PVEBackups.BackupTasks),
Filtered: filteredCount,
}
return NewJSONResult(response), nil
}
func (e *PulseToolExecutor) executeGetHostRAIDStatus(_ context.Context, args map[string]interface{}) (CallToolResult, error) {
if e.diskHealthProvider == nil {
return NewTextResult("Disk health provider not available."), nil
}
hostFilter, _ := args["host"].(string)
stateFilter, _ := args["state"].(string)
hosts := e.diskHealthProvider.GetHosts()
var hostSummaries []HostRAIDSummary
for _, host := range hosts {
// Apply host filter
if hostFilter != "" && host.ID != hostFilter && host.Hostname != hostFilter && host.DisplayName != hostFilter {
continue
}
// Skip hosts without RAID arrays
if len(host.RAID) == 0 {
continue
}
var arrays []HostRAIDArraySummary
for _, raid := range host.RAID {
// Apply state filter
if stateFilter != "" && !strings.EqualFold(raid.State, stateFilter) {
continue
}
var devices []HostRAIDDeviceSummary
for _, dev := range raid.Devices {
devices = append(devices, HostRAIDDeviceSummary{
Device: dev.Device,
State: dev.State,
Slot: dev.Slot,
})
}
if devices == nil {
devices = []HostRAIDDeviceSummary{}
}
arrays = append(arrays, HostRAIDArraySummary{
Device: raid.Device,
Name: raid.Name,
Level: raid.Level,
State: raid.State,
TotalDevices: raid.TotalDevices,
ActiveDevices: raid.ActiveDevices,
WorkingDevices: raid.WorkingDevices,
FailedDevices: raid.FailedDevices,
SpareDevices: raid.SpareDevices,
UUID: raid.UUID,
RebuildPercent: raid.RebuildPercent,
RebuildSpeed: raid.RebuildSpeed,
Devices: devices,
})
}
if len(arrays) > 0 {
if arrays == nil {
arrays = []HostRAIDArraySummary{}
}
hostSummaries = append(hostSummaries, HostRAIDSummary{
Hostname: host.Hostname,
HostID: host.ID,
Arrays: arrays,
})
}
}
if hostSummaries == nil {
hostSummaries = []HostRAIDSummary{}
}
if len(hostSummaries) == 0 {
if hostFilter != "" {
return NewTextResult(fmt.Sprintf("No RAID arrays found for host '%s'.", hostFilter)), nil
}
return NewTextResult("No RAID arrays found across any hosts. RAID monitoring requires host agents to be configured."), nil
}
response := HostRAIDStatusResponse{
Hosts: hostSummaries,
Total: len(hostSummaries),
}
return NewJSONResult(response), nil
}
func (e *PulseToolExecutor) executeGetHostCephDetails(_ context.Context, args map[string]interface{}) (CallToolResult, error) {
if e.diskHealthProvider == nil {
return NewTextResult("Disk health provider not available."), nil
}
hostFilter, _ := args["host"].(string)
hosts := e.diskHealthProvider.GetHosts()
var hostSummaries []HostCephSummary
for _, host := range hosts {
// Apply host filter
if hostFilter != "" && host.ID != hostFilter && host.Hostname != hostFilter && host.DisplayName != hostFilter {
continue
}
// Skip hosts without Ceph data
if host.Ceph == nil {
continue
}
ceph := host.Ceph
// Build health messages from checks and summary
var healthMessages []HostCephHealthMessage
for checkName, check := range ceph.Health.Checks {
msg := check.Message
if msg == "" {
msg = checkName
}
healthMessages = append(healthMessages, HostCephHealthMessage{
Severity: check.Severity,
Message: msg,
})
}
for _, summary := range ceph.Health.Summary {
healthMessages = append(healthMessages, HostCephHealthMessage{
Severity: summary.Severity,
Message: summary.Message,
})
}
// Build monitor summary
var monSummary *HostCephMonSummary
if ceph.MonMap.NumMons > 0 {
var monitors []HostCephMonitorSummary
for _, mon := range ceph.MonMap.Monitors {
monitors = append(monitors, HostCephMonitorSummary{
Name: mon.Name,
Rank: mon.Rank,
Addr: mon.Addr,
Status: mon.Status,
})
}
monSummary = &HostCephMonSummary{
NumMons: ceph.MonMap.NumMons,
Monitors: monitors,
}
}
// Build manager summary
var mgrSummary *HostCephMgrSummary
if ceph.MgrMap.NumMgrs > 0 || ceph.MgrMap.Available {
mgrSummary = &HostCephMgrSummary{
Available: ceph.MgrMap.Available,
NumMgrs: ceph.MgrMap.NumMgrs,
ActiveMgr: ceph.MgrMap.ActiveMgr,
Standbys: ceph.MgrMap.Standbys,
}
}
// Build pool summaries
var pools []HostCephPoolSummary
for _, pool := range ceph.Pools {
pools = append(pools, HostCephPoolSummary{
ID: pool.ID,
Name: pool.Name,
BytesUsed: pool.BytesUsed,
BytesAvailable: pool.BytesAvailable,
Objects: pool.Objects,
PercentUsed: pool.PercentUsed,
})
}
if healthMessages == nil {
healthMessages = []HostCephHealthMessage{}
}
if pools == nil {
pools = []HostCephPoolSummary{}
}
hostSummaries = append(hostSummaries, HostCephSummary{
Hostname: host.Hostname,
HostID: host.ID,
FSID: ceph.FSID,
Health: HostCephHealthSummary{
Status: ceph.Health.Status,
Messages: healthMessages,
},
MonMap: monSummary,
MgrMap: mgrSummary,
OSDMap: HostCephOSDSummary{
NumOSDs: ceph.OSDMap.NumOSDs,
NumUp: ceph.OSDMap.NumUp,
NumIn: ceph.OSDMap.NumIn,
NumDown: ceph.OSDMap.NumDown,
NumOut: ceph.OSDMap.NumOut,
},
PGMap: HostCephPGSummary{
NumPGs: ceph.PGMap.NumPGs,
BytesTotal: ceph.PGMap.BytesTotal,
BytesUsed: ceph.PGMap.BytesUsed,
BytesAvailable: ceph.PGMap.BytesAvailable,
UsagePercent: ceph.PGMap.UsagePercent,
DegradedRatio: ceph.PGMap.DegradedRatio,
MisplacedRatio: ceph.PGMap.MisplacedRatio,
ReadBytesPerSec: ceph.PGMap.ReadBytesPerSec,
WriteBytesPerSec: ceph.PGMap.WriteBytesPerSec,
ReadOpsPerSec: ceph.PGMap.ReadOpsPerSec,
WriteOpsPerSec: ceph.PGMap.WriteOpsPerSec,
},
Pools: pools,
CollectedAt: ceph.CollectedAt,
})
}
if hostSummaries == nil {
hostSummaries = []HostCephSummary{}
}
if len(hostSummaries) == 0 {
if hostFilter != "" {
return NewTextResult(fmt.Sprintf("No Ceph data found for host '%s'.", hostFilter)), nil
}
return NewTextResult("No Ceph data found from host agents. Ceph monitoring requires host agents to be configured on Ceph nodes."), nil
}
response := HostCephDetailsResponse{
Hosts: hostSummaries,
Total: len(hostSummaries),
}
return NewJSONResult(response), nil
}
func (e *PulseToolExecutor) executeGetResourceDisks(_ context.Context, args map[string]interface{}) (CallToolResult, error) {
if e.stateProvider == nil {
return NewTextResult("State provider not available."), nil
}
resourceFilter, _ := args["resource_id"].(string)
typeFilter, _ := args["type"].(string)
instanceFilter, _ := args["instance"].(string)
minUsage, _ := args["min_usage"].(float64)
state := e.stateProvider.GetState()
var resources []ResourceDisksSummary
// Process VMs
if typeFilter == "" || strings.EqualFold(typeFilter, "vm") {
for _, vm := range state.VMs {
// Apply filters
if resourceFilter != "" && vm.ID != resourceFilter && fmt.Sprintf("%d", vm.VMID) != resourceFilter {
continue
}
if instanceFilter != "" && vm.Instance != instanceFilter {
continue
}
// Skip VMs without disk data
if len(vm.Disks) == 0 {
continue
}
var disks []ResourceDiskInfo
maxUsage := 0.0
for _, disk := range vm.Disks {
if disk.Usage > maxUsage {
maxUsage = disk.Usage
}
disks = append(disks, ResourceDiskInfo{
Device: disk.Device,
Mountpoint: disk.Mountpoint,
Type: disk.Type,
TotalBytes: disk.Total,
UsedBytes: disk.Used,
FreeBytes: disk.Free,
Usage: disk.Usage,
})
}
// Apply min_usage filter
if minUsage > 0 && maxUsage < minUsage {
continue
}
if disks == nil {
disks = []ResourceDiskInfo{}
}
resources = append(resources, ResourceDisksSummary{
ID: vm.ID,
VMID: vm.VMID,
Name: vm.Name,
Type: "vm",
Node: vm.Node,
Instance: vm.Instance,
Disks: disks,
})
}
}
// Process containers
if typeFilter == "" || strings.EqualFold(typeFilter, "lxc") {
for _, ct := range state.Containers {
// Apply filters
if resourceFilter != "" && ct.ID != resourceFilter && fmt.Sprintf("%d", ct.VMID) != resourceFilter {
continue
}
if instanceFilter != "" && ct.Instance != instanceFilter {
continue
}
// Skip containers without disk data
if len(ct.Disks) == 0 {
continue
}
var disks []ResourceDiskInfo
maxUsage := 0.0
for _, disk := range ct.Disks {
if disk.Usage > maxUsage {
maxUsage = disk.Usage
}
disks = append(disks, ResourceDiskInfo{
Device: disk.Device,
Mountpoint: disk.Mountpoint,
Type: disk.Type,
TotalBytes: disk.Total,
UsedBytes: disk.Used,
FreeBytes: disk.Free,
Usage: disk.Usage,
})
}
// Apply min_usage filter
if minUsage > 0 && maxUsage < minUsage {
continue
}
if disks == nil {
disks = []ResourceDiskInfo{}
}
resources = append(resources, ResourceDisksSummary{
ID: ct.ID,
VMID: ct.VMID,
Name: ct.Name,
Type: "lxc",
Node: ct.Node,
Instance: ct.Instance,
Disks: disks,
})
}
}
if resources == nil {
resources = []ResourceDisksSummary{}
}
if len(resources) == 0 {
if resourceFilter != "" {
return NewTextResult(fmt.Sprintf("No disk data found for resource '%s'. Guest agent may not be installed or disk info unavailable.", resourceFilter)), nil
}
return NewTextResult("No disk data available for any VMs or containers. Disk details require guest agents to be installed and running."), nil
}
response := ResourceDisksResponse{
Resources: resources,
Total: len(resources),
}
return NewJSONResult(response), nil
}