Add vCenter cache rebuild functionality and related API endpoint
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2026-02-16 08:46:38 +11:00
parent 6fbd6bc9d2
commit bc84931c37
7 changed files with 425 additions and 24 deletions

View File

@@ -749,6 +749,12 @@ func CreateTableReport(logger *slog.Logger, Database db.Database, ctx context.Co
}
if isDailySummary || isMonthlySummary {
reportHeaders := make([]string, 0, len(specs))
for _, spec := range specs {
reportHeaders = append(reportHeaders, spec.Name)
}
addSummaryPivotSheet(logger, xlsx, sheetName, reportHeaders, rowCount, tableName)
meta := reportMetadata{
TableName: tableName,
ReportType: reportTypeFromTable(tableName),
@@ -883,6 +889,144 @@ func addTotalsChartSheet(logger *slog.Logger, database db.Database, ctx context.
}
}
type summaryPivotSpec struct {
Title string
TitleCell string
PivotName string
PivotRange string
RowFields []string
DataField string
DataName string
DataSummary string
}
func addSummaryPivotSheet(logger *slog.Logger, xlsx *excelize.File, dataSheet string, headers []string, rowCount int, tableName string) {
if logger == nil {
logger = slog.Default()
}
const summarySheet = "Summary"
if _, err := xlsx.NewSheet(summarySheet); err != nil {
logger.Warn("failed to create summary worksheet", "table", tableName, "error", err)
return
}
if rowCount <= 0 {
xlsx.SetCellValue(summarySheet, "A1", "Summary")
xlsx.SetCellValue(summarySheet, "A3", "No data rows were available to build pivot tables.")
if err := SetColAutoWidth(xlsx, summarySheet); err != nil {
logger.Warn("failed to size summary worksheet columns", "table", tableName, "error", err)
}
return
}
if len(headers) == 0 {
logger.Warn("summary worksheet skipped due to empty headers", "table", tableName)
return
}
endCell, err := excelize.CoordinatesToCellName(len(headers), rowCount+1)
if err != nil {
logger.Warn("summary worksheet skipped due to invalid data range", "table", tableName, "error", err)
return
}
dataRange := fmt.Sprintf("%s!A1:%s", quoteSheetName(dataSheet), endCell)
lowerToHeader := make(map[string]string, len(headers))
for _, header := range headers {
lowerToHeader[strings.ToLower(strings.TrimSpace(header))] = header
}
resolveField := func(name string) (string, bool) {
header, ok := lowerToHeader[strings.ToLower(strings.TrimSpace(name))]
return header, ok
}
specs := []summaryPivotSpec{
{
Title: "Sum of Avg vCPUs",
TitleCell: "A1",
PivotName: "PivotAvgVcpu",
PivotRange: fmt.Sprintf("%s!A3:H1000", quoteSheetName(summarySheet)),
RowFields: []string{"Datacenter", "ResourcePool"},
DataField: "AvgVcpuCount",
DataName: "Sum of Avg vCPUs",
DataSummary: "Sum",
},
{
Title: "Sum of Avg RAM",
TitleCell: "J1",
PivotName: "PivotAvgRam",
PivotRange: fmt.Sprintf("%s!J3:P1000", quoteSheetName(summarySheet)),
RowFields: []string{"Datacenter"},
DataField: "AvgRamGB",
DataName: "Sum of Avg RAM",
DataSummary: "Sum",
},
{
Title: "Sum of prorated VM count",
TitleCell: "A1003",
PivotName: "PivotProratedVmCount",
PivotRange: fmt.Sprintf("%s!A1005:H2002", quoteSheetName(summarySheet)),
RowFields: []string{"Datacenter"},
DataField: "AvgIsPresent",
DataName: "Sum of prorated VM count",
DataSummary: "Sum",
},
{
Title: "Count of VM Name",
TitleCell: "J1003",
PivotName: "PivotVmNameCount",
PivotRange: fmt.Sprintf("%s!J1005:P2002", quoteSheetName(summarySheet)),
RowFields: []string{"Datacenter"},
DataField: "Name",
DataName: "Count of VM Name",
DataSummary: "Count",
},
}
for _, spec := range specs {
xlsx.SetCellValue(summarySheet, spec.TitleCell, spec.Title)
rows := make([]excelize.PivotTableField, 0, len(spec.RowFields))
missingField := false
for _, rowField := range spec.RowFields {
resolved, ok := resolveField(rowField)
if !ok {
logger.Warn("summary pivot skipped: missing row field", "table", tableName, "pivot", spec.PivotName, "field", rowField)
missingField = true
break
}
rows = append(rows, excelize.PivotTableField{Data: resolved})
}
if missingField {
continue
}
dataField, ok := resolveField(spec.DataField)
if !ok {
logger.Warn("summary pivot skipped: missing data field", "table", tableName, "pivot", spec.PivotName, "field", spec.DataField)
continue
}
if err := xlsx.AddPivotTable(&excelize.PivotTableOptions{
Name: spec.PivotName,
DataRange: dataRange,
PivotTableRange: spec.PivotRange,
Rows: rows,
Data: []excelize.PivotTableField{{Data: dataField, Name: spec.DataName, Subtotal: spec.DataSummary}},
RowGrandTotals: true,
ColGrandTotals: true,
ShowDrill: true,
ShowRowHeaders: true,
ShowColHeaders: true,
ShowLastColumn: true,
PivotTableStyleName: "PivotStyleLight16",
}); err != nil {
logger.Warn("failed to add summary pivot table", "table", tableName, "pivot", spec.PivotName, "error", err)
}
}
if err := SetColAutoWidth(xlsx, summarySheet); err != nil {
logger.Warn("failed to size summary worksheet columns", "table", tableName, "error", err)
}
}
func tableColumns(ctx context.Context, dbConn *sqlx.DB, tableName string) ([]string, error) {
driver := strings.ToLower(dbConn.DriverName())
switch driver {
@@ -1464,29 +1608,29 @@ FROM diag, agg_diag
`, vmKeyExpr, overlapExpr, selected.TableName, templateExclusionFilter(), vmKeyExpr, prevTableName, templateExclusionFilter(), missingOverlapExpr, aggOverlapExpr)
query = dbConn.Rebind(query)
var row struct {
VmCount int64 `db:"vm_count"`
VcpuTotal int64 `db:"vcpu_total"`
RamTotal int64 `db:"ram_total"`
PresenceRatio float64 `db:"presence_ratio"`
TinTotal float64 `db:"tin_total"`
BronzeTotal float64 `db:"bronze_total"`
SilverTotal float64 `db:"silver_total"`
GoldTotal float64 `db:"gold_total"`
RowCount int64 `db:"row_count"`
DistinctKeys int64 `db:"distinct_keys"`
UnknownKeys int64 `db:"unknown_keys"`
MissingVmID int64 `db:"missing_vm_id"`
MissingVmUUID int64 `db:"missing_vm_uuid"`
MissingName int64 `db:"missing_name"`
PresenceOverOne int64 `db:"presence_over_one"`
PresenceUnderZero int64 `db:"presence_under_zero"`
BasePresenceSum float64 `db:"base_presence_sum"`
AggCount int64 `db:"agg_count"`
MissingCreation int64 `db:"missing_creation"`
MissingDeletion int64 `db:"missing_deletion"`
CreatedInInterval int64 `db:"created_in_interval"`
DeletedInInterval int64 `db:"deleted_in_interval"`
PartialPresence int64 `db:"partial_presence"`
VmCount int64 `db:"vm_count"`
VcpuTotal int64 `db:"vcpu_total"`
RamTotal int64 `db:"ram_total"`
PresenceRatio float64 `db:"presence_ratio"`
TinTotal float64 `db:"tin_total"`
BronzeTotal float64 `db:"bronze_total"`
SilverTotal float64 `db:"silver_total"`
GoldTotal float64 `db:"gold_total"`
RowCount int64 `db:"row_count"`
DistinctKeys int64 `db:"distinct_keys"`
UnknownKeys int64 `db:"unknown_keys"`
MissingVmID int64 `db:"missing_vm_id"`
MissingVmUUID int64 `db:"missing_vm_uuid"`
MissingName int64 `db:"missing_name"`
PresenceOverOne int64 `db:"presence_over_one"`
PresenceUnderZero int64 `db:"presence_under_zero"`
BasePresenceSum float64 `db:"base_presence_sum"`
AggCount int64 `db:"agg_count"`
MissingCreation int64 `db:"missing_creation"`
MissingDeletion int64 `db:"missing_deletion"`
CreatedInInterval int64 `db:"created_in_interval"`
DeletedInInterval int64 `db:"deleted_in_interval"`
PartialPresence int64 `db:"partial_presence"`
}
overlapArgs := []interface{}{
hourEndUnix, hourEndUnix,

View File

@@ -874,7 +874,7 @@ func (v *Vcenter) BuildResourcePoolLookup() (map[string]string, error) {
// Helper function to retrieve the full folder path for the VM
func (v *Vcenter) GetVMFolderPath(vm mo.VirtualMachine) (string, error) {
v.Logger.Debug("Commencing vm folder path search")
v.Logger.Debug("commencing vm folder path search", "vcenter", v.Vurl, "vm_id", vm.Reference().Value)
entities, err := mo.Ancestors(v.ctx, v.client.Client, v.client.ServiceContent.PropertyCollector, vm.Reference())
if err != nil {