Refactor code to use 'any' type and improve context handling
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2026-02-18 16:16:27 +11:00
parent 6517a30fa2
commit f2d6b3158b
36 changed files with 197 additions and 175 deletions

View File

@@ -168,7 +168,7 @@ func looksLikePostgresDSN(dsn string) bool {
} }
// ConvertToSQLParams is a utility function that generically converts a struct to a corresponding sqlc-generated struct // ConvertToSQLParams is a utility function that generically converts a struct to a corresponding sqlc-generated struct
func ConvertToSQLParams(input interface{}, output interface{}) { func ConvertToSQLParams(input any, output any) {
inputVal := reflect.ValueOf(input).Elem() inputVal := reflect.ValueOf(input).Elem()
outputVal := reflect.ValueOf(output).Elem() outputVal := reflect.ValueOf(output).Elem()
@@ -182,15 +182,15 @@ func ConvertToSQLParams(input interface{}, output interface{}) {
// Handle fields of type sql.NullString, sql.NullInt64, and normal string/int64 fields // Handle fields of type sql.NullString, sql.NullInt64, and normal string/int64 fields
switch outputField.Type() { switch outputField.Type() {
case reflect.TypeOf(sql.NullString{}): case reflect.TypeFor[sql.NullString]():
// Handle sql.NullString // Handle sql.NullString
if inputField.Kind() == reflect.Ptr && inputField.IsNil() { if inputField.Kind() == reflect.Pointer && inputField.IsNil() {
outputField.Set(reflect.ValueOf(sql.NullString{Valid: false})) outputField.Set(reflect.ValueOf(sql.NullString{Valid: false}))
} else { } else {
outputField.Set(reflect.ValueOf(sql.NullString{String: inputField.String(), Valid: true})) outputField.Set(reflect.ValueOf(sql.NullString{String: inputField.String(), Valid: true}))
} }
case reflect.TypeOf(sql.NullInt64{}): case reflect.TypeFor[sql.NullInt64]():
// Handle sql.NullInt64 // Handle sql.NullInt64
if inputField.Int() == 0 { if inputField.Int() == 0 {
outputField.Set(reflect.ValueOf(sql.NullInt64{Valid: false})) outputField.Set(reflect.ValueOf(sql.NullInt64{Valid: false}))
@@ -198,7 +198,7 @@ func ConvertToSQLParams(input interface{}, output interface{}) {
outputField.Set(reflect.ValueOf(sql.NullInt64{Int64: inputField.Int(), Valid: true})) outputField.Set(reflect.ValueOf(sql.NullInt64{Int64: inputField.Int(), Valid: true}))
} }
case reflect.TypeOf(sql.NullFloat64{}): case reflect.TypeFor[sql.NullFloat64]():
// Handle sql.NullFloat64 // Handle sql.NullFloat64
if inputField.Float() == 0 { if inputField.Float() == 0 {
outputField.Set(reflect.ValueOf(sql.NullFloat64{Valid: false})) outputField.Set(reflect.ValueOf(sql.NullFloat64{Valid: false}))
@@ -206,19 +206,19 @@ func ConvertToSQLParams(input interface{}, output interface{}) {
outputField.Set(reflect.ValueOf(sql.NullFloat64{Float64: inputField.Float(), Valid: true})) outputField.Set(reflect.ValueOf(sql.NullFloat64{Float64: inputField.Float(), Valid: true}))
} }
case reflect.TypeOf(""): case reflect.TypeFor[string]():
// Handle normal string fields // Handle normal string fields
if inputField.Kind() == reflect.Ptr && inputField.IsNil() { if inputField.Kind() == reflect.Pointer && inputField.IsNil() {
outputField.SetString("") // Set to empty string if input is nil outputField.SetString("") // Set to empty string if input is nil
} else { } else {
outputField.SetString(inputField.String()) outputField.SetString(inputField.String())
} }
case reflect.TypeOf(int64(0)): case reflect.TypeFor[int64]():
// Handle normal int64 fields // Handle normal int64 fields
outputField.SetInt(inputField.Int()) outputField.SetInt(inputField.Int())
case reflect.TypeOf(float64(0)): case reflect.TypeFor[float64]():
// Handle normal float64 fields // Handle normal float64 fields
outputField.SetFloat(inputField.Float()) outputField.SetFloat(inputField.Float())

View File

@@ -45,8 +45,32 @@ type ensureOnceState struct {
done bool done bool
} }
type loggerContextKey struct{}
var ensureOnceRegistry sync.Map var ensureOnceRegistry sync.Map
// WithLoggerContext stores a logger in context for downstream DB helper logging.
func WithLoggerContext(ctx context.Context, logger *slog.Logger) context.Context {
if ctx == nil {
ctx = context.Background()
}
if logger == nil {
return ctx
}
return context.WithValue(ctx, loggerContextKey{}, logger)
}
// LoggerFromContext returns a logger previously stored via WithLoggerContext.
func LoggerFromContext(ctx context.Context) *slog.Logger {
if ctx == nil {
return nil
}
if logger, ok := ctx.Value(loggerContextKey{}).(*slog.Logger); ok && logger != nil {
return logger
}
return nil
}
// ensureOncePerDB runs fn once per DB connection for a given logical key. // ensureOncePerDB runs fn once per DB connection for a given logical key.
// The function is considered complete only when fn returns nil. // The function is considered complete only when fn returns nil.
func ensureOncePerDB(dbConn *sqlx.DB, name string, fn func() error) error { func ensureOncePerDB(dbConn *sqlx.DB, name string, fn func() error) error {
@@ -98,7 +122,7 @@ func EnsureColumns(ctx context.Context, dbConn *sqlx.DB, tableName string, colum
return nil return nil
} }
func execLog(ctx context.Context, dbConn *sqlx.DB, query string, args ...interface{}) (sql.Result, error) { func execLog(ctx context.Context, dbConn *sqlx.DB, query string, args ...any) (sql.Result, error) {
res, err := dbConn.ExecContext(ctx, query, args...) res, err := dbConn.ExecContext(ctx, query, args...)
if err != nil { if err != nil {
q := strings.TrimSpace(query) q := strings.TrimSpace(query)
@@ -120,7 +144,7 @@ func execLog(ctx context.Context, dbConn *sqlx.DB, query string, args ...interfa
return res, err return res, err
} }
func getLog(ctx context.Context, dbConn *sqlx.DB, dest interface{}, query string, args ...interface{}) error { func getLog(ctx context.Context, dbConn *sqlx.DB, dest any, query string, args ...any) error {
err := dbConn.GetContext(ctx, dest, query, args...) err := dbConn.GetContext(ctx, dest, query, args...)
if err != nil { if err != nil {
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
@@ -137,7 +161,7 @@ func getLog(ctx context.Context, dbConn *sqlx.DB, dest interface{}, query string
return err return err
} }
func selectLog(ctx context.Context, dbConn *sqlx.DB, dest interface{}, query string, args ...interface{}) error { func selectLog(ctx context.Context, dbConn *sqlx.DB, dest any, query string, args ...any) error {
err := dbConn.SelectContext(ctx, dest, query, args...) err := dbConn.SelectContext(ctx, dest, query, args...)
if err != nil { if err != nil {
if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) { if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) {
@@ -602,7 +626,7 @@ func ApplySQLiteTuning(ctx context.Context, dbConn *sqlx.DB) {
pragmaCtx, cancel := context.WithTimeout(ctx, 2*time.Second) pragmaCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
_, err := execLog(pragmaCtx, dbConn, pragma) _, err := execLog(pragmaCtx, dbConn, pragma)
cancel() cancel()
if logger, ok := ctx.Value("logger").(*slog.Logger); ok && logger != nil { if logger := LoggerFromContext(ctx); logger != nil {
logger.Debug("Applied SQLite tuning pragma", "pragma", pragma, "error", err) logger.Debug("Applied SQLite tuning pragma", "pragma", pragma, "error", err)
} }
} }
@@ -783,7 +807,7 @@ ON CONFLICT ("Vcenter","VmId","VmUuid") DO UPDATE SET
"DeletedAt"=NULL "DeletedAt"=NULL
` `
query = sqlx.Rebind(bindType, query) query = sqlx.Rebind(bindType, query)
args := []interface{}{vcenter, vmID, vmUUID, name, cluster, firstSeen, seen.Unix()} args := []any{vcenter, vmID, vmUUID, name, cluster, firstSeen, seen.Unix()}
_, err := dbConn.ExecContext(ctx, query, args...) _, err := dbConn.ExecContext(ctx, query, args...)
if err != nil { if err != nil {
slog.Warn("lifecycle upsert exec failed", "vcenter", vcenter, "vm_id", vmID, "vm_uuid", vmUUID, "driver", driver, "args_len", len(args), "args", fmt.Sprint(args), "query", strings.TrimSpace(query), "error", err) slog.Warn("lifecycle upsert exec failed", "vcenter", vcenter, "vm_id", vmID, "vm_uuid", vmUUID, "driver", driver, "args_len", len(args), "args", fmt.Sprint(args), "query", strings.TrimSpace(query), "error", err)
@@ -814,7 +838,7 @@ ON CONFLICT ("Vcenter","VmId","VmUuid") DO UPDATE SET
"Cluster"=COALESCE(NULLIF(vm_lifecycle_cache."Cluster", ''), EXCLUDED."Cluster") "Cluster"=COALESCE(NULLIF(vm_lifecycle_cache."Cluster", ''), EXCLUDED."Cluster")
` `
query = sqlx.Rebind(bindType, query) query = sqlx.Rebind(bindType, query)
args := []interface{}{vcenter, vmID, vmUUID, name, cluster, deletedAt, deletedAt, deletedAt} args := []any{vcenter, vmID, vmUUID, name, cluster, deletedAt, deletedAt, deletedAt}
_, err := dbConn.ExecContext(ctx, query, args...) _, err := dbConn.ExecContext(ctx, query, args...)
if err != nil { if err != nil {
slog.Warn("lifecycle delete exec failed", "vcenter", vcenter, "vm_id", vmID, "vm_uuid", vmUUID, "driver", driver, "args_len", len(args), "args", fmt.Sprint(args), "query", strings.TrimSpace(query), "error", err) slog.Warn("lifecycle delete exec failed", "vcenter", vcenter, "vm_id", vmID, "vm_uuid", vmUUID, "driver", driver, "args_len", len(args), "args", fmt.Sprint(args), "query", strings.TrimSpace(query), "error", err)
@@ -845,7 +869,7 @@ ON CONFLICT ("Vcenter","VmId","VmUuid") DO UPDATE SET
"Cluster"=COALESCE(NULLIF(vm_lifecycle_cache."Cluster", ''), EXCLUDED."Cluster") "Cluster"=COALESCE(NULLIF(vm_lifecycle_cache."Cluster", ''), EXCLUDED."Cluster")
` `
query = sqlx.Rebind(bindType, query) query = sqlx.Rebind(bindType, query)
args := []interface{}{vcenter, vmID, vmUUID, name, cluster, deletedAt, deletedAt, deletedAt} args := []any{vcenter, vmID, vmUUID, name, cluster, deletedAt, deletedAt, deletedAt}
_, err := dbConn.ExecContext(ctx, query, args...) _, err := dbConn.ExecContext(ctx, query, args...)
if err != nil { if err != nil {
slog.Warn("lifecycle delete event exec failed", "vcenter", vcenter, "vm_id", vmID, "vm_uuid", vmUUID, "driver", driver, "args_len", len(args), "args", fmt.Sprint(args), "query", strings.TrimSpace(query), "error", err) slog.Warn("lifecycle delete event exec failed", "vcenter", vcenter, "vm_id", vmID, "vm_uuid", vmUUID, "driver", driver, "args_len", len(args), "args", fmt.Sprint(args), "query", strings.TrimSpace(query), "error", err)
@@ -988,7 +1012,7 @@ ON CONFLICT ("Date","Vcenter","VmId","VmUuid") DO UPDATE SET
"PoweredOn"=$25, "PoweredOn"=$25,
"SrmPlaceholder"=$26 "SrmPlaceholder"=$26
` `
args := []interface{}{ args := []any{
day, v.Vcenter, v.VmId, v.VmUuid, v.Name, v.CreationTime, v.DeletionTime, v.SamplesPresent, v.TotalSamples, day, v.Vcenter, v.VmId, v.VmUuid, v.Name, v.CreationTime, v.DeletionTime, v.SamplesPresent, v.TotalSamples,
v.SumVcpu, v.SumRam, v.SumDisk, v.TinHits, v.BronzeHits, v.SilverHits, v.GoldHits, v.SumVcpu, v.SumRam, v.SumDisk, v.TinHits, v.BronzeHits, v.SilverHits, v.GoldHits,
v.LastResourcePool, v.LastDatacenter, v.LastCluster, v.LastFolder, v.LastProvisionedDisk, v.LastVcpuCount, v.LastRamGB, v.IsTemplate, v.PoweredOn, v.SrmPlaceholder, v.LastResourcePool, v.LastDatacenter, v.LastCluster, v.LastFolder, v.LastProvisionedDisk, v.LastVcpuCount, v.LastRamGB, v.IsTemplate, v.PoweredOn, v.SrmPlaceholder,
@@ -1469,7 +1493,7 @@ WHERE "Vcenter" = $4 AND "VmId" = $5 AND "VmUuid" = $6
return err return err
} }
func nullString(val sql.NullString) interface{} { func nullString(val sql.NullString) any {
if val.Valid { if val.Valid {
return val.String return val.String
} }
@@ -2088,17 +2112,17 @@ type VmLifecycleDiagnostics struct {
FinalLifecycle VmLifecycle FinalLifecycle VmLifecycle
} }
func vmLookupPredicate(vmID, vmUUID, name string) (string, []interface{}, bool) { func vmLookupPredicate(vmID, vmUUID, name string) (string, []any, bool) {
vmID = strings.TrimSpace(vmID) vmID = strings.TrimSpace(vmID)
vmUUID = strings.TrimSpace(vmUUID) vmUUID = strings.TrimSpace(vmUUID)
name = strings.TrimSpace(name) name = strings.TrimSpace(name)
switch { switch {
case vmID != "": case vmID != "":
return `"VmId" = ?`, []interface{}{vmID}, true return `"VmId" = ?`, []any{vmID}, true
case vmUUID != "": case vmUUID != "":
return `"VmUuid" = ?`, []interface{}{vmUUID}, true return `"VmUuid" = ?`, []any{vmUUID}, true
case name != "": case name != "":
return `lower("Name") = ?`, []interface{}{strings.ToLower(name)}, true return `lower("Name") = ?`, []any{strings.ToLower(name)}, true
default: default:
return "", nil, false return "", nil, false
} }
@@ -2960,7 +2984,7 @@ SET "AvgIsPresent" = CASE
END END
`, summaryTable, endExpr, startExpr, endExpr, startExpr) `, summaryTable, endExpr, startExpr, endExpr, startExpr)
query = dbConn.Rebind(query) query = dbConn.Rebind(query)
args := []interface{}{ args := []any{
windowEnd, windowEnd, windowEnd, windowEnd,
windowStart, windowStart, windowStart, windowStart,
windowEnd, windowEnd, windowEnd, windowEnd,
@@ -3530,7 +3554,7 @@ FROM snapshot_runs
WHERE "Success" = 'FALSE' AND "Attempts" < ? WHERE "Success" = 'FALSE' AND "Attempts" < ?
ORDER BY "LastAttempt" ASC ORDER BY "LastAttempt" ASC
` `
args := []interface{}{maxAttempts} args := []any{maxAttempts}
if driver == "pgx" || driver == "postgres" { if driver == "pgx" || driver == "postgres" {
query = ` query = `
SELECT "Vcenter","SnapshotTime","Attempts" SELECT "Vcenter","SnapshotTime","Attempts"

View File

@@ -131,7 +131,7 @@ INSERT INTO vm_hourly_stats (
"Datacenter","Cluster","Folder","ProvisionedDisk","VcpuCount","RamGB","IsTemplate","PoweredOn","SrmPlaceholder" "Datacenter","Cluster","Folder","ProvisionedDisk","VcpuCount","RamGB","IsTemplate","PoweredOn","SrmPlaceholder"
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
` `
rows := [][]interface{}{ rows := [][]any{
{int64(1000), "vc-a", "vm-1", "uuid-1", "demo-vm", int64(900), int64(0), "Tin", "dc", "cluster", "folder", 100.0, int64(2), int64(4), "FALSE", "TRUE", "FALSE"}, {int64(1000), "vc-a", "vm-1", "uuid-1", "demo-vm", int64(900), int64(0), "Tin", "dc", "cluster", "folder", 100.0, int64(2), int64(4), "FALSE", "TRUE", "FALSE"},
{int64(2000), "vc-a", "vm-1", "uuid-1", "demo-vm", int64(900), int64(0), "Gold", "dc", "cluster", "folder", 150.0, int64(4), int64(8), "FALSE", "TRUE", "FALSE"}, {int64(2000), "vc-a", "vm-1", "uuid-1", "demo-vm", int64(900), int64(0), "Gold", "dc", "cluster", "folder", 150.0, int64(4), int64(8), "FALSE", "TRUE", "FALSE"},
} }
@@ -371,7 +371,7 @@ INSERT INTO vm_hourly_stats (
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
` `
// First row carries an old deletion marker, later row proves VM is still present. // First row carries an old deletion marker, later row proves VM is still present.
rows := [][]interface{}{ rows := [][]any{
{int64(1700000000), "vc-a", "vm-1", "uuid-1", "demo-vm", int64(1699999000), int64(1700003600), "Tin", "dc", "cluster", "folder", 100.0, int64(2), int64(4), "FALSE", "TRUE", "FALSE"}, {int64(1700000000), "vc-a", "vm-1", "uuid-1", "demo-vm", int64(1699999000), int64(1700003600), "Tin", "dc", "cluster", "folder", 100.0, int64(2), int64(4), "FALSE", "TRUE", "FALSE"},
{int64(1700100000), "vc-a", "vm-1", "uuid-1", "demo-vm", int64(1699999000), int64(0), "Gold", "dc", "cluster", "folder", 120.0, int64(4), int64(8), "FALSE", "TRUE", "FALSE"}, {int64(1700100000), "vc-a", "vm-1", "uuid-1", "demo-vm", int64(1699999000), int64(0), "Gold", "dc", "cluster", "folder", 120.0, int64(4), int64(8), "FALSE", "TRUE", "FALSE"},
} }
@@ -563,7 +563,7 @@ CREATE TABLE %s (
INSERT INTO %s ("Vcenter","Name","VmId","VmUuid","AvgVcpuCount","AvgRamGB") INSERT INTO %s ("Vcenter","Name","VmId","VmUuid","AvgVcpuCount","AvgRamGB")
VALUES (?,?,?,?,?,?) VALUES (?,?,?,?,?,?)
`, summaryTable) `, summaryTable)
rows := [][]interface{}{ rows := [][]any{
{"vc-a", "vm-1", "1", "u1", 2.0, 4.0}, {"vc-a", "vm-1", "1", "u1", 2.0, 4.0},
{"vc-a", "vm-2", "2", "u2", 3.0, 5.0}, {"vc-a", "vm-2", "2", "u2", 3.0, 5.0},
{"vc-b", "vm-3", "3", "u3", 1.0, 2.0}, {"vc-b", "vm-3", "3", "u3", 1.0, 2.0},
@@ -633,7 +633,7 @@ CREATE TABLE %s (
INSERT INTO %s ("Vcenter","Name","VmId","VmUuid","AvgVcpuCount","AvgRamGB") INSERT INTO %s ("Vcenter","Name","VmId","VmUuid","AvgVcpuCount","AvgRamGB")
VALUES (?,?,?,?,?,?) VALUES (?,?,?,?,?,?)
`, summaryTable) `, summaryTable)
for _, args := range [][]interface{}{ for _, args := range [][]any{
{"vc-a", "vm-1", "1", "u1", 4.0, 8.0}, {"vc-a", "vm-1", "1", "u1", 4.0, 8.0},
{"vc-a", "vm-2", "2", "u2", 2.0, 6.0}, {"vc-a", "vm-2", "2", "u2", 2.0, 6.0},
} { } {
@@ -697,7 +697,7 @@ CREATE TABLE %s (
} }
insert1 := fmt.Sprintf(`INSERT INTO %s ("Vcenter","Name","VmId","VmUuid","AvgVcpuCount","AvgRamGB") VALUES (?,?,?,?,?,?)`, table1) insert1 := fmt.Sprintf(`INSERT INTO %s ("Vcenter","Name","VmId","VmUuid","AvgVcpuCount","AvgRamGB") VALUES (?,?,?,?,?,?)`, table1)
insert2 := fmt.Sprintf(`INSERT INTO %s ("Vcenter","Name","VmId","VmUuid","AvgVcpuCount","AvgRamGB") VALUES (?,?,?,?,?,?)`, table2) insert2 := fmt.Sprintf(`INSERT INTO %s ("Vcenter","Name","VmId","VmUuid","AvgVcpuCount","AvgRamGB") VALUES (?,?,?,?,?,?)`, table2)
for _, args := range [][]interface{}{ for _, args := range [][]any{
{"vc-a", "vm-1", "1", "u1", 2.0, 4.0}, {"vc-a", "vm-1", "1", "u1", 2.0, 4.0},
{"vc-b", "vm-2", "2", "u2", 3.0, 5.0}, {"vc-b", "vm-2", "2", "u2", 3.0, 5.0},
} { } {

View File

@@ -55,7 +55,7 @@ type rebindDBTX struct {
db *sqlx.DB db *sqlx.DB
} }
func (r rebindDBTX) ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) { func (r rebindDBTX) ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) {
return r.db.ExecContext(ctx, rebindQuery(query), args...) return r.db.ExecContext(ctx, rebindQuery(query), args...)
} }
@@ -63,11 +63,11 @@ func (r rebindDBTX) PrepareContext(ctx context.Context, query string) (*sql.Stmt
return r.db.PrepareContext(ctx, rebindQuery(query)) return r.db.PrepareContext(ctx, rebindQuery(query))
} }
func (r rebindDBTX) QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) { func (r rebindDBTX) QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) {
return r.db.QueryContext(ctx, rebindQuery(query), args...) return r.db.QueryContext(ctx, rebindQuery(query), args...)
} }
func (r rebindDBTX) QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row { func (r rebindDBTX) QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row {
return r.db.QueryRowContext(ctx, rebindQuery(query), args...) return r.db.QueryRowContext(ctx, rebindQuery(query), args...)
} }

View File

@@ -237,15 +237,15 @@ func copySQLiteTableIntoPostgres(ctx context.Context, source *sqlx.DB, destinati
var rowsCopied int64 var rowsCopied int64
for rows.Next() { for rows.Next() {
rawValues := make([]interface{}, len(columns)) rawValues := make([]any, len(columns))
scanTargets := make([]interface{}, len(columns)) scanTargets := make([]any, len(columns))
for i := range rawValues { for i := range rawValues {
scanTargets[i] = &rawValues[i] scanTargets[i] = &rawValues[i]
} }
if err := rows.Scan(scanTargets...); err != nil { if err := rows.Scan(scanTargets...); err != nil {
return rowsCopied, fmt.Errorf("failed to scan row from sqlite table %q: %w", tableName, err) return rowsCopied, fmt.Errorf("failed to scan row from sqlite table %q: %w", tableName, err)
} }
args := make([]interface{}, len(columns)) args := make([]any, len(columns))
for i, col := range columns { for i, col := range columns {
args[i] = coerceSQLiteValueForPostgres(rawValues[i], col.DestinationType) args[i] = coerceSQLiteValueForPostgres(rawValues[i], col.DestinationType)
} }
@@ -366,7 +366,7 @@ ORDER BY ordinal_position
return nil return nil
} }
func coerceSQLiteValueForPostgres(value interface{}, destinationType string) interface{} { func coerceSQLiteValueForPostgres(value any, destinationType string) any {
if value == nil { if value == nil {
return nil return nil
} }
@@ -385,7 +385,7 @@ func coerceSQLiteValueForPostgres(value interface{}, destinationType string) int
return value return value
} }
func coerceBoolValue(value interface{}) (bool, bool) { func coerceBoolValue(value any) (bool, bool) {
switch v := value.(type) { switch v := value.(type) {
case bool: case bool:
return v, true return v, true

View File

@@ -51,9 +51,9 @@ func TestIntersectImportColumns(t *testing.T) {
func TestCoerceSQLiteValueForPostgresBoolean(t *testing.T) { func TestCoerceSQLiteValueForPostgresBoolean(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
input interface{} input any
destinationType string destinationType string
want interface{} want any
}{ }{
{name: "string true", input: "true", destinationType: "boolean", want: true}, {name: "string true", input: "true", destinationType: "boolean", want: true},
{name: "string false", input: "0", destinationType: "boolean", want: false}, {name: "string false", input: "0", destinationType: "boolean", want: false},

3
go.mod
View File

@@ -12,7 +12,7 @@ require (
github.com/swaggo/swag v1.16.6 github.com/swaggo/swag v1.16.6
github.com/vmware/govmomi v0.52.0 github.com/vmware/govmomi v0.52.0
github.com/xuri/excelize/v2 v2.10.0 github.com/xuri/excelize/v2 v2.10.0
gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1
modernc.org/sqlite v1.44.0 modernc.org/sqlite v1.44.0
) )
@@ -58,6 +58,7 @@ require (
golang.org/x/text v0.33.0 // indirect golang.org/x/text v0.33.0 // indirect
golang.org/x/tools v0.41.0 // indirect golang.org/x/tools v0.41.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
modernc.org/libc v1.67.4 // indirect modernc.org/libc v1.67.4 // indirect
modernc.org/mathutil v1.7.1 // indirect modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect modernc.org/memory v1.11.0 // indirect

View File

@@ -238,7 +238,7 @@ func CreateUpdatesReport(logger *slog.Logger, Database db.Database, ctx context.
} }
// Helper function to get the actual value of sql.Null types // Helper function to get the actual value of sql.Null types
func getFieldValue(field reflect.Value) interface{} { func getFieldValue(field reflect.Value) any {
switch field.Kind() { switch field.Kind() {
case reflect.Struct: case reflect.Struct:
// Handle sql.Null types based on their concrete type // Handle sql.Null types based on their concrete type

View File

@@ -9,6 +9,7 @@ import (
"math" "math"
"os" "os"
"path/filepath" "path/filepath"
"slices"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
@@ -844,8 +845,8 @@ func addTotalsChartSheet(logger *slog.Logger, database db.Database, ctx context.
if logger == nil { if logger == nil {
logger = slog.Default() logger = slog.Default()
} }
if strings.HasPrefix(tableName, "inventory_daily_summary_") { if after, ok := strings.CutPrefix(tableName, "inventory_daily_summary_"); ok {
suffix := strings.TrimPrefix(tableName, "inventory_daily_summary_") suffix := after
dayStart, err := time.ParseInLocation("20060102", suffix, time.Local) dayStart, err := time.ParseInLocation("20060102", suffix, time.Local)
if err != nil { if err != nil {
logger.Debug("hourly totals skip: invalid daily summary suffix", "table", tableName, "suffix", suffix, "error", err) logger.Debug("hourly totals skip: invalid daily summary suffix", "table", tableName, "suffix", suffix, "error", err)
@@ -878,8 +879,8 @@ func addTotalsChartSheet(logger *slog.Logger, database db.Database, ctx context.
return return
} }
if strings.HasPrefix(tableName, "inventory_monthly_summary_") { if after, ok := strings.CutPrefix(tableName, "inventory_monthly_summary_"); ok {
suffix := strings.TrimPrefix(tableName, "inventory_monthly_summary_") suffix := after
monthStart, err := time.ParseInLocation("200601", suffix, time.Local) monthStart, err := time.ParseInLocation("200601", suffix, time.Local)
if err != nil { if err != nil {
logger.Debug("daily totals skip: invalid monthly summary suffix", "table", tableName, "suffix", suffix, "error", err) logger.Debug("daily totals skip: invalid monthly summary suffix", "table", tableName, "suffix", suffix, "error", err)
@@ -1001,10 +1002,7 @@ func titleCellFromPivotRange(pivotRange, fallback string) string {
if err != nil { if err != nil {
return fallback return fallback
} }
titleRow := row - 2 titleRow := max(row-2, 1)
if titleRow < 1 {
titleRow = 1
}
cell, err := excelize.CoordinatesToCellName(col, titleRow) cell, err := excelize.CoordinatesToCellName(col, titleRow)
if err != nil { if err != nil {
return fallback return fallback
@@ -1356,16 +1354,16 @@ func reportTypeFromTable(tableName string) string {
} }
func reportWindowFromTable(tableName string) (time.Time, time.Time, bool) { func reportWindowFromTable(tableName string) (time.Time, time.Time, bool) {
if strings.HasPrefix(tableName, "inventory_daily_summary_") { if after, ok := strings.CutPrefix(tableName, "inventory_daily_summary_"); ok {
suffix := strings.TrimPrefix(tableName, "inventory_daily_summary_") suffix := after
dayStart, err := time.ParseInLocation("20060102", suffix, time.Local) dayStart, err := time.ParseInLocation("20060102", suffix, time.Local)
if err != nil { if err != nil {
return time.Time{}, time.Time{}, false return time.Time{}, time.Time{}, false
} }
return dayStart, dayStart.AddDate(0, 0, 1), true return dayStart, dayStart.AddDate(0, 0, 1), true
} }
if strings.HasPrefix(tableName, "inventory_monthly_summary_") { if after, ok := strings.CutPrefix(tableName, "inventory_monthly_summary_"); ok {
suffix := strings.TrimPrefix(tableName, "inventory_monthly_summary_") suffix := after
monthStart, err := time.ParseInLocation("200601", suffix, time.Local) monthStart, err := time.ParseInLocation("200601", suffix, time.Local)
if err != nil { if err != nil {
return time.Time{}, time.Time{}, false return time.Time{}, time.Time{}, false
@@ -1383,7 +1381,7 @@ func addReportMetadataSheet(logger *slog.Logger, xlsx *excelize.File, meta repor
} }
rows := []struct { rows := []struct {
key string key string
value interface{} value any
}{ }{
{"ReportTable", meta.TableName}, {"ReportTable", meta.TableName},
{"ReportType", meta.ReportType}, {"ReportType", meta.ReportType},
@@ -1398,28 +1396,28 @@ func addReportMetadataSheet(logger *slog.Logger, xlsx *excelize.File, meta repor
rows = append(rows, rows = append(rows,
struct { struct {
key string key string
value interface{} value any
}{"DataWindowStart", meta.WindowStart.Format(time.RFC3339)}, }{"DataWindowStart", meta.WindowStart.Format(time.RFC3339)},
struct { struct {
key string key string
value interface{} value any
}{"DataWindowEnd", meta.WindowEnd.Format(time.RFC3339)}, }{"DataWindowEnd", meta.WindowEnd.Format(time.RFC3339)},
struct { struct {
key string key string
value interface{} value any
}{"DataWindowTimezone", time.Local.String()}, }{"DataWindowTimezone", time.Local.String()},
) )
} }
if meta.DBDriver != "" { if meta.DBDriver != "" {
rows = append(rows, struct { rows = append(rows, struct {
key string key string
value interface{} value any
}{"DatabaseDriver", meta.DBDriver}) }{"DatabaseDriver", meta.DBDriver})
} }
if meta.Duration > 0 && meta.RowCount > 0 { if meta.Duration > 0 && meta.RowCount > 0 {
rows = append(rows, struct { rows = append(rows, struct {
key string key string
value interface{} value any
}{"RowsPerSecond", math.Round((float64(meta.RowCount)/meta.Duration.Seconds())*1000) / 1000}) }{"RowsPerSecond", math.Round((float64(meta.RowCount)/meta.Duration.Seconds())*1000) / 1000})
} }
for i, row := range rows { for i, row := range rows {
@@ -1433,9 +1431,9 @@ func addReportMetadataSheet(logger *slog.Logger, xlsx *excelize.File, meta repor
} }
} }
func scanRowValues(rows *sqlx.Rows, columnCount int) ([]interface{}, error) { func scanRowValues(rows *sqlx.Rows, columnCount int) ([]any, error) {
rawValues := make([]interface{}, columnCount) rawValues := make([]any, columnCount)
scanArgs := make([]interface{}, columnCount) scanArgs := make([]any, columnCount)
for i := range rawValues { for i := range rawValues {
scanArgs[i] = &rawValues[i] scanArgs[i] = &rawValues[i]
} }
@@ -1445,7 +1443,7 @@ func scanRowValues(rows *sqlx.Rows, columnCount int) ([]interface{}, error) {
return rawValues, nil return rawValues, nil
} }
func normalizeCellValue(value interface{}) interface{} { func normalizeCellValue(value any) any {
switch v := value.(type) { switch v := value.(type) {
case nil: case nil:
return "" return ""
@@ -1749,14 +1747,14 @@ FROM diag, agg_diag
DeletedInInterval int64 `db:"deleted_in_interval"` DeletedInInterval int64 `db:"deleted_in_interval"`
PartialPresence int64 `db:"partial_presence"` PartialPresence int64 `db:"partial_presence"`
} }
overlapArgs := []interface{}{ overlapArgs := []any{
hourEndUnix, hourEndUnix, hourEndUnix, hourEndUnix,
hourStartUnix, hourStartUnix, hourStartUnix, hourStartUnix,
hourEndUnix, hourEndUnix, hourEndUnix, hourEndUnix,
hourStartUnix, hourStartUnix, hourStartUnix, hourStartUnix,
durationSeconds, durationSeconds,
} }
args := make([]interface{}, 0, len(overlapArgs)*3+6) args := make([]any, 0, len(overlapArgs)*3+6)
args = append(args, overlapArgs...) args = append(args, overlapArgs...)
args = append(args, overlapArgs...) args = append(args, overlapArgs...)
args = append(args, hourStartUnix, hourEndUnix) args = append(args, hourStartUnix, hourEndUnix)
@@ -1847,7 +1845,7 @@ func estimateSnapshotInterval(records []SnapshotRecord) time.Duration {
if len(diffs) == 0 { if len(diffs) == 0 {
return time.Hour return time.Hour
} }
sort.Slice(diffs, func(i, j int) bool { return diffs[i] < diffs[j] }) slices.Sort(diffs)
median := diffs[len(diffs)/2] median := diffs[len(diffs)/2]
if median <= 0 { if median <= 0 {
return time.Hour return time.Hour
@@ -2032,7 +2030,7 @@ func writeTotalsChart(logger *slog.Logger, xlsx *excelize.File, sheetName string
makeChart("K52", "F", "G", "H", "I") makeChart("K52", "F", "G", "H", "I")
} }
func formatEpochHuman(value interface{}) string { func formatEpochHuman(value any) string {
var epoch int64 var epoch int64
switch v := value.(type) { switch v := value.(type) {
case nil: case nil:

View File

@@ -21,7 +21,7 @@ func TestAddSummaryPivotSheetCreatesPivotTables(t *testing.T) {
t.Fatalf("SetSheetRow header failed: %v", err) t.Fatalf("SetSheetRow header failed: %v", err)
} }
row1 := []interface{}{"vm-1", "dc-1", "pool-1", 4.0, 16.0, 1.0} row1 := []any{"vm-1", "dc-1", "pool-1", 4.0, 16.0, 1.0}
if err := xlsx.SetSheetRow(dataSheet, "A2", &row1); err != nil { if err := xlsx.SetSheetRow(dataSheet, "A2", &row1); err != nil {
t.Fatalf("SetSheetRow data failed: %v", err) t.Fatalf("SetSheetRow data failed: %v", err)
} }

View File

@@ -10,7 +10,7 @@ import (
"strings" "strings"
"vctp/internal/utils" "vctp/internal/utils"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v3"
) )
var ( var (

View File

@@ -180,7 +180,7 @@ WHERE job_name = ?
return err return err
} }
func nullableString(s string) interface{} { func nullableString(s string) any {
if s == "" { if s == "" {
return nil return nil
} }

View File

@@ -8,7 +8,7 @@ import (
"log/slog" "log/slog"
"os" "os"
"runtime" "runtime"
"sort" "slices"
"strings" "strings"
"sync" "sync"
"time" "time"
@@ -295,7 +295,7 @@ func (c *CronTask) aggregateDailySummaryGo(ctx context.Context, dayStart, dayEnd
for _, snap := range hourlySnapshots { for _, snap := range hourlySnapshots {
snapTimes = append(snapTimes, snap.SnapshotTime.Unix()) snapTimes = append(snapTimes, snap.SnapshotTime.Unix())
} }
sort.Slice(snapTimes, func(i, j int) bool { return snapTimes[i] < snapTimes[j] }) slices.Sort(snapTimes)
} }
lifecycleDeletions := c.applyLifecycleDeletions(ctx, aggMap, dayStart, dayEnd) lifecycleDeletions := c.applyLifecycleDeletions(ctx, aggMap, dayStart, dayEnd)
@@ -353,7 +353,7 @@ LIMIT 1
for t := range set { for t := range set {
times = append(times, t) times = append(times, t)
} }
sort.Slice(times, func(i, j int) bool { return times[i] < times[j] }) slices.Sort(times)
vcenterSnapTimes[vcenter] = times vcenterSnapTimes[vcenter] = times
} }
@@ -843,20 +843,12 @@ func (c *CronTask) applyInventoryCreations(ctx context.Context, agg map[dailyAgg
func (c *CronTask) scanHourlyTablesParallel(ctx context.Context, snapshots []report.SnapshotRecord) (map[dailyAggKey]*dailyAggVal, error) { func (c *CronTask) scanHourlyTablesParallel(ctx context.Context, snapshots []report.SnapshotRecord) (map[dailyAggKey]*dailyAggVal, error) {
agg := make(map[dailyAggKey]*dailyAggVal, 1024) agg := make(map[dailyAggKey]*dailyAggVal, 1024)
mu := sync.Mutex{} mu := sync.Mutex{}
workers := runtime.NumCPU() workers := min(max(runtime.NumCPU(), 2), len(snapshots))
if workers < 2 {
workers = 2
}
if workers > len(snapshots) {
workers = len(snapshots)
}
jobs := make(chan report.SnapshotRecord, len(snapshots)) jobs := make(chan report.SnapshotRecord, len(snapshots))
wg := sync.WaitGroup{} wg := sync.WaitGroup{}
for i := 0; i < workers; i++ { for i := 0; i < workers; i++ {
wg.Add(1) wg.Go(func() {
go func() {
defer wg.Done()
for snap := range jobs { for snap := range jobs {
rows, err := c.scanHourlyTable(ctx, snap) rows, err := c.scanHourlyTable(ctx, snap)
if err != nil { if err != nil {
@@ -873,7 +865,7 @@ func (c *CronTask) scanHourlyTablesParallel(ctx context.Context, snapshots []rep
} }
mu.Unlock() mu.Unlock()
} }
}() })
} }
for _, snap := range snapshots { for _, snap := range snapshots {
jobs <- snap jobs <- snap
@@ -1114,7 +1106,7 @@ WHERE "SnapshotTime" >= ? AND "SnapshotTime" < ?`
for t := range timeSet { for t := range timeSet {
snapTimes = append(snapTimes, t) snapTimes = append(snapTimes, t)
} }
sort.Slice(snapTimes, func(i, j int) bool { return snapTimes[i] < snapTimes[j] }) slices.Sort(snapTimes)
return agg, snapTimes, rows.Err() return agg, snapTimes, rows.Err()
} }
@@ -1166,7 +1158,7 @@ INSERT INTO %s (
silverPct = float64(v.silverHits) * 100 / float64(v.samples) silverPct = float64(v.silverHits) * 100 / float64(v.samples)
goldPct = float64(v.goldHits) * 100 / float64(v.samples) goldPct = float64(v.goldHits) * 100 / float64(v.samples)
} }
args := []interface{}{ args := []any{
v.key.Name, v.key.Name,
v.key.Vcenter, v.key.Vcenter,
nullIfEmpty(v.key.VmId), nullIfEmpty(v.key.VmId),
@@ -1214,7 +1206,7 @@ func int64OrZero(v sql.NullInt64) int64 {
return 0 return 0
} }
func nullIfEmpty(s string) interface{} { func nullIfEmpty(s string) any {
if strings.TrimSpace(s) == "" { if strings.TrimSpace(s) == "" {
return nil return nil
} }
@@ -1224,13 +1216,13 @@ func nullIfEmpty(s string) interface{} {
func makePlaceholders(driver string, n int) string { func makePlaceholders(driver string, n int) string {
if driver == "sqlite" { if driver == "sqlite" {
parts := make([]string, n) parts := make([]string, n)
for i := 0; i < n; i++ { for i := range n {
parts[i] = "?" parts[i] = "?"
} }
return strings.Join(parts, ",") return strings.Join(parts, ",")
} }
parts := make([]string, n) parts := make([]string, n)
for i := 0; i < n; i++ { for i := range n {
parts[i] = fmt.Sprintf("$%d", i+1) parts[i] = fmt.Sprintf("$%d", i+1)
} }
return strings.Join(parts, ",") return strings.Join(parts, ",")

View File

@@ -61,7 +61,7 @@ func insertHourlyCache(ctx context.Context, dbConn *sqlx.DB, rows []InventorySna
defer stmt.Close() defer stmt.Close()
for _, r := range rows { for _, r := range rows {
args := []interface{}{ args := []any{
r.SnapshotTime, r.Vcenter, r.VmId, r.VmUuid, r.Name, r.CreationTime, r.DeletionTime, r.ResourcePool, r.SnapshotTime, r.Vcenter, r.VmId, r.VmUuid, r.Name, r.CreationTime, r.DeletionTime, r.ResourcePool,
r.Datacenter, r.Cluster, r.Folder, r.ProvisionedDisk, r.VcpuCount, r.RamGB, r.IsTemplate, r.PoweredOn, r.SrmPlaceholder, r.Datacenter, r.Cluster, r.Folder, r.ProvisionedDisk, r.VcpuCount, r.RamGB, r.IsTemplate, r.PoweredOn, r.SrmPlaceholder,
} }
@@ -105,7 +105,7 @@ func insertHourlyBatch(ctx context.Context, dbConn *sqlx.DB, tableName string, r
} }
defer stmt.Close() defer stmt.Close()
for _, row := range rows { for _, row := range rows {
args := []interface{}{ args := []any{
row.InventoryId, row.InventoryId,
row.Name, row.Name,
row.Vcenter, row.Vcenter,
@@ -138,7 +138,7 @@ func insertHourlyBatch(ctx context.Context, dbConn *sqlx.DB, tableName string, r
defer stmt.Close() defer stmt.Close()
for _, row := range rows { for _, row := range rows {
args := []interface{}{ args := []any{
row.InventoryId, row.InventoryId,
row.Name, row.Name,
row.Vcenter, row.Vcenter,

View File

@@ -27,7 +27,7 @@ func acquireSnapshotProbe(ctx context.Context) (func(), error) {
} }
} }
func boolStringFromInterface(value interface{}) string { func boolStringFromInterface(value any) string {
switch v := value.(type) { switch v := value.(type) {
case nil: case nil:
return "" return ""
@@ -164,7 +164,7 @@ func SnapshotTooSoon(prevUnix, currUnix int64, expectedSeconds int64) bool {
} }
// querySnapshotRows builds a SELECT with proper rebind for the given table/columns/where. // querySnapshotRows builds a SELECT with proper rebind for the given table/columns/where.
func querySnapshotRows(ctx context.Context, dbConn *sqlx.DB, table string, columns []string, where string, args ...interface{}) (*sqlx.Rows, error) { func querySnapshotRows(ctx context.Context, dbConn *sqlx.DB, table string, columns []string, where string, args ...any) (*sqlx.Rows, error) {
if err := db.ValidateTableName(table); err != nil { if err := db.ValidateTableName(table); err != nil {
return nil, err return nil, err
} }

View File

@@ -24,8 +24,6 @@ import (
"github.com/vmware/govmomi/vim25/types" "github.com/vmware/govmomi/vim25/types"
) )
type ctxLoggerKey struct{}
type deletionCandidate struct { type deletionCandidate struct {
vmID string vmID string
vmUUID string vmUUID string
@@ -42,10 +40,7 @@ type vcenterResources struct {
} }
func loggerFromCtx(ctx context.Context, fallback *slog.Logger) *slog.Logger { func loggerFromCtx(ctx context.Context, fallback *slog.Logger) *slog.Logger {
if ctx == nil { if l := db.LoggerFromContext(ctx); l != nil {
return fallback
}
if l, ok := ctx.Value(ctxLoggerKey{}).(*slog.Logger); ok && l != nil {
return l return l
} }
return fallback return fallback
@@ -132,10 +127,7 @@ func (c *CronTask) RunVcenterSnapshotHourly(ctx context.Context, logger *slog.Lo
if err != nil { if err != nil {
return err return err
} }
minIntervalSeconds := intWithDefault(c.Settings.Values.Settings.VcenterInventorySnapshotSeconds, 3600) / 3 minIntervalSeconds := max(intWithDefault(c.Settings.Values.Settings.VcenterInventorySnapshotSeconds, 3600)/3, 1)
if minIntervalSeconds < 1 {
minIntervalSeconds = 1
}
if !lastSnapshot.IsZero() && startTime.Sub(lastSnapshot) < time.Duration(minIntervalSeconds)*time.Second { if !lastSnapshot.IsZero() && startTime.Sub(lastSnapshot) < time.Duration(minIntervalSeconds)*time.Second {
c.Logger.Info("Skipping hourly snapshot, last snapshot too recent", c.Logger.Info("Skipping hourly snapshot, last snapshot too recent",
"last_snapshot", lastSnapshot, "last_snapshot", lastSnapshot,
@@ -217,7 +209,7 @@ func (c *CronTask) RunVcenterSnapshotHourly(ctx context.Context, logger *slog.Lo
metrics.RecordHourlySnapshot(startTime, rowCount, err) metrics.RecordHourlySnapshot(startTime, rowCount, err)
var deferredTables []string var deferredTables []string
deferredReportTables.Range(func(key, _ interface{}) bool { deferredReportTables.Range(func(key, _ any) bool {
name, ok := key.(string) name, ok := key.(string)
if ok && strings.TrimSpace(name) != "" && name != tableName { if ok && strings.TrimSpace(name) != "" && name != tableName {
deferredTables = append(deferredTables, name) deferredTables = append(deferredTables, name)
@@ -488,10 +480,7 @@ func buildUnionQuery(tables []string, columns []string, whereClause string) (str
batches := make([]string, 0, (len(tables)/maxCompoundTerms)+1) batches := make([]string, 0, (len(tables)/maxCompoundTerms)+1)
batchIndex := 0 batchIndex := 0
for start := 0; start < len(tables); start += maxCompoundTerms { for start := 0; start < len(tables); start += maxCompoundTerms {
end := start + maxCompoundTerms end := min(start+maxCompoundTerms, len(tables))
if end > len(tables) {
end = len(tables)
}
queries := make([]string, 0, end-start) queries := make([]string, 0, end-start)
for _, table := range tables[start:end] { for _, table := range tables[start:end] {
safeName, err := db.SafeTableName(table) safeName, err := db.SafeTableName(table)
@@ -1337,7 +1326,7 @@ func (c *CronTask) initVcenterResources(ctx context.Context, log *slog.Logger, u
func (c *CronTask) captureHourlySnapshotForVcenter(ctx context.Context, startTime time.Time, tableName string, url string, deferredReportTables *sync.Map) error { func (c *CronTask) captureHourlySnapshotForVcenter(ctx context.Context, startTime time.Time, tableName string, url string, deferredReportTables *sync.Map) error {
log := c.Logger.With("vcenter", url) log := c.Logger.With("vcenter", url)
ctx = context.WithValue(ctx, ctxLoggerKey{}, log) ctx = db.WithLoggerContext(ctx, log)
started := time.Now() started := time.Now()
log.Debug("connecting to vcenter for hourly snapshot", "url", url) log.Debug("connecting to vcenter for hourly snapshot", "url", url)
vc, resources, cleanup, err := c.initVcenterResources(ctx, log, url, startTime, started) vc, resources, cleanup, err := c.initVcenterResources(ctx, log, url, startTime, started)

View File

@@ -7,7 +7,7 @@ import (
"log/slog" "log/slog"
"os" "os"
"runtime" "runtime"
"sort" "slices"
"strings" "strings"
"sync" "sync"
"time" "time"
@@ -246,7 +246,7 @@ func (c *CronTask) aggregateMonthlySummaryGoHourly(ctx context.Context, monthSta
for _, snap := range hourlySnapshots { for _, snap := range hourlySnapshots {
snapTimes = append(snapTimes, snap.SnapshotTime.Unix()) snapTimes = append(snapTimes, snap.SnapshotTime.Unix())
} }
sort.Slice(snapTimes, func(i, j int) bool { return snapTimes[i] < snapTimes[j] }) slices.Sort(snapTimes)
} }
lifecycleDeletions := c.applyLifecycleDeletions(ctx, aggMap, monthStart, monthEnd) lifecycleDeletions := c.applyLifecycleDeletions(ctx, aggMap, monthStart, monthEnd)
@@ -394,20 +394,12 @@ func (c *CronTask) aggregateMonthlySummaryGo(ctx context.Context, monthStart, mo
func (c *CronTask) scanDailyTablesParallel(ctx context.Context, snapshots []report.SnapshotRecord) (map[monthlyAggKey]*monthlyAggVal, error) { func (c *CronTask) scanDailyTablesParallel(ctx context.Context, snapshots []report.SnapshotRecord) (map[monthlyAggKey]*monthlyAggVal, error) {
agg := make(map[monthlyAggKey]*monthlyAggVal, 1024) agg := make(map[monthlyAggKey]*monthlyAggVal, 1024)
mu := sync.Mutex{} mu := sync.Mutex{}
workers := runtime.NumCPU() workers := min(max(runtime.NumCPU(), 2), len(snapshots))
if workers < 2 {
workers = 2
}
if workers > len(snapshots) {
workers = len(snapshots)
}
jobs := make(chan report.SnapshotRecord, len(snapshots)) jobs := make(chan report.SnapshotRecord, len(snapshots))
wg := sync.WaitGroup{} wg := sync.WaitGroup{}
for i := 0; i < workers; i++ { for i := 0; i < workers; i++ {
wg.Add(1) wg.Go(func() {
go func() {
defer wg.Done()
for snap := range jobs { for snap := range jobs {
rows, err := c.scanDailyTable(ctx, snap) rows, err := c.scanDailyTable(ctx, snap)
if err != nil { if err != nil {
@@ -424,7 +416,7 @@ func (c *CronTask) scanDailyTablesParallel(ctx context.Context, snapshots []repo
} }
mu.Unlock() mu.Unlock()
} }
}() })
} }
for _, snap := range snapshots { for _, snap := range snapshots {
jobs <- snap jobs <- snap

View File

@@ -449,8 +449,8 @@ func resolveVcenterPassword(logger *slog.Logger, cipher *secrets.Secrets, legacy
} }
// New format: explicit prefix so we can distinguish ciphertext from plaintext safely. // New format: explicit prefix so we can distinguish ciphertext from plaintext safely.
if strings.HasPrefix(raw, encryptedVcenterPasswordPrefix) { if after, ok := strings.CutPrefix(raw, encryptedVcenterPasswordPrefix); ok {
enc := strings.TrimPrefix(raw, encryptedVcenterPasswordPrefix) enc := after
pass, usedLegacyKey, err := decryptVcenterPasswordWithFallback(logger, cipher, legacyDecryptKeys, enc) pass, usedLegacyKey, err := decryptVcenterPasswordWithFallback(logger, cipher, legacyDecryptKeys, enc)
if err != nil { if err != nil {
return nil, "", fmt.Errorf("prefixed password decrypt failed: %w", err) return nil, "", fmt.Errorf("prefixed password decrypt failed: %w", err)

View File

@@ -1,7 +1,6 @@
package handler package handler
import ( import (
"context"
"database/sql" "database/sql"
"fmt" "fmt"
"net/http" "net/http"
@@ -42,7 +41,7 @@ func (h *Handler) DailyCreationDiagnostics(w http.ResponseWriter, r *http.Reques
return return
} }
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) ctx, cancel := withRequestTimeout(r, 10*time.Second)
defer cancel() defer cancel()
dbConn := h.Database.DB() dbConn := h.Database.DB()

View File

@@ -1,7 +1,6 @@
package handler package handler
import ( import (
"context"
"fmt" "fmt"
"net/http" "net/http"
"vctp/internal/report" "vctp/internal/report"
@@ -16,8 +15,8 @@ import (
// @Failure 500 {object} models.ErrorResponse "Report generation failed" // @Failure 500 {object} models.ErrorResponse "Report generation failed"
// @Router /api/report/inventory [get] // @Router /api/report/inventory [get]
func (h *Handler) InventoryReportDownload(w http.ResponseWriter, r *http.Request) { func (h *Handler) InventoryReportDownload(w http.ResponseWriter, r *http.Request) {
ctx, cancel := withRequestTimeout(r, reportRequestTimeout)
ctx := context.Background() defer cancel()
// Generate the XLSX report // Generate the XLSX report
reportData, err := report.CreateInventoryReport(h.Logger, h.Database, ctx) reportData, err := report.CreateInventoryReport(h.Logger, h.Database, ctx)
@@ -45,8 +44,8 @@ func (h *Handler) InventoryReportDownload(w http.ResponseWriter, r *http.Request
// @Failure 500 {object} models.ErrorResponse "Report generation failed" // @Failure 500 {object} models.ErrorResponse "Report generation failed"
// @Router /api/report/updates [get] // @Router /api/report/updates [get]
func (h *Handler) UpdateReportDownload(w http.ResponseWriter, r *http.Request) { func (h *Handler) UpdateReportDownload(w http.ResponseWriter, r *http.Request) {
ctx, cancel := withRequestTimeout(r, reportRequestTimeout)
ctx := context.Background() defer cancel()
// Generate the XLSX report // Generate the XLSX report
reportData, err := report.CreateUpdatesReport(h.Logger, h.Database, ctx) reportData, err := report.CreateUpdatesReport(h.Logger, h.Database, ctx)

View File

@@ -0,0 +1,24 @@
package handler
import (
"context"
"net/http"
"time"
)
const (
defaultRequestTimeout = 2 * time.Minute
reportRequestTimeout = 10 * time.Minute
longRunningRequestTimeout = 2 * time.Hour
)
func withRequestTimeout(r *http.Request, timeout time.Duration) (context.Context, context.CancelFunc) {
base := context.Background()
if r != nil {
base = r.Context()
}
if timeout <= 0 {
return base, func() {}
}
return context.WithTimeout(base, timeout)
}

View File

@@ -1,7 +1,6 @@
package handler package handler
import ( import (
"context"
"net/http" "net/http"
"strings" "strings"
"time" "time"
@@ -50,7 +49,8 @@ func (h *Handler) SnapshotAggregateForce(w http.ResponseWriter, r *http.Request)
return return
} }
ctx := context.Background() ctx, cancel := withRequestTimeout(r, longRunningRequestTimeout)
defer cancel()
settingsCopy := *h.Settings.Values settingsCopy := *h.Settings.Values
if granularity != "" { if granularity != "" {
settingsCopy.Settings.MonthlyAggregationGranularity = granularity settingsCopy.Settings.MonthlyAggregationGranularity = granularity

View File

@@ -1,7 +1,6 @@
package handler package handler
import ( import (
"context"
"net/http" "net/http"
"strings" "strings"
"time" "time"
@@ -26,7 +25,8 @@ func (h *Handler) SnapshotForceHourly(w http.ResponseWriter, r *http.Request) {
return return
} }
ctx := context.Background() ctx, cancel := withRequestTimeout(r, longRunningRequestTimeout)
defer cancel()
ct := &tasks.CronTask{ ct := &tasks.CronTask{
Logger: h.Logger, Logger: h.Logger,
Database: h.Database, Database: h.Database,

View File

@@ -1,7 +1,6 @@
package handler package handler
import ( import (
"context"
"net/http" "net/http"
"vctp/internal/report" "vctp/internal/report"
"vctp/server/models" "vctp/server/models"
@@ -16,7 +15,8 @@ import (
// @Failure 500 {object} models.SnapshotMigrationResponse "Server error" // @Failure 500 {object} models.SnapshotMigrationResponse "Server error"
// @Router /api/snapshots/migrate [post] // @Router /api/snapshots/migrate [post]
func (h *Handler) SnapshotMigrate(w http.ResponseWriter, r *http.Request) { func (h *Handler) SnapshotMigrate(w http.ResponseWriter, r *http.Request) {
ctx := context.Background() ctx, cancel := withRequestTimeout(r, reportRequestTimeout)
defer cancel()
stats, err := report.MigrateSnapshotRegistry(ctx, h.Database) stats, err := report.MigrateSnapshotRegistry(ctx, h.Database)
if err != nil { if err != nil {
writeJSON(w, http.StatusInternalServerError, models.SnapshotMigrationResponse{ writeJSON(w, http.StatusInternalServerError, models.SnapshotMigrationResponse{

View File

@@ -1,7 +1,6 @@
package handler package handler
import ( import (
"context"
"fmt" "fmt"
"net/http" "net/http"
"net/url" "net/url"
@@ -58,7 +57,8 @@ func (h *Handler) SnapshotMonthlyList(w http.ResponseWriter, r *http.Request) {
// @Failure 500 {object} models.ErrorResponse "Server error" // @Failure 500 {object} models.ErrorResponse "Server error"
// @Router /api/report/snapshot [get] // @Router /api/report/snapshot [get]
func (h *Handler) SnapshotReportDownload(w http.ResponseWriter, r *http.Request) { func (h *Handler) SnapshotReportDownload(w http.ResponseWriter, r *http.Request) {
ctx := context.Background() ctx, cancel := withRequestTimeout(r, reportRequestTimeout)
defer cancel()
tableName := r.URL.Query().Get("table") tableName := r.URL.Query().Get("table")
if tableName == "" { if tableName == "" {
writeJSONError(w, http.StatusBadRequest, "Missing table parameter") writeJSONError(w, http.StatusBadRequest, "Missing table parameter")
@@ -80,7 +80,8 @@ func (h *Handler) SnapshotReportDownload(w http.ResponseWriter, r *http.Request)
} }
func (h *Handler) renderSnapshotList(w http.ResponseWriter, r *http.Request, snapshotType string, title string, renderer func([]views.SnapshotEntry) templ.Component) { func (h *Handler) renderSnapshotList(w http.ResponseWriter, r *http.Request, snapshotType string, title string, renderer func([]views.SnapshotEntry) templ.Component) {
ctx := context.Background() ctx, cancel := withRequestTimeout(r, defaultRequestTimeout)
defer cancel()
if err := report.EnsureSnapshotRegistry(ctx, h.Database); err != nil { if err := report.EnsureSnapshotRegistry(ctx, h.Database); err != nil {
h.Logger.Error("Failed to ensure snapshot registry", "error", err) h.Logger.Error("Failed to ensure snapshot registry", "error", err)
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
@@ -107,10 +108,7 @@ func (h *Handler) renderSnapshotList(w http.ResponseWriter, r *http.Request, sna
case "monthly": case "monthly":
group = record.SnapshotTime.Format("2006") group = record.SnapshotTime.Format("2006")
} }
count := record.SnapshotCount count := max(record.SnapshotCount, 0)
if count < 0 {
count = 0
}
entries = append(entries, views.SnapshotEntry{ entries = append(entries, views.SnapshotEntry{
Label: label, Label: label,
Link: "/reports/" + url.PathEscape(record.TableName) + ".xlsx", Link: "/reports/" + url.PathEscape(record.TableName) + ".xlsx",

View File

@@ -1,7 +1,6 @@
package handler package handler
import ( import (
"context"
"fmt" "fmt"
"net/http" "net/http"
) )
@@ -19,6 +18,8 @@ func (h *Handler) UpdateCleanup(w http.ResponseWriter, r *http.Request) {
if h.denyLegacyAPI(w, "/api/cleanup/updates") { if h.denyLegacyAPI(w, "/api/cleanup/updates") {
return return
} }
ctx, cancel := withRequestTimeout(r, defaultRequestTimeout)
defer cancel()
/* /*
// Get the current time // Get the current time
@@ -35,11 +36,11 @@ func (h *Handler) UpdateCleanup(w http.ResponseWriter, r *http.Request) {
} }
h.Logger.Debug("database params", "params", params) h.Logger.Debug("database params", "params", params)
err := h.Database.Queries().CleanupUpdates(context.Background(), params) err := h.Database.Queries().CleanupUpdates(ctx, params)
*/ */
//err := h.Database.Queries().InventoryCleanupTemplates(context.Background()) //err := h.Database.Queries().InventoryCleanupTemplates(ctx)
err := h.Database.Queries().CleanupUpdatesNullVm(context.Background()) err := h.Database.Queries().CleanupUpdatesNullVm(ctx)
if err != nil { if err != nil {
h.Logger.Error("Error received cleaning updates table", "error", err) h.Logger.Error("Error received cleaning updates table", "error", err)

View File

@@ -1,7 +1,6 @@
package handler package handler
import ( import (
"context"
"database/sql" "database/sql"
"errors" "errors"
"fmt" "fmt"
@@ -23,7 +22,8 @@ func (h *Handler) VcCleanup(w http.ResponseWriter, r *http.Request) {
return return
} }
ctx := context.Background() ctx, cancel := withRequestTimeout(r, defaultRequestTimeout)
defer cancel()
// Get the parameters // Get the parameters
vcUrl := r.URL.Query().Get("vc_url") vcUrl := r.URL.Query().Get("vc_url")

View File

@@ -108,7 +108,7 @@ func (h *Handler) rebuildOneVcenterCache(ctx context.Context, vcURL string) (int
return 0, 0, 0, fmt.Errorf("unable to connect to vcenter: %w", err) return 0, 0, 0, fmt.Errorf("unable to connect to vcenter: %w", err)
} }
defer func() { defer func() {
logoutCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) logoutCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 5*time.Second)
defer cancel() defer cancel()
if err := vc.Logout(logoutCtx); err != nil { if err := vc.Logout(logoutCtx); err != nil {
h.Logger.Warn("vcenter cache rebuild logout failed", "vcenter", vcURL, "error", err) h.Logger.Warn("vcenter cache rebuild logout failed", "vcenter", vcURL, "error", err)

View File

@@ -1,7 +1,6 @@
package handler package handler
import ( import (
"context"
"database/sql" "database/sql"
"errors" "errors"
"fmt" "fmt"
@@ -25,7 +24,8 @@ func (h *Handler) VmCleanup(w http.ResponseWriter, r *http.Request) {
return return
} }
ctx := context.Background() ctx, cancel := withRequestTimeout(r, defaultRequestTimeout)
defer cancel()
// Get the parameters // Get the parameters
vmId := r.URL.Query().Get("vm_id") vmId := r.URL.Query().Get("vm_id")

View File

@@ -1,7 +1,6 @@
package handler package handler
import ( import (
"context"
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"fmt" "fmt"
@@ -30,6 +29,8 @@ func (h *Handler) VmCreateEvent(w http.ResponseWriter, r *http.Request) {
if h.denyLegacyAPI(w, "/api/event/vm/create") { if h.denyLegacyAPI(w, "/api/event/vm/create") {
return return
} }
ctx, cancel := withRequestTimeout(r, defaultRequestTimeout)
defer cancel()
var ( var (
unixTimestamp int64 unixTimestamp int64
@@ -96,7 +97,7 @@ func (h *Handler) VmCreateEvent(w http.ResponseWriter, r *http.Request) {
h.Logger.Debug("database params", "params", params) h.Logger.Debug("database params", "params", params)
// Insert the new inventory record into the database // Insert the new inventory record into the database
result, err := h.Database.Queries().CreateEvent(context.Background(), params) result, err := h.Database.Queries().CreateEvent(ctx, params)
if err != nil { if err != nil {
h.Logger.Error("unable to perform database insert", "error", err) h.Logger.Error("unable to perform database insert", "error", err)
writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("Error: %v", err)) writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("Error: %v", err))
@@ -109,7 +110,7 @@ func (h *Handler) VmCreateEvent(w http.ResponseWriter, r *http.Request) {
} }
// prettyPrint comes from https://gist.github.com/sfate/9d45f6c5405dc4c9bf63bf95fe6d1a7c // prettyPrint comes from https://gist.github.com/sfate/9d45f6c5405dc4c9bf63bf95fe6d1a7c
func prettyPrint(args ...interface{}) { func prettyPrint(args ...any) {
var caller string var caller string
timeNow := time.Now().Format("01-02-2006 15:04:05") timeNow := time.Now().Format("01-02-2006 15:04:05")

View File

@@ -1,7 +1,6 @@
package handler package handler
import ( import (
"context"
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"fmt" "fmt"
@@ -28,6 +27,8 @@ func (h *Handler) VmDeleteEvent(w http.ResponseWriter, r *http.Request) {
if h.denyLegacyAPI(w, "/api/event/vm/delete") { if h.denyLegacyAPI(w, "/api/event/vm/delete") {
return return
} }
ctx, cancel := withRequestTimeout(r, defaultRequestTimeout)
defer cancel()
var ( var (
deletedTimestamp int64 deletedTimestamp int64
@@ -71,7 +72,7 @@ func (h *Handler) VmDeleteEvent(w http.ResponseWriter, r *http.Request) {
DatacenterName: sql.NullString{String: event.CloudEvent.Data.Datacenter.Name, Valid: event.CloudEvent.Data.Datacenter.Name != ""}, DatacenterName: sql.NullString{String: event.CloudEvent.Data.Datacenter.Name, Valid: event.CloudEvent.Data.Datacenter.Name != ""},
} }
h.Logger.Debug("database params", "params", params) h.Logger.Debug("database params", "params", params)
err = h.Database.Queries().InventoryMarkDeleted(context.Background(), params) err = h.Database.Queries().InventoryMarkDeleted(ctx, params)
if err != nil { if err != nil {
h.Logger.Error("Error received marking VM as deleted", "error", err) h.Logger.Error("Error received marking VM as deleted", "error", err)

View File

@@ -1,7 +1,6 @@
package handler package handler
import ( import (
"context"
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"errors" "errors"
@@ -58,7 +57,8 @@ func (h *Handler) VmImport(w http.ResponseWriter, r *http.Request) {
return return
} }
ctx := context.Background() ctx, cancel := withRequestTimeout(r, defaultRequestTimeout)
defer cancel()
// Query Inventory table for this VM before adding it // Query Inventory table for this VM before adding it
h.Logger.Debug("Checking inventory table for VM record") h.Logger.Debug("Checking inventory table for VM record")

View File

@@ -41,7 +41,8 @@ func (h *Handler) VmModifyEvent(w http.ResponseWriter, r *http.Request) {
var unixTimestamp int64 var unixTimestamp int64
re := regexp.MustCompile(`/([^/]+)/[^/]+\.vmdk$`) re := regexp.MustCompile(`/([^/]+)/[^/]+\.vmdk$`)
ctx := context.Background() ctx, cancel := withRequestTimeout(r, defaultRequestTimeout)
defer cancel()
reqBody, err := io.ReadAll(r.Body) reqBody, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
@@ -189,7 +190,7 @@ func (h *Handler) VmModifyEvent(w http.ResponseWriter, r *http.Request) {
// If we found a disk change belonging to this VM then recalculate the disk size // If we found a disk change belonging to this VM then recalculate the disk size
if diskChangeFound { if diskChangeFound {
params.UpdateType = "diskchange" params.UpdateType = "diskchange"
diskSize := h.calculateNewDiskSize(event) diskSize := h.calculateNewDiskSize(ctx, event)
params.NewProvisionedDisk = sql.NullFloat64{Float64: diskSize, Valid: diskSize > 0} params.NewProvisionedDisk = sql.NullFloat64{Float64: diskSize, Valid: diskSize > 0}
} }
} }
@@ -333,7 +334,7 @@ func (h *Handler) processConfigChanges(configChanges string) []map[string]string
return result return result
} }
func (h *Handler) calculateNewDiskSize(event models.CloudEventReceived) float64 { func (h *Handler) calculateNewDiskSize(ctx context.Context, event models.CloudEventReceived) float64 {
var diskSize float64 var diskSize float64
var totalDiskBytes int64 var totalDiskBytes int64
h.Logger.Debug("connecting to vcenter") h.Logger.Debug("connecting to vcenter")
@@ -368,7 +369,9 @@ func (h *Handler) calculateNewDiskSize(event models.CloudEventReceived) float64
} }
} }
_ = vc.Logout(context.Background()) logoutCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 5*time.Second)
defer cancel()
_ = vc.Logout(logoutCtx)
h.Logger.Debug("Calculated new disk size", "value", diskSize) h.Logger.Debug("Calculated new disk size", "value", diskSize)

View File

@@ -1,7 +1,6 @@
package handler package handler
import ( import (
"context"
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"errors" "errors"
@@ -34,7 +33,8 @@ func (h *Handler) VmMoveEvent(w http.ResponseWriter, r *http.Request) {
params := queries.CreateUpdateParams{} params := queries.CreateUpdateParams{}
var unixTimestamp int64 var unixTimestamp int64
ctx := context.Background() ctx, cancel := withRequestTimeout(r, defaultRequestTimeout)
defer cancel()
reqBody, err := io.ReadAll(r.Body) reqBody, err := io.ReadAll(r.Body)
if err != nil { if err != nil {

View File

@@ -1,7 +1,6 @@
package handler package handler
import ( import (
"context"
"database/sql" "database/sql"
"net/http" "net/http"
"vctp/db/queries" "vctp/db/queries"
@@ -28,7 +27,8 @@ func (h *Handler) VmUpdateDetails(w http.ResponseWriter, r *http.Request) {
var vmUuid string var vmUuid string
var dbUuid string var dbUuid string
ctx := context.Background() ctx, cancel := withRequestTimeout(r, longRunningRequestTimeout)
defer cancel()
// reload settings in case vcenter list has changed // reload settings in case vcenter list has changed
h.Settings.ReadYMLSettings() h.Settings.ReadYMLSettings()
@@ -101,7 +101,7 @@ func (h *Handler) VmUpdateDetails(w http.ResponseWriter, r *http.Request) {
} }
h.Logger.Debug("database params", "params", params) h.Logger.Debug("database params", "params", params)
err := h.Database.Queries().InventoryUpdate(context.Background(), params) err := h.Database.Queries().InventoryUpdate(ctx, params)
if err != nil { if err != nil {
h.Logger.Error("Error received updating inventory for VM", "name", vmObj.Name, "error", err) h.Logger.Error("Error received updating inventory for VM", "name", vmObj.Name, "error", err)

View File

@@ -31,9 +31,9 @@ type CloudEventReceived struct {
} `json:"Datacenter"` } `json:"Datacenter"`
Name string `json:"Name"` Name string `json:"Name"`
} `json:"Datacenter"` } `json:"Datacenter"`
Ds interface{} `json:"Ds"` Ds any `json:"Ds"`
Dvs interface{} `json:"Dvs"` Dvs any `json:"Dvs"`
FullFormattedMessage string `json:"FullFormattedMessage"` FullFormattedMessage string `json:"FullFormattedMessage"`
Host struct { Host struct {
Host struct { Host struct {
Type string `json:"Type"` Type string `json:"Type"`
@@ -42,7 +42,7 @@ type CloudEventReceived struct {
Name string `json:"Name"` Name string `json:"Name"`
} `json:"Host"` } `json:"Host"`
Key int `json:"Key"` Key int `json:"Key"`
Net interface{} `json:"Net"` Net any `json:"Net"`
NewParent *CloudEventResourcePool `json:"NewParent"` NewParent *CloudEventResourcePool `json:"NewParent"`
OldParent *CloudEventResourcePool `json:"OldParent"` OldParent *CloudEventResourcePool `json:"OldParent"`
SrcTemplate *CloudEventVm `json:"SrcTemplate"` SrcTemplate *CloudEventVm `json:"SrcTemplate"`
@@ -158,7 +158,7 @@ type ConfigSpec struct {
} `json:"StorageIOAllocation"` } `json:"StorageIOAllocation"`
VDiskID any `json:"VDiskId"` VDiskID any `json:"VDiskId"`
VFlashCacheConfigInfo any `json:"VFlashCacheConfigInfo"` VFlashCacheConfigInfo any `json:"VFlashCacheConfigInfo"`
} `json:"Device,omitempty"` } `json:"Device"`
FileOperation string `json:"FileOperation"` FileOperation string `json:"FileOperation"`
Operation string `json:"Operation"` Operation string `json:"Operation"`
Profile []struct { Profile []struct {