fix drone and sqlc generation
Some checks failed
continuous-integration/drone/push Build was killed

This commit is contained in:
2026-01-13 19:49:13 +11:00
parent ea1eeb5c21
commit a81613a8c2
28 changed files with 3718 additions and 288 deletions

View File

@@ -27,15 +27,22 @@ steps:
CGO_ENABLED: 0
GOMODCACHE: '/drone/src/pkg.mod'
GOCACHE: '/drone/src/pkg.build'
GOBIN: '/drone/src/pkg.tools'
volumes:
- name: shared
path: /shared
commands:
#- cp /shared/index.html ./www/
#- go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
#- sqlc generate
- chmod +x .drone.sh
- ./.drone.sh
- 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
- sqlc generate
- templ generate -path ./components
- swag init --exclude "pkg.mod,pkg.build,pkg.tools" -o server/router/docs
- chmod +x ./scripts/*.sh
- ./scripts/update-swagger-ui.sh
- ./scripts/drone.sh
- cp ./build/cbs-linux-amd64 /shared/
- name: dell-sftp-deploy
image: hypervtechnics/drone-sftp

View File

@@ -1,92 +0,0 @@
name: CI
on:
push:
branches:
- main
paths-ignore:
- '.github/**'
pull_request:
branches:
- main
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: 1.22.x
- run: go mod download
- run: go install github.com/a-h/templ/cmd/templ@v0.2.771
- run: make generate-templ
- uses: sqlc-dev/setup-sqlc@v4
with:
sqlc-version: '1.27.0'
- run: sqlc vet
- run: sqlc generate
- name: Lint
uses: golangci/golangci-lint-action@v3
with:
version: v1.54
skip-pkg-cache: true
test:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: 1.22.x
- run: go mod download
- run: go install github.com/a-h/templ/cmd/templ@v0.2.771
- run: make generate-templ
- uses: sqlc-dev/setup-sqlc@v4
with:
sqlc-version: '1.27.0'
- run: sqlc generate
- name: Test
run: go test -race ./...
e2e:
name: End-to-End
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: 1.22.x
- run: go mod download
- run: go install github.com/a-h/templ/cmd/templ@v0.2.771
- run: templ generate -path ./components
- uses: sqlc-dev/setup-sqlc@v4
with:
sqlc-version: '1.27.0'
- run: sqlc generate
- run: go test ./... -tags=e2e
docker-publish:
name: Publish Docker
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs:
- lint
- test
- e2e
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/metadata-action@v5
id: meta
with:
images: ghcr.io/piszmog/vctp
- uses: docker/build-push-action@v5
with:
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View File

@@ -1,77 +0,0 @@
name: Release
on:
workflow_dispatch:
inputs:
version:
description: The version to release (e.g. v1.0.0)
required: true
type: string
jobs:
release:
name: Release
permissions:
contents: write
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: 1.22.x
- run: go mod download
- run: go install github.com/a-h/templ/cmd/templ@v0.2.771
- name: Generate Templ Files
run: make generate-templ
- name: Generate CSS
run: |
curl -sLO https://github.com/tailwindlabs/tailwindcss/releases/latest/download/tailwindcss-linux-x64
chmod +x tailwindcss-linux-x64
mv tailwindcss-linux-x64 tailwindcss
./tailwindcss -i ./styles/input.css -o ./dist/assets/css/output@${{ github.event.inputs.version }}.css --minify
- uses: sqlc-dev/setup-sqlc@v4
with:
sqlc-version: '1.27.0'
- run: sqlc generate
- name: Build Application
run: go build -o ./app -ldflags="-s -w -X version.Value=${{ github.event.inputs.version }}"
- name: Create Tag
uses: piszmog/create-tag@v1
with:
version: ${{ github.event.inputs.version }}
token: ${{ secrets.GITHUB_TOKEN }}
- name: Release
uses: softprops/action-gh-release@v2
with:
name: ${{ github.event.inputs.version }}
tag_name: ${{ github.event.inputs.version }}
generate_release_notes: true
files: app
publish:
name: Publish Docker
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
needs:
- release
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/metadata-action@v5
id: meta
with:
images: ghcr.io/piszmog/my-app
tags: |
type=raw,value=${{ github.event.inputs.version }}
- uses: docker/build-push-action@v5
with:
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
VERSION=$${{ github.event.inputs.version }}

View File

@@ -100,7 +100,7 @@ This is where `templ` files live. Anything you want to render to the user goes h
### DB
This is the directory that `sqlc` generates to. Update `queries.sql` to build
your database operations.
your database operations. The schema for `sqlc` lives in `db/schema.sql`.
Once `queries.sql` is updated, run `make generate-sql` to update the generated models
@@ -133,6 +133,12 @@ DB_DRIVER=postgres DB_URL=postgres://user:pass@localhost:5432/vctp?sslmode=disab
PostgreSQL migrations live in `db/migrations_postgres`, while SQLite migrations remain in
`db/migrations`.
#### Snapshot Retention
Hourly and daily snapshot table retention can be configured with environment variables:
- `HOURLY_SNAPSHOT_MAX_AGE_DAYS` (default: 60)
- `DAILY_SNAPSHOT_MAX_AGE_MONTHS` (default: 12)
### Dist
This is where your assets live. Any Javascript, images, or styling needs to go in the

View File

@@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.778
// templ: version: v0.3.977
package core
//lint:file-ignore SA4006 This context is only used if a nested component is present.
@@ -29,11 +29,11 @@ func Footer() templ.Component {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<footer class=\"fixed p-1 bottom-0 bg-gray-100 w-full border-t\"><div class=\"rounded-lg p-4 text-xs italic text-gray-700 text-center\">&copy; Nathan Coad (nathan.coad@dell.com)</div></footer>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<footer class=\"fixed p-1 bottom-0 bg-gray-100 w-full border-t\"><div class=\"rounded-lg p-4 text-xs italic text-gray-700 text-center\">&copy; Nathan Coad (nathan.coad@dell.com)</div></footer>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
return nil
})
}

View File

@@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.778
// templ: version: v0.3.977
package core
//lint:file-ignore SA4006 This context is only used if a nested component is present.
@@ -31,24 +31,24 @@ func Header() templ.Component {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><meta name=\"description\" content=\"vCTP API endpoint\"><title>vCTP API</title><script src=\"/assets/js/htmx@v2.0.2.min.js\"></script><link href=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><meta name=\"description\" content=\"vCTP API endpoint\"><title>vCTP API</title><script src=\"/assets/js/htmx@v2.0.2.min.js\"></script><link href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs("/assets/css/output@" + version.Value + ".css")
var templ_7745c5c3_Var2 templ.SafeURL
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinURLErrs("/assets/css/output@" + version.Value + ".css")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/header.templ`, Line: 12, Col: 61}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/core/header.templ`, Line: 12, Col: 61}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" rel=\"stylesheet\"></head>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" rel=\"stylesheet\"></head>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
return nil
})
}

View File

@@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.778
// templ: version: v0.3.977
package views
//lint:file-ignore SA4006 This context is only used if a nested component is present.
@@ -39,7 +39,7 @@ func Index(info BuildInfo) templ.Component {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<!doctype html><html lang=\"en\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -47,46 +47,46 @@ func Index(info BuildInfo) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<body class=\"flex flex-col min-h-screen\"><main class=\"flex-grow\"><div><h1 class=\"text-5xl font-bold\">Build Information</h1><p class=\"mt-4\"><strong>Build Time:</strong> ")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<body class=\"flex flex-col min-h-screen\"><main class=\"flex-grow\"><div><h1 class=\"text-5xl font-bold\">Build Information</h1><p class=\"mt-4\"><strong>Build Time:</strong> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(info.BuildTime)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/index.templ`, Line: 21, Col: 80}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/views/index.templ`, Line: 21, Col: 80}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p><p class=\"mt-4\"><strong>SHA1 Version:</strong> ")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</p><p class=\"mt-4\"><strong>SHA1 Version:</strong> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(info.SHA1Ver)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/index.templ`, Line: 22, Col: 80}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/views/index.templ`, Line: 22, Col: 80}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p><p class=\"mt-4\"><strong>Go Runtime Version:</strong> ")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</p><p class=\"mt-4\"><strong>Go Runtime Version:</strong> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(info.GoVersion)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/index.templ`, Line: 23, Col: 88}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/views/index.templ`, Line: 23, Col: 88}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p></div></main></body>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</p></div></main></body>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -94,11 +94,11 @@ func Index(info BuildInfo) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</html>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
return nil
})
}

View File

@@ -0,0 +1,47 @@
package views
import (
"vctp/components/core"
)
type SnapshotEntry struct {
Label string
Link string
}
templ SnapshotHourlyList(entries []SnapshotEntry) {
@SnapshotListPage("Hourly Inventory Snapshots", "inventory snapshots captured hourly", entries)
}
templ SnapshotDailyList(entries []SnapshotEntry) {
@SnapshotListPage("Daily Inventory Snapshots", "daily summaries of hourly inventory snapshots", entries)
}
templ SnapshotMonthlyList(entries []SnapshotEntry) {
@SnapshotListPage("Monthly Inventory Snapshots", "monthly summary aggregated from daily snapshots", entries)
}
templ SnapshotListPage(title string, subtitle string, entries []SnapshotEntry) {
<!DOCTYPE html>
<html lang="en">
@core.Header()
<body class="flex flex-col min-h-screen">
<main class="flex-grow">
<div>
<h1 class="text-4xl font-bold">{title}</h1>
<p class="mt-2 text-gray-600">{subtitle}</p>
<ul class="mt-6 space-y-2">
for _, entry := range entries {
<li>
<a class="text-blue-600 hover:underline" href={entry.Link}>
{entry.Label}
</a>
</li>
}
</ul>
</div>
</main>
</body>
@core.Footer()
</html>
}

View File

@@ -0,0 +1,214 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.977
package views
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import (
"vctp/components/core"
)
type SnapshotEntry struct {
Label string
Link string
}
func SnapshotHourlyList(entries []SnapshotEntry) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = SnapshotListPage("Hourly Inventory Snapshots", "inventory snapshots captured hourly", entries).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func SnapshotDailyList(entries []SnapshotEntry) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var2 := templ.GetChildren(ctx)
if templ_7745c5c3_Var2 == nil {
templ_7745c5c3_Var2 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = SnapshotListPage("Daily Inventory Snapshots", "daily summaries of hourly inventory snapshots", entries).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func SnapshotMonthlyList(entries []SnapshotEntry) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var3 := templ.GetChildren(ctx)
if templ_7745c5c3_Var3 == nil {
templ_7745c5c3_Var3 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = SnapshotListPage("Monthly Inventory Snapshots", "monthly summary aggregated from daily snapshots", entries).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func SnapshotListPage(title string, subtitle string, entries []SnapshotEntry) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var4 := templ.GetChildren(ctx)
if templ_7745c5c3_Var4 == nil {
templ_7745c5c3_Var4 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = core.Header().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<body class=\"flex flex-col min-h-screen\"><main class=\"flex-grow\"><div><h1 class=\"text-4xl font-bold\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/views/snapshots.templ`, Line: 31, Col: 42}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</h1><p class=\"mt-2 text-gray-600\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(subtitle)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/views/snapshots.templ`, Line: 32, Col: 44}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</p><ul class=\"mt-6 space-y-2\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, entry := range entries {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<li><a class=\"text-blue-600 hover:underline\" href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 templ.SafeURL
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinURLErrs(entry.Link)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/views/snapshots.templ`, Line: 36, Col: 65}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Label)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/views/snapshots.templ`, Line: 37, Col: 21}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</a></li>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</ul></div></main></body>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = core.Footer().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@@ -6,17 +6,17 @@ ALTER TABLE "Inventory" RENAME COLUMN SrmPlaceholder TO SrmPlaceholder_old;
ALTER TABLE "Inventory" ADD COLUMN IsTemplate TEXT NOT NULL DEFAULT "FALSE";
ALTER TABLE "Inventory" ADD COLUMN PoweredOn TEXT NOT NULL DEFAULT "FALSE";
ALTER TABLE "Inventory" ADD COLUMN SrmPlaceholder TEXT NOT NULL DEFAULT "FALSE";
UPDATE Inventory
UPDATE "Inventory"
SET IsTemplate = CASE
WHEN IsTemplate_old = 1 THEN 'TRUE'
ELSE 'FALSE'
END;
UPDATE Inventory
UPDATE "Inventory"
SET PoweredOn = CASE
WHEN PowerState_old = 1 THEN 'TRUE'
ELSE 'FALSE'
END;
UPDATE Inventory
UPDATE "Inventory"
SET SrmPlaceholder = CASE
WHEN SrmPlaceholder_old = 1 THEN 'TRUE'
ELSE 'FALSE'
@@ -35,17 +35,17 @@ ALTER TABLE "Inventory" RENAME COLUMN SrmPlaceholder TO SrmPlaceholder_old;
ALTER TABLE "Inventory" ADD COLUMN IsTemplate INTEGER;
ALTER TABLE "Inventory" ADD COLUMN PowerState INTEGER;
ALTER TABLE "Inventory" ADD COLUMN SrmPlaceholder INTEGER;
UPDATE Inventory
UPDATE "Inventory"
SET IsTemplate = CASE
WHEN IsTemplate_old = 'TRUE' THEN 1
ELSE 0
END;
UPDATE Inventory
UPDATE "Inventory"
SET PowerState = CASE
WHEN PoweredOn_old = 'TRUE' THEN 1
ELSE 0
END;
UPDATE Inventory
UPDATE "Inventory"
SET SrmPlaceholder = CASE
WHEN SrmPlaceholder_old = 'TRUE' THEN 1
ELSE 0

View File

@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.27.0
// sqlc v1.29.0
package queries

View File

@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.27.0
// sqlc v1.29.0
package queries
@@ -8,7 +8,7 @@ import (
"database/sql"
)
type Events struct {
type Event struct {
Eid int64
CloudId string
Source string
@@ -60,7 +60,7 @@ type InventoryHistory struct {
PreviousProvisionedDisk sql.NullFloat64
}
type Updates struct {
type Update struct {
Uid int64
InventoryId sql.NullInt64
UpdateTime sql.NullInt64

View File

@@ -1,37 +1,37 @@
-- name: ListInventory :many
SELECT * FROM "Inventory"
SELECT * FROM inventory
ORDER BY "Name";
-- name: GetReportInventory :many
SELECT * FROM "Inventory"
SELECT * FROM inventory
ORDER BY "CreationTime";
-- name: GetInventoryByName :many
SELECT * FROM "Inventory"
SELECT * FROM inventory
WHERE "Name" = ?;
-- name: GetInventoryByVcenter :many
SELECT * FROM "Inventory"
SELECT * FROM inventory
WHERE "Vcenter" = ?;
-- name: GetInventoryVmId :one
SELECT * FROM "Inventory"
SELECT * FROM inventory
WHERE "VmId" = sqlc.arg('vmId') AND "Datacenter" = sqlc.arg('datacenterName');
-- name: GetInventoryVmUuid :one
SELECT * FROM "Inventory"
SELECT * FROM inventory
WHERE "VmUuid" = sqlc.arg('vmUuid') AND "Datacenter" = sqlc.arg('datacenterName');
-- name: GetInventoryVcUrl :many
SELECT * FROM "Inventory"
SELECT * FROM inventory
WHERE "Vcenter" = sqlc.arg('vc');
-- name: GetInventoryEventId :one
SELECT * FROM "Inventory"
SELECT * FROM inventory
WHERE "CloudId" = ? LIMIT 1;
-- name: CreateInventory :one
INSERT INTO "Inventory" (
INSERT INTO inventory (
"Name", "Vcenter", "VmId", "VmUuid", "EventKey", "CloudId", "CreationTime", "ResourcePool", "VmType", "IsTemplate", "Datacenter", "Cluster", "Folder", "ProvisionedDisk", "InitialVcpus", "InitialRam", "SrmPlaceholder", "PoweredOn"
) VALUES(
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
@@ -39,32 +39,32 @@ INSERT INTO "Inventory" (
RETURNING *;
-- name: InventoryUpdate :exec
UPDATE "Inventory"
UPDATE inventory
SET "VmUuid" = sqlc.arg('uuid'), "SrmPlaceholder" = sqlc.arg('srmPlaceholder')
WHERE "Iid" = sqlc.arg('iid');
-- name: InventoryMarkDeleted :exec
UPDATE "Inventory"
UPDATE inventory
SET "DeletionTime" = sqlc.arg('deletionTime')
WHERE "VmId" = sqlc.arg('vmId') AND "Datacenter" = sqlc.arg('datacenterName');
-- name: InventoryCleanup :exec
DELETE FROM "Inventory"
DELETE FROM inventory
WHERE "VmId" = sqlc.arg('vmId') AND "Datacenter" = sqlc.arg('datacenterName')
RETURNING *;
-- name: InventoryCleanupVcenter :exec
DELETE FROM "Inventory"
DELETE FROM inventory
WHERE "Vcenter" = sqlc.arg('vc')
RETURNING *;
-- name: InventoryCleanupTemplates :exec
DELETE FROM "Inventory"
DELETE FROM inventory
WHERE "IsTemplate" = 'TRUE'
RETURNING *;
-- name: CreateUpdate :one
INSERT INTO "Updates" (
INSERT INTO updates (
"InventoryId", "Name", "EventKey", "EventId", "UpdateTime", "UpdateType", "NewVcpus", "NewRam", "NewResourcePool", "NewProvisionedDisk", "UserName", "PlaceholderChange", "RawChangeString"
) VALUES(
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
@@ -72,25 +72,25 @@ INSERT INTO "Updates" (
RETURNING *;
-- name: GetReportUpdates :many
SELECT * FROM "Updates"
SELECT * FROM updates
ORDER BY "UpdateTime";
-- name: GetVmUpdates :many
SELECT * FROM "Updates"
SELECT * FROM updates
WHERE "UpdateType" = sqlc.arg('updateType') AND "InventoryId" = sqlc.arg('InventoryId');
-- name: CleanupUpdates :exec
DELETE FROM "Updates"
DELETE FROM updates
WHERE "UpdateType" = sqlc.arg('updateType') AND "UpdateTime" <= sqlc.arg('updateTime')
RETURNING *;
-- name: CleanupUpdatesNullVm :exec
DELETE FROM "Updates"
DELETE FROM updates
WHERE "InventoryId" IS NULL
RETURNING *;
-- name: CreateEvent :one
INSERT INTO "Events" (
INSERT INTO events (
"CloudId", "Source", "EventTime", "ChainId", "VmId", "VmName", "EventType", "EventKey", "DatacenterId", "DatacenterName", "ComputeResourceId", "ComputeResourceName", "UserName"
) VALUES(
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
@@ -98,22 +98,22 @@ INSERT INTO "Events" (
RETURNING *;
-- name: ListEvents :many
SELECT * FROM "Events"
SELECT * FROM events
ORDER BY "EventTime";
-- name: ListUnprocessedEvents :many
SELECT * FROM "Events"
SELECT * FROM events
WHERE "Processed" = 0
AND "EventTime" > sqlc.arg('eventTime')
ORDER BY "EventTime";
-- name: UpdateEventsProcessed :exec
UPDATE "Events"
UPDATE events
SET "Processed" = 1
WHERE "Eid" = sqlc.arg('eid');
-- name: CreateInventoryHistory :one
INSERT INTO "InventoryHistory" (
INSERT INTO inventory_history (
"InventoryId", "ReportDate", "UpdateTime", "PreviousVcpus", "PreviousRam", "PreviousResourcePool", "PreviousProvisionedDisk"
) VALUES(
?, ?, ?, ?, ?, ?, ?

View File

@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.27.0
// sqlc v1.29.0
// source: query.sql
package queries
@@ -11,7 +11,7 @@ import (
)
const cleanupUpdates = `-- name: CleanupUpdates :exec
DELETE FROM "Updates"
DELETE FROM updates
WHERE "UpdateType" = ?1 AND "UpdateTime" <= ?2
RETURNING Uid, InventoryId, UpdateTime, UpdateType, NewVcpus, NewRam, NewResourcePool, EventKey, EventId, NewProvisionedDisk, UserName, PlaceholderChange, Name, RawChangeString
`
@@ -27,7 +27,7 @@ func (q *Queries) CleanupUpdates(ctx context.Context, arg CleanupUpdatesParams)
}
const cleanupUpdatesNullVm = `-- name: CleanupUpdatesNullVm :exec
DELETE FROM "Updates"
DELETE FROM updates
WHERE "InventoryId" IS NULL
RETURNING Uid, InventoryId, UpdateTime, UpdateType, NewVcpus, NewRam, NewResourcePool, EventKey, EventId, NewProvisionedDisk, UserName, PlaceholderChange, Name, RawChangeString
`
@@ -38,7 +38,7 @@ func (q *Queries) CleanupUpdatesNullVm(ctx context.Context) error {
}
const createEvent = `-- name: CreateEvent :one
INSERT INTO "Events" (
INSERT INTO events (
"CloudId", "Source", "EventTime", "ChainId", "VmId", "VmName", "EventType", "EventKey", "DatacenterId", "DatacenterName", "ComputeResourceId", "ComputeResourceName", "UserName"
) VALUES(
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
@@ -62,7 +62,7 @@ type CreateEventParams struct {
UserName sql.NullString
}
func (q *Queries) CreateEvent(ctx context.Context, arg CreateEventParams) (Events, error) {
func (q *Queries) CreateEvent(ctx context.Context, arg CreateEventParams) (Event, error) {
row := q.db.QueryRowContext(ctx, createEvent,
arg.CloudId,
arg.Source,
@@ -78,7 +78,7 @@ func (q *Queries) CreateEvent(ctx context.Context, arg CreateEventParams) (Event
arg.ComputeResourceName,
arg.UserName,
)
var i Events
var i Event
err := row.Scan(
&i.Eid,
&i.CloudId,
@@ -100,7 +100,7 @@ func (q *Queries) CreateEvent(ctx context.Context, arg CreateEventParams) (Event
}
const createInventory = `-- name: CreateInventory :one
INSERT INTO "Inventory" (
INSERT INTO inventory (
"Name", "Vcenter", "VmId", "VmUuid", "EventKey", "CloudId", "CreationTime", "ResourcePool", "VmType", "IsTemplate", "Datacenter", "Cluster", "Folder", "ProvisionedDisk", "InitialVcpus", "InitialRam", "SrmPlaceholder", "PoweredOn"
) VALUES(
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
@@ -177,7 +177,7 @@ func (q *Queries) CreateInventory(ctx context.Context, arg CreateInventoryParams
}
const createInventoryHistory = `-- name: CreateInventoryHistory :one
INSERT INTO "InventoryHistory" (
INSERT INTO inventory_history (
"InventoryId", "ReportDate", "UpdateTime", "PreviousVcpus", "PreviousRam", "PreviousResourcePool", "PreviousProvisionedDisk"
) VALUES(
?, ?, ?, ?, ?, ?, ?
@@ -220,7 +220,7 @@ func (q *Queries) CreateInventoryHistory(ctx context.Context, arg CreateInventor
}
const createUpdate = `-- name: CreateUpdate :one
INSERT INTO "Updates" (
INSERT INTO updates (
"InventoryId", "Name", "EventKey", "EventId", "UpdateTime", "UpdateType", "NewVcpus", "NewRam", "NewResourcePool", "NewProvisionedDisk", "UserName", "PlaceholderChange", "RawChangeString"
) VALUES(
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
@@ -244,7 +244,7 @@ type CreateUpdateParams struct {
RawChangeString []byte
}
func (q *Queries) CreateUpdate(ctx context.Context, arg CreateUpdateParams) (Updates, error) {
func (q *Queries) CreateUpdate(ctx context.Context, arg CreateUpdateParams) (Update, error) {
row := q.db.QueryRowContext(ctx, createUpdate,
arg.InventoryId,
arg.Name,
@@ -260,7 +260,7 @@ func (q *Queries) CreateUpdate(ctx context.Context, arg CreateUpdateParams) (Upd
arg.PlaceholderChange,
arg.RawChangeString,
)
var i Updates
var i Update
err := row.Scan(
&i.Uid,
&i.InventoryId,
@@ -281,7 +281,7 @@ func (q *Queries) CreateUpdate(ctx context.Context, arg CreateUpdateParams) (Upd
}
const getInventoryByName = `-- name: GetInventoryByName :many
SELECT Iid, Name, Vcenter, VmId, EventKey, CloudId, CreationTime, DeletionTime, ResourcePool, VmType, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid FROM "Inventory"
SELECT Iid, Name, Vcenter, VmId, EventKey, CloudId, CreationTime, DeletionTime, ResourcePool, VmType, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid FROM inventory
WHERE "Name" = ?
`
@@ -330,7 +330,7 @@ func (q *Queries) GetInventoryByName(ctx context.Context, name string) ([]Invent
}
const getInventoryByVcenter = `-- name: GetInventoryByVcenter :many
SELECT Iid, Name, Vcenter, VmId, EventKey, CloudId, CreationTime, DeletionTime, ResourcePool, VmType, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid FROM "Inventory"
SELECT Iid, Name, Vcenter, VmId, EventKey, CloudId, CreationTime, DeletionTime, ResourcePool, VmType, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid FROM inventory
WHERE "Vcenter" = ?
`
@@ -379,7 +379,7 @@ func (q *Queries) GetInventoryByVcenter(ctx context.Context, vcenter string) ([]
}
const getInventoryEventId = `-- name: GetInventoryEventId :one
SELECT Iid, Name, Vcenter, VmId, EventKey, CloudId, CreationTime, DeletionTime, ResourcePool, VmType, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid FROM "Inventory"
SELECT Iid, Name, Vcenter, VmId, EventKey, CloudId, CreationTime, DeletionTime, ResourcePool, VmType, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid FROM inventory
WHERE "CloudId" = ? LIMIT 1
`
@@ -412,7 +412,7 @@ func (q *Queries) GetInventoryEventId(ctx context.Context, cloudid sql.NullStrin
}
const getInventoryVcUrl = `-- name: GetInventoryVcUrl :many
SELECT Iid, Name, Vcenter, VmId, EventKey, CloudId, CreationTime, DeletionTime, ResourcePool, VmType, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid FROM "Inventory"
SELECT Iid, Name, Vcenter, VmId, EventKey, CloudId, CreationTime, DeletionTime, ResourcePool, VmType, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid FROM inventory
WHERE "Vcenter" = ?1
`
@@ -461,7 +461,7 @@ func (q *Queries) GetInventoryVcUrl(ctx context.Context, vc string) ([]Inventory
}
const getInventoryVmId = `-- name: GetInventoryVmId :one
SELECT Iid, Name, Vcenter, VmId, EventKey, CloudId, CreationTime, DeletionTime, ResourcePool, VmType, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid FROM "Inventory"
SELECT Iid, Name, Vcenter, VmId, EventKey, CloudId, CreationTime, DeletionTime, ResourcePool, VmType, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid FROM inventory
WHERE "VmId" = ?1 AND "Datacenter" = ?2
`
@@ -499,7 +499,7 @@ func (q *Queries) GetInventoryVmId(ctx context.Context, arg GetInventoryVmIdPara
}
const getInventoryVmUuid = `-- name: GetInventoryVmUuid :one
SELECT Iid, Name, Vcenter, VmId, EventKey, CloudId, CreationTime, DeletionTime, ResourcePool, VmType, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid FROM "Inventory"
SELECT Iid, Name, Vcenter, VmId, EventKey, CloudId, CreationTime, DeletionTime, ResourcePool, VmType, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid FROM inventory
WHERE "VmUuid" = ?1 AND "Datacenter" = ?2
`
@@ -537,7 +537,7 @@ func (q *Queries) GetInventoryVmUuid(ctx context.Context, arg GetInventoryVmUuid
}
const getReportInventory = `-- name: GetReportInventory :many
SELECT Iid, Name, Vcenter, VmId, EventKey, CloudId, CreationTime, DeletionTime, ResourcePool, VmType, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid FROM "Inventory"
SELECT Iid, Name, Vcenter, VmId, EventKey, CloudId, CreationTime, DeletionTime, ResourcePool, VmType, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid FROM inventory
ORDER BY "CreationTime"
`
@@ -586,19 +586,19 @@ func (q *Queries) GetReportInventory(ctx context.Context) ([]Inventory, error) {
}
const getReportUpdates = `-- name: GetReportUpdates :many
SELECT Uid, InventoryId, UpdateTime, UpdateType, NewVcpus, NewRam, NewResourcePool, EventKey, EventId, NewProvisionedDisk, UserName, PlaceholderChange, Name, RawChangeString FROM "Updates"
SELECT Uid, InventoryId, UpdateTime, UpdateType, NewVcpus, NewRam, NewResourcePool, EventKey, EventId, NewProvisionedDisk, UserName, PlaceholderChange, Name, RawChangeString FROM updates
ORDER BY "UpdateTime"
`
func (q *Queries) GetReportUpdates(ctx context.Context) ([]Updates, error) {
func (q *Queries) GetReportUpdates(ctx context.Context) ([]Update, error) {
rows, err := q.db.QueryContext(ctx, getReportUpdates)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Updates
var items []Update
for rows.Next() {
var i Updates
var i Update
if err := rows.Scan(
&i.Uid,
&i.InventoryId,
@@ -629,7 +629,7 @@ func (q *Queries) GetReportUpdates(ctx context.Context) ([]Updates, error) {
}
const getVmUpdates = `-- name: GetVmUpdates :many
SELECT Uid, InventoryId, UpdateTime, UpdateType, NewVcpus, NewRam, NewResourcePool, EventKey, EventId, NewProvisionedDisk, UserName, PlaceholderChange, Name, RawChangeString FROM "Updates"
SELECT Uid, InventoryId, UpdateTime, UpdateType, NewVcpus, NewRam, NewResourcePool, EventKey, EventId, NewProvisionedDisk, UserName, PlaceholderChange, Name, RawChangeString FROM updates
WHERE "UpdateType" = ?1 AND "InventoryId" = ?2
`
@@ -638,15 +638,15 @@ type GetVmUpdatesParams struct {
InventoryId sql.NullInt64
}
func (q *Queries) GetVmUpdates(ctx context.Context, arg GetVmUpdatesParams) ([]Updates, error) {
func (q *Queries) GetVmUpdates(ctx context.Context, arg GetVmUpdatesParams) ([]Update, error) {
rows, err := q.db.QueryContext(ctx, getVmUpdates, arg.UpdateType, arg.InventoryId)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Updates
var items []Update
for rows.Next() {
var i Updates
var i Update
if err := rows.Scan(
&i.Uid,
&i.InventoryId,
@@ -677,7 +677,7 @@ func (q *Queries) GetVmUpdates(ctx context.Context, arg GetVmUpdatesParams) ([]U
}
const inventoryCleanup = `-- name: InventoryCleanup :exec
DELETE FROM "Inventory"
DELETE FROM inventory
WHERE "VmId" = ?1 AND "Datacenter" = ?2
RETURNING Iid, Name, Vcenter, VmId, EventKey, CloudId, CreationTime, DeletionTime, ResourcePool, VmType, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid
`
@@ -693,7 +693,7 @@ func (q *Queries) InventoryCleanup(ctx context.Context, arg InventoryCleanupPara
}
const inventoryCleanupTemplates = `-- name: InventoryCleanupTemplates :exec
DELETE FROM "Inventory"
DELETE FROM inventory
WHERE "IsTemplate" = 'TRUE'
RETURNING Iid, Name, Vcenter, VmId, EventKey, CloudId, CreationTime, DeletionTime, ResourcePool, VmType, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid
`
@@ -704,7 +704,7 @@ func (q *Queries) InventoryCleanupTemplates(ctx context.Context) error {
}
const inventoryCleanupVcenter = `-- name: InventoryCleanupVcenter :exec
DELETE FROM "Inventory"
DELETE FROM inventory
WHERE "Vcenter" = ?1
RETURNING Iid, Name, Vcenter, VmId, EventKey, CloudId, CreationTime, DeletionTime, ResourcePool, VmType, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid
`
@@ -715,7 +715,7 @@ func (q *Queries) InventoryCleanupVcenter(ctx context.Context, vc string) error
}
const inventoryMarkDeleted = `-- name: InventoryMarkDeleted :exec
UPDATE "Inventory"
UPDATE inventory
SET "DeletionTime" = ?1
WHERE "VmId" = ?2 AND "Datacenter" = ?3
`
@@ -732,7 +732,7 @@ func (q *Queries) InventoryMarkDeleted(ctx context.Context, arg InventoryMarkDel
}
const inventoryUpdate = `-- name: InventoryUpdate :exec
UPDATE "Inventory"
UPDATE inventory
SET "VmUuid" = ?1, "SrmPlaceholder" = ?2
WHERE "Iid" = ?3
`
@@ -749,19 +749,19 @@ func (q *Queries) InventoryUpdate(ctx context.Context, arg InventoryUpdateParams
}
const listEvents = `-- name: ListEvents :many
SELECT Eid, CloudId, Source, EventTime, ChainId, VmId, EventKey, DatacenterName, ComputeResourceName, UserName, Processed, DatacenterId, ComputeResourceId, VmName, EventType FROM "Events"
SELECT Eid, CloudId, Source, EventTime, ChainId, VmId, EventKey, DatacenterName, ComputeResourceName, UserName, Processed, DatacenterId, ComputeResourceId, VmName, EventType FROM events
ORDER BY "EventTime"
`
func (q *Queries) ListEvents(ctx context.Context) ([]Events, error) {
func (q *Queries) ListEvents(ctx context.Context) ([]Event, error) {
rows, err := q.db.QueryContext(ctx, listEvents)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Events
var items []Event
for rows.Next() {
var i Events
var i Event
if err := rows.Scan(
&i.Eid,
&i.CloudId,
@@ -793,7 +793,7 @@ func (q *Queries) ListEvents(ctx context.Context) ([]Events, error) {
}
const listInventory = `-- name: ListInventory :many
SELECT Iid, Name, Vcenter, VmId, EventKey, CloudId, CreationTime, DeletionTime, ResourcePool, VmType, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid FROM "Inventory"
SELECT Iid, Name, Vcenter, VmId, EventKey, CloudId, CreationTime, DeletionTime, ResourcePool, VmType, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid FROM inventory
ORDER BY "Name"
`
@@ -842,21 +842,21 @@ func (q *Queries) ListInventory(ctx context.Context) ([]Inventory, error) {
}
const listUnprocessedEvents = `-- name: ListUnprocessedEvents :many
SELECT Eid, CloudId, Source, EventTime, ChainId, VmId, EventKey, DatacenterName, ComputeResourceName, UserName, Processed, DatacenterId, ComputeResourceId, VmName, EventType FROM "Events"
SELECT Eid, CloudId, Source, EventTime, ChainId, VmId, EventKey, DatacenterName, ComputeResourceName, UserName, Processed, DatacenterId, ComputeResourceId, VmName, EventType FROM events
WHERE "Processed" = 0
AND "EventTime" > ?1
ORDER BY "EventTime"
`
func (q *Queries) ListUnprocessedEvents(ctx context.Context, eventtime sql.NullInt64) ([]Events, error) {
func (q *Queries) ListUnprocessedEvents(ctx context.Context, eventtime sql.NullInt64) ([]Event, error) {
rows, err := q.db.QueryContext(ctx, listUnprocessedEvents, eventtime)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Events
var items []Event
for rows.Next() {
var i Events
var i Event
if err := rows.Scan(
&i.Eid,
&i.CloudId,
@@ -888,7 +888,7 @@ func (q *Queries) ListUnprocessedEvents(ctx context.Context, eventtime sql.NullI
}
const updateEventsProcessed = `-- name: UpdateEventsProcessed :exec
UPDATE "Events"
UPDATE events
SET "Processed" = 1
WHERE "Eid" = ?1
`

68
db/schema.sql Normal file
View File

@@ -0,0 +1,68 @@
CREATE TABLE IF NOT EXISTS inventory (
"Iid" INTEGER PRIMARY KEY AUTOINCREMENT,
"Name" TEXT NOT NULL,
"Vcenter" TEXT NOT NULL,
"VmId" TEXT,
"EventKey" TEXT,
"CloudId" TEXT,
"CreationTime" INTEGER,
"DeletionTime" INTEGER,
"ResourcePool" TEXT,
"VmType" TEXT,
"Datacenter" TEXT,
"Cluster" TEXT,
"Folder" TEXT,
"ProvisionedDisk" REAL,
"InitialVcpus" INTEGER,
"InitialRam" INTEGER,
"IsTemplate" TEXT NOT NULL DEFAULT "FALSE",
"PoweredOn" TEXT NOT NULL DEFAULT "FALSE",
"SrmPlaceholder" TEXT NOT NULL DEFAULT "FALSE",
"VmUuid" TEXT
);
CREATE TABLE IF NOT EXISTS updates (
"Uid" INTEGER PRIMARY KEY AUTOINCREMENT,
"InventoryId" INTEGER,
"UpdateTime" INTEGER,
"UpdateType" TEXT NOT NULL,
"NewVcpus" INTEGER,
"NewRam" INTEGER,
"NewResourcePool" TEXT,
"EventKey" TEXT,
"EventId" TEXT,
"NewProvisionedDisk" REAL,
"UserName" TEXT,
"PlaceholderChange" TEXT,
"Name" TEXT,
"RawChangeString" BLOB
);
CREATE TABLE IF NOT EXISTS events (
"Eid" INTEGER PRIMARY KEY AUTOINCREMENT,
"CloudId" TEXT NOT NULL,
"Source" TEXT NOT NULL,
"EventTime" INTEGER,
"ChainId" TEXT NOT NULL,
"VmId" TEXT,
"EventKey" TEXT,
"DatacenterName" TEXT,
"ComputeResourceName" TEXT,
"UserName" TEXT,
"Processed" INTEGER NOT NULL DEFAULT 0,
"DatacenterId" TEXT,
"ComputeResourceId" TEXT,
"VmName" TEXT,
"EventType" TEXT
);
CREATE TABLE IF NOT EXISTS inventory_history (
"Hid" INTEGER PRIMARY KEY AUTOINCREMENT,
"InventoryId" INTEGER,
"ReportDate" INTEGER,
"UpdateTime" INTEGER,
"PreviousVcpus" INTEGER,
"PreviousRam" INTEGER,
"PreviousResourcePool" TEXT,
"PreviousProvisionedDisk" REAL
);

View File

@@ -0,0 +1,293 @@
package report
import (
"bytes"
"context"
"database/sql"
"fmt"
"log/slog"
"strings"
"time"
"vctp/db"
"github.com/jmoiron/sqlx"
"github.com/xuri/excelize/v2"
)
func ListTablesByPrefix(ctx context.Context, database db.Database, prefix string) ([]string, error) {
dbConn := database.DB()
driver := strings.ToLower(dbConn.DriverName())
pattern := prefix + "%"
var rows *sqlx.Rows
var err error
switch driver {
case "sqlite":
rows, err = dbConn.QueryxContext(ctx, `
SELECT name
FROM sqlite_master
WHERE type = 'table'
AND name LIKE ?
ORDER BY name DESC
`, pattern)
case "pgx", "postgres":
rows, err = dbConn.QueryxContext(ctx, `
SELECT tablename
FROM pg_catalog.pg_tables
WHERE schemaname = 'public'
AND tablename LIKE $1
ORDER BY tablename DESC
`, pattern)
default:
return nil, fmt.Errorf("unsupported driver for listing tables: %s", driver)
}
if err != nil {
return nil, err
}
defer rows.Close()
tables := make([]string, 0)
for rows.Next() {
var name string
if err := rows.Scan(&name); err != nil {
return nil, err
}
tables = append(tables, name)
}
return tables, rows.Err()
}
func FormatSnapshotLabel(prefix string, tableName string) (string, bool) {
if !strings.HasPrefix(tableName, prefix) {
return "", false
}
suffix := strings.TrimPrefix(tableName, prefix)
switch prefix {
case "inventory_daily_":
if t, err := time.Parse("20060102", suffix); err == nil {
return t.Format("2006-01-02"), true
}
case "inventory_daily_summary_":
if t, err := time.Parse("20060102", suffix); err == nil {
return t.Format("2006-01-02"), true
}
case "inventory_monthly_summary_":
if t, err := time.Parse("200601", suffix); err == nil {
return t.Format("2006-01"), true
}
}
return "", false
}
func CreateTableReport(logger *slog.Logger, Database db.Database, ctx context.Context, tableName string) ([]byte, error) {
if err := validateTableName(tableName); err != nil {
return nil, err
}
dbConn := Database.DB()
columns, err := tableColumns(ctx, dbConn, tableName)
if err != nil {
return nil, err
}
if len(columns) == 0 {
return nil, fmt.Errorf("no columns found for table %s", tableName)
}
query := fmt.Sprintf(`SELECT * FROM %s`, tableName)
orderBy := snapshotOrderBy(columns)
if orderBy != "" {
query = fmt.Sprintf(`%s ORDER BY "%s" DESC`, query, orderBy)
}
rows, err := dbConn.QueryxContext(ctx, query)
if err != nil {
return nil, err
}
defer rows.Close()
sheetName := "Snapshot Report"
var buffer bytes.Buffer
xlsx := excelize.NewFile()
if err := xlsx.SetSheetName("Sheet1", sheetName); err != nil {
return nil, err
}
if err := xlsx.SetDocProps(&excelize.DocProperties{
Creator: "vctp",
Created: time.Now().Format(time.RFC3339),
}); err != nil {
logger.Error("Error setting document properties", "error", err, "sheet_name", sheetName)
}
for i, columnName := range columns {
cell := fmt.Sprintf("%s1", string(rune('A'+i)))
xlsx.SetCellValue(sheetName, cell, columnName)
}
if endCell, err := excelize.CoordinatesToCellName(len(columns), 1); err == nil {
filterRange := "A1:" + endCell
if err := xlsx.AutoFilter(sheetName, filterRange, nil); err != nil {
logger.Error("Error setting autofilter", "error", err)
}
}
headerStyle, err := xlsx.NewStyle(&excelize.Style{
Font: &excelize.Font{
Bold: true,
},
})
if err == nil {
if err := xlsx.SetRowStyle(sheetName, 1, 1, headerStyle); err != nil {
logger.Error("Error setting header style", "error", err)
}
}
rowIndex := 2
for rows.Next() {
values, err := scanRowValues(rows, len(columns))
if err != nil {
return nil, err
}
for colIndex, value := range values {
cell := fmt.Sprintf("%s%d", string(rune('A'+colIndex)), rowIndex)
xlsx.SetCellValue(sheetName, cell, normalizeCellValue(value))
}
rowIndex++
}
if err := rows.Err(); err != nil {
return nil, err
}
if err := xlsx.SetPanes(sheetName, &excelize.Panes{
Freeze: true,
Split: false,
XSplit: 0,
YSplit: 1,
TopLeftCell: "A2",
ActivePane: "bottomLeft",
Selection: []excelize.Selection{
{SQRef: "A2", ActiveCell: "A2", Pane: "bottomLeft"},
},
}); err != nil {
logger.Error("Error freezing top row", "error", err)
}
if err := xlsx.Write(&buffer); err != nil {
return nil, err
}
return buffer.Bytes(), nil
}
func validateTableName(name string) error {
if name == "" {
return fmt.Errorf("table name is empty")
}
for _, r := range name {
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '_' {
continue
}
return fmt.Errorf("invalid table name: %s", name)
}
return nil
}
func tableColumns(ctx context.Context, dbConn *sqlx.DB, tableName string) ([]string, error) {
driver := strings.ToLower(dbConn.DriverName())
switch driver {
case "sqlite":
query := fmt.Sprintf(`PRAGMA table_info("%s")`, tableName)
rows, err := dbConn.QueryxContext(ctx, query)
if err != nil {
return nil, err
}
defer rows.Close()
columns := make([]string, 0)
for rows.Next() {
var (
cid int
name string
colType string
notNull int
defaultVal sql.NullString
pk int
)
if err := rows.Scan(&cid, &name, &colType, &notNull, &defaultVal, &pk); err != nil {
return nil, err
}
columns = append(columns, name)
}
return columns, rows.Err()
case "pgx", "postgres":
rows, err := dbConn.QueryxContext(ctx, `
SELECT column_name
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = $1
ORDER BY ordinal_position
`, tableName)
if err != nil {
return nil, err
}
defer rows.Close()
columns := make([]string, 0)
for rows.Next() {
var name string
if err := rows.Scan(&name); err != nil {
return nil, err
}
columns = append(columns, name)
}
return columns, rows.Err()
default:
return nil, fmt.Errorf("unsupported driver for table columns: %s", driver)
}
}
func snapshotOrderBy(columns []string) string {
normalized := make(map[string]struct{}, len(columns))
for _, col := range columns {
normalized[strings.ToLower(col)] = struct{}{}
}
if _, ok := normalized["snapshottime"]; ok {
return "SnapshotTime"
}
if _, ok := normalized["samplespresent"]; ok {
return "SamplesPresent"
}
if _, ok := normalized["avgispresent"]; ok {
return "AvgIsPresent"
}
if _, ok := normalized["name"]; ok {
return "Name"
}
return ""
}
func scanRowValues(rows *sqlx.Rows, columnCount int) ([]interface{}, error) {
rawValues := make([]interface{}, columnCount)
scanArgs := make([]interface{}, columnCount)
for i := range rawValues {
scanArgs[i] = &rawValues[i]
}
if err := rows.Scan(scanArgs...); err != nil {
return nil, err
}
return rawValues, nil
}
func normalizeCellValue(value interface{}) interface{} {
switch v := value.(type) {
case nil:
return ""
case []byte:
return string(v)
case time.Time:
return v.Format(time.RFC3339)
default:
return v
}
}

View File

@@ -0,0 +1,674 @@
package tasks
import (
"context"
"database/sql"
"fmt"
"log/slog"
"os"
"strconv"
"strings"
"time"
"vctp/db/queries"
"vctp/internal/report"
"vctp/internal/vcenter"
"github.com/jmoiron/sqlx"
"github.com/vmware/govmomi/vim25/mo"
"github.com/vmware/govmomi/vim25/types"
)
type inventorySnapshotRow struct {
InventoryId sql.NullInt64
Name string
Vcenter string
VmId sql.NullString
EventKey sql.NullString
CloudId sql.NullString
CreationTime sql.NullInt64
DeletionTime sql.NullInt64
ResourcePool sql.NullString
VmType sql.NullString
Datacenter sql.NullString
Cluster sql.NullString
Folder sql.NullString
ProvisionedDisk sql.NullFloat64
InitialVcpus sql.NullInt64
InitialRam sql.NullInt64
IsTemplate string
PoweredOn string
SrmPlaceholder string
VmUuid sql.NullString
SnapshotTime int64
IsPresent string
}
// RunVcenterSnapshotHourly records hourly inventory snapshots into a daily table.
func (c *CronTask) RunVcenterSnapshotHourly(ctx context.Context, logger *slog.Logger) error {
startTime := time.Now()
tableName, err := dailyInventoryTableName(startTime)
if err != nil {
return err
}
dbConn := c.Database.DB()
if err := ensureDailyInventoryTable(ctx, dbConn, tableName); err != nil {
return err
}
// reload settings in case vcenter list has changed
c.Settings.ReadYMLSettings()
for _, url := range c.Settings.Values.Settings.VcenterAddresses {
c.Logger.Debug("connecting to vcenter for hourly snapshot", "url", url)
vc := vcenter.New(c.Logger, c.VcCreds)
vc.Login(url)
vcVms, err := vc.GetAllVmReferences()
if err != nil {
c.Logger.Error("unable to get VMs from vcenter", "error", err, "url", url)
vc.Logout()
continue
}
inventoryRows, err := c.Database.Queries().GetInventoryByVcenter(ctx, url)
if err != nil {
c.Logger.Error("unable to query inventory table", "error", err, "url", url)
vc.Logout()
continue
}
inventoryByVmID := make(map[string]queries.Inventory, len(inventoryRows))
for _, inv := range inventoryRows {
if inv.VmId.Valid {
inventoryByVmID[inv.VmId.String] = inv
}
}
presentSnapshots := make(map[string]inventorySnapshotRow, len(vcVms))
for _, vm := range vcVms {
if strings.HasPrefix(vm.Name(), "vCLS-") {
continue
}
vmObj, err := vc.ConvertObjToMoVM(vm)
if err != nil {
c.Logger.Error("failed to read VM details", "vm_id", vm.Reference().Value, "error", err)
continue
}
if vmObj.Config != nil && vmObj.Config.Template {
continue
}
var inv *queries.Inventory
if existing, ok := inventoryByVmID[vm.Reference().Value]; ok {
existingCopy := existing
inv = &existingCopy
}
row, err := snapshotFromVM(vmObj, vc, startTime, inv)
if err != nil {
c.Logger.Error("unable to build snapshot for VM", "vm_id", vm.Reference().Value, "error", err)
continue
}
row.IsPresent = "TRUE"
presentSnapshots[vm.Reference().Value] = row
}
for _, row := range presentSnapshots {
if err := insertDailyInventoryRow(ctx, dbConn, tableName, row); err != nil {
c.Logger.Error("failed to insert hourly snapshot", "error", err, "vm_id", row.VmId.String)
}
}
for _, inv := range inventoryRows {
vmID := inv.VmId.String
if vmID != "" {
if _, ok := presentSnapshots[vmID]; ok {
continue
}
}
row := snapshotFromInventory(inv, startTime)
row.IsPresent = "FALSE"
if err := insertDailyInventoryRow(ctx, dbConn, tableName, row); err != nil {
c.Logger.Error("failed to insert missing VM snapshot", "error", err, "vm_id", row.VmId.String)
}
}
vc.Logout()
}
c.Logger.Debug("Finished hourly vcenter snapshot")
return nil
}
// RunVcenterDailyAggregate summarizes hourly snapshots into a daily summary table.
func (c *CronTask) RunVcenterDailyAggregate(ctx context.Context, logger *slog.Logger) error {
targetTime := time.Now().Add(-time.Minute)
sourceTable, err := dailyInventoryTableName(targetTime)
if err != nil {
return err
}
summaryTable, err := dailySummaryTableName(targetTime)
if err != nil {
return err
}
dbConn := c.Database.DB()
if err := ensureDailySummaryTable(ctx, dbConn, summaryTable); err != nil {
return err
}
insertQuery := fmt.Sprintf(`
INSERT INTO %s (
"InventoryId", "Name", "Vcenter", "VmId", "EventKey", "CloudId", "CreationTime", "DeletionTime",
"ResourcePool", "VmType", "Datacenter", "Cluster", "Folder", "ProvisionedDisk", "InitialVcpus",
"InitialRam", "IsTemplate", "PoweredOn", "SrmPlaceholder", "VmUuid", "SamplesPresent"
)
SELECT
"InventoryId", "Name", "Vcenter", "VmId", "EventKey", "CloudId", "CreationTime", "DeletionTime",
"ResourcePool", "VmType", "Datacenter", "Cluster", "Folder", "ProvisionedDisk", "InitialVcpus",
"InitialRam", "IsTemplate", "PoweredOn", "SrmPlaceholder", "VmUuid",
SUM(CASE WHEN "IsPresent" = 'TRUE' THEN 1 ELSE 0 END) AS "SamplesPresent"
FROM %s
GROUP BY
"InventoryId", "Name", "Vcenter", "VmId", "EventKey", "CloudId", "CreationTime", "DeletionTime",
"ResourcePool", "VmType", "Datacenter", "Cluster", "Folder", "ProvisionedDisk", "InitialVcpus",
"InitialRam", "IsTemplate", "PoweredOn", "SrmPlaceholder", "VmUuid";
`, summaryTable, sourceTable)
if _, err := dbConn.ExecContext(ctx, insertQuery); err != nil {
c.Logger.Error("failed to aggregate daily inventory", "error", err, "source_table", sourceTable)
return err
}
c.Logger.Debug("Finished daily inventory aggregation", "source_table", sourceTable, "summary_table", summaryTable)
return nil
}
// RunVcenterMonthlyAggregate summarizes the previous month's daily snapshots.
func (c *CronTask) RunVcenterMonthlyAggregate(ctx context.Context, logger *slog.Logger) error {
now := time.Now()
firstOfThisMonth := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
targetMonth := firstOfThisMonth.AddDate(0, -1, 0)
monthPrefix := fmt.Sprintf("inventory_daily_%s", targetMonth.Format("200601"))
dailyTables, err := report.ListTablesByPrefix(ctx, c.Database, monthPrefix)
if err != nil {
return err
}
if len(dailyTables) == 0 {
return fmt.Errorf("no daily snapshot tables found for %s", targetMonth.Format("2006-01"))
}
monthlyTable, err := monthlySummaryTableName(targetMonth)
if err != nil {
return err
}
dbConn := c.Database.DB()
if err := ensureMonthlySummaryTable(ctx, dbConn, monthlyTable); err != nil {
return err
}
unionQuery := buildUnionQuery(dailyTables, []string{
`"InventoryId"`, `"Name"`, `"Vcenter"`, `"VmId"`, `"EventKey"`, `"CloudId"`, `"CreationTime"`,
`"DeletionTime"`, `"ResourcePool"`, `"VmType"`, `"Datacenter"`, `"Cluster"`, `"Folder"`,
`"ProvisionedDisk"`, `"InitialVcpus"`, `"InitialRam"`, `"IsTemplate"`, `"PoweredOn"`,
`"SrmPlaceholder"`, `"VmUuid"`, `"IsPresent"`,
})
if strings.TrimSpace(unionQuery) == "" {
return fmt.Errorf("no valid daily snapshot tables found for %s", targetMonth.Format("2006-01"))
}
insertQuery := fmt.Sprintf(`
INSERT INTO %s (
"InventoryId", "Name", "Vcenter", "VmId", "EventKey", "CloudId", "CreationTime", "DeletionTime",
"ResourcePool", "VmType", "Datacenter", "Cluster", "Folder", "ProvisionedDisk", "InitialVcpus",
"InitialRam", "IsTemplate", "PoweredOn", "SrmPlaceholder", "VmUuid",
"AvgVcpus", "AvgRam", "AvgIsPresent"
)
SELECT
"InventoryId", "Name", "Vcenter", "VmId", "EventKey", "CloudId", "CreationTime", "DeletionTime",
"ResourcePool", "VmType", "Datacenter", "Cluster", "Folder", "ProvisionedDisk", "InitialVcpus",
"InitialRam", "IsTemplate", "PoweredOn", "SrmPlaceholder", "VmUuid",
AVG(CASE WHEN "InitialVcpus" IS NOT NULL THEN "InitialVcpus" END) AS "AvgVcpus",
AVG(CASE WHEN "InitialRam" IS NOT NULL THEN "InitialRam" END) AS "AvgRam",
AVG(CASE WHEN "IsPresent" = 'TRUE' THEN 1 ELSE 0 END) AS "AvgIsPresent"
FROM (
%s
) snapshots
GROUP BY
"InventoryId", "Name", "Vcenter", "VmId", "EventKey", "CloudId", "CreationTime", "DeletionTime",
"ResourcePool", "VmType", "Datacenter", "Cluster", "Folder", "ProvisionedDisk", "InitialVcpus",
"InitialRam", "IsTemplate", "PoweredOn", "SrmPlaceholder", "VmUuid";
`, monthlyTable, unionQuery)
if _, err := dbConn.ExecContext(ctx, insertQuery); err != nil {
c.Logger.Error("failed to aggregate monthly inventory", "error", err, "month", targetMonth.Format("2006-01"))
return err
}
c.Logger.Debug("Finished monthly inventory aggregation", "summary_table", monthlyTable)
return nil
}
// RunSnapshotCleanup drops hourly and daily snapshot tables older than retention.
func (c *CronTask) RunSnapshotCleanup(ctx context.Context, logger *slog.Logger) error {
now := time.Now()
hourlyMaxDays := getEnvInt("HOURLY_SNAPSHOT_MAX_AGE_DAYS", 60)
dailyMaxMonths := getEnvInt("DAILY_SNAPSHOT_MAX_AGE_MONTHS", 12)
hourlyCutoff := now.AddDate(0, 0, -hourlyMaxDays)
dailyCutoff := now.AddDate(0, -dailyMaxMonths, 0)
dbConn := c.Database.DB()
hourlyTables, err := report.ListTablesByPrefix(ctx, c.Database, "inventory_daily_")
if err != nil {
return err
}
for _, table := range hourlyTables {
if strings.HasPrefix(table, "inventory_daily_summary_") {
continue
}
tableDate, ok := parseSnapshotDate(table, "inventory_daily_", "20060102")
if !ok {
continue
}
if tableDate.Before(truncateDate(hourlyCutoff)) {
if err := dropSnapshotTable(ctx, dbConn, table); err != nil {
c.Logger.Error("failed to drop hourly snapshot table", "error", err, "table", table)
}
}
}
dailyTables, err := report.ListTablesByPrefix(ctx, c.Database, "inventory_daily_summary_")
if err != nil {
return err
}
for _, table := range dailyTables {
tableDate, ok := parseSnapshotDate(table, "inventory_daily_summary_", "20060102")
if !ok {
continue
}
if tableDate.Before(truncateDate(dailyCutoff)) {
if err := dropSnapshotTable(ctx, dbConn, table); err != nil {
c.Logger.Error("failed to drop daily snapshot table", "error", err, "table", table)
}
}
}
c.Logger.Debug("Finished snapshot cleanup")
return nil
}
func dailyInventoryTableName(t time.Time) (string, error) {
return safeTableName(fmt.Sprintf("inventory_daily_%s", t.Format("20060102")))
}
func dailySummaryTableName(t time.Time) (string, error) {
return safeTableName(fmt.Sprintf("inventory_daily_summary_%s", t.Format("20060102")))
}
func monthlySummaryTableName(t time.Time) (string, error) {
return safeTableName(fmt.Sprintf("inventory_monthly_summary_%s", t.Format("200601")))
}
func safeTableName(name string) (string, error) {
for _, r := range name {
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '_' {
continue
}
return "", fmt.Errorf("invalid table name: %s", name)
}
return name, nil
}
func ensureDailyInventoryTable(ctx context.Context, dbConn *sqlx.DB, tableName string) error {
ddl := fmt.Sprintf(`CREATE TABLE IF NOT EXISTS %s (
"InventoryId" BIGINT,
"Name" TEXT NOT NULL,
"Vcenter" TEXT NOT NULL,
"VmId" TEXT,
"EventKey" TEXT,
"CloudId" TEXT,
"CreationTime" BIGINT,
"DeletionTime" BIGINT,
"ResourcePool" TEXT,
"VmType" TEXT,
"Datacenter" TEXT,
"Cluster" TEXT,
"Folder" TEXT,
"ProvisionedDisk" REAL,
"InitialVcpus" BIGINT,
"InitialRam" BIGINT,
"IsTemplate" TEXT,
"PoweredOn" TEXT,
"SrmPlaceholder" TEXT,
"VmUuid" TEXT,
"SnapshotTime" BIGINT NOT NULL,
"IsPresent" TEXT NOT NULL
);`, tableName)
_, err := dbConn.ExecContext(ctx, ddl)
return err
}
func ensureDailySummaryTable(ctx context.Context, dbConn *sqlx.DB, tableName string) error {
ddl := fmt.Sprintf(`CREATE TABLE IF NOT EXISTS %s (
"InventoryId" BIGINT,
"Name" TEXT NOT NULL,
"Vcenter" TEXT NOT NULL,
"VmId" TEXT,
"EventKey" TEXT,
"CloudId" TEXT,
"CreationTime" BIGINT,
"DeletionTime" BIGINT,
"ResourcePool" TEXT,
"VmType" TEXT,
"Datacenter" TEXT,
"Cluster" TEXT,
"Folder" TEXT,
"ProvisionedDisk" REAL,
"InitialVcpus" BIGINT,
"InitialRam" BIGINT,
"IsTemplate" TEXT,
"PoweredOn" TEXT,
"SrmPlaceholder" TEXT,
"VmUuid" TEXT,
"SamplesPresent" BIGINT NOT NULL
);`, tableName)
_, err := dbConn.ExecContext(ctx, ddl)
return err
}
func ensureMonthlySummaryTable(ctx context.Context, dbConn *sqlx.DB, tableName string) error {
ddl := fmt.Sprintf(`CREATE TABLE IF NOT EXISTS %s (
"InventoryId" BIGINT,
"Name" TEXT NOT NULL,
"Vcenter" TEXT NOT NULL,
"VmId" TEXT,
"EventKey" TEXT,
"CloudId" TEXT,
"CreationTime" BIGINT,
"DeletionTime" BIGINT,
"ResourcePool" TEXT,
"VmType" TEXT,
"Datacenter" TEXT,
"Cluster" TEXT,
"Folder" TEXT,
"ProvisionedDisk" REAL,
"InitialVcpus" BIGINT,
"InitialRam" BIGINT,
"IsTemplate" TEXT,
"PoweredOn" TEXT,
"SrmPlaceholder" TEXT,
"VmUuid" TEXT,
"AvgVcpus" REAL,
"AvgRam" REAL,
"AvgIsPresent" REAL
);`, tableName)
_, err := dbConn.ExecContext(ctx, ddl)
return err
}
func buildUnionQuery(tables []string, columns []string) string {
queries := make([]string, 0, len(tables))
columnList := strings.Join(columns, ", ")
for _, table := range tables {
if _, err := safeTableName(table); err != nil {
continue
}
queries = append(queries, fmt.Sprintf("SELECT %s FROM %s", columnList, table))
}
return strings.Join(queries, "\nUNION ALL\n")
}
func parseSnapshotDate(table string, prefix string, layout string) (time.Time, bool) {
if !strings.HasPrefix(table, prefix) {
return time.Time{}, false
}
suffix := strings.TrimPrefix(table, prefix)
parsed, err := time.Parse(layout, suffix)
if err != nil {
return time.Time{}, false
}
return parsed, true
}
func truncateDate(t time.Time) time.Time {
return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())
}
func dropSnapshotTable(ctx context.Context, dbConn *sqlx.DB, table string) error {
if _, err := safeTableName(table); err != nil {
return err
}
_, err := dbConn.ExecContext(ctx, fmt.Sprintf("DROP TABLE IF EXISTS %s", table))
return err
}
func getEnvInt(key string, fallback int) int {
raw := strings.TrimSpace(os.Getenv(key))
if raw == "" {
return fallback
}
value, err := strconv.Atoi(raw)
if err != nil || value < 0 {
return fallback
}
return value
}
func snapshotFromVM(vmObject *mo.VirtualMachine, vc *vcenter.Vcenter, snapshotTime time.Time, inv *queries.Inventory) (inventorySnapshotRow, error) {
if vmObject == nil {
return inventorySnapshotRow{}, fmt.Errorf("missing VM object")
}
row := inventorySnapshotRow{
Name: vmObject.Name,
Vcenter: vc.Vurl,
VmId: sql.NullString{String: vmObject.Reference().Value, Valid: vmObject.Reference().Value != ""},
SnapshotTime: snapshotTime.Unix(),
}
if inv != nil {
row.InventoryId = sql.NullInt64{Int64: inv.Iid, Valid: inv.Iid > 0}
row.EventKey = inv.EventKey
row.CloudId = inv.CloudId
row.DeletionTime = inv.DeletionTime
row.VmType = inv.VmType
}
if vmObject.Config != nil {
row.VmUuid = sql.NullString{String: vmObject.Config.Uuid, Valid: vmObject.Config.Uuid != ""}
if !vmObject.Config.CreateDate.IsZero() {
row.CreationTime = sql.NullInt64{Int64: vmObject.Config.CreateDate.Unix(), Valid: true}
}
row.InitialVcpus = sql.NullInt64{Int64: int64(vmObject.Config.Hardware.NumCPU), Valid: vmObject.Config.Hardware.NumCPU > 0}
row.InitialRam = sql.NullInt64{Int64: int64(vmObject.Config.Hardware.MemoryMB), Valid: vmObject.Config.Hardware.MemoryMB > 0}
totalDiskBytes := int64(0)
for _, device := range vmObject.Config.Hardware.Device {
if disk, ok := device.(*types.VirtualDisk); ok {
totalDiskBytes += disk.CapacityInBytes
}
}
if totalDiskBytes > 0 {
row.ProvisionedDisk = sql.NullFloat64{Float64: float64(totalDiskBytes / 1024 / 1024 / 1024), Valid: true}
}
if vmObject.Config.ManagedBy != nil && vmObject.Config.ManagedBy.ExtensionKey == "com.vmware.vcDr" && vmObject.Config.ManagedBy.Type == "placeholderVm" {
row.SrmPlaceholder = "TRUE"
} else {
row.SrmPlaceholder = "FALSE"
}
if vmObject.Config.Template {
row.IsTemplate = "TRUE"
} else {
row.IsTemplate = "FALSE"
}
}
if vmObject.Runtime.PowerState == "poweredOff" {
row.PoweredOn = "FALSE"
} else {
row.PoweredOn = "TRUE"
}
if inv != nil {
row.ResourcePool = inv.ResourcePool
row.Datacenter = inv.Datacenter
row.Cluster = inv.Cluster
row.Folder = inv.Folder
if !row.CreationTime.Valid {
row.CreationTime = inv.CreationTime
}
if !row.ProvisionedDisk.Valid {
row.ProvisionedDisk = inv.ProvisionedDisk
}
if !row.InitialVcpus.Valid {
row.InitialVcpus = inv.InitialVcpus
}
if !row.InitialRam.Valid {
row.InitialRam = inv.InitialRam
}
if row.IsTemplate == "" {
row.IsTemplate = boolStringFromInterface(inv.IsTemplate)
}
if row.PoweredOn == "" {
row.PoweredOn = boolStringFromInterface(inv.PoweredOn)
}
if row.SrmPlaceholder == "" {
row.SrmPlaceholder = boolStringFromInterface(inv.SrmPlaceholder)
}
if !row.VmUuid.Valid {
row.VmUuid = inv.VmUuid
}
}
if row.ResourcePool.String == "" {
if rpName, err := vc.GetVmResourcePool(*vmObject); err == nil {
row.ResourcePool = sql.NullString{String: rpName, Valid: rpName != ""}
}
}
if row.Folder.String == "" {
if folderPath, err := vc.GetVMFolderPath(*vmObject); err == nil {
row.Folder = sql.NullString{String: folderPath, Valid: folderPath != ""}
}
}
if row.Cluster.String == "" {
if clusterName, err := vc.GetClusterFromHost(vmObject.Runtime.Host); err == nil {
row.Cluster = sql.NullString{String: clusterName, Valid: clusterName != ""}
}
}
if row.Datacenter.String == "" {
if dcName, err := vc.GetDatacenterForVM(*vmObject); err == nil {
row.Datacenter = sql.NullString{String: dcName, Valid: dcName != ""}
}
}
return row, nil
}
func snapshotFromInventory(inv queries.Inventory, snapshotTime time.Time) inventorySnapshotRow {
return inventorySnapshotRow{
InventoryId: sql.NullInt64{Int64: inv.Iid, Valid: inv.Iid > 0},
Name: inv.Name,
Vcenter: inv.Vcenter,
VmId: inv.VmId,
EventKey: inv.EventKey,
CloudId: inv.CloudId,
CreationTime: inv.CreationTime,
DeletionTime: inv.DeletionTime,
ResourcePool: inv.ResourcePool,
VmType: inv.VmType,
Datacenter: inv.Datacenter,
Cluster: inv.Cluster,
Folder: inv.Folder,
ProvisionedDisk: inv.ProvisionedDisk,
InitialVcpus: inv.InitialVcpus,
InitialRam: inv.InitialRam,
IsTemplate: boolStringFromInterface(inv.IsTemplate),
PoweredOn: boolStringFromInterface(inv.PoweredOn),
SrmPlaceholder: boolStringFromInterface(inv.SrmPlaceholder),
VmUuid: inv.VmUuid,
SnapshotTime: snapshotTime.Unix(),
}
}
func insertDailyInventoryRow(ctx context.Context, dbConn *sqlx.DB, tableName string, row inventorySnapshotRow) error {
query := fmt.Sprintf(`
INSERT INTO %s (
"InventoryId", "Name", "Vcenter", "VmId", "EventKey", "CloudId", "CreationTime", "DeletionTime",
"ResourcePool", "VmType", "Datacenter", "Cluster", "Folder", "ProvisionedDisk", "InitialVcpus",
"InitialRam", "IsTemplate", "PoweredOn", "SrmPlaceholder", "VmUuid", "SnapshotTime", "IsPresent"
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
`, tableName)
query = sqlx.Rebind(sqlx.BindType(dbConn.DriverName()), query)
_, err := dbConn.ExecContext(ctx, query,
row.InventoryId,
row.Name,
row.Vcenter,
row.VmId,
row.EventKey,
row.CloudId,
row.CreationTime,
row.DeletionTime,
row.ResourcePool,
row.VmType,
row.Datacenter,
row.Cluster,
row.Folder,
row.ProvisionedDisk,
row.InitialVcpus,
row.InitialRam,
row.IsTemplate,
row.PoweredOn,
row.SrmPlaceholder,
row.VmUuid,
row.SnapshotTime,
row.IsPresent,
)
return err
}
func boolStringFromInterface(value interface{}) string {
switch v := value.(type) {
case nil:
return ""
case string:
return v
case []byte:
return string(v)
case bool:
if v {
return "TRUE"
}
return "FALSE"
case int:
if v != 0 {
return "TRUE"
}
return "FALSE"
case int64:
if v != 0 {
return "TRUE"
}
return "FALSE"
default:
return fmt.Sprint(v)
}
}

97
main.go
View File

@@ -23,12 +23,14 @@ import (
)
var (
bindDisableTls bool
sha1ver string // sha1 revision used to build the program
buildTime string // when the executable was built
cronFrequency time.Duration
cronInvFrequency time.Duration
encryptionKey = []byte("5L1l3B5KvwOCzUHMAlCgsgUTRAYMfSpa")
bindDisableTls bool
sha1ver string // sha1 revision used to build the program
buildTime string // when the executable was built
cronFrequency time.Duration
cronInvFrequency time.Duration
cronSnapshotFrequency time.Duration
cronAggregateFrequency time.Duration
encryptionKey = []byte("5L1l3B5KvwOCzUHMAlCgsgUTRAYMfSpa")
)
func main() {
@@ -189,6 +191,30 @@ func main() {
}
logger.Debug("Setting VM inventory polling cronjob frequency to", "frequency", cronInvFrequency)
cronSnapshotFrequencyString := os.Getenv("VCENTER_INVENTORY_SNAPSHOT_SECONDS")
if cronSnapshotFrequencyString != "" {
cronSnapshotFrequency, err = time.ParseDuration(cronSnapshotFrequencyString)
if err != nil {
slog.Error("Can't convert VCENTER_INVENTORY_SNAPSHOT_SECONDS value to time duration. Defaulting to 3600", "value", cronSnapshotFrequencyString, "error", err)
cronSnapshotFrequency = time.Hour
}
} else {
cronSnapshotFrequency = time.Hour
}
logger.Debug("Setting VM inventory snapshot cronjob frequency to", "frequency", cronSnapshotFrequency)
cronAggregateFrequencyString := os.Getenv("VCENTER_INVENTORY_AGGREGATE_SECONDS")
if cronAggregateFrequencyString != "" {
cronAggregateFrequency, err = time.ParseDuration(cronAggregateFrequencyString)
if err != nil {
slog.Error("Can't convert VCENTER_INVENTORY_AGGREGATE_SECONDS value to time duration. Defaulting to 86400", "value", cronAggregateFrequencyString, "error", err)
cronAggregateFrequency = time.Hour * 24
}
} else {
cronAggregateFrequency = time.Hour * 24
}
logger.Debug("Setting VM inventory aggregation cronjob frequency to", "frequency", cronAggregateFrequency)
// start background processing for events stored in events table
startsAt := time.Now().Add(time.Second * 10)
job, err := c.NewJob(
@@ -219,6 +245,65 @@ func main() {
}
logger.Debug("Created vcenter inventory cron job", "job", job2.ID(), "starting_at", startsAt2)
startsAt3 := time.Now().Add(cronSnapshotFrequency)
if cronSnapshotFrequency == time.Hour {
startsAt3 = time.Now().Truncate(time.Hour).Add(time.Hour)
}
job3, err := c.NewJob(
gocron.DurationJob(cronSnapshotFrequency),
gocron.NewTask(func() {
ct.RunVcenterSnapshotHourly(ctx, logger)
}), gocron.WithSingletonMode(gocron.LimitModeReschedule),
gocron.WithStartAt(gocron.WithStartDateTime(startsAt3)),
)
if err != nil {
logger.Error("failed to start vcenter inventory snapshot cron job", "error", err)
os.Exit(1)
}
logger.Debug("Created vcenter inventory snapshot cron job", "job", job3.ID(), "starting_at", startsAt3)
startsAt4 := time.Now().Add(cronAggregateFrequency)
if cronAggregateFrequency == time.Hour*24 {
now := time.Now()
startsAt4 = time.Date(now.Year(), now.Month(), now.Day()+1, 0, 0, 0, 0, now.Location())
}
job4, err := c.NewJob(
gocron.DurationJob(cronAggregateFrequency),
gocron.NewTask(func() {
ct.RunVcenterDailyAggregate(ctx, logger)
}), gocron.WithSingletonMode(gocron.LimitModeReschedule),
gocron.WithStartAt(gocron.WithStartDateTime(startsAt4)),
)
if err != nil {
logger.Error("failed to start vcenter inventory aggregation cron job", "error", err)
os.Exit(1)
}
logger.Debug("Created vcenter inventory aggregation cron job", "job", job4.ID(), "starting_at", startsAt4)
job5, err := c.NewJob(
gocron.CronJob("0 0 1 * *", false),
gocron.NewTask(func() {
ct.RunVcenterMonthlyAggregate(ctx, logger)
}), gocron.WithSingletonMode(gocron.LimitModeReschedule),
)
if err != nil {
logger.Error("failed to start vcenter monthly aggregation cron job", "error", err)
os.Exit(1)
}
logger.Debug("Created vcenter monthly aggregation cron job", "job", job5.ID())
job6, err := c.NewJob(
gocron.CronJob("0 30 2 * *", false),
gocron.NewTask(func() {
ct.RunSnapshotCleanup(ctx, logger)
}), gocron.WithSingletonMode(gocron.LimitModeReschedule),
)
if err != nil {
logger.Error("failed to start snapshot cleanup cron job", "error", err)
os.Exit(1)
}
logger.Debug("Created snapshot cleanup cron job", "job", job6.ID())
// start cron scheduler
c.Start()

View File

@@ -3,16 +3,19 @@
# disable CGO for cross-compiling
export CGO_ENABLED=0
commit=$(git rev-parse HEAD)
#tag=$(git describe --tags --abbrev=0)
buildtime=$(TZ=Australia/Sydney date +%Y-%m-%dT%T%z)
git_version=$(git describe --tags --always --long --dirty)
package_name=vctp
commit=$(git rev-parse HEAD)
buildtime=$(date +%Y-%m-%dT%T%z)
#Extract the version from yml
package_version=$(grep 'version:' "$package_name.yml" | awk '{print $2}' | tr -d '"' | sed 's/^v//')
#platforms=("linux/amd64" "darwin/amd64")
platforms=("linux/amd64")
echo Building $package_name with git version: $git_version
echo Building::
echo - Version $package_version
echo - Commit $commit
echo - Build Time $buildtime
for platform in "${platforms[@]}"
do
platform_split=(${platform//\// })
@@ -25,14 +28,16 @@ do
starttime=$(TZ=Australia/Sydney date +%Y-%m-%dT%T%z)
echo "build commences at $starttime"
env GOOS=$GOOS GOARCH=$GOARCH go build -trimpath -ldflags="-X main.sha1ver=$commit -X main.buildTime=$buildtime" -o build/$output_name $package
env GOOS=$GOOS GOARCH=$GOARCH go build -trimpath -ldflags="-X main.version=$package_version -X main.commit=$commit -X main.buildTime=$buildtime" -o build/$output_name $package
if [ $? -ne 0 ]; then
echo 'An error has occurred! Aborting the script execution...'
exit 1
fi
gzip build/$output_name
#gzip build/$output_name
echo "build complete at $buildtime : $output_name"
sha256sum build/${output_name}.gz > build/${output_name}_checksum.txt
#sha256sum build/${output_name}.gz > build/${output_name}_checksum.txt
done
#nfpm package --config $package_name.yml --packager rpm --target build/
ls -lah build

51
scripts/update-swagger-ui.sh Executable file
View File

@@ -0,0 +1,51 @@
#!/usr/bin/env bash
set -euo pipefail
# Usage: ./update-swagger-ui.sh [version]
# Example: ./update-swagger-ui.sh v5.17.14
# If no version is provided, defaults below is used.
VERSION="${1:-v5.29.5}"
TARGET_DIR="server/router/swagger-ui-dist"
TARBALL_URL="https://github.com/swagger-api/swagger-ui/archive/refs/tags/${VERSION}.tar.gz"
echo ">> Fetching Swagger UI ${VERSION}"
tmpdir="$(mktemp -d)"
cleanup() { rm -rf "$tmpdir"; }
trap cleanup EXIT
# Requirements check
for cmd in curl tar; do
command -v "$cmd" >/dev/null 2>&1 || { echo "ERROR: $cmd not found"; exit 1; }
done
# Download & unpack
curl -fsSL "$TARBALL_URL" | tar -xz -C "$tmpdir"
SRC_DIR="${tmpdir}/swagger-ui-${VERSION#v}/dist"
if [[ ! -d "$SRC_DIR" ]]; then
echo "ERROR: Unpacked dist not found at $SRC_DIR"
exit 1
fi
# Replace target
rm -rf "$TARGET_DIR"
mkdir -p "$TARGET_DIR"
# Use cp -a for portability (avoids rsync dependency)
cp -a "${SRC_DIR}/." "$TARGET_DIR/"
INDEX="${TARGET_DIR}/swagger-initializer.js"
if [[ ! -f "$INDEX" ]]; then
echo "ERROR: ${INDEX} not found after copy"
exit 1
fi
echo ">> Patching swagger-initializer.js to point at /swagger.json"
sed -i -E \
-e 's#configUrl:[[:space:]]*["'\''"][^"'\''"]*["'\''"]#url: "/swagger.json"#' \
-e 's#url:[[:space:]]*["'\''"][^"'\''"]*["'\''"]#url: "/swagger.json"#' \
-e 's#urls:[[:space:]]*\[[^]]*\]#url: "/swagger.json"#' \
-e '/url:[[:space:]]*"[^\"]*swagger\.json"[[:space:]]*,?$/a\ validatorUrl: null,' \
"$INDEX"
echo ">> Done. Files are in ${TARGET_DIR}"

124
server/handler/snapshots.go Normal file
View File

@@ -0,0 +1,124 @@
package handler
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"vctp/components/views"
"vctp/internal/report"
"github.com/a-h/templ"
)
// SnapshotHourlyList renders the hourly snapshot list page.
// @Summary List hourly snapshots
// @Description Lists hourly inventory snapshot tables.
// @Tags snapshots
// @Produce text/html
// @Success 200 {string} string "HTML page"
// @Failure 500 {string} string "Server error"
// @Router /snapshots/hourly [get]
func (h *Handler) SnapshotHourlyList(w http.ResponseWriter, r *http.Request) {
h.renderSnapshotList(w, r, "inventory_daily_", "Hourly Inventory Snapshots", views.SnapshotHourlyList)
}
// SnapshotDailyList renders the daily snapshot list page.
// @Summary List daily snapshots
// @Description Lists daily summary snapshot tables.
// @Tags snapshots
// @Produce text/html
// @Success 200 {string} string "HTML page"
// @Failure 500 {string} string "Server error"
// @Router /snapshots/daily [get]
func (h *Handler) SnapshotDailyList(w http.ResponseWriter, r *http.Request) {
h.renderSnapshotList(w, r, "inventory_daily_summary_", "Daily Inventory Snapshots", views.SnapshotDailyList)
}
// SnapshotMonthlyList renders the monthly snapshot list page.
// @Summary List monthly snapshots
// @Description Lists monthly summary snapshot tables.
// @Tags snapshots
// @Produce text/html
// @Success 200 {string} string "HTML page"
// @Failure 500 {string} string "Server error"
// @Router /snapshots/monthly [get]
func (h *Handler) SnapshotMonthlyList(w http.ResponseWriter, r *http.Request) {
h.renderSnapshotList(w, r, "inventory_monthly_summary_", "Monthly Inventory Snapshots", views.SnapshotMonthlyList)
}
// SnapshotReportDownload streams a snapshot table as XLSX.
// @Summary Download snapshot report
// @Description Downloads a snapshot table as an XLSX file.
// @Tags snapshots
// @Produce application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
// @Param table query string true "Snapshot table name"
// @Success 200 {file} file "Snapshot XLSX report"
// @Failure 400 {object} map[string]string "Invalid request"
// @Failure 500 {object} map[string]string "Server error"
// @Router /api/report/snapshot [get]
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",
})
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),
})
return
}
filename := fmt.Sprintf("%s.xlsx", tableName)
w.Header().Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
w.Header().Set("File-Name", filename)
w.Write(reportData)
}
func (h *Handler) renderSnapshotList(w http.ResponseWriter, r *http.Request, prefix string, title string, renderer func([]views.SnapshotEntry) templ.Component) {
ctx := context.Background()
tables, err := report.ListTablesByPrefix(ctx, h.Database, prefix)
if err != nil {
h.Logger.Error("Failed to list snapshot tables", "error", err, "prefix", prefix)
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, "Unable to list snapshot tables: %s\n", err)
return
}
entries := make([]views.SnapshotEntry, 0, len(tables))
for _, table := range tables {
if prefix == "inventory_daily_" && strings.HasPrefix(table, "inventory_daily_summary_") {
continue
}
label := table
if parsed, ok := report.FormatSnapshotLabel(prefix, table); ok {
label = parsed
}
entries = append(entries, views.SnapshotEntry{
Label: label,
Link: "/api/report/snapshot?table=" + url.QueryEscape(table),
})
}
if err := renderer(entries).Render(r.Context(), w); err != nil {
h.Logger.Error("Failed to render snapshot list", "error", err, "title", title)
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, "Failed to render snapshot list")
}
}

1
server/router/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
swagger-ui-dist/

768
server/router/docs/docs.go Normal file
View File

@@ -0,0 +1,768 @@
// Package docs Code generated by swaggo/swag. DO NOT EDIT
package docs
import "github.com/swaggo/swag"
const docTemplate = `{
"schemes": {{ marshal .Schemes }},
"swagger": "2.0",
"info": {
"description": "{{escape .Description}}",
"title": "{{.Title}}",
"contact": {},
"version": "{{.Version}}"
},
"host": "{{.Host}}",
"basePath": "{{.BasePath}}",
"paths": {
"/": {
"get": {
"description": "Renders the main UI page.",
"produces": [
"text/html"
],
"tags": [
"ui"
],
"summary": "Home page",
"responses": {
"200": {
"description": "HTML page",
"schema": {
"type": "string"
}
},
"500": {
"description": "Render failed",
"schema": {
"type": "string"
}
}
}
}
},
"/api/cleanup/updates": {
"delete": {
"description": "Removes update records that are no longer associated with a VM.",
"produces": [
"text/plain"
],
"tags": [
"maintenance"
],
"summary": "Cleanup updates",
"responses": {
"200": {
"description": "Cleanup completed",
"schema": {
"type": "string"
}
},
"500": {
"description": "Server error",
"schema": {
"type": "string"
}
}
}
}
},
"/api/cleanup/vcenter": {
"delete": {
"description": "Removes all inventory entries associated with a vCenter URL.",
"produces": [
"application/json"
],
"tags": [
"maintenance"
],
"summary": "Cleanup vCenter inventory",
"parameters": [
{
"type": "string",
"description": "vCenter URL",
"name": "vc_url",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "Cleanup completed",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"400": {
"description": "Invalid request",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/api/encrypt": {
"post": {
"description": "Encrypts a plaintext value and returns the ciphertext.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"crypto"
],
"summary": "Encrypt data",
"parameters": [
{
"description": "Plaintext payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
],
"responses": {
"200": {
"description": "Ciphertext response",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Server error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/api/event/vm/create": {
"post": {
"description": "Parses a VM create CloudEvent and stores the event data.",
"consumes": [
"application/json"
],
"produces": [
"text/plain"
],
"tags": [
"events"
],
"summary": "Record VM create event",
"parameters": [
{
"description": "CloudEvent payload",
"name": "event",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.CloudEventReceived"
}
}
],
"responses": {
"200": {
"description": "Create event processed",
"schema": {
"type": "string"
}
},
"400": {
"description": "Invalid request",
"schema": {
"type": "string"
}
},
"500": {
"description": "Server error",
"schema": {
"type": "string"
}
}
}
}
},
"/api/event/vm/delete": {
"post": {
"description": "Parses a VM delete CloudEvent and marks the VM as deleted in inventory.",
"consumes": [
"application/json"
],
"produces": [
"text/plain"
],
"tags": [
"events"
],
"summary": "Record VM delete event",
"parameters": [
{
"description": "CloudEvent payload",
"name": "event",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.CloudEventReceived"
}
}
],
"responses": {
"200": {
"description": "Delete event processed",
"schema": {
"type": "string"
}
},
"400": {
"description": "Invalid request",
"schema": {
"type": "string"
}
},
"500": {
"description": "Server error",
"schema": {
"type": "string"
}
}
}
}
},
"/api/event/vm/modify": {
"post": {
"description": "Parses a VM modify CloudEvent and creates an update record when relevant changes are detected.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"events"
],
"summary": "Record VM modify event",
"parameters": [
{
"description": "CloudEvent payload",
"name": "event",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.CloudEventReceived"
}
}
],
"responses": {
"200": {
"description": "Modify event processed",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"202": {
"description": "No relevant changes",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Server error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/api/event/vm/move": {
"post": {
"description": "Parses a VM move CloudEvent and creates an update record.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"events"
],
"summary": "Record VM move event",
"parameters": [
{
"description": "CloudEvent payload",
"name": "event",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.CloudEventReceived"
}
}
],
"responses": {
"200": {
"description": "Move event processed",
"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/import/vm": {
"post": {
"description": "Imports existing VM inventory data in bulk.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"inventory"
],
"summary": "Import VMs",
"parameters": [
{
"description": "Bulk import payload",
"name": "import",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.ImportReceived"
}
}
],
"responses": {
"200": {
"description": "Import processed",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Server error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/api/inventory/vm/delete": {
"delete": {
"description": "Removes a VM inventory entry by VM ID and datacenter name.",
"produces": [
"application/json"
],
"tags": [
"inventory"
],
"summary": "Cleanup VM inventory entry",
"parameters": [
{
"type": "string",
"description": "VM ID",
"name": "vm_id",
"in": "query",
"required": true
},
{
"type": "string",
"description": "Datacenter name",
"name": "datacenter_name",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "Cleanup completed",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"400": {
"description": "Invalid request",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/api/inventory/vm/update": {
"post": {
"description": "Queries vCenter and updates inventory records with missing details.",
"produces": [
"text/plain"
],
"tags": [
"inventory"
],
"summary": "Refresh VM details",
"responses": {
"200": {
"description": "Update completed",
"schema": {
"type": "string"
}
},
"500": {
"description": "Server error",
"schema": {
"type": "string"
}
}
}
}
},
"/api/report/inventory": {
"get": {
"description": "Generates an inventory XLSX report and returns it as a file download.",
"produces": [
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
],
"tags": [
"reports"
],
"summary": "Download inventory report",
"responses": {
"200": {
"description": "Inventory XLSX report",
"schema": {
"type": "file"
}
},
"500": {
"description": "Report generation failed",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/api/report/snapshot": {
"get": {
"description": "Downloads a snapshot table as an XLSX file.",
"produces": [
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
],
"tags": [
"snapshots"
],
"summary": "Download snapshot report",
"parameters": [
{
"type": "string",
"description": "Snapshot table name",
"name": "table",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "Snapshot XLSX report",
"schema": {
"type": "file"
}
},
"400": {
"description": "Invalid request",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Server error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/api/report/updates": {
"get": {
"description": "Generates an updates XLSX report and returns it as a file download.",
"produces": [
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
],
"tags": [
"reports"
],
"summary": "Download updates report",
"responses": {
"200": {
"description": "Updates XLSX report",
"schema": {
"type": "file"
}
},
"500": {
"description": "Report generation failed",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/snapshots/daily": {
"get": {
"description": "Lists daily summary snapshot tables.",
"produces": [
"text/html"
],
"tags": [
"snapshots"
],
"summary": "List daily snapshots",
"responses": {
"200": {
"description": "HTML page",
"schema": {
"type": "string"
}
},
"500": {
"description": "Server error",
"schema": {
"type": "string"
}
}
}
}
},
"/snapshots/hourly": {
"get": {
"description": "Lists hourly inventory snapshot tables.",
"produces": [
"text/html"
],
"tags": [
"snapshots"
],
"summary": "List hourly snapshots",
"responses": {
"200": {
"description": "HTML page",
"schema": {
"type": "string"
}
},
"500": {
"description": "Server error",
"schema": {
"type": "string"
}
}
}
}
},
"/snapshots/monthly": {
"get": {
"description": "Lists monthly summary snapshot tables.",
"produces": [
"text/html"
],
"tags": [
"snapshots"
],
"summary": "List monthly snapshots",
"responses": {
"200": {
"description": "HTML page",
"schema": {
"type": "string"
}
},
"500": {
"description": "Server error",
"schema": {
"type": "string"
}
}
}
}
}
},
"definitions": {
"models.CloudEventReceived": {
"type": "object"
},
"models.CloudEventResourcePool": {
"type": "object",
"properties": {
"Name": {
"type": "string"
},
"ResourcePool": {
"type": "object",
"properties": {
"Type": {
"type": "string"
},
"Value": {
"type": "string"
}
}
}
}
},
"models.CloudEventVm": {
"type": "object",
"properties": {
"Name": {
"type": "string"
},
"Vm": {
"type": "object",
"properties": {
"Type": {
"type": "string"
},
"Value": {
"type": "string"
}
}
}
}
},
"models.ImportReceived": {
"type": "object",
"properties": {
"Cluster": {
"type": "string"
},
"CreationTime": {
"type": "integer"
},
"Datacenter": {
"type": "string"
},
"Folder": {
"type": "string"
},
"InitialRam": {
"type": "integer"
},
"InitialVcpus": {
"type": "integer"
},
"Name": {
"type": "string"
},
"PowerState": {
"type": "integer"
},
"ProvisionedDisk": {
"type": "number"
},
"ResourcePool": {
"type": "string"
},
"Vcenter": {
"type": "string"
},
"VmId": {
"type": "string"
}
}
}
}
}`
// SwaggerInfo holds exported Swagger Info so clients can modify it
var SwaggerInfo = &swag.Spec{
Version: "",
Host: "",
BasePath: "",
Schemes: []string{},
Title: "",
Description: "",
InfoInstanceName: "swagger",
SwaggerTemplate: docTemplate,
LeftDelim: "{{",
RightDelim: "}}",
}
func init() {
swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo)
}

View File

@@ -0,0 +1,739 @@
{
"swagger": "2.0",
"info": {
"contact": {}
},
"paths": {
"/": {
"get": {
"description": "Renders the main UI page.",
"produces": [
"text/html"
],
"tags": [
"ui"
],
"summary": "Home page",
"responses": {
"200": {
"description": "HTML page",
"schema": {
"type": "string"
}
},
"500": {
"description": "Render failed",
"schema": {
"type": "string"
}
}
}
}
},
"/api/cleanup/updates": {
"delete": {
"description": "Removes update records that are no longer associated with a VM.",
"produces": [
"text/plain"
],
"tags": [
"maintenance"
],
"summary": "Cleanup updates",
"responses": {
"200": {
"description": "Cleanup completed",
"schema": {
"type": "string"
}
},
"500": {
"description": "Server error",
"schema": {
"type": "string"
}
}
}
}
},
"/api/cleanup/vcenter": {
"delete": {
"description": "Removes all inventory entries associated with a vCenter URL.",
"produces": [
"application/json"
],
"tags": [
"maintenance"
],
"summary": "Cleanup vCenter inventory",
"parameters": [
{
"type": "string",
"description": "vCenter URL",
"name": "vc_url",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "Cleanup completed",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"400": {
"description": "Invalid request",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/api/encrypt": {
"post": {
"description": "Encrypts a plaintext value and returns the ciphertext.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"crypto"
],
"summary": "Encrypt data",
"parameters": [
{
"description": "Plaintext payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
],
"responses": {
"200": {
"description": "Ciphertext response",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Server error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/api/event/vm/create": {
"post": {
"description": "Parses a VM create CloudEvent and stores the event data.",
"consumes": [
"application/json"
],
"produces": [
"text/plain"
],
"tags": [
"events"
],
"summary": "Record VM create event",
"parameters": [
{
"description": "CloudEvent payload",
"name": "event",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.CloudEventReceived"
}
}
],
"responses": {
"200": {
"description": "Create event processed",
"schema": {
"type": "string"
}
},
"400": {
"description": "Invalid request",
"schema": {
"type": "string"
}
},
"500": {
"description": "Server error",
"schema": {
"type": "string"
}
}
}
}
},
"/api/event/vm/delete": {
"post": {
"description": "Parses a VM delete CloudEvent and marks the VM as deleted in inventory.",
"consumes": [
"application/json"
],
"produces": [
"text/plain"
],
"tags": [
"events"
],
"summary": "Record VM delete event",
"parameters": [
{
"description": "CloudEvent payload",
"name": "event",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.CloudEventReceived"
}
}
],
"responses": {
"200": {
"description": "Delete event processed",
"schema": {
"type": "string"
}
},
"400": {
"description": "Invalid request",
"schema": {
"type": "string"
}
},
"500": {
"description": "Server error",
"schema": {
"type": "string"
}
}
}
}
},
"/api/event/vm/modify": {
"post": {
"description": "Parses a VM modify CloudEvent and creates an update record when relevant changes are detected.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"events"
],
"summary": "Record VM modify event",
"parameters": [
{
"description": "CloudEvent payload",
"name": "event",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.CloudEventReceived"
}
}
],
"responses": {
"200": {
"description": "Modify event processed",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"202": {
"description": "No relevant changes",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Server error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/api/event/vm/move": {
"post": {
"description": "Parses a VM move CloudEvent and creates an update record.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"events"
],
"summary": "Record VM move event",
"parameters": [
{
"description": "CloudEvent payload",
"name": "event",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.CloudEventReceived"
}
}
],
"responses": {
"200": {
"description": "Move event processed",
"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/import/vm": {
"post": {
"description": "Imports existing VM inventory data in bulk.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"inventory"
],
"summary": "Import VMs",
"parameters": [
{
"description": "Bulk import payload",
"name": "import",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.ImportReceived"
}
}
],
"responses": {
"200": {
"description": "Import processed",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Server error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/api/inventory/vm/delete": {
"delete": {
"description": "Removes a VM inventory entry by VM ID and datacenter name.",
"produces": [
"application/json"
],
"tags": [
"inventory"
],
"summary": "Cleanup VM inventory entry",
"parameters": [
{
"type": "string",
"description": "VM ID",
"name": "vm_id",
"in": "query",
"required": true
},
{
"type": "string",
"description": "Datacenter name",
"name": "datacenter_name",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "Cleanup completed",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"400": {
"description": "Invalid request",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/api/inventory/vm/update": {
"post": {
"description": "Queries vCenter and updates inventory records with missing details.",
"produces": [
"text/plain"
],
"tags": [
"inventory"
],
"summary": "Refresh VM details",
"responses": {
"200": {
"description": "Update completed",
"schema": {
"type": "string"
}
},
"500": {
"description": "Server error",
"schema": {
"type": "string"
}
}
}
}
},
"/api/report/inventory": {
"get": {
"description": "Generates an inventory XLSX report and returns it as a file download.",
"produces": [
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
],
"tags": [
"reports"
],
"summary": "Download inventory report",
"responses": {
"200": {
"description": "Inventory XLSX report",
"schema": {
"type": "file"
}
},
"500": {
"description": "Report generation failed",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/api/report/snapshot": {
"get": {
"description": "Downloads a snapshot table as an XLSX file.",
"produces": [
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
],
"tags": [
"snapshots"
],
"summary": "Download snapshot report",
"parameters": [
{
"type": "string",
"description": "Snapshot table name",
"name": "table",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "Snapshot XLSX report",
"schema": {
"type": "file"
}
},
"400": {
"description": "Invalid request",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Server error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/api/report/updates": {
"get": {
"description": "Generates an updates XLSX report and returns it as a file download.",
"produces": [
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
],
"tags": [
"reports"
],
"summary": "Download updates report",
"responses": {
"200": {
"description": "Updates XLSX report",
"schema": {
"type": "file"
}
},
"500": {
"description": "Report generation failed",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/snapshots/daily": {
"get": {
"description": "Lists daily summary snapshot tables.",
"produces": [
"text/html"
],
"tags": [
"snapshots"
],
"summary": "List daily snapshots",
"responses": {
"200": {
"description": "HTML page",
"schema": {
"type": "string"
}
},
"500": {
"description": "Server error",
"schema": {
"type": "string"
}
}
}
}
},
"/snapshots/hourly": {
"get": {
"description": "Lists hourly inventory snapshot tables.",
"produces": [
"text/html"
],
"tags": [
"snapshots"
],
"summary": "List hourly snapshots",
"responses": {
"200": {
"description": "HTML page",
"schema": {
"type": "string"
}
},
"500": {
"description": "Server error",
"schema": {
"type": "string"
}
}
}
}
},
"/snapshots/monthly": {
"get": {
"description": "Lists monthly summary snapshot tables.",
"produces": [
"text/html"
],
"tags": [
"snapshots"
],
"summary": "List monthly snapshots",
"responses": {
"200": {
"description": "HTML page",
"schema": {
"type": "string"
}
},
"500": {
"description": "Server error",
"schema": {
"type": "string"
}
}
}
}
}
},
"definitions": {
"models.CloudEventReceived": {
"type": "object"
},
"models.CloudEventResourcePool": {
"type": "object",
"properties": {
"Name": {
"type": "string"
},
"ResourcePool": {
"type": "object",
"properties": {
"Type": {
"type": "string"
},
"Value": {
"type": "string"
}
}
}
}
},
"models.CloudEventVm": {
"type": "object",
"properties": {
"Name": {
"type": "string"
},
"Vm": {
"type": "object",
"properties": {
"Type": {
"type": "string"
},
"Value": {
"type": "string"
}
}
}
}
},
"models.ImportReceived": {
"type": "object",
"properties": {
"Cluster": {
"type": "string"
},
"CreationTime": {
"type": "integer"
},
"Datacenter": {
"type": "string"
},
"Folder": {
"type": "string"
},
"InitialRam": {
"type": "integer"
},
"InitialVcpus": {
"type": "integer"
},
"Name": {
"type": "string"
},
"PowerState": {
"type": "integer"
},
"ProvisionedDisk": {
"type": "number"
},
"ResourcePool": {
"type": "string"
},
"Vcenter": {
"type": "string"
},
"VmId": {
"type": "string"
}
}
}
}
}

View File

@@ -0,0 +1,483 @@
definitions:
models.CloudEventReceived:
type: object
models.CloudEventResourcePool:
properties:
Name:
type: string
ResourcePool:
properties:
Type:
type: string
Value:
type: string
type: object
type: object
models.CloudEventVm:
properties:
Name:
type: string
Vm:
properties:
Type:
type: string
Value:
type: string
type: object
type: object
models.ImportReceived:
properties:
Cluster:
type: string
CreationTime:
type: integer
Datacenter:
type: string
Folder:
type: string
InitialRam:
type: integer
InitialVcpus:
type: integer
Name:
type: string
PowerState:
type: integer
ProvisionedDisk:
type: number
ResourcePool:
type: string
Vcenter:
type: string
VmId:
type: string
type: object
info:
contact: {}
paths:
/:
get:
description: Renders the main UI page.
produces:
- text/html
responses:
"200":
description: HTML page
schema:
type: string
"500":
description: Render failed
schema:
type: string
summary: Home page
tags:
- ui
/api/cleanup/updates:
delete:
description: Removes update records that are no longer associated with a VM.
produces:
- text/plain
responses:
"200":
description: Cleanup completed
schema:
type: string
"500":
description: Server error
schema:
type: string
summary: Cleanup updates
tags:
- maintenance
/api/cleanup/vcenter:
delete:
description: Removes all inventory entries associated with a vCenter URL.
parameters:
- description: vCenter URL
in: query
name: vc_url
required: true
type: string
produces:
- application/json
responses:
"200":
description: Cleanup completed
schema:
additionalProperties:
type: string
type: object
"400":
description: Invalid request
schema:
additionalProperties:
type: string
type: object
summary: Cleanup vCenter inventory
tags:
- maintenance
/api/encrypt:
post:
consumes:
- application/json
description: Encrypts a plaintext value and returns the ciphertext.
parameters:
- description: Plaintext payload
in: body
name: payload
required: true
schema:
additionalProperties:
type: string
type: object
produces:
- application/json
responses:
"200":
description: Ciphertext response
schema:
additionalProperties:
type: string
type: object
"500":
description: Server error
schema:
additionalProperties:
type: string
type: object
summary: Encrypt data
tags:
- crypto
/api/event/vm/create:
post:
consumes:
- application/json
description: Parses a VM create CloudEvent and stores the event data.
parameters:
- description: CloudEvent payload
in: body
name: event
required: true
schema:
$ref: '#/definitions/models.CloudEventReceived'
produces:
- text/plain
responses:
"200":
description: Create event processed
schema:
type: string
"400":
description: Invalid request
schema:
type: string
"500":
description: Server error
schema:
type: string
summary: Record VM create event
tags:
- events
/api/event/vm/delete:
post:
consumes:
- application/json
description: Parses a VM delete CloudEvent and marks the VM as deleted in inventory.
parameters:
- description: CloudEvent payload
in: body
name: event
required: true
schema:
$ref: '#/definitions/models.CloudEventReceived'
produces:
- text/plain
responses:
"200":
description: Delete event processed
schema:
type: string
"400":
description: Invalid request
schema:
type: string
"500":
description: Server error
schema:
type: string
summary: Record VM delete event
tags:
- events
/api/event/vm/modify:
post:
consumes:
- application/json
description: Parses a VM modify CloudEvent and creates an update record when
relevant changes are detected.
parameters:
- description: CloudEvent payload
in: body
name: event
required: true
schema:
$ref: '#/definitions/models.CloudEventReceived'
produces:
- application/json
responses:
"200":
description: Modify event processed
schema:
additionalProperties:
type: string
type: object
"202":
description: No relevant changes
schema:
additionalProperties:
type: string
type: object
"500":
description: Server error
schema:
additionalProperties:
type: string
type: object
summary: Record VM modify event
tags:
- events
/api/event/vm/move:
post:
consumes:
- application/json
description: Parses a VM move CloudEvent and creates an update record.
parameters:
- description: CloudEvent payload
in: body
name: event
required: true
schema:
$ref: '#/definitions/models.CloudEventReceived'
produces:
- application/json
responses:
"200":
description: Move event processed
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: Record VM move event
tags:
- events
/api/import/vm:
post:
consumes:
- application/json
description: Imports existing VM inventory data in bulk.
parameters:
- description: Bulk import payload
in: body
name: import
required: true
schema:
$ref: '#/definitions/models.ImportReceived'
produces:
- application/json
responses:
"200":
description: Import processed
schema:
additionalProperties:
type: string
type: object
"500":
description: Server error
schema:
additionalProperties:
type: string
type: object
summary: Import VMs
tags:
- inventory
/api/inventory/vm/delete:
delete:
description: Removes a VM inventory entry by VM ID and datacenter name.
parameters:
- description: VM ID
in: query
name: vm_id
required: true
type: string
- description: Datacenter name
in: query
name: datacenter_name
required: true
type: string
produces:
- application/json
responses:
"200":
description: Cleanup completed
schema:
additionalProperties:
type: string
type: object
"400":
description: Invalid request
schema:
additionalProperties:
type: string
type: object
summary: Cleanup VM inventory entry
tags:
- inventory
/api/inventory/vm/update:
post:
description: Queries vCenter and updates inventory records with missing details.
produces:
- text/plain
responses:
"200":
description: Update completed
schema:
type: string
"500":
description: Server error
schema:
type: string
summary: Refresh VM details
tags:
- inventory
/api/report/inventory:
get:
description: Generates an inventory XLSX report and returns it as a file download.
produces:
- application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
responses:
"200":
description: Inventory XLSX report
schema:
type: file
"500":
description: Report generation failed
schema:
additionalProperties:
type: string
type: object
summary: Download inventory report
tags:
- reports
/api/report/snapshot:
get:
description: Downloads a snapshot table as an XLSX file.
parameters:
- description: Snapshot table name
in: query
name: table
required: true
type: string
produces:
- application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
responses:
"200":
description: Snapshot XLSX report
schema:
type: file
"400":
description: Invalid request
schema:
additionalProperties:
type: string
type: object
"500":
description: Server error
schema:
additionalProperties:
type: string
type: object
summary: Download snapshot report
tags:
- snapshots
/api/report/updates:
get:
description: Generates an updates XLSX report and returns it as a file download.
produces:
- application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
responses:
"200":
description: Updates XLSX report
schema:
type: file
"500":
description: Report generation failed
schema:
additionalProperties:
type: string
type: object
summary: Download updates report
tags:
- reports
/snapshots/daily:
get:
description: Lists daily summary snapshot tables.
produces:
- text/html
responses:
"200":
description: HTML page
schema:
type: string
"500":
description: Server error
schema:
type: string
summary: List daily snapshots
tags:
- snapshots
/snapshots/hourly:
get:
description: Lists hourly inventory snapshot tables.
produces:
- text/html
responses:
"200":
description: HTML page
schema:
type: string
"500":
description: Server error
schema:
type: string
summary: List hourly snapshots
tags:
- snapshots
/snapshots/monthly:
get:
description: Lists monthly summary snapshot tables.
produces:
- text/html
responses:
"200":
description: HTML page
schema:
type: string
"500":
description: Server error
schema:
type: string
summary: List monthly snapshots
tags:
- snapshots
swagger: "2.0"

View File

@@ -1,6 +1,7 @@
package router
import (
"io/fs"
"log/slog"
"net/http"
"net/http/pprof"
@@ -46,10 +47,31 @@ func New(logger *slog.Logger, database db.Database, buildTime string, sha1ver st
mux.HandleFunc("/api/report/inventory", h.InventoryReportDownload)
mux.HandleFunc("/api/report/updates", h.UpdateReportDownload)
mux.HandleFunc("/api/report/snapshot", h.SnapshotReportDownload)
mux.HandleFunc("/snapshots/hourly", h.SnapshotHourlyList)
mux.HandleFunc("/snapshots/daily", h.SnapshotDailyList)
mux.HandleFunc("/snapshots/monthly", h.SnapshotMonthlyList)
// endpoint for encrypting vcenter credential
mux.HandleFunc("/api/encrypt", h.EncryptData)
// serve swagger related components from the embedded fs
swaggerSub, err := fs.Sub(swaggerUI, "swagger-ui-dist")
if err != nil {
logger.Error("failed to load swagger ui assets", "error", err)
} else {
mux.Handle("/swagger/", http.StripPrefix("/swagger/", http.FileServer(http.FS(swaggerSub))))
}
mux.HandleFunc("/swagger", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/swagger/", http.StatusPermanentRedirect)
})
mux.HandleFunc("/swagger.json", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write(swaggerSpec)
})
// Register pprof handlers
mux.HandleFunc("/debug/pprof/", pprof.Index)
mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)

View File

@@ -0,0 +1,11 @@
package router
import (
"embed"
)
//go:embed swagger-ui-dist/*
var swaggerUI embed.FS
//go:embed docs/swagger.json
var swaggerSpec []byte

View File

@@ -3,7 +3,8 @@ sql:
- engine: sqlite
queries:
- db/queries/query.sql
schema: db/migrations
schema:
- db/schema.sql
gen:
go:
package: queries