diff --git a/internal/vcenter/vcenter.go b/internal/vcenter/vcenter.go index e61a766..4797917 100644 --- a/internal/vcenter/vcenter.go +++ b/internal/vcenter/vcenter.go @@ -3,7 +3,6 @@ package vcenter import ( "context" "fmt" - "log" "log/slog" "net/url" "path" @@ -58,10 +57,19 @@ func New(logger *slog.Logger, creds *VcenterLogin) *Vcenter { } func (v *Vcenter) Login(vUrl string) error { + if v == nil { + return fmt.Errorf("vcenter is nil") + } + if strings.TrimSpace(vUrl) == "" { + return fmt.Errorf("vcenter URL is empty") + } + if v.credentials == nil { + return fmt.Errorf("vcenter credentials are nil") + } // Connect to vCenter u, err := soap.ParseURL(vUrl) if err != nil { - log.Fatalf("Error parsing vCenter URL: %s", err) + return fmt.Errorf("error parsing vCenter URL: %w", err) } v.Vurl = vUrl diff --git a/server/handler/snapshotForceHourly.go b/server/handler/snapshotForceHourly.go new file mode 100644 index 0000000..8c78ce5 --- /dev/null +++ b/server/handler/snapshotForceHourly.go @@ -0,0 +1,51 @@ +package handler + +import ( + "context" + "encoding/json" + "net/http" + "strings" + "time" + "vctp/internal/tasks" +) + +// SnapshotForceHourly triggers an on-demand hourly snapshot run. +// @Summary Trigger hourly snapshot (manual) +// @Description Manually trigger an hourly snapshot for all configured vCenters. Requires confirmation text to avoid accidental execution. +// @Tags snapshots +// @Accept json +// @Produce json +// @Param confirm query string true "Confirmation text; must be 'FORCE'" +// @Success 200 {object} map[string]string "Snapshot started" +// @Failure 400 {object} map[string]string "Invalid request" +// @Failure 500 {object} map[string]string "Server error" +// @Router /api/snapshots/hourly/force [post] +func (h *Handler) SnapshotForceHourly(w http.ResponseWriter, r *http.Request) { + confirm := strings.TrimSpace(r.URL.Query().Get("confirm")) + if strings.ToUpper(confirm) != "FORCE" { + writeJSONError(w, http.StatusBadRequest, "confirm must be 'FORCE'") + return + } + + ctx := context.Background() + ct := &tasks.CronTask{ + Logger: h.Logger, + Database: h.Database, + Settings: h.Settings, + VcCreds: h.VcCreds, + } + + started := time.Now() + h.Logger.Info("Manual hourly snapshot requested") + if err := ct.RunVcenterSnapshotHourly(ctx, h.Logger.With("manual", true)); err != nil { + h.Logger.Error("Manual hourly snapshot failed", "error", err) + writeJSONError(w, http.StatusInternalServerError, err.Error()) + return + } + + h.Logger.Info("Manual hourly snapshot completed", "duration", time.Since(started)) + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{ + "status": "OK", + }) +} diff --git a/server/router/docs/docs.go b/server/router/docs/docs.go index a36075e..8ade03d 100644 --- a/server/router/docs/docs.go +++ b/server/router/docs/docs.go @@ -647,6 +647,59 @@ const docTemplate = `{ } } }, + "/api/snapshots/hourly/force": { + "post": { + "description": "Manually trigger an hourly snapshot for all configured vCenters. Requires confirmation text to avoid accidental execution.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "snapshots" + ], + "summary": "Trigger hourly snapshot (manual)", + "parameters": [ + { + "type": "string", + "description": "Confirmation text; must be 'FORCE'", + "name": "confirm", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "Snapshot started", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "Invalid request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Server error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, "/api/snapshots/migrate": { "post": { "description": "Rebuilds the snapshot registry from existing tables and renames hourly tables to epoch-based names.", diff --git a/server/router/docs/swagger.json b/server/router/docs/swagger.json index 9b5c3b3..5ea2f1f 100644 --- a/server/router/docs/swagger.json +++ b/server/router/docs/swagger.json @@ -636,6 +636,59 @@ } } }, + "/api/snapshots/hourly/force": { + "post": { + "description": "Manually trigger an hourly snapshot for all configured vCenters. Requires confirmation text to avoid accidental execution.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "snapshots" + ], + "summary": "Trigger hourly snapshot (manual)", + "parameters": [ + { + "type": "string", + "description": "Confirmation text; must be 'FORCE'", + "name": "confirm", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "Snapshot started", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "Invalid request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Server error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, "/api/snapshots/migrate": { "post": { "description": "Rebuilds the snapshot registry from existing tables and renames hourly tables to epoch-based names.", diff --git a/server/router/docs/swagger.yaml b/server/router/docs/swagger.yaml index 33017ab..91dd989 100644 --- a/server/router/docs/swagger.yaml +++ b/server/router/docs/swagger.yaml @@ -580,6 +580,42 @@ paths: summary: Force snapshot aggregation tags: - snapshots + /api/snapshots/hourly/force: + post: + consumes: + - application/json + description: Manually trigger an hourly snapshot for all configured vCenters. + Requires confirmation text to avoid accidental execution. + parameters: + - description: Confirmation text; must be 'FORCE' + in: query + name: confirm + required: true + type: string + produces: + - application/json + responses: + "200": + description: Snapshot started + schema: + additionalProperties: + type: string + type: object + "400": + description: Invalid request + schema: + additionalProperties: + type: string + type: object + "500": + description: Server error + schema: + additionalProperties: + type: string + type: object + summary: Trigger hourly snapshot (manual) + tags: + - snapshots /api/snapshots/migrate: post: description: Rebuilds the snapshot registry from existing tables and renames diff --git a/server/router/router.go b/server/router/router.go index 61b5f00..ba34ac6 100644 --- a/server/router/router.go +++ b/server/router/router.go @@ -63,6 +63,7 @@ func New(logger *slog.Logger, database db.Database, buildTime string, sha1ver st mux.HandleFunc("/api/report/updates", h.UpdateReportDownload) mux.HandleFunc("/api/report/snapshot", h.SnapshotReportDownload) mux.HandleFunc("/api/snapshots/aggregate", h.SnapshotAggregateForce) + mux.HandleFunc("/api/snapshots/hourly/force", h.SnapshotForceHourly) mux.HandleFunc("/api/snapshots/migrate", h.SnapshotMigrate) mux.HandleFunc("/api/snapshots/regenerate-hourly-reports", h.SnapshotRegenerateHourlyReports) mux.HandleFunc("/metrics", h.Metrics)