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

@@ -0,0 +1,162 @@
package handler
import (
"context"
"fmt"
"math"
"net/http"
"strings"
"time"
"vctp/db"
"vctp/internal/vcenter"
"vctp/server/models"
)
// VcenterCacheRebuild force-regenerates cached vCenter reference data in the database.
// @Summary Rebuild vCenter object cache
// @Description Rebuilds cached folder/resource-pool/host(cluster+datacenter) references from vCenter and rewrites the database cache tables.
// @Tags vcenters
// @Produce json
// @Param vcenter query string false "Optional single vCenter URL to rebuild; defaults to all configured vCenters"
// @Success 200 {object} models.VcenterCacheRebuildResponse "Cache rebuild summary"
// @Failure 400 {object} models.ErrorResponse "Invalid request"
// @Failure 405 {object} models.ErrorResponse "Method not allowed"
// @Failure 500 {object} models.VcenterCacheRebuildResponse "All rebuild attempts failed"
// @Router /api/vcenters/cache/rebuild [post]
func (h *Handler) VcenterCacheRebuild(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeJSONError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
ctx := r.Context()
h.Settings.ReadYMLSettings()
requested := strings.TrimSpace(r.URL.Query().Get("vcenter"))
configured := h.Settings.Values.Settings.VcenterAddresses
targets := make([]string, 0, len(configured))
for _, raw := range configured {
vcURL := strings.TrimSpace(raw)
if vcURL == "" {
continue
}
if requested != "" && vcURL != requested {
continue
}
targets = append(targets, vcURL)
}
if requested != "" && len(targets) == 0 {
writeJSONError(w, http.StatusBadRequest, fmt.Sprintf("requested vcenter is not configured: %s", requested))
return
}
if len(targets) == 0 {
writeJSONError(w, http.StatusBadRequest, "no vcenter addresses configured")
return
}
if err := db.EnsureVcenterReferenceCacheTables(ctx, h.Database.DB()); err != nil {
h.Logger.Error("failed to ensure vcenter cache tables", "error", err)
writeJSONError(w, http.StatusInternalServerError, "failed to ensure vcenter cache tables")
return
}
resp := models.VcenterCacheRebuildResponse{
Status: "OK",
Total: len(targets),
Results: make([]models.VcenterCacheRebuildResult, 0, len(targets)),
}
for _, vcURL := range targets {
start := time.Now()
result := models.VcenterCacheRebuildResult{
Vcenter: vcURL,
}
folderEntries, poolEntries, hostEntries, err := h.rebuildOneVcenterCache(ctx, vcURL)
result.DurationSeconds = math.Round(time.Since(start).Seconds()*1000) / 1000
if err != nil {
result.Error = err.Error()
resp.Failed++
h.Logger.Warn("vcenter cache rebuild failed", "vcenter", vcURL, "error", err)
} else {
result.FolderEntries = folderEntries
result.ResourcePoolEntries = poolEntries
result.HostEntries = hostEntries
resp.Succeeded++
h.Logger.Info("vcenter cache rebuild completed", "vcenter", vcURL, "folder_entries", folderEntries, "resource_pool_entries", poolEntries, "host_entries", hostEntries, "duration", time.Since(start))
}
resp.Results = append(resp.Results, result)
}
switch {
case resp.Failed == 0:
resp.Status = "OK"
writeJSON(w, http.StatusOK, resp)
case resp.Succeeded == 0:
resp.Status = "ERROR"
writeJSON(w, http.StatusInternalServerError, resp)
default:
resp.Status = "PARTIAL"
writeJSON(w, http.StatusOK, resp)
}
}
func (h *Handler) rebuildOneVcenterCache(ctx context.Context, vcURL string) (int, int, int, error) {
vc := vcenter.New(h.Logger, h.VcCreds)
if err := vc.Login(vcURL); err != nil {
return 0, 0, 0, fmt.Errorf("unable to connect to vcenter: %w", err)
}
defer func() {
logoutCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := vc.Logout(logoutCtx); err != nil {
h.Logger.Warn("vcenter cache rebuild logout failed", "vcenter", vcURL, "error", err)
}
}()
folderLookup, err := vc.BuildFolderPathLookup()
if err != nil {
return 0, 0, 0, fmt.Errorf("failed to build folder cache from vcenter: %w", err)
}
resourcePools, err := vc.BuildResourcePoolLookup()
if err != nil {
return 0, 0, 0, fmt.Errorf("failed to build resource pool cache from vcenter: %w", err)
}
hostLookup, err := vc.BuildHostLookup()
if err != nil {
return 0, 0, 0, fmt.Errorf("failed to build host cache from vcenter: %w", err)
}
dbConn := h.Database.DB()
if err := db.ClearVcenterReferenceCache(ctx, dbConn, vcURL); err != nil {
return 0, 0, 0, fmt.Errorf("failed to clear existing vcenter cache rows: %w", err)
}
lastSeen := time.Now().Unix()
folderCount := 0
for folderRef, folderPath := range folderLookup {
if err := db.UpsertVcenterFolderCache(ctx, dbConn, vcURL, folderRef, folderPath, lastSeen); err != nil {
return 0, 0, 0, fmt.Errorf("failed to persist folder cache: %w", err)
}
folderCount++
}
poolCount := 0
for poolRef, poolName := range resourcePools {
if err := db.UpsertVcenterResourcePoolCache(ctx, dbConn, vcURL, poolRef, poolName, lastSeen); err != nil {
return 0, 0, 0, fmt.Errorf("failed to persist resource pool cache: %w", err)
}
poolCount++
}
hostCount := 0
for hostRef, entry := range hostLookup {
if err := db.UpsertVcenterHostCache(ctx, dbConn, vcURL, hostRef, entry.Cluster, entry.Datacenter, lastSeen); err != nil {
return 0, 0, 0, fmt.Errorf("failed to persist host cache: %w", err)
}
hostCount++
}
return folderCount, poolCount, hostCount, nil
}

View File

@@ -59,3 +59,22 @@ type SnapshotRepairSuiteResponse struct {
MonthlyRefined string `json:"monthly_refined"`
MonthlyFailed string `json:"monthly_failed"`
}
// VcenterCacheRebuildResult describes rebuild results for a single vCenter.
type VcenterCacheRebuildResult struct {
Vcenter string `json:"vcenter"`
FolderEntries int `json:"folder_entries"`
ResourcePoolEntries int `json:"resource_pool_entries"`
HostEntries int `json:"host_entries"`
DurationSeconds float64 `json:"duration_seconds"`
Error string `json:"error,omitempty"`
}
// VcenterCacheRebuildResponse describes the vCenter object cache rebuild response.
type VcenterCacheRebuildResponse struct {
Status string `json:"status"`
Total int `json:"total"`
Succeeded int `json:"succeeded"`
Failed int `json:"failed"`
Results []VcenterCacheRebuildResult `json:"results"`
}

View File

@@ -74,6 +74,7 @@ func New(logger *slog.Logger, database db.Database, buildTime string, sha1ver st
mux.HandleFunc("/vcenters/totals", h.VcenterTotals)
mux.HandleFunc("/vcenters/totals/daily", h.VcenterTotalsDaily)
mux.HandleFunc("/vcenters/totals/hourly", h.VcenterTotalsHourlyDetailed)
mux.HandleFunc("/api/vcenters/cache/rebuild", h.VcenterCacheRebuild)
mux.HandleFunc("/metrics", h.Metrics)
mux.HandleFunc("/snapshots/hourly", h.SnapshotHourlyList)