[CI SKIP] bugfixes for vm deletion tracking
This commit is contained in:
383
internal/tasks/inventoryHelpers.go
Normal file
383
internal/tasks/inventoryHelpers.go
Normal file
@@ -0,0 +1,383 @@
|
||||
package tasks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"vctp/db"
|
||||
"vctp/db/queries"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
func boolStringFromInterface(value interface{}) string {
|
||||
switch v := value.(type) {
|
||||
case nil:
|
||||
return ""
|
||||
case string:
|
||||
return v
|
||||
case []byte:
|
||||
return string(v)
|
||||
case bool:
|
||||
if v {
|
||||
return "TRUE"
|
||||
}
|
||||
return "FALSE"
|
||||
case int:
|
||||
if v != 0 {
|
||||
return "TRUE"
|
||||
}
|
||||
return "FALSE"
|
||||
case int64:
|
||||
if v != 0 {
|
||||
return "TRUE"
|
||||
}
|
||||
return "FALSE"
|
||||
default:
|
||||
return fmt.Sprint(v)
|
||||
}
|
||||
}
|
||||
|
||||
// latestHourlySnapshotBefore finds the most recent hourly snapshot table prior to the given time.
|
||||
func latestHourlySnapshotBefore(ctx context.Context, dbConn *sqlx.DB, cutoff time.Time) (string, error) {
|
||||
driver := strings.ToLower(dbConn.DriverName())
|
||||
var rows *sqlx.Rows
|
||||
var err error
|
||||
switch driver {
|
||||
case "sqlite":
|
||||
rows, err = dbConn.QueryxContext(ctx, `
|
||||
SELECT name FROM sqlite_master
|
||||
WHERE type = 'table' AND name LIKE 'inventory_hourly_%'
|
||||
`)
|
||||
case "pgx", "postgres":
|
||||
rows, err = dbConn.QueryxContext(ctx, `
|
||||
SELECT tablename FROM pg_catalog.pg_tables
|
||||
WHERE schemaname = 'public' AND tablename LIKE 'inventory_hourly_%'
|
||||
`)
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported driver for snapshot lookup: %s", driver)
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var latest string
|
||||
var latestTime int64
|
||||
for rows.Next() {
|
||||
var name string
|
||||
if scanErr := rows.Scan(&name); scanErr != nil {
|
||||
continue
|
||||
}
|
||||
if !strings.HasPrefix(name, "inventory_hourly_") {
|
||||
continue
|
||||
}
|
||||
suffix := strings.TrimPrefix(name, "inventory_hourly_")
|
||||
epoch, parseErr := strconv.ParseInt(suffix, 10, 64)
|
||||
if parseErr != nil {
|
||||
continue
|
||||
}
|
||||
if epoch < cutoff.Unix() && epoch > latestTime {
|
||||
latestTime = epoch
|
||||
latest = name
|
||||
}
|
||||
}
|
||||
return latest, nil
|
||||
}
|
||||
|
||||
// markMissingFromPrevious marks VMs that were present in the previous snapshot but missing now.
|
||||
func (c *CronTask) markMissingFromPrevious(ctx context.Context, dbConn *sqlx.DB, prevTable string, vcenter string, snapshotTime time.Time,
|
||||
currentByID map[string]InventorySnapshotRow, currentByUuid map[string]struct{}, currentByName map[string]struct{},
|
||||
invByID map[string]queries.Inventory, invByUuid map[string]queries.Inventory, invByName map[string]queries.Inventory) int {
|
||||
|
||||
if err := db.ValidateTableName(prevTable); err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
query := fmt.Sprintf(`SELECT "VmId","VmUuid","Name","Cluster","Datacenter","DeletionTime" FROM %s WHERE "Vcenter" = ?`, prevTable)
|
||||
query = sqlx.Rebind(sqlx.BindType(dbConn.DriverName()), query)
|
||||
|
||||
type prevRow struct {
|
||||
VmId sql.NullString `db:"VmId"`
|
||||
VmUuid sql.NullString `db:"VmUuid"`
|
||||
Name string `db:"Name"`
|
||||
Cluster sql.NullString `db:"Cluster"`
|
||||
Datacenter sql.NullString `db:"Datacenter"`
|
||||
DeletionTime sql.NullInt64 `db:"DeletionTime"`
|
||||
}
|
||||
|
||||
rows, err := dbConn.QueryxContext(ctx, query, vcenter)
|
||||
if err != nil {
|
||||
c.Logger.Warn("failed to read previous snapshot for deletion detection", "error", err, "table", prevTable, "vcenter", vcenter)
|
||||
return 0
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
missing := 0
|
||||
for rows.Next() {
|
||||
var r prevRow
|
||||
if err := rows.StructScan(&r); err != nil {
|
||||
continue
|
||||
}
|
||||
vmID := r.VmId.String
|
||||
uuid := r.VmUuid.String
|
||||
name := r.Name
|
||||
cluster := r.Cluster.String
|
||||
|
||||
found := false
|
||||
if vmID != "" {
|
||||
if _, ok := currentByID[vmID]; ok {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found && uuid != "" {
|
||||
if _, ok := currentByUuid[uuid]; ok {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found && name != "" {
|
||||
if _, ok := currentByName[name]; ok {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
// If the name is missing but UUID+Cluster still exists in inventory/current, treat it as present (rename, not delete).
|
||||
if !found && uuid != "" && cluster != "" {
|
||||
if inv, ok := invByUuid[uuid]; ok && strings.EqualFold(inv.Cluster.String, cluster) {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if found {
|
||||
continue
|
||||
}
|
||||
|
||||
var inv queries.Inventory
|
||||
var ok bool
|
||||
if vmID != "" {
|
||||
inv, ok = invByID[vmID]
|
||||
}
|
||||
if !ok && uuid != "" {
|
||||
inv, ok = invByUuid[uuid]
|
||||
}
|
||||
if !ok && name != "" {
|
||||
inv, ok = invByName[name]
|
||||
}
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if inv.DeletionTime.Valid {
|
||||
continue
|
||||
}
|
||||
|
||||
delTime := sql.NullInt64{Int64: snapshotTime.Unix(), Valid: true}
|
||||
if err := c.Database.Queries().InventoryMarkDeleted(ctx, queries.InventoryMarkDeletedParams{
|
||||
DeletionTime: delTime,
|
||||
VmId: inv.VmId,
|
||||
DatacenterName: inv.Datacenter,
|
||||
}); err != nil {
|
||||
c.Logger.Warn("failed to mark inventory record deleted from previous snapshot", "error", err, "vm_id", inv.VmId.String)
|
||||
continue
|
||||
}
|
||||
// Also update lifecycle cache so deletion time is available for rollups.
|
||||
vmUUID := ""
|
||||
if inv.VmUuid.Valid {
|
||||
vmUUID = inv.VmUuid.String
|
||||
}
|
||||
if err := db.MarkVmDeletedWithDetails(ctx, dbConn, vcenter, inv.VmId.String, vmUUID, inv.Name, inv.Cluster.String, delTime.Int64); err != nil {
|
||||
c.Logger.Warn("failed to mark lifecycle cache deleted from previous snapshot", "error", err, "vm_id", inv.VmId.String, "vm_uuid", vmUUID, "vcenter", vcenter)
|
||||
}
|
||||
c.Logger.Debug("Detected VM missing compared to previous snapshot", "name", inv.Name, "vm_id", inv.VmId.String, "vm_uuid", inv.VmUuid.String, "vcenter", vcenter, "snapshot_time", snapshotTime, "prev_table", prevTable)
|
||||
missing++
|
||||
}
|
||||
|
||||
return missing
|
||||
}
|
||||
|
||||
// countNewFromPrevious returns how many VMs are present in the current snapshot but not in the previous snapshot.
|
||||
func countNewFromPrevious(ctx context.Context, dbConn *sqlx.DB, prevTable string, vcenter string, current map[string]InventorySnapshotRow) int {
|
||||
if err := db.ValidateTableName(prevTable); err != nil {
|
||||
return len(current)
|
||||
}
|
||||
query := fmt.Sprintf(`SELECT "VmId","VmUuid","Name" FROM %s WHERE "Vcenter" = ?`, prevTable)
|
||||
query = sqlx.Rebind(sqlx.BindType(dbConn.DriverName()), query)
|
||||
|
||||
rows, err := dbConn.QueryxContext(ctx, query, vcenter)
|
||||
if err != nil {
|
||||
return len(current)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
prevIDs := make(map[string]struct{})
|
||||
prevUUIDs := make(map[string]struct{})
|
||||
prevNames := make(map[string]struct{})
|
||||
for rows.Next() {
|
||||
var vmID, vmUUID, name string
|
||||
if scanErr := rows.Scan(&vmID, &vmUUID, &name); scanErr != nil {
|
||||
continue
|
||||
}
|
||||
if vmID != "" {
|
||||
prevIDs[vmID] = struct{}{}
|
||||
}
|
||||
if vmUUID != "" {
|
||||
prevUUIDs[vmUUID] = struct{}{}
|
||||
}
|
||||
if name != "" {
|
||||
prevNames[name] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
newCount := 0
|
||||
for _, cur := range current {
|
||||
id := cur.VmId.String
|
||||
uuid := cur.VmUuid.String
|
||||
name := cur.Name
|
||||
if id != "" {
|
||||
if _, ok := prevIDs[id]; ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if uuid != "" {
|
||||
if _, ok := prevUUIDs[uuid]; ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if name != "" {
|
||||
if _, ok := prevNames[name]; ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
newCount++
|
||||
}
|
||||
return newCount
|
||||
}
|
||||
|
||||
// listNewFromPrevious returns the rows present now but not in the previous snapshot.
|
||||
func listNewFromPrevious(ctx context.Context, dbConn *sqlx.DB, prevTable string, vcenter string, current map[string]InventorySnapshotRow) []InventorySnapshotRow {
|
||||
if err := db.ValidateTableName(prevTable); err != nil {
|
||||
all := make([]InventorySnapshotRow, 0, len(current))
|
||||
for _, cur := range current {
|
||||
all = append(all, cur)
|
||||
}
|
||||
return all
|
||||
}
|
||||
query := fmt.Sprintf(`SELECT "VmId","VmUuid","Name" FROM %s WHERE "Vcenter" = ?`, prevTable)
|
||||
query = sqlx.Rebind(sqlx.BindType(dbConn.DriverName()), query)
|
||||
|
||||
rows, err := dbConn.QueryxContext(ctx, query, vcenter)
|
||||
if err != nil {
|
||||
all := make([]InventorySnapshotRow, 0, len(current))
|
||||
for _, cur := range current {
|
||||
all = append(all, cur)
|
||||
}
|
||||
return all
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
prevIDs := make(map[string]struct{})
|
||||
prevUUIDs := make(map[string]struct{})
|
||||
prevNames := make(map[string]struct{})
|
||||
for rows.Next() {
|
||||
var vmID, vmUUID, name string
|
||||
if scanErr := rows.Scan(&vmID, &vmUUID, &name); scanErr != nil {
|
||||
continue
|
||||
}
|
||||
if vmID != "" {
|
||||
prevIDs[vmID] = struct{}{}
|
||||
}
|
||||
if vmUUID != "" {
|
||||
prevUUIDs[vmUUID] = struct{}{}
|
||||
}
|
||||
if name != "" {
|
||||
prevNames[name] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
newRows := make([]InventorySnapshotRow, 0)
|
||||
for _, cur := range current {
|
||||
id := cur.VmId.String
|
||||
uuid := cur.VmUuid.String
|
||||
name := cur.Name
|
||||
if id != "" {
|
||||
if _, ok := prevIDs[id]; ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if uuid != "" {
|
||||
if _, ok := prevUUIDs[uuid]; ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if name != "" {
|
||||
if _, ok := prevNames[name]; ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
newRows = append(newRows, cur)
|
||||
}
|
||||
return newRows
|
||||
}
|
||||
|
||||
// findVMInHourlySnapshots searches recent hourly snapshot tables for a VM by ID for the given vCenter.
|
||||
// extraTables are searched first (e.g., known previous snapshot tables).
|
||||
func findVMInHourlySnapshots(ctx context.Context, dbConn *sqlx.DB, vcenter string, vmID string, extraTables ...string) (InventorySnapshotRow, bool) {
|
||||
if vmID == "" {
|
||||
return InventorySnapshotRow{}, false
|
||||
}
|
||||
// Use a short timeout to avoid hanging if the DB is busy.
|
||||
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// First search any explicit tables provided.
|
||||
for _, table := range extraTables {
|
||||
if table == "" {
|
||||
continue
|
||||
}
|
||||
if err := db.ValidateTableName(table); err != nil {
|
||||
continue
|
||||
}
|
||||
query := fmt.Sprintf(`SELECT "VmId","VmUuid","Name","Datacenter","Cluster" FROM %s WHERE "Vcenter" = ? AND "VmId" = ? LIMIT 1`, table)
|
||||
query = sqlx.Rebind(sqlx.BindType(dbConn.DriverName()), query)
|
||||
var row InventorySnapshotRow
|
||||
if err := dbConn.QueryRowxContext(ctx, query, vcenter, vmID).Scan(&row.VmId, &row.VmUuid, &row.Name, &row.Datacenter, &row.Cluster); err == nil {
|
||||
return row, true
|
||||
}
|
||||
}
|
||||
|
||||
// Try a handful of most recent hourly tables from the registry.
|
||||
rows, err := dbConn.QueryxContext(ctx, `
|
||||
SELECT table_name
|
||||
FROM snapshot_registry
|
||||
WHERE snapshot_type = 'hourly'
|
||||
ORDER BY snapshot_time DESC
|
||||
LIMIT 20
|
||||
`)
|
||||
if err != nil {
|
||||
return InventorySnapshotRow{}, false
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
checked := 0
|
||||
for rows.Next() {
|
||||
var table string
|
||||
if scanErr := rows.Scan(&table); scanErr != nil {
|
||||
continue
|
||||
}
|
||||
if err := db.ValidateTableName(table); err != nil {
|
||||
continue
|
||||
}
|
||||
query := fmt.Sprintf(`SELECT "VmId","VmUuid","Name","Datacenter","Cluster" FROM %s WHERE "Vcenter" = ? AND "VmId" = ? LIMIT 1`, table)
|
||||
query = sqlx.Rebind(sqlx.BindType(dbConn.DriverName()), query)
|
||||
var row InventorySnapshotRow
|
||||
if err := dbConn.QueryRowxContext(ctx, query, vcenter, vmID).Scan(&row.VmId, &row.VmUuid, &row.Name, &row.Datacenter, &row.Cluster); err == nil {
|
||||
return row, true
|
||||
}
|
||||
checked++
|
||||
if checked >= 10 { // limit work
|
||||
break
|
||||
}
|
||||
}
|
||||
return InventorySnapshotRow{}, false
|
||||
}
|
||||
Reference in New Issue
Block a user