[ci skip] more suggested improvements
This commit is contained in:
@@ -34,9 +34,9 @@ steps:
|
||||
path: /shared
|
||||
commands:
|
||||
- export PATH=/drone/src/pkg.tools:$PATH
|
||||
- go install github.com/a-h/templ/cmd/templ@latest
|
||||
- go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
|
||||
- go install github.com/swaggo/swag/cmd/swag@latest
|
||||
- go install github.com/a-h/templ/cmd/templ@v0.3.977
|
||||
- go install github.com/sqlc-dev/sqlc/cmd/sqlc@v1.29.0
|
||||
- go install github.com/swaggo/swag/cmd/swag@v1.16.6
|
||||
# - go install github.com/goreleaser/nfpm/v2/cmd/nfpm@latest
|
||||
- sqlc generate
|
||||
- templ generate -path ./components
|
||||
|
||||
55
README.md
55
README.md
@@ -86,6 +86,40 @@ Hourly and daily snapshot table retention can be configured in the settings file
|
||||
- `settings.hourly_snapshot_max_age_days` (default: 60)
|
||||
- `settings.daily_snapshot_max_age_months` (default: 12)
|
||||
|
||||
## Runtime Environment Flags
|
||||
These flags are read from the process environment (for example via `/etc/default/vctp` on systemd installs):
|
||||
|
||||
- `VCTP_ENCRYPTION_KEY`: optional explicit key source for credential encryption/decryption.
|
||||
Recommended for stable behavior across host migrations/rebuilds.
|
||||
- `VCTP_ENABLE_EXPERIMENTAL_POSTGRES`: set to `1` to enable experimental PostgreSQL driver startup.
|
||||
- `VCTP_ENABLE_LEGACY_API`: set to `1` to re-enable deprecated legacy API endpoints temporarily.
|
||||
|
||||
## Credential Encryption Lifecycle
|
||||
At startup, vCTP resolves `settings.vcenter_password` using this order:
|
||||
|
||||
1. If value starts with `enc:v1:`, decrypt using the active key.
|
||||
2. If no prefix, attempt legacy ciphertext decryption (active key, then legacy fallback keys).
|
||||
3. If decrypt fails and value length is greater than 2, treat value as plaintext.
|
||||
|
||||
When steps 2 or 3 succeed, vCTP rewrites the setting in-place to `enc:v1:<ciphertext>`.
|
||||
|
||||
Behavior notes:
|
||||
- Plaintext values with length `<= 2` are rejected.
|
||||
- Malformed ciphertext is rejected safely (short payloads do not panic).
|
||||
- Legacy encrypted values can still be migrated forward automatically.
|
||||
|
||||
## Deprecated API Endpoints
|
||||
These endpoints are considered legacy and are disabled by default unless `VCTP_ENABLE_LEGACY_API=1`:
|
||||
|
||||
- `/api/event/vm/create`
|
||||
- `/api/event/vm/modify`
|
||||
- `/api/event/vm/move`
|
||||
- `/api/event/vm/delete`
|
||||
- `/api/cleanup/updates`
|
||||
- `/api/cleanup/vcenter`
|
||||
|
||||
When disabled, they return HTTP `410 Gone` with JSON error payload.
|
||||
|
||||
## Settings Reference
|
||||
All configuration lives under the top-level `settings:` key in `vctp.yml`.
|
||||
|
||||
@@ -94,7 +128,7 @@ General:
|
||||
- `settings.log_output`: log format, `text` or `json`
|
||||
|
||||
Database:
|
||||
- `settings.database_driver`: `sqlite` or `postgres`
|
||||
- `settings.database_driver`: `sqlite` or `postgres` (experimental; requires `VCTP_ENABLE_EXPERIMENTAL_POSTGRES=1`)
|
||||
- `settings.database_url`: SQLite file path/DSN or PostgreSQL DSN
|
||||
|
||||
HTTP/TLS:
|
||||
@@ -138,9 +172,9 @@ Filters/chargeback:
|
||||
## Pre-requisite tools
|
||||
|
||||
```shell
|
||||
go install github.com/a-h/templ/cmd/templ@latest
|
||||
go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
|
||||
go install github.com/swaggo/swag/cmd/swag@latest
|
||||
go install github.com/a-h/templ/cmd/templ@v0.3.977
|
||||
go install github.com/sqlc-dev/sqlc/cmd/sqlc@v1.29.0
|
||||
go install github.com/swaggo/swag/cmd/swag@v1.16.6
|
||||
```
|
||||
|
||||
## Database
|
||||
@@ -163,6 +197,19 @@ Run `templ generate -path ./components` to generate code based on template files
|
||||
## Documentation
|
||||
Run `swag init --exclude "pkg.mod,pkg.build,pkg.tools" -o server/router/docs`
|
||||
|
||||
## Tests
|
||||
Run the test suite:
|
||||
|
||||
```shell
|
||||
go test ./...
|
||||
```
|
||||
|
||||
Recommended static analysis:
|
||||
|
||||
```shell
|
||||
go vet ./...
|
||||
```
|
||||
|
||||
## CI/CD (Drone)
|
||||
- `.drone.yml` defines a Docker pipeline:
|
||||
- Restore/build caches for Go modules/tools.
|
||||
|
||||
@@ -3,7 +3,6 @@ package handler
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
@@ -186,20 +185,18 @@ LIMIT %d
|
||||
}
|
||||
|
||||
response := models.DailyCreationDiagnosticsResponse{
|
||||
Status: "OK",
|
||||
Date: parsed.Format("2006-01-02"),
|
||||
Table: tableName,
|
||||
TotalRows: totalRows,
|
||||
MissingCreationCount: missingTotal,
|
||||
MissingCreationPct: missingPct,
|
||||
AvgIsPresentLtOneCount: avgIsPresentLtOne,
|
||||
MissingCreationPartialCount: missingPartialCount,
|
||||
MissingByVcenter: byVcenter,
|
||||
Samples: samples,
|
||||
Status: "OK",
|
||||
Date: parsed.Format("2006-01-02"),
|
||||
Table: tableName,
|
||||
TotalRows: totalRows,
|
||||
MissingCreationCount: missingTotal,
|
||||
MissingCreationPct: missingPct,
|
||||
AvgIsPresentLtOneCount: avgIsPresentLtOne,
|
||||
MissingCreationPartialCount: missingPartialCount,
|
||||
MissingByVcenter: byVcenter,
|
||||
Samples: samples,
|
||||
MissingCreationPartialSamples: partialSamples,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(response)
|
||||
writeJSON(w, http.StatusOK, response)
|
||||
}
|
||||
|
||||
@@ -57,9 +57,7 @@ func (h *Handler) EncryptData(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
h.Logger.Debug("encrypted plaintext payload", "input_length", len(plaintext))
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{
|
||||
writeJSON(w, http.StatusOK, map[string]string{
|
||||
"status": "OK",
|
||||
"message": cipherText,
|
||||
"prefixed": encryptedValuePrefixV1 + cipherText,
|
||||
|
||||
135
server/handler/encryptData_test.go
Normal file
135
server/handler/encryptData_test.go
Normal file
@@ -0,0 +1,135 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"vctp/internal/secrets"
|
||||
)
|
||||
|
||||
func newEncryptTestHandler() (*Handler, *secrets.Secrets) {
|
||||
logger := newTestLogger()
|
||||
key := []byte("0123456789abcdef0123456789abcdef")
|
||||
secret := secrets.New(logger, key)
|
||||
return &Handler{
|
||||
Logger: logger,
|
||||
Secret: secret,
|
||||
}, secret
|
||||
}
|
||||
|
||||
func decodeResponse(t *testing.T, rr *httptest.ResponseRecorder) map[string]string {
|
||||
t.Helper()
|
||||
var resp map[string]string
|
||||
if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("failed to decode response body %q: %v", rr.Body.String(), err)
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
func TestEncryptDataRejectsWrongMethod(t *testing.T) {
|
||||
h, _ := newEncryptTestHandler()
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/encrypt", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
h.EncryptData(rr, req)
|
||||
|
||||
if rr.Code != http.StatusMethodNotAllowed {
|
||||
t.Fatalf("expected %d, got %d", http.StatusMethodNotAllowed, rr.Code)
|
||||
}
|
||||
resp := decodeResponse(t, rr)
|
||||
if resp["status"] != "ERROR" {
|
||||
t.Fatalf("expected status ERROR, got %#v", resp)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncryptDataRejectsInvalidJSON(t *testing.T) {
|
||||
h, _ := newEncryptTestHandler()
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/encrypt", strings.NewReader("{"))
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
h.EncryptData(rr, req)
|
||||
|
||||
if rr.Code != http.StatusBadRequest {
|
||||
t.Fatalf("expected %d, got %d", http.StatusBadRequest, rr.Code)
|
||||
}
|
||||
resp := decodeResponse(t, rr)
|
||||
if resp["status"] != "ERROR" {
|
||||
t.Fatalf("expected status ERROR, got %#v", resp)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncryptDataAcceptsPlaintextField(t *testing.T) {
|
||||
h, secret := newEncryptTestHandler()
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/encrypt", strings.NewReader(`{"plaintext":"super-secret"}`))
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
h.EncryptData(rr, req)
|
||||
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("expected %d, got %d", http.StatusOK, rr.Code)
|
||||
}
|
||||
resp := decodeResponse(t, rr)
|
||||
if resp["status"] != "OK" {
|
||||
t.Fatalf("expected status OK, got %#v", resp)
|
||||
}
|
||||
if resp["ciphertext"] == "" || resp["prefixed"] == "" {
|
||||
t.Fatalf("expected ciphertext+prefixed fields, got %#v", resp)
|
||||
}
|
||||
if !strings.HasPrefix(resp["prefixed"], encryptedValuePrefixV1) {
|
||||
t.Fatalf("expected prefixed value with %q, got %q", encryptedValuePrefixV1, resp["prefixed"])
|
||||
}
|
||||
if !strings.EqualFold(resp["message"], resp["ciphertext"]) {
|
||||
t.Fatalf("expected message to mirror ciphertext, got %#v", resp)
|
||||
}
|
||||
plain, err := secret.Decrypt(resp["ciphertext"])
|
||||
if err != nil {
|
||||
t.Fatalf("unable to decrypt ciphertext response: %v", err)
|
||||
}
|
||||
if string(plain) != "super-secret" {
|
||||
t.Fatalf("unexpected decrypted value %q", string(plain))
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncryptDataAcceptsLegacyValueField(t *testing.T) {
|
||||
h, secret := newEncryptTestHandler()
|
||||
body := bytes.NewBufferString(`{"value":"legacy-input"}`)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/encrypt", body)
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
h.EncryptData(rr, req)
|
||||
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("expected %d, got %d", http.StatusOK, rr.Code)
|
||||
}
|
||||
resp := decodeResponse(t, rr)
|
||||
cipherText := resp["ciphertext"]
|
||||
if cipherText == "" {
|
||||
t.Fatalf("expected ciphertext in response, got %#v", resp)
|
||||
}
|
||||
plain, err := secret.Decrypt(cipherText)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to decrypt ciphertext response: %v", err)
|
||||
}
|
||||
if string(plain) != "legacy-input" {
|
||||
t.Fatalf("unexpected decrypted value %q", string(plain))
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncryptDataRejectsMissingPayloadValue(t *testing.T) {
|
||||
h, _ := newEncryptTestHandler()
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/encrypt", strings.NewReader(`{}`))
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
h.EncryptData(rr, req)
|
||||
|
||||
if rr.Code != http.StatusBadRequest {
|
||||
t.Fatalf("expected %d, got %d", http.StatusBadRequest, rr.Code)
|
||||
}
|
||||
resp := decodeResponse(t, rr)
|
||||
if resp["status"] != "ERROR" {
|
||||
t.Fatalf("expected status ERROR, got %#v", resp)
|
||||
}
|
||||
}
|
||||
63
server/handler/legacy_gate_test.go
Normal file
63
server/handler/legacy_gate_test.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDenyLegacyAPIDisabledByDefault(t *testing.T) {
|
||||
t.Setenv(legacyAPIEnvVar, "")
|
||||
h := &Handler{Logger: newTestLogger()}
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
denied := h.denyLegacyAPI(rr, "/api/event/vm/create")
|
||||
if !denied {
|
||||
t.Fatal("expected legacy API to be denied by default")
|
||||
}
|
||||
if rr.Code != http.StatusGone {
|
||||
t.Fatalf("expected %d, got %d", http.StatusGone, rr.Code)
|
||||
}
|
||||
if !strings.Contains(rr.Body.String(), "deprecated") {
|
||||
t.Fatalf("unexpected response body: %s", rr.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDenyLegacyAPIEnabledViaEnv(t *testing.T) {
|
||||
t.Setenv(legacyAPIEnvVar, "1")
|
||||
h := &Handler{Logger: newTestLogger()}
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
denied := h.denyLegacyAPI(rr, "/api/event/vm/create")
|
||||
if denied {
|
||||
t.Fatal("expected legacy API to be allowed when env var is set")
|
||||
}
|
||||
if rr.Body.Len() != 0 {
|
||||
t.Fatalf("expected no response body write, got: %s", rr.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestVmCreateEventHonorsLegacyGate(t *testing.T) {
|
||||
h := &Handler{Logger: newTestLogger()}
|
||||
|
||||
t.Run("disabled", func(t *testing.T) {
|
||||
t.Setenv(legacyAPIEnvVar, "")
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/event/vm/create", strings.NewReader("{invalid"))
|
||||
rr := httptest.NewRecorder()
|
||||
h.VmCreateEvent(rr, req)
|
||||
if rr.Code != http.StatusGone {
|
||||
t.Fatalf("expected %d, got %d", http.StatusGone, rr.Code)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("enabled", func(t *testing.T) {
|
||||
t.Setenv(legacyAPIEnvVar, "1")
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/event/vm/create", strings.NewReader("{invalid"))
|
||||
rr := httptest.NewRecorder()
|
||||
h.VmCreateEvent(rr, req)
|
||||
if rr.Code != http.StatusBadRequest {
|
||||
t.Fatalf("expected %d when gate is open, got %d", http.StatusBadRequest, rr.Code)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"vctp/internal/report"
|
||||
@@ -24,12 +23,7 @@ func (h *Handler) InventoryReportDownload(w http.ResponseWriter, r *http.Request
|
||||
reportData, err := report.CreateInventoryReport(h.Logger, h.Database, ctx)
|
||||
if err != nil {
|
||||
h.Logger.Error("Failed to create report", "error", err)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"status": "ERROR",
|
||||
"message": fmt.Sprintf("Unable to create xlsx report: '%s'", err),
|
||||
})
|
||||
writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("Unable to create xlsx report: '%s'", err))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -58,12 +52,7 @@ func (h *Handler) UpdateReportDownload(w http.ResponseWriter, r *http.Request) {
|
||||
reportData, err := report.CreateUpdatesReport(h.Logger, h.Database, ctx)
|
||||
if err != nil {
|
||||
h.Logger.Error("Failed to create report", "error", err)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"status": "ERROR",
|
||||
"message": fmt.Sprintf("Unable to create xlsx report: '%s'", err),
|
||||
})
|
||||
writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("Unable to create xlsx report: '%s'", err))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
41
server/handler/response.go
Normal file
41
server/handler/response.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"vctp/server/models"
|
||||
)
|
||||
|
||||
func writeJSON(w http.ResponseWriter, statusCode int, payload any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(statusCode)
|
||||
_ = json.NewEncoder(w).Encode(payload)
|
||||
}
|
||||
|
||||
func writeJSONStatus(w http.ResponseWriter, statusCode int, status string) {
|
||||
writeJSON(w, statusCode, models.StatusResponse{
|
||||
Status: status,
|
||||
})
|
||||
}
|
||||
|
||||
func writeJSONStatusMessage(w http.ResponseWriter, statusCode int, status, message string) {
|
||||
writeJSON(w, statusCode, models.StatusMessageResponse{
|
||||
Status: status,
|
||||
Message: message,
|
||||
})
|
||||
}
|
||||
|
||||
func writeJSONOK(w http.ResponseWriter) {
|
||||
writeJSONStatus(w, http.StatusOK, "OK")
|
||||
}
|
||||
|
||||
func writeJSONOKMessage(w http.ResponseWriter, message string) {
|
||||
writeJSONStatusMessage(w, http.StatusOK, "OK", message)
|
||||
}
|
||||
|
||||
func writeJSONError(w http.ResponseWriter, statusCode int, message string) {
|
||||
writeJSON(w, statusCode, models.ErrorResponse{
|
||||
Status: "ERROR",
|
||||
Message: message,
|
||||
})
|
||||
}
|
||||
@@ -2,13 +2,11 @@ package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
"vctp/internal/settings"
|
||||
"vctp/internal/tasks"
|
||||
"vctp/server/models"
|
||||
)
|
||||
|
||||
// SnapshotAggregateForce forces regeneration of a daily or monthly summary table.
|
||||
@@ -102,18 +100,5 @@ func (h *Handler) SnapshotAggregateForce(w http.ResponseWriter, r *http.Request)
|
||||
"granularity", granularity,
|
||||
"duration", time.Since(startedAt),
|
||||
)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"status": "OK",
|
||||
})
|
||||
}
|
||||
|
||||
func writeJSONError(w http.ResponseWriter, status int, message string) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
json.NewEncoder(w).Encode(models.ErrorResponse{
|
||||
Status: "ERROR",
|
||||
Message: message,
|
||||
})
|
||||
writeJSONOK(w)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -44,8 +43,5 @@ func (h *Handler) SnapshotForceHourly(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
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",
|
||||
})
|
||||
writeJSONOK(w)
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@ package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"vctp/internal/report"
|
||||
"vctp/server/models"
|
||||
)
|
||||
|
||||
// SnapshotMigrate rebuilds the snapshot registry and normalizes hourly table names.
|
||||
@@ -19,20 +19,28 @@ func (h *Handler) SnapshotMigrate(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.Background()
|
||||
stats, err := report.MigrateSnapshotRegistry(ctx, h.Database)
|
||||
if err != nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "ERROR",
|
||||
"error": err.Error(),
|
||||
"stats": stats,
|
||||
writeJSON(w, http.StatusInternalServerError, models.SnapshotMigrationResponse{
|
||||
Status: "ERROR",
|
||||
Error: err.Error(),
|
||||
Stats: models.SnapshotMigrationStats{
|
||||
HourlyRenamed: stats.HourlyRenamed,
|
||||
HourlyRegistered: stats.HourlyRegistered,
|
||||
DailyRegistered: stats.DailyRegistered,
|
||||
MonthlyRegistered: stats.MonthlyRegistered,
|
||||
Errors: stats.Errors,
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "OK",
|
||||
"stats": stats,
|
||||
writeJSON(w, http.StatusOK, models.SnapshotMigrationResponse{
|
||||
Status: "OK",
|
||||
Stats: models.SnapshotMigrationStats{
|
||||
HourlyRenamed: stats.HourlyRenamed,
|
||||
HourlyRegistered: stats.HourlyRegistered,
|
||||
DailyRegistered: stats.DailyRegistered,
|
||||
MonthlyRegistered: stats.MonthlyRegistered,
|
||||
Errors: stats.Errors,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
"vctp/internal/report"
|
||||
"vctp/server/models"
|
||||
)
|
||||
|
||||
// SnapshotRegenerateHourlyReports regenerates missing hourly snapshot XLSX reports on disk.
|
||||
@@ -54,15 +54,14 @@ func (h *Handler) SnapshotRegenerateHourlyReports(w http.ResponseWriter, r *http
|
||||
regenerated++
|
||||
}
|
||||
|
||||
resp := map[string]interface{}{
|
||||
"status": "OK",
|
||||
"total": len(records),
|
||||
"regenerated": regenerated,
|
||||
"skipped": skipped,
|
||||
"errors": errors,
|
||||
"reports_dir": reportsDir,
|
||||
"snapshotType": "hourly",
|
||||
resp := models.SnapshotRegenerateReportsResponse{
|
||||
Status: "OK",
|
||||
Total: len(records),
|
||||
Regenerated: regenerated,
|
||||
Skipped: skipped,
|
||||
Errors: errors,
|
||||
ReportsDir: reportsDir,
|
||||
SnapshotType: "hourly",
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
writeJSON(w, http.StatusOK, resp)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
@@ -10,6 +9,7 @@ import (
|
||||
"time"
|
||||
"vctp/db"
|
||||
"vctp/internal/report"
|
||||
"vctp/server/models"
|
||||
)
|
||||
|
||||
// SnapshotRepair scans existing daily summaries and backfills missing SnapshotTime and lifecycle fields.
|
||||
@@ -21,20 +21,18 @@ import (
|
||||
// @Router /api/snapshots/repair [post]
|
||||
func (h *Handler) SnapshotRepair(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
writeJSONError(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||
return
|
||||
}
|
||||
h.Logger.Info("snapshot repair started", "scope", "daily")
|
||||
repaired, failed := h.repairDailySummaries(r.Context(), time.Now())
|
||||
h.Logger.Info("snapshot repair finished", "daily_repaired", repaired, "daily_failed", failed)
|
||||
|
||||
resp := map[string]string{
|
||||
"status": "ok",
|
||||
"repaired": strconv.Itoa(repaired),
|
||||
"failed": strconv.Itoa(failed),
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(resp)
|
||||
writeJSON(w, http.StatusOK, models.SnapshotRepairResponse{
|
||||
Status: "OK",
|
||||
Repaired: strconv.Itoa(repaired),
|
||||
Failed: strconv.Itoa(failed),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) repairDailySummaries(ctx context.Context, now time.Time) (repaired int, failed int) {
|
||||
@@ -42,8 +40,8 @@ func (h *Handler) repairDailySummaries(ctx context.Context, now time.Time) (repa
|
||||
|
||||
dailyRecs, err := report.SnapshotRecordsWithFallback(ctx, h.Database, "daily", "inventory_daily_summary_", "20060102", time.Time{}, now)
|
||||
if err != nil {
|
||||
h.Logger.Warn("failed to list daily summaries", "error", err)
|
||||
return 0, 1
|
||||
h.Logger.Warn("failed to list daily summaries", "error", err)
|
||||
return 0, 1
|
||||
}
|
||||
|
||||
for _, rec := range dailyRecs {
|
||||
@@ -53,27 +51,27 @@ func (h *Handler) repairDailySummaries(ctx context.Context, now time.Time) (repa
|
||||
|
||||
if err := db.EnsureSummaryTable(ctx, dbConn, rec.TableName); err != nil {
|
||||
h.Logger.Warn("ensure summary table failed", "table", rec.TableName, "error", err)
|
||||
failed++
|
||||
continue
|
||||
}
|
||||
failed++
|
||||
continue
|
||||
}
|
||||
|
||||
hourlyRecs, err := report.SnapshotRecordsWithFallback(ctx, h.Database, "hourly", "inventory_hourly_", "epoch", dayStart, dayEnd)
|
||||
if err != nil || len(hourlyRecs) == 0 {
|
||||
h.Logger.Warn("no hourly snapshots for repair window", "table", rec.TableName, "error", err)
|
||||
failed++
|
||||
continue
|
||||
}
|
||||
hourlyRecs, err := report.SnapshotRecordsWithFallback(ctx, h.Database, "hourly", "inventory_hourly_", "epoch", dayStart, dayEnd)
|
||||
if err != nil || len(hourlyRecs) == 0 {
|
||||
h.Logger.Warn("no hourly snapshots for repair window", "table", rec.TableName, "error", err)
|
||||
failed++
|
||||
continue
|
||||
}
|
||||
|
||||
cols := []string{
|
||||
`"InventoryId"`, `"Name"`, `"Vcenter"`, `"VmId"`, `"EventKey"`, `"CloudId"`, `"CreationTime"`,
|
||||
`"DeletionTime"`, `"ResourcePool"`, `"Datacenter"`, `"Cluster"`, `"Folder"`,
|
||||
`"ProvisionedDisk"`, `"VcpuCount"`, `"RamGB"`, `"IsTemplate"`, `"PoweredOn"`,
|
||||
`"SrmPlaceholder"`, `"VmUuid"`, `"SnapshotTime"`,
|
||||
}
|
||||
union, err := buildUnionFromRecords(hourlyRecs, cols, `COALESCE(CAST("IsTemplate" AS TEXT), '') NOT IN ('TRUE','true','1')`)
|
||||
if err != nil {
|
||||
h.Logger.Warn("failed to build union for repair", "table", rec.TableName, "error", err)
|
||||
failed++
|
||||
cols := []string{
|
||||
`"InventoryId"`, `"Name"`, `"Vcenter"`, `"VmId"`, `"EventKey"`, `"CloudId"`, `"CreationTime"`,
|
||||
`"DeletionTime"`, `"ResourcePool"`, `"Datacenter"`, `"Cluster"`, `"Folder"`,
|
||||
`"ProvisionedDisk"`, `"VcpuCount"`, `"RamGB"`, `"IsTemplate"`, `"PoweredOn"`,
|
||||
`"SrmPlaceholder"`, `"VmUuid"`, `"SnapshotTime"`,
|
||||
}
|
||||
union, err := buildUnionFromRecords(hourlyRecs, cols, `COALESCE(CAST("IsTemplate" AS TEXT), '') NOT IN ('TRUE','true','1')`)
|
||||
if err != nil {
|
||||
h.Logger.Warn("failed to build union for repair", "table", rec.TableName, "error", err)
|
||||
failed++
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -106,7 +104,7 @@ func (h *Handler) repairDailySummaries(ctx context.Context, now time.Time) (repa
|
||||
// @Router /api/snapshots/repair/all [post]
|
||||
func (h *Handler) SnapshotRepairSuite(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
writeJSONError(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -135,15 +133,13 @@ func (h *Handler) SnapshotRepairSuite(w http.ResponseWriter, r *http.Request) {
|
||||
h.Logger.Info("repair suite step", "step", "monthly_refine")
|
||||
monthlyRefined, monthlyFailed := h.refineMonthlyFromDaily(ctx, time.Now())
|
||||
|
||||
resp := map[string]string{
|
||||
"status": "ok",
|
||||
"daily_repaired": strconv.Itoa(dailyRepaired),
|
||||
"daily_failed": strconv.Itoa(dailyFailed),
|
||||
"monthly_refined": strconv.Itoa(monthlyRefined),
|
||||
"monthly_failed": strconv.Itoa(monthlyFailed),
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(resp)
|
||||
writeJSON(w, http.StatusOK, models.SnapshotRepairSuiteResponse{
|
||||
Status: "OK",
|
||||
DailyRepaired: strconv.Itoa(dailyRepaired),
|
||||
DailyFailed: strconv.Itoa(dailyFailed),
|
||||
MonthlyRefined: strconv.Itoa(monthlyRefined),
|
||||
MonthlyFailed: strconv.Itoa(monthlyFailed),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) refineMonthlyFromDaily(ctx context.Context, now time.Time) (refined int, failed int) {
|
||||
@@ -178,11 +174,11 @@ func (h *Handler) refineMonthlyFromDaily(ctx context.Context, now time.Time) (re
|
||||
continue
|
||||
}
|
||||
|
||||
union, err := buildUnionFromRecords(recs, cols, `COALESCE(CAST("IsTemplate" AS TEXT), '') NOT IN ('TRUE','true','1')`)
|
||||
if err != nil {
|
||||
h.Logger.Warn("failed to build union for monthly refine", "table", summaryTable, "error", err)
|
||||
failed++
|
||||
continue
|
||||
union, err := buildUnionFromRecords(recs, cols, `COALESCE(CAST("IsTemplate" AS TEXT), '') NOT IN ('TRUE','true','1')`)
|
||||
if err != nil {
|
||||
h.Logger.Warn("failed to build union for monthly refine", "table", summaryTable, "error", err)
|
||||
failed++
|
||||
continue
|
||||
}
|
||||
|
||||
if err := db.RefineCreationDeletionFromUnion(ctx, dbConn, summaryTable, union); err != nil {
|
||||
|
||||
@@ -2,7 +2,6 @@ package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -62,24 +61,14 @@ func (h *Handler) SnapshotReportDownload(w http.ResponseWriter, r *http.Request)
|
||||
ctx := context.Background()
|
||||
tableName := r.URL.Query().Get("table")
|
||||
if tableName == "" {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"status": "ERROR",
|
||||
"message": "Missing table parameter",
|
||||
})
|
||||
writeJSONError(w, http.StatusBadRequest, "Missing table parameter")
|
||||
return
|
||||
}
|
||||
|
||||
reportData, err := report.CreateTableReport(h.Logger, h.Database, ctx, tableName)
|
||||
if err != nil {
|
||||
h.Logger.Error("Failed to create snapshot report", "error", err, "table", tableName)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"status": "ERROR",
|
||||
"message": fmt.Sprintf("Unable to create snapshot report: '%s'", err),
|
||||
})
|
||||
writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("Unable to create snapshot report: '%s'", err))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
10
server/handler/test_helpers_test.go
Normal file
10
server/handler/test_helpers_test.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
func newTestLogger() *slog.Logger {
|
||||
return slog.New(slog.NewTextHandler(io.Discard, nil))
|
||||
}
|
||||
@@ -43,12 +43,9 @@ func (h *Handler) UpdateCleanup(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if err != nil {
|
||||
h.Logger.Error("Error received cleaning updates table", "error", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
fmt.Fprintf(w, "Delete Request unsuccessful %s\n", err)
|
||||
writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("Delete Request unsuccessful %s", err))
|
||||
} else {
|
||||
h.Logger.Debug("Processed update cleanup successfully")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
// TODO - return some JSON
|
||||
fmt.Fprintf(w, "Processed update cleanup successfully")
|
||||
writeJSONOKMessage(w, "Processed update cleanup successfully")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package handler
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
@@ -35,21 +34,11 @@ func (h *Handler) VcCleanup(w http.ResponseWriter, r *http.Request) {
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
h.Logger.Error("No VMs found for vcenter", "url", vcUrl)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"status": "ERROR",
|
||||
"message": fmt.Sprintf("No match to vcenter details specified. vc_url: '%s'", vcUrl),
|
||||
})
|
||||
writeJSONError(w, http.StatusBadRequest, fmt.Sprintf("No match to vcenter details specified. vc_url: '%s'", vcUrl))
|
||||
return
|
||||
} else {
|
||||
h.Logger.Error("Error checking for vcenter to cleanup", "error", err, "url", vcUrl)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"status": "ERROR",
|
||||
"message": fmt.Sprintf("Error checking for vcenter to cleanup. error: '%s'", err),
|
||||
})
|
||||
writeJSONError(w, http.StatusBadRequest, fmt.Sprintf("Error checking for vcenter to cleanup. error: '%s'", err))
|
||||
return
|
||||
}
|
||||
} else {
|
||||
@@ -57,33 +46,18 @@ func (h *Handler) VcCleanup(w http.ResponseWriter, r *http.Request) {
|
||||
err = h.Database.Queries().InventoryCleanupVcenter(ctx, vcUrl)
|
||||
if err != nil {
|
||||
h.Logger.Error("Error cleaning up VMs from Inventory table", "error", err, "url", vcUrl)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"status": "ERROR",
|
||||
"message": fmt.Sprintf("Error cleaning up VMs from Inventory table. error: '%s'", err),
|
||||
})
|
||||
writeJSONError(w, http.StatusBadRequest, fmt.Sprintf("Error cleaning up VMs from Inventory table. error: '%s'", err))
|
||||
return
|
||||
} else {
|
||||
// Successful cleanup
|
||||
h.Logger.Debug("VMs successfully removed from inventory for vcenter", "url", vcUrl)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"status": "OK",
|
||||
"message": fmt.Sprintf("Removed VMs from Inventory table for vcenter '%s'", vcUrl),
|
||||
})
|
||||
writeJSONOKMessage(w, fmt.Sprintf("Removed VMs from Inventory table for vcenter '%s'", vcUrl))
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
h.Logger.Error("Parameters not correctly specified", "url", vcUrl)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"status": "ERROR",
|
||||
"message": fmt.Sprintf("Parameters not correctly specified. vc_url: '%s'", vcUrl),
|
||||
})
|
||||
writeJSONError(w, http.StatusBadRequest, fmt.Sprintf("Parameters not correctly specified. vc_url: '%s'", vcUrl))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package handler
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
@@ -38,21 +37,11 @@ func (h *Handler) VmCleanup(w http.ResponseWriter, r *http.Request) {
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
h.Logger.Error("No VM found matching parameters", "vm_id", vmId, "datacenter_name", datacenterName)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"status": "ERROR",
|
||||
"message": fmt.Sprintf("No match to VM details specified. vm_id: '%s', datacenter_name: '%s'", vmId, datacenterName),
|
||||
})
|
||||
writeJSONError(w, http.StatusBadRequest, fmt.Sprintf("No match to VM details specified. vm_id: '%s', datacenter_name: '%s'", vmId, datacenterName))
|
||||
return
|
||||
} else {
|
||||
h.Logger.Error("Error checking for VM to cleanup", "error", err, "vm_id", vmId, "datacenter_name", datacenterName)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"status": "ERROR",
|
||||
"message": fmt.Sprintf("Error checking for VM to cleanup. error: '%s'", err),
|
||||
})
|
||||
writeJSONError(w, http.StatusBadRequest, fmt.Sprintf("Error checking for VM to cleanup. error: '%s'", err))
|
||||
return
|
||||
}
|
||||
} else {
|
||||
@@ -68,33 +57,18 @@ func (h *Handler) VmCleanup(w http.ResponseWriter, r *http.Request) {
|
||||
err = h.Database.Queries().InventoryCleanup(ctx, params)
|
||||
if err != nil {
|
||||
h.Logger.Error("Error cleaning up VM from Inventory table", "error", err, "vm_id", vmId, "datacenter_name", datacenterName)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"status": "ERROR",
|
||||
"message": fmt.Sprintf("Error cleaning up VM from Inventory table. error: '%s'", err),
|
||||
})
|
||||
writeJSONError(w, http.StatusBadRequest, fmt.Sprintf("Error cleaning up VM from Inventory table. error: '%s'", err))
|
||||
return
|
||||
} else {
|
||||
// Successful cleanup
|
||||
h.Logger.Debug("VM successfully removed from inventory", "vm_name", vm.Name, "iid", vm.Iid, "vm_id", vmId, "datacenter_name", datacenterName)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"status": "OK",
|
||||
"message": fmt.Sprintf("VM '%s' removed from Inventory table", vm.Name),
|
||||
})
|
||||
writeJSONOKMessage(w, fmt.Sprintf("VM '%s' removed from Inventory table", vm.Name))
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
h.Logger.Error("Parameters not correctly specified", "vm_id", vmId, "datacenter_name", datacenterName)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"status": "ERROR",
|
||||
"message": fmt.Sprintf("Parameters not correctly specified. vm_id: '%s', datacenter_name: '%s'", vmId, datacenterName),
|
||||
})
|
||||
writeJSONError(w, http.StatusBadRequest, fmt.Sprintf("Parameters not correctly specified. vm_id: '%s', datacenter_name: '%s'", vmId, datacenterName))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,11 +20,11 @@ import (
|
||||
// @Tags events
|
||||
// @Deprecated
|
||||
// @Accept json
|
||||
// @Produce text/plain
|
||||
// @Produce json
|
||||
// @Param event body models.CloudEventReceived true "CloudEvent payload"
|
||||
// @Success 200 {string} string "Create event processed"
|
||||
// @Failure 400 {string} string "Invalid request"
|
||||
// @Failure 500 {string} string "Server error"
|
||||
// @Success 200 {object} models.StatusMessageResponse "Create event processed"
|
||||
// @Failure 400 {object} models.ErrorResponse "Invalid request"
|
||||
// @Failure 500 {object} models.ErrorResponse "Server error"
|
||||
// @Router /api/event/vm/create [post]
|
||||
func (h *Handler) VmCreateEvent(w http.ResponseWriter, r *http.Request) {
|
||||
if h.denyLegacyAPI(w, "/api/event/vm/create") {
|
||||
@@ -41,8 +41,7 @@ func (h *Handler) VmCreateEvent(w http.ResponseWriter, r *http.Request) {
|
||||
reqBody, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
h.Logger.Error("Invalid data received", "error", err)
|
||||
fmt.Fprintf(w, "Invalid data received")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
writeJSONError(w, http.StatusInternalServerError, "Invalid data received")
|
||||
return
|
||||
} else {
|
||||
h.Logger.Debug("received input data", "length", len(reqBody))
|
||||
@@ -52,7 +51,7 @@ func (h *Handler) VmCreateEvent(w http.ResponseWriter, r *http.Request) {
|
||||
var event models.CloudEventReceived
|
||||
if err := json.Unmarshal(reqBody, &event); err != nil {
|
||||
h.Logger.Error("unable to decode json", "error", err)
|
||||
http.Error(w, "Invalid JSON body", http.StatusBadRequest)
|
||||
writeJSONError(w, http.StatusBadRequest, "Invalid JSON body")
|
||||
return
|
||||
} else {
|
||||
h.Logger.Debug("successfully decoded JSON")
|
||||
@@ -100,16 +99,13 @@ func (h *Handler) VmCreateEvent(w http.ResponseWriter, r *http.Request) {
|
||||
result, err := h.Database.Queries().CreateEvent(context.Background(), params)
|
||||
if err != nil {
|
||||
h.Logger.Error("unable to perform database insert", "error", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
fmt.Fprintf(w, "Error : %v\n", err)
|
||||
writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("Error: %v", err))
|
||||
return
|
||||
} else {
|
||||
h.Logger.Debug("created database record", "insert_result", result)
|
||||
}
|
||||
|
||||
//h.Logger.Debug("received create request", "body", string(reqBody))
|
||||
w.WriteHeader(http.StatusOK)
|
||||
fmt.Fprintf(w, "Create Request : %v\n", result)
|
||||
writeJSONOKMessage(w, fmt.Sprintf("Create request processed: %v", result))
|
||||
}
|
||||
|
||||
// prettyPrint comes from https://gist.github.com/sfate/9d45f6c5405dc4c9bf63bf95fe6d1a7c
|
||||
|
||||
@@ -18,11 +18,11 @@ import (
|
||||
// @Tags events
|
||||
// @Deprecated
|
||||
// @Accept json
|
||||
// @Produce text/plain
|
||||
// @Produce json
|
||||
// @Param event body models.CloudEventReceived true "CloudEvent payload"
|
||||
// @Success 200 {string} string "Delete event processed"
|
||||
// @Failure 400 {string} string "Invalid request"
|
||||
// @Failure 500 {string} string "Server error"
|
||||
// @Success 200 {object} models.StatusMessageResponse "Delete event processed"
|
||||
// @Failure 400 {object} models.ErrorResponse "Invalid request"
|
||||
// @Failure 500 {object} models.ErrorResponse "Server error"
|
||||
// @Router /api/event/vm/delete [post]
|
||||
func (h *Handler) VmDeleteEvent(w http.ResponseWriter, r *http.Request) {
|
||||
if h.denyLegacyAPI(w, "/api/event/vm/delete") {
|
||||
@@ -36,8 +36,7 @@ func (h *Handler) VmDeleteEvent(w http.ResponseWriter, r *http.Request) {
|
||||
reqBody, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
h.Logger.Error("Invalid data received", "error", err)
|
||||
fmt.Fprintf(w, "Invalid data received")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
writeJSONError(w, http.StatusInternalServerError, "Invalid data received")
|
||||
return
|
||||
} else {
|
||||
//h.Logger.Debug("received input data", "length", len(reqBody))
|
||||
@@ -48,7 +47,7 @@ func (h *Handler) VmDeleteEvent(w http.ResponseWriter, r *http.Request) {
|
||||
if err := json.Unmarshal(reqBody, &event); err != nil {
|
||||
h.Logger.Error("unable to decode json", "error", err)
|
||||
prettyPrint(event)
|
||||
http.Error(w, "Invalid JSON body", http.StatusBadRequest)
|
||||
writeJSONError(w, http.StatusBadRequest, "Invalid JSON body")
|
||||
return
|
||||
} else {
|
||||
h.Logger.Debug("successfully decoded deletion type cloud event", "vm_id", event.CloudEvent.Data.VM.VM.Value)
|
||||
@@ -76,13 +75,10 @@ func (h *Handler) VmDeleteEvent(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if err != nil {
|
||||
h.Logger.Error("Error received marking VM as deleted", "error", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
fmt.Fprintf(w, "Delete Request unsuccessful %s\n", err)
|
||||
writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("Delete Request unsuccessful %s", err))
|
||||
} else {
|
||||
h.Logger.Debug("Processed VM Deletion event successfully")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
// TODO - return some JSON
|
||||
fmt.Fprintf(w, "Processed VM Deletion event successfully")
|
||||
writeJSONOKMessage(w, "Processed VM Deletion event successfully")
|
||||
}
|
||||
|
||||
//h.Logger.Debug("received delete request", "body", string(reqBody))
|
||||
|
||||
@@ -29,12 +29,7 @@ func (h *Handler) VmImport(w http.ResponseWriter, r *http.Request) {
|
||||
reqBody, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
h.Logger.Error("Invalid data received", "length", len(reqBody), "error", err)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"status": "ERROR",
|
||||
"message": fmt.Sprintf("Invalid data received: '%s'", err),
|
||||
})
|
||||
writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("Invalid data received: '%s'", err))
|
||||
return
|
||||
|
||||
} else {
|
||||
@@ -45,12 +40,7 @@ func (h *Handler) VmImport(w http.ResponseWriter, r *http.Request) {
|
||||
var inData models.ImportReceived
|
||||
if err := json.Unmarshal(reqBody, &inData); err != nil {
|
||||
h.Logger.Error("Unable to decode json request body", "length", len(reqBody), "error", err)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"status": "ERROR",
|
||||
"message": fmt.Sprintf("Unable to decode json request body: '%s'", err),
|
||||
})
|
||||
writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("Unable to decode json request body: '%s'", err))
|
||||
return
|
||||
} else {
|
||||
//h.Logger.Debug("successfully decoded JSON")
|
||||
@@ -59,12 +49,7 @@ func (h *Handler) VmImport(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if strings.HasPrefix(inData.Name, "vCLS-") {
|
||||
h.Logger.Info("Skipping internal vCLS VM import", "vm_name", inData.Name)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"status": "OK",
|
||||
"message": fmt.Sprintf("Skipped internal VM '%s'", inData.Name),
|
||||
})
|
||||
writeJSONOKMessage(w, fmt.Sprintf("Skipped internal VM '%s'", inData.Name))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -103,10 +88,5 @@ func (h *Handler) VmImport(w http.ResponseWriter, r *http.Request) {
|
||||
h.Logger.Info("not adding vm to inventory table since record alraedy exists", "vm_id", inData.VmId, "datacenter_name", inData.Datacenter)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"status": "OK",
|
||||
"message": fmt.Sprintf("Successfully processed import request for VM '%s'", inData.Name),
|
||||
})
|
||||
writeJSONOKMessage(w, fmt.Sprintf("Successfully processed import request for VM '%s'", inData.Name))
|
||||
}
|
||||
|
||||
@@ -46,12 +46,7 @@ func (h *Handler) VmModifyEvent(w http.ResponseWriter, r *http.Request) {
|
||||
reqBody, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
h.Logger.Error("Invalid data received", "length", len(reqBody), "error", err)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"status": "ERROR",
|
||||
"message": fmt.Sprintf("Invalid data received: '%s'", err),
|
||||
})
|
||||
writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("Invalid data received: '%s'", err))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -59,12 +54,7 @@ func (h *Handler) VmModifyEvent(w http.ResponseWriter, r *http.Request) {
|
||||
var event models.CloudEventReceived
|
||||
if err := json.Unmarshal(reqBody, &event); err != nil {
|
||||
h.Logger.Error("Unable to decode json request body", "length", len(reqBody), "error", err)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"status": "ERROR",
|
||||
"message": fmt.Sprintf("Unable to decode json request body: '%s'", err),
|
||||
})
|
||||
writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("Unable to decode json request body: '%s'", err))
|
||||
return
|
||||
} else {
|
||||
//h.Logger.Debug("successfully decoded JSON")
|
||||
@@ -74,12 +64,7 @@ func (h *Handler) VmModifyEvent(w http.ResponseWriter, r *http.Request) {
|
||||
if event.CloudEvent.Data.ConfigChanges == nil {
|
||||
h.Logger.Warn("Received event contains no config change")
|
||||
prettyPrint(event)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"status": "OK",
|
||||
"message": "Received update event successfully but no config changes were found",
|
||||
})
|
||||
writeJSONStatusMessage(w, http.StatusAccepted, "OK", "Received update event successfully but no config changes were found")
|
||||
return
|
||||
} else {
|
||||
h.Logger.Info("Received event contains config change info", "source", event.CloudEvent.Source,
|
||||
@@ -239,13 +224,8 @@ func (h *Handler) VmModifyEvent(w http.ResponseWriter, r *http.Request) {
|
||||
iid, err2 := h.AddVmToInventory(event, ctx, unixTimestamp)
|
||||
if err2 != nil {
|
||||
h.Logger.Error("Received error adding VM to inventory", "error", err2)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"status": "ERROR",
|
||||
"message": fmt.Sprintf("Valid request but experienced error adding vm id '%s' in datacenter name '%s' to inventory table : %s",
|
||||
event.CloudEvent.Data.VM.VM.Value, event.CloudEvent.Data.Datacenter.Name, err2),
|
||||
})
|
||||
writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("Valid request but experienced error adding vm id '%s' in datacenter name '%s' to inventory table : %s",
|
||||
event.CloudEvent.Data.VM.VM.Value, event.CloudEvent.Data.Datacenter.Name, err2))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -253,24 +233,14 @@ func (h *Handler) VmModifyEvent(w http.ResponseWriter, r *http.Request) {
|
||||
params.InventoryId = sql.NullInt64{Int64: iid, Valid: iid > 0}
|
||||
} else {
|
||||
h.Logger.Error("Received zero for inventory id when adding VM to inventory")
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"status": "ERROR",
|
||||
"message": fmt.Sprintf("Valid request but received zero result when adding vm id '%s' in datacenter name '%s' to inventory table",
|
||||
event.CloudEvent.Data.VM.VM.Value, event.CloudEvent.Data.Datacenter.Name),
|
||||
})
|
||||
writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("Valid request but received zero result when adding vm id '%s' in datacenter name '%s' to inventory table",
|
||||
event.CloudEvent.Data.VM.VM.Value, event.CloudEvent.Data.Datacenter.Name))
|
||||
return
|
||||
}
|
||||
} else {
|
||||
h.Logger.Error("unable to find existing inventory record for this VM", "error", err)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"status": "ERROR",
|
||||
"message": fmt.Sprintf("Valid request but could not locate vm id '%s' and datacenter name '%s' within inventory table : %s",
|
||||
event.CloudEvent.Data.VM.VM.Value, event.CloudEvent.Data.Datacenter.Name, err),
|
||||
})
|
||||
writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("Valid request but could not locate vm id '%s' and datacenter name '%s' within inventory table : %s",
|
||||
event.CloudEvent.Data.VM.VM.Value, event.CloudEvent.Data.Datacenter.Name, err))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -282,11 +252,7 @@ func (h *Handler) VmModifyEvent(w http.ResponseWriter, r *http.Request) {
|
||||
if params.UpdateType == "diskChange" && invResult.ProvisionedDisk.Float64 == params.NewProvisionedDisk.Float64 {
|
||||
h.Logger.Info("VM update type was for disk size but current size of VM matches inventory record, no need for update record",
|
||||
"vm_name", invResult.Name, "db_value", invResult.ProvisionedDisk.Float64, "new_value", params.NewProvisionedDisk.Float64)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"status": "OK",
|
||||
"message": "Successfully processed vm modify event",
|
||||
})
|
||||
writeJSONOKMessage(w, "Successfully processed vm modify event")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -303,23 +269,17 @@ func (h *Handler) VmModifyEvent(w http.ResponseWriter, r *http.Request) {
|
||||
result, err := h.Database.Queries().CreateUpdate(ctx, params)
|
||||
if err != nil {
|
||||
h.Logger.Error("unable to perform database insert", "error", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
fmt.Fprintf(w, "Error : %v\n", err)
|
||||
writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("Error : %v", err))
|
||||
return
|
||||
} else {
|
||||
h.Logger.Debug("created database record", "insert_result", result)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"status": "OK",
|
||||
"message": "Successfully processed vm modify event",
|
||||
})
|
||||
writeJSONOKMessage(w, "Successfully processed vm modify event")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
h.Logger.Debug("Didn't find any configuration changes of interest", "id", event.CloudEvent.ID,
|
||||
"vm", event.CloudEvent.Data.VM.Name, "config_changes", configChanges)
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
fmt.Fprintf(w, "Processed update event but no config changes were of interest\n")
|
||||
writeJSONStatusMessage(w, http.StatusAccepted, "OK", "Processed update event but no config changes were of interest")
|
||||
//prettyPrint(event.CloudEvent.Data.ConfigSpec)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,8 +39,7 @@ func (h *Handler) VmMoveEvent(w http.ResponseWriter, r *http.Request) {
|
||||
reqBody, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
h.Logger.Error("Invalid data received", "error", err)
|
||||
fmt.Fprintf(w, "Invalid data received")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
writeJSONError(w, http.StatusInternalServerError, "Invalid data received")
|
||||
return
|
||||
} else {
|
||||
//h.Logger.Debug("received input data", "length", len(reqBody))
|
||||
@@ -51,12 +50,7 @@ func (h *Handler) VmMoveEvent(w http.ResponseWriter, r *http.Request) {
|
||||
if err := json.Unmarshal(reqBody, &event); err != nil {
|
||||
h.Logger.Error("unable to unmarshal json", "error", err)
|
||||
prettyPrint(reqBody)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"status": "ERROR",
|
||||
"message": fmt.Sprintf("Unable to unmarshal JSON in request body: '%s'", err),
|
||||
})
|
||||
writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("Unable to unmarshal JSON in request body: '%s'", err))
|
||||
return
|
||||
} else {
|
||||
h.Logger.Debug("successfully decoded JSON")
|
||||
@@ -66,12 +60,7 @@ func (h *Handler) VmMoveEvent(w http.ResponseWriter, r *http.Request) {
|
||||
if event.CloudEvent.Data.OldParent == nil || event.CloudEvent.Data.NewParent == nil {
|
||||
h.Logger.Error("No resource pool data found in cloud event")
|
||||
prettyPrint(event)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"status": "ERROR",
|
||||
"message": "CloudEvent missing resource pool data",
|
||||
})
|
||||
writeJSONError(w, http.StatusBadRequest, "CloudEvent missing resource pool data")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -92,13 +81,8 @@ func (h *Handler) VmMoveEvent(w http.ResponseWriter, r *http.Request) {
|
||||
iid, err2 := h.AddVmToInventory(event, ctx, unixTimestamp)
|
||||
if err2 != nil {
|
||||
h.Logger.Error("Received error adding VM to inventory", "error", err2)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"status": "ERROR",
|
||||
"message": fmt.Sprintf("Valid request but experienced error adding vm id '%s' in datacenter name '%s' to inventory table : %s",
|
||||
event.CloudEvent.Data.VM.VM.Value, event.CloudEvent.Data.Datacenter.Name, err2),
|
||||
})
|
||||
writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("Valid request but experienced error adding vm id '%s' in datacenter name '%s' to inventory table : %s",
|
||||
event.CloudEvent.Data.VM.VM.Value, event.CloudEvent.Data.Datacenter.Name, err2))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -106,13 +90,8 @@ func (h *Handler) VmMoveEvent(w http.ResponseWriter, r *http.Request) {
|
||||
params.InventoryId = sql.NullInt64{Int64: iid, Valid: iid > 0}
|
||||
} else {
|
||||
h.Logger.Error("Received zero for inventory id when adding VM to inventory")
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"status": "ERROR",
|
||||
"message": fmt.Sprintf("Valid request but received zero result when adding vm id '%s' in datacenter name '%s' to inventory table",
|
||||
event.CloudEvent.Data.VM.VM.Value, event.CloudEvent.Data.Datacenter.Name),
|
||||
})
|
||||
writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("Valid request but received zero result when adding vm id '%s' in datacenter name '%s' to inventory table",
|
||||
event.CloudEvent.Data.VM.VM.Value, event.CloudEvent.Data.Datacenter.Name))
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -142,22 +121,12 @@ func (h *Handler) VmMoveEvent(w http.ResponseWriter, r *http.Request) {
|
||||
result, err := h.Database.Queries().CreateUpdate(ctx, params)
|
||||
if err != nil {
|
||||
h.Logger.Error("unable to perform database insert", "error", err)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"status": "ERROR",
|
||||
"message": fmt.Sprintf("Unable to insert move event into database: '%s'", err),
|
||||
})
|
||||
writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("Unable to insert move event into database: '%s'", err))
|
||||
return
|
||||
|
||||
} else {
|
||||
h.Logger.Debug("created database record", "insert_result", result)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
//fmt.Fprintf(w, "Processed update event: %v\n", result)
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"status": "OK",
|
||||
"message": "Successfully processed move event",
|
||||
})
|
||||
writeJSONOKMessage(w, "Successfully processed move event")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package handler
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"vctp/db/queries"
|
||||
"vctp/internal/vcenter"
|
||||
@@ -13,9 +12,9 @@ import (
|
||||
// @Summary Refresh VM details
|
||||
// @Description Queries vCenter and updates inventory records with missing details.
|
||||
// @Tags inventory
|
||||
// @Produce text/plain
|
||||
// @Success 200 {string} string "Update completed"
|
||||
// @Failure 500 {string} string "Server error"
|
||||
// @Produce json
|
||||
// @Success 200 {object} models.StatusMessageResponse "Update completed"
|
||||
// @Failure 500 {object} models.ErrorResponse "Server error"
|
||||
// @Router /api/inventory/vm/update [post]
|
||||
func (h *Handler) VmUpdateDetails(w http.ResponseWriter, r *http.Request) {
|
||||
var matchFound bool
|
||||
@@ -42,8 +41,8 @@ func (h *Handler) VmUpdateDetails(w http.ResponseWriter, r *http.Request) {
|
||||
results, err := h.Database.Queries().GetInventoryByVcenter(ctx, url)
|
||||
if err != nil {
|
||||
h.Logger.Error("Unable to query inventory table", "error", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
fmt.Fprintf(w, "Unable to query inventory table %s\n", err)
|
||||
writeJSONError(w, http.StatusInternalServerError, "Unable to query inventory table")
|
||||
return
|
||||
}
|
||||
|
||||
if len(results) == 0 {
|
||||
@@ -115,7 +114,5 @@ func (h *Handler) VmUpdateDetails(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
h.Logger.Debug("Processed vm update successfully")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
// TODO - return some JSON
|
||||
fmt.Fprintf(w, "Processed vm update successfully")
|
||||
writeJSONOKMessage(w, "Processed vm update successfully")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user