Compare commits
57 Commits
3ceba1a117
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| b4c52e296c | |||
| 0820cbb65e | |||
| f171c7f0eb | |||
| 7c76825813 | |||
| 9dc94bd405 | |||
| 6ee848edb5 | |||
| 63794be38d | |||
| 7273961cfc | |||
| d55916766b | |||
| ab01c0fc4d | |||
| 588a552e4c | |||
| 871904f63e | |||
| 268919219e | |||
| f0bacab729 | |||
| 75a5f31a2f | |||
| 1b91c73a18 | |||
| 2ea0f937c5 | |||
| e5e5be37a3 | |||
| 96567f6211 | |||
| 7971098caf | |||
| 645a20829f | |||
| debac1f684 | |||
| 8dee30ea97 | |||
| bba308ad28 | |||
| 3f985dcd4d | |||
| 0beafb5b00 | |||
| ea68331208 | |||
| 4d754ee263 | |||
| 11f7d36bfc | |||
| 50e9921955 | |||
| 457d9395f0 | |||
| 8b2c8ae85d | |||
| 434c7136e9 | |||
| 877b65f10b | |||
| 8df1d145f8 | |||
| 9be3a3d807 | |||
| 1fca81a7b3 | |||
| 56f021590d | |||
| 44ae2094f3 | |||
| 417c7c8127 | |||
| 7fac6e3920 | |||
| 98899e306f | |||
| cfc4efee0e | |||
| b9ab34db0a | |||
| 013ae4568e | |||
| 5c34a9eacd | |||
| 13af853c45 | |||
| 5130d37632 | |||
| b297b8293c | |||
| 7b600b2359 | |||
| aa4567d7c1 | |||
| ca8b39ba0e | |||
| 7400e08c54 | |||
| ffe0c01fd7 | |||
| 5cc89968d9 | |||
| 0f0bdf19c3 | |||
| 6d1bb09167 |
@@ -37,7 +37,7 @@ steps:
|
||||
- go install github.com/a-h/templ/cmd/templ@latest
|
||||
- go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
|
||||
- go install github.com/swaggo/swag/cmd/swag@latest
|
||||
- go install github.com/goreleaser/nfpm/v2/cmd/nfpm@latest
|
||||
# - go install github.com/goreleaser/nfpm/v2/cmd/nfpm@latest
|
||||
- sqlc generate
|
||||
- templ generate -path ./components
|
||||
- swag init --exclude "pkg.mod,pkg.build,pkg.tools" -o server/router/docs
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -10,6 +10,9 @@
|
||||
*.dylib
|
||||
vctp
|
||||
build/
|
||||
reports/
|
||||
reports/*.xlsx
|
||||
settings.yaml
|
||||
|
||||
# Certificates
|
||||
*.pem
|
||||
@@ -42,7 +45,7 @@ appengine-generated/
|
||||
tmp/
|
||||
pb_data/
|
||||
|
||||
# General
|
||||
# Generalis
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
131
README.md
131
README.md
@@ -1,4 +1,102 @@
|
||||
# Initial setup
|
||||
# Overview
|
||||
vCTP is a vSphere Chargeback Tracking Platform, designed for a specific customer, so some decisions may not be applicable for your use case.
|
||||
|
||||
## Snapshots and Reports
|
||||
- Hourly snapshots capture inventory per vCenter (concurrency via `hourly_snapshot_concurrency`).
|
||||
- Daily summaries aggregate the hourly snapshots for the day; monthly summaries aggregate daily summaries for the month.
|
||||
- Snapshots are registered in `snapshot_registry` so regeneration via `/api/snapshots/aggregate` can locate the correct tables (fallback scanning is also supported).
|
||||
- Reports (XLSX with totals/charts) are generated automatically after hourly, daily, and monthly jobs and written to a reports directory.
|
||||
- Prometheus metrics are exposed at `/metrics`:
|
||||
- Snapshots/aggregations: `vctp_hourly_snapshots_total`, `vctp_hourly_snapshots_failed_total`, `vctp_hourly_snapshot_last_unix`, `vctp_hourly_snapshot_last_rows`, `vctp_daily_aggregations_total`, `vctp_daily_aggregations_failed_total`, `vctp_daily_aggregation_duration_seconds`, `vctp_monthly_aggregations_total`, `vctp_monthly_aggregations_failed_total`, `vctp_monthly_aggregation_duration_seconds`, `vctp_reports_available`
|
||||
- vCenter health/perf: `vctp_vcenter_connect_failures_total{vcenter}`, `vctp_vcenter_snapshot_duration_seconds{vcenter}`, `vctp_vcenter_inventory_size{vcenter}`
|
||||
|
||||
## RPM Layout (summary)
|
||||
The RPM installs the service and defaults under `/usr/bin`, config under `/etc/dtms`, and data under `/var/lib/vctp`:
|
||||
- Binary: `/usr/bin/vctp-linux-amd64`
|
||||
- Systemd unit: `/etc/systemd/system/vctp.service`
|
||||
- Defaults/env: `/etc/dtms/vctp.yml` (override with `-settings`), `/etc/default/vctp` (environment)
|
||||
- TLS cert/key: `/etc/dtms/vctp.crt` and `/etc/dtms/vctp.key` (generated if absent)
|
||||
- Data: SQLite DB and reports default to `/var/lib/vctp` (reports under `/var/lib/vctp/reports`)
|
||||
- Scripts: preinstall/postinstall handle directory creation and permissions.
|
||||
|
||||
## Settings File
|
||||
Configuration now lives in the YAML settings file. By default the service reads
|
||||
`/etc/dtms/vctp.yml`, or you can override it with the `-settings` flag.
|
||||
|
||||
```shell
|
||||
vctp -settings /path/to/vctp.yml
|
||||
```
|
||||
|
||||
### Database Configuration
|
||||
By default the app uses SQLite and creates/opens `db.sqlite3`. You can opt into PostgreSQL
|
||||
by updating the settings file:
|
||||
|
||||
- `settings.database_driver`: `sqlite` (default) or `postgres`
|
||||
- `settings.database_url`: SQLite file path/DSN or PostgreSQL DSN
|
||||
|
||||
Examples:
|
||||
```yaml
|
||||
settings:
|
||||
database_driver: sqlite
|
||||
database_url: ./db.sqlite3
|
||||
|
||||
settings:
|
||||
database_driver: postgres
|
||||
database_url: postgres://user:pass@localhost:5432/vctp?sslmode=disable
|
||||
```
|
||||
|
||||
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 in the settings file:
|
||||
|
||||
- `settings.hourly_snapshot_max_age_days` (default: 60)
|
||||
- `settings.daily_snapshot_max_age_months` (default: 12)
|
||||
|
||||
### Settings Reference
|
||||
All configuration lives under the top-level `settings:` key in `vctp.yml`.
|
||||
|
||||
General:
|
||||
- `settings.log_level`: logging verbosity (e.g., `debug`, `info`, `warn`, `error`)
|
||||
- `settings.log_output`: log format, `text` or `json`
|
||||
|
||||
Database:
|
||||
- `settings.database_driver`: `sqlite` or `postgres`
|
||||
- `settings.database_url`: SQLite file path/DSN or PostgreSQL DSN
|
||||
|
||||
HTTP/TLS:
|
||||
- `settings.bind_ip`: IP address to bind the HTTP server
|
||||
- `settings.bind_port`: TCP port to bind the HTTP server
|
||||
- `settings.bind_disable_tls`: `true` to serve plain HTTP (no TLS)
|
||||
- `settings.tls_cert_filename`: PEM certificate path (TLS mode)
|
||||
- `settings.tls_key_filename`: PEM private key path (TLS mode)
|
||||
|
||||
vCenter:
|
||||
- `settings.vcenter_username`: vCenter username
|
||||
- `settings.vcenter_password`: vCenter password (encrypted at startup)
|
||||
- `settings.vcenter_insecure`: `true` to skip TLS verification
|
||||
- `settings.vcenter_event_polling_seconds`: event polling interval (0 disables)
|
||||
- `settings.vcenter_inventory_polling_seconds`: inventory polling interval (0 disables)
|
||||
- `settings.vcenter_inventory_snapshot_seconds`: hourly snapshot cadence (seconds)
|
||||
- `settings.vcenter_inventory_aggregate_seconds`: daily aggregation cadence (seconds)
|
||||
- `settings.vcenter_addresses`: list of vCenter SDK URLs to monitor
|
||||
|
||||
Snapshots:
|
||||
- `settings.hourly_snapshot_concurrency`: max concurrent vCenter snapshots (0 = unlimited)
|
||||
- `settings.hourly_snapshot_max_age_days`: retention for hourly tables
|
||||
- `settings.daily_snapshot_max_age_months`: retention for daily tables
|
||||
- `settings.snapshot_cleanup_cron`: cron expression for cleanup job
|
||||
- `settings.reports_dir`: directory to store generated XLSX reports (default: `/var/lib/vctp/reports`)
|
||||
- `settings.hourly_snapshot_retry_seconds`: interval for retrying failed hourly snapshots (default: 300 seconds)
|
||||
- `settings.hourly_snapshot_max_retries`: maximum retry attempts per vCenter snapshot (default: 3)
|
||||
|
||||
Filters/chargeback:
|
||||
- `settings.tenants_to_filter`: list of tenant name patterns to exclude
|
||||
- `settings.node_charge_clusters`: list of cluster name patterns for node chargeback
|
||||
- `settings.srm_activeactive_vms`: list of SRM Active/Active VM name patterns
|
||||
|
||||
# Developer setup
|
||||
|
||||
## Pre-requisite tools
|
||||
|
||||
@@ -28,27 +126,10 @@ Run `templ generate -path ./components` to generate code based on template files
|
||||
## Documentation
|
||||
Run `swag init --exclude "pkg.mod,pkg.build,pkg.tools" -o server/router/docs`
|
||||
|
||||
#### Database Configuration
|
||||
By default the app uses SQLite and creates/opens `db.sqlite3`. You can opt into PostgreSQL
|
||||
by setting environment variables:
|
||||
|
||||
- `DB_DRIVER`: `sqlite` (default) or `postgres`
|
||||
- `DB_URL`: SQLite file path/DSN or PostgreSQL DSN
|
||||
|
||||
Examples:
|
||||
```shell
|
||||
# SQLite (default)
|
||||
DB_DRIVER=sqlite DB_URL=./db.sqlite3
|
||||
|
||||
# PostgreSQL
|
||||
DB_DRIVER=postgres DB_URL=postgres://user:pass@localhost:5432/vctp?sslmode=disable
|
||||
```
|
||||
|
||||
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)
|
||||
## CI/CD (Drone)
|
||||
- `.drone.yml` defines a Docker pipeline:
|
||||
- Restore/build caches for Go modules/tools.
|
||||
- Build step installs generators (`templ`, `sqlc`, `swag`), regenerates code/docs, runs project scripts, and produces the `vctp-linux-amd64` binary.
|
||||
- RPM step packages via `nfpm` using `vctp.yml`, emits RPMs into `./build/`.
|
||||
- Optional SFTP deploy step uploads build artifacts (e.g., `vctp*`) to a remote host.
|
||||
- Cache rebuild step preserves Go caches across runs.
|
||||
|
||||
@@ -8,80 +8,11 @@ templ Header() {
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<meta name="description" content="vCTP API endpoint"/>
|
||||
<title>vCTP API</title>
|
||||
<link rel="icon" href="/favicon.ico"/>
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"/>
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"/>
|
||||
<script src="/assets/js/htmx@v2.0.2.min.js"></script>
|
||||
<link href={ "/assets/css/output@" + version.Value + ".css" } rel="stylesheet"/>
|
||||
<style>
|
||||
:root {
|
||||
--web2-blue: #3b82f6;
|
||||
--web2-cyan: #22d3ee;
|
||||
--web2-slate: #0f172a;
|
||||
--web2-card: #ffffff;
|
||||
--web2-shadow: 0 20px 40px rgba(15, 23, 42, 0.15);
|
||||
--web2-soft-shadow: 0 8px 20px rgba(15, 23, 42, 0.12);
|
||||
}
|
||||
body {
|
||||
font-family: "Trebuchet MS", "Lucida Grande", "Verdana", sans-serif;
|
||||
color: var(--web2-slate);
|
||||
}
|
||||
.web2-bg {
|
||||
background: radial-gradient(circle at top left, #e0f2fe 0%, #f8fafc 45%, #e2e8f0 100%);
|
||||
}
|
||||
.web2-shell {
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem 1.5rem 4rem;
|
||||
}
|
||||
.web2-header {
|
||||
background: linear-gradient(135deg, #0ea5e9 0%, #6366f1 100%);
|
||||
color: #fff;
|
||||
border-radius: 22px;
|
||||
box-shadow: var(--web2-shadow);
|
||||
padding: 1.5rem 2rem;
|
||||
}
|
||||
.web2-card {
|
||||
background: var(--web2-card);
|
||||
border-radius: 18px;
|
||||
box-shadow: var(--web2-soft-shadow);
|
||||
padding: 1.5rem 1.75rem;
|
||||
}
|
||||
.web2-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.35);
|
||||
padding: 0.35rem 0.8rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.85rem;
|
||||
letter-spacing: 0.02em;
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
.web2-link {
|
||||
color: var(--web2-blue);
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
.web2-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.web2-button {
|
||||
background: linear-gradient(180deg, #60a5fa 0%, #2563eb 100%);
|
||||
color: #fff;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 999px;
|
||||
box-shadow: 0 6px 16px rgba(37, 99, 235, 0.35);
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
}
|
||||
.web2-button:hover {
|
||||
filter: brightness(1.05);
|
||||
}
|
||||
.web2-list li {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 14px;
|
||||
padding: 0.85rem 1.1rem;
|
||||
box-shadow: 0 6px 16px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
</style>
|
||||
<link href="/assets/css/web3.css" rel="stylesheet"/>
|
||||
</head>
|
||||
}
|
||||
|
||||
@@ -31,20 +31,20 @@ func Header() templ.Component {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
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=\"")
|
||||
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><link rel=\"icon\" href=\"/favicon.ico\"><link rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"/favicon-16x16.png\"><link rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/favicon-32x32.png\"><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 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: `core/header.templ`, Line: 15, 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 = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" rel=\"stylesheet\"><style>\n\t\t\t:root {\n\t\t\t\t--web2-blue: #3b82f6;\n\t\t\t\t--web2-cyan: #22d3ee;\n\t\t\t\t--web2-slate: #0f172a;\n\t\t\t\t--web2-card: #ffffff;\n\t\t\t\t--web2-shadow: 0 20px 40px rgba(15, 23, 42, 0.15);\n\t\t\t\t--web2-soft-shadow: 0 8px 20px rgba(15, 23, 42, 0.12);\n\t\t\t}\n\t\t\tbody {\n\t\t\t\tfont-family: \"Trebuchet MS\", \"Lucida Grande\", \"Verdana\", sans-serif;\n\t\t\t\tcolor: var(--web2-slate);\n\t\t\t}\n\t\t\t.web2-bg {\n\t\t\t\tbackground: radial-gradient(circle at top left, #e0f2fe 0%, #f8fafc 45%, #e2e8f0 100%);\n\t\t\t}\n\t\t\t.web2-shell {\n\t\t\t\tmax-width: 1100px;\n\t\t\t\tmargin: 0 auto;\n\t\t\t\tpadding: 2rem 1.5rem 4rem;\n\t\t\t}\n\t\t\t.web2-header {\n\t\t\t\tbackground: linear-gradient(135deg, #0ea5e9 0%, #6366f1 100%);\n\t\t\t\tcolor: #fff;\n\t\t\t\tborder-radius: 22px;\n\t\t\t\tbox-shadow: var(--web2-shadow);\n\t\t\t\tpadding: 1.5rem 2rem;\n\t\t\t}\n\t\t\t.web2-card {\n\t\t\t\tbackground: var(--web2-card);\n\t\t\t\tborder-radius: 18px;\n\t\t\t\tbox-shadow: var(--web2-soft-shadow);\n\t\t\t\tpadding: 1.5rem 1.75rem;\n\t\t\t}\n\t\t\t.web2-pill {\n\t\t\t\tdisplay: inline-flex;\n\t\t\t\talign-items: center;\n\t\t\t\tgap: 0.4rem;\n\t\t\t\tbackground: rgba(255, 255, 255, 0.2);\n\t\t\t\tborder: 1px solid rgba(255, 255, 255, 0.35);\n\t\t\t\tpadding: 0.35rem 0.8rem;\n\t\t\t\tborder-radius: 999px;\n\t\t\t\tfont-size: 0.85rem;\n\t\t\t\tletter-spacing: 0.02em;\n\t\t\t\tbackdrop-filter: blur(6px);\n\t\t\t}\n\t\t\t.web2-link {\n\t\t\t\tcolor: var(--web2-blue);\n\t\t\t\ttext-decoration: none;\n\t\t\t\tfont-weight: 600;\n\t\t\t}\n\t\t\t.web2-link:hover {\n\t\t\t\ttext-decoration: underline;\n\t\t\t}\n\t\t\t.web2-button {\n\t\t\t\tbackground: linear-gradient(180deg, #60a5fa 0%, #2563eb 100%);\n\t\t\t\tcolor: #fff;\n\t\t\t\tpadding: 0.5rem 1rem;\n\t\t\t\tborder-radius: 999px;\n\t\t\t\tbox-shadow: 0 6px 16px rgba(37, 99, 235, 0.35);\n\t\t\t\tfont-weight: 600;\n\t\t\t\ttext-decoration: none;\n\t\t\t}\n\t\t\t.web2-button:hover {\n\t\t\t\tfilter: brightness(1.05);\n\t\t\t}\n\t\t\t.web2-list li {\n\t\t\t\tbackground: rgba(255, 255, 255, 0.9);\n\t\t\t\tborder-radius: 14px;\n\t\t\t\tpadding: 0.85rem 1.1rem;\n\t\t\t\tbox-shadow: 0 6px 16px rgba(15, 23, 42, 0.08);\n\t\t\t}\n\t\t</style></head>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" rel=\"stylesheet\"><link href=\"/assets/css/web3.css\" rel=\"stylesheet\"></head>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
@@ -20,13 +20,16 @@ templ Index(info BuildInfo) {
|
||||
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<div class="web2-pill">vCTP Console</div>
|
||||
<h1 class="mt-3 text-4xl font-bold">Build Intelligence Dashboard</h1>
|
||||
<p class="mt-2 text-sm opacity-90">A glossy, snapshot-ready view of what is running.</p>
|
||||
<h1 class="mt-3 text-4xl font-bold">Chargeback Intelligence Dashboard</h1>
|
||||
<p class="mt-2 text-sm text-slate-600">Point in time snapshots of consumption.</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<div class="web2-button-group">
|
||||
<a class="web2-button" href="/snapshots/hourly">Hourly Snapshots</a>
|
||||
<a class="web2-button" href="/snapshots/daily">Daily Snapshots</a>
|
||||
<a class="web2-button" href="/snapshots/monthly">Monthly Snapshots</a>
|
||||
<a class="web2-button" href="/vm/trace">VM Trace</a>
|
||||
<a class="web2-button" href="/vcenters">vCenters</a>
|
||||
<a class="web2-button" href="/swagger/">Swagger UI</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -47,14 +47,14 @@ func Index(info BuildInfo) templ.Component {
|
||||
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 web2-bg\"><main class=\"flex-grow web2-shell space-y-8\"><section class=\"web2-header\"><div class=\"flex flex-col gap-4 md:flex-row md:items-center md:justify-between\"><div><div class=\"web2-pill\">vCTP Console</div><h1 class=\"mt-3 text-4xl font-bold\">Build Intelligence Dashboard</h1><p class=\"mt-2 text-sm opacity-90\">A glossy, snapshot-ready view of what is running.</p></div><div class=\"flex flex-wrap gap-3\"><a class=\"web2-button\" href=\"/snapshots/hourly\">Hourly Snapshots</a> <a class=\"web2-button\" href=\"/snapshots/daily\">Daily Snapshots</a> <a class=\"web2-button\" href=\"/snapshots/monthly\">Monthly Snapshots</a></div></div></section><section class=\"grid gap-6 md:grid-cols-3\"><div class=\"web2-card\"><p class=\"text-xs uppercase tracking-[0.2em] text-slate-400\">Build Time</p><p class=\"mt-3 text-xl font-semibold\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<body class=\"flex flex-col min-h-screen web2-bg\"><main class=\"flex-grow web2-shell space-y-8\"><section class=\"web2-header\"><div class=\"flex flex-col gap-4 md:flex-row md:items-center md:justify-between\"><div><div class=\"web2-pill\">vCTP Console</div><h1 class=\"mt-3 text-4xl font-bold\">Chargeback Intelligence Dashboard</h1><p class=\"mt-2 text-sm text-slate-600\">Point in time snapshots of consumption.</p></div><div class=\"web2-button-group\"><a class=\"web2-button\" href=\"/snapshots/hourly\">Hourly Snapshots</a> <a class=\"web2-button\" href=\"/snapshots/daily\">Daily Snapshots</a> <a class=\"web2-button\" href=\"/snapshots/monthly\">Monthly Snapshots</a> <a class=\"web2-button\" href=\"/vm/trace\">VM Trace</a> <a class=\"web2-button\" href=\"/vcenters\">vCenters</a> <a class=\"web2-button\" href=\"/swagger/\">Swagger UI</a></div></div></section><section class=\"grid gap-6 md:grid-cols-3\"><div class=\"web2-card\"><p class=\"text-xs uppercase tracking-[0.2em] text-slate-400\">Build Time</p><p class=\"mt-3 text-xl font-semibold\">")
|
||||
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: 37, Col: 59}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/index.templ`, Line: 40, Col: 59}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -67,7 +67,7 @@ func Index(info BuildInfo) templ.Component {
|
||||
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: 41, Col: 57}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/index.templ`, Line: 44, Col: 57}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -80,7 +80,7 @@ func Index(info BuildInfo) templ.Component {
|
||||
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: 45, Col: 59}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/index.templ`, Line: 48, Col: 59}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
@@ -1,12 +1,56 @@
|
||||
package views
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"vctp/components/core"
|
||||
)
|
||||
|
||||
type SnapshotEntry struct {
|
||||
Label string
|
||||
Link string
|
||||
Count int64
|
||||
Group string
|
||||
}
|
||||
|
||||
type VcenterLink struct {
|
||||
Name string
|
||||
Link string
|
||||
}
|
||||
|
||||
type VcenterTotalsEntry struct {
|
||||
Snapshot string
|
||||
RawTime int64
|
||||
VmCount int64
|
||||
VcpuTotal int64
|
||||
RamTotalGB int64
|
||||
}
|
||||
|
||||
type VcenterTotalsMeta struct {
|
||||
ViewType string
|
||||
TypeLabel string
|
||||
HourlyLink string
|
||||
DailyLink string
|
||||
MonthlyLink string
|
||||
HourlyClass string
|
||||
DailyClass string
|
||||
MonthlyClass string
|
||||
}
|
||||
|
||||
type VcenterChartData struct {
|
||||
PointsVm string
|
||||
PointsVcpu string
|
||||
PointsRam string
|
||||
Width int
|
||||
Height int
|
||||
GridX []float64
|
||||
GridY []float64
|
||||
YTicks []ChartTick
|
||||
XTicks []ChartTick
|
||||
}
|
||||
|
||||
type ChartTick struct {
|
||||
Pos float64
|
||||
Label string
|
||||
}
|
||||
|
||||
templ SnapshotHourlyList(entries []SnapshotEntry) {
|
||||
@@ -32,25 +76,207 @@ templ SnapshotListPage(title string, subtitle string, entries []SnapshotEntry) {
|
||||
<div>
|
||||
<div class="web2-pill">Snapshot Library</div>
|
||||
<h1 class="mt-3 text-4xl font-bold">{title}</h1>
|
||||
<p class="mt-2 text-sm opacity-90">{subtitle}</p>
|
||||
<p class="mt-2 text-sm text-slate-600">{subtitle}</p>
|
||||
</div>
|
||||
<a class="web2-button" href="/">Back to Dashboard</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="web2-card">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center justify-between gap-3 mb-4 flex-wrap">
|
||||
<h2 class="text-lg font-semibold">Available Exports</h2>
|
||||
<span class="text-xs uppercase tracking-[0.2em] text-slate-400">{len(entries)} files</span>
|
||||
<span class="web2-badge">{len(entries)} files</span>
|
||||
</div>
|
||||
<div class="overflow-hidden border border-slate-200 rounded">
|
||||
<table class="web2-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Snapshot</th>
|
||||
<th>Records</th>
|
||||
<th class="text-right">Download</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
for i, entry := range entries {
|
||||
if entry.Group != "" && (i == 0 || entries[i-1].Group != entry.Group) {
|
||||
<tr class="web2-group-row">
|
||||
<td colspan="3" class="font-semibold text-slate-700">{entry.Group}</td>
|
||||
</tr>
|
||||
}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="flex flex-col">
|
||||
<span class="text-sm font-semibold text-slate-700">{entry.Label}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="web2-badge">{entry.Count} records</span>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<a class="web2-link" href={entry.Link}>Download XLSX</a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
@core.Footer()
|
||||
</html>
|
||||
}
|
||||
|
||||
templ VcenterList(links []VcenterLink) {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
@core.Header()
|
||||
<body class="flex flex-col min-h-screen web2-bg">
|
||||
<main class="flex-grow web2-shell space-y-8">
|
||||
<section class="web2-header">
|
||||
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<div class="web2-pill">vCenter Inventory</div>
|
||||
<h1 class="mt-3 text-4xl font-bold">Monitored vCenters</h1>
|
||||
<p class="mt-2 text-sm text-slate-600">Select a vCenter to view snapshot totals over time.</p>
|
||||
</div>
|
||||
<a class="web2-button" href="/">Back to Dashboard</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="web2-card">
|
||||
<div class="flex items-center justify-between gap-3 mb-4 flex-wrap">
|
||||
<h2 class="text-lg font-semibold">vCenters</h2>
|
||||
<span class="web2-badge">{len(links)} total</span>
|
||||
</div>
|
||||
<div class="overflow-hidden border border-slate-200 rounded">
|
||||
<table class="web2-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>vCenter</th>
|
||||
<th class="text-right">Totals</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
for _, link := range links {
|
||||
<tr>
|
||||
<td class="font-semibold text-slate-700">{link.Name}</td>
|
||||
<td class="text-right">
|
||||
<a class="web2-link" href={link.Link}>View Totals</a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
@core.Footer()
|
||||
</html>
|
||||
}
|
||||
|
||||
templ VcenterTotalsPage(vcenter string, entries []VcenterTotalsEntry, chart VcenterChartData, meta VcenterTotalsMeta) {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
@core.Header()
|
||||
<body class="flex flex-col min-h-screen web2-bg">
|
||||
<main class="flex-grow web2-shell space-y-8 max-w-screen-2xl mx-auto" style="max-width: 1400px;">
|
||||
<section class="web2-header">
|
||||
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<div class="web2-pill">vCenter Totals</div>
|
||||
<h1 class="mt-3 text-4xl font-bold">Totals for {vcenter}</h1>
|
||||
<p class="mt-2 text-sm text-slate-600">{meta.TypeLabel} snapshots of VM count, vCPU, and RAM over time.</p>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<a class="web2-button secondary" href="/vcenters">All vCenters</a>
|
||||
<a class="web2-button" href="/">Dashboard</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="web3-button-group mt-8 mb-3">
|
||||
<a class={meta.HourlyClass} href={meta.HourlyLink}>Hourly</a>
|
||||
<a class={meta.DailyClass} href={meta.DailyLink}>Daily</a>
|
||||
<a class={meta.MonthlyClass} href={meta.MonthlyLink}>Monthly</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="web2-card">
|
||||
<div class="flex items-center justify-between gap-3 mb-4 flex-wrap">
|
||||
<h2 class="text-lg font-semibold">{meta.TypeLabel} Snapshots</h2>
|
||||
<span class="web2-badge">{len(entries)} records</span>
|
||||
</div>
|
||||
if chart.PointsVm != "" {
|
||||
<div class="mb-6 overflow-auto">
|
||||
<svg width="100%" height={fmt.Sprintf("%d", chart.Height+80)} viewBox={"0 0 " + fmt.Sprintf("%d", chart.Width) + " " + fmt.Sprintf("%d", chart.Height+70)} role="img" aria-label="Totals over time">
|
||||
<defs>
|
||||
<linearGradient id="grid" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#e2e8f0" stop-opacity="0.6"></stop>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect x="40" y="10" width={fmt.Sprintf("%d", chart.Width-60)} height={fmt.Sprintf("%d", chart.Height)} fill="white" stroke="#e2e8f0"></rect>
|
||||
<!-- grid lines -->
|
||||
<g stroke="#e2e8f0" stroke-width="1" stroke-dasharray="2,4">
|
||||
for _, y := range chart.GridY {
|
||||
<line x1="40" y1={fmt.Sprintf("%.1f", y)} x2={fmt.Sprintf("%d", chart.Width-20)} y2={fmt.Sprintf("%.1f", y)} />
|
||||
}
|
||||
for _, x := range chart.GridX {
|
||||
<line x1={fmt.Sprintf("%.1f", x)} y1="10" x2={fmt.Sprintf("%.1f", x)} y2={fmt.Sprintf("%d", chart.Height+10)} />
|
||||
}
|
||||
</g>
|
||||
<!-- axes -->
|
||||
<line x1="40" y1={fmt.Sprintf("%d", chart.Height+10)} x2={fmt.Sprintf("%d", chart.Width-20)} y2={fmt.Sprintf("%d", chart.Height+10)} stroke="#94a3b8" stroke-width="1.5"></line>
|
||||
<line x1="40" y1="10" x2="40" y2={fmt.Sprintf("%d", chart.Height+10)} stroke="#94a3b8" stroke-width="1.5"></line>
|
||||
<!-- data -->
|
||||
<polyline points={chart.PointsVm} fill="none" stroke="#2563eb" stroke-width="2.5"></polyline>
|
||||
<polyline points={chart.PointsVcpu} fill="none" stroke="#16a34a" stroke-width="2.5"></polyline>
|
||||
<polyline points={chart.PointsRam} fill="none" stroke="#ea580c" stroke-width="2.5"></polyline>
|
||||
<!-- tick labels -->
|
||||
<g font-size="10" fill="#475569" text-anchor="end">
|
||||
for _, tick := range chart.YTicks {
|
||||
<text x="36" y={fmt.Sprintf("%.1f", tick.Pos+3)}>{tick.Label}</text>
|
||||
}
|
||||
</g>
|
||||
<g font-size="10" fill="#475569" text-anchor="middle">
|
||||
for _, tick := range chart.XTicks {
|
||||
<text x={fmt.Sprintf("%.1f", tick.Pos)} y={fmt.Sprintf("%d", chart.Height+24)}>{tick.Label}</text>
|
||||
}
|
||||
</g>
|
||||
<!-- legend -->
|
||||
<g font-size="12" fill="#475569" transform={"translate(40 " + fmt.Sprintf("%d", chart.Height+54) + ")"}>
|
||||
<rect x="0" y="0" width="14" height="8" fill="#2563eb"></rect><text x="22" y="12">VMs</text>
|
||||
<rect x="90" y="0" width="14" height="8" fill="#16a34a"></rect><text x="112" y="12">vCPU</text>
|
||||
<rect x="180" y="0" width="14" height="8" fill="#ea580c"></rect><text x="202" y="12">RAM (GB)</text>
|
||||
</g>
|
||||
<!-- axis labels -->
|
||||
<text x="15" y="20" transform={"rotate(-90 15 20)"} font-size="12" fill="#475569">Totals</text>
|
||||
<text x={fmt.Sprintf("%d", chart.Width/2)} y={fmt.Sprintf("%d", chart.Height+70)} font-size="12" fill="#475569">Snapshot sequence (newest right)</text>
|
||||
</svg>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="overflow-hidden border border-slate-200 rounded">
|
||||
<table class="web2-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Snapshot Time</th>
|
||||
<th class="text-right">VMs</th>
|
||||
<th class="text-right">vCPUs</th>
|
||||
<th class="text-right">RAM (GB)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
for _, entry := range entries {
|
||||
<tr>
|
||||
<td>{entry.Snapshot}</td>
|
||||
<td class="text-right">{entry.VmCount}</td>
|
||||
<td class="text-right">{entry.VcpuTotal}</td>
|
||||
<td class="text-right">{entry.RamTotalGB}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<ul class="mt-6 space-y-3 web2-list">
|
||||
for _, entry := range entries {
|
||||
<li class="flex items-center justify-between gap-4">
|
||||
<span class="text-sm font-semibold text-slate-700">{entry.Label}</span>
|
||||
<a class="web2-link" href={entry.Link}>Download XLSX</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
|
||||
@@ -9,12 +9,56 @@ import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"vctp/components/core"
|
||||
)
|
||||
|
||||
type SnapshotEntry struct {
|
||||
Label string
|
||||
Link string
|
||||
Count int64
|
||||
Group string
|
||||
}
|
||||
|
||||
type VcenterLink struct {
|
||||
Name string
|
||||
Link string
|
||||
}
|
||||
|
||||
type VcenterTotalsEntry struct {
|
||||
Snapshot string
|
||||
RawTime int64
|
||||
VmCount int64
|
||||
VcpuTotal int64
|
||||
RamTotalGB int64
|
||||
}
|
||||
|
||||
type VcenterTotalsMeta struct {
|
||||
ViewType string
|
||||
TypeLabel string
|
||||
HourlyLink string
|
||||
DailyLink string
|
||||
MonthlyLink string
|
||||
HourlyClass string
|
||||
DailyClass string
|
||||
MonthlyClass string
|
||||
}
|
||||
|
||||
type VcenterChartData struct {
|
||||
PointsVm string
|
||||
PointsVcpu string
|
||||
PointsRam string
|
||||
Width int
|
||||
Height int
|
||||
GridX []float64
|
||||
GridY []float64
|
||||
YTicks []ChartTick
|
||||
XTicks []ChartTick
|
||||
}
|
||||
|
||||
type ChartTick struct {
|
||||
Pos float64
|
||||
Label string
|
||||
}
|
||||
|
||||
func SnapshotHourlyList(entries []SnapshotEntry) templ.Component {
|
||||
@@ -140,75 +184,107 @@ func SnapshotListPage(title string, subtitle string, entries []SnapshotEntry) te
|
||||
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: `views/snapshots.templ`, Line: 34, Col: 49}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 78, Col: 49}
|
||||
}
|
||||
_, 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-sm opacity-90\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</h1><p class=\"mt-2 text-sm text-slate-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: `views/snapshots.templ`, Line: 35, Col: 51}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 79, Col: 55}
|
||||
}
|
||||
_, 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></div><a class=\"web2-button\" href=\"/\">Back to Dashboard</a></div></section><section class=\"web2-card\"><div class=\"flex items-center justify-between\"><h2 class=\"text-lg font-semibold\">Available Exports</h2><span class=\"text-xs uppercase tracking-[0.2em] text-slate-400\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</p></div><a class=\"web2-button\" href=\"/\">Back to Dashboard</a></div></section><section class=\"web2-card\"><div class=\"flex items-center justify-between gap-3 mb-4 flex-wrap\"><h2 class=\"text-lg font-semibold\">Available Exports</h2><span class=\"web2-badge\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(len(entries))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 44, Col: 83}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 88, Col: 44}
|
||||
}
|
||||
_, 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, 5, " files</span></div><ul class=\"mt-6 space-y-3 web2-list\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, " files</span></div><div class=\"overflow-hidden border border-slate-200 rounded\"><table class=\"web2-table\"><thead><tr><th>Snapshot</th><th>Records</th><th class=\"text-right\">Download</th></tr></thead> <tbody>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, entry := range entries {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<li class=\"flex items-center justify-between gap-4\"><span class=\"text-sm font-semibold text-slate-700\">")
|
||||
for i, entry := range entries {
|
||||
if entry.Group != "" && (i == 0 || entries[i-1].Group != entry.Group) {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<tr class=\"web2-group-row\"><td colspan=\"3\" class=\"font-semibold text-slate-700\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Group)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 103, Col: 76}
|
||||
}
|
||||
_, 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, "</td></tr>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, " <tr><td><div class=\"flex flex-col\"><span class=\"text-sm font-semibold text-slate-700\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Label)
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Label)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 49, Col: 71}
|
||||
}
|
||||
_, 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, "</span> <a class=\"web2-link\" href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var9 templ.SafeURL
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinURLErrs(entry.Link)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 50, Col: 45}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 109, Col: 75}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\">Download XLSX</a></li>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</span></div></td><td><span class=\"web2-badge\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Count)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 113, Col: 48}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, " records</span></td><td class=\"text-right\"><a class=\"web2-link\" href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var11 templ.SafeURL
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinURLErrs(entry.Link)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 116, Col: 48}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\">Download XLSX</a></td></tr>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</ul></section></main></body>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</tbody></table></div></section></main></body>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -216,7 +292,749 @@ func SnapshotListPage(title string, subtitle string, entries []SnapshotEntry) te
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</html>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</html>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func VcenterList(links []VcenterLink) 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_Var12 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var12 == nil {
|
||||
templ_7745c5c3_Var12 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<!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, 15, "<body class=\"flex flex-col min-h-screen web2-bg\"><main class=\"flex-grow web2-shell space-y-8\"><section class=\"web2-header\"><div class=\"flex flex-col gap-4 md:flex-row md:items-center md:justify-between\"><div><div class=\"web2-pill\">vCenter Inventory</div><h1 class=\"mt-3 text-4xl font-bold\">Monitored vCenters</h1><p class=\"mt-2 text-sm text-slate-600\">Select a vCenter to view snapshot totals over time.</p></div><a class=\"web2-button\" href=\"/\">Back to Dashboard</a></div></section><section class=\"web2-card\"><div class=\"flex items-center justify-between gap-3 mb-4 flex-wrap\"><h2 class=\"text-lg font-semibold\">vCenters</h2><span class=\"web2-badge\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var13 string
|
||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(len(links))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 150, Col: 42}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, " total</span></div><div class=\"overflow-hidden border border-slate-200 rounded\"><table class=\"web2-table\"><thead><tr><th>vCenter</th><th class=\"text-right\">Totals</th></tr></thead> <tbody>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, link := range links {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<tr><td class=\"font-semibold text-slate-700\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var14 string
|
||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(link.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 163, Col: 61}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</td><td class=\"text-right\"><a class=\"web2-link\" href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var15 templ.SafeURL
|
||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinURLErrs(link.Link)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 165, Col: 47}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\">View Totals</a></td></tr>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</tbody></table></div></section></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, 21, "</html>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func VcenterTotalsPage(vcenter string, entries []VcenterTotalsEntry, chart VcenterChartData, meta VcenterTotalsMeta) 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_Var16 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var16 == nil {
|
||||
templ_7745c5c3_Var16 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<!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, 23, "<body class=\"flex flex-col min-h-screen web2-bg\"><main class=\"flex-grow web2-shell space-y-8 max-w-screen-2xl mx-auto\" style=\"max-width: 1400px;\"><section class=\"web2-header\"><div class=\"flex flex-col gap-4 md:flex-row md:items-center md:justify-between\"><div><div class=\"web2-pill\">vCenter Totals</div><h1 class=\"mt-3 text-4xl font-bold\">Totals for ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var17 string
|
||||
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(vcenter)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 189, Col: 63}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</h1><p class=\"mt-2 text-sm text-slate-600\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var18 string
|
||||
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(meta.TypeLabel)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 190, Col: 62}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, " snapshots of VM count, vCPU, and RAM over time.</p></div><div class=\"flex gap-3\"><a class=\"web2-button secondary\" href=\"/vcenters\">All vCenters</a> <a class=\"web2-button\" href=\"/\">Dashboard</a></div></div><div class=\"web3-button-group mt-8 mb-3\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var19 = []any{meta.HourlyClass}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var19...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<a class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var20 string
|
||||
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var19).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "\" href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var21 templ.SafeURL
|
||||
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinURLErrs(meta.HourlyLink)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 198, Col: 56}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "\">Hourly</a> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var22 = []any{meta.DailyClass}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var22...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<a class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var23 string
|
||||
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var22).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "\" href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var24 templ.SafeURL
|
||||
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinURLErrs(meta.DailyLink)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 199, Col: 54}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "\">Daily</a> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var25 = []any{meta.MonthlyClass}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var25...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<a class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var26 string
|
||||
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var25).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "\" href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var27 templ.SafeURL
|
||||
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinURLErrs(meta.MonthlyLink)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 200, Col: 58}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "\">Monthly</a></div></section><section class=\"web2-card\"><div class=\"flex items-center justify-between gap-3 mb-4 flex-wrap\"><h2 class=\"text-lg font-semibold\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var28 string
|
||||
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(meta.TypeLabel)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 206, Col: 56}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, " Snapshots</h2><span class=\"web2-badge\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var29 string
|
||||
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(len(entries))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 207, Col: 45}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, " records</span></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if chart.PointsVm != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<div class=\"mb-6 overflow-auto\"><svg width=\"100%\" height=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var30 string
|
||||
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", chart.Height+80))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 211, Col: 67}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "\" viewBox=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var31 string
|
||||
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs("0 0 " + fmt.Sprintf("%d", chart.Width) + " " + fmt.Sprintf("%d", chart.Height+70))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 211, Col: 160}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "\" role=\"img\" aria-label=\"Totals over time\"><defs><linearGradient id=\"grid\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"><stop offset=\"0%\" stop-color=\"#e2e8f0\" stop-opacity=\"0.6\"></stop></linearGradient></defs> <rect x=\"40\" y=\"10\" width=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var32 string
|
||||
templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", chart.Width-60))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 217, Col: 68}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "\" height=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var33 string
|
||||
templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", chart.Height))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 217, Col: 109}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "\" fill=\"white\" stroke=\"#e2e8f0\"></rect><!-- grid lines --><g stroke=\"#e2e8f0\" stroke-width=\"1\" stroke-dasharray=\"2,4\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, y := range chart.GridY {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "<line x1=\"40\" y1=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var34 string
|
||||
templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1f", y))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 221, Col: 51}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "\" x2=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var35 string
|
||||
templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", chart.Width-20))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 221, Col: 90}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "\" y2=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var36 string
|
||||
templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1f", y))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 221, Col: 118}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "\"></line> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
for _, x := range chart.GridX {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "<line x1=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var37 string
|
||||
templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1f", x))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 224, Col: 43}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "\" y1=\"10\" x2=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var38 string
|
||||
templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1f", x))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 224, Col: 79}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "\" y2=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var39 string
|
||||
templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", chart.Height+10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 224, Col: 119}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "\"></line>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "</g><!-- axes --><line x1=\"40\" y1=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var40 string
|
||||
templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", chart.Height+10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 228, Col: 61}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "\" x2=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var41 string
|
||||
templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", chart.Width-20))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 228, Col: 100}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "\" y2=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var42 string
|
||||
templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", chart.Height+10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 228, Col: 140}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "\" stroke=\"#94a3b8\" stroke-width=\"1.5\"></line> <line x1=\"40\" y1=\"10\" x2=\"40\" y2=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var43 string
|
||||
templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", chart.Height+10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 229, Col: 77}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "\" stroke=\"#94a3b8\" stroke-width=\"1.5\"></line><!-- data --><polyline points=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var44 string
|
||||
templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(chart.PointsVm)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 231, Col: 40}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "\" fill=\"none\" stroke=\"#2563eb\" stroke-width=\"2.5\"></polyline> <polyline points=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var45 string
|
||||
templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinStringErrs(chart.PointsVcpu)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 232, Col: 42}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "\" fill=\"none\" stroke=\"#16a34a\" stroke-width=\"2.5\"></polyline> <polyline points=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var46 string
|
||||
templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.JoinStringErrs(chart.PointsRam)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 233, Col: 41}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var46))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "\" fill=\"none\" stroke=\"#ea580c\" stroke-width=\"2.5\"></polyline><!-- tick labels --><g font-size=\"10\" fill=\"#475569\" text-anchor=\"end\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, tick := range chart.YTicks {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "<text x=\"36\" y=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var47 string
|
||||
templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1f", tick.Pos+3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 237, Col: 58}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var47))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var48 string
|
||||
templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.JoinStringErrs(tick.Label)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 237, Col: 71}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var48))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "</text>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "</g> <g font-size=\"10\" fill=\"#475569\" text-anchor=\"middle\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, tick := range chart.XTicks {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "<text x=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var49 string
|
||||
templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1f", tick.Pos))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 242, Col: 49}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var49))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "\" y=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var50 string
|
||||
templ_7745c5c3_Var50, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", chart.Height+24))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 242, Col: 88}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var50))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var51 string
|
||||
templ_7745c5c3_Var51, templ_7745c5c3_Err = templ.JoinStringErrs(tick.Label)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 242, Col: 101}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var51))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "</text>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "</g><!-- legend --><g font-size=\"12\" fill=\"#475569\" transform=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var52 string
|
||||
templ_7745c5c3_Var52, templ_7745c5c3_Err = templ.JoinStringErrs("translate(40 " + fmt.Sprintf("%d", chart.Height+54) + ")")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 246, Col: 111}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var52))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "\"><rect x=\"0\" y=\"0\" width=\"14\" height=\"8\" fill=\"#2563eb\"></rect><text x=\"22\" y=\"12\">VMs</text> <rect x=\"90\" y=\"0\" width=\"14\" height=\"8\" fill=\"#16a34a\"></rect><text x=\"112\" y=\"12\">vCPU</text> <rect x=\"180\" y=\"0\" width=\"14\" height=\"8\" fill=\"#ea580c\"></rect><text x=\"202\" y=\"12\">RAM (GB)</text></g><!-- axis labels --><text x=\"15\" y=\"20\" transform=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var53 string
|
||||
templ_7745c5c3_Var53, templ_7745c5c3_Err = templ.JoinStringErrs("rotate(-90 15 20)")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 252, Col: 59}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var53))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "\" font-size=\"12\" fill=\"#475569\">Totals</text> <text x=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var54 string
|
||||
templ_7745c5c3_Var54, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", chart.Width/2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 253, Col: 50}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var54))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "\" y=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var55 string
|
||||
templ_7745c5c3_Var55, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", chart.Height+70))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 253, Col: 89}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var55))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "\" font-size=\"12\" fill=\"#475569\">Snapshot sequence (newest right)</text></svg></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "<div class=\"overflow-hidden border border-slate-200 rounded\"><table class=\"web2-table\"><thead><tr><th>Snapshot Time</th><th class=\"text-right\">VMs</th><th class=\"text-right\">vCPUs</th><th class=\"text-right\">RAM (GB)</th></tr></thead> <tbody>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, entry := range entries {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "<tr><td>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var56 string
|
||||
templ_7745c5c3_Var56, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Snapshot)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 271, Col: 29}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var56))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "</td><td class=\"text-right\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var57 string
|
||||
templ_7745c5c3_Var57, templ_7745c5c3_Err = templ.JoinStringErrs(entry.VmCount)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 272, Col: 47}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var57))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "</td><td class=\"text-right\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var58 string
|
||||
templ_7745c5c3_Var58, templ_7745c5c3_Err = templ.JoinStringErrs(entry.VcpuTotal)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 273, Col: 49}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var58))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "</td><td class=\"text-right\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var59 string
|
||||
templ_7745c5c3_Var59, templ_7745c5c3_Err = templ.JoinStringErrs(entry.RamTotalGB)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 274, Col: 50}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var59))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "</td></tr>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "</tbody></table></div></section></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, 78, "</html>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
170
components/views/vm_trace.templ
Normal file
170
components/views/vm_trace.templ
Normal file
@@ -0,0 +1,170 @@
|
||||
package views
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"vctp/components/core"
|
||||
)
|
||||
|
||||
type VmTraceEntry struct {
|
||||
Snapshot string
|
||||
RawTime int64
|
||||
Name string
|
||||
VmId string
|
||||
VmUuid string
|
||||
Vcenter string
|
||||
ResourcePool string
|
||||
VcpuCount int64
|
||||
RamGB int64
|
||||
ProvisionedDisk float64
|
||||
CreationTime string
|
||||
DeletionTime string
|
||||
}
|
||||
|
||||
type VmTraceChart struct {
|
||||
PointsVcpu string
|
||||
PointsRam string
|
||||
PointsTin string
|
||||
PointsBronze string
|
||||
PointsSilver string
|
||||
PointsGold string
|
||||
Width int
|
||||
Height int
|
||||
GridX []float64
|
||||
GridY []float64
|
||||
XTicks []ChartTick
|
||||
YTicks []ChartTick
|
||||
}
|
||||
|
||||
templ VmTracePage(query string, display_query string, vm_id string, vm_uuid string, vm_name string, creationLabel string, deletionLabel string, entries []VmTraceEntry, chart VmTraceChart) {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
@core.Header()
|
||||
<body class="flex flex-col min-h-screen web2-bg">
|
||||
<main class="flex-grow web2-shell space-y-8 max-w-screen-2xl mx-auto" style="max-width: 1400px;">
|
||||
<section class="web2-header">
|
||||
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<div class="web2-pill">VM Trace</div>
|
||||
<h1 class="mt-3 text-4xl font-bold">Snapshot history{display_query}</h1>
|
||||
<p class="mt-2 text-sm text-slate-600">Timeline of vCPU, RAM, and resource pool changes across snapshots.</p>
|
||||
</div>
|
||||
<div class="flex gap-3 flex-wrap">
|
||||
<a class="web2-button" href="/">Dashboard</a>
|
||||
</div>
|
||||
</div>
|
||||
<form method="get" action="/vm/trace" class="mt-4 grid gap-3 md:grid-cols-3">
|
||||
<div class="flex flex-col gap-1">
|
||||
<label class="text-sm text-slate-600" for="vm_id">VM ID</label>
|
||||
<input class="web2-card border border-slate-200 px-3 py-2 rounded" type="text" id="vm_id" name="vm_id" value={vm_id} placeholder="vm-12345"/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label class="text-sm text-slate-600" for="vm_uuid">VM UUID</label>
|
||||
<input class="web2-card border border-slate-200 px-3 py-2 rounded" type="text" id="vm_uuid" name="vm_uuid" value={vm_uuid} placeholder="uuid..."/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label class="text-sm text-slate-600" for="name">Name</label>
|
||||
<input class="web2-card border border-slate-200 px-3 py-2 rounded" type="text" id="name" name="name" value={vm_name} placeholder="VM name"/>
|
||||
</div>
|
||||
<div class="md:col-span-3 flex gap-2">
|
||||
<button class="web3-button active" type="submit">Load VM Trace</button>
|
||||
<a class="web3-button" href="/vm/trace">Clear</a>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="web2-card">
|
||||
<div class="flex items-center justify-between gap-3 mb-4 flex-wrap">
|
||||
<h2 class="text-lg font-semibold">Snapshot Timeline</h2>
|
||||
<span class="web2-badge">{len(entries)} samples</span>
|
||||
</div>
|
||||
if chart.PointsVcpu != "" {
|
||||
<div class="mb-6 overflow-auto">
|
||||
<svg width="100%" height="360" viewBox={"0 0 " + fmt.Sprintf("%d", chart.Width) + " 320"} role="img" aria-label="VM timeline">
|
||||
<rect x="40" y="10" width={fmt.Sprintf("%d", chart.Width-60)} height={fmt.Sprintf("%d", chart.Height)} fill="white" stroke="#e2e8f0"></rect>
|
||||
<g stroke="#e2e8f0" stroke-width="1" stroke-dasharray="2,4">
|
||||
for _, y := range chart.GridY {
|
||||
<line x1="40" y1={fmt.Sprintf("%.1f", y)} x2={fmt.Sprintf("%d", chart.Width-20)} y2={fmt.Sprintf("%.1f", y)} />
|
||||
}
|
||||
for _, x := range chart.GridX {
|
||||
<line x1={fmt.Sprintf("%.1f", x)} y1="10" x2={fmt.Sprintf("%.1f", x)} y2={fmt.Sprintf("%d", chart.Height+10)} />
|
||||
}
|
||||
</g>
|
||||
<line x1="40" y1={fmt.Sprintf("%d", chart.Height+10)} x2={fmt.Sprintf("%d", chart.Width-20)} y2={fmt.Sprintf("%d", chart.Height+10)} stroke="#94a3b8" stroke-width="1.5"></line>
|
||||
<line x1="40" y1="10" x2="40" y2={fmt.Sprintf("%d", chart.Height+10)} stroke="#94a3b8" stroke-width="1.5"></line>
|
||||
<polyline points={chart.PointsVcpu} fill="none" stroke="#2563eb" stroke-width="2.5"></polyline>
|
||||
<polyline points={chart.PointsRam} fill="none" stroke="#16a34a" stroke-width="2.5"></polyline>
|
||||
<polyline points={chart.PointsTin} fill="none" stroke="#0ea5e9" stroke-width="1.5" stroke-dasharray="4,4"></polyline>
|
||||
<polyline points={chart.PointsBronze} fill="none" stroke="#a855f7" stroke-width="1.5" stroke-dasharray="4,4"></polyline>
|
||||
<polyline points={chart.PointsSilver} fill="none" stroke="#94a3b8" stroke-width="1.5" stroke-dasharray="4,4"></polyline>
|
||||
<polyline points={chart.PointsGold} fill="none" stroke="#f59e0b" stroke-width="1.5" stroke-dasharray="4,4"></polyline>
|
||||
<g font-size="10" fill="#475569" text-anchor="end">
|
||||
for _, tick := range chart.YTicks {
|
||||
<text x="36" y={fmt.Sprintf("%.1f", tick.Pos+3)}>{tick.Label}</text>
|
||||
}
|
||||
</g>
|
||||
<g font-size="10" fill="#475569" text-anchor="middle">
|
||||
for _, tick := range chart.XTicks {
|
||||
<text x={fmt.Sprintf("%.1f", tick.Pos)} y={fmt.Sprintf("%d", chart.Height+24)}>{tick.Label}</text>
|
||||
}
|
||||
</g>
|
||||
<g font-size="12" fill="#475569" transform={"translate(40 " + fmt.Sprintf("%d", chart.Height+50) + ")"}>
|
||||
<rect x="0" y="0" width="14" height="8" fill="#2563eb"></rect><text x="22" y="12">vCPU</text>
|
||||
<rect x="90" y="0" width="14" height="8" fill="#16a34a"></rect><text x="112" y="12">RAM (GB)</text>
|
||||
<rect x="200" y="0" width="14" height="8" fill="#0ea5e9"></rect><text x="222" y="12">Tin</text>
|
||||
<rect x="260" y="0" width="14" height="8" fill="#a855f7"></rect><text x="282" y="12">Bronze</text>
|
||||
<rect x="340" y="0" width="14" height="8" fill="#94a3b8"></rect><text x="362" y="12">Silver</text>
|
||||
<rect x="420" y="0" width="14" height="8" fill="#f59e0b"></rect><text x="442" y="12">Gold</text>
|
||||
</g>
|
||||
<text x="15" y="20" transform={"rotate(-90 15 20)"} font-size="12" fill="#475569">Resources / Pool</text>
|
||||
<text x={fmt.Sprintf("%d", chart.Width/2)} y={fmt.Sprintf("%d", chart.Height+70)} font-size="12" fill="#475569">Snapshots (oldest left, newest right)</text>
|
||||
</svg>
|
||||
</div>
|
||||
}
|
||||
<div class="grid gap-3 md:grid-cols-2 mb-4">
|
||||
<div class="web2-card">
|
||||
<p class="text-xs uppercase tracking-[0.15em] text-slate-500">Creation time</p>
|
||||
<p class="mt-2 text-base font-semibold text-slate-800">{creationLabel}</p>
|
||||
</div>
|
||||
<div class="web2-card">
|
||||
<p class="text-xs uppercase tracking-[0.15em] text-slate-500">Deletion time</p>
|
||||
<p class="mt-2 text-base font-semibold text-slate-800">{deletionLabel}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overflow-hidden border border-slate-200 rounded">
|
||||
<table class="web2-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Snapshot</th>
|
||||
<th>VM Name</th>
|
||||
<th>VmId</th>
|
||||
<th>VmUuid</th>
|
||||
<th>Vcenter</th>
|
||||
<th>Resource Pool</th>
|
||||
<th class="text-right">vCPUs</th>
|
||||
<th class="text-right">RAM (GB)</th>
|
||||
<th class="text-right">Disk</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
for _, e := range entries {
|
||||
<tr>
|
||||
<td>{e.Snapshot}</td>
|
||||
<td>{e.Name}</td>
|
||||
<td>{e.VmId}</td>
|
||||
<td>{e.VmUuid}</td>
|
||||
<td>{e.Vcenter}</td>
|
||||
<td>{e.ResourcePool}</td>
|
||||
<td class="text-right">{e.VcpuCount}</td>
|
||||
<td class="text-right">{e.RamGB}</td>
|
||||
<td class="text-right">{fmt.Sprintf("%.1f", e.ProvisionedDisk)}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
@core.Footer()
|
||||
</html>
|
||||
}
|
||||
719
components/views/vm_trace_templ.go
Normal file
719
components/views/vm_trace_templ.go
Normal file
@@ -0,0 +1,719 @@
|
||||
// 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 (
|
||||
"fmt"
|
||||
"vctp/components/core"
|
||||
)
|
||||
|
||||
type VmTraceEntry struct {
|
||||
Snapshot string
|
||||
RawTime int64
|
||||
Name string
|
||||
VmId string
|
||||
VmUuid string
|
||||
Vcenter string
|
||||
ResourcePool string
|
||||
VcpuCount int64
|
||||
RamGB int64
|
||||
ProvisionedDisk float64
|
||||
CreationTime string
|
||||
DeletionTime string
|
||||
}
|
||||
|
||||
type VmTraceChart struct {
|
||||
PointsVcpu string
|
||||
PointsRam string
|
||||
PointsTin string
|
||||
PointsBronze string
|
||||
PointsSilver string
|
||||
PointsGold string
|
||||
Width int
|
||||
Height int
|
||||
GridX []float64
|
||||
GridY []float64
|
||||
XTicks []ChartTick
|
||||
YTicks []ChartTick
|
||||
}
|
||||
|
||||
func VmTracePage(query string, display_query string, vm_id string, vm_uuid string, vm_name string, creationLabel string, deletionLabel string, entries []VmTraceEntry, chart VmTraceChart) 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 = 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 web2-bg\"><main class=\"flex-grow web2-shell space-y-8 max-w-screen-2xl mx-auto\" style=\"max-width: 1400px;\"><section class=\"web2-header\"><div class=\"flex flex-col gap-4 md:flex-row md:items-center md:justify-between\"><div><div class=\"web2-pill\">VM Trace</div><h1 class=\"mt-3 text-4xl font-bold\">Snapshot history")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(display_query)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 48, Col: 74}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</h1><p class=\"mt-2 text-sm text-slate-600\">Timeline of vCPU, RAM, and resource pool changes across snapshots.</p></div><div class=\"flex gap-3 flex-wrap\"><a class=\"web2-button\" href=\"/\">Dashboard</a></div></div><form method=\"get\" action=\"/vm/trace\" class=\"mt-4 grid gap-3 md:grid-cols-3\"><div class=\"flex flex-col gap-1\"><label class=\"text-sm text-slate-600\" for=\"vm_id\">VM ID</label> <input class=\"web2-card border border-slate-200 px-3 py-2 rounded\" type=\"text\" id=\"vm_id\" name=\"vm_id\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(vm_id)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 58, Col: 123}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" placeholder=\"vm-12345\"></div><div class=\"flex flex-col gap-1\"><label class=\"text-sm text-slate-600\" for=\"vm_uuid\">VM UUID</label> <input class=\"web2-card border border-slate-200 px-3 py-2 rounded\" type=\"text\" id=\"vm_uuid\" name=\"vm_uuid\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(vm_uuid)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 62, Col: 129}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\" placeholder=\"uuid...\"></div><div class=\"flex flex-col gap-1\"><label class=\"text-sm text-slate-600\" for=\"name\">Name</label> <input class=\"web2-card border border-slate-200 px-3 py-2 rounded\" type=\"text\" id=\"name\" name=\"name\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(vm_name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 66, Col: 123}
|
||||
}
|
||||
_, 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, 6, "\" placeholder=\"VM name\"></div><div class=\"md:col-span-3 flex gap-2\"><button class=\"web3-button active\" type=\"submit\">Load VM Trace</button> <a class=\"web3-button\" href=\"/vm/trace\">Clear</a></div></form></section><section class=\"web2-card\"><div class=\"flex items-center justify-between gap-3 mb-4 flex-wrap\"><h2 class=\"text-lg font-semibold\">Snapshot Timeline</h2><span class=\"web2-badge\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(len(entries))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 78, 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, 7, " samples</span></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if chart.PointsVcpu != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<div class=\"mb-6 overflow-auto\"><svg width=\"100%\" height=\"360\" viewBox=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs("0 0 " + fmt.Sprintf("%d", chart.Width) + " 320")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 82, Col: 95}
|
||||
}
|
||||
_, 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, 9, "\" role=\"img\" aria-label=\"VM timeline\"><rect x=\"40\" y=\"10\" width=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", chart.Width-60))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 83, Col: 68}
|
||||
}
|
||||
_, 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, 10, "\" height=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", chart.Height))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 83, Col: 109}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" fill=\"white\" stroke=\"#e2e8f0\"></rect> <g stroke=\"#e2e8f0\" stroke-width=\"1\" stroke-dasharray=\"2,4\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, y := range chart.GridY {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<line x1=\"40\" y1=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1f", y))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 86, Col: 50}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\" x2=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", chart.Width-20))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 86, Col: 89}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\" y2=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var12 string
|
||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1f", y))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 86, Col: 117}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\"></line> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
for _, x := range chart.GridX {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<line x1=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var13 string
|
||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1f", x))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 89, Col: 42}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\" y1=\"10\" x2=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var14 string
|
||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1f", x))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 89, Col: 78}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\" y2=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var15 string
|
||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", chart.Height+10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 89, Col: 118}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\"></line>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</g> <line x1=\"40\" y1=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var16 string
|
||||
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", chart.Height+10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 92, Col: 60}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "\" x2=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var17 string
|
||||
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", chart.Width-20))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 92, Col: 99}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "\" y2=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var18 string
|
||||
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", chart.Height+10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 92, Col: 139}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\" stroke=\"#94a3b8\" stroke-width=\"1.5\"></line> <line x1=\"40\" y1=\"10\" x2=\"40\" y2=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var19 string
|
||||
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", chart.Height+10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 93, Col: 76}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\" stroke=\"#94a3b8\" stroke-width=\"1.5\"></line> <polyline points=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var20 string
|
||||
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(chart.PointsVcpu)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 94, Col: 42}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "\" fill=\"none\" stroke=\"#2563eb\" stroke-width=\"2.5\"></polyline> <polyline points=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var21 string
|
||||
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(chart.PointsRam)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 95, Col: 41}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "\" fill=\"none\" stroke=\"#16a34a\" stroke-width=\"2.5\"></polyline> <polyline points=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var22 string
|
||||
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(chart.PointsTin)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 96, Col: 41}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "\" fill=\"none\" stroke=\"#0ea5e9\" stroke-width=\"1.5\" stroke-dasharray=\"4,4\"></polyline> <polyline points=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var23 string
|
||||
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(chart.PointsBronze)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 97, Col: 44}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "\" fill=\"none\" stroke=\"#a855f7\" stroke-width=\"1.5\" stroke-dasharray=\"4,4\"></polyline> <polyline points=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var24 string
|
||||
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(chart.PointsSilver)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 98, Col: 44}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "\" fill=\"none\" stroke=\"#94a3b8\" stroke-width=\"1.5\" stroke-dasharray=\"4,4\"></polyline> <polyline points=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var25 string
|
||||
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(chart.PointsGold)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 99, Col: 42}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "\" fill=\"none\" stroke=\"#f59e0b\" stroke-width=\"1.5\" stroke-dasharray=\"4,4\"></polyline> <g font-size=\"10\" fill=\"#475569\" text-anchor=\"end\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, tick := range chart.YTicks {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<text x=\"36\" y=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var26 string
|
||||
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1f", tick.Pos+3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 102, Col: 57}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var27 string
|
||||
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(tick.Label)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 102, Col: 70}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "</text>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</g> <g font-size=\"10\" fill=\"#475569\" text-anchor=\"middle\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, tick := range chart.XTicks {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "<text x=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var28 string
|
||||
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1f", tick.Pos))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 107, Col: 48}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "\" y=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var29 string
|
||||
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", chart.Height+24))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 107, Col: 87}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var30 string
|
||||
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(tick.Label)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 107, Col: 100}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</text>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</g> <g font-size=\"12\" fill=\"#475569\" transform=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var31 string
|
||||
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs("translate(40 " + fmt.Sprintf("%d", chart.Height+50) + ")")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 110, Col: 110}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "\"><rect x=\"0\" y=\"0\" width=\"14\" height=\"8\" fill=\"#2563eb\"></rect><text x=\"22\" y=\"12\">vCPU</text> <rect x=\"90\" y=\"0\" width=\"14\" height=\"8\" fill=\"#16a34a\"></rect><text x=\"112\" y=\"12\">RAM (GB)</text> <rect x=\"200\" y=\"0\" width=\"14\" height=\"8\" fill=\"#0ea5e9\"></rect><text x=\"222\" y=\"12\">Tin</text> <rect x=\"260\" y=\"0\" width=\"14\" height=\"8\" fill=\"#a855f7\"></rect><text x=\"282\" y=\"12\">Bronze</text> <rect x=\"340\" y=\"0\" width=\"14\" height=\"8\" fill=\"#94a3b8\"></rect><text x=\"362\" y=\"12\">Silver</text> <rect x=\"420\" y=\"0\" width=\"14\" height=\"8\" fill=\"#f59e0b\"></rect><text x=\"442\" y=\"12\">Gold</text></g> <text x=\"15\" y=\"20\" transform=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var32 string
|
||||
templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs("rotate(-90 15 20)")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 118, Col: 58}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "\" font-size=\"12\" fill=\"#475569\">Resources / Pool</text> <text x=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var33 string
|
||||
templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", chart.Width/2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 119, Col: 49}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "\" y=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var34 string
|
||||
templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", chart.Height+70))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 119, Col: 88}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "\" font-size=\"12\" fill=\"#475569\">Snapshots (oldest left, newest right)</text></svg></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "<div class=\"grid gap-3 md:grid-cols-2 mb-4\"><div class=\"web2-card\"><p class=\"text-xs uppercase tracking-[0.15em] text-slate-500\">Creation time</p><p class=\"mt-2 text-base font-semibold text-slate-800\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var35 string
|
||||
templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(creationLabel)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 126, Col: 76}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "</p></div><div class=\"web2-card\"><p class=\"text-xs uppercase tracking-[0.15em] text-slate-500\">Deletion time</p><p class=\"mt-2 text-base font-semibold text-slate-800\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var36 string
|
||||
templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(deletionLabel)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 130, Col: 76}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "</p></div></div><div class=\"overflow-hidden border border-slate-200 rounded\"><table class=\"web2-table\"><thead><tr><th>Snapshot</th><th>VM Name</th><th>VmId</th><th>VmUuid</th><th>Vcenter</th><th>Resource Pool</th><th class=\"text-right\">vCPUs</th><th class=\"text-right\">RAM (GB)</th><th class=\"text-right\">Disk</th></tr></thead> <tbody>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, e := range entries {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "<tr><td>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var37 string
|
||||
templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(e.Snapshot)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 151, Col: 25}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "</td><td>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var38 string
|
||||
templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(e.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 152, Col: 21}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "</td><td>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var39 string
|
||||
templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(e.VmId)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 153, Col: 21}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "</td><td>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var40 string
|
||||
templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(e.VmUuid)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 154, Col: 23}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "</td><td>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var41 string
|
||||
templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(e.Vcenter)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 155, Col: 24}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "</td><td>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var42 string
|
||||
templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs(e.ResourcePool)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 156, Col: 29}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "</td><td class=\"text-right\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var43 string
|
||||
templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(e.VcpuCount)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 157, Col: 45}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "</td><td class=\"text-right\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var44 string
|
||||
templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(e.RamGB)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 158, Col: 41}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "</td><td class=\"text-right\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var45 string
|
||||
templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1f", e.ProvisionedDisk))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 159, Col: 72}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "</td></tr>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "</tbody></table></div></section></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, 58, "</html>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
1607
db/helpers.go
Normal file
1607
db/helpers.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -10,7 +10,6 @@ CREATE TABLE IF NOT EXISTS "Inventory" (
|
||||
"CreationTime" INTEGER,
|
||||
"DeletionTime" INTEGER,
|
||||
"ResourcePool" TEXT,
|
||||
"VmType" TEXT,
|
||||
"Datacenter" TEXT,
|
||||
"Cluster" TEXT,
|
||||
"Folder" TEXT,
|
||||
|
||||
14
db/migrations/20250115094500_snapshot_registry.sql
Normal file
14
db/migrations/20250115094500_snapshot_registry.sql
Normal file
@@ -0,0 +1,14 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
CREATE TABLE IF NOT EXISTS snapshot_registry (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
snapshot_type TEXT NOT NULL,
|
||||
table_name TEXT NOT NULL UNIQUE,
|
||||
snapshot_time BIGINT NOT NULL
|
||||
);
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
DROP TABLE snapshot_registry;
|
||||
-- +goose StatementEnd
|
||||
48
db/migrations/20250116090000_drop_vmtype.sql
Normal file
48
db/migrations/20250116090000_drop_vmtype.sql
Normal file
@@ -0,0 +1,48 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
PRAGMA foreign_keys=OFF;
|
||||
|
||||
ALTER TABLE "Inventory" RENAME TO "Inventory_old";
|
||||
|
||||
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,
|
||||
"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
|
||||
);
|
||||
|
||||
INSERT INTO "Inventory" (
|
||||
"Iid", "Name", "Vcenter", "VmId", "EventKey", "CloudId", "CreationTime", "DeletionTime",
|
||||
"ResourcePool", "Datacenter", "Cluster", "Folder", "ProvisionedDisk", "InitialVcpus",
|
||||
"InitialRam", "IsTemplate", "PoweredOn", "SrmPlaceholder", "VmUuid"
|
||||
)
|
||||
SELECT
|
||||
"Iid", "Name", "Vcenter", "VmId", "EventKey", "CloudId", "CreationTime", "DeletionTime",
|
||||
"ResourcePool", "Datacenter", "Cluster", "Folder", "ProvisionedDisk", "InitialVcpus",
|
||||
"InitialRam", "IsTemplate", "PoweredOn", "SrmPlaceholder", "VmUuid"
|
||||
FROM "Inventory_old";
|
||||
|
||||
DROP TABLE "Inventory_old";
|
||||
|
||||
PRAGMA foreign_keys=ON;
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
ALTER TABLE "Inventory" ADD COLUMN "VmType" TEXT;
|
||||
-- +goose StatementEnd
|
||||
5
db/migrations/20250116101000_snapshot_count.sql
Normal file
5
db/migrations/20250116101000_snapshot_count.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
-- +goose Up
|
||||
ALTER TABLE snapshot_registry ADD COLUMN snapshot_count BIGINT NOT NULL DEFAULT 0;
|
||||
|
||||
-- +goose Down
|
||||
ALTER TABLE snapshot_registry DROP COLUMN snapshot_count;
|
||||
@@ -0,0 +1,5 @@
|
||||
-- +goose Up
|
||||
CREATE INDEX IF NOT EXISTS idx_snapshot_registry_type_time ON snapshot_registry (snapshot_type, snapshot_time);
|
||||
|
||||
-- +goose Down
|
||||
DROP INDEX IF EXISTS idx_snapshot_registry_type_time;
|
||||
@@ -10,7 +10,6 @@ CREATE TABLE IF NOT EXISTS "Inventory" (
|
||||
"CreationTime" BIGINT,
|
||||
"DeletionTime" BIGINT,
|
||||
"ResourcePool" TEXT,
|
||||
"VmType" TEXT,
|
||||
"Datacenter" TEXT,
|
||||
"Cluster" TEXT,
|
||||
"Folder" TEXT,
|
||||
|
||||
14
db/migrations_postgres/20250115094500_snapshot_registry.sql
Normal file
14
db/migrations_postgres/20250115094500_snapshot_registry.sql
Normal file
@@ -0,0 +1,14 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
CREATE TABLE IF NOT EXISTS snapshot_registry (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
snapshot_type TEXT NOT NULL,
|
||||
table_name TEXT NOT NULL UNIQUE,
|
||||
snapshot_time BIGINT NOT NULL
|
||||
);
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
DROP TABLE snapshot_registry;
|
||||
-- +goose StatementEnd
|
||||
9
db/migrations_postgres/20250116090000_drop_vmtype.sql
Normal file
9
db/migrations_postgres/20250116090000_drop_vmtype.sql
Normal file
@@ -0,0 +1,9 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
ALTER TABLE "Inventory" DROP COLUMN IF EXISTS "VmType";
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
ALTER TABLE "Inventory" ADD COLUMN "VmType" TEXT;
|
||||
-- +goose StatementEnd
|
||||
5
db/migrations_postgres/20250116101000_snapshot_count.sql
Normal file
5
db/migrations_postgres/20250116101000_snapshot_count.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
-- +goose Up
|
||||
ALTER TABLE snapshot_registry ADD COLUMN IF NOT EXISTS snapshot_count BIGINT NOT NULL DEFAULT 0;
|
||||
|
||||
-- +goose Down
|
||||
ALTER TABLE snapshot_registry DROP COLUMN IF EXISTS snapshot_count;
|
||||
@@ -0,0 +1,5 @@
|
||||
-- +goose Up
|
||||
CREATE INDEX IF NOT EXISTS idx_snapshot_registry_type_time ON snapshot_registry (snapshot_type, snapshot_time);
|
||||
|
||||
-- +goose Down
|
||||
DROP INDEX IF EXISTS idx_snapshot_registry_type_time;
|
||||
@@ -9,70 +9,94 @@ import (
|
||||
)
|
||||
|
||||
type Event struct {
|
||||
Eid int64
|
||||
CloudId string
|
||||
Source string
|
||||
EventTime sql.NullInt64
|
||||
ChainId string
|
||||
VmId sql.NullString
|
||||
EventKey sql.NullString
|
||||
DatacenterName sql.NullString
|
||||
ComputeResourceName sql.NullString
|
||||
UserName sql.NullString
|
||||
Processed int64
|
||||
DatacenterId sql.NullString
|
||||
ComputeResourceId sql.NullString
|
||||
VmName sql.NullString
|
||||
EventType sql.NullString
|
||||
Eid int64 `db:"Eid" json:"Eid"`
|
||||
CloudId string `db:"CloudId" json:"CloudId"`
|
||||
Source string `db:"Source" json:"Source"`
|
||||
EventTime sql.NullInt64 `db:"EventTime" json:"EventTime"`
|
||||
ChainId string `db:"ChainId" json:"ChainId"`
|
||||
VmId sql.NullString `db:"VmId" json:"VmId"`
|
||||
EventKey sql.NullString `db:"EventKey" json:"EventKey"`
|
||||
DatacenterName sql.NullString `db:"DatacenterName" json:"DatacenterName"`
|
||||
ComputeResourceName sql.NullString `db:"ComputeResourceName" json:"ComputeResourceName"`
|
||||
UserName sql.NullString `db:"UserName" json:"UserName"`
|
||||
Processed int64 `db:"Processed" json:"Processed"`
|
||||
DatacenterId sql.NullString `db:"DatacenterId" json:"DatacenterId"`
|
||||
ComputeResourceId sql.NullString `db:"ComputeResourceId" json:"ComputeResourceId"`
|
||||
VmName sql.NullString `db:"VmName" json:"VmName"`
|
||||
EventType sql.NullString `db:"EventType" json:"EventType"`
|
||||
}
|
||||
|
||||
type Inventory struct {
|
||||
Iid int64
|
||||
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 interface{}
|
||||
PoweredOn interface{}
|
||||
SrmPlaceholder interface{}
|
||||
VmUuid sql.NullString
|
||||
Iid int64 `db:"Iid" json:"Iid"`
|
||||
Name string `db:"Name" json:"Name"`
|
||||
Vcenter string `db:"Vcenter" json:"Vcenter"`
|
||||
VmId sql.NullString `db:"VmId" json:"VmId"`
|
||||
EventKey sql.NullString `db:"EventKey" json:"EventKey"`
|
||||
CloudId sql.NullString `db:"CloudId" json:"CloudId"`
|
||||
CreationTime sql.NullInt64 `db:"CreationTime" json:"CreationTime"`
|
||||
DeletionTime sql.NullInt64 `db:"DeletionTime" json:"DeletionTime"`
|
||||
ResourcePool sql.NullString `db:"ResourcePool" json:"ResourcePool"`
|
||||
Datacenter sql.NullString `db:"Datacenter" json:"Datacenter"`
|
||||
Cluster sql.NullString `db:"Cluster" json:"Cluster"`
|
||||
Folder sql.NullString `db:"Folder" json:"Folder"`
|
||||
ProvisionedDisk sql.NullFloat64 `db:"ProvisionedDisk" json:"ProvisionedDisk"`
|
||||
InitialVcpus sql.NullInt64 `db:"InitialVcpus" json:"InitialVcpus"`
|
||||
InitialRam sql.NullInt64 `db:"InitialRam" json:"InitialRam"`
|
||||
IsTemplate interface{} `db:"IsTemplate" json:"IsTemplate"`
|
||||
PoweredOn interface{} `db:"PoweredOn" json:"PoweredOn"`
|
||||
SrmPlaceholder interface{} `db:"SrmPlaceholder" json:"SrmPlaceholder"`
|
||||
VmUuid sql.NullString `db:"VmUuid" json:"VmUuid"`
|
||||
}
|
||||
|
||||
type InventoryHistory struct {
|
||||
Hid int64
|
||||
InventoryId sql.NullInt64
|
||||
ReportDate sql.NullInt64
|
||||
UpdateTime sql.NullInt64
|
||||
PreviousVcpus sql.NullInt64
|
||||
PreviousRam sql.NullInt64
|
||||
PreviousResourcePool sql.NullString
|
||||
PreviousProvisionedDisk sql.NullFloat64
|
||||
Hid int64 `db:"Hid" json:"Hid"`
|
||||
InventoryId sql.NullInt64 `db:"InventoryId" json:"InventoryId"`
|
||||
ReportDate sql.NullInt64 `db:"ReportDate" json:"ReportDate"`
|
||||
UpdateTime sql.NullInt64 `db:"UpdateTime" json:"UpdateTime"`
|
||||
PreviousVcpus sql.NullInt64 `db:"PreviousVcpus" json:"PreviousVcpus"`
|
||||
PreviousRam sql.NullInt64 `db:"PreviousRam" json:"PreviousRam"`
|
||||
PreviousResourcePool sql.NullString `db:"PreviousResourcePool" json:"PreviousResourcePool"`
|
||||
PreviousProvisionedDisk sql.NullFloat64 `db:"PreviousProvisionedDisk" json:"PreviousProvisionedDisk"`
|
||||
}
|
||||
|
||||
type PragmaTableInfo struct {
|
||||
Cid sql.NullInt64 `db:"cid" json:"cid"`
|
||||
Name sql.NullString `db:"name" json:"name"`
|
||||
Type sql.NullString `db:"type" json:"type"`
|
||||
Notnull sql.NullInt64 `db:"notnull" json:"notnull"`
|
||||
DfltValue sql.NullString `db:"dflt_value" json:"dflt_value"`
|
||||
Pk sql.NullInt64 `db:"pk" json:"pk"`
|
||||
}
|
||||
|
||||
type SnapshotRegistry struct {
|
||||
ID int64 `db:"id" json:"id"`
|
||||
SnapshotType string `db:"snapshot_type" json:"snapshot_type"`
|
||||
TableName string `db:"table_name" json:"table_name"`
|
||||
SnapshotTime int64 `db:"snapshot_time" json:"snapshot_time"`
|
||||
SnapshotCount int64 `db:"snapshot_count" json:"snapshot_count"`
|
||||
}
|
||||
|
||||
type SqliteMaster struct {
|
||||
Type sql.NullString `db:"type" json:"type"`
|
||||
Name sql.NullString `db:"name" json:"name"`
|
||||
TblName sql.NullString `db:"tbl_name" json:"tbl_name"`
|
||||
Rootpage sql.NullInt64 `db:"rootpage" json:"rootpage"`
|
||||
Sql sql.NullString `db:"sql" json:"sql"`
|
||||
}
|
||||
|
||||
type Update struct {
|
||||
Uid int64
|
||||
InventoryId sql.NullInt64
|
||||
UpdateTime sql.NullInt64
|
||||
UpdateType string
|
||||
NewVcpus sql.NullInt64
|
||||
NewRam sql.NullInt64
|
||||
NewResourcePool sql.NullString
|
||||
EventKey sql.NullString
|
||||
EventId sql.NullString
|
||||
NewProvisionedDisk sql.NullFloat64
|
||||
UserName sql.NullString
|
||||
PlaceholderChange sql.NullString
|
||||
Name sql.NullString
|
||||
RawChangeString []byte
|
||||
Uid int64 `db:"Uid" json:"Uid"`
|
||||
InventoryId sql.NullInt64 `db:"InventoryId" json:"InventoryId"`
|
||||
UpdateTime sql.NullInt64 `db:"UpdateTime" json:"UpdateTime"`
|
||||
UpdateType string `db:"UpdateType" json:"UpdateType"`
|
||||
NewVcpus sql.NullInt64 `db:"NewVcpus" json:"NewVcpus"`
|
||||
NewRam sql.NullInt64 `db:"NewRam" json:"NewRam"`
|
||||
NewResourcePool sql.NullString `db:"NewResourcePool" json:"NewResourcePool"`
|
||||
EventKey sql.NullString `db:"EventKey" json:"EventKey"`
|
||||
EventId sql.NullString `db:"EventId" json:"EventId"`
|
||||
NewProvisionedDisk sql.NullFloat64 `db:"NewProvisionedDisk" json:"NewProvisionedDisk"`
|
||||
UserName sql.NullString `db:"UserName" json:"UserName"`
|
||||
PlaceholderChange sql.NullString `db:"PlaceholderChange" json:"PlaceholderChange"`
|
||||
Name sql.NullString `db:"Name" json:"Name"`
|
||||
RawChangeString []byte `db:"RawChangeString" json:"RawChangeString"`
|
||||
}
|
||||
|
||||
@@ -32,9 +32,9 @@ WHERE "CloudId" = ? LIMIT 1;
|
||||
|
||||
-- name: CreateInventory :one
|
||||
INSERT INTO inventory (
|
||||
"Name", "Vcenter", "VmId", "VmUuid", "EventKey", "CloudId", "CreationTime", "ResourcePool", "VmType", "IsTemplate", "Datacenter", "Cluster", "Folder", "ProvisionedDisk", "InitialVcpus", "InitialRam", "SrmPlaceholder", "PoweredOn"
|
||||
"Name", "Vcenter", "VmId", "VmUuid", "EventKey", "CloudId", "CreationTime", "ResourcePool", "IsTemplate", "Datacenter", "Cluster", "Folder", "ProvisionedDisk", "InitialVcpus", "InitialRam", "SrmPlaceholder", "PoweredOn"
|
||||
) VALUES(
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
||||
)
|
||||
RETURNING *;
|
||||
|
||||
@@ -119,3 +119,13 @@ INSERT INTO inventory_history (
|
||||
?, ?, ?, ?, ?, ?, ?
|
||||
)
|
||||
RETURNING *;
|
||||
|
||||
-- name: SqliteTableExists :one
|
||||
SELECT COUNT(1) AS count
|
||||
FROM sqlite_master
|
||||
WHERE type = 'table' AND name = sqlc.arg('table_name');
|
||||
|
||||
-- name: SqliteColumnExists :one
|
||||
SELECT COUNT(1) AS count
|
||||
FROM pragma_table_info
|
||||
WHERE name = sqlc.arg('column_name');
|
||||
|
||||
@@ -17,8 +17,8 @@ RETURNING Uid, InventoryId, UpdateTime, UpdateType, NewVcpus, NewRam, NewResourc
|
||||
`
|
||||
|
||||
type CleanupUpdatesParams struct {
|
||||
UpdateType string
|
||||
UpdateTime sql.NullInt64
|
||||
UpdateType string `db:"updateType" json:"updateType"`
|
||||
UpdateTime sql.NullInt64 `db:"updateTime" json:"updateTime"`
|
||||
}
|
||||
|
||||
func (q *Queries) CleanupUpdates(ctx context.Context, arg CleanupUpdatesParams) error {
|
||||
@@ -47,19 +47,19 @@ RETURNING Eid, CloudId, Source, EventTime, ChainId, VmId, EventKey, DatacenterNa
|
||||
`
|
||||
|
||||
type CreateEventParams struct {
|
||||
CloudId string
|
||||
Source string
|
||||
EventTime sql.NullInt64
|
||||
ChainId string
|
||||
VmId sql.NullString
|
||||
VmName sql.NullString
|
||||
EventType sql.NullString
|
||||
EventKey sql.NullString
|
||||
DatacenterId sql.NullString
|
||||
DatacenterName sql.NullString
|
||||
ComputeResourceId sql.NullString
|
||||
ComputeResourceName sql.NullString
|
||||
UserName sql.NullString
|
||||
CloudId string `db:"CloudId" json:"CloudId"`
|
||||
Source string `db:"Source" json:"Source"`
|
||||
EventTime sql.NullInt64 `db:"EventTime" json:"EventTime"`
|
||||
ChainId string `db:"ChainId" json:"ChainId"`
|
||||
VmId sql.NullString `db:"VmId" json:"VmId"`
|
||||
VmName sql.NullString `db:"VmName" json:"VmName"`
|
||||
EventType sql.NullString `db:"EventType" json:"EventType"`
|
||||
EventKey sql.NullString `db:"EventKey" json:"EventKey"`
|
||||
DatacenterId sql.NullString `db:"DatacenterId" json:"DatacenterId"`
|
||||
DatacenterName sql.NullString `db:"DatacenterName" json:"DatacenterName"`
|
||||
ComputeResourceId sql.NullString `db:"ComputeResourceId" json:"ComputeResourceId"`
|
||||
ComputeResourceName sql.NullString `db:"ComputeResourceName" json:"ComputeResourceName"`
|
||||
UserName sql.NullString `db:"UserName" json:"UserName"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateEvent(ctx context.Context, arg CreateEventParams) (Event, error) {
|
||||
@@ -101,32 +101,31 @@ func (q *Queries) CreateEvent(ctx context.Context, arg CreateEventParams) (Event
|
||||
|
||||
const createInventory = `-- name: CreateInventory :one
|
||||
INSERT INTO inventory (
|
||||
"Name", "Vcenter", "VmId", "VmUuid", "EventKey", "CloudId", "CreationTime", "ResourcePool", "VmType", "IsTemplate", "Datacenter", "Cluster", "Folder", "ProvisionedDisk", "InitialVcpus", "InitialRam", "SrmPlaceholder", "PoweredOn"
|
||||
"Name", "Vcenter", "VmId", "VmUuid", "EventKey", "CloudId", "CreationTime", "ResourcePool", "IsTemplate", "Datacenter", "Cluster", "Folder", "ProvisionedDisk", "InitialVcpus", "InitialRam", "SrmPlaceholder", "PoweredOn"
|
||||
) VALUES(
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
||||
)
|
||||
RETURNING Iid, Name, Vcenter, VmId, EventKey, CloudId, CreationTime, DeletionTime, ResourcePool, VmType, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid
|
||||
RETURNING Iid, Name, Vcenter, VmId, EventKey, CloudId, CreationTime, DeletionTime, ResourcePool, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid
|
||||
`
|
||||
|
||||
type CreateInventoryParams struct {
|
||||
Name string
|
||||
Vcenter string
|
||||
VmId sql.NullString
|
||||
VmUuid sql.NullString
|
||||
EventKey sql.NullString
|
||||
CloudId sql.NullString
|
||||
CreationTime sql.NullInt64
|
||||
ResourcePool sql.NullString
|
||||
VmType sql.NullString
|
||||
IsTemplate interface{}
|
||||
Datacenter sql.NullString
|
||||
Cluster sql.NullString
|
||||
Folder sql.NullString
|
||||
ProvisionedDisk sql.NullFloat64
|
||||
InitialVcpus sql.NullInt64
|
||||
InitialRam sql.NullInt64
|
||||
SrmPlaceholder interface{}
|
||||
PoweredOn interface{}
|
||||
Name string `db:"Name" json:"Name"`
|
||||
Vcenter string `db:"Vcenter" json:"Vcenter"`
|
||||
VmId sql.NullString `db:"VmId" json:"VmId"`
|
||||
VmUuid sql.NullString `db:"VmUuid" json:"VmUuid"`
|
||||
EventKey sql.NullString `db:"EventKey" json:"EventKey"`
|
||||
CloudId sql.NullString `db:"CloudId" json:"CloudId"`
|
||||
CreationTime sql.NullInt64 `db:"CreationTime" json:"CreationTime"`
|
||||
ResourcePool sql.NullString `db:"ResourcePool" json:"ResourcePool"`
|
||||
IsTemplate interface{} `db:"IsTemplate" json:"IsTemplate"`
|
||||
Datacenter sql.NullString `db:"Datacenter" json:"Datacenter"`
|
||||
Cluster sql.NullString `db:"Cluster" json:"Cluster"`
|
||||
Folder sql.NullString `db:"Folder" json:"Folder"`
|
||||
ProvisionedDisk sql.NullFloat64 `db:"ProvisionedDisk" json:"ProvisionedDisk"`
|
||||
InitialVcpus sql.NullInt64 `db:"InitialVcpus" json:"InitialVcpus"`
|
||||
InitialRam sql.NullInt64 `db:"InitialRam" json:"InitialRam"`
|
||||
SrmPlaceholder interface{} `db:"SrmPlaceholder" json:"SrmPlaceholder"`
|
||||
PoweredOn interface{} `db:"PoweredOn" json:"PoweredOn"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateInventory(ctx context.Context, arg CreateInventoryParams) (Inventory, error) {
|
||||
@@ -139,7 +138,6 @@ func (q *Queries) CreateInventory(ctx context.Context, arg CreateInventoryParams
|
||||
arg.CloudId,
|
||||
arg.CreationTime,
|
||||
arg.ResourcePool,
|
||||
arg.VmType,
|
||||
arg.IsTemplate,
|
||||
arg.Datacenter,
|
||||
arg.Cluster,
|
||||
@@ -161,7 +159,6 @@ func (q *Queries) CreateInventory(ctx context.Context, arg CreateInventoryParams
|
||||
&i.CreationTime,
|
||||
&i.DeletionTime,
|
||||
&i.ResourcePool,
|
||||
&i.VmType,
|
||||
&i.Datacenter,
|
||||
&i.Cluster,
|
||||
&i.Folder,
|
||||
@@ -186,13 +183,13 @@ RETURNING Hid, InventoryId, ReportDate, UpdateTime, PreviousVcpus, PreviousRam,
|
||||
`
|
||||
|
||||
type CreateInventoryHistoryParams struct {
|
||||
InventoryId sql.NullInt64
|
||||
ReportDate sql.NullInt64
|
||||
UpdateTime sql.NullInt64
|
||||
PreviousVcpus sql.NullInt64
|
||||
PreviousRam sql.NullInt64
|
||||
PreviousResourcePool sql.NullString
|
||||
PreviousProvisionedDisk sql.NullFloat64
|
||||
InventoryId sql.NullInt64 `db:"InventoryId" json:"InventoryId"`
|
||||
ReportDate sql.NullInt64 `db:"ReportDate" json:"ReportDate"`
|
||||
UpdateTime sql.NullInt64 `db:"UpdateTime" json:"UpdateTime"`
|
||||
PreviousVcpus sql.NullInt64 `db:"PreviousVcpus" json:"PreviousVcpus"`
|
||||
PreviousRam sql.NullInt64 `db:"PreviousRam" json:"PreviousRam"`
|
||||
PreviousResourcePool sql.NullString `db:"PreviousResourcePool" json:"PreviousResourcePool"`
|
||||
PreviousProvisionedDisk sql.NullFloat64 `db:"PreviousProvisionedDisk" json:"PreviousProvisionedDisk"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateInventoryHistory(ctx context.Context, arg CreateInventoryHistoryParams) (InventoryHistory, error) {
|
||||
@@ -229,19 +226,19 @@ RETURNING Uid, InventoryId, UpdateTime, UpdateType, NewVcpus, NewRam, NewResourc
|
||||
`
|
||||
|
||||
type CreateUpdateParams struct {
|
||||
InventoryId sql.NullInt64
|
||||
Name sql.NullString
|
||||
EventKey sql.NullString
|
||||
EventId sql.NullString
|
||||
UpdateTime sql.NullInt64
|
||||
UpdateType string
|
||||
NewVcpus sql.NullInt64
|
||||
NewRam sql.NullInt64
|
||||
NewResourcePool sql.NullString
|
||||
NewProvisionedDisk sql.NullFloat64
|
||||
UserName sql.NullString
|
||||
PlaceholderChange sql.NullString
|
||||
RawChangeString []byte
|
||||
InventoryId sql.NullInt64 `db:"InventoryId" json:"InventoryId"`
|
||||
Name sql.NullString `db:"Name" json:"Name"`
|
||||
EventKey sql.NullString `db:"EventKey" json:"EventKey"`
|
||||
EventId sql.NullString `db:"EventId" json:"EventId"`
|
||||
UpdateTime sql.NullInt64 `db:"UpdateTime" json:"UpdateTime"`
|
||||
UpdateType string `db:"UpdateType" json:"UpdateType"`
|
||||
NewVcpus sql.NullInt64 `db:"NewVcpus" json:"NewVcpus"`
|
||||
NewRam sql.NullInt64 `db:"NewRam" json:"NewRam"`
|
||||
NewResourcePool sql.NullString `db:"NewResourcePool" json:"NewResourcePool"`
|
||||
NewProvisionedDisk sql.NullFloat64 `db:"NewProvisionedDisk" json:"NewProvisionedDisk"`
|
||||
UserName sql.NullString `db:"UserName" json:"UserName"`
|
||||
PlaceholderChange sql.NullString `db:"PlaceholderChange" json:"PlaceholderChange"`
|
||||
RawChangeString []byte `db:"RawChangeString" json:"RawChangeString"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateUpdate(ctx context.Context, arg CreateUpdateParams) (Update, error) {
|
||||
@@ -281,7 +278,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, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid FROM inventory
|
||||
WHERE "Name" = ?
|
||||
`
|
||||
|
||||
@@ -304,7 +301,6 @@ func (q *Queries) GetInventoryByName(ctx context.Context, name string) ([]Invent
|
||||
&i.CreationTime,
|
||||
&i.DeletionTime,
|
||||
&i.ResourcePool,
|
||||
&i.VmType,
|
||||
&i.Datacenter,
|
||||
&i.Cluster,
|
||||
&i.Folder,
|
||||
@@ -330,7 +326,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, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid FROM inventory
|
||||
WHERE "Vcenter" = ?
|
||||
`
|
||||
|
||||
@@ -353,7 +349,6 @@ func (q *Queries) GetInventoryByVcenter(ctx context.Context, vcenter string) ([]
|
||||
&i.CreationTime,
|
||||
&i.DeletionTime,
|
||||
&i.ResourcePool,
|
||||
&i.VmType,
|
||||
&i.Datacenter,
|
||||
&i.Cluster,
|
||||
&i.Folder,
|
||||
@@ -379,7 +374,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, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid FROM inventory
|
||||
WHERE "CloudId" = ? LIMIT 1
|
||||
`
|
||||
|
||||
@@ -396,7 +391,6 @@ func (q *Queries) GetInventoryEventId(ctx context.Context, cloudid sql.NullStrin
|
||||
&i.CreationTime,
|
||||
&i.DeletionTime,
|
||||
&i.ResourcePool,
|
||||
&i.VmType,
|
||||
&i.Datacenter,
|
||||
&i.Cluster,
|
||||
&i.Folder,
|
||||
@@ -412,7 +406,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, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid FROM inventory
|
||||
WHERE "Vcenter" = ?1
|
||||
`
|
||||
|
||||
@@ -435,7 +429,6 @@ func (q *Queries) GetInventoryVcUrl(ctx context.Context, vc string) ([]Inventory
|
||||
&i.CreationTime,
|
||||
&i.DeletionTime,
|
||||
&i.ResourcePool,
|
||||
&i.VmType,
|
||||
&i.Datacenter,
|
||||
&i.Cluster,
|
||||
&i.Folder,
|
||||
@@ -461,13 +454,13 @@ 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, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid FROM inventory
|
||||
WHERE "VmId" = ?1 AND "Datacenter" = ?2
|
||||
`
|
||||
|
||||
type GetInventoryVmIdParams struct {
|
||||
VmId sql.NullString
|
||||
DatacenterName sql.NullString
|
||||
VmId sql.NullString `db:"vmId" json:"vmId"`
|
||||
DatacenterName sql.NullString `db:"datacenterName" json:"datacenterName"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetInventoryVmId(ctx context.Context, arg GetInventoryVmIdParams) (Inventory, error) {
|
||||
@@ -483,7 +476,6 @@ func (q *Queries) GetInventoryVmId(ctx context.Context, arg GetInventoryVmIdPara
|
||||
&i.CreationTime,
|
||||
&i.DeletionTime,
|
||||
&i.ResourcePool,
|
||||
&i.VmType,
|
||||
&i.Datacenter,
|
||||
&i.Cluster,
|
||||
&i.Folder,
|
||||
@@ -499,13 +491,13 @@ 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, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid FROM inventory
|
||||
WHERE "VmUuid" = ?1 AND "Datacenter" = ?2
|
||||
`
|
||||
|
||||
type GetInventoryVmUuidParams struct {
|
||||
VmUuid sql.NullString
|
||||
DatacenterName sql.NullString
|
||||
VmUuid sql.NullString `db:"vmUuid" json:"vmUuid"`
|
||||
DatacenterName sql.NullString `db:"datacenterName" json:"datacenterName"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetInventoryVmUuid(ctx context.Context, arg GetInventoryVmUuidParams) (Inventory, error) {
|
||||
@@ -521,7 +513,6 @@ func (q *Queries) GetInventoryVmUuid(ctx context.Context, arg GetInventoryVmUuid
|
||||
&i.CreationTime,
|
||||
&i.DeletionTime,
|
||||
&i.ResourcePool,
|
||||
&i.VmType,
|
||||
&i.Datacenter,
|
||||
&i.Cluster,
|
||||
&i.Folder,
|
||||
@@ -537,7 +528,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, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid FROM inventory
|
||||
ORDER BY "CreationTime"
|
||||
`
|
||||
|
||||
@@ -560,7 +551,6 @@ func (q *Queries) GetReportInventory(ctx context.Context) ([]Inventory, error) {
|
||||
&i.CreationTime,
|
||||
&i.DeletionTime,
|
||||
&i.ResourcePool,
|
||||
&i.VmType,
|
||||
&i.Datacenter,
|
||||
&i.Cluster,
|
||||
&i.Folder,
|
||||
@@ -634,8 +624,8 @@ WHERE "UpdateType" = ?1 AND "InventoryId" = ?2
|
||||
`
|
||||
|
||||
type GetVmUpdatesParams struct {
|
||||
UpdateType string
|
||||
InventoryId sql.NullInt64
|
||||
UpdateType string `db:"updateType" json:"updateType"`
|
||||
InventoryId sql.NullInt64 `db:"InventoryId" json:"InventoryId"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetVmUpdates(ctx context.Context, arg GetVmUpdatesParams) ([]Update, error) {
|
||||
@@ -679,12 +669,12 @@ func (q *Queries) GetVmUpdates(ctx context.Context, arg GetVmUpdatesParams) ([]U
|
||||
const inventoryCleanup = `-- name: InventoryCleanup :exec
|
||||
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
|
||||
RETURNING Iid, Name, Vcenter, VmId, EventKey, CloudId, CreationTime, DeletionTime, ResourcePool, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid
|
||||
`
|
||||
|
||||
type InventoryCleanupParams struct {
|
||||
VmId sql.NullString
|
||||
DatacenterName sql.NullString
|
||||
VmId sql.NullString `db:"vmId" json:"vmId"`
|
||||
DatacenterName sql.NullString `db:"datacenterName" json:"datacenterName"`
|
||||
}
|
||||
|
||||
func (q *Queries) InventoryCleanup(ctx context.Context, arg InventoryCleanupParams) error {
|
||||
@@ -695,7 +685,7 @@ func (q *Queries) InventoryCleanup(ctx context.Context, arg InventoryCleanupPara
|
||||
const inventoryCleanupTemplates = `-- name: InventoryCleanupTemplates :exec
|
||||
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
|
||||
RETURNING Iid, Name, Vcenter, VmId, EventKey, CloudId, CreationTime, DeletionTime, ResourcePool, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid
|
||||
`
|
||||
|
||||
func (q *Queries) InventoryCleanupTemplates(ctx context.Context) error {
|
||||
@@ -706,7 +696,7 @@ func (q *Queries) InventoryCleanupTemplates(ctx context.Context) error {
|
||||
const inventoryCleanupVcenter = `-- name: InventoryCleanupVcenter :exec
|
||||
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
|
||||
RETURNING Iid, Name, Vcenter, VmId, EventKey, CloudId, CreationTime, DeletionTime, ResourcePool, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid
|
||||
`
|
||||
|
||||
func (q *Queries) InventoryCleanupVcenter(ctx context.Context, vc string) error {
|
||||
@@ -721,9 +711,9 @@ WHERE "VmId" = ?2 AND "Datacenter" = ?3
|
||||
`
|
||||
|
||||
type InventoryMarkDeletedParams struct {
|
||||
DeletionTime sql.NullInt64
|
||||
VmId sql.NullString
|
||||
DatacenterName sql.NullString
|
||||
DeletionTime sql.NullInt64 `db:"deletionTime" json:"deletionTime"`
|
||||
VmId sql.NullString `db:"vmId" json:"vmId"`
|
||||
DatacenterName sql.NullString `db:"datacenterName" json:"datacenterName"`
|
||||
}
|
||||
|
||||
func (q *Queries) InventoryMarkDeleted(ctx context.Context, arg InventoryMarkDeletedParams) error {
|
||||
@@ -738,9 +728,9 @@ WHERE "Iid" = ?3
|
||||
`
|
||||
|
||||
type InventoryUpdateParams struct {
|
||||
Uuid sql.NullString
|
||||
SrmPlaceholder interface{}
|
||||
Iid int64
|
||||
Uuid sql.NullString `db:"uuid" json:"uuid"`
|
||||
SrmPlaceholder interface{} `db:"srmPlaceholder" json:"srmPlaceholder"`
|
||||
Iid int64 `db:"iid" json:"iid"`
|
||||
}
|
||||
|
||||
func (q *Queries) InventoryUpdate(ctx context.Context, arg InventoryUpdateParams) error {
|
||||
@@ -793,7 +783,7 @@ func (q *Queries) ListEvents(ctx context.Context) ([]Event, 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, Datacenter, Cluster, Folder, ProvisionedDisk, InitialVcpus, InitialRam, IsTemplate, PoweredOn, SrmPlaceholder, VmUuid FROM inventory
|
||||
ORDER BY "Name"
|
||||
`
|
||||
|
||||
@@ -816,7 +806,6 @@ func (q *Queries) ListInventory(ctx context.Context) ([]Inventory, error) {
|
||||
&i.CreationTime,
|
||||
&i.DeletionTime,
|
||||
&i.ResourcePool,
|
||||
&i.VmType,
|
||||
&i.Datacenter,
|
||||
&i.Cluster,
|
||||
&i.Folder,
|
||||
@@ -887,6 +876,32 @@ func (q *Queries) ListUnprocessedEvents(ctx context.Context, eventtime sql.NullI
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const sqliteColumnExists = `-- name: SqliteColumnExists :one
|
||||
SELECT COUNT(1) AS count
|
||||
FROM pragma_table_info
|
||||
WHERE name = ?1
|
||||
`
|
||||
|
||||
func (q *Queries) SqliteColumnExists(ctx context.Context, columnName sql.NullString) (int64, error) {
|
||||
row := q.db.QueryRowContext(ctx, sqliteColumnExists, columnName)
|
||||
var count int64
|
||||
err := row.Scan(&count)
|
||||
return count, err
|
||||
}
|
||||
|
||||
const sqliteTableExists = `-- name: SqliteTableExists :one
|
||||
SELECT COUNT(1) AS count
|
||||
FROM sqlite_master
|
||||
WHERE type = 'table' AND name = ?1
|
||||
`
|
||||
|
||||
func (q *Queries) SqliteTableExists(ctx context.Context, tableName sql.NullString) (int64, error) {
|
||||
row := q.db.QueryRowContext(ctx, sqliteTableExists, tableName)
|
||||
var count int64
|
||||
err := row.Scan(&count)
|
||||
return count, err
|
||||
}
|
||||
|
||||
const updateEventsProcessed = `-- name: UpdateEventsProcessed :exec
|
||||
UPDATE events
|
||||
SET "Processed" = 1
|
||||
|
||||
@@ -8,7 +8,6 @@ CREATE TABLE IF NOT EXISTS inventory (
|
||||
"CreationTime" INTEGER,
|
||||
"DeletionTime" INTEGER,
|
||||
"ResourcePool" TEXT,
|
||||
"VmType" TEXT,
|
||||
"Datacenter" TEXT,
|
||||
"Cluster" TEXT,
|
||||
"Folder" TEXT,
|
||||
@@ -66,3 +65,31 @@ CREATE TABLE IF NOT EXISTS inventory_history (
|
||||
"PreviousResourcePool" TEXT,
|
||||
"PreviousProvisionedDisk" REAL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS snapshot_registry (
|
||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
"snapshot_type" TEXT NOT NULL,
|
||||
"table_name" TEXT NOT NULL UNIQUE,
|
||||
"snapshot_time" INTEGER NOT NULL,
|
||||
"snapshot_count" BIGINT NOT NULL DEFAULT 0
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_snapshot_registry_type_time ON snapshot_registry (snapshot_type, snapshot_time);
|
||||
|
||||
-- The following tables are declared for sqlc type-checking only.
|
||||
-- Do not apply this file as a migration.
|
||||
CREATE TABLE sqlite_master (
|
||||
"type" TEXT,
|
||||
"name" TEXT,
|
||||
"tbl_name" TEXT,
|
||||
"rootpage" INTEGER,
|
||||
"sql" TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE pragma_table_info (
|
||||
"cid" INTEGER,
|
||||
"name" TEXT,
|
||||
"type" TEXT,
|
||||
"notnull" INTEGER,
|
||||
"dflt_value" TEXT,
|
||||
"pk" INTEGER
|
||||
);
|
||||
|
||||
146
dist/assets/css/web3.css
vendored
Normal file
146
dist/assets/css/web3.css
vendored
Normal file
@@ -0,0 +1,146 @@
|
||||
:root {
|
||||
--web2-blue: #1d9bf0;
|
||||
--web2-slate: #0f172a;
|
||||
--web2-muted: #64748b;
|
||||
--web2-card: #ffffff;
|
||||
--web2-border: #e5e7eb;
|
||||
}
|
||||
body {
|
||||
font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif;
|
||||
color: var(--web2-slate);
|
||||
}
|
||||
.web2-bg {
|
||||
background: #ffffff;
|
||||
}
|
||||
.web2-shell {
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem 1.5rem 4rem;
|
||||
}
|
||||
.web2-header {
|
||||
background: var(--web2-card);
|
||||
border: 1px solid var(--web2-border);
|
||||
border-radius: 4px;
|
||||
padding: 1.5rem 2rem;
|
||||
}
|
||||
.web2-card {
|
||||
background: var(--web2-card);
|
||||
border: 1px solid var(--web2-border);
|
||||
border-radius: 4px;
|
||||
padding: 1.5rem 1.75rem;
|
||||
}
|
||||
.web2-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
background: #f8fafc;
|
||||
border: 1px solid var(--web2-border);
|
||||
color: var(--web2-muted);
|
||||
padding: 0.2rem 0.6rem;
|
||||
border-radius: 3px;
|
||||
font-size: 0.85rem;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
.web2-link {
|
||||
color: var(--web2-blue);
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
.web2-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.web2-button {
|
||||
background: var(--web2-blue);
|
||||
color: #fff;
|
||||
padding: 0.45rem 0.9rem;
|
||||
border-radius: 3px;
|
||||
border: 1px solid #1482d0;
|
||||
box-shadow: none;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
}
|
||||
.web2-button:hover {
|
||||
background: #1787d4;
|
||||
}
|
||||
.web2-button-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.web2-button-group .web2-button {
|
||||
margin: 0 0.5rem 0.5rem 0;
|
||||
}
|
||||
.web3-button {
|
||||
background: #f3f4f6;
|
||||
color: #0f172a;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #e5e7eb;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease, box-shadow 0.15s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
.web3-button:hover {
|
||||
background: #e2e8f0;
|
||||
border-color: #cbd5e1;
|
||||
}
|
||||
.web3-button.active {
|
||||
background: #dbeafe;
|
||||
border-color: #93c5fd;
|
||||
color: #1d4ed8;
|
||||
box-shadow: 0 0 0 2px rgba(147, 197, 253, 0.35);
|
||||
}
|
||||
.web3-button-group {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.web2-list li {
|
||||
background: #ffffff;
|
||||
border: 1px solid var(--web2-border);
|
||||
border-radius: 3px;
|
||||
padding: 0.75rem 1rem;
|
||||
box-shadow: none;
|
||||
}
|
||||
.web2-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
.web2-table thead th {
|
||||
text-align: left;
|
||||
padding: 0.75rem 0.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--web2-muted);
|
||||
border-bottom: 1px solid var(--web2-border);
|
||||
}
|
||||
.web2-table tbody td {
|
||||
padding: 0.9rem 0.5rem;
|
||||
border-bottom: 1px solid var(--web2-border);
|
||||
}
|
||||
.web2-table tbody tr:nth-child(odd) {
|
||||
background: #f8fafc;
|
||||
}
|
||||
.web2-table tbody tr:nth-child(even) {
|
||||
background: #ffffff;
|
||||
}
|
||||
.web2-group-row td {
|
||||
background: #e8eef5;
|
||||
color: #0f172a;
|
||||
border-bottom: 1px solid var(--web2-border);
|
||||
padding: 0.65rem 0.5rem;
|
||||
}
|
||||
.web2-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
border: 1px solid var(--web2-border);
|
||||
padding: 0.15rem 0.45rem;
|
||||
border-radius: 3px;
|
||||
font-size: 0.8rem;
|
||||
color: var(--web2-muted);
|
||||
background: #f8fafc;
|
||||
}
|
||||
2
dist/dist.go
vendored
2
dist/dist.go
vendored
@@ -4,5 +4,5 @@ import (
|
||||
"embed"
|
||||
)
|
||||
|
||||
//go:embed all:assets
|
||||
//go:embed all:assets favicon.ico favicon-16x16.png favicon-32x32.png
|
||||
var AssetsDir embed.FS
|
||||
|
||||
BIN
dist/favicon-16x16.png
vendored
Normal file
BIN
dist/favicon-16x16.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 520 B |
BIN
dist/favicon-32x32.png
vendored
Normal file
BIN
dist/favicon-32x32.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
BIN
dist/favicon.ico
vendored
Normal file
BIN
dist/favicon.ico
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -95,14 +95,33 @@ func beforeAll() {
|
||||
|
||||
func startApp() error {
|
||||
port := getPort()
|
||||
app = exec.Command("go", "run", "main.go")
|
||||
settingsPath := "./test-settings.yml"
|
||||
settingsBody := fmt.Sprintf(`settings:
|
||||
log_level: "debug"
|
||||
log_output: "text"
|
||||
database_driver: "sqlite"
|
||||
database_url: "./test-db.sqlite3"
|
||||
bind_ip: "127.0.0.1"
|
||||
bind_port: %d
|
||||
bind_disable_tls: true
|
||||
tls_cert_filename:
|
||||
tls_key_filename:
|
||||
vcenter_username: "test"
|
||||
vcenter_password: "test"
|
||||
vcenter_insecure: true
|
||||
vcenter_event_polling_seconds: 60
|
||||
vcenter_inventory_polling_seconds: 7200
|
||||
vcenter_inventory_snapshot_seconds: 3600
|
||||
vcenter_inventory_aggregate_seconds: 86400
|
||||
hourly_snapshot_max_age_days: 1
|
||||
daily_snapshot_max_age_months: 1
|
||||
`, port)
|
||||
if err := os.WriteFile("../"+settingsPath, []byte(settingsBody), 0o600); err != nil {
|
||||
return err
|
||||
}
|
||||
app = exec.Command("go", "run", "main.go", "-settings", settingsPath)
|
||||
app.Dir = "../"
|
||||
app.Env = append(
|
||||
os.Environ(),
|
||||
"DB_URL=./test-db.sqlite3",
|
||||
fmt.Sprintf("PORT=%d", port),
|
||||
"LOG_LEVEL=DEBUG",
|
||||
)
|
||||
app.Env = os.Environ()
|
||||
|
||||
var err error
|
||||
baseUrL, err = url.Parse(fmt.Sprintf("http://localhost:%d", port))
|
||||
@@ -188,6 +207,9 @@ func afterAll() {
|
||||
if err := os.Remove("../test-db.sqlite3"); err != nil {
|
||||
log.Fatalf("could not remove test-db.sqlite3: %v", err)
|
||||
}
|
||||
if err := os.Remove("../test-settings.yml"); err != nil {
|
||||
log.Fatalf("could not remove test-settings.yml: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// beforeEach creates a new context and page for each test,
|
||||
|
||||
26
go.mod
26
go.mod
@@ -1,33 +1,48 @@
|
||||
module vctp
|
||||
|
||||
go 1.25.0
|
||||
go 1.25.5
|
||||
|
||||
require (
|
||||
github.com/a-h/templ v0.3.977
|
||||
github.com/go-co-op/gocron/v2 v2.19.0
|
||||
github.com/jackc/pgx/v5 v5.8.0
|
||||
github.com/jmoiron/sqlx v1.4.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/pressly/goose/v3 v3.26.0
|
||||
github.com/prometheus/client_golang v1.19.0
|
||||
github.com/swaggo/swag v1.16.6
|
||||
github.com/vmware/govmomi v0.52.0
|
||||
github.com/xuri/excelize/v2 v2.10.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
modernc.org/sqlite v1.43.0
|
||||
modernc.org/sqlite v1.44.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/PuerkitoBio/purell v1.1.1 // indirect
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||
github.com/go-openapi/jsonreference v0.19.6 // indirect
|
||||
github.com/go-openapi/spec v0.20.4 // indirect
|
||||
github.com/go-openapi/swag v0.19.15 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/jonboulle/clockwork v0.5.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.6 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mfridman/interpolate v0.0.2 // indirect
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.5.0 // indirect
|
||||
github.com/prometheus/common v0.48.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/richardlehane/mscfb v1.0.6 // indirect
|
||||
github.com/richardlehane/msoleps v1.0.5 // indirect
|
||||
github.com/richardlehane/msoleps v1.0.6 // indirect
|
||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||
github.com/sethvargo/go-retry v0.3.0 // indirect
|
||||
github.com/tiendc/go-deepcopy v1.7.2 // indirect
|
||||
@@ -36,10 +51,13 @@ require (
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/crypto v0.47.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect
|
||||
golang.org/x/mod v0.32.0 // indirect
|
||||
golang.org/x/net v0.49.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/text v0.33.0 // indirect
|
||||
golang.org/x/tools v0.41.0 // indirect
|
||||
google.golang.org/protobuf v1.33.0 // indirect
|
||||
modernc.org/libc v1.67.4 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
|
||||
65
go.sum
65
go.sum
@@ -1,7 +1,18 @@
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
|
||||
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
github.com/a-h/templ v0.3.977 h1:kiKAPXTZE2Iaf8JbtM21r54A8bCNsncrfnokZZSrSDg=
|
||||
github.com/a-h/templ v0.3.977/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -9,6 +20,16 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/go-co-op/gocron/v2 v2.19.0 h1:OKf2y6LXPs/BgBI2fl8PxUpNAI1DA9Mg+hSeGOS38OU=
|
||||
github.com/go-co-op/gocron/v2 v2.19.0/go.mod h1:5lEiCKk1oVJV39Zg7/YG10OnaVrDAV5GGR6O0663k6U=
|
||||
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
|
||||
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs=
|
||||
github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
|
||||
github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M=
|
||||
github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
|
||||
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
|
||||
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||
@@ -30,16 +51,23 @@ github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
|
||||
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
|
||||
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
|
||||
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
@@ -48,16 +76,25 @@ github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6B
|
||||
github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg=
|
||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pressly/goose/v3 v3.26.0 h1:KJakav68jdH0WDvoAcj8+n61WqOIaPGgH0bJWS6jpmM=
|
||||
github.com/pressly/goose/v3 v3.26.0/go.mod h1:4hC1KrritdCxtuFsqgs1R4AU5bWtTAf+cnWvfhf2DNY=
|
||||
github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
|
||||
github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
|
||||
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
|
||||
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
|
||||
github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
|
||||
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/richardlehane/mscfb v1.0.6 h1:eN3bvvZCp00bs7Zf52bxNwAx5lJDBK1tCuH19qq5aC8=
|
||||
github.com/richardlehane/mscfb v1.0.6/go.mod h1:pe0+IUIc0AHh0+teNzBlJCtSyZdFOGgV4ZK9bsoV+Jo=
|
||||
github.com/richardlehane/msoleps v1.0.5 h1:kNlmACZuwC8ZWPLoJtD+HtZOsKJgYn7gXgUIcRB7dbo=
|
||||
github.com/richardlehane/msoleps v1.0.5/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
|
||||
github.com/richardlehane/msoleps v1.0.6 h1:9BvkpjvD+iUBalUY4esMwv6uBkfOip/Lzvd93jvR9gg=
|
||||
github.com/richardlehane/msoleps v1.0.6/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
@@ -66,9 +103,12 @@ github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah
|
||||
github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
|
||||
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
|
||||
github.com/tiendc/go-deepcopy v1.7.2 h1:Ut2yYR7W9tWjTQitganoIue4UGxZwCcJy3orjrrIj44=
|
||||
github.com/tiendc/go-deepcopy v1.7.2/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ=
|
||||
github.com/vmware/govmomi v0.52.0 h1:JyxQ1IQdllrY7PJbv2am9mRsv3p9xWlIQ66bv+XnyLw=
|
||||
@@ -91,23 +131,36 @@ golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
|
||||
golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
|
||||
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
|
||||
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
|
||||
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
|
||||
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
||||
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
|
||||
@@ -132,8 +185,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.43.0 h1:8YqiFx3G1VhHTXO2Q00bl1Wz9KhS9Q5okwfp9Y97VnA=
|
||||
modernc.org/sqlite v1.43.0/go.mod h1:+VkC6v3pLOAE0A0uVucQEcbVW0I5nHCeDaBf+DpsQT8=
|
||||
modernc.org/sqlite v1.44.0 h1:YjCKJnzZde2mLVy0cMKTSL4PxCmbIguOq9lGp8ZvGOc=
|
||||
modernc.org/sqlite v1.44.0/go.mod h1:2Dq41ir5/qri7QJJJKNZcP4UF7TsX/KNeykYgPDtGhE=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
|
||||
125
internal/metrics/metrics.go
Normal file
125
internal/metrics/metrics.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
var (
|
||||
registry = prometheus.NewRegistry()
|
||||
|
||||
HourlySnapshotTotal = prometheus.NewCounter(prometheus.CounterOpts{Name: "vctp_hourly_snapshots_total", Help: "Total number of hourly snapshot jobs completed."})
|
||||
HourlySnapshotFailures = prometheus.NewCounter(prometheus.CounterOpts{Name: "vctp_hourly_snapshots_failed_total", Help: "Hourly snapshot jobs that failed."})
|
||||
HourlySnapshotLast = prometheus.NewGauge(prometheus.GaugeOpts{Name: "vctp_hourly_snapshot_last_unix", Help: "Unix timestamp of the last hourly snapshot start time."})
|
||||
HourlySnapshotRows = prometheus.NewGauge(prometheus.GaugeOpts{Name: "vctp_hourly_snapshot_last_rows", Help: "Row count of the last hourly snapshot table."})
|
||||
|
||||
DailyAggregationsTotal = prometheus.NewCounter(prometheus.CounterOpts{Name: "vctp_daily_aggregations_total", Help: "Total number of daily aggregation jobs completed."})
|
||||
DailyAggregationFailures = prometheus.NewCounter(prometheus.CounterOpts{Name: "vctp_daily_aggregations_failed_total", Help: "Daily aggregation jobs that failed."})
|
||||
DailyAggregationDuration = prometheus.NewHistogram(prometheus.HistogramOpts{
|
||||
Name: "vctp_daily_aggregation_duration_seconds",
|
||||
Help: "Duration of daily aggregation jobs.",
|
||||
Buckets: prometheus.ExponentialBuckets(1, 2, 10),
|
||||
})
|
||||
|
||||
MonthlyAggregationsTotal = prometheus.NewCounter(prometheus.CounterOpts{Name: "vctp_monthly_aggregations_total", Help: "Total number of monthly aggregation jobs completed."})
|
||||
MonthlyAggregationFailures = prometheus.NewCounter(prometheus.CounterOpts{Name: "vctp_monthly_aggregations_failed_total", Help: "Monthly aggregation jobs that failed."})
|
||||
MonthlyAggregationDuration = prometheus.NewHistogram(prometheus.HistogramOpts{
|
||||
Name: "vctp_monthly_aggregation_duration_seconds",
|
||||
Help: "Duration of monthly aggregation jobs.",
|
||||
Buckets: prometheus.ExponentialBuckets(1, 2, 10),
|
||||
})
|
||||
|
||||
ReportsAvailable = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "vctp_reports_available",
|
||||
Help: "Number of downloadable reports present on disk.",
|
||||
})
|
||||
|
||||
VcenterConnectFailures = prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Name: "vctp_vcenter_connect_failures_total",
|
||||
Help: "Failed connections to vCenter during snapshot runs.",
|
||||
}, []string{"vcenter"})
|
||||
|
||||
VcenterSnapshotDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{
|
||||
Name: "vctp_vcenter_snapshot_duration_seconds",
|
||||
Help: "Duration of per-vCenter hourly snapshot jobs.",
|
||||
Buckets: prometheus.ExponentialBuckets(0.5, 2, 10),
|
||||
}, []string{"vcenter"})
|
||||
|
||||
VcenterInventorySize = prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Name: "vctp_vcenter_inventory_size",
|
||||
Help: "Number of VMs seen in the last successful snapshot per vCenter.",
|
||||
}, []string{"vcenter"})
|
||||
)
|
||||
|
||||
func init() {
|
||||
registry.MustRegister(
|
||||
HourlySnapshotTotal,
|
||||
HourlySnapshotFailures,
|
||||
HourlySnapshotLast,
|
||||
HourlySnapshotRows,
|
||||
DailyAggregationsTotal,
|
||||
DailyAggregationFailures,
|
||||
DailyAggregationDuration,
|
||||
MonthlyAggregationsTotal,
|
||||
MonthlyAggregationFailures,
|
||||
MonthlyAggregationDuration,
|
||||
ReportsAvailable,
|
||||
VcenterConnectFailures,
|
||||
VcenterSnapshotDuration,
|
||||
VcenterInventorySize,
|
||||
)
|
||||
}
|
||||
|
||||
// Handler returns an http.Handler that serves Prometheus metrics.
|
||||
func Handler() http.Handler {
|
||||
return promhttp.HandlerFor(registry, promhttp.HandlerOpts{})
|
||||
}
|
||||
|
||||
// RecordVcenterSnapshot logs per-vCenter snapshot metrics.
|
||||
func RecordVcenterSnapshot(vcenter string, duration time.Duration, vmCount int64, err error) {
|
||||
VcenterSnapshotDuration.WithLabelValues(vcenter).Observe(duration.Seconds())
|
||||
if err != nil {
|
||||
VcenterConnectFailures.WithLabelValues(vcenter).Inc()
|
||||
return
|
||||
}
|
||||
VcenterInventorySize.WithLabelValues(vcenter).Set(float64(vmCount))
|
||||
}
|
||||
|
||||
// RecordHourlySnapshot logs aggregate hourly snapshot results.
|
||||
func RecordHourlySnapshot(start time.Time, rows int64, err error) {
|
||||
HourlySnapshotLast.Set(float64(start.Unix()))
|
||||
HourlySnapshotRows.Set(float64(rows))
|
||||
if err != nil {
|
||||
HourlySnapshotFailures.Inc()
|
||||
return
|
||||
}
|
||||
HourlySnapshotTotal.Inc()
|
||||
}
|
||||
|
||||
// RecordDailyAggregation logs daily aggregation metrics.
|
||||
func RecordDailyAggregation(duration time.Duration, err error) {
|
||||
DailyAggregationDuration.Observe(duration.Seconds())
|
||||
if err != nil {
|
||||
DailyAggregationFailures.Inc()
|
||||
return
|
||||
}
|
||||
DailyAggregationsTotal.Inc()
|
||||
}
|
||||
|
||||
// RecordMonthlyAggregation logs monthly aggregation metrics.
|
||||
func RecordMonthlyAggregation(duration time.Duration, err error) {
|
||||
MonthlyAggregationDuration.Observe(duration.Seconds())
|
||||
if err != nil {
|
||||
MonthlyAggregationFailures.Inc()
|
||||
return
|
||||
}
|
||||
MonthlyAggregationsTotal.Inc()
|
||||
}
|
||||
|
||||
// SetReportsAvailable updates the gauge for report files found on disk.
|
||||
func SetReportsAvailable(count int) {
|
||||
ReportsAvailable.Set(float64(count))
|
||||
}
|
||||
@@ -113,12 +113,10 @@ func CreateInventoryReport(logger *slog.Logger, Database db.Database, ctx contex
|
||||
}
|
||||
|
||||
// Set column autowidth
|
||||
/*
|
||||
err = SetColAutoWidth(xlsx, sheetName)
|
||||
if err != nil {
|
||||
fmt.Printf("Error setting auto width : '%s'\n", err)
|
||||
}
|
||||
*/
|
||||
err = SetColAutoWidth(xlsx, sheetName)
|
||||
if err != nil {
|
||||
logger.Error("Error setting auto width", "error", err)
|
||||
}
|
||||
|
||||
// Save the Excel file into a byte buffer
|
||||
if err := xlsx.Write(&buffer); err != nil {
|
||||
@@ -226,12 +224,10 @@ func CreateUpdatesReport(logger *slog.Logger, Database db.Database, ctx context.
|
||||
}
|
||||
|
||||
// Set column autowidth
|
||||
/*
|
||||
err = SetColAutoWidth(xlsx, sheetName)
|
||||
if err != nil {
|
||||
fmt.Printf("Error setting auto width : '%s'\n", err)
|
||||
}
|
||||
*/
|
||||
err = SetColAutoWidth(xlsx, sheetName)
|
||||
if err != nil {
|
||||
logger.Error("Error setting auto width", "error", err)
|
||||
}
|
||||
|
||||
// Save the Excel file into a byte buffer
|
||||
if err := xlsx.Write(&buffer); err != nil {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"vctp/internal/utils"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
@@ -19,10 +20,39 @@ type Settings struct {
|
||||
// SettingsYML struct holds various runtime data that is too cumbersome to specify via command line, eg replacement properties
|
||||
type SettingsYML struct {
|
||||
Settings struct {
|
||||
TenantsToFilter []string `yaml:"tenants_to_filter"`
|
||||
NodeChargeClusters []string `yaml:"node_charge_clusters"`
|
||||
SrmActiveActiveVms []string `yaml:"srm_activeactive_vms"`
|
||||
VcenterAddresses []string `yaml:"vcenter_addresses"`
|
||||
LogLevel string `yaml:"log_level"`
|
||||
LogOutput string `yaml:"log_output"`
|
||||
DatabaseDriver string `yaml:"database_driver"`
|
||||
DatabaseURL string `yaml:"database_url"`
|
||||
BindIP string `yaml:"bind_ip"`
|
||||
BindPort int `yaml:"bind_port"`
|
||||
BindDisableTLS bool `yaml:"bind_disable_tls"`
|
||||
TLSCertFilename string `yaml:"tls_cert_filename"`
|
||||
TLSKeyFilename string `yaml:"tls_key_filename"`
|
||||
VcenterUsername string `yaml:"vcenter_username"`
|
||||
VcenterPassword string `yaml:"vcenter_password"`
|
||||
VcenterInsecure bool `yaml:"vcenter_insecure"`
|
||||
VcenterEventPollingSeconds int `yaml:"vcenter_event_polling_seconds"`
|
||||
VcenterInventoryPollingSeconds int `yaml:"vcenter_inventory_polling_seconds"`
|
||||
VcenterInventorySnapshotSeconds int `yaml:"vcenter_inventory_snapshot_seconds"`
|
||||
VcenterInventoryAggregateSeconds int `yaml:"vcenter_inventory_aggregate_seconds"`
|
||||
HourlySnapshotConcurrency int `yaml:"hourly_snapshot_concurrency"`
|
||||
HourlySnapshotMaxAgeDays int `yaml:"hourly_snapshot_max_age_days"`
|
||||
DailySnapshotMaxAgeMonths int `yaml:"daily_snapshot_max_age_months"`
|
||||
SnapshotCleanupCron string `yaml:"snapshot_cleanup_cron"`
|
||||
ReportsDir string `yaml:"reports_dir"`
|
||||
HourlyJobTimeoutSeconds int `yaml:"hourly_job_timeout_seconds"`
|
||||
HourlySnapshotTimeoutSeconds int `yaml:"hourly_snapshot_timeout_seconds"`
|
||||
HourlySnapshotRetrySeconds int `yaml:"hourly_snapshot_retry_seconds"`
|
||||
HourlySnapshotMaxRetries int `yaml:"hourly_snapshot_max_retries"`
|
||||
DailyJobTimeoutSeconds int `yaml:"daily_job_timeout_seconds"`
|
||||
MonthlyJobTimeoutSeconds int `yaml:"monthly_job_timeout_seconds"`
|
||||
CleanupJobTimeoutSeconds int `yaml:"cleanup_job_timeout_seconds"`
|
||||
TenantsToFilter []string `yaml:"tenants_to_filter"`
|
||||
NodeChargeClusters []string `yaml:"node_charge_clusters"`
|
||||
SrmActiveActiveVms []string `yaml:"srm_activeactive_vms"`
|
||||
VcenterAddresses []string `yaml:"vcenter_addresses"`
|
||||
PostgresWorkMemMB int `yaml:"postgres_work_mem_mb"`
|
||||
} `yaml:"settings"`
|
||||
}
|
||||
|
||||
@@ -60,8 +90,57 @@ func (s *Settings) ReadYMLSettings() error {
|
||||
return fmt.Errorf("unable to decode settings file : '%s'", err)
|
||||
}
|
||||
|
||||
s.Logger.Debug("Updating settings", "settings", settings)
|
||||
// Avoid logging sensitive fields (e.g., credentials).
|
||||
redacted := settings
|
||||
redacted.Settings.VcenterPassword = "REDACTED"
|
||||
s.Logger.Debug("Updating settings", "settings", redacted)
|
||||
s.Values = &settings
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Settings) WriteYMLSettings() error {
|
||||
if s.Values == nil {
|
||||
return errors.New("settings are not loaded")
|
||||
}
|
||||
if len(s.SettingsPath) == 0 {
|
||||
return errors.New("settings file path not specified")
|
||||
}
|
||||
|
||||
data, err := yaml.Marshal(s.Values)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to encode settings file: %w", err)
|
||||
}
|
||||
|
||||
mode := os.FileMode(0o644)
|
||||
if info, err := os.Stat(s.SettingsPath); err == nil {
|
||||
mode = info.Mode().Perm()
|
||||
}
|
||||
|
||||
dir := filepath.Dir(s.SettingsPath)
|
||||
tmp, err := os.CreateTemp(dir, "vctp-settings-*.yml")
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create temp settings file: %w", err)
|
||||
}
|
||||
tmpName := tmp.Name()
|
||||
defer func() {
|
||||
_ = os.Remove(tmpName)
|
||||
}()
|
||||
|
||||
if _, err := tmp.Write(data); err != nil {
|
||||
_ = tmp.Close()
|
||||
return fmt.Errorf("unable to write temp settings file: %w", err)
|
||||
}
|
||||
if err := tmp.Chmod(mode); err != nil {
|
||||
_ = tmp.Close()
|
||||
return fmt.Errorf("unable to set temp settings permissions: %w", err)
|
||||
}
|
||||
if err := tmp.Close(); err != nil {
|
||||
return fmt.Errorf("unable to close temp settings file: %w", err)
|
||||
}
|
||||
if err := os.Rename(tmpName, s.SettingsPath); err != nil {
|
||||
return fmt.Errorf("unable to replace settings file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
32
internal/tasks/aggregateCommon.go
Normal file
32
internal/tasks/aggregateCommon.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package tasks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
"vctp/db"
|
||||
)
|
||||
|
||||
// runAggregateJob wraps aggregation cron jobs with timeout, migration check, and circuit breaker semantics.
|
||||
func (c *CronTask) runAggregateJob(ctx context.Context, jobName string, timeout time.Duration, fn func(context.Context) error) (err error) {
|
||||
jobCtx := ctx
|
||||
if timeout > 0 {
|
||||
var cancel context.CancelFunc
|
||||
jobCtx, cancel = context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
}
|
||||
tracker := NewCronTracker(c.Database)
|
||||
done, skip, err := tracker.Start(jobCtx, jobName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if skip {
|
||||
return nil
|
||||
}
|
||||
defer func() { done(err) }()
|
||||
|
||||
if err := db.CheckMigrationState(jobCtx, c.Database.DB()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return fn(jobCtx)
|
||||
}
|
||||
160
internal/tasks/cronstatus.go
Normal file
160
internal/tasks/cronstatus.go
Normal file
@@ -0,0 +1,160 @@
|
||||
package tasks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
"vctp/db"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
// CronTracker manages re-entry protection and status recording for cron jobs.
|
||||
type CronTracker struct {
|
||||
db db.Database
|
||||
bindType int
|
||||
}
|
||||
|
||||
func NewCronTracker(database db.Database) *CronTracker {
|
||||
return &CronTracker{
|
||||
db: database,
|
||||
bindType: sqlx.BindType(database.DB().DriverName()),
|
||||
}
|
||||
}
|
||||
|
||||
// ClearAllInProgress resets any stuck in-progress flags (e.g., after crashes).
|
||||
func (c *CronTracker) ClearAllInProgress(ctx context.Context) error {
|
||||
if err := c.ensureTable(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := c.db.DB().ExecContext(ctx, `UPDATE cron_status SET in_progress = FALSE`)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *CronTracker) ensureTable(ctx context.Context) error {
|
||||
conn := c.db.DB()
|
||||
driver := conn.DriverName()
|
||||
var ddl string
|
||||
switch driver {
|
||||
case "pgx", "postgres":
|
||||
ddl = `
|
||||
CREATE TABLE IF NOT EXISTS cron_status (
|
||||
job_name TEXT PRIMARY KEY,
|
||||
started_at BIGINT NOT NULL,
|
||||
ended_at BIGINT NOT NULL,
|
||||
duration_ms BIGINT NOT NULL,
|
||||
last_error TEXT,
|
||||
in_progress BOOLEAN NOT NULL DEFAULT FALSE
|
||||
);`
|
||||
default:
|
||||
ddl = `
|
||||
CREATE TABLE IF NOT EXISTS cron_status (
|
||||
job_name TEXT PRIMARY KEY,
|
||||
started_at BIGINT NOT NULL,
|
||||
ended_at BIGINT NOT NULL,
|
||||
duration_ms BIGINT NOT NULL,
|
||||
last_error TEXT,
|
||||
in_progress BOOLEAN NOT NULL DEFAULT FALSE
|
||||
);`
|
||||
}
|
||||
_, err := conn.ExecContext(ctx, ddl)
|
||||
return err
|
||||
}
|
||||
|
||||
// Start marks a job as in-progress; returns a completion callback and whether to skip because it's already running.
|
||||
func (c *CronTracker) Start(ctx context.Context, job string) (func(error), bool, error) {
|
||||
if err := c.ensureTable(ctx); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
conn := c.db.DB()
|
||||
now := time.Now().Unix()
|
||||
|
||||
tx, err := conn.BeginTxx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
var inProgress bool
|
||||
query := sqlx.Rebind(c.bindType, `SELECT in_progress FROM cron_status WHERE job_name = ?`)
|
||||
err = tx.QueryRowContext(ctx, query, job).Scan(&inProgress)
|
||||
if err != nil {
|
||||
// no row, insert
|
||||
if err := upsertCron(tx, c.bindType, job, now, false); err != nil {
|
||||
tx.Rollback()
|
||||
return nil, false, err
|
||||
}
|
||||
} else {
|
||||
if inProgress {
|
||||
tx.Rollback()
|
||||
return nil, true, nil
|
||||
}
|
||||
if err := markCronStart(tx, c.bindType, job, now); err != nil {
|
||||
tx.Rollback()
|
||||
return nil, false, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
done := func(runErr error) {
|
||||
_ = c.finish(context.Background(), job, now, runErr)
|
||||
}
|
||||
return done, false, nil
|
||||
}
|
||||
|
||||
func (c *CronTracker) finish(ctx context.Context, job string, startedAt int64, runErr error) error {
|
||||
conn := c.db.DB()
|
||||
duration := time.Since(time.Unix(startedAt, 0)).Milliseconds()
|
||||
tx, err := conn.BeginTxx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
lastErr := ""
|
||||
if runErr != nil {
|
||||
lastErr = runErr.Error()
|
||||
}
|
||||
err = upsertCronFinish(tx, c.bindType, job, duration, lastErr)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func upsertCron(tx *sqlx.Tx, bindType int, job string, startedAt int64, inProgress bool) error {
|
||||
query := `
|
||||
INSERT INTO cron_status (job_name, started_at, ended_at, duration_ms, last_error, in_progress)
|
||||
VALUES (?, ?, 0, 0, NULL, ?)
|
||||
ON CONFLICT (job_name) DO UPDATE SET started_at = excluded.started_at, in_progress = excluded.in_progress, ended_at = excluded.ended_at, duration_ms = excluded.duration_ms, last_error = excluded.last_error
|
||||
`
|
||||
_, err := tx.Exec(sqlx.Rebind(bindType, query), job, startedAt, inProgress)
|
||||
return err
|
||||
}
|
||||
|
||||
func markCronStart(tx *sqlx.Tx, bindType int, job string, startedAt int64) error {
|
||||
query := `
|
||||
UPDATE cron_status
|
||||
SET started_at = ?, in_progress = TRUE, ended_at = 0, duration_ms = 0, last_error = NULL
|
||||
WHERE job_name = ?
|
||||
`
|
||||
_, err := tx.Exec(sqlx.Rebind(bindType, query), startedAt, job)
|
||||
return err
|
||||
}
|
||||
|
||||
func upsertCronFinish(tx *sqlx.Tx, bindType int, job string, durationMS int64, lastErr string) error {
|
||||
query := `
|
||||
UPDATE cron_status
|
||||
SET ended_at = ?, duration_ms = ?, last_error = ?, in_progress = FALSE
|
||||
WHERE job_name = ?
|
||||
`
|
||||
_, err := tx.Exec(sqlx.Rebind(bindType, query), time.Now().Unix(), durationMS, nullableString(lastErr), job)
|
||||
return err
|
||||
}
|
||||
|
||||
func nullableString(s string) interface{} {
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
return s
|
||||
}
|
||||
166
internal/tasks/dailyAggregate.go
Normal file
166
internal/tasks/dailyAggregate.go
Normal file
@@ -0,0 +1,166 @@
|
||||
package tasks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
"vctp/db"
|
||||
"vctp/internal/metrics"
|
||||
"vctp/internal/report"
|
||||
)
|
||||
|
||||
// RunVcenterDailyAggregate summarizes hourly snapshots into a daily summary table.
|
||||
func (c *CronTask) RunVcenterDailyAggregate(ctx context.Context, logger *slog.Logger) (err error) {
|
||||
jobTimeout := durationFromSeconds(c.Settings.Values.Settings.DailyJobTimeoutSeconds, 15*time.Minute)
|
||||
return c.runAggregateJob(ctx, "daily_aggregate", jobTimeout, func(jobCtx context.Context) error {
|
||||
startedAt := time.Now()
|
||||
defer func() {
|
||||
logger.Info("Daily summary job finished", "duration", time.Since(startedAt))
|
||||
}()
|
||||
targetTime := time.Now().Add(-time.Minute)
|
||||
// Always force regeneration on the scheduled run to refresh data even if a manual run happened earlier.
|
||||
return c.aggregateDailySummary(jobCtx, targetTime, true)
|
||||
})
|
||||
}
|
||||
|
||||
func (c *CronTask) AggregateDailySummary(ctx context.Context, date time.Time, force bool) error {
|
||||
return c.aggregateDailySummary(ctx, date, force)
|
||||
}
|
||||
|
||||
func (c *CronTask) aggregateDailySummary(ctx context.Context, targetTime time.Time, force bool) error {
|
||||
jobStart := time.Now()
|
||||
dayStart := time.Date(targetTime.Year(), targetTime.Month(), targetTime.Day(), 0, 0, 0, 0, targetTime.Location())
|
||||
dayEnd := dayStart.AddDate(0, 0, 1)
|
||||
summaryTable, err := dailySummaryTableName(targetTime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dbConn := c.Database.DB()
|
||||
db.SetPostgresWorkMem(ctx, dbConn, c.Settings.Values.Settings.PostgresWorkMemMB)
|
||||
if err := db.EnsureSummaryTable(ctx, dbConn, summaryTable); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := report.EnsureSnapshotRegistry(ctx, c.Database); err != nil {
|
||||
return err
|
||||
}
|
||||
if rowsExist, err := db.TableHasRows(ctx, dbConn, summaryTable); err != nil {
|
||||
return err
|
||||
} else if rowsExist && !force {
|
||||
c.Logger.Debug("Daily summary already exists, skipping aggregation", "summary_table", summaryTable)
|
||||
return nil
|
||||
} else if rowsExist && force {
|
||||
if err := clearTable(ctx, dbConn, summaryTable); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
hourlySnapshots, err := report.SnapshotRecordsWithFallback(ctx, c.Database, "hourly", "inventory_hourly_", "epoch", dayStart, dayEnd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hourlySnapshots = filterRecordsInRange(hourlySnapshots, dayStart, dayEnd)
|
||||
hourlySnapshots = filterSnapshotsWithRows(ctx, dbConn, hourlySnapshots)
|
||||
if len(hourlySnapshots) == 0 {
|
||||
return fmt.Errorf("no hourly snapshot tables found for %s", dayStart.Format("2006-01-02"))
|
||||
}
|
||||
|
||||
hourlyTables := make([]string, 0, len(hourlySnapshots))
|
||||
for _, snapshot := range hourlySnapshots {
|
||||
hourlyTables = append(hourlyTables, snapshot.TableName)
|
||||
// Ensure indexes exist on historical hourly tables for faster aggregation.
|
||||
if err := db.EnsureSnapshotIndexes(ctx, dbConn, snapshot.TableName); err != nil {
|
||||
c.Logger.Warn("failed to ensure indexes on hourly table", "table", snapshot.TableName, "error", err)
|
||||
}
|
||||
}
|
||||
unionQuery, err := buildUnionQuery(hourlyTables, summaryUnionColumns, templateExclusionFilter())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
currentTotals, err := db.SnapshotTotalsForUnion(ctx, dbConn, unionQuery)
|
||||
if err != nil {
|
||||
c.Logger.Warn("unable to calculate daily totals", "error", err, "date", dayStart.Format("2006-01-02"))
|
||||
} else {
|
||||
c.Logger.Info("Daily snapshot totals",
|
||||
"date", dayStart.Format("2006-01-02"),
|
||||
"vm_count", currentTotals.VmCount,
|
||||
"vcpu_total", currentTotals.VcpuTotal,
|
||||
"ram_total_gb", currentTotals.RamTotal,
|
||||
"disk_total_gb", currentTotals.DiskTotal,
|
||||
)
|
||||
}
|
||||
|
||||
prevStart := dayStart.AddDate(0, 0, -1)
|
||||
prevEnd := dayStart
|
||||
prevSnapshots, err := report.SnapshotRecordsWithFallback(ctx, c.Database, "hourly", "inventory_hourly_", "epoch", prevStart, prevEnd)
|
||||
if err == nil && len(prevSnapshots) > 0 {
|
||||
prevSnapshots = filterRecordsInRange(prevSnapshots, prevStart, prevEnd)
|
||||
prevSnapshots = filterSnapshotsWithRows(ctx, dbConn, prevSnapshots)
|
||||
prevTables := make([]string, 0, len(prevSnapshots))
|
||||
for _, snapshot := range prevSnapshots {
|
||||
prevTables = append(prevTables, snapshot.TableName)
|
||||
}
|
||||
prevUnion, err := buildUnionQuery(prevTables, summaryUnionColumns, templateExclusionFilter())
|
||||
if err == nil {
|
||||
prevTotals, err := db.SnapshotTotalsForUnion(ctx, dbConn, prevUnion)
|
||||
if err != nil {
|
||||
c.Logger.Warn("unable to calculate previous day totals", "error", err, "date", prevStart.Format("2006-01-02"))
|
||||
} else {
|
||||
c.Logger.Info("Daily snapshot comparison",
|
||||
"current_date", dayStart.Format("2006-01-02"),
|
||||
"previous_date", prevStart.Format("2006-01-02"),
|
||||
"vm_delta", currentTotals.VmCount-prevTotals.VmCount,
|
||||
"vcpu_delta", currentTotals.VcpuTotal-prevTotals.VcpuTotal,
|
||||
"ram_delta_gb", currentTotals.RamTotal-prevTotals.RamTotal,
|
||||
"disk_delta_gb", currentTotals.DiskTotal-prevTotals.DiskTotal,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
c.Logger.Warn("unable to build previous day union", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
insertQuery, err := db.BuildDailySummaryInsert(summaryTable, unionQuery)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := dbConn.ExecContext(ctx, insertQuery); err != nil {
|
||||
c.Logger.Error("failed to aggregate daily inventory", "error", err, "date", dayStart.Format("2006-01-02"))
|
||||
return err
|
||||
}
|
||||
// Backfill missing creation times to the start of the day for rows where vCenter had no creation info.
|
||||
if _, err := dbConn.ExecContext(ctx,
|
||||
`UPDATE `+summaryTable+` SET "CreationTime" = $1 WHERE "CreationTime" IS NULL OR "CreationTime" = 0`,
|
||||
dayStart.Unix(),
|
||||
); err != nil {
|
||||
c.Logger.Warn("failed to normalize creation times for daily summary", "error", err, "table", summaryTable)
|
||||
}
|
||||
if err := db.RefineCreationDeletionFromUnion(ctx, dbConn, summaryTable, unionQuery); err != nil {
|
||||
c.Logger.Warn("failed to refine creation/deletion times", "error", err, "table", summaryTable)
|
||||
}
|
||||
db.AnalyzeTableIfPostgres(ctx, dbConn, summaryTable)
|
||||
rowCount, err := db.TableRowCount(ctx, dbConn, summaryTable)
|
||||
if err != nil {
|
||||
c.Logger.Warn("unable to count daily summary rows", "error", err, "table", summaryTable)
|
||||
}
|
||||
if err := report.RegisterSnapshot(ctx, c.Database, "daily", summaryTable, dayStart, rowCount); err != nil {
|
||||
c.Logger.Warn("failed to register daily snapshot", "error", err, "table", summaryTable)
|
||||
}
|
||||
|
||||
if err := c.generateReport(ctx, summaryTable); err != nil {
|
||||
c.Logger.Warn("failed to generate daily report", "error", err, "table", summaryTable)
|
||||
metrics.RecordDailyAggregation(time.Since(jobStart), err)
|
||||
return err
|
||||
}
|
||||
|
||||
c.Logger.Debug("Finished daily inventory aggregation", "summary_table", summaryTable)
|
||||
metrics.RecordDailyAggregation(time.Since(jobStart), nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
func dailySummaryTableName(t time.Time) (string, error) {
|
||||
return db.SafeTableName(fmt.Sprintf("inventory_daily_summary_%s", t.Format("20060102")))
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -20,6 +20,10 @@ import (
|
||||
|
||||
// use gocron to check vcenters for VMs or updates we don't know about
|
||||
func (c *CronTask) RunVcenterPoll(ctx context.Context, logger *slog.Logger) error {
|
||||
startedAt := time.Now()
|
||||
defer func() {
|
||||
logger.Info("Vcenter poll job finished", "duration", time.Since(startedAt))
|
||||
}()
|
||||
var matchFound bool
|
||||
|
||||
// reload settings in case vcenter list has changed
|
||||
@@ -262,6 +266,11 @@ func (c *CronTask) AddVmToInventory(vmObject *mo.VirtualMachine, vc *vcenter.Vce
|
||||
return errors.New("can't process empty vm object")
|
||||
}
|
||||
|
||||
if strings.HasPrefix(vmObject.Name, "vCLS-") {
|
||||
c.Logger.Debug("Skipping internal vCLS VM", "vm_name", vmObject.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
c.Logger.Debug("found VM")
|
||||
|
||||
/*
|
||||
|
||||
133
internal/tasks/monthlyAggregate.go
Normal file
133
internal/tasks/monthlyAggregate.go
Normal file
@@ -0,0 +1,133 @@
|
||||
package tasks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
"vctp/db"
|
||||
"vctp/internal/metrics"
|
||||
"vctp/internal/report"
|
||||
)
|
||||
|
||||
// RunVcenterMonthlyAggregate summarizes the previous month's daily snapshots.
|
||||
func (c *CronTask) RunVcenterMonthlyAggregate(ctx context.Context, logger *slog.Logger) (err error) {
|
||||
jobTimeout := durationFromSeconds(c.Settings.Values.Settings.MonthlyJobTimeoutSeconds, 20*time.Minute)
|
||||
return c.runAggregateJob(ctx, "monthly_aggregate", jobTimeout, func(jobCtx context.Context) error {
|
||||
startedAt := time.Now()
|
||||
defer func() {
|
||||
logger.Info("Monthly summary job finished", "duration", time.Since(startedAt))
|
||||
}()
|
||||
now := time.Now()
|
||||
firstOfThisMonth := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
|
||||
targetMonth := firstOfThisMonth.AddDate(0, -1, 0)
|
||||
return c.aggregateMonthlySummary(jobCtx, targetMonth, false)
|
||||
})
|
||||
}
|
||||
|
||||
func (c *CronTask) AggregateMonthlySummary(ctx context.Context, month time.Time, force bool) error {
|
||||
return c.aggregateMonthlySummary(ctx, month, force)
|
||||
}
|
||||
|
||||
func (c *CronTask) aggregateMonthlySummary(ctx context.Context, targetMonth time.Time, force bool) error {
|
||||
jobStart := time.Now()
|
||||
if err := report.EnsureSnapshotRegistry(ctx, c.Database); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
monthStart := time.Date(targetMonth.Year(), targetMonth.Month(), 1, 0, 0, 0, 0, targetMonth.Location())
|
||||
monthEnd := monthStart.AddDate(0, 1, 0)
|
||||
dailySnapshots, err := report.SnapshotRecordsWithFallback(ctx, c.Database, "daily", "inventory_daily_summary_", "20060102", monthStart, monthEnd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dailySnapshots = filterRecordsInRange(dailySnapshots, monthStart, monthEnd)
|
||||
|
||||
dbConn := c.Database.DB()
|
||||
db.SetPostgresWorkMem(ctx, dbConn, c.Settings.Values.Settings.PostgresWorkMemMB)
|
||||
dailySnapshots = filterSnapshotsWithRows(ctx, dbConn, dailySnapshots)
|
||||
if len(dailySnapshots) == 0 {
|
||||
return fmt.Errorf("no hourly snapshot tables found for %s", targetMonth.Format("2006-01"))
|
||||
}
|
||||
|
||||
monthlyTable, err := monthlySummaryTableName(targetMonth)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := db.EnsureSummaryTable(ctx, dbConn, monthlyTable); err != nil {
|
||||
return err
|
||||
}
|
||||
if rowsExist, err := db.TableHasRows(ctx, dbConn, monthlyTable); err != nil {
|
||||
return err
|
||||
} else if rowsExist && !force {
|
||||
c.Logger.Debug("Monthly summary already exists, skipping aggregation", "summary_table", monthlyTable)
|
||||
return nil
|
||||
} else if rowsExist && force {
|
||||
if err := clearTable(ctx, dbConn, monthlyTable); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
dailyTables := make([]string, 0, len(dailySnapshots))
|
||||
for _, snapshot := range dailySnapshots {
|
||||
dailyTables = append(dailyTables, snapshot.TableName)
|
||||
}
|
||||
unionQuery, err := buildUnionQuery(dailyTables, summaryUnionColumns, templateExclusionFilter())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
monthlyTotals, err := db.SnapshotTotalsForUnion(ctx, dbConn, unionQuery)
|
||||
if err != nil {
|
||||
c.Logger.Warn("unable to calculate monthly totals", "error", err, "month", targetMonth.Format("2006-01"))
|
||||
} else {
|
||||
c.Logger.Info("Monthly snapshot totals",
|
||||
"month", targetMonth.Format("2006-01"),
|
||||
"vm_count", monthlyTotals.VmCount,
|
||||
"vcpu_total", monthlyTotals.VcpuTotal,
|
||||
"ram_total_gb", monthlyTotals.RamTotal,
|
||||
"disk_total_gb", monthlyTotals.DiskTotal,
|
||||
)
|
||||
}
|
||||
|
||||
insertQuery, err := db.BuildMonthlySummaryInsert(monthlyTable, unionQuery)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
// Backfill missing creation times to the start of the month for rows lacking creation info.
|
||||
if _, err := dbConn.ExecContext(ctx,
|
||||
`UPDATE `+monthlyTable+` SET "CreationTime" = $1 WHERE "CreationTime" IS NULL OR "CreationTime" = 0`,
|
||||
monthStart.Unix(),
|
||||
); err != nil {
|
||||
c.Logger.Warn("failed to normalize creation times for monthly summary", "error", err, "table", monthlyTable)
|
||||
}
|
||||
rowCount, err := db.TableRowCount(ctx, dbConn, monthlyTable)
|
||||
if err != nil {
|
||||
c.Logger.Warn("unable to count monthly summary rows", "error", err, "table", monthlyTable)
|
||||
}
|
||||
if err := report.RegisterSnapshot(ctx, c.Database, "monthly", monthlyTable, targetMonth, rowCount); err != nil {
|
||||
c.Logger.Warn("failed to register monthly snapshot", "error", err, "table", monthlyTable)
|
||||
}
|
||||
|
||||
db.AnalyzeTableIfPostgres(ctx, dbConn, monthlyTable)
|
||||
|
||||
if err := c.generateReport(ctx, monthlyTable); err != nil {
|
||||
c.Logger.Warn("failed to generate monthly report", "error", err, "table", monthlyTable)
|
||||
metrics.RecordMonthlyAggregation(time.Since(jobStart), err)
|
||||
return err
|
||||
}
|
||||
|
||||
c.Logger.Debug("Finished monthly inventory aggregation", "summary_table", monthlyTable)
|
||||
metrics.RecordMonthlyAggregation(time.Since(jobStart), nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
func monthlySummaryTableName(t time.Time) (string, error) {
|
||||
return db.SafeTableName(fmt.Sprintf("inventory_monthly_summary_%s", t.Format("200601")))
|
||||
}
|
||||
@@ -14,6 +14,10 @@ import (
|
||||
|
||||
// use gocron to check events in the Events table
|
||||
func (c *CronTask) RunVmCheck(ctx context.Context, logger *slog.Logger) error {
|
||||
startedAt := time.Now()
|
||||
defer func() {
|
||||
logger.Info("Event processing job finished", "duration", time.Since(startedAt))
|
||||
}()
|
||||
var (
|
||||
numVcpus int32
|
||||
numRam int32
|
||||
@@ -83,6 +87,14 @@ func (c *CronTask) RunVmCheck(ctx context.Context, logger *slog.Logger) error {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(vmObject.Name, "vCLS-") {
|
||||
c.Logger.Info("Skipping internal vCLS VM event", "vm_name", vmObject.Name)
|
||||
if err := c.Database.Queries().UpdateEventsProcessed(ctx, evt.Eid); err != nil {
|
||||
c.Logger.Error("Unable to mark vCLS event as processed", "event_id", evt.Eid, "error", err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
//c.Logger.Debug("found VM")
|
||||
srmPlaceholder = "FALSE" // Default assumption
|
||||
//prettyPrint(vmObject)
|
||||
|
||||
@@ -9,8 +9,9 @@ import (
|
||||
|
||||
// CronTask stores runtime information to be used by tasks
|
||||
type CronTask struct {
|
||||
Logger *slog.Logger
|
||||
Database db.Database
|
||||
Settings *settings.Settings
|
||||
VcCreds *vcenter.VcenterLogin
|
||||
Logger *slog.Logger
|
||||
Database db.Database
|
||||
Settings *settings.Settings
|
||||
VcCreds *vcenter.VcenterLogin
|
||||
FirstHourlySnapshotCheck bool
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -20,6 +21,10 @@ func GetFilePath(path string) string {
|
||||
|
||||
// check if filename exists
|
||||
if _, err := os.Stat(path); os.IsNotExist((err)) {
|
||||
if filepath.IsAbs(path) {
|
||||
slog.Info("File not found, using absolute path", "filename", path)
|
||||
return path
|
||||
}
|
||||
slog.Info("File not found, searching in same directory as binary", "filename", path)
|
||||
// if not, check that it exists in the same directory as the currently executing binary
|
||||
ex, err2 := os.Executable()
|
||||
@@ -66,3 +71,29 @@ func SleepWithContext(ctx context.Context, d time.Duration) {
|
||||
case <-timer.C:
|
||||
}
|
||||
}
|
||||
|
||||
// EnvInt parses an environment variable into an int; returns (value, true) when set and valid.
|
||||
func EnvInt(key string) (int, bool) {
|
||||
val := os.Getenv(key)
|
||||
if val == "" {
|
||||
return 0, false
|
||||
}
|
||||
parsed, err := strconv.Atoi(val)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
return parsed, true
|
||||
}
|
||||
|
||||
// DurationFromEnv parses an environment variable representing seconds into a duration, defaulting when unset/invalid.
|
||||
func DurationFromEnv(key string, fallback time.Duration) time.Duration {
|
||||
val := os.Getenv(key)
|
||||
if val == "" {
|
||||
return fallback
|
||||
}
|
||||
seconds, err := strconv.ParseInt(val, 10, 64)
|
||||
if err != nil || seconds <= 0 {
|
||||
return fallback
|
||||
}
|
||||
return time.Duration(seconds) * time.Second
|
||||
}
|
||||
|
||||
@@ -3,10 +3,8 @@ package vcenter
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"log/slog"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
@@ -30,6 +28,7 @@ type Vcenter struct {
|
||||
type VcenterLogin struct {
|
||||
Username string
|
||||
Password string
|
||||
Insecure bool
|
||||
}
|
||||
|
||||
type VmProperties struct {
|
||||
@@ -37,6 +36,13 @@ type VmProperties struct {
|
||||
ResourcePool string
|
||||
}
|
||||
|
||||
type HostLookup struct {
|
||||
Cluster string
|
||||
Datacenter string
|
||||
}
|
||||
|
||||
type FolderLookup map[string]string
|
||||
|
||||
// New creates a new Vcenter with the given logger
|
||||
func New(logger *slog.Logger, creds *VcenterLogin) *Vcenter {
|
||||
|
||||
@@ -51,17 +57,19 @@ func New(logger *slog.Logger, creds *VcenterLogin) *Vcenter {
|
||||
}
|
||||
|
||||
func (v *Vcenter) Login(vUrl string) error {
|
||||
var insecure bool
|
||||
|
||||
// TODO - fix this
|
||||
insecureString := os.Getenv("VCENTER_INSECURE")
|
||||
//username := os.Getenv("VCENTER_USERNAME")
|
||||
//password := os.Getenv("VCENTER_PASSWORD")
|
||||
|
||||
if v == nil {
|
||||
return fmt.Errorf("vcenter is nil")
|
||||
}
|
||||
if strings.TrimSpace(vUrl) == "" {
|
||||
return fmt.Errorf("vcenter URL is empty")
|
||||
}
|
||||
if v.credentials == nil {
|
||||
return fmt.Errorf("vcenter credentials are nil")
|
||||
}
|
||||
// Connect to vCenter
|
||||
u, err := soap.ParseURL(vUrl)
|
||||
if err != nil {
|
||||
log.Fatalf("Error parsing vCenter URL: %s", err)
|
||||
return fmt.Errorf("error parsing vCenter URL: %w", err)
|
||||
}
|
||||
v.Vurl = vUrl
|
||||
|
||||
@@ -74,11 +82,7 @@ func (v *Vcenter) Login(vUrl string) error {
|
||||
}
|
||||
*/
|
||||
|
||||
if insecureString == "true" {
|
||||
insecure = true
|
||||
}
|
||||
|
||||
c, err := govmomi.NewClient(v.ctx, u, insecure)
|
||||
c, err := govmomi.NewClient(v.ctx, u, v.credentials.Insecure)
|
||||
if err != nil {
|
||||
v.Logger.Error("Unable to connect to vCenter", "error", err)
|
||||
return fmt.Errorf("unable to connect to vCenter : %s", err)
|
||||
@@ -154,6 +158,146 @@ func (v *Vcenter) GetAllVmReferences() ([]*object.VirtualMachine, error) {
|
||||
return results, err
|
||||
}
|
||||
|
||||
// GetAllVMsWithProps returns all VMs with the properties needed for snapshotting in a single property-collector call.
|
||||
func (v *Vcenter) GetAllVMsWithProps() ([]mo.VirtualMachine, error) {
|
||||
m := view.NewManager(v.client.Client)
|
||||
cv, err := m.CreateContainerView(v.ctx, v.client.ServiceContent.RootFolder, []string{"VirtualMachine"}, true)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create VM container view: %w", err)
|
||||
}
|
||||
defer cv.Destroy(v.ctx)
|
||||
|
||||
var vms []mo.VirtualMachine
|
||||
props := []string{
|
||||
"name",
|
||||
"parent",
|
||||
"config.uuid",
|
||||
"config.createDate",
|
||||
"config.hardware",
|
||||
"config.managedBy",
|
||||
"config.template",
|
||||
"runtime.powerState",
|
||||
"runtime.host",
|
||||
"resourcePool",
|
||||
}
|
||||
if err := cv.Retrieve(v.ctx, []string{"VirtualMachine"}, props, &vms); err != nil {
|
||||
return nil, fmt.Errorf("failed to retrieve VMs: %w", err)
|
||||
}
|
||||
return vms, nil
|
||||
}
|
||||
|
||||
func (v *Vcenter) BuildHostLookup() (map[string]HostLookup, error) {
|
||||
finder := find.NewFinder(v.client.Client, true)
|
||||
datacenters, err := finder.DatacenterList(v.ctx, "*")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list datacenters: %w", err)
|
||||
}
|
||||
|
||||
lookup := make(map[string]HostLookup)
|
||||
clusterCache := make(map[string]string)
|
||||
|
||||
for _, dc := range datacenters {
|
||||
finder.SetDatacenter(dc)
|
||||
hosts, err := finder.HostSystemList(v.ctx, "*")
|
||||
if err != nil {
|
||||
v.Logger.Warn("failed to list hosts for datacenter", "datacenter", dc.Name(), "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, host := range hosts {
|
||||
ref := host.Reference()
|
||||
var moHost mo.HostSystem
|
||||
if err := v.client.RetrieveOne(v.ctx, ref, []string{"parent"}, &moHost); err != nil {
|
||||
v.Logger.Warn("failed to retrieve host info", "host", host.Name(), "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
clusterName := ""
|
||||
if moHost.Parent != nil {
|
||||
if cached, ok := clusterCache[moHost.Parent.Value]; ok {
|
||||
clusterName = cached
|
||||
} else {
|
||||
var moCompute mo.ComputeResource
|
||||
if err := v.client.RetrieveOne(v.ctx, *moHost.Parent, []string{"name"}, &moCompute); err == nil {
|
||||
clusterName = moCompute.Name
|
||||
clusterCache[moHost.Parent.Value] = clusterName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lookup[ref.Value] = HostLookup{
|
||||
Cluster: clusterName,
|
||||
Datacenter: dc.Name(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return lookup, nil
|
||||
}
|
||||
|
||||
func (v *Vcenter) BuildFolderPathLookup() (FolderLookup, error) {
|
||||
m := view.NewManager(v.client.Client)
|
||||
folders, err := m.CreateContainerView(v.ctx, v.client.ServiceContent.RootFolder, []string{"Folder"}, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer folders.Destroy(v.ctx)
|
||||
|
||||
var results []mo.Folder
|
||||
if err := folders.Retrieve(v.ctx, []string{"Folder"}, []string{"name", "parent"}, &results); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nameByID := make(map[string]string, len(results))
|
||||
parentByID := make(map[string]*types.ManagedObjectReference, len(results))
|
||||
for _, folder := range results {
|
||||
nameByID[folder.Reference().Value] = folder.Name
|
||||
parentByID[folder.Reference().Value] = folder.Parent
|
||||
}
|
||||
|
||||
paths := make(FolderLookup, len(results))
|
||||
var buildPath func(id string) string
|
||||
buildPath = func(id string) string {
|
||||
if pathValue, ok := paths[id]; ok {
|
||||
return pathValue
|
||||
}
|
||||
name, ok := nameByID[id]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
parent := parentByID[id]
|
||||
if parent == nil || parent.Type == "Datacenter" {
|
||||
paths[id] = path.Join("/", name)
|
||||
return paths[id]
|
||||
}
|
||||
if parent.Type != "Folder" {
|
||||
paths[id] = path.Join("/", name)
|
||||
return paths[id]
|
||||
}
|
||||
parentPath := buildPath(parent.Value)
|
||||
if parentPath == "" {
|
||||
paths[id] = path.Join("/", name)
|
||||
return paths[id]
|
||||
}
|
||||
paths[id] = path.Join(parentPath, name)
|
||||
return paths[id]
|
||||
}
|
||||
|
||||
for id := range nameByID {
|
||||
_ = buildPath(id)
|
||||
}
|
||||
|
||||
return paths, nil
|
||||
}
|
||||
|
||||
func (v *Vcenter) GetVMFolderPathFromLookup(vm mo.VirtualMachine, lookup FolderLookup) (string, bool) {
|
||||
if vm.Parent == nil || lookup == nil {
|
||||
return "", false
|
||||
}
|
||||
pathValue, ok := lookup[vm.Parent.Value]
|
||||
return pathValue, ok
|
||||
}
|
||||
|
||||
func (v *Vcenter) ConvertObjToMoVM(vmObj *object.VirtualMachine) (*mo.VirtualMachine, error) {
|
||||
// Use the InventoryPath to extract the datacenter name and VM path
|
||||
inventoryPath := vmObj.InventoryPath
|
||||
@@ -268,10 +412,14 @@ func (v *Vcenter) GetClusterFromHost(hostRef *types.ManagedObjectReference) (str
|
||||
v.Logger.Error("cant get host", "error", err)
|
||||
return "", err
|
||||
}
|
||||
if host == nil {
|
||||
v.Logger.Warn("host lookup returned nil", "host_ref", hostRef)
|
||||
return "", nil
|
||||
}
|
||||
|
||||
v.Logger.Debug("host parent", "parent", host.Parent)
|
||||
|
||||
if host.Parent.Type == "ClusterComputeResource" {
|
||||
if host.Parent != nil && host.Parent.Type == "ClusterComputeResource" {
|
||||
// Retrieve properties of the compute resource
|
||||
var moCompute mo.ComputeResource
|
||||
err = v.client.RetrieveOne(v.ctx, *host.Parent, nil, &moCompute)
|
||||
@@ -478,6 +626,27 @@ func (v *Vcenter) GetVmResourcePool(vm mo.VirtualMachine) (string, error) {
|
||||
return resourcePool, nil
|
||||
}
|
||||
|
||||
// BuildResourcePoolLookup creates a cache of resource pool MoRef -> name for fast lookups.
|
||||
func (v *Vcenter) BuildResourcePoolLookup() (map[string]string, error) {
|
||||
m := view.NewManager(v.client.Client)
|
||||
cv, err := m.CreateContainerView(v.ctx, v.client.ServiceContent.RootFolder, []string{"ResourcePool"}, true)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create resource pool view: %w", err)
|
||||
}
|
||||
defer cv.Destroy(v.ctx)
|
||||
|
||||
var pools []mo.ResourcePool
|
||||
if err := cv.Retrieve(v.ctx, []string{"ResourcePool"}, []string{"name"}, &pools); err != nil {
|
||||
return nil, fmt.Errorf("failed to retrieve resource pools: %w", err)
|
||||
}
|
||||
|
||||
lookup := make(map[string]string, len(pools))
|
||||
for _, pool := range pools {
|
||||
lookup[pool.Reference().Value] = pool.Name
|
||||
}
|
||||
return lookup, nil
|
||||
}
|
||||
|
||||
// Helper function to retrieve the full folder path for the VM
|
||||
func (v *Vcenter) GetVMFolderPath(vm mo.VirtualMachine) (string, error) {
|
||||
//finder := find.NewFinder(v.client.Client, true)
|
||||
@@ -494,7 +663,8 @@ func (v *Vcenter) GetVMFolderPath(vm mo.VirtualMachine) (string, error) {
|
||||
folderPath := ""
|
||||
//v.Logger.Debug("parent is", "parent", parentRef)
|
||||
|
||||
for parentRef.Type != "Datacenter" {
|
||||
maxHops := 128
|
||||
for parentRef != nil && parentRef.Type != "Datacenter" && maxHops > 0 {
|
||||
// Retrieve the parent object
|
||||
//parentObj, err := finder.ObjectReference(v.ctx, *parentRef)
|
||||
//if err != nil {
|
||||
@@ -522,6 +692,11 @@ func (v *Vcenter) GetVMFolderPath(vm mo.VirtualMachine) (string, error) {
|
||||
return "", fmt.Errorf("unexpected parent type: %s", parentObj.Reference().Type)
|
||||
}
|
||||
//break
|
||||
maxHops--
|
||||
}
|
||||
|
||||
if parentRef == nil || maxHops == 0 {
|
||||
return "", fmt.Errorf("folder traversal terminated early for VM %s", vm.Name)
|
||||
}
|
||||
|
||||
return folderPath, nil
|
||||
|
||||
10
log/log.go
10
log/log.go
@@ -65,10 +65,9 @@ func ToLevel(level string) Level {
|
||||
}
|
||||
}
|
||||
|
||||
// GetLevel returns the log level from the environment variable.
|
||||
// GetLevel returns the default log level.
|
||||
func GetLevel() Level {
|
||||
level := os.Getenv("LOG_LEVEL")
|
||||
return ToLevel(level)
|
||||
return LevelInfo
|
||||
}
|
||||
|
||||
// Output represents the log output.
|
||||
@@ -93,8 +92,7 @@ func ToOutput(output string) Output {
|
||||
}
|
||||
}
|
||||
|
||||
// GetOutput returns the log output from the environment variable.
|
||||
// GetOutput returns the default log output.
|
||||
func GetOutput() Output {
|
||||
output := os.Getenv("LOG_OUTPUT")
|
||||
return ToOutput(output)
|
||||
return OutputText
|
||||
}
|
||||
|
||||
259
main.go
259
main.go
@@ -2,8 +2,8 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
@@ -18,8 +18,9 @@ import (
|
||||
"vctp/server"
|
||||
"vctp/server/router"
|
||||
|
||||
"crypto/sha256"
|
||||
"github.com/go-co-op/gocron/v2"
|
||||
"github.com/joho/godotenv"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -30,26 +31,34 @@ var (
|
||||
cronInvFrequency time.Duration
|
||||
cronSnapshotFrequency time.Duration
|
||||
cronAggregateFrequency time.Duration
|
||||
encryptionKey = []byte("5L1l3B5KvwOCzUHMAlCgsgUTRAYMfSpa")
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Load data from environment file
|
||||
envFilename := utils.GetFilePath(".env")
|
||||
err := godotenv.Load(envFilename)
|
||||
if err != nil {
|
||||
panic("Error loading .env file")
|
||||
}
|
||||
const fallbackEncryptionKey = "5L1l3B5KvwOCzUHMAlCgsgUTRAYMfSpa"
|
||||
|
||||
logger := log.New(
|
||||
log.GetLevel(),
|
||||
log.GetOutput(),
|
||||
)
|
||||
func main() {
|
||||
settingsPath := flag.String("settings", "/etc/dtms/vctp.yml", "Path to settings YAML")
|
||||
flag.Parse()
|
||||
|
||||
bootstrapLogger := log.New(log.LevelInfo, log.OutputText)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
// Load settings from yaml
|
||||
s := settings.New(bootstrapLogger, *settingsPath)
|
||||
err := s.ReadYMLSettings()
|
||||
if err != nil {
|
||||
bootstrapLogger.Error("failed to open yaml settings file", "error", err, "filename", *settingsPath)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
logger := log.New(
|
||||
log.ToLevel(strings.ToLower(strings.TrimSpace(s.Values.Settings.LogLevel))),
|
||||
log.ToOutput(strings.ToLower(strings.TrimSpace(s.Values.Settings.LogOutput))),
|
||||
)
|
||||
s.Logger = logger
|
||||
|
||||
// Configure database
|
||||
dbDriver := os.Getenv("DB_DRIVER")
|
||||
dbDriver := strings.TrimSpace(s.Values.Settings.DatabaseDriver)
|
||||
if dbDriver == "" {
|
||||
dbDriver = "sqlite"
|
||||
}
|
||||
@@ -57,12 +66,12 @@ func main() {
|
||||
if normalizedDriver == "" || normalizedDriver == "sqlite3" {
|
||||
normalizedDriver = "sqlite"
|
||||
}
|
||||
dbURL := os.Getenv("DB_URL")
|
||||
dbURL := strings.TrimSpace(s.Values.Settings.DatabaseURL)
|
||||
if dbURL == "" && normalizedDriver == "sqlite" {
|
||||
dbURL = utils.GetFilePath("db.sqlite3")
|
||||
}
|
||||
|
||||
database, err := db.New(logger, db.Config{Driver: dbDriver, DSN: dbURL})
|
||||
database, err := db.New(logger, db.Config{Driver: normalizedDriver, DSN: dbURL})
|
||||
if err != nil {
|
||||
logger.Error("Failed to create database", "error", err)
|
||||
os.Exit(1)
|
||||
@@ -70,56 +79,36 @@ func main() {
|
||||
defer database.Close()
|
||||
//defer database.DB().Close()
|
||||
|
||||
if err = db.Migrate(database, dbDriver); err != nil {
|
||||
if err = db.Migrate(database, normalizedDriver); err != nil {
|
||||
logger.Error("failed to migrate database", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Load settings from yaml
|
||||
settingsFile := os.Getenv("SETTINGS_FILE")
|
||||
if settingsFile == "" {
|
||||
settingsFile = "settings.yaml"
|
||||
}
|
||||
|
||||
// TODO - how to pass this to the other packages that will need this info?
|
||||
s := settings.New(logger, settingsFile)
|
||||
err = s.ReadYMLSettings()
|
||||
//s, err := settings.ReadYMLSettings(logger, settingsFile)
|
||||
if err != nil {
|
||||
logger.Error("failed to open yaml settings file", "error", err, "filename", settingsFile)
|
||||
//os.Exit(1)
|
||||
} else {
|
||||
logger.Debug("Loaded yaml settings", "contents", s)
|
||||
}
|
||||
|
||||
// Determine bind IP
|
||||
bindIP := os.Getenv("BIND_IP")
|
||||
bindIP := strings.TrimSpace(s.Values.Settings.BindIP)
|
||||
if bindIP == "" {
|
||||
bindIP = utils.GetOutboundIP().String()
|
||||
}
|
||||
// Determine bind port
|
||||
bindPort := os.Getenv("BIND_PORT")
|
||||
if bindPort == "" {
|
||||
bindPort = "9443"
|
||||
bindPort := s.Values.Settings.BindPort
|
||||
if bindPort == 0 {
|
||||
bindPort = 9443
|
||||
}
|
||||
bindAddress := fmt.Sprint(bindIP, ":", bindPort)
|
||||
//logger.Info("Will listen on address", "ip", bindIP, "port", bindPort)
|
||||
|
||||
// Determine bind disable TLS
|
||||
bindDisableTlsEnv := os.Getenv("BIND_DISABLE_TLS")
|
||||
if bindDisableTlsEnv == "true" {
|
||||
bindDisableTls = true
|
||||
}
|
||||
bindDisableTls = s.Values.Settings.BindDisableTLS
|
||||
|
||||
// Get file names for TLS cert/key
|
||||
tlsCertFilename := os.Getenv("TLS_CERT_FILE")
|
||||
tlsCertFilename := strings.TrimSpace(s.Values.Settings.TLSCertFilename)
|
||||
if tlsCertFilename != "" {
|
||||
tlsCertFilename = utils.GetFilePath(tlsCertFilename)
|
||||
} else {
|
||||
tlsCertFilename = "./cert.pem"
|
||||
}
|
||||
|
||||
tlsKeyFilename := os.Getenv("TLS_KEY_FILE")
|
||||
tlsKeyFilename := strings.TrimSpace(s.Values.Settings.TLSKeyFilename)
|
||||
if tlsKeyFilename != "" {
|
||||
tlsKeyFilename = utils.GetFilePath(tlsKeyFilename)
|
||||
} else {
|
||||
@@ -132,9 +121,10 @@ func main() {
|
||||
utils.GenerateCerts(tlsCertFilename, tlsKeyFilename)
|
||||
}
|
||||
|
||||
// Load vcenter credentials from .env
|
||||
a := secrets.New(logger, encryptionKey)
|
||||
vcEp := os.Getenv("VCENTER_PASSWORD")
|
||||
// Load vcenter credentials from serttings, decrypt if required
|
||||
encKey := deriveEncryptionKey(logger)
|
||||
a := secrets.New(logger, encKey)
|
||||
vcEp := strings.TrimSpace(s.Values.Settings.VcenterPassword)
|
||||
if len(vcEp) == 0 {
|
||||
logger.Error("No vcenter password configured")
|
||||
os.Exit(1)
|
||||
@@ -143,13 +133,26 @@ func main() {
|
||||
if err != nil {
|
||||
logger.Error("failed to decrypt vcenter credentials. Assuming un-encrypted", "error", err)
|
||||
vcPass = []byte(vcEp)
|
||||
//os.Exit(1)
|
||||
if cipherText, encErr := a.Encrypt([]byte(vcEp)); encErr != nil {
|
||||
logger.Warn("failed to encrypt vcenter credentials", "error", encErr)
|
||||
} else {
|
||||
s.Values.Settings.VcenterPassword = cipherText
|
||||
if err := s.WriteYMLSettings(); err != nil {
|
||||
logger.Warn("failed to update settings with encrypted vcenter password", "error", err)
|
||||
} else {
|
||||
logger.Info("encrypted vcenter password stored in settings file")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
creds := vcenter.VcenterLogin{
|
||||
//insecureString := os.Getenv("VCENTER_INSECURE")
|
||||
Username: os.Getenv("VCENTER_USERNAME"),
|
||||
Username: strings.TrimSpace(s.Values.Settings.VcenterUsername),
|
||||
Password: string(vcPass),
|
||||
Insecure: s.Values.Settings.VcenterInsecure,
|
||||
}
|
||||
if creds.Username == "" {
|
||||
logger.Error("No vcenter username configured")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Prepare the task scheduler
|
||||
@@ -161,89 +164,18 @@ func main() {
|
||||
|
||||
// Pass useful information to the cron jobs
|
||||
ct := &tasks.CronTask{
|
||||
Logger: logger,
|
||||
Database: database,
|
||||
Settings: s,
|
||||
VcCreds: &creds,
|
||||
Logger: logger,
|
||||
Database: database,
|
||||
Settings: s,
|
||||
VcCreds: &creds,
|
||||
FirstHourlySnapshotCheck: true,
|
||||
}
|
||||
|
||||
cronFrequencyString := os.Getenv("VCENTER_EVENT_POLLING_SECONDS")
|
||||
if cronFrequencyString != "" {
|
||||
cronFrequency, err = time.ParseDuration(cronFrequencyString)
|
||||
if err != nil {
|
||||
slog.Error("Can't convert VCENTER_EVENT_POLLING_SECONDS value to time duration. Defaulting to 60s", "value", cronFrequencyString, "error", err)
|
||||
cronFrequency = time.Second * 60
|
||||
}
|
||||
} else {
|
||||
cronFrequency = time.Second * 60
|
||||
}
|
||||
logger.Debug("Setting VM event polling cronjob frequency to", "frequency", cronFrequency)
|
||||
|
||||
cronInventoryFrequencyString := os.Getenv("VCENTER_INVENTORY_POLLING_SECONDS")
|
||||
if cronInventoryFrequencyString != "" {
|
||||
cronInvFrequency, err = time.ParseDuration(cronInventoryFrequencyString)
|
||||
if err != nil {
|
||||
slog.Error("Can't convert VCENTER_INVENTORY_POLLING_SECONDS value to time duration. Defaulting to 7200", "value", cronInventoryFrequencyString, "error", err)
|
||||
cronInvFrequency = time.Second * 7200
|
||||
}
|
||||
} else {
|
||||
cronInvFrequency = time.Second * 7200
|
||||
}
|
||||
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
|
||||
}
|
||||
cronSnapshotFrequency = durationFromSeconds(s.Values.Settings.VcenterInventorySnapshotSeconds, 3600)
|
||||
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(
|
||||
gocron.DurationJob(cronFrequency),
|
||||
gocron.NewTask(func() {
|
||||
ct.RunVmCheck(ctx, logger)
|
||||
}), gocron.WithSingletonMode(gocron.LimitModeReschedule),
|
||||
gocron.WithStartAt(gocron.WithStartDateTime(startsAt)),
|
||||
)
|
||||
if err != nil {
|
||||
logger.Error("failed to start event processing cron job", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
logger.Debug("Created event processing cron job", "job", job.ID(), "starting_at", startsAt)
|
||||
|
||||
// start background checks of vcenter inventory
|
||||
startsAt2 := time.Now().Add(cronInvFrequency)
|
||||
job2, err := c.NewJob(
|
||||
gocron.DurationJob(cronInvFrequency),
|
||||
gocron.NewTask(func() {
|
||||
ct.RunVcenterPoll(ctx, logger)
|
||||
}), gocron.WithSingletonMode(gocron.LimitModeReschedule),
|
||||
gocron.WithStartAt(gocron.WithStartDateTime(startsAt2)),
|
||||
)
|
||||
if err != nil {
|
||||
logger.Error("failed to start vcenter inventory cron job", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
logger.Debug("Created vcenter inventory cron job", "job", job2.ID(), "starting_at", startsAt2)
|
||||
cronAggregateFrequency = durationFromSeconds(s.Values.Settings.VcenterInventoryAggregateSeconds, 86400)
|
||||
logger.Debug("Setting VM inventory daily aggregation cronjob frequency to", "frequency", cronAggregateFrequency)
|
||||
|
||||
startsAt3 := time.Now().Add(cronSnapshotFrequency)
|
||||
if cronSnapshotFrequency == time.Hour {
|
||||
@@ -265,7 +197,7 @@ func main() {
|
||||
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())
|
||||
startsAt4 = time.Date(now.Year(), now.Month(), now.Day()+1, 0, 10, 0, 0, now.Location())
|
||||
}
|
||||
job4, err := c.NewJob(
|
||||
gocron.DurationJob(cronAggregateFrequency),
|
||||
@@ -280,8 +212,10 @@ func main() {
|
||||
}
|
||||
logger.Debug("Created vcenter inventory aggregation cron job", "job", job4.ID(), "starting_at", startsAt4)
|
||||
|
||||
monthlyCron := "0 0 1 * *"
|
||||
logger.Debug("Setting monthly aggregation cron schedule", "cron", monthlyCron)
|
||||
job5, err := c.NewJob(
|
||||
gocron.CronJob("0 0 1 * *", false),
|
||||
gocron.CronJob(monthlyCron, false),
|
||||
gocron.NewTask(func() {
|
||||
ct.RunVcenterMonthlyAggregate(ctx, logger)
|
||||
}), gocron.WithSingletonMode(gocron.LimitModeReschedule),
|
||||
@@ -292,10 +226,22 @@ func main() {
|
||||
}
|
||||
logger.Debug("Created vcenter monthly aggregation cron job", "job", job5.ID())
|
||||
|
||||
snapshotCleanupCron := strings.TrimSpace(s.Values.Settings.SnapshotCleanupCron)
|
||||
if snapshotCleanupCron == "" {
|
||||
snapshotCleanupCron = "30 2 * * *"
|
||||
}
|
||||
job6, err := c.NewJob(
|
||||
gocron.CronJob("0 30 2 * *", false),
|
||||
gocron.CronJob(snapshotCleanupCron, false),
|
||||
gocron.NewTask(func() {
|
||||
ct.RunSnapshotCleanup(ctx, logger)
|
||||
if strings.EqualFold(s.Values.Settings.DatabaseDriver, "sqlite") {
|
||||
logger.Info("Performing sqlite VACUUM after snapshot cleanup")
|
||||
if _, err := ct.Database.DB().ExecContext(ctx, "VACUUM"); err != nil {
|
||||
logger.Warn("VACUUM failed after snapshot cleanup", "error", err)
|
||||
} else {
|
||||
logger.Debug("VACUUM completed after snapshot cleanup")
|
||||
}
|
||||
}
|
||||
}), gocron.WithSingletonMode(gocron.LimitModeReschedule),
|
||||
)
|
||||
if err != nil {
|
||||
@@ -304,6 +250,23 @@ func main() {
|
||||
}
|
||||
logger.Debug("Created snapshot cleanup cron job", "job", job6.ID())
|
||||
|
||||
// Retry failed hourly snapshots
|
||||
retrySeconds := s.Values.Settings.HourlySnapshotRetrySeconds
|
||||
if retrySeconds <= 0 {
|
||||
retrySeconds = 300
|
||||
}
|
||||
job7, err := c.NewJob(
|
||||
gocron.DurationJob(time.Duration(retrySeconds)*time.Second),
|
||||
gocron.NewTask(func() {
|
||||
ct.RunHourlySnapshotRetry(ctx, logger)
|
||||
}), gocron.WithSingletonMode(gocron.LimitModeReschedule),
|
||||
)
|
||||
if err != nil {
|
||||
logger.Error("failed to start hourly snapshot retry cron job", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
logger.Debug("Created hourly snapshot retry cron job", "job", job7.ID(), "interval_seconds", retrySeconds)
|
||||
|
||||
// start cron scheduler
|
||||
c.Start()
|
||||
|
||||
@@ -325,3 +288,33 @@ func main() {
|
||||
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func durationFromSeconds(value int, fallback int) time.Duration {
|
||||
if value <= 0 {
|
||||
return time.Second * time.Duration(fallback)
|
||||
}
|
||||
return time.Second * time.Duration(value)
|
||||
}
|
||||
|
||||
func deriveEncryptionKey(logger *slog.Logger) []byte {
|
||||
if runtime.GOOS == "linux" {
|
||||
if data, err := os.ReadFile("/sys/class/dmi/id/product_uuid"); err == nil {
|
||||
src := strings.TrimSpace(string(data))
|
||||
if src != "" {
|
||||
sum := sha256.Sum256([]byte(src))
|
||||
logger.Debug("derived encryption key from BIOS UUID")
|
||||
return sum[:]
|
||||
}
|
||||
}
|
||||
if data, err := os.ReadFile("/etc/machine-id"); err == nil {
|
||||
src := strings.TrimSpace(string(data))
|
||||
if src != "" {
|
||||
sum := sha256.Sum256([]byte(src))
|
||||
logger.Debug("derived encryption key from machine-id")
|
||||
return sum[:]
|
||||
}
|
||||
}
|
||||
}
|
||||
logger.Warn("using fallback encryption key; hardware UUID not available")
|
||||
return []byte(fallbackEncryptionKey)
|
||||
}
|
||||
|
||||
@@ -10,10 +10,14 @@ 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")
|
||||
host_os=$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||
host_arch=$(uname -m)
|
||||
platforms=("linux/amd64")
|
||||
if [[ "$host_os" == "darwin" && ( "$host_arch" == "x86_64" || "$host_arch" == "amd64" || "$host_arch" == "arm64" ) ]]; then
|
||||
platforms=("darwin/amd64")
|
||||
fi
|
||||
|
||||
echo Building::
|
||||
echo Building: $package_name
|
||||
echo - Version $package_version
|
||||
echo - Commit $commit
|
||||
echo - Build Time $buildtime
|
||||
@@ -30,7 +34,7 @@ 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.version=$package_version -X main.commit=$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.sha1ver=$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
|
||||
@@ -40,6 +44,4 @@ do
|
||||
#sha256sum build/${output_name}.gz > build/${output_name}_checksum.txt
|
||||
done
|
||||
|
||||
nfpm package --config $package_name.yml --packager rpm --target build/
|
||||
|
||||
ls -lah build
|
||||
|
||||
@@ -4,7 +4,7 @@ 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}"
|
||||
VERSION="${1:-v5.31.0}"
|
||||
|
||||
TARGET_DIR="server/router/swagger-ui-dist"
|
||||
TARBALL_URL="https://github.com/swagger-api/swagger-ui/archive/refs/tags/${VERSION}.tar.gz"
|
||||
@@ -41,11 +41,19 @@ fi
|
||||
|
||||
echo ">> Patching swagger-initializer.js to point at /swagger.json"
|
||||
|
||||
sed -i -E \
|
||||
if sed --version >/dev/null 2>&1; then
|
||||
SED_INPLACE=(-i)
|
||||
else
|
||||
SED_INPLACE=(-i '')
|
||||
fi
|
||||
|
||||
append_validator=$'/url:[[:space:]]*"[^"]*swagger\\.json"[[:space:]]*,?$/a\\\n validatorUrl: null,'
|
||||
|
||||
sed "${SED_INPLACE[@]}" -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,' \
|
||||
-e "$append_validator" \
|
||||
"$INDEX"
|
||||
|
||||
echo ">> Done. Files are in ${TARGET_DIR}"
|
||||
echo ">> Done. Files are in ${TARGET_DIR}"
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"vctp/db"
|
||||
"vctp/internal/secrets"
|
||||
"vctp/internal/settings"
|
||||
"vctp/internal/vcenter"
|
||||
|
||||
"github.com/a-h/templ"
|
||||
)
|
||||
|
||||
// Handler handles requests.
|
||||
@@ -23,12 +19,3 @@ type Handler struct {
|
||||
Secret *secrets.Secrets
|
||||
Settings *settings.Settings
|
||||
}
|
||||
|
||||
func (h *Handler) html(ctx context.Context, w http.ResponseWriter, status int, t templ.Component) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.WriteHeader(status)
|
||||
|
||||
if err := t.Render(ctx, w); err != nil {
|
||||
h.Logger.Error("Failed to render component", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
17
server/handler/metrics.go
Normal file
17
server/handler/metrics.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"vctp/internal/metrics"
|
||||
)
|
||||
|
||||
// Metrics exposes Prometheus metrics.
|
||||
// @Summary Prometheus metrics
|
||||
// @Description Exposes Prometheus metrics for vctp.
|
||||
// @Tags metrics
|
||||
// @Produce plain
|
||||
// @Success 200 "Prometheus metrics"
|
||||
// @Router /metrics [get]
|
||||
func (h *Handler) Metrics(w http.ResponseWriter, r *http.Request) {
|
||||
metrics.Handler().ServeHTTP(w, r)
|
||||
}
|
||||
97
server/handler/snapshotAggregate.go
Normal file
97
server/handler/snapshotAggregate.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
"vctp/internal/tasks"
|
||||
)
|
||||
|
||||
// SnapshotAggregateForce forces regeneration of a daily or monthly summary table.
|
||||
// @Summary Force snapshot aggregation
|
||||
// @Description Forces regeneration of a daily or monthly summary table for a specified date or month.
|
||||
// @Tags snapshots
|
||||
// @Produce json
|
||||
// @Param type query string true "Aggregation type: daily or monthly"
|
||||
// @Param date query string true "Daily date (YYYY-MM-DD) or monthly date (YYYY-MM)"
|
||||
// @Success 200 {object} map[string]string "Aggregation complete"
|
||||
// @Failure 400 {object} map[string]string "Invalid request"
|
||||
// @Failure 500 {object} map[string]string "Server error"
|
||||
// @Router /api/snapshots/aggregate [post]
|
||||
func (h *Handler) SnapshotAggregateForce(w http.ResponseWriter, r *http.Request) {
|
||||
snapshotType := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("type")))
|
||||
dateValue := strings.TrimSpace(r.URL.Query().Get("date"))
|
||||
startedAt := time.Now()
|
||||
loc := time.Now().Location()
|
||||
|
||||
if snapshotType == "" || dateValue == "" {
|
||||
h.Logger.Warn("Snapshot aggregation request missing parameters",
|
||||
"type", snapshotType,
|
||||
"date", dateValue,
|
||||
)
|
||||
writeJSONError(w, http.StatusBadRequest, "type and date are required")
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ct := &tasks.CronTask{
|
||||
Logger: h.Logger,
|
||||
Database: h.Database,
|
||||
Settings: h.Settings,
|
||||
}
|
||||
|
||||
switch snapshotType {
|
||||
case "daily":
|
||||
parsed, err := time.ParseInLocation("2006-01-02", dateValue, loc)
|
||||
if err != nil {
|
||||
h.Logger.Warn("Snapshot aggregation invalid daily date format", "date", dateValue)
|
||||
writeJSONError(w, http.StatusBadRequest, "date must be YYYY-MM-DD")
|
||||
return
|
||||
}
|
||||
h.Logger.Info("Starting daily snapshot aggregation", "date", parsed.Format("2006-01-02"), "force", true)
|
||||
if err := ct.AggregateDailySummary(ctx, parsed, true); err != nil {
|
||||
h.Logger.Error("Daily snapshot aggregation failed", "date", parsed.Format("2006-01-02"), "error", err)
|
||||
writeJSONError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
case "monthly":
|
||||
parsed, err := time.ParseInLocation("2006-01", dateValue, loc)
|
||||
if err != nil {
|
||||
h.Logger.Warn("Snapshot aggregation invalid monthly date format", "date", dateValue)
|
||||
writeJSONError(w, http.StatusBadRequest, "date must be YYYY-MM")
|
||||
return
|
||||
}
|
||||
h.Logger.Info("Starting monthly snapshot aggregation", "date", parsed.Format("2006-01"), "force", true)
|
||||
if err := ct.AggregateMonthlySummary(ctx, parsed, true); err != nil {
|
||||
h.Logger.Error("Monthly snapshot aggregation failed", "date", parsed.Format("2006-01"), "error", err)
|
||||
writeJSONError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
default:
|
||||
h.Logger.Warn("Snapshot aggregation invalid type", "type", snapshotType)
|
||||
writeJSONError(w, http.StatusBadRequest, "type must be daily or monthly")
|
||||
return
|
||||
}
|
||||
|
||||
h.Logger.Info("Snapshot aggregation completed",
|
||||
"type", snapshotType,
|
||||
"date", dateValue,
|
||||
"duration", time.Since(startedAt),
|
||||
)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"status": "OK",
|
||||
})
|
||||
}
|
||||
|
||||
func writeJSONError(w http.ResponseWriter, status int, message string) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"status": "ERROR",
|
||||
"message": message,
|
||||
})
|
||||
}
|
||||
51
server/handler/snapshotForceHourly.go
Normal file
51
server/handler/snapshotForceHourly.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
"vctp/internal/tasks"
|
||||
)
|
||||
|
||||
// SnapshotForceHourly triggers an on-demand hourly snapshot run.
|
||||
// @Summary Trigger hourly snapshot (manual)
|
||||
// @Description Manually trigger an hourly snapshot for all configured vCenters. Requires confirmation text to avoid accidental execution.
|
||||
// @Tags snapshots
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param confirm query string true "Confirmation text; must be 'FORCE'"
|
||||
// @Success 200 {object} map[string]string "Snapshot started"
|
||||
// @Failure 400 {object} map[string]string "Invalid request"
|
||||
// @Failure 500 {object} map[string]string "Server error"
|
||||
// @Router /api/snapshots/hourly/force [post]
|
||||
func (h *Handler) SnapshotForceHourly(w http.ResponseWriter, r *http.Request) {
|
||||
confirm := strings.TrimSpace(r.URL.Query().Get("confirm"))
|
||||
if strings.ToUpper(confirm) != "FORCE" {
|
||||
writeJSONError(w, http.StatusBadRequest, "confirm must be 'FORCE'")
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ct := &tasks.CronTask{
|
||||
Logger: h.Logger,
|
||||
Database: h.Database,
|
||||
Settings: h.Settings,
|
||||
VcCreds: h.VcCreds,
|
||||
}
|
||||
|
||||
started := time.Now()
|
||||
h.Logger.Info("Manual hourly snapshot requested")
|
||||
if err := ct.RunVcenterSnapshotHourly(ctx, h.Logger.With("manual", true)); err != nil {
|
||||
h.Logger.Error("Manual hourly snapshot failed", "error", err)
|
||||
writeJSONError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.Logger.Info("Manual hourly snapshot completed", "duration", time.Since(started))
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"status": "OK",
|
||||
})
|
||||
}
|
||||
38
server/handler/snapshotMigrate.go
Normal file
38
server/handler/snapshotMigrate.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"vctp/internal/report"
|
||||
)
|
||||
|
||||
// SnapshotMigrate rebuilds the snapshot registry and normalizes hourly table names.
|
||||
// @Summary Migrate snapshot registry
|
||||
// @Description Rebuilds the snapshot registry from existing tables and renames hourly tables to epoch-based names.
|
||||
// @Tags snapshots
|
||||
// @Produce json
|
||||
// @Success 200 {object} map[string]interface{} "Migration results"
|
||||
// @Failure 500 {object} map[string]string "Server error"
|
||||
// @Router /api/snapshots/migrate [post]
|
||||
func (h *Handler) SnapshotMigrate(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.Background()
|
||||
stats, err := report.MigrateSnapshotRegistry(ctx, h.Database)
|
||||
if err != nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "ERROR",
|
||||
"error": err.Error(),
|
||||
"stats": stats,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "OK",
|
||||
"stats": stats,
|
||||
})
|
||||
}
|
||||
68
server/handler/snapshotRegenerateHourly.go
Normal file
68
server/handler/snapshotRegenerateHourly.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
"vctp/internal/report"
|
||||
)
|
||||
|
||||
// SnapshotRegenerateHourlyReports regenerates missing hourly snapshot XLSX reports on disk.
|
||||
// @Summary Regenerate hourly snapshot reports
|
||||
// @Description Regenerates XLSX reports for hourly snapshots when the report files are missing or empty.
|
||||
// @Tags snapshots
|
||||
// @Produce json
|
||||
// @Success 200 {object} map[string]interface{} "Regeneration summary"
|
||||
// @Failure 500 {object} map[string]string "Server error"
|
||||
// @Router /api/snapshots/regenerate-hourly-reports [post]
|
||||
func (h *Handler) SnapshotRegenerateHourlyReports(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
reportsDir := strings.TrimSpace(h.Settings.Values.Settings.ReportsDir)
|
||||
if reportsDir == "" {
|
||||
reportsDir = "/var/lib/vctp/reports"
|
||||
}
|
||||
if err := os.MkdirAll(reportsDir, 0o755); err != nil {
|
||||
h.Logger.Error("failed to create reports directory", "error", err, "path", reportsDir)
|
||||
writeJSONError(w, http.StatusInternalServerError, "failed to create reports directory")
|
||||
return
|
||||
}
|
||||
|
||||
start := time.Unix(0, 0)
|
||||
end := time.Now().AddDate(10, 0, 0) // sufficiently in the future to include all records
|
||||
records, err := report.SnapshotRecordsWithFallback(ctx, h.Database, "hourly", "inventory_hourly_", "epoch", start, end)
|
||||
if err != nil {
|
||||
h.Logger.Error("failed to list hourly snapshots", "error", err)
|
||||
writeJSONError(w, http.StatusInternalServerError, "failed to list hourly snapshots")
|
||||
return
|
||||
}
|
||||
|
||||
var regenerated, skipped, errors int
|
||||
for _, rec := range records {
|
||||
dest := filepath.Join(reportsDir, rec.TableName+".xlsx")
|
||||
if info, err := os.Stat(dest); err == nil && info.Size() > 0 {
|
||||
skipped++
|
||||
continue
|
||||
}
|
||||
if _, err := report.SaveTableReport(h.Logger, h.Database, ctx, rec.TableName, reportsDir); err != nil {
|
||||
errors++
|
||||
h.Logger.Warn("failed to regenerate hourly report", "table", rec.TableName, "error", err)
|
||||
continue
|
||||
}
|
||||
regenerated++
|
||||
}
|
||||
|
||||
resp := map[string]interface{}{
|
||||
"status": "OK",
|
||||
"total": len(records),
|
||||
"regenerated": regenerated,
|
||||
"skipped": skipped,
|
||||
"errors": errors,
|
||||
"reports_dir": reportsDir,
|
||||
"snapshotType": "hourly",
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"vctp/components/views"
|
||||
"vctp/internal/report"
|
||||
|
||||
@@ -22,7 +21,7 @@ import (
|
||||
// @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)
|
||||
h.renderSnapshotList(w, r, "hourly", "Hourly Inventory Snapshots", views.SnapshotHourlyList)
|
||||
}
|
||||
|
||||
// SnapshotDailyList renders the daily snapshot list page.
|
||||
@@ -34,7 +33,7 @@ func (h *Handler) SnapshotHourlyList(w http.ResponseWriter, r *http.Request) {
|
||||
// @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)
|
||||
h.renderSnapshotList(w, r, "daily", "Daily Inventory Snapshots", views.SnapshotDailyList)
|
||||
}
|
||||
|
||||
// SnapshotMonthlyList renders the monthly snapshot list page.
|
||||
@@ -46,7 +45,7 @@ func (h *Handler) SnapshotDailyList(w http.ResponseWriter, r *http.Request) {
|
||||
// @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)
|
||||
h.renderSnapshotList(w, r, "monthly", "Monthly Inventory Snapshots", views.SnapshotMonthlyList)
|
||||
}
|
||||
|
||||
// SnapshotReportDownload streams a snapshot table as XLSX.
|
||||
@@ -91,28 +90,39 @@ func (h *Handler) SnapshotReportDownload(w http.ResponseWriter, r *http.Request)
|
||||
w.Write(reportData)
|
||||
}
|
||||
|
||||
func (h *Handler) renderSnapshotList(w http.ResponseWriter, r *http.Request, prefix string, title string, renderer func([]views.SnapshotEntry) templ.Component) {
|
||||
func (h *Handler) renderSnapshotList(w http.ResponseWriter, r *http.Request, snapshotType string, title string, renderer func([]views.SnapshotEntry) templ.Component) {
|
||||
ctx := context.Background()
|
||||
tables, err := report.ListTablesByPrefix(ctx, h.Database, prefix)
|
||||
if err := report.EnsureSnapshotRegistry(ctx, h.Database); err != nil {
|
||||
h.Logger.Error("Failed to ensure snapshot registry", "error", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
fmt.Fprintf(w, "Unable to list snapshot tables: %s\n", err)
|
||||
return
|
||||
}
|
||||
records, err := report.ListSnapshots(ctx, h.Database, snapshotType)
|
||||
if err != nil {
|
||||
h.Logger.Error("Failed to list snapshot tables", "error", err, "prefix", prefix)
|
||||
h.Logger.Error("Failed to list snapshots", "error", err, "snapshot_type", snapshotType)
|
||||
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 := make([]views.SnapshotEntry, 0, len(records))
|
||||
for _, record := range records {
|
||||
label := report.FormatSnapshotLabel(snapshotType, record.SnapshotTime, record.TableName)
|
||||
group := ""
|
||||
switch snapshotType {
|
||||
case "hourly":
|
||||
group = record.SnapshotTime.Format("2006-01-02")
|
||||
case "daily":
|
||||
group = record.SnapshotTime.Format("January 2006")
|
||||
case "monthly":
|
||||
group = record.SnapshotTime.Format("2006")
|
||||
}
|
||||
entries = append(entries, views.SnapshotEntry{
|
||||
Label: label,
|
||||
Link: "/api/report/snapshot?table=" + url.QueryEscape(table),
|
||||
Link: "/reports/" + url.PathEscape(record.TableName) + ".xlsx",
|
||||
Count: record.SnapshotCount,
|
||||
Group: group,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -7,9 +7,10 @@ import (
|
||||
)
|
||||
|
||||
// UpdateCleanup removes orphaned update records.
|
||||
// @Summary Cleanup updates
|
||||
// @Description Removes update records that are no longer associated with a VM.
|
||||
// @Summary Cleanup updates (deprecated)
|
||||
// @Description Deprecated: Removes update records that are no longer associated with a VM.
|
||||
// @Tags maintenance
|
||||
// @Deprecated
|
||||
// @Produce text/plain
|
||||
// @Success 200 {string} string "Cleanup completed"
|
||||
// @Failure 500 {string} string "Server error"
|
||||
|
||||
@@ -10,9 +10,10 @@ import (
|
||||
)
|
||||
|
||||
// VcCleanup removes inventory entries for a vCenter instance.
|
||||
// @Summary Cleanup vCenter inventory
|
||||
// @Description Removes all inventory entries associated with a vCenter URL.
|
||||
// @Summary Cleanup vCenter inventory (deprecated)
|
||||
// @Description Deprecated: Removes all inventory entries associated with a vCenter URL.
|
||||
// @Tags maintenance
|
||||
// @Deprecated
|
||||
// @Produce json
|
||||
// @Param vc_url query string true "vCenter URL"
|
||||
// @Success 200 {object} map[string]string "Cleanup completed"
|
||||
|
||||
231
server/handler/vcenters.go
Normal file
231
server/handler/vcenters.go
Normal file
@@ -0,0 +1,231 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"vctp/components/views"
|
||||
"vctp/db"
|
||||
)
|
||||
|
||||
// VcenterList renders a list of vCenters being monitored.
|
||||
// @Summary List vCenters
|
||||
// @Description Lists all vCenters with recorded snapshot totals.
|
||||
// @Tags vcenters
|
||||
// @Produce text/html
|
||||
// @Success 200 {string} string "HTML page"
|
||||
// @Router /vcenters [get]
|
||||
func (h *Handler) VcenterList(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
if err := db.SyncVcenterTotalsFromSnapshots(ctx, h.Database.DB()); err != nil {
|
||||
h.Logger.Warn("failed to sync vcenter totals", "error", err)
|
||||
}
|
||||
vcs, err := db.ListVcenters(ctx, h.Database.DB())
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("failed to list vcenters: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
links := make([]views.VcenterLink, 0, len(vcs))
|
||||
for _, vc := range vcs {
|
||||
links = append(links, views.VcenterLink{
|
||||
Name: vc,
|
||||
Link: "/vcenters/totals?vcenter=" + url.QueryEscape(vc),
|
||||
})
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
if err := views.VcenterList(links).Render(ctx, w); err != nil {
|
||||
http.Error(w, "Failed to render template", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
// VcenterTotals renders totals for a vCenter.
|
||||
// @Summary vCenter totals
|
||||
// @Description Shows per-snapshot totals for a vCenter.
|
||||
// @Tags vcenters
|
||||
// @Produce text/html
|
||||
// @Param vcenter query string true "vCenter URL"
|
||||
// @Param type query string false "hourly|daily|monthly (default: hourly)"
|
||||
// @Param limit query int false "Limit results (default 200)"
|
||||
// @Success 200 {string} string "HTML page"
|
||||
// @Failure 400 {string} string "Missing vcenter"
|
||||
// @Router /vcenters/totals [get]
|
||||
func (h *Handler) VcenterTotals(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
vc := r.URL.Query().Get("vcenter")
|
||||
if vc == "" {
|
||||
http.Error(w, "vcenter is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
viewType := strings.ToLower(r.URL.Query().Get("type"))
|
||||
if viewType == "" {
|
||||
viewType = "hourly"
|
||||
}
|
||||
switch viewType {
|
||||
case "hourly", "daily", "monthly":
|
||||
default:
|
||||
viewType = "hourly"
|
||||
}
|
||||
if viewType == "hourly" {
|
||||
if err := db.SyncVcenterTotalsFromSnapshots(ctx, h.Database.DB()); err != nil {
|
||||
h.Logger.Warn("failed to sync vcenter totals", "error", err)
|
||||
}
|
||||
}
|
||||
limit := 200
|
||||
if l := r.URL.Query().Get("limit"); l != "" {
|
||||
if v, err := strconv.Atoi(l); err == nil && v > 0 {
|
||||
limit = v
|
||||
}
|
||||
}
|
||||
rows, err := db.ListVcenterTotalsByType(ctx, h.Database.DB(), vc, viewType, limit)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("failed to list totals: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
entries := make([]views.VcenterTotalsEntry, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
entries = append(entries, views.VcenterTotalsEntry{
|
||||
Snapshot: time.Unix(row.SnapshotTime, 0).Local().Format("2006-01-02 15:04:05"),
|
||||
RawTime: row.SnapshotTime,
|
||||
VmCount: row.VmCount,
|
||||
VcpuTotal: row.VcpuTotal,
|
||||
RamTotalGB: row.RamTotalGB,
|
||||
})
|
||||
}
|
||||
chart := buildVcenterChart(entries)
|
||||
meta := buildVcenterMeta(vc, viewType)
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
if err := views.VcenterTotalsPage(vc, entries, chart, meta).Render(ctx, w); err != nil {
|
||||
http.Error(w, "Failed to render template", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func buildVcenterMeta(vcenter string, viewType string) views.VcenterTotalsMeta {
|
||||
active := viewType
|
||||
if active == "" {
|
||||
active = "hourly"
|
||||
}
|
||||
meta := views.VcenterTotalsMeta{
|
||||
ViewType: active,
|
||||
TypeLabel: "Hourly",
|
||||
HourlyLink: "/vcenters/totals?vcenter=" + url.QueryEscape(vcenter) + "&type=hourly",
|
||||
DailyLink: "/vcenters/totals?vcenter=" + url.QueryEscape(vcenter) + "&type=daily",
|
||||
MonthlyLink: "/vcenters/totals?vcenter=" + url.QueryEscape(vcenter) + "&type=monthly",
|
||||
HourlyClass: "web3-button",
|
||||
DailyClass: "web3-button",
|
||||
MonthlyClass: "web3-button",
|
||||
}
|
||||
switch active {
|
||||
case "daily":
|
||||
meta.TypeLabel = "Daily"
|
||||
meta.DailyClass = "web3-button active"
|
||||
case "monthly":
|
||||
meta.TypeLabel = "Monthly"
|
||||
meta.MonthlyClass = "web3-button active"
|
||||
default:
|
||||
meta.ViewType = "hourly"
|
||||
meta.HourlyClass = "web3-button active"
|
||||
}
|
||||
return meta
|
||||
}
|
||||
|
||||
func buildVcenterChart(entries []views.VcenterTotalsEntry) views.VcenterChartData {
|
||||
if len(entries) == 0 {
|
||||
return views.VcenterChartData{}
|
||||
}
|
||||
// Plot oldest on the left, newest on the right.
|
||||
plot := make([]views.VcenterTotalsEntry, 0, len(entries))
|
||||
for i := len(entries) - 1; i >= 0; i-- {
|
||||
plot = append(plot, entries[i])
|
||||
}
|
||||
|
||||
width := 1200.0
|
||||
height := 260.0
|
||||
plotWidth := width - 60.0
|
||||
startX := 40.0
|
||||
maxVal := float64(0)
|
||||
for _, e := range plot {
|
||||
if float64(e.VmCount) > maxVal {
|
||||
maxVal = float64(e.VmCount)
|
||||
}
|
||||
if float64(e.VcpuTotal) > maxVal {
|
||||
maxVal = float64(e.VcpuTotal)
|
||||
}
|
||||
if float64(e.RamTotalGB) > maxVal {
|
||||
maxVal = float64(e.RamTotalGB)
|
||||
}
|
||||
}
|
||||
if maxVal == 0 {
|
||||
maxVal = 1
|
||||
}
|
||||
stepX := plotWidth
|
||||
if len(plot) > 1 {
|
||||
stepX = plotWidth / float64(len(plot)-1)
|
||||
}
|
||||
pointsVm := ""
|
||||
pointsVcpu := ""
|
||||
pointsRam := ""
|
||||
for i, e := range plot {
|
||||
x := startX + float64(i)*stepX
|
||||
yVm := 10 + (1-(float64(e.VmCount)/maxVal))*height
|
||||
yVcpu := 10 + (1-(float64(e.VcpuTotal)/maxVal))*height
|
||||
yRam := 10 + (1-(float64(e.RamTotalGB)/maxVal))*height
|
||||
if i == 0 {
|
||||
pointsVm = fmt.Sprintf("%.1f,%.1f", x, yVm)
|
||||
pointsVcpu = fmt.Sprintf("%.1f,%.1f", x, yVcpu)
|
||||
pointsRam = fmt.Sprintf("%.1f,%.1f", x, yRam)
|
||||
} else {
|
||||
pointsVm = pointsVm + " " + fmt.Sprintf("%.1f,%.1f", x, yVm)
|
||||
pointsVcpu = pointsVcpu + " " + fmt.Sprintf("%.1f,%.1f", x, yVcpu)
|
||||
pointsRam = pointsRam + " " + fmt.Sprintf("%.1f,%.1f", x, yRam)
|
||||
}
|
||||
}
|
||||
gridX := []float64{}
|
||||
if len(plot) > 1 {
|
||||
for i := 0; i < len(plot); i++ {
|
||||
gridX = append(gridX, startX+float64(i)*stepX)
|
||||
}
|
||||
}
|
||||
gridY := []float64{}
|
||||
for i := 0; i <= 4; i++ {
|
||||
gridY = append(gridY, 10+float64(i)*(height/4))
|
||||
}
|
||||
yTicks := []views.ChartTick{}
|
||||
for i := 0; i <= 4; i++ {
|
||||
val := maxVal * float64(4-i) / 4
|
||||
pos := 10 + float64(i)*(height/4)
|
||||
yTicks = append(yTicks, views.ChartTick{Pos: pos, Label: fmt.Sprintf("%.0f", val)})
|
||||
}
|
||||
xTicks := []views.ChartTick{}
|
||||
maxTicks := 6
|
||||
stepIdx := 1
|
||||
if len(plot) > 1 {
|
||||
stepIdx = (len(plot)-1)/maxTicks + 1
|
||||
}
|
||||
for idx := 0; idx < len(plot); idx += stepIdx {
|
||||
x := startX + float64(idx)*stepX
|
||||
label := time.Unix(plot[idx].RawTime, 0).Local().Format("01-02 15:04")
|
||||
xTicks = append(xTicks, views.ChartTick{Pos: x, Label: label})
|
||||
}
|
||||
if len(plot) > 1 {
|
||||
lastIdx := len(plot) - 1
|
||||
xLast := startX + float64(lastIdx)*stepX
|
||||
labelLast := time.Unix(plot[lastIdx].RawTime, 0).Local().Format("01-02 15:04")
|
||||
if len(xTicks) == 0 || xTicks[len(xTicks)-1].Pos != xLast {
|
||||
xTicks = append(xTicks, views.ChartTick{Pos: xLast, Label: labelLast})
|
||||
}
|
||||
}
|
||||
return views.VcenterChartData{
|
||||
PointsVm: pointsVm,
|
||||
PointsVcpu: pointsVcpu,
|
||||
PointsRam: pointsRam,
|
||||
Width: int(width),
|
||||
Height: int(height),
|
||||
GridX: gridX,
|
||||
GridY: gridY,
|
||||
YTicks: yTicks,
|
||||
XTicks: xTicks,
|
||||
}
|
||||
}
|
||||
@@ -15,9 +15,10 @@ import (
|
||||
)
|
||||
|
||||
// VmCreateEvent records a VM creation CloudEvent.
|
||||
// @Summary Record VM create event
|
||||
// @Description Parses a VM create CloudEvent and stores the event data.
|
||||
// @Summary Record VM create event (deprecated)
|
||||
// @Description Deprecated: Parses a VM create CloudEvent and stores the event data.
|
||||
// @Tags events
|
||||
// @Deprecated
|
||||
// @Accept json
|
||||
// @Produce text/plain
|
||||
// @Param event body models.CloudEventReceived true "CloudEvent payload"
|
||||
|
||||
@@ -13,9 +13,10 @@ import (
|
||||
)
|
||||
|
||||
// VmDeleteEvent records a VM deletion CloudEvent in the inventory.
|
||||
// @Summary Record VM delete event
|
||||
// @Description Parses a VM delete CloudEvent and marks the VM as deleted in inventory.
|
||||
// @Summary Record VM delete event (deprecated)
|
||||
// @Description Deprecated: Parses a VM delete CloudEvent and marks the VM as deleted in inventory.
|
||||
// @Tags events
|
||||
// @Deprecated
|
||||
// @Accept json
|
||||
// @Produce text/plain
|
||||
// @Param event body models.CloudEventReceived true "CloudEvent payload"
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"vctp/db"
|
||||
queries "vctp/db/queries"
|
||||
models "vctp/server/models"
|
||||
@@ -56,6 +57,17 @@ func (h *Handler) VmImport(w http.ResponseWriter, r *http.Request) {
|
||||
//prettyPrint(inData)
|
||||
}
|
||||
|
||||
if strings.HasPrefix(inData.Name, "vCLS-") {
|
||||
h.Logger.Info("Skipping internal vCLS VM import", "vm_name", inData.Name)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"status": "OK",
|
||||
"message": fmt.Sprintf("Skipped internal VM '%s'", inData.Name),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Query Inventory table for this VM before adding it
|
||||
|
||||
@@ -20,9 +20,10 @@ import (
|
||||
)
|
||||
|
||||
// VmModifyEvent records a VM modification CloudEvent.
|
||||
// @Summary Record VM modify event
|
||||
// @Description Parses a VM modify CloudEvent and creates an update record when relevant changes are detected.
|
||||
// @Summary Record VM modify event (deprecated)
|
||||
// @Description Deprecated: Parses a VM modify CloudEvent and creates an update record when relevant changes are detected.
|
||||
// @Tags events
|
||||
// @Deprecated
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param event body models.CloudEventReceived true "CloudEvent payload"
|
||||
@@ -116,22 +117,23 @@ func (h *Handler) VmModifyEvent(w http.ResponseWriter, r *http.Request) {
|
||||
params.UpdateType = "reconfigure"
|
||||
}
|
||||
case "config.managedBy": // This changes when a VM becomes a placeholder or vice versa
|
||||
if change["newValue"] == "(extensionKey = \"com.vmware.vcDr\", type = \"placeholderVm\")" {
|
||||
switch change["newValue"] {
|
||||
case "(extensionKey = \"com.vmware.vcDr\", type = \"placeholderVm\")":
|
||||
params.PlaceholderChange = sql.NullString{String: "placeholderVm", Valid: true}
|
||||
h.Logger.Debug("placeholderVm")
|
||||
changeFound = true
|
||||
params.UpdateType = "srm"
|
||||
} else if change["newValue"] == "<unset>" {
|
||||
case "<unset>":
|
||||
params.PlaceholderChange = sql.NullString{String: "Vm", Valid: true}
|
||||
h.Logger.Debug("vm")
|
||||
changeFound = true
|
||||
params.UpdateType = "srm"
|
||||
} else if change["newValue"] == "testVm" {
|
||||
case "testVm":
|
||||
h.Logger.Debug("testVm")
|
||||
params.PlaceholderChange = sql.NullString{String: "testVm", Valid: true}
|
||||
changeFound = true
|
||||
params.UpdateType = "srm"
|
||||
} else {
|
||||
default:
|
||||
h.Logger.Error("Unexpected value for managedBy configuration", "new_value", change["newValue"])
|
||||
}
|
||||
|
||||
@@ -145,12 +147,13 @@ func (h *Handler) VmModifyEvent(w http.ResponseWriter, r *http.Request) {
|
||||
// TODO - track when this happens, maybe need a new database column?
|
||||
case "config.managedBy.type":
|
||||
h.Logger.Debug("config.managedBy.type")
|
||||
if change["newValue"] == "testVm" {
|
||||
switch change["newValue"] {
|
||||
case "testVm":
|
||||
h.Logger.Debug("testVm")
|
||||
params.PlaceholderChange = sql.NullString{String: "testVm", Valid: true}
|
||||
changeFound = true
|
||||
params.UpdateType = "srm"
|
||||
} else if change["newValue"] == "\\\"placeholderVm\\\"" {
|
||||
case "\\\"placeholderVm\\\"":
|
||||
h.Logger.Debug("placeholderVm")
|
||||
params.PlaceholderChange = sql.NullString{String: "placeholderVm", Valid: true}
|
||||
changeFound = true
|
||||
@@ -441,6 +444,14 @@ func (h *Handler) AddVmToInventory(evt models.CloudEventReceived, ctx context.Co
|
||||
|
||||
}
|
||||
|
||||
if strings.HasPrefix(vmObject.Name, "vCLS-") {
|
||||
h.Logger.Info("Skipping internal vCLS VM", "vm_name", vmObject.Name)
|
||||
if err := vc.Logout(); err != nil {
|
||||
h.Logger.Error("unable to logout of vcenter", "error", err)
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
//c.Logger.Debug("found VM")
|
||||
srmPlaceholder = "FALSE" // Default assumption
|
||||
//prettyPrint(vmObject)
|
||||
|
||||
@@ -15,9 +15,10 @@ import (
|
||||
)
|
||||
|
||||
// VmMoveEvent records a VM move CloudEvent as an update.
|
||||
// @Summary Record VM move event
|
||||
// @Description Parses a VM move CloudEvent and creates an update record.
|
||||
// @Summary Record VM move event (deprecated)
|
||||
// @Description Deprecated: Parses a VM move CloudEvent and creates an update record.
|
||||
// @Tags events
|
||||
// @Deprecated
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param event body models.CloudEventReceived true "CloudEvent payload"
|
||||
|
||||
227
server/handler/vmTrace.go
Normal file
227
server/handler/vmTrace.go
Normal file
@@ -0,0 +1,227 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
"vctp/components/views"
|
||||
"vctp/db"
|
||||
)
|
||||
|
||||
// VmTrace shows per-snapshot details for a VM across all snapshots.
|
||||
// @Summary Trace VM history
|
||||
// @Description Shows VM resource history across snapshots, with chart and table.
|
||||
// @Tags vm
|
||||
// @Produce text/html
|
||||
// @Param vm_id query string false "VM ID"
|
||||
// @Param vm_uuid query string false "VM UUID"
|
||||
// @Param name query string false "VM name"
|
||||
// @Success 200 {string} string "HTML page"
|
||||
// @Failure 400 {string} string "Missing identifier"
|
||||
// @Router /vm/trace [get]
|
||||
func (h *Handler) VmTrace(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
vmID := r.URL.Query().Get("vm_id")
|
||||
vmUUID := r.URL.Query().Get("vm_uuid")
|
||||
name := r.URL.Query().Get("name")
|
||||
|
||||
var entries []views.VmTraceEntry
|
||||
chart := views.VmTraceChart{}
|
||||
queryLabel := firstNonEmpty(vmID, vmUUID, name)
|
||||
displayQuery := ""
|
||||
if queryLabel != "" {
|
||||
displayQuery = " for " + queryLabel
|
||||
}
|
||||
creationLabel := ""
|
||||
deletionLabel := ""
|
||||
|
||||
// Only fetch data when a query is provided; otherwise render empty page with form.
|
||||
if vmID != "" || vmUUID != "" || name != "" {
|
||||
h.Logger.Info("vm trace request", "vm_id", vmID, "vm_uuid", vmUUID, "name", name)
|
||||
lifecycle, lifeErr := db.FetchVmLifecycle(ctx, h.Database.DB(), vmID, vmUUID, name)
|
||||
if lifeErr != nil {
|
||||
h.Logger.Warn("failed to fetch VM lifecycle", "error", lifeErr)
|
||||
}
|
||||
rows, err := db.FetchVmTrace(ctx, h.Database.DB(), vmID, vmUUID, name)
|
||||
if err != nil {
|
||||
h.Logger.Error("failed to fetch VM trace", "error", err)
|
||||
http.Error(w, fmt.Sprintf("failed to fetch VM trace: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
h.Logger.Info("vm trace results", "row_count", len(rows))
|
||||
entries = make([]views.VmTraceEntry, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
creation := int64(0)
|
||||
if row.CreationTime.Valid {
|
||||
creation = row.CreationTime.Int64
|
||||
}
|
||||
deletion := int64(0)
|
||||
if row.DeletionTime.Valid {
|
||||
deletion = row.DeletionTime.Int64
|
||||
}
|
||||
entries = append(entries, views.VmTraceEntry{
|
||||
Snapshot: time.Unix(row.SnapshotTime, 0).Local().Format("2006-01-02 15:04:05"),
|
||||
RawTime: row.SnapshotTime,
|
||||
Name: row.Name,
|
||||
VmId: row.VmId,
|
||||
VmUuid: row.VmUuid,
|
||||
Vcenter: row.Vcenter,
|
||||
ResourcePool: row.ResourcePool,
|
||||
VcpuCount: row.VcpuCount,
|
||||
RamGB: row.RamGB,
|
||||
ProvisionedDisk: row.ProvisionedDisk,
|
||||
CreationTime: formatMaybeTime(creation),
|
||||
DeletionTime: formatMaybeTime(deletion),
|
||||
})
|
||||
}
|
||||
chart = buildVmTraceChart(entries)
|
||||
|
||||
if len(entries) > 0 {
|
||||
if lifecycle.CreationTime > 0 {
|
||||
creationLabel = time.Unix(lifecycle.CreationTime, 0).Local().Format("2006-01-02 15:04:05")
|
||||
} else {
|
||||
creationLabel = time.Unix(entries[0].RawTime, 0).Local().Format("2006-01-02 15:04:05")
|
||||
}
|
||||
if lifecycle.DeletionTime > 0 {
|
||||
deletionLabel = time.Unix(lifecycle.DeletionTime, 0).Local().Format("2006-01-02 15:04:05")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
if err := views.VmTracePage(queryLabel, displayQuery, vmID, vmUUID, name, creationLabel, deletionLabel, entries, chart).Render(ctx, w); err != nil {
|
||||
http.Error(w, "Failed to render template", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func buildVmTraceChart(entries []views.VmTraceEntry) views.VmTraceChart {
|
||||
if len(entries) == 0 {
|
||||
return views.VmTraceChart{}
|
||||
}
|
||||
width := 1200.0
|
||||
height := 220.0
|
||||
plotWidth := width - 60.0
|
||||
startX := 40.0
|
||||
maxVal := float64(0)
|
||||
for _, e := range entries {
|
||||
if float64(e.VcpuCount) > maxVal {
|
||||
maxVal = float64(e.VcpuCount)
|
||||
}
|
||||
if float64(e.RamGB) > maxVal {
|
||||
maxVal = float64(e.RamGB)
|
||||
}
|
||||
}
|
||||
if maxVal == 0 {
|
||||
maxVal = 1
|
||||
}
|
||||
stepX := plotWidth
|
||||
if len(entries) > 1 {
|
||||
stepX = plotWidth / float64(len(entries)-1)
|
||||
}
|
||||
scale := height / maxVal
|
||||
var ptsVcpu, ptsRam, ptsTin, ptsBronze, ptsSilver, ptsGold string
|
||||
appendPt := func(s string, x, y float64) string {
|
||||
if s == "" {
|
||||
return fmt.Sprintf("%.1f,%.1f", x, y)
|
||||
}
|
||||
return s + " " + fmt.Sprintf("%.1f,%.1f", x, y)
|
||||
}
|
||||
for i, e := range entries {
|
||||
x := startX + float64(i)*stepX
|
||||
yVcpu := 10 + height - float64(e.VcpuCount)*scale
|
||||
yRam := 10 + height - float64(e.RamGB)*scale
|
||||
ptsVcpu = appendPt(ptsVcpu, x, yVcpu)
|
||||
ptsRam = appendPt(ptsRam, x, yRam)
|
||||
poolY := map[string]float64{
|
||||
"tin": 10 + height - scale*maxVal,
|
||||
"bronze": 10 + height - scale*maxVal*0.9,
|
||||
"silver": 10 + height - scale*maxVal*0.8,
|
||||
"gold": 10 + height - scale*maxVal*0.7,
|
||||
}
|
||||
lower := strings.ToLower(e.ResourcePool)
|
||||
if lower == "tin" {
|
||||
ptsTin = appendPt(ptsTin, x, poolY["tin"])
|
||||
} else {
|
||||
ptsTin = appendPt(ptsTin, x, 10+height)
|
||||
}
|
||||
if lower == "bronze" {
|
||||
ptsBronze = appendPt(ptsBronze, x, poolY["bronze"])
|
||||
} else {
|
||||
ptsBronze = appendPt(ptsBronze, x, 10+height)
|
||||
}
|
||||
if lower == "silver" {
|
||||
ptsSilver = appendPt(ptsSilver, x, poolY["silver"])
|
||||
} else {
|
||||
ptsSilver = appendPt(ptsSilver, x, 10+height)
|
||||
}
|
||||
if lower == "gold" {
|
||||
ptsGold = appendPt(ptsGold, x, poolY["gold"])
|
||||
} else {
|
||||
ptsGold = appendPt(ptsGold, x, 10+height)
|
||||
}
|
||||
}
|
||||
gridY := []float64{}
|
||||
for i := 0; i <= 4; i++ {
|
||||
gridY = append(gridY, 10+float64(i)*(height/4))
|
||||
}
|
||||
gridX := []float64{}
|
||||
for i := 0; i < len(entries); i++ {
|
||||
gridX = append(gridX, startX+float64(i)*stepX)
|
||||
}
|
||||
yTicks := []views.ChartTick{}
|
||||
for i := 0; i <= 4; i++ {
|
||||
val := maxVal * float64(4-i) / 4
|
||||
pos := 10 + float64(i)*(height/4)
|
||||
yTicks = append(yTicks, views.ChartTick{Pos: pos, Label: fmt.Sprintf("%.0f", val)})
|
||||
}
|
||||
xTicks := []views.ChartTick{}
|
||||
maxTicks := 8
|
||||
stepIdx := 1
|
||||
if len(entries) > 1 {
|
||||
stepIdx = (len(entries)-1)/maxTicks + 1
|
||||
}
|
||||
for idx := 0; idx < len(entries); idx += stepIdx {
|
||||
x := startX + float64(idx)*stepX
|
||||
label := time.Unix(entries[idx].RawTime, 0).Local().Format("01-02 15:04")
|
||||
xTicks = append(xTicks, views.ChartTick{Pos: x, Label: label})
|
||||
}
|
||||
if len(entries) > 1 {
|
||||
lastIdx := len(entries) - 1
|
||||
xLast := startX + float64(lastIdx)*stepX
|
||||
labelLast := time.Unix(entries[lastIdx].RawTime, 0).Local().Format("01-02 15:04")
|
||||
if len(xTicks) == 0 || xTicks[len(xTicks)-1].Pos != xLast {
|
||||
xTicks = append(xTicks, views.ChartTick{Pos: xLast, Label: labelLast})
|
||||
}
|
||||
}
|
||||
return views.VmTraceChart{
|
||||
PointsVcpu: ptsVcpu,
|
||||
PointsRam: ptsRam,
|
||||
PointsTin: ptsTin,
|
||||
PointsBronze: ptsBronze,
|
||||
PointsSilver: ptsSilver,
|
||||
PointsGold: ptsGold,
|
||||
Width: int(width),
|
||||
Height: int(height),
|
||||
GridX: gridX,
|
||||
GridY: gridY,
|
||||
XTicks: xTicks,
|
||||
YTicks: yTicks,
|
||||
}
|
||||
}
|
||||
|
||||
func firstNonEmpty(vals ...string) string {
|
||||
for _, v := range vals {
|
||||
if v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func formatMaybeTime(ts int64) string {
|
||||
if ts == 0 {
|
||||
return ""
|
||||
}
|
||||
return time.Unix(ts, 0).Local().Format("2006-01-02 15:04:05")
|
||||
}
|
||||
@@ -25,10 +25,14 @@ func (l *LoggingMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
l.handler.ServeHTTP(w, r)
|
||||
|
||||
requestPath := r.URL.RequestURI()
|
||||
if requestPath == "" {
|
||||
requestPath = r.URL.Path
|
||||
}
|
||||
l.logger.Debug(
|
||||
"Request recieved",
|
||||
slog.String("method", r.Method),
|
||||
slog.String("path", r.URL.Path),
|
||||
slog.String("request", requestPath),
|
||||
slog.String("remote", r.RemoteAddr),
|
||||
slog.Duration("duration", time.Since(start)),
|
||||
)
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
type JSONRawMessage json.RawMessage
|
||||
|
||||
type CloudEventReceived struct {
|
||||
CloudEvent struct {
|
||||
ID string `json:"id"`
|
||||
@@ -53,7 +55,7 @@ type CloudEventReceived struct {
|
||||
Value string `json:"Value"`
|
||||
} `json:"Vm"`
|
||||
} `json:"Vm"`
|
||||
ConfigSpec *json.RawMessage `json:"configSpec"`
|
||||
ConfigSpec *JSONRawMessage `json:"configSpec" swaggertype:"object"`
|
||||
ConfigChanges *ConfigChangesReceived `json:"configChanges"` // Modified to separate struct
|
||||
} `json:"data"`
|
||||
} `json:"cloudEvent"`
|
||||
|
||||
@@ -43,14 +43,15 @@ const docTemplate = `{
|
||||
},
|
||||
"/api/cleanup/updates": {
|
||||
"delete": {
|
||||
"description": "Removes update records that are no longer associated with a VM.",
|
||||
"description": "Deprecated: Removes update records that are no longer associated with a VM.",
|
||||
"produces": [
|
||||
"text/plain"
|
||||
],
|
||||
"tags": [
|
||||
"maintenance"
|
||||
],
|
||||
"summary": "Cleanup updates",
|
||||
"summary": "Cleanup updates (deprecated)",
|
||||
"deprecated": true,
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Cleanup completed",
|
||||
@@ -69,14 +70,15 @@ const docTemplate = `{
|
||||
},
|
||||
"/api/cleanup/vcenter": {
|
||||
"delete": {
|
||||
"description": "Removes all inventory entries associated with a vCenter URL.",
|
||||
"description": "Deprecated: Removes all inventory entries associated with a vCenter URL.",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"maintenance"
|
||||
],
|
||||
"summary": "Cleanup vCenter inventory",
|
||||
"summary": "Cleanup vCenter inventory (deprecated)",
|
||||
"deprecated": true,
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
@@ -159,7 +161,7 @@ const docTemplate = `{
|
||||
},
|
||||
"/api/event/vm/create": {
|
||||
"post": {
|
||||
"description": "Parses a VM create CloudEvent and stores the event data.",
|
||||
"description": "Deprecated: Parses a VM create CloudEvent and stores the event data.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -169,7 +171,8 @@ const docTemplate = `{
|
||||
"tags": [
|
||||
"events"
|
||||
],
|
||||
"summary": "Record VM create event",
|
||||
"summary": "Record VM create event (deprecated)",
|
||||
"deprecated": true,
|
||||
"parameters": [
|
||||
{
|
||||
"description": "CloudEvent payload",
|
||||
@@ -205,7 +208,7 @@ const docTemplate = `{
|
||||
},
|
||||
"/api/event/vm/delete": {
|
||||
"post": {
|
||||
"description": "Parses a VM delete CloudEvent and marks the VM as deleted in inventory.",
|
||||
"description": "Deprecated: Parses a VM delete CloudEvent and marks the VM as deleted in inventory.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -215,7 +218,8 @@ const docTemplate = `{
|
||||
"tags": [
|
||||
"events"
|
||||
],
|
||||
"summary": "Record VM delete event",
|
||||
"summary": "Record VM delete event (deprecated)",
|
||||
"deprecated": true,
|
||||
"parameters": [
|
||||
{
|
||||
"description": "CloudEvent payload",
|
||||
@@ -251,7 +255,7 @@ const docTemplate = `{
|
||||
},
|
||||
"/api/event/vm/modify": {
|
||||
"post": {
|
||||
"description": "Parses a VM modify CloudEvent and creates an update record when relevant changes are detected.",
|
||||
"description": "Deprecated: Parses a VM modify CloudEvent and creates an update record when relevant changes are detected.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -261,7 +265,8 @@ const docTemplate = `{
|
||||
"tags": [
|
||||
"events"
|
||||
],
|
||||
"summary": "Record VM modify event",
|
||||
"summary": "Record VM modify event (deprecated)",
|
||||
"deprecated": true,
|
||||
"parameters": [
|
||||
{
|
||||
"description": "CloudEvent payload",
|
||||
@@ -306,7 +311,7 @@ const docTemplate = `{
|
||||
},
|
||||
"/api/event/vm/move": {
|
||||
"post": {
|
||||
"description": "Parses a VM move CloudEvent and creates an update record.",
|
||||
"description": "Deprecated: Parses a VM move CloudEvent and creates an update record.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -316,7 +321,8 @@ const docTemplate = `{
|
||||
"tags": [
|
||||
"events"
|
||||
],
|
||||
"summary": "Record VM move event",
|
||||
"summary": "Record VM move event (deprecated)",
|
||||
"deprecated": true,
|
||||
"parameters": [
|
||||
{
|
||||
"description": "CloudEvent payload",
|
||||
@@ -584,6 +590,193 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/snapshots/aggregate": {
|
||||
"post": {
|
||||
"description": "Forces regeneration of a daily or monthly summary table for a specified date or month.",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"snapshots"
|
||||
],
|
||||
"summary": "Force snapshot aggregation",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Aggregation type: daily or monthly",
|
||||
"name": "type",
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Daily date (YYYY-MM-DD) or monthly date (YYYY-MM)",
|
||||
"name": "date",
|
||||
"in": "query",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Aggregation complete",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid request",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Server error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/snapshots/hourly/force": {
|
||||
"post": {
|
||||
"description": "Manually trigger an hourly snapshot for all configured vCenters. Requires confirmation text to avoid accidental execution.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"snapshots"
|
||||
],
|
||||
"summary": "Trigger hourly snapshot (manual)",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Confirmation text; must be 'FORCE'",
|
||||
"name": "confirm",
|
||||
"in": "query",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Snapshot started",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid request",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Server error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/snapshots/migrate": {
|
||||
"post": {
|
||||
"description": "Rebuilds the snapshot registry from existing tables and renames hourly tables to epoch-based names.",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"snapshots"
|
||||
],
|
||||
"summary": "Migrate snapshot registry",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Migration results",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Server error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/snapshots/regenerate-hourly-reports": {
|
||||
"post": {
|
||||
"description": "Regenerates XLSX reports for hourly snapshots when the report files are missing or empty.",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"snapshots"
|
||||
],
|
||||
"summary": "Regenerate hourly snapshot reports",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Regeneration summary",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Server error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/metrics": {
|
||||
"get": {
|
||||
"description": "Exposes Prometheus metrics for vctp.",
|
||||
"produces": [
|
||||
"text/plain"
|
||||
],
|
||||
"tags": [
|
||||
"metrics"
|
||||
],
|
||||
"summary": "Prometheus metrics",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Prometheus metrics"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/snapshots/daily": {
|
||||
"get": {
|
||||
"description": "Lists daily summary snapshot tables.",
|
||||
@@ -661,11 +854,273 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/vcenters": {
|
||||
"get": {
|
||||
"description": "Lists all vCenters with recorded snapshot totals.",
|
||||
"produces": [
|
||||
"text/html"
|
||||
],
|
||||
"tags": [
|
||||
"vcenters"
|
||||
],
|
||||
"summary": "List vCenters",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "HTML page",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/vcenters/totals": {
|
||||
"get": {
|
||||
"description": "Shows per-snapshot totals for a vCenter.",
|
||||
"produces": [
|
||||
"text/html"
|
||||
],
|
||||
"tags": [
|
||||
"vcenters"
|
||||
],
|
||||
"summary": "vCenter totals",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "vCenter URL",
|
||||
"name": "vcenter",
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "hourly|daily|monthly (default: hourly)",
|
||||
"name": "type",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Limit results (default 200)",
|
||||
"name": "limit",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "HTML page",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Missing vcenter",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/vm/trace": {
|
||||
"get": {
|
||||
"description": "Shows VM resource history across snapshots, with chart and table.",
|
||||
"produces": [
|
||||
"text/html"
|
||||
],
|
||||
"tags": [
|
||||
"vm"
|
||||
],
|
||||
"summary": "Trace VM history",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "VM ID",
|
||||
"name": "vm_id",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "VM UUID",
|
||||
"name": "vm_uuid",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "VM name",
|
||||
"name": "name",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "HTML page",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Missing identifier",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"models.CloudEventReceived": {
|
||||
"type": "object"
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cloudEvent": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ChainId": {
|
||||
"type": "integer"
|
||||
},
|
||||
"ChangeTag": {
|
||||
"type": "string"
|
||||
},
|
||||
"ComputeResource": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ComputeResource": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Type": {
|
||||
"type": "string"
|
||||
},
|
||||
"Value": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"CreatedTime": {
|
||||
"description": "Modified from time.Time",
|
||||
"type": "string"
|
||||
},
|
||||
"Datacenter": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Datacenter": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Type": {
|
||||
"type": "string"
|
||||
},
|
||||
"Value": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Ds": {},
|
||||
"Dvs": {},
|
||||
"FullFormattedMessage": {
|
||||
"type": "string"
|
||||
},
|
||||
"Host": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Host": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Type": {
|
||||
"type": "string"
|
||||
},
|
||||
"Value": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Key": {
|
||||
"type": "integer"
|
||||
},
|
||||
"Net": {},
|
||||
"NewParent": {
|
||||
"$ref": "#/definitions/models.CloudEventResourcePool"
|
||||
},
|
||||
"OldParent": {
|
||||
"$ref": "#/definitions/models.CloudEventResourcePool"
|
||||
},
|
||||
"SrcTemplate": {
|
||||
"$ref": "#/definitions/models.CloudEventVm"
|
||||
},
|
||||
"Template": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"UserName": {
|
||||
"type": "string"
|
||||
},
|
||||
"Vm": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Name": {
|
||||
"type": "string"
|
||||
},
|
||||
"Vm": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Type": {
|
||||
"type": "string"
|
||||
},
|
||||
"Value": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"configChanges": {
|
||||
"description": "Modified to separate struct",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/models.ConfigChangesReceived"
|
||||
}
|
||||
]
|
||||
},
|
||||
"configSpec": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"type": "string"
|
||||
},
|
||||
"specversion": {
|
||||
"type": "string"
|
||||
},
|
||||
"time": {
|
||||
"description": "Modified from time.Time",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.CloudEventResourcePool": {
|
||||
"type": "object",
|
||||
@@ -705,6 +1160,14 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.ConfigChangesReceived": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"modified": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.ImportReceived": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -32,14 +32,15 @@
|
||||
},
|
||||
"/api/cleanup/updates": {
|
||||
"delete": {
|
||||
"description": "Removes update records that are no longer associated with a VM.",
|
||||
"description": "Deprecated: Removes update records that are no longer associated with a VM.",
|
||||
"produces": [
|
||||
"text/plain"
|
||||
],
|
||||
"tags": [
|
||||
"maintenance"
|
||||
],
|
||||
"summary": "Cleanup updates",
|
||||
"summary": "Cleanup updates (deprecated)",
|
||||
"deprecated": true,
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Cleanup completed",
|
||||
@@ -58,14 +59,15 @@
|
||||
},
|
||||
"/api/cleanup/vcenter": {
|
||||
"delete": {
|
||||
"description": "Removes all inventory entries associated with a vCenter URL.",
|
||||
"description": "Deprecated: Removes all inventory entries associated with a vCenter URL.",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"maintenance"
|
||||
],
|
||||
"summary": "Cleanup vCenter inventory",
|
||||
"summary": "Cleanup vCenter inventory (deprecated)",
|
||||
"deprecated": true,
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
@@ -148,7 +150,7 @@
|
||||
},
|
||||
"/api/event/vm/create": {
|
||||
"post": {
|
||||
"description": "Parses a VM create CloudEvent and stores the event data.",
|
||||
"description": "Deprecated: Parses a VM create CloudEvent and stores the event data.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -158,7 +160,8 @@
|
||||
"tags": [
|
||||
"events"
|
||||
],
|
||||
"summary": "Record VM create event",
|
||||
"summary": "Record VM create event (deprecated)",
|
||||
"deprecated": true,
|
||||
"parameters": [
|
||||
{
|
||||
"description": "CloudEvent payload",
|
||||
@@ -194,7 +197,7 @@
|
||||
},
|
||||
"/api/event/vm/delete": {
|
||||
"post": {
|
||||
"description": "Parses a VM delete CloudEvent and marks the VM as deleted in inventory.",
|
||||
"description": "Deprecated: Parses a VM delete CloudEvent and marks the VM as deleted in inventory.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -204,7 +207,8 @@
|
||||
"tags": [
|
||||
"events"
|
||||
],
|
||||
"summary": "Record VM delete event",
|
||||
"summary": "Record VM delete event (deprecated)",
|
||||
"deprecated": true,
|
||||
"parameters": [
|
||||
{
|
||||
"description": "CloudEvent payload",
|
||||
@@ -240,7 +244,7 @@
|
||||
},
|
||||
"/api/event/vm/modify": {
|
||||
"post": {
|
||||
"description": "Parses a VM modify CloudEvent and creates an update record when relevant changes are detected.",
|
||||
"description": "Deprecated: Parses a VM modify CloudEvent and creates an update record when relevant changes are detected.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -250,7 +254,8 @@
|
||||
"tags": [
|
||||
"events"
|
||||
],
|
||||
"summary": "Record VM modify event",
|
||||
"summary": "Record VM modify event (deprecated)",
|
||||
"deprecated": true,
|
||||
"parameters": [
|
||||
{
|
||||
"description": "CloudEvent payload",
|
||||
@@ -295,7 +300,7 @@
|
||||
},
|
||||
"/api/event/vm/move": {
|
||||
"post": {
|
||||
"description": "Parses a VM move CloudEvent and creates an update record.",
|
||||
"description": "Deprecated: Parses a VM move CloudEvent and creates an update record.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -305,7 +310,8 @@
|
||||
"tags": [
|
||||
"events"
|
||||
],
|
||||
"summary": "Record VM move event",
|
||||
"summary": "Record VM move event (deprecated)",
|
||||
"deprecated": true,
|
||||
"parameters": [
|
||||
{
|
||||
"description": "CloudEvent payload",
|
||||
@@ -573,6 +579,193 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/snapshots/aggregate": {
|
||||
"post": {
|
||||
"description": "Forces regeneration of a daily or monthly summary table for a specified date or month.",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"snapshots"
|
||||
],
|
||||
"summary": "Force snapshot aggregation",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Aggregation type: daily or monthly",
|
||||
"name": "type",
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Daily date (YYYY-MM-DD) or monthly date (YYYY-MM)",
|
||||
"name": "date",
|
||||
"in": "query",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Aggregation complete",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid request",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Server error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/snapshots/hourly/force": {
|
||||
"post": {
|
||||
"description": "Manually trigger an hourly snapshot for all configured vCenters. Requires confirmation text to avoid accidental execution.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"snapshots"
|
||||
],
|
||||
"summary": "Trigger hourly snapshot (manual)",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Confirmation text; must be 'FORCE'",
|
||||
"name": "confirm",
|
||||
"in": "query",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Snapshot started",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid request",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Server error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/snapshots/migrate": {
|
||||
"post": {
|
||||
"description": "Rebuilds the snapshot registry from existing tables and renames hourly tables to epoch-based names.",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"snapshots"
|
||||
],
|
||||
"summary": "Migrate snapshot registry",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Migration results",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Server error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/snapshots/regenerate-hourly-reports": {
|
||||
"post": {
|
||||
"description": "Regenerates XLSX reports for hourly snapshots when the report files are missing or empty.",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"snapshots"
|
||||
],
|
||||
"summary": "Regenerate hourly snapshot reports",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Regeneration summary",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Server error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/metrics": {
|
||||
"get": {
|
||||
"description": "Exposes Prometheus metrics for vctp.",
|
||||
"produces": [
|
||||
"text/plain"
|
||||
],
|
||||
"tags": [
|
||||
"metrics"
|
||||
],
|
||||
"summary": "Prometheus metrics",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Prometheus metrics"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/snapshots/daily": {
|
||||
"get": {
|
||||
"description": "Lists daily summary snapshot tables.",
|
||||
@@ -650,11 +843,273 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/vcenters": {
|
||||
"get": {
|
||||
"description": "Lists all vCenters with recorded snapshot totals.",
|
||||
"produces": [
|
||||
"text/html"
|
||||
],
|
||||
"tags": [
|
||||
"vcenters"
|
||||
],
|
||||
"summary": "List vCenters",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "HTML page",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/vcenters/totals": {
|
||||
"get": {
|
||||
"description": "Shows per-snapshot totals for a vCenter.",
|
||||
"produces": [
|
||||
"text/html"
|
||||
],
|
||||
"tags": [
|
||||
"vcenters"
|
||||
],
|
||||
"summary": "vCenter totals",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "vCenter URL",
|
||||
"name": "vcenter",
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "hourly|daily|monthly (default: hourly)",
|
||||
"name": "type",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Limit results (default 200)",
|
||||
"name": "limit",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "HTML page",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Missing vcenter",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/vm/trace": {
|
||||
"get": {
|
||||
"description": "Shows VM resource history across snapshots, with chart and table.",
|
||||
"produces": [
|
||||
"text/html"
|
||||
],
|
||||
"tags": [
|
||||
"vm"
|
||||
],
|
||||
"summary": "Trace VM history",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "VM ID",
|
||||
"name": "vm_id",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "VM UUID",
|
||||
"name": "vm_uuid",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "VM name",
|
||||
"name": "name",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "HTML page",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Missing identifier",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"models.CloudEventReceived": {
|
||||
"type": "object"
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cloudEvent": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ChainId": {
|
||||
"type": "integer"
|
||||
},
|
||||
"ChangeTag": {
|
||||
"type": "string"
|
||||
},
|
||||
"ComputeResource": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ComputeResource": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Type": {
|
||||
"type": "string"
|
||||
},
|
||||
"Value": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"CreatedTime": {
|
||||
"description": "Modified from time.Time",
|
||||
"type": "string"
|
||||
},
|
||||
"Datacenter": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Datacenter": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Type": {
|
||||
"type": "string"
|
||||
},
|
||||
"Value": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Ds": {},
|
||||
"Dvs": {},
|
||||
"FullFormattedMessage": {
|
||||
"type": "string"
|
||||
},
|
||||
"Host": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Host": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Type": {
|
||||
"type": "string"
|
||||
},
|
||||
"Value": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Key": {
|
||||
"type": "integer"
|
||||
},
|
||||
"Net": {},
|
||||
"NewParent": {
|
||||
"$ref": "#/definitions/models.CloudEventResourcePool"
|
||||
},
|
||||
"OldParent": {
|
||||
"$ref": "#/definitions/models.CloudEventResourcePool"
|
||||
},
|
||||
"SrcTemplate": {
|
||||
"$ref": "#/definitions/models.CloudEventVm"
|
||||
},
|
||||
"Template": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"UserName": {
|
||||
"type": "string"
|
||||
},
|
||||
"Vm": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Name": {
|
||||
"type": "string"
|
||||
},
|
||||
"Vm": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Type": {
|
||||
"type": "string"
|
||||
},
|
||||
"Value": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"configChanges": {
|
||||
"description": "Modified to separate struct",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/models.ConfigChangesReceived"
|
||||
}
|
||||
]
|
||||
},
|
||||
"configSpec": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"type": "string"
|
||||
},
|
||||
"specversion": {
|
||||
"type": "string"
|
||||
},
|
||||
"time": {
|
||||
"description": "Modified from time.Time",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.CloudEventResourcePool": {
|
||||
"type": "object",
|
||||
@@ -694,6 +1149,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.ConfigChangesReceived": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"modified": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.ImportReceived": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -1,5 +1,101 @@
|
||||
definitions:
|
||||
models.CloudEventReceived:
|
||||
properties:
|
||||
cloudEvent:
|
||||
properties:
|
||||
data:
|
||||
properties:
|
||||
ChainId:
|
||||
type: integer
|
||||
ChangeTag:
|
||||
type: string
|
||||
ComputeResource:
|
||||
properties:
|
||||
ComputeResource:
|
||||
properties:
|
||||
Type:
|
||||
type: string
|
||||
Value:
|
||||
type: string
|
||||
type: object
|
||||
Name:
|
||||
type: string
|
||||
type: object
|
||||
CreatedTime:
|
||||
description: Modified from time.Time
|
||||
type: string
|
||||
Datacenter:
|
||||
properties:
|
||||
Datacenter:
|
||||
properties:
|
||||
Type:
|
||||
type: string
|
||||
Value:
|
||||
type: string
|
||||
type: object
|
||||
Name:
|
||||
type: string
|
||||
type: object
|
||||
Ds: {}
|
||||
Dvs: {}
|
||||
FullFormattedMessage:
|
||||
type: string
|
||||
Host:
|
||||
properties:
|
||||
Host:
|
||||
properties:
|
||||
Type:
|
||||
type: string
|
||||
Value:
|
||||
type: string
|
||||
type: object
|
||||
Name:
|
||||
type: string
|
||||
type: object
|
||||
Key:
|
||||
type: integer
|
||||
Net: {}
|
||||
NewParent:
|
||||
$ref: '#/definitions/models.CloudEventResourcePool'
|
||||
OldParent:
|
||||
$ref: '#/definitions/models.CloudEventResourcePool'
|
||||
SrcTemplate:
|
||||
$ref: '#/definitions/models.CloudEventVm'
|
||||
Template:
|
||||
type: boolean
|
||||
UserName:
|
||||
type: string
|
||||
Vm:
|
||||
properties:
|
||||
Name:
|
||||
type: string
|
||||
Vm:
|
||||
properties:
|
||||
Type:
|
||||
type: string
|
||||
Value:
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
configChanges:
|
||||
allOf:
|
||||
- $ref: '#/definitions/models.ConfigChangesReceived'
|
||||
description: Modified to separate struct
|
||||
configSpec:
|
||||
type: object
|
||||
type: object
|
||||
id:
|
||||
type: string
|
||||
source:
|
||||
type: string
|
||||
specversion:
|
||||
type: string
|
||||
time:
|
||||
description: Modified from time.Time
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
models.CloudEventResourcePool:
|
||||
properties:
|
||||
@@ -25,6 +121,11 @@ definitions:
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
models.ConfigChangesReceived:
|
||||
properties:
|
||||
modified:
|
||||
type: string
|
||||
type: object
|
||||
models.ImportReceived:
|
||||
properties:
|
||||
Cluster:
|
||||
@@ -74,7 +175,9 @@ paths:
|
||||
- ui
|
||||
/api/cleanup/updates:
|
||||
delete:
|
||||
description: Removes update records that are no longer associated with a VM.
|
||||
deprecated: true
|
||||
description: 'Deprecated: Removes update records that are no longer associated
|
||||
with a VM.'
|
||||
produces:
|
||||
- text/plain
|
||||
responses:
|
||||
@@ -86,12 +189,14 @@ paths:
|
||||
description: Server error
|
||||
schema:
|
||||
type: string
|
||||
summary: Cleanup updates
|
||||
summary: Cleanup updates (deprecated)
|
||||
tags:
|
||||
- maintenance
|
||||
/api/cleanup/vcenter:
|
||||
delete:
|
||||
description: Removes all inventory entries associated with a vCenter URL.
|
||||
deprecated: true
|
||||
description: 'Deprecated: Removes all inventory entries associated with a vCenter
|
||||
URL.'
|
||||
parameters:
|
||||
- description: vCenter URL
|
||||
in: query
|
||||
@@ -113,7 +218,7 @@ paths:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
summary: Cleanup vCenter inventory
|
||||
summary: Cleanup vCenter inventory (deprecated)
|
||||
tags:
|
||||
- maintenance
|
||||
/api/encrypt:
|
||||
@@ -152,7 +257,9 @@ paths:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Parses a VM create CloudEvent and stores the event data.
|
||||
deprecated: true
|
||||
description: 'Deprecated: Parses a VM create CloudEvent and stores the event
|
||||
data.'
|
||||
parameters:
|
||||
- description: CloudEvent payload
|
||||
in: body
|
||||
@@ -175,14 +282,16 @@ paths:
|
||||
description: Server error
|
||||
schema:
|
||||
type: string
|
||||
summary: Record VM create event
|
||||
summary: Record VM create event (deprecated)
|
||||
tags:
|
||||
- events
|
||||
/api/event/vm/delete:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Parses a VM delete CloudEvent and marks the VM as deleted in inventory.
|
||||
deprecated: true
|
||||
description: 'Deprecated: Parses a VM delete CloudEvent and marks the VM as
|
||||
deleted in inventory.'
|
||||
parameters:
|
||||
- description: CloudEvent payload
|
||||
in: body
|
||||
@@ -205,15 +314,16 @@ paths:
|
||||
description: Server error
|
||||
schema:
|
||||
type: string
|
||||
summary: Record VM delete event
|
||||
summary: Record VM delete event (deprecated)
|
||||
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.
|
||||
deprecated: true
|
||||
description: 'Deprecated: Parses a VM modify CloudEvent and creates an update
|
||||
record when relevant changes are detected.'
|
||||
parameters:
|
||||
- description: CloudEvent payload
|
||||
in: body
|
||||
@@ -242,14 +352,16 @@ paths:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
summary: Record VM modify event
|
||||
summary: Record VM modify event (deprecated)
|
||||
tags:
|
||||
- events
|
||||
/api/event/vm/move:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Parses a VM move CloudEvent and creates an update record.
|
||||
deprecated: true
|
||||
description: 'Deprecated: Parses a VM move CloudEvent and creates an update
|
||||
record.'
|
||||
parameters:
|
||||
- description: CloudEvent payload
|
||||
in: body
|
||||
@@ -278,7 +390,7 @@ paths:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
summary: Record VM move event
|
||||
summary: Record VM move event (deprecated)
|
||||
tags:
|
||||
- events
|
||||
/api/import/vm:
|
||||
@@ -429,6 +541,134 @@ paths:
|
||||
summary: Download updates report
|
||||
tags:
|
||||
- reports
|
||||
/api/snapshots/aggregate:
|
||||
post:
|
||||
description: Forces regeneration of a daily or monthly summary table for a specified
|
||||
date or month.
|
||||
parameters:
|
||||
- description: 'Aggregation type: daily or monthly'
|
||||
in: query
|
||||
name: type
|
||||
required: true
|
||||
type: string
|
||||
- description: Daily date (YYYY-MM-DD) or monthly date (YYYY-MM)
|
||||
in: query
|
||||
name: date
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Aggregation complete
|
||||
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: Force snapshot aggregation
|
||||
tags:
|
||||
- snapshots
|
||||
/api/snapshots/hourly/force:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Manually trigger an hourly snapshot for all configured vCenters.
|
||||
Requires confirmation text to avoid accidental execution.
|
||||
parameters:
|
||||
- description: Confirmation text; must be 'FORCE'
|
||||
in: query
|
||||
name: confirm
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Snapshot started
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
"400":
|
||||
description: Invalid request
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
"500":
|
||||
description: Server error
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
summary: Trigger hourly snapshot (manual)
|
||||
tags:
|
||||
- snapshots
|
||||
/api/snapshots/migrate:
|
||||
post:
|
||||
description: Rebuilds the snapshot registry from existing tables and renames
|
||||
hourly tables to epoch-based names.
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Migration results
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
"500":
|
||||
description: Server error
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
summary: Migrate snapshot registry
|
||||
tags:
|
||||
- snapshots
|
||||
/api/snapshots/regenerate-hourly-reports:
|
||||
post:
|
||||
description: Regenerates XLSX reports for hourly snapshots when the report files
|
||||
are missing or empty.
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Regeneration summary
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
"500":
|
||||
description: Server error
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
summary: Regenerate hourly snapshot reports
|
||||
tags:
|
||||
- snapshots
|
||||
/metrics:
|
||||
get:
|
||||
description: Exposes Prometheus metrics for vctp.
|
||||
produces:
|
||||
- text/plain
|
||||
responses:
|
||||
"200":
|
||||
description: Prometheus metrics
|
||||
summary: Prometheus metrics
|
||||
tags:
|
||||
- metrics
|
||||
/snapshots/daily:
|
||||
get:
|
||||
description: Lists daily summary snapshot tables.
|
||||
@@ -480,4 +720,78 @@ paths:
|
||||
summary: List monthly snapshots
|
||||
tags:
|
||||
- snapshots
|
||||
/vcenters:
|
||||
get:
|
||||
description: Lists all vCenters with recorded snapshot totals.
|
||||
produces:
|
||||
- text/html
|
||||
responses:
|
||||
"200":
|
||||
description: HTML page
|
||||
schema:
|
||||
type: string
|
||||
summary: List vCenters
|
||||
tags:
|
||||
- vcenters
|
||||
/vcenters/totals:
|
||||
get:
|
||||
description: Shows per-snapshot totals for a vCenter.
|
||||
parameters:
|
||||
- description: vCenter URL
|
||||
in: query
|
||||
name: vcenter
|
||||
required: true
|
||||
type: string
|
||||
- description: 'hourly|daily|monthly (default: hourly)'
|
||||
in: query
|
||||
name: type
|
||||
type: string
|
||||
- description: Limit results (default 200)
|
||||
in: query
|
||||
name: limit
|
||||
type: integer
|
||||
produces:
|
||||
- text/html
|
||||
responses:
|
||||
"200":
|
||||
description: HTML page
|
||||
schema:
|
||||
type: string
|
||||
"400":
|
||||
description: Missing vcenter
|
||||
schema:
|
||||
type: string
|
||||
summary: vCenter totals
|
||||
tags:
|
||||
- vcenters
|
||||
/vm/trace:
|
||||
get:
|
||||
description: Shows VM resource history across snapshots, with chart and table.
|
||||
parameters:
|
||||
- description: VM ID
|
||||
in: query
|
||||
name: vm_id
|
||||
type: string
|
||||
- description: VM UUID
|
||||
in: query
|
||||
name: vm_uuid
|
||||
type: string
|
||||
- description: VM name
|
||||
in: query
|
||||
name: name
|
||||
type: string
|
||||
produces:
|
||||
- text/html
|
||||
responses:
|
||||
"200":
|
||||
description: HTML page
|
||||
schema:
|
||||
type: string
|
||||
"400":
|
||||
description: Missing identifier
|
||||
schema:
|
||||
type: string
|
||||
summary: Trace VM history
|
||||
tags:
|
||||
- vm
|
||||
swagger: "2.0"
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"vctp/db"
|
||||
"vctp/dist"
|
||||
"vctp/internal/secrets"
|
||||
@@ -28,7 +30,19 @@ func New(logger *slog.Logger, database db.Database, buildTime string, sha1ver st
|
||||
|
||||
mux := http.NewServeMux()
|
||||
|
||||
reportsDir := settings.Values.Settings.ReportsDir
|
||||
if reportsDir == "" {
|
||||
reportsDir = "/var/lib/vctp/reports"
|
||||
}
|
||||
if err := os.MkdirAll(reportsDir, 0o755); err != nil {
|
||||
logger.Warn("failed to create reports directory", "error", err, "path", reportsDir)
|
||||
}
|
||||
|
||||
mux.Handle("/assets/", middleware.CacheMiddleware(http.FileServer(http.FS(dist.AssetsDir))))
|
||||
mux.Handle("/favicon.ico", middleware.CacheMiddleware(http.FileServer(http.FS(dist.AssetsDir))))
|
||||
mux.Handle("/favicon-16x16.png", middleware.CacheMiddleware(http.FileServer(http.FS(dist.AssetsDir))))
|
||||
mux.Handle("/favicon-32x32.png", middleware.CacheMiddleware(http.FileServer(http.FS(dist.AssetsDir))))
|
||||
mux.Handle("/reports/", http.StripPrefix("/reports/", http.FileServer(http.Dir(filepath.Clean(reportsDir)))))
|
||||
mux.HandleFunc("/", h.Home)
|
||||
mux.HandleFunc("/api/event/vm/create", h.VmCreateEvent)
|
||||
mux.HandleFunc("/api/event/vm/modify", h.VmModifyEvent)
|
||||
@@ -48,6 +62,14 @@ 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("/api/snapshots/aggregate", h.SnapshotAggregateForce)
|
||||
mux.HandleFunc("/api/snapshots/hourly/force", h.SnapshotForceHourly)
|
||||
mux.HandleFunc("/api/snapshots/migrate", h.SnapshotMigrate)
|
||||
mux.HandleFunc("/api/snapshots/regenerate-hourly-reports", h.SnapshotRegenerateHourlyReports)
|
||||
mux.HandleFunc("/vm/trace", h.VmTrace)
|
||||
mux.HandleFunc("/vcenters", h.VcenterList)
|
||||
mux.HandleFunc("/vcenters/totals", h.VcenterTotals)
|
||||
mux.HandleFunc("/metrics", h.Metrics)
|
||||
|
||||
mux.HandleFunc("/snapshots/hourly", h.SnapshotHourlyList)
|
||||
mux.HandleFunc("/snapshots/daily", h.SnapshotDailyList)
|
||||
@@ -61,16 +83,16 @@ func New(logger *slog.Logger, database db.Database, buildTime string, sha1ver st
|
||||
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.Handle("/swagger/", middleware.CacheMiddleware(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) {
|
||||
mux.Handle("/swagger.json", middleware.CacheMiddleware(http.HandlerFunc(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)
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
settings:
|
||||
tenants_to_filter:
|
||||
- "DecomVM"
|
||||
node_charge_clusters:
|
||||
- ".*CMD.*"
|
||||
srm_activeactive_vms:
|
||||
vcenter_addresses:
|
||||
- "https://vc.lab.local/sdk"
|
||||
@@ -8,7 +8,6 @@ SUDOERS_FILE="/etc/sudoers.d/${USER}"
|
||||
# create a group & user if not exists
|
||||
getent group "$GROUP" >/dev/null || groupadd -r "$GROUP"; /bin/true
|
||||
getent passwd "$USER" >/dev/null || useradd -r -g "$GROUP" -m -s /bin/bash -c "vctp service" "$USER"
|
||||
getent passwd tftp >/dev/null || useradd -r -g tftp -s /sbin/nologin tftp
|
||||
|
||||
# create vctp config directory if it doesn't exist
|
||||
[ -d /etc/dtms ] || mkdir -p /etc/dtms
|
||||
@@ -21,6 +20,7 @@ getent passwd tftp >/dev/null || useradd -r -g tftp -s /sbin/nologin tftp
|
||||
|
||||
# create vctp data directory if it doesn't exist
|
||||
[ -d /var/lib/vctp ] || mkdir -p /var/lib/vctp
|
||||
[ -d /var/lib/vctp/reports ] || mkdir -p /var/lib/vctp/reports
|
||||
|
||||
# set user ownership on vctp data directory if not already done
|
||||
[ "$(stat -c "%U" /var/lib/vctp)" = "$USER" ] || chown -R "$USER" /var/lib/vctp
|
||||
|
||||
@@ -1 +1 @@
|
||||
CPE_OPTS='-config /etc/dtms/vctp.yml -log-level info -log-output text'
|
||||
CPE_OPTS='-settings /etc/dtms/vctp.yml'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[Unit]
|
||||
Description=vCTP monitors VMware VM inventory and event data to build chargeback reports
|
||||
Description=vSphere Chargeback Tracking Platform
|
||||
Documentation=https://gitlab.dell.com/
|
||||
ConditionPathExists=/usr/bin/vctp-linux-amd64
|
||||
After=network.target
|
||||
@@ -8,6 +8,7 @@ After=network.target
|
||||
Type=simple
|
||||
EnvironmentFile=/etc/default/vctp
|
||||
User=vctp
|
||||
Group=dtms
|
||||
ExecStart=/usr/bin/vctp-linux-amd64 $CPE_OPTS
|
||||
ExecStartPost=/usr/bin/sleep 3
|
||||
Restart=always
|
||||
|
||||
52
src/vctp.yml
52
src/vctp.yml
@@ -1,25 +1,33 @@
|
||||
settings:
|
||||
data_location: "/var/lib/cbs"
|
||||
kickstart_location: "/var/lib/cbs/ks"
|
||||
database_filename: "/var/lib/cbs/cbs.db"
|
||||
log_level: "info"
|
||||
log_output: "text"
|
||||
database_driver: "sqlite"
|
||||
database_url: "/var/lib/vctp/db.sqlite3"
|
||||
reports_dir: /var/lib/vctp/reports
|
||||
bind_ip:
|
||||
bind_port: 443
|
||||
bind_port: 9443
|
||||
bind_disable_tls: false
|
||||
tls_cert_filename: "/etc/dtms/cbs.crt"
|
||||
tls_key_filename: "/etc/dtms/cbs.key"
|
||||
tftp_root_directory: "/var/lib/tftpboot"
|
||||
tftp_images_subdirectory: "images"
|
||||
replacements:
|
||||
omapi:
|
||||
key_name: "OMAPI"
|
||||
key_secret:
|
||||
special_files:
|
||||
ldap_groups:
|
||||
ldap_bind_address: ""
|
||||
ldap_base_dn: ""
|
||||
ldap_trust_cert_file: ""
|
||||
ldap_disable_validation: false
|
||||
ldap_insecure: false
|
||||
auth_token_lifespan_hours: 2
|
||||
auth_api_key: ""
|
||||
|
||||
tls_cert_filename: "/var/lib/vctp/vctp.crt"
|
||||
tls_key_filename: "/var/lib/vctp/vctp.key"
|
||||
vcenter_username: ""
|
||||
vcenter_password: ""
|
||||
vcenter_insecure: false
|
||||
vcenter_event_polling_seconds: 60
|
||||
vcenter_inventory_polling_seconds: 7200
|
||||
vcenter_inventory_snapshot_seconds: 3600
|
||||
vcenter_inventory_aggregate_seconds: 86400
|
||||
hourly_snapshot_concurrency: 0
|
||||
hourly_snapshot_max_age_days: 60
|
||||
daily_snapshot_max_age_months: 12
|
||||
snapshot_cleanup_cron: "30 2 * * *"
|
||||
hourly_snapshot_retry_seconds: 300
|
||||
hourly_snapshot_max_retries: 3
|
||||
hourly_job_timeout_seconds: 1200
|
||||
hourly_snapshot_timeout_seconds: 600
|
||||
daily_job_timeout_seconds: 900
|
||||
monthly_job_timeout_seconds: 1200
|
||||
cleanup_job_timeout_seconds: 600
|
||||
tenants_to_filter:
|
||||
node_charge_clusters:
|
||||
srm_activeactive_vms:
|
||||
vcenter_addresses:
|
||||
|
||||
Reference in New Issue
Block a user