Implement targeted VM property refresh and backfill logic for snapshot rows
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2026-02-13 16:09:29 +11:00
parent e2779f80c0
commit c446638eac
3 changed files with 286 additions and 0 deletions

View File

@@ -690,6 +690,33 @@ func snapshotFromVM(ctx context.Context, dbConn *sqlx.DB, vmObject *mo.VirtualMa
return InventorySnapshotRow{}, fmt.Errorf("missing VM object")
}
// Rarely, bulk property retrieval can return partial VM objects (for example, nil Config).
// Do a targeted per-VM refresh before building the snapshot row to avoid writing sparse rows.
if vmObject.Config == nil || vmObject.ResourcePool == nil || vmObject.Parent == nil || vmObject.Runtime.Host == nil || vmObject.Runtime.PowerState == "" {
if refreshed, err := vc.GetVMWithSnapshotPropsByRef(vmObject.Reference()); err == nil && refreshed != nil {
if vmObject.Name == "" && refreshed.Name != "" {
vmObject.Name = refreshed.Name
}
if vmObject.Parent == nil && refreshed.Parent != nil {
vmObject.Parent = refreshed.Parent
}
if vmObject.Config == nil && refreshed.Config != nil {
vmObject.Config = refreshed.Config
}
if vmObject.ResourcePool == nil && refreshed.ResourcePool != nil {
vmObject.ResourcePool = refreshed.ResourcePool
}
if vmObject.Runtime.Host == nil && refreshed.Runtime.Host != nil {
vmObject.Runtime.Host = refreshed.Runtime.Host
}
if vmObject.Runtime.PowerState == "" && refreshed.Runtime.PowerState != "" {
vmObject.Runtime.PowerState = refreshed.Runtime.PowerState
}
} else if vc.Logger != nil {
vc.Logger.Warn("failed targeted VM property refresh for snapshot row", "vm_id", vmObject.Reference().Value, "error", err)
}
}
row := InventorySnapshotRow{
Name: vmObject.Name,
Vcenter: vc.Vurl,
@@ -903,9 +930,144 @@ func snapshotFromVM(ctx context.Context, dbConn *sqlx.DB, vmObject *mo.VirtualMa
}
}
if dbConn != nil && needsSnapshotBackfill(row) {
if backfillSnapshotRowFromHourlyCache(ctx, dbConn, &row) && vc.Logger != nil {
vc.Logger.Debug("backfilled sparse VM snapshot row from hourly cache", "vm_id", row.VmId.String, "name", row.Name, "vcenter", row.Vcenter)
}
}
if row.SrmPlaceholder == "" {
row.SrmPlaceholder = "FALSE"
}
if needsSnapshotBackfill(row) && vc.Logger != nil {
vc.Logger.Warn("snapshot row remains sparse after fallbacks",
"vm_id", row.VmId.String,
"name", row.Name,
"vcenter", row.Vcenter,
"has_creation_time", row.CreationTime.Valid,
"has_cluster", row.Cluster.Valid,
"has_disk", row.ProvisionedDisk.Valid,
"has_vcpu", row.VcpuCount.Valid,
"has_ram", row.RamGB.Valid,
"has_vm_uuid", row.VmUuid.Valid,
)
}
return row, nil
}
func needsSnapshotBackfill(row InventorySnapshotRow) bool {
return !row.CreationTime.Valid ||
!row.ProvisionedDisk.Valid ||
!row.VcpuCount.Valid ||
!row.RamGB.Valid ||
!row.Cluster.Valid ||
!row.Datacenter.Valid ||
row.SrmPlaceholder == "" ||
!row.VmUuid.Valid
}
func backfillSnapshotRowFromHourlyCache(ctx context.Context, dbConn *sqlx.DB, row *InventorySnapshotRow) bool {
if row == nil || dbConn == nil {
return false
}
if strings.TrimSpace(row.Vcenter) == "" || strings.TrimSpace(row.VmId.String) == "" {
return false
}
if !db.TableExists(ctx, dbConn, "vm_hourly_stats") {
return false
}
query := `
SELECT
"CreationTime",
"ResourcePool",
"Datacenter",
"Cluster",
"Folder",
"ProvisionedDisk",
"VcpuCount",
"RamGB",
"IsTemplate",
"PoweredOn",
"SrmPlaceholder",
"VmUuid"
FROM vm_hourly_stats
WHERE "Vcenter" = ? AND "VmId" = ?
ORDER BY "SnapshotTime" DESC
LIMIT 1`
query = sqlx.Rebind(sqlx.BindType(dbConn.DriverName()), query)
var cached struct {
CreationTime sql.NullInt64 `db:"CreationTime"`
ResourcePool sql.NullString `db:"ResourcePool"`
Datacenter sql.NullString `db:"Datacenter"`
Cluster sql.NullString `db:"Cluster"`
Folder sql.NullString `db:"Folder"`
ProvisionedDisk sql.NullFloat64 `db:"ProvisionedDisk"`
VcpuCount sql.NullInt64 `db:"VcpuCount"`
RamGB sql.NullInt64 `db:"RamGB"`
IsTemplate sql.NullString `db:"IsTemplate"`
PoweredOn sql.NullString `db:"PoweredOn"`
SrmPlaceholder sql.NullString `db:"SrmPlaceholder"`
VmUuid sql.NullString `db:"VmUuid"`
}
if err := dbConn.GetContext(ctx, &cached, query, row.Vcenter, row.VmId.String); err != nil {
return false
}
changed := false
if !row.CreationTime.Valid && cached.CreationTime.Valid {
row.CreationTime = cached.CreationTime
changed = true
}
if (!row.ResourcePool.Valid || row.ResourcePool.String == "") && cached.ResourcePool.Valid {
pool := normalizeResourcePool(cached.ResourcePool.String)
row.ResourcePool = sql.NullString{String: pool, Valid: pool != ""}
changed = true
}
if (!row.Datacenter.Valid || row.Datacenter.String == "") && cached.Datacenter.Valid {
row.Datacenter = cached.Datacenter
changed = true
}
if (!row.Cluster.Valid || row.Cluster.String == "") && cached.Cluster.Valid {
row.Cluster = cached.Cluster
changed = true
}
if (!row.Folder.Valid || row.Folder.String == "") && cached.Folder.Valid {
row.Folder = cached.Folder
changed = true
}
if !row.ProvisionedDisk.Valid && cached.ProvisionedDisk.Valid {
row.ProvisionedDisk = cached.ProvisionedDisk
changed = true
}
if !row.VcpuCount.Valid && cached.VcpuCount.Valid {
row.VcpuCount = cached.VcpuCount
changed = true
}
if !row.RamGB.Valid && cached.RamGB.Valid {
row.RamGB = cached.RamGB
changed = true
}
if row.IsTemplate == "" && cached.IsTemplate.Valid {
row.IsTemplate = boolStringFromInterface(cached.IsTemplate.String)
changed = true
}
if row.PoweredOn == "" && cached.PoweredOn.Valid {
row.PoweredOn = boolStringFromInterface(cached.PoweredOn.String)
changed = true
}
if row.SrmPlaceholder == "" && cached.SrmPlaceholder.Valid {
row.SrmPlaceholder = boolStringFromInterface(cached.SrmPlaceholder.String)
changed = true
}
if !row.VmUuid.Valid && cached.VmUuid.Valid {
row.VmUuid = cached.VmUuid
changed = true
}
return changed
}
func snapshotFromInventory(inv queries.Inventory, snapshotTime time.Time) InventorySnapshotRow {
return InventorySnapshotRow{
InventoryId: sql.NullInt64{Int64: inv.Iid, Valid: inv.Iid > 0},