mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-19 16:27:37 +00:00
parent
509852b28d
commit
82ba940524
3 changed files with 271 additions and 1 deletions
|
|
@ -1,6 +1,9 @@
|
|||
package models
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// StateSnapshot represents a snapshot of the state without mutex
|
||||
type StateSnapshot struct {
|
||||
|
|
@ -246,6 +249,99 @@ func (s *State) GetSnapshot() StateSnapshot {
|
|||
return snapshot
|
||||
}
|
||||
|
||||
// MergeLinkedHostDisksIntoGuests supplements the filesystem listings of VMs
|
||||
// and containers with disks reported by a unified pulse-agent running inside
|
||||
// the same guest (matched via Host.LinkedVMID / LinkedContainerID).
|
||||
//
|
||||
// The qemu-guest-agent's get-fsinfo can miss filesystems on certain guest
|
||||
// configurations (notably ZFS mounts on PBS, see #1438), while the unified
|
||||
// pulse-agent has direct OS-level visibility through hostmetrics. When a Host
|
||||
// is linked to a guest, this method appends the host agent's Disks, deduped by
|
||||
// mountpoint, to the guest's Disks slice and updates the guest aggregate Disk
|
||||
// usage to include the new partitions.
|
||||
//
|
||||
// Disks already present on the guest from the qemu-guest-agent path take
|
||||
// precedence; host-agent entries are only added for mountpoints the guest does
|
||||
// not already list.
|
||||
func (s *StateSnapshot) MergeLinkedHostDisksIntoGuests() {
|
||||
if s == nil || len(s.Hosts) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
hostByVMID := make(map[string]*Host)
|
||||
hostByContainerID := make(map[string]*Host)
|
||||
for i := range s.Hosts {
|
||||
h := &s.Hosts[i]
|
||||
if id := strings.TrimSpace(h.LinkedVMID); id != "" {
|
||||
hostByVMID[id] = h
|
||||
}
|
||||
if id := strings.TrimSpace(h.LinkedContainerID); id != "" {
|
||||
hostByContainerID[id] = h
|
||||
}
|
||||
}
|
||||
|
||||
if len(hostByVMID) > 0 {
|
||||
for i := range s.VMs {
|
||||
mergeHostDisksIntoGuest(&s.VMs[i].Disk, &s.VMs[i].Disks, hostByVMID[s.VMs[i].ID])
|
||||
}
|
||||
}
|
||||
if len(hostByContainerID) > 0 {
|
||||
for i := range s.Containers {
|
||||
mergeHostDisksIntoGuest(&s.Containers[i].Disk, &s.Containers[i].Disks, hostByContainerID[s.Containers[i].ID])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func mergeHostDisksIntoGuest(guestAggregate *Disk, guestDisks *[]Disk, host *Host) {
|
||||
if host == nil || len(host.Disks) == 0 || guestDisks == nil {
|
||||
return
|
||||
}
|
||||
|
||||
existingMounts := make(map[string]struct{}, len(*guestDisks))
|
||||
for _, d := range *guestDisks {
|
||||
mp := strings.TrimSpace(d.Mountpoint)
|
||||
if mp != "" {
|
||||
existingMounts[mp] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
var added []Disk
|
||||
for _, d := range host.Disks {
|
||||
mp := strings.TrimSpace(d.Mountpoint)
|
||||
if mp == "" {
|
||||
continue
|
||||
}
|
||||
if _, exists := existingMounts[mp]; exists {
|
||||
continue
|
||||
}
|
||||
existingMounts[mp] = struct{}{}
|
||||
added = append(added, d)
|
||||
}
|
||||
|
||||
if len(added) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
merged := make([]Disk, 0, len(*guestDisks)+len(added))
|
||||
merged = append(merged, *guestDisks...)
|
||||
merged = append(merged, added...)
|
||||
*guestDisks = merged
|
||||
|
||||
if guestAggregate == nil {
|
||||
return
|
||||
}
|
||||
for _, d := range added {
|
||||
if d.Total > 0 {
|
||||
guestAggregate.Total += d.Total
|
||||
guestAggregate.Used += d.Used
|
||||
guestAggregate.Free += d.Free
|
||||
}
|
||||
}
|
||||
if guestAggregate.Total > 0 {
|
||||
guestAggregate.Usage = float64(guestAggregate.Used) / float64(guestAggregate.Total) * 100
|
||||
}
|
||||
}
|
||||
|
||||
// GetLastUpdate returns the current state freshness marker without cloning the
|
||||
// full snapshot payload.
|
||||
func (s *State) GetLastUpdate() time.Time {
|
||||
|
|
|
|||
170
internal/models/state_snapshot_merge_test.go
Normal file
170
internal/models/state_snapshot_merge_test.go
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
package models
|
||||
|
||||
import "testing"
|
||||
|
||||
// TestMergeLinkedHostDisksIntoGuests covers #1438: when a unified pulse-agent
|
||||
// runs inside a VM (LinkedVMID set), filesystems it reports that the
|
||||
// qemu-guest-agent's fsinfo missed, typically ZFS mounts on PBS, must be
|
||||
// surfaced in the VM Overview filesystem list. Mountpoints already covered by
|
||||
// qemu-guest-agent fsinfo take precedence and are not touched.
|
||||
func TestMergeLinkedHostDisksIntoGuests(t *testing.T) {
|
||||
t.Run("adds host-only mounts to a linked VM and updates aggregate", func(t *testing.T) {
|
||||
s := &StateSnapshot{
|
||||
VMs: []VM{
|
||||
{
|
||||
ID: "pve01-101",
|
||||
Disk: Disk{
|
||||
Total: 85_100_000_000,
|
||||
Used: 20_500_000_000,
|
||||
Free: 64_600_000_000,
|
||||
Usage: 24,
|
||||
},
|
||||
Disks: []Disk{
|
||||
{Mountpoint: "/", Total: 25_600_000_000, Used: 6_990_000_000, Free: 18_610_000_000, Type: "ext4"},
|
||||
{Mountpoint: "/mnt/datastore/pbs01s3repl01", Total: 59_500_000_000, Used: 13_500_000_000, Free: 46_000_000_000, Type: "ext4"},
|
||||
},
|
||||
},
|
||||
},
|
||||
Hosts: []Host{
|
||||
{
|
||||
ID: "host-pbs01",
|
||||
Hostname: "pbs01",
|
||||
LinkedVMID: "pve01-101",
|
||||
Disks: []Disk{
|
||||
{Mountpoint: "/", Total: 27_000_000_000, Used: 6_990_000_000, Type: "ext4"},
|
||||
{Mountpoint: "/mnt/datastore/pbs01replic", Total: 934_000_000_000, Used: 575_000_000_000, Free: 359_000_000_000, Type: "zfs"},
|
||||
{Mountpoint: "/mnt/datastore/pbs01s3repl01", Total: 62_700_000_000, Used: 13_500_000_000, Type: "zfs"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
s.MergeLinkedHostDisksIntoGuests()
|
||||
|
||||
vm := s.VMs[0]
|
||||
if got := len(vm.Disks); got != 3 {
|
||||
t.Fatalf("expected 3 disks, got %d", got)
|
||||
}
|
||||
if vm.Disks[0].Mountpoint != "/" || vm.Disks[0].Type != "ext4" {
|
||||
t.Errorf("first disk mutated: %+v", vm.Disks[0])
|
||||
}
|
||||
if vm.Disks[1].Mountpoint != "/mnt/datastore/pbs01s3repl01" || vm.Disks[1].Type != "ext4" {
|
||||
t.Errorf("second disk should remain qemu-guest-agent ext4 entry, got: %+v", vm.Disks[1])
|
||||
}
|
||||
if vm.Disks[2].Mountpoint != "/mnt/datastore/pbs01replic" || vm.Disks[2].Type != "zfs" {
|
||||
t.Errorf("expected ZFS dataset appended, got: %+v", vm.Disks[2])
|
||||
}
|
||||
|
||||
expectedTotal := int64(85_100_000_000 + 934_000_000_000)
|
||||
if vm.Disk.Total != expectedTotal {
|
||||
t.Errorf("aggregate Total = %d, want %d", vm.Disk.Total, expectedTotal)
|
||||
}
|
||||
expectedUsed := int64(20_500_000_000 + 575_000_000_000)
|
||||
if vm.Disk.Used != expectedUsed {
|
||||
t.Errorf("aggregate Used = %d, want %d", vm.Disk.Used, expectedUsed)
|
||||
}
|
||||
if vm.Disk.Usage <= 0 || vm.Disk.Usage > 100 {
|
||||
t.Errorf("aggregate Usage out of range: %.2f", vm.Disk.Usage)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("does not mutate VM when no host is linked", func(t *testing.T) {
|
||||
s := &StateSnapshot{
|
||||
VMs: []VM{
|
||||
{
|
||||
ID: "pve01-101",
|
||||
Disk: Disk{Total: 1000, Used: 500, Usage: 50},
|
||||
Disks: []Disk{{Mountpoint: "/", Total: 1000, Used: 500}},
|
||||
},
|
||||
},
|
||||
Hosts: []Host{
|
||||
{ID: "host-other", LinkedVMID: "pve01-999", Disks: []Disk{{Mountpoint: "/data", Total: 9999}}},
|
||||
},
|
||||
}
|
||||
|
||||
s.MergeLinkedHostDisksIntoGuests()
|
||||
|
||||
if len(s.VMs[0].Disks) != 1 {
|
||||
t.Errorf("expected VM to be unchanged, got %d disks", len(s.VMs[0].Disks))
|
||||
}
|
||||
if s.VMs[0].Disk.Total != 1000 {
|
||||
t.Errorf("expected aggregate unchanged, got Total=%d", s.VMs[0].Disk.Total)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("merges into linked container", func(t *testing.T) {
|
||||
s := &StateSnapshot{
|
||||
Containers: []Container{
|
||||
{
|
||||
ID: "pve01-200",
|
||||
Disks: []Disk{{Mountpoint: "/", Total: 8_000_000_000, Used: 1_000_000_000, Type: "ext4"}},
|
||||
Disk: Disk{Total: 8_000_000_000, Used: 1_000_000_000, Usage: 12.5},
|
||||
},
|
||||
},
|
||||
Hosts: []Host{
|
||||
{
|
||||
ID: "host-ct",
|
||||
LinkedContainerID: "pve01-200",
|
||||
Disks: []Disk{{Mountpoint: "/var/lib/zfs", Total: 100_000_000_000, Used: 50_000_000_000, Type: "zfs"}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
s.MergeLinkedHostDisksIntoGuests()
|
||||
|
||||
if len(s.Containers[0].Disks) != 2 {
|
||||
t.Fatalf("expected 2 disks on container, got %d", len(s.Containers[0].Disks))
|
||||
}
|
||||
if s.Containers[0].Disks[1].Mountpoint != "/var/lib/zfs" {
|
||||
t.Errorf("expected ZFS dataset appended to container, got %+v", s.Containers[0].Disks[1])
|
||||
}
|
||||
if s.Containers[0].Disk.Total != 108_000_000_000 {
|
||||
t.Errorf("aggregate Total = %d, want %d", s.Containers[0].Disk.Total, 108_000_000_000)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ignores host disks with empty mountpoints", func(t *testing.T) {
|
||||
s := &StateSnapshot{
|
||||
VMs: []VM{
|
||||
{ID: "pve01-101", Disks: []Disk{{Mountpoint: "/", Total: 1000}}},
|
||||
},
|
||||
Hosts: []Host{
|
||||
{
|
||||
ID: "host-pbs01",
|
||||
LinkedVMID: "pve01-101",
|
||||
Disks: []Disk{
|
||||
{Mountpoint: "", Total: 9999},
|
||||
{Mountpoint: "/zfs", Total: 5000, Type: "zfs"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
s.MergeLinkedHostDisksIntoGuests()
|
||||
|
||||
if len(s.VMs[0].Disks) != 2 {
|
||||
t.Fatalf("expected only valid mountpoints merged, got %d", len(s.VMs[0].Disks))
|
||||
}
|
||||
if s.VMs[0].Disks[1].Mountpoint != "/zfs" {
|
||||
t.Errorf("expected /zfs appended, got %+v", s.VMs[0].Disks[1])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("does not mutate underlying state slice", func(t *testing.T) {
|
||||
original := []Disk{{Mountpoint: "/", Total: 1000}}
|
||||
s := &StateSnapshot{
|
||||
VMs: []VM{
|
||||
{ID: "pve01-101", Disks: original},
|
||||
},
|
||||
Hosts: []Host{
|
||||
{ID: "host-pbs01", LinkedVMID: "pve01-101", Disks: []Disk{{Mountpoint: "/zfs", Total: 5000}}},
|
||||
},
|
||||
}
|
||||
|
||||
s.MergeLinkedHostDisksIntoGuests()
|
||||
|
||||
if len(original) != 1 {
|
||||
t.Errorf("merge mutated the original slice: len=%d, want 1", len(original))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -2712,6 +2712,10 @@ func (m *Monitor) GetState() models.StateSnapshot {
|
|||
// counts or recently resolved incidents from cached state.
|
||||
state.ActiveAlerts = m.activeAlertsSnapshot()
|
||||
state.RecentlyResolved = m.recentlyResolvedAlertsSnapshot()
|
||||
// Surface filesystems reported by a unified pulse-agent inside a guest
|
||||
// (for example ZFS mounts that qemu-guest-agent's get-fsinfo cannot see
|
||||
// on PBS, #1438) in the guest overview disk listing.
|
||||
state.MergeLinkedHostDisksIntoGuests()
|
||||
return state
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue