mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-22 11:13:26 +00:00
fix: make PMG backup detection more robust for VMID=0 (addresses #359)
- Handle VMID as both string and number types consistently - Check for both 'ct' and 'lxc' backup types (PBS uses 'ct') - Check for both 'vm' and 'qemu' backup types for consistency - Always check VMID=0 first before checking backup type - PBS stores PMG backups as 'ct' type with VMID='0' (string) This should properly identify all PMG host config backups regardless of whether they come from PBS or regular storage, and regardless of whether VMID is a string or number.
This commit is contained in:
@@ -199,24 +199,20 @@ const UnifiedBackups: Component = () => {
|
||||
|
||||
// Determine the display type based on VMID and backup type
|
||||
// VMID 0 = host config backup (e.g. PMG)
|
||||
// PBS stores PMG backups as 'ct' type with VMID='0'
|
||||
let displayType: GuestType;
|
||||
|
||||
// Debug logging for PMG backup detection
|
||||
if (backup.vmid === '0' || backup.vmid === 0 || parseInt(backup.vmid) === 0) {
|
||||
console.log('PBS backup with VMID=0 detected:', {
|
||||
vmid: backup.vmid,
|
||||
vmidType: typeof backup.vmid,
|
||||
backupType: backup.backupType,
|
||||
instance: backup.instance,
|
||||
comment: backup.comment
|
||||
});
|
||||
}
|
||||
// Check for VMID=0 which indicates host backup (handle both string and number)
|
||||
const isVmidZero = backup.vmid === '0' || backup.vmid === 0 || parseInt(String(backup.vmid)) === 0;
|
||||
|
||||
if (parseInt(backup.vmid) === 0 || backup.backupType === 'host') {
|
||||
if (isVmidZero || backup.backupType === 'host') {
|
||||
displayType = 'Host';
|
||||
} else if (backup.backupType === 'vm' || backup.backupType === 'VM') {
|
||||
displayType = 'VM';
|
||||
} else if (backup.backupType === 'ct' || backup.backupType === 'lxc') {
|
||||
displayType = 'LXC';
|
||||
} else {
|
||||
// Default fallback
|
||||
displayType = 'LXC';
|
||||
}
|
||||
|
||||
@@ -266,29 +262,22 @@ const UnifiedBackups: Component = () => {
|
||||
}
|
||||
|
||||
// Determine the display type based on backup.type and VMID
|
||||
// VMID 0 = host config backup (e.g. PMG/PVE host configs)
|
||||
let displayType: GuestType;
|
||||
|
||||
// Debug logging for PMG backup detection in storage backups
|
||||
if (backup.vmid === 0) {
|
||||
console.log('Storage backup with VMID=0 detected:', {
|
||||
vmid: backup.vmid,
|
||||
vmidType: typeof backup.vmid,
|
||||
type: backup.type,
|
||||
volid: backup.volid,
|
||||
notes: backup.notes,
|
||||
storage: backup.storage
|
||||
});
|
||||
}
|
||||
// Check for VMID=0 which indicates host backup
|
||||
const isVmidZero = backup.vmid === 0 || backup.vmid === '0' || parseInt(String(backup.vmid)) === 0;
|
||||
|
||||
// Check for host backups - VMID 0 or type 'host' (for PMG/PVE host configs)
|
||||
if (backup.vmid === 0 || backup.type === 'host') {
|
||||
if (isVmidZero || backup.type === 'host') {
|
||||
displayType = 'Host';
|
||||
} else if (backup.type === 'qemu') {
|
||||
} else if (backup.type === 'qemu' || backup.type === 'vm') {
|
||||
displayType = 'VM';
|
||||
} else if (backup.type === 'lxc') {
|
||||
} else if (backup.type === 'lxc' || backup.type === 'ct') {
|
||||
displayType = 'LXC';
|
||||
} else {
|
||||
displayType = 'LXC'; // Default fallback (most people have more containers than VMs)
|
||||
// Default fallback
|
||||
displayType = 'LXC';
|
||||
}
|
||||
|
||||
// For PBS backups through storage: show Proxmox node in Node column, PBS storage in Location
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
package proxmox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// GetZFSPoolsWithDetails gets both the list and detailed info for all ZFS pools on a node
|
||||
// This combines the list and detail endpoints to get complete information
|
||||
func (c *Client) GetZFSPoolsWithDetails(ctx context.Context, node string) ([]ZFSPoolInfo, error) {
|
||||
// First get the list of pools
|
||||
pools, err := c.GetZFSPoolStatus(ctx, node)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list ZFS pools: %w", err)
|
||||
}
|
||||
|
||||
// Now get details for each pool
|
||||
var poolInfos []ZFSPoolInfo
|
||||
for _, pool := range pools {
|
||||
info := ZFSPoolInfo{
|
||||
Name: pool.Name,
|
||||
Health: pool.Health,
|
||||
Size: pool.Size,
|
||||
Alloc: pool.Alloc,
|
||||
Free: pool.Free,
|
||||
Frag: pool.Frag,
|
||||
Dedup: pool.Dedup,
|
||||
}
|
||||
|
||||
// Try to get detailed info, but don't fail if it's not available
|
||||
detail, err := c.GetZFSPoolDetail(ctx, node, pool.Name)
|
||||
if err != nil {
|
||||
log.Debug().
|
||||
Err(err).
|
||||
Str("node", node).
|
||||
Str("pool", pool.Name).
|
||||
Msg("Could not get ZFS pool details, using basic info only")
|
||||
// Continue with basic info
|
||||
} else {
|
||||
info.State = detail.State
|
||||
info.Status = detail.Status
|
||||
info.Scan = detail.Scan
|
||||
info.Errors = detail.Errors
|
||||
info.Devices = detail.Children
|
||||
}
|
||||
|
||||
poolInfos = append(poolInfos, info)
|
||||
}
|
||||
|
||||
return poolInfos, nil
|
||||
}
|
||||
|
||||
// ZFSPoolInfo combines list and detail info for a complete picture
|
||||
type ZFSPoolInfo struct {
|
||||
// From list endpoint
|
||||
Name string `json:"name"`
|
||||
Health string `json:"health"`
|
||||
Size uint64 `json:"size"`
|
||||
Alloc uint64 `json:"alloc"`
|
||||
Free uint64 `json:"free"`
|
||||
Frag int `json:"frag"`
|
||||
Dedup float64 `json:"dedup"`
|
||||
|
||||
// From detail endpoint (may be empty if not available)
|
||||
State string `json:"state,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
Scan string `json:"scan,omitempty"`
|
||||
Errors string `json:"errors,omitempty"`
|
||||
Devices []ZFSPoolDevice `json:"devices,omitempty"`
|
||||
}
|
||||
|
||||
// ConvertToModelZFSPool converts the combined pool info to our model
|
||||
func (p *ZFSPoolInfo) ConvertToModelZFSPool() *ZFSPool {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Use State if available, otherwise fall back to Health
|
||||
state := p.State
|
||||
if state == "" {
|
||||
state = p.Health
|
||||
}
|
||||
|
||||
pool := &ZFSPool{
|
||||
Name: p.Name,
|
||||
State: state,
|
||||
Health: p.Health,
|
||||
Status: p.Status,
|
||||
Scan: p.Scan,
|
||||
Errors: p.Errors,
|
||||
}
|
||||
|
||||
// Extract error counts from devices if available
|
||||
pool.Devices = make([]ZFSDevice, 0)
|
||||
for _, dev := range p.Devices {
|
||||
pool.Devices = append(pool.Devices, convertDeviceRecursive(dev)...)
|
||||
}
|
||||
|
||||
// Calculate total errors from all devices
|
||||
for _, dev := range pool.Devices {
|
||||
pool.ReadErrors += dev.ReadErrors
|
||||
pool.WriteErrors += dev.WriteErrors
|
||||
pool.ChecksumErrors += dev.ChecksumErrors
|
||||
}
|
||||
|
||||
return pool
|
||||
}
|
||||
|
||||
// ZFSPool represents complete ZFS pool information for monitoring
|
||||
type ZFSPool struct {
|
||||
Name string `json:"name"`
|
||||
State string `json:"state"`
|
||||
Health string `json:"health"`
|
||||
Status string `json:"status"`
|
||||
Scan string `json:"scan"`
|
||||
Errors string `json:"errors"`
|
||||
ReadErrors int64 `json:"readErrors"`
|
||||
WriteErrors int64 `json:"writeErrors"`
|
||||
ChecksumErrors int64 `json:"checksumErrors"`
|
||||
Devices []ZFSDevice `json:"devices"`
|
||||
}
|
||||
|
||||
// ZFSDevice represents a device in the pool (flattened from tree structure)
|
||||
type ZFSDevice struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
State string `json:"state"`
|
||||
ReadErrors int64 `json:"readErrors"`
|
||||
WriteErrors int64 `json:"writeErrors"`
|
||||
ChecksumErrors int64 `json:"checksumErrors"`
|
||||
IsLeaf bool `json:"isLeaf"`
|
||||
}
|
||||
|
||||
// convertDeviceRecursive flattens the device tree into a list
|
||||
func convertDeviceRecursive(dev ZFSPoolDevice) []ZFSDevice {
|
||||
var devices []ZFSDevice
|
||||
|
||||
// Determine device type based on name and structure
|
||||
deviceType := "disk"
|
||||
if dev.Leaf == 0 && len(dev.Children) > 0 {
|
||||
// It's a vdev (mirror, raidz, etc.)
|
||||
if dev.Name == "mirror" || dev.Name[:6] == "mirror" {
|
||||
deviceType = "mirror"
|
||||
} else if len(dev.Name) >= 5 && dev.Name[:5] == "raidz" {
|
||||
deviceType = dev.Name // raidz, raidz2, raidz3
|
||||
} else {
|
||||
deviceType = "vdev"
|
||||
}
|
||||
}
|
||||
|
||||
// Add this device if it has errors or is not healthy
|
||||
if dev.State != "ONLINE" || dev.Read > 0 || dev.Write > 0 || dev.Cksum > 0 {
|
||||
devices = append(devices, ZFSDevice{
|
||||
Name: dev.Name,
|
||||
Type: deviceType,
|
||||
State: dev.State,
|
||||
ReadErrors: dev.Read,
|
||||
WriteErrors: dev.Write,
|
||||
ChecksumErrors: dev.Cksum,
|
||||
IsLeaf: dev.Leaf == 1,
|
||||
})
|
||||
}
|
||||
|
||||
// Process children
|
||||
for _, child := range dev.Children {
|
||||
devices = append(devices, convertDeviceRecursive(child)...)
|
||||
}
|
||||
|
||||
return devices
|
||||
}
|
||||
Reference in New Issue
Block a user