This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -10,6 +10,7 @@
|
|||||||
*.dylib
|
*.dylib
|
||||||
vctp
|
vctp
|
||||||
build/
|
build/
|
||||||
|
settings.yaml
|
||||||
|
|
||||||
# Certificates
|
# Certificates
|
||||||
*.pem
|
*.pem
|
||||||
|
|||||||
32
README.md
32
README.md
@@ -28,27 +28,37 @@ Run `templ generate -path ./components` to generate code based on template files
|
|||||||
## Documentation
|
## Documentation
|
||||||
Run `swag init --exclude "pkg.mod,pkg.build,pkg.tools" -o server/router/docs`
|
Run `swag init --exclude "pkg.mod,pkg.build,pkg.tools" -o server/router/docs`
|
||||||
|
|
||||||
|
#### 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
|
#### Database Configuration
|
||||||
By default the app uses SQLite and creates/opens `db.sqlite3`. You can opt into PostgreSQL
|
By default the app uses SQLite and creates/opens `db.sqlite3`. You can opt into PostgreSQL
|
||||||
by setting environment variables:
|
by updating the settings file:
|
||||||
|
|
||||||
- `DB_DRIVER`: `sqlite` (default) or `postgres`
|
- `settings.database_driver`: `sqlite` (default) or `postgres`
|
||||||
- `DB_URL`: SQLite file path/DSN or PostgreSQL DSN
|
- `settings.database_url`: SQLite file path/DSN or PostgreSQL DSN
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
```shell
|
```yaml
|
||||||
# SQLite (default)
|
settings:
|
||||||
DB_DRIVER=sqlite DB_URL=./db.sqlite3
|
database_driver: sqlite
|
||||||
|
database_url: ./db.sqlite3
|
||||||
|
|
||||||
# PostgreSQL
|
settings:
|
||||||
DB_DRIVER=postgres DB_URL=postgres://user:pass@localhost:5432/vctp?sslmode=disable
|
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
|
PostgreSQL migrations live in `db/migrations_postgres`, while SQLite migrations remain in
|
||||||
`db/migrations`.
|
`db/migrations`.
|
||||||
|
|
||||||
#### Snapshot Retention
|
#### Snapshot Retention
|
||||||
Hourly and daily snapshot table retention can be configured with environment variables:
|
Hourly and daily snapshot table retention can be configured in the settings file:
|
||||||
|
|
||||||
- `HOURLY_SNAPSHOT_MAX_AGE_DAYS` (default: 60)
|
- `settings.hourly_snapshot_max_age_days` (default: 60)
|
||||||
- `DAILY_SNAPSHOT_MAX_AGE_MONTHS` (default: 12)
|
- `settings.daily_snapshot_max_age_months` (default: 12)
|
||||||
|
|||||||
@@ -8,23 +8,25 @@ templ Header() {
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
<meta name="description" content="vCTP API endpoint"/>
|
<meta name="description" content="vCTP API endpoint"/>
|
||||||
<title>vCTP API</title>
|
<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>
|
<script src="/assets/js/htmx@v2.0.2.min.js"></script>
|
||||||
<link href={ "/assets/css/output@" + version.Value + ".css" } rel="stylesheet"/>
|
<link href={ "/assets/css/output@" + version.Value + ".css" } rel="stylesheet"/>
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--web2-blue: #3b82f6;
|
--web2-blue: #1d9bf0;
|
||||||
--web2-cyan: #22d3ee;
|
|
||||||
--web2-slate: #0f172a;
|
--web2-slate: #0f172a;
|
||||||
|
--web2-muted: #64748b;
|
||||||
--web2-card: #ffffff;
|
--web2-card: #ffffff;
|
||||||
--web2-shadow: 0 20px 40px rgba(15, 23, 42, 0.15);
|
--web2-border: #e5e7eb;
|
||||||
--web2-soft-shadow: 0 8px 20px rgba(15, 23, 42, 0.12);
|
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
font-family: "Trebuchet MS", "Lucida Grande", "Verdana", sans-serif;
|
font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif;
|
||||||
color: var(--web2-slate);
|
color: var(--web2-slate);
|
||||||
}
|
}
|
||||||
.web2-bg {
|
.web2-bg {
|
||||||
background: radial-gradient(circle at top left, #e0f2fe 0%, #f8fafc 45%, #e2e8f0 100%);
|
background: #ffffff;
|
||||||
}
|
}
|
||||||
.web2-shell {
|
.web2-shell {
|
||||||
max-width: 1100px;
|
max-width: 1100px;
|
||||||
@@ -32,29 +34,28 @@ templ Header() {
|
|||||||
padding: 2rem 1.5rem 4rem;
|
padding: 2rem 1.5rem 4rem;
|
||||||
}
|
}
|
||||||
.web2-header {
|
.web2-header {
|
||||||
background: linear-gradient(135deg, #0ea5e9 0%, #6366f1 100%);
|
background: var(--web2-card);
|
||||||
color: #fff;
|
border: 1px solid var(--web2-border);
|
||||||
border-radius: 22px;
|
border-radius: 4px;
|
||||||
box-shadow: var(--web2-shadow);
|
|
||||||
padding: 1.5rem 2rem;
|
padding: 1.5rem 2rem;
|
||||||
}
|
}
|
||||||
.web2-card {
|
.web2-card {
|
||||||
background: var(--web2-card);
|
background: var(--web2-card);
|
||||||
border-radius: 18px;
|
border: 1px solid var(--web2-border);
|
||||||
box-shadow: var(--web2-soft-shadow);
|
border-radius: 4px;
|
||||||
padding: 1.5rem 1.75rem;
|
padding: 1.5rem 1.75rem;
|
||||||
}
|
}
|
||||||
.web2-pill {
|
.web2-pill {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.4rem;
|
gap: 0.4rem;
|
||||||
background: rgba(255, 255, 255, 0.2);
|
background: #f8fafc;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.35);
|
border: 1px solid var(--web2-border);
|
||||||
padding: 0.35rem 0.8rem;
|
color: var(--web2-muted);
|
||||||
border-radius: 999px;
|
padding: 0.2rem 0.6rem;
|
||||||
|
border-radius: 3px;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
letter-spacing: 0.02em;
|
letter-spacing: 0.02em;
|
||||||
backdrop-filter: blur(6px);
|
|
||||||
}
|
}
|
||||||
.web2-link {
|
.web2-link {
|
||||||
color: var(--web2-blue);
|
color: var(--web2-blue);
|
||||||
@@ -65,22 +66,24 @@ templ Header() {
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
.web2-button {
|
.web2-button {
|
||||||
background: linear-gradient(180deg, #60a5fa 0%, #2563eb 100%);
|
background: var(--web2-blue);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.45rem 0.9rem;
|
||||||
border-radius: 999px;
|
border-radius: 3px;
|
||||||
box-shadow: 0 6px 16px rgba(37, 99, 235, 0.35);
|
border: 1px solid #1482d0;
|
||||||
|
box-shadow: none;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
.web2-button:hover {
|
.web2-button:hover {
|
||||||
filter: brightness(1.05);
|
background: #1787d4;
|
||||||
}
|
}
|
||||||
.web2-list li {
|
.web2-list li {
|
||||||
background: rgba(255, 255, 255, 0.9);
|
background: #ffffff;
|
||||||
border-radius: 14px;
|
border: 1px solid var(--web2-border);
|
||||||
padding: 0.85rem 1.1rem;
|
border-radius: 3px;
|
||||||
box-shadow: 0 6px 16px rgba(15, 23, 42, 0.08);
|
padding: 0.75rem 1rem;
|
||||||
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -31,20 +31,20 @@ func Header() templ.Component {
|
|||||||
templ_7745c5c3_Var1 = templ.NopComponent
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
}
|
}
|
||||||
ctx = templ.ClearChildren(ctx)
|
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 {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var2 templ.SafeURL
|
var templ_7745c5c3_Var2 templ.SafeURL
|
||||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinURLErrs("/assets/css/output@" + version.Value + ".css")
|
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinURLErrs("/assets/css/output@" + version.Value + ".css")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/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))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
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\"><style>\n\t\t\t:root {\n\t\t\t\t--web2-blue: #1d9bf0;\n\t\t\t\t--web2-slate: #0f172a;\n\t\t\t\t--web2-muted: #64748b;\n\t\t\t\t--web2-card: #ffffff;\n\t\t\t\t--web2-border: #e5e7eb;\n\t\t\t}\n\t\t\tbody {\n\t\t\t\tfont-family: \"Segoe UI\", \"Helvetica Neue\", Arial, 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: #ffffff;\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: var(--web2-card);\n\t\t\t\tborder: 1px solid var(--web2-border);\n\t\t\t\tborder-radius: 4px;\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: 1px solid var(--web2-border);\n\t\t\t\tborder-radius: 4px;\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: #f8fafc;\n\t\t\t\tborder: 1px solid var(--web2-border);\n\t\t\t\tcolor: var(--web2-muted);\n\t\t\t\tpadding: 0.2rem 0.6rem;\n\t\t\t\tborder-radius: 3px;\n\t\t\t\tfont-size: 0.85rem;\n\t\t\t\tletter-spacing: 0.02em;\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: var(--web2-blue);\n\t\t\t\tcolor: #fff;\n\t\t\t\tpadding: 0.45rem 0.9rem;\n\t\t\t\tborder-radius: 3px;\n\t\t\t\tborder: 1px solid #1482d0;\n\t\t\t\tbox-shadow: none;\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\tbackground: #1787d4;\n\t\t\t}\n\t\t\t.web2-list li {\n\t\t\t\tbackground: #ffffff;\n\t\t\t\tborder: 1px solid var(--web2-border);\n\t\t\t\tborder-radius: 3px;\n\t\t\t\tpadding: 0.75rem 1rem;\n\t\t\t\tbox-shadow: none;\n\t\t\t}\n\t\t</style></head>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,10 +20,10 @@ templ Index(info BuildInfo) {
|
|||||||
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<div class="web2-pill">vCTP Console</div>
|
<div class="web2-pill">vCTP Console</div>
|
||||||
<h1 class="mt-3 text-4xl font-bold">Build Intelligence Dashboard</h1>
|
<h1 class="mt-3 text-4xl font-bold">Chargeback Intelligence Dashboard</h1>
|
||||||
<p class="mt-2 text-sm opacity-90">A glossy, snapshot-ready view of what is running.</p>
|
<p class="mt-2 text-sm text-slate-600">Point in time snapshots of consumption.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-wrap gap-3">
|
<div class="flex flex-wrap gap-4">
|
||||||
<a class="web2-button" href="/snapshots/hourly">Hourly Snapshots</a>
|
<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/daily">Daily Snapshots</a>
|
||||||
<a class="web2-button" href="/snapshots/monthly">Monthly Snapshots</a>
|
<a class="web2-button" href="/snapshots/monthly">Monthly Snapshots</a>
|
||||||
|
|||||||
@@ -47,14 +47,14 @@ func Index(info BuildInfo) templ.Component {
|
|||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
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> <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\">")
|
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=\"flex flex-wrap gap-4\"><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=\"/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 {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var2 string
|
var templ_7745c5c3_Var2 string
|
||||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(info.BuildTime)
|
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(info.BuildTime)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/views/index.templ`, Line: 38, Col: 59}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/index.templ`, Line: 38, Col: 59}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -67,7 +67,7 @@ func Index(info BuildInfo) templ.Component {
|
|||||||
var templ_7745c5c3_Var3 string
|
var templ_7745c5c3_Var3 string
|
||||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(info.SHA1Ver)
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(info.SHA1Ver)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/views/index.templ`, Line: 42, Col: 57}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/index.templ`, Line: 42, Col: 57}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -80,7 +80,7 @@ func Index(info BuildInfo) templ.Component {
|
|||||||
var templ_7745c5c3_Var4 string
|
var templ_7745c5c3_Var4 string
|
||||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(info.GoVersion)
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(info.GoVersion)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/views/index.templ`, Line: 46, Col: 59}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/index.templ`, Line: 46, Col: 59}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
|
|||||||
@@ -32,15 +32,15 @@ templ SnapshotListPage(title string, subtitle string, entries []SnapshotEntry) {
|
|||||||
<div>
|
<div>
|
||||||
<div class="web2-pill">Snapshot Library</div>
|
<div class="web2-pill">Snapshot Library</div>
|
||||||
<h1 class="mt-3 text-4xl font-bold">{title}</h1>
|
<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>
|
</div>
|
||||||
<a class="web2-button" href="/">Back to Dashboard</a>
|
<a class="web2-button" href="/">Back to Dashboard</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="web2-card">
|
<section class="web2-card">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between gap-3">
|
||||||
<h2 class="text-lg font-semibold">Available Exports</h2>
|
<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="text-xs uppercase tracking-[0.2em] text-slate-400">{len(entries)} files</span>
|
||||||
</div>
|
</div>
|
||||||
<ul class="mt-6 space-y-3 web2-list">
|
<ul class="mt-6 space-y-3 web2-list">
|
||||||
|
|||||||
@@ -140,33 +140,33 @@ func SnapshotListPage(title string, subtitle string, entries []SnapshotEntry) te
|
|||||||
var templ_7745c5c3_Var5 string
|
var templ_7745c5c3_Var5 string
|
||||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(title)
|
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(title)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/views/snapshots.templ`, Line: 34, Col: 49}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 34, Col: 49}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
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 {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var6 string
|
var templ_7745c5c3_Var6 string
|
||||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(subtitle)
|
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(subtitle)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/views/snapshots.templ`, Line: 35, Col: 51}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 35, Col: 55}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
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\"><h2 class=\"text-lg font-semibold\">Available Exports </h2><span class=\"text-xs uppercase tracking-[0.2em] text-slate-400\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var7 string
|
var templ_7745c5c3_Var7 string
|
||||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(len(entries))
|
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(len(entries))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/views/snapshots.templ`, Line: 44, Col: 83}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 44, Col: 83}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -184,7 +184,7 @@ func SnapshotListPage(title string, subtitle string, entries []SnapshotEntry) te
|
|||||||
var templ_7745c5c3_Var8 string
|
var templ_7745c5c3_Var8 string
|
||||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Label)
|
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Label)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/views/snapshots.templ`, Line: 49, Col: 71}
|
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))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -197,7 +197,7 @@ func SnapshotListPage(title string, subtitle string, entries []SnapshotEntry) te
|
|||||||
var templ_7745c5c3_Var9 templ.SafeURL
|
var templ_7745c5c3_Var9 templ.SafeURL
|
||||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinURLErrs(entry.Link)
|
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinURLErrs(entry.Link)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/views/snapshots.templ`, Line: 50, Col: 45}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 50, Col: 45}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
|
|||||||
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
|
||||||
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
|
||||||
@@ -66,3 +66,10 @@ CREATE TABLE IF NOT EXISTS inventory_history (
|
|||||||
"PreviousResourcePool" TEXT,
|
"PreviousResourcePool" TEXT,
|
||||||
"PreviousProvisionedDisk" REAL
|
"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
|
||||||
|
);
|
||||||
|
|||||||
2
dist/dist.go
vendored
2
dist/dist.go
vendored
@@ -4,5 +4,5 @@ import (
|
|||||||
"embed"
|
"embed"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed all:assets
|
//go:embed all:assets favicon.ico favicon-16x16.png favicon-32x32.png
|
||||||
var AssetsDir embed.FS
|
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 {
|
func startApp() error {
|
||||||
port := getPort()
|
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.Dir = "../"
|
||||||
app.Env = append(
|
app.Env = os.Environ()
|
||||||
os.Environ(),
|
|
||||||
"DB_URL=./test-db.sqlite3",
|
|
||||||
fmt.Sprintf("PORT=%d", port),
|
|
||||||
"LOG_LEVEL=DEBUG",
|
|
||||||
)
|
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
baseUrL, err = url.Parse(fmt.Sprintf("http://localhost:%d", port))
|
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 {
|
if err := os.Remove("../test-db.sqlite3"); err != nil {
|
||||||
log.Fatalf("could not remove test-db.sqlite3: %v", err)
|
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,
|
// beforeEach creates a new context and page for each test,
|
||||||
|
|||||||
19
go.mod
19
go.mod
@@ -1,33 +1,42 @@
|
|||||||
module vctp
|
module vctp
|
||||||
|
|
||||||
go 1.25.0
|
go 1.25.5
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/a-h/templ v0.3.977
|
github.com/a-h/templ v0.3.977
|
||||||
github.com/go-co-op/gocron/v2 v2.19.0
|
github.com/go-co-op/gocron/v2 v2.19.0
|
||||||
github.com/jackc/pgx/v5 v5.8.0
|
github.com/jackc/pgx/v5 v5.8.0
|
||||||
github.com/jmoiron/sqlx v1.4.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/pressly/goose/v3 v3.26.0
|
||||||
|
github.com/swaggo/swag v1.16.6
|
||||||
github.com/vmware/govmomi v0.52.0
|
github.com/vmware/govmomi v0.52.0
|
||||||
github.com/xuri/excelize/v2 v2.10.0
|
github.com/xuri/excelize/v2 v2.10.0
|
||||||
gopkg.in/yaml.v2 v2.4.0
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
modernc.org/sqlite v1.43.0
|
modernc.org/sqlite v1.44.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
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/dustin/go-humanize v1.0.1 // 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/google/uuid v1.6.0 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
github.com/jonboulle/clockwork v0.5.0 // 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/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mfridman/interpolate v0.0.2 // indirect
|
github.com/mfridman/interpolate v0.0.2 // indirect
|
||||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/richardlehane/mscfb v1.0.6 // 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/robfig/cron/v3 v3.0.1 // indirect
|
||||||
github.com/sethvargo/go-retry v0.3.0 // indirect
|
github.com/sethvargo/go-retry v0.3.0 // indirect
|
||||||
github.com/tiendc/go-deepcopy v1.7.2 // indirect
|
github.com/tiendc/go-deepcopy v1.7.2 // indirect
|
||||||
@@ -36,10 +45,12 @@ require (
|
|||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
golang.org/x/crypto v0.47.0 // indirect
|
golang.org/x/crypto v0.47.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // 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/net v0.49.0 // indirect
|
||||||
golang.org/x/sync v0.19.0 // indirect
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
golang.org/x/sys v0.40.0 // indirect
|
golang.org/x/sys v0.40.0 // indirect
|
||||||
golang.org/x/text v0.33.0 // indirect
|
golang.org/x/text v0.33.0 // indirect
|
||||||
|
golang.org/x/tools v0.41.0 // indirect
|
||||||
modernc.org/libc v1.67.4 // indirect
|
modernc.org/libc v1.67.4 // indirect
|
||||||
modernc.org/mathutil v1.7.1 // indirect
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
modernc.org/memory v1.11.0 // indirect
|
modernc.org/memory v1.11.0 // indirect
|
||||||
|
|||||||
47
go.sum
47
go.sum
@@ -1,7 +1,14 @@
|
|||||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
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 h1:kiKAPXTZE2Iaf8JbtM21r54A8bCNsncrfnokZZSrSDg=
|
||||||
github.com/a-h/templ v0.3.977/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo=
|
github.com/a-h/templ v0.3.977/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo=
|
||||||
|
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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
@@ -9,6 +16,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/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 h1:OKf2y6LXPs/BgBI2fl8PxUpNAI1DA9Mg+hSeGOS38OU=
|
||||||
github.com/go-co-op/gocron/v2 v2.19.0/go.mod h1:5lEiCKk1oVJV39Zg7/YG10OnaVrDAV5GGR6O0663k6U=
|
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.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 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||||
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||||
@@ -30,16 +47,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/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 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
|
||||||
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
|
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 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
|
||||||
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
|
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 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
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 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
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=
|
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||||
@@ -48,6 +72,7 @@ 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/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 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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 h1:KJakav68jdH0WDvoAcj8+n61WqOIaPGgH0bJWS6jpmM=
|
||||||
@@ -58,6 +83,8 @@ github.com/richardlehane/mscfb v1.0.6 h1:eN3bvvZCp00bs7Zf52bxNwAx5lJDBK1tCuH19qq
|
|||||||
github.com/richardlehane/mscfb v1.0.6/go.mod h1:pe0+IUIc0AHh0+teNzBlJCtSyZdFOGgV4ZK9bsoV+Jo=
|
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 h1:kNlmACZuwC8ZWPLoJtD+HtZOsKJgYn7gXgUIcRB7dbo=
|
||||||
github.com/richardlehane/msoleps v1.0.5/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
|
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 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
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=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
@@ -66,9 +93,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/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/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.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.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 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
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 h1:Ut2yYR7W9tWjTQitganoIue4UGxZwCcJy3orjrrIj44=
|
||||||
github.com/tiendc/go-deepcopy v1.7.2/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ=
|
github.com/tiendc/go-deepcopy v1.7.2/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ=
|
||||||
github.com/vmware/govmomi v0.52.0 h1:JyxQ1IQdllrY7PJbv2am9mRsv3p9xWlIQ66bv+XnyLw=
|
github.com/vmware/govmomi v0.52.0 h1:JyxQ1IQdllrY7PJbv2am9mRsv3p9xWlIQ66bv+XnyLw=
|
||||||
@@ -91,23 +121,34 @@ golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
|
|||||||
golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
|
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 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
|
||||||
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
|
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 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
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 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
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 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
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 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
||||||
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
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 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
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-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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
|
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
|
||||||
@@ -134,6 +175,8 @@ modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
|||||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||||
modernc.org/sqlite v1.43.0 h1:8YqiFx3G1VhHTXO2Q00bl1Wz9KhS9Q5okwfp9Y97VnA=
|
modernc.org/sqlite v1.43.0 h1:8YqiFx3G1VhHTXO2Q00bl1Wz9KhS9Q5okwfp9Y97VnA=
|
||||||
modernc.org/sqlite v1.43.0/go.mod h1:+VkC6v3pLOAE0A0uVucQEcbVW0I5nHCeDaBf+DpsQT8=
|
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 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
|
|||||||
@@ -14,6 +14,12 @@ import (
|
|||||||
"github.com/xuri/excelize/v2"
|
"github.com/xuri/excelize/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type SnapshotRecord struct {
|
||||||
|
TableName string
|
||||||
|
SnapshotTime time.Time
|
||||||
|
SnapshotType string
|
||||||
|
}
|
||||||
|
|
||||||
func ListTablesByPrefix(ctx context.Context, database db.Database, prefix string) ([]string, error) {
|
func ListTablesByPrefix(ctx context.Context, database db.Database, prefix string) ([]string, error) {
|
||||||
dbConn := database.DB()
|
dbConn := database.DB()
|
||||||
driver := strings.ToLower(dbConn.DriverName())
|
driver := strings.ToLower(dbConn.DriverName())
|
||||||
@@ -59,26 +65,139 @@ ORDER BY tablename DESC
|
|||||||
return tables, rows.Err()
|
return tables, rows.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
func FormatSnapshotLabel(prefix string, tableName string) (string, bool) {
|
func EnsureSnapshotRegistry(ctx context.Context, database db.Database) error {
|
||||||
if !strings.HasPrefix(tableName, prefix) {
|
dbConn := database.DB()
|
||||||
return "", false
|
driver := strings.ToLower(dbConn.DriverName())
|
||||||
|
switch driver {
|
||||||
|
case "sqlite":
|
||||||
|
_, err := dbConn.ExecContext(ctx, `
|
||||||
|
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
|
||||||
|
)
|
||||||
|
`)
|
||||||
|
return err
|
||||||
|
case "pgx", "postgres":
|
||||||
|
_, err := dbConn.ExecContext(ctx, `
|
||||||
|
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
|
||||||
|
)
|
||||||
|
`)
|
||||||
|
return err
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported driver for snapshot registry: %s", driver)
|
||||||
}
|
}
|
||||||
suffix := strings.TrimPrefix(tableName, prefix)
|
}
|
||||||
switch prefix {
|
|
||||||
case "inventory_daily_":
|
func RegisterSnapshot(ctx context.Context, database db.Database, snapshotType string, tableName string, snapshotTime time.Time) error {
|
||||||
if t, err := time.Parse("20060102", suffix); err == nil {
|
if snapshotType == "" || tableName == "" {
|
||||||
return t.Format("2006-01-02"), true
|
return fmt.Errorf("snapshot type or table name is empty")
|
||||||
}
|
}
|
||||||
case "inventory_daily_summary_":
|
dbConn := database.DB()
|
||||||
if t, err := time.Parse("20060102", suffix); err == nil {
|
driver := strings.ToLower(dbConn.DriverName())
|
||||||
return t.Format("2006-01-02"), true
|
switch driver {
|
||||||
}
|
case "sqlite":
|
||||||
case "inventory_monthly_summary_":
|
_, err := dbConn.ExecContext(ctx, `
|
||||||
if t, err := time.Parse("200601", suffix); err == nil {
|
INSERT OR IGNORE INTO snapshot_registry (snapshot_type, table_name, snapshot_time)
|
||||||
return t.Format("2006-01"), true
|
VALUES (?, ?, ?)
|
||||||
}
|
`, snapshotType, tableName, snapshotTime.Unix())
|
||||||
|
return err
|
||||||
|
case "pgx", "postgres":
|
||||||
|
_, err := dbConn.ExecContext(ctx, `
|
||||||
|
INSERT INTO snapshot_registry (snapshot_type, table_name, snapshot_time)
|
||||||
|
VALUES ($1, $2, $3)
|
||||||
|
ON CONFLICT (table_name) DO NOTHING
|
||||||
|
`, snapshotType, tableName, snapshotTime.Unix())
|
||||||
|
return err
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported driver for snapshot registry: %s", driver)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteSnapshotRecord(ctx context.Context, database db.Database, tableName string) error {
|
||||||
|
if tableName == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
dbConn := database.DB()
|
||||||
|
driver := strings.ToLower(dbConn.DriverName())
|
||||||
|
switch driver {
|
||||||
|
case "sqlite":
|
||||||
|
_, err := dbConn.ExecContext(ctx, `DELETE FROM snapshot_registry WHERE table_name = ?`, tableName)
|
||||||
|
return err
|
||||||
|
case "pgx", "postgres":
|
||||||
|
_, err := dbConn.ExecContext(ctx, `DELETE FROM snapshot_registry WHERE table_name = $1`, tableName)
|
||||||
|
return err
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported driver for snapshot registry: %s", driver)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListSnapshots(ctx context.Context, database db.Database, snapshotType string) ([]SnapshotRecord, error) {
|
||||||
|
dbConn := database.DB()
|
||||||
|
driver := strings.ToLower(dbConn.DriverName())
|
||||||
|
|
||||||
|
var rows *sqlx.Rows
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch driver {
|
||||||
|
case "sqlite":
|
||||||
|
rows, err = dbConn.QueryxContext(ctx, `
|
||||||
|
SELECT table_name, snapshot_time, snapshot_type
|
||||||
|
FROM snapshot_registry
|
||||||
|
WHERE snapshot_type = ?
|
||||||
|
ORDER BY snapshot_time DESC, table_name DESC
|
||||||
|
`, snapshotType)
|
||||||
|
case "pgx", "postgres":
|
||||||
|
rows, err = dbConn.QueryxContext(ctx, `
|
||||||
|
SELECT table_name, snapshot_time, snapshot_type
|
||||||
|
FROM snapshot_registry
|
||||||
|
WHERE snapshot_type = $1
|
||||||
|
ORDER BY snapshot_time DESC, table_name DESC
|
||||||
|
`, snapshotType)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported driver for listing snapshots: %s", driver)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
records := make([]SnapshotRecord, 0)
|
||||||
|
for rows.Next() {
|
||||||
|
var (
|
||||||
|
tableName string
|
||||||
|
snapshotTime int64
|
||||||
|
recordType string
|
||||||
|
)
|
||||||
|
if err := rows.Scan(&tableName, &snapshotTime, &recordType); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
records = append(records, SnapshotRecord{
|
||||||
|
TableName: tableName,
|
||||||
|
SnapshotTime: time.Unix(snapshotTime, 0),
|
||||||
|
SnapshotType: recordType,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return records, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func FormatSnapshotLabel(snapshotType string, snapshotTime time.Time, tableName string) string {
|
||||||
|
switch snapshotType {
|
||||||
|
case "hourly":
|
||||||
|
return snapshotTime.Format("2006-01-02 15:00")
|
||||||
|
case "daily":
|
||||||
|
return snapshotTime.Format("2006-01-02")
|
||||||
|
case "monthly":
|
||||||
|
return snapshotTime.Format("2006-01")
|
||||||
|
default:
|
||||||
|
return tableName
|
||||||
}
|
}
|
||||||
return "", false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateTableReport(logger *slog.Logger, Database db.Database, ctx context.Context, tableName string) ([]byte, error) {
|
func CreateTableReport(logger *slog.Logger, Database db.Database, ctx context.Context, tableName string) ([]byte, error) {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"vctp/internal/utils"
|
"vctp/internal/utils"
|
||||||
|
|
||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v2"
|
||||||
@@ -19,10 +20,29 @@ type Settings struct {
|
|||||||
// SettingsYML struct holds various runtime data that is too cumbersome to specify via command line, eg replacement properties
|
// SettingsYML struct holds various runtime data that is too cumbersome to specify via command line, eg replacement properties
|
||||||
type SettingsYML struct {
|
type SettingsYML struct {
|
||||||
Settings struct {
|
Settings struct {
|
||||||
TenantsToFilter []string `yaml:"tenants_to_filter"`
|
LogLevel string `yaml:"log_level"`
|
||||||
NodeChargeClusters []string `yaml:"node_charge_clusters"`
|
LogOutput string `yaml:"log_output"`
|
||||||
SrmActiveActiveVms []string `yaml:"srm_activeactive_vms"`
|
DatabaseDriver string `yaml:"database_driver"`
|
||||||
VcenterAddresses []string `yaml:"vcenter_addresses"`
|
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"`
|
||||||
|
HourlySnapshotMaxAgeDays int `yaml:"hourly_snapshot_max_age_days"`
|
||||||
|
DailySnapshotMaxAgeMonths int `yaml:"daily_snapshot_max_age_months"`
|
||||||
|
SnapshotCleanupCron string `yaml:"snapshot_cleanup_cron"`
|
||||||
|
TenantsToFilter []string `yaml:"tenants_to_filter"`
|
||||||
|
NodeChargeClusters []string `yaml:"node_charge_clusters"`
|
||||||
|
SrmActiveActiveVms []string `yaml:"srm_activeactive_vms"`
|
||||||
|
VcenterAddresses []string `yaml:"vcenter_addresses"`
|
||||||
} `yaml:"settings"`
|
} `yaml:"settings"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,3 +85,49 @@ func (s *Settings) ReadYMLSettings() error {
|
|||||||
|
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ import (
|
|||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
"vctp/db/queries"
|
"vctp/db/queries"
|
||||||
@@ -33,8 +31,8 @@ type inventorySnapshotRow struct {
|
|||||||
Cluster sql.NullString
|
Cluster sql.NullString
|
||||||
Folder sql.NullString
|
Folder sql.NullString
|
||||||
ProvisionedDisk sql.NullFloat64
|
ProvisionedDisk sql.NullFloat64
|
||||||
InitialVcpus sql.NullInt64
|
VcpuCount sql.NullInt64
|
||||||
InitialRam sql.NullInt64
|
RamGB sql.NullInt64
|
||||||
IsTemplate string
|
IsTemplate string
|
||||||
PoweredOn string
|
PoweredOn string
|
||||||
SrmPlaceholder string
|
SrmPlaceholder string
|
||||||
@@ -46,7 +44,7 @@ type inventorySnapshotRow struct {
|
|||||||
// RunVcenterSnapshotHourly records hourly inventory snapshots into a daily table.
|
// RunVcenterSnapshotHourly records hourly inventory snapshots into a daily table.
|
||||||
func (c *CronTask) RunVcenterSnapshotHourly(ctx context.Context, logger *slog.Logger) error {
|
func (c *CronTask) RunVcenterSnapshotHourly(ctx context.Context, logger *slog.Logger) error {
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
tableName, err := dailyInventoryTableName(startTime)
|
tableName, err := hourlyInventoryTableName(startTime)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -55,6 +53,12 @@ func (c *CronTask) RunVcenterSnapshotHourly(ctx context.Context, logger *slog.Lo
|
|||||||
if err := ensureDailyInventoryTable(ctx, dbConn, tableName); err != nil {
|
if err := ensureDailyInventoryTable(ctx, dbConn, tableName); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := report.EnsureSnapshotRegistry(ctx, c.Database); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := report.RegisterSnapshot(ctx, c.Database, "hourly", tableName, startTime); err != nil {
|
||||||
|
c.Logger.Warn("failed to register hourly snapshot", "error", err, "table", tableName)
|
||||||
|
}
|
||||||
|
|
||||||
// reload settings in case vcenter list has changed
|
// reload settings in case vcenter list has changed
|
||||||
c.Settings.ReadYMLSettings()
|
c.Settings.ReadYMLSettings()
|
||||||
@@ -62,7 +66,10 @@ func (c *CronTask) RunVcenterSnapshotHourly(ctx context.Context, logger *slog.Lo
|
|||||||
for _, url := range c.Settings.Values.Settings.VcenterAddresses {
|
for _, url := range c.Settings.Values.Settings.VcenterAddresses {
|
||||||
c.Logger.Debug("connecting to vcenter for hourly snapshot", "url", url)
|
c.Logger.Debug("connecting to vcenter for hourly snapshot", "url", url)
|
||||||
vc := vcenter.New(c.Logger, c.VcCreds)
|
vc := vcenter.New(c.Logger, c.VcCreds)
|
||||||
vc.Login(url)
|
if err := vc.Login(url); err != nil {
|
||||||
|
c.Logger.Error("unable to connect to vcenter for hourly snapshot", "error", err, "url", url)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
vcVms, err := vc.GetAllVmReferences()
|
vcVms, err := vc.GetAllVmReferences()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -70,6 +77,10 @@ func (c *CronTask) RunVcenterSnapshotHourly(ctx context.Context, logger *slog.Lo
|
|||||||
vc.Logout()
|
vc.Logout()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
canDetectMissing := len(vcVms) > 0
|
||||||
|
if !canDetectMissing {
|
||||||
|
c.Logger.Warn("no VMs returned from vcenter; skipping missing VM detection", "url", url)
|
||||||
|
}
|
||||||
|
|
||||||
inventoryRows, err := c.Database.Queries().GetInventoryByVcenter(ctx, url)
|
inventoryRows, err := c.Database.Queries().GetInventoryByVcenter(ctx, url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -116,8 +127,8 @@ func (c *CronTask) RunVcenterSnapshotHourly(ctx context.Context, logger *slog.Lo
|
|||||||
presentSnapshots[vm.Reference().Value] = row
|
presentSnapshots[vm.Reference().Value] = row
|
||||||
|
|
||||||
totals.VmCount++
|
totals.VmCount++
|
||||||
totals.VcpuTotal += nullInt64ToInt(row.InitialVcpus)
|
totals.VcpuTotal += nullInt64ToInt(row.VcpuCount)
|
||||||
totals.RamTotal += nullInt64ToInt(row.InitialRam)
|
totals.RamTotal += nullInt64ToInt(row.RamGB)
|
||||||
totals.DiskTotal += nullFloat64ToFloat(row.ProvisionedDisk)
|
totals.DiskTotal += nullFloat64ToFloat(row.ProvisionedDisk)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,7 +138,15 @@ func (c *CronTask) RunVcenterSnapshotHourly(ctx context.Context, logger *slog.Lo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !canDetectMissing {
|
||||||
|
vc.Logout()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
for _, inv := range inventoryRows {
|
for _, inv := range inventoryRows {
|
||||||
|
if strings.HasPrefix(inv.Name, "vCLS-") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
vmID := inv.VmId.String
|
vmID := inv.VmId.String
|
||||||
if vmID != "" {
|
if vmID != "" {
|
||||||
if _, ok := presentSnapshots[vmID]; ok {
|
if _, ok := presentSnapshots[vmID]; ok {
|
||||||
@@ -137,6 +156,17 @@ func (c *CronTask) RunVcenterSnapshotHourly(ctx context.Context, logger *slog.Lo
|
|||||||
|
|
||||||
row := snapshotFromInventory(inv, startTime)
|
row := snapshotFromInventory(inv, startTime)
|
||||||
row.IsPresent = "FALSE"
|
row.IsPresent = "FALSE"
|
||||||
|
if !row.DeletionTime.Valid {
|
||||||
|
deletionTime := startTime.Unix()
|
||||||
|
row.DeletionTime = sql.NullInt64{Int64: deletionTime, Valid: true}
|
||||||
|
if err := c.Database.Queries().InventoryMarkDeleted(ctx, queries.InventoryMarkDeletedParams{
|
||||||
|
DeletionTime: row.DeletionTime,
|
||||||
|
VmId: inv.VmId,
|
||||||
|
DatacenterName: inv.Datacenter,
|
||||||
|
}); err != nil {
|
||||||
|
c.Logger.Warn("failed to mark inventory record deleted", "error", err, "vm_id", row.VmId.String)
|
||||||
|
}
|
||||||
|
}
|
||||||
if err := insertDailyInventoryRow(ctx, dbConn, tableName, row); err != nil {
|
if err := insertDailyInventoryRow(ctx, dbConn, tableName, row); err != nil {
|
||||||
c.Logger.Error("failed to insert missing VM snapshot", "error", err, "vm_id", row.VmId.String)
|
c.Logger.Error("failed to insert missing VM snapshot", "error", err, "vm_id", row.VmId.String)
|
||||||
}
|
}
|
||||||
@@ -148,7 +178,7 @@ func (c *CronTask) RunVcenterSnapshotHourly(ctx context.Context, logger *slog.Lo
|
|||||||
"vcenter", url,
|
"vcenter", url,
|
||||||
"vm_count", totals.VmCount,
|
"vm_count", totals.VmCount,
|
||||||
"vcpu_total", totals.VcpuTotal,
|
"vcpu_total", totals.VcpuTotal,
|
||||||
"ram_total_mb", totals.RamTotal,
|
"ram_total_gb", totals.RamTotal,
|
||||||
"disk_total_gb", totals.DiskTotal,
|
"disk_total_gb", totals.DiskTotal,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -160,7 +190,7 @@ func (c *CronTask) RunVcenterSnapshotHourly(ctx context.Context, logger *slog.Lo
|
|||||||
// RunVcenterDailyAggregate summarizes hourly snapshots into a daily summary table.
|
// RunVcenterDailyAggregate summarizes hourly snapshots into a daily summary table.
|
||||||
func (c *CronTask) RunVcenterDailyAggregate(ctx context.Context, logger *slog.Logger) error {
|
func (c *CronTask) RunVcenterDailyAggregate(ctx context.Context, logger *slog.Logger) error {
|
||||||
targetTime := time.Now().Add(-time.Minute)
|
targetTime := time.Now().Add(-time.Minute)
|
||||||
sourceTable, err := dailyInventoryTableName(targetTime)
|
sourceTable, err := hourlyInventoryTableName(targetTime)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -173,6 +203,9 @@ func (c *CronTask) RunVcenterDailyAggregate(ctx context.Context, logger *slog.Lo
|
|||||||
if err := ensureDailySummaryTable(ctx, dbConn, summaryTable); err != nil {
|
if err := ensureDailySummaryTable(ctx, dbConn, summaryTable); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := report.EnsureSnapshotRegistry(ctx, c.Database); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
currentTotals, err := snapshotTotalsForTable(ctx, dbConn, sourceTable)
|
currentTotals, err := snapshotTotalsForTable(ctx, dbConn, sourceTable)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -182,12 +215,12 @@ func (c *CronTask) RunVcenterDailyAggregate(ctx context.Context, logger *slog.Lo
|
|||||||
"table", sourceTable,
|
"table", sourceTable,
|
||||||
"vm_count", currentTotals.VmCount,
|
"vm_count", currentTotals.VmCount,
|
||||||
"vcpu_total", currentTotals.VcpuTotal,
|
"vcpu_total", currentTotals.VcpuTotal,
|
||||||
"ram_total_mb", currentTotals.RamTotal,
|
"ram_total_gb", currentTotals.RamTotal,
|
||||||
"disk_total_gb", currentTotals.DiskTotal,
|
"disk_total_gb", currentTotals.DiskTotal,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
prevTable, _ := dailyInventoryTableName(targetTime.AddDate(0, 0, -1))
|
prevTable, _ := hourlyInventoryTableName(targetTime.AddDate(0, 0, -1))
|
||||||
if prevTable != "" && tableExists(ctx, dbConn, prevTable) {
|
if prevTable != "" && tableExists(ctx, dbConn, prevTable) {
|
||||||
prevTotals, err := snapshotTotalsForTable(ctx, dbConn, prevTable)
|
prevTotals, err := snapshotTotalsForTable(ctx, dbConn, prevTable)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -198,7 +231,7 @@ func (c *CronTask) RunVcenterDailyAggregate(ctx context.Context, logger *slog.Lo
|
|||||||
"previous_table", prevTable,
|
"previous_table", prevTable,
|
||||||
"vm_delta", currentTotals.VmCount-prevTotals.VmCount,
|
"vm_delta", currentTotals.VmCount-prevTotals.VmCount,
|
||||||
"vcpu_delta", currentTotals.VcpuTotal-prevTotals.VcpuTotal,
|
"vcpu_delta", currentTotals.VcpuTotal-prevTotals.VcpuTotal,
|
||||||
"ram_delta_mb", currentTotals.RamTotal-prevTotals.RamTotal,
|
"ram_delta_gb", currentTotals.RamTotal-prevTotals.RamTotal,
|
||||||
"disk_delta_gb", currentTotals.DiskTotal-prevTotals.DiskTotal,
|
"disk_delta_gb", currentTotals.DiskTotal-prevTotals.DiskTotal,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -207,19 +240,19 @@ func (c *CronTask) RunVcenterDailyAggregate(ctx context.Context, logger *slog.Lo
|
|||||||
insertQuery := fmt.Sprintf(`
|
insertQuery := fmt.Sprintf(`
|
||||||
INSERT INTO %s (
|
INSERT INTO %s (
|
||||||
"InventoryId", "Name", "Vcenter", "VmId", "EventKey", "CloudId", "CreationTime", "DeletionTime",
|
"InventoryId", "Name", "Vcenter", "VmId", "EventKey", "CloudId", "CreationTime", "DeletionTime",
|
||||||
"ResourcePool", "VmType", "Datacenter", "Cluster", "Folder", "ProvisionedDisk", "InitialVcpus",
|
"ResourcePool", "VmType", "Datacenter", "Cluster", "Folder", "ProvisionedDisk", "VcpuCount",
|
||||||
"InitialRam", "IsTemplate", "PoweredOn", "SrmPlaceholder", "VmUuid",
|
"RamGB", "IsTemplate", "PoweredOn", "SrmPlaceholder", "VmUuid",
|
||||||
"SamplesPresent", "AvgVcpus", "AvgRam", "AvgDisk", "AvgIsPresent",
|
"SamplesPresent", "AvgVcpuCount", "AvgRamGB", "AvgProvisionedDisk", "AvgIsPresent",
|
||||||
"PoolTinPct", "PoolBronzePct", "PoolSilverPct", "PoolGoldPct"
|
"PoolTinPct", "PoolBronzePct", "PoolSilverPct", "PoolGoldPct"
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
"InventoryId", "Name", "Vcenter", "VmId", "EventKey", "CloudId", "CreationTime", "DeletionTime",
|
"InventoryId", "Name", "Vcenter", "VmId", "EventKey", "CloudId", "CreationTime", "DeletionTime",
|
||||||
"ResourcePool", "VmType", "Datacenter", "Cluster", "Folder", "ProvisionedDisk", "InitialVcpus",
|
"ResourcePool", "VmType", "Datacenter", "Cluster", "Folder", "ProvisionedDisk", "VcpuCount",
|
||||||
"InitialRam", "IsTemplate", "PoweredOn", "SrmPlaceholder", "VmUuid",
|
"RamGB", "IsTemplate", "PoweredOn", "SrmPlaceholder", "VmUuid",
|
||||||
SUM(CASE WHEN "IsPresent" = 'TRUE' THEN 1 ELSE 0 END) AS "SamplesPresent",
|
SUM(CASE WHEN "IsPresent" = 'TRUE' THEN 1 ELSE 0 END) AS "SamplesPresent",
|
||||||
AVG(CASE WHEN "IsPresent" = 'TRUE' AND "InitialVcpus" IS NOT NULL THEN "InitialVcpus" END) AS "AvgVcpus",
|
AVG(CASE WHEN "IsPresent" = 'TRUE' AND "VcpuCount" IS NOT NULL THEN "VcpuCount" END) AS "AvgVcpuCount",
|
||||||
AVG(CASE WHEN "IsPresent" = 'TRUE' AND "InitialRam" IS NOT NULL THEN "InitialRam" END) AS "AvgRam",
|
AVG(CASE WHEN "IsPresent" = 'TRUE' AND "RamGB" IS NOT NULL THEN "RamGB" END) AS "AvgRamGB",
|
||||||
AVG(CASE WHEN "IsPresent" = 'TRUE' AND "ProvisionedDisk" IS NOT NULL THEN "ProvisionedDisk" END) AS "AvgDisk",
|
AVG(CASE WHEN "IsPresent" = 'TRUE' AND "ProvisionedDisk" IS NOT NULL THEN "ProvisionedDisk" END) AS "AvgProvisionedDisk",
|
||||||
AVG(CASE WHEN "IsPresent" = 'TRUE' THEN 1 ELSE 0 END) AS "AvgIsPresent",
|
AVG(CASE WHEN "IsPresent" = 'TRUE' THEN 1 ELSE 0 END) AS "AvgIsPresent",
|
||||||
100.0 * SUM(CASE WHEN "IsPresent" = 'TRUE' AND LOWER("ResourcePool") = 'tin' THEN 1 ELSE 0 END)
|
100.0 * SUM(CASE WHEN "IsPresent" = 'TRUE' AND LOWER("ResourcePool") = 'tin' THEN 1 ELSE 0 END)
|
||||||
/ NULLIF(SUM(CASE WHEN "IsPresent" = 'TRUE' THEN 1 ELSE 0 END), 0) AS "PoolTinPct",
|
/ NULLIF(SUM(CASE WHEN "IsPresent" = 'TRUE' THEN 1 ELSE 0 END), 0) AS "PoolTinPct",
|
||||||
@@ -232,14 +265,17 @@ SELECT
|
|||||||
FROM %s
|
FROM %s
|
||||||
GROUP BY
|
GROUP BY
|
||||||
"InventoryId", "Name", "Vcenter", "VmId", "EventKey", "CloudId", "CreationTime", "DeletionTime",
|
"InventoryId", "Name", "Vcenter", "VmId", "EventKey", "CloudId", "CreationTime", "DeletionTime",
|
||||||
"ResourcePool", "VmType", "Datacenter", "Cluster", "Folder", "ProvisionedDisk", "InitialVcpus",
|
"ResourcePool", "VmType", "Datacenter", "Cluster", "Folder", "ProvisionedDisk", "VcpuCount",
|
||||||
"InitialRam", "IsTemplate", "PoweredOn", "SrmPlaceholder", "VmUuid";
|
"RamGB", "IsTemplate", "PoweredOn", "SrmPlaceholder", "VmUuid";
|
||||||
`, summaryTable, sourceTable)
|
`, summaryTable, sourceTable)
|
||||||
|
|
||||||
if _, err := dbConn.ExecContext(ctx, insertQuery); err != nil {
|
if _, err := dbConn.ExecContext(ctx, insertQuery); err != nil {
|
||||||
c.Logger.Error("failed to aggregate daily inventory", "error", err, "source_table", sourceTable)
|
c.Logger.Error("failed to aggregate daily inventory", "error", err, "source_table", sourceTable)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := report.RegisterSnapshot(ctx, c.Database, "daily", summaryTable, targetTime); err != nil {
|
||||||
|
c.Logger.Warn("failed to register daily snapshot", "error", err, "table", summaryTable)
|
||||||
|
}
|
||||||
|
|
||||||
c.Logger.Debug("Finished daily inventory aggregation", "source_table", sourceTable, "summary_table", summaryTable)
|
c.Logger.Debug("Finished daily inventory aggregation", "source_table", sourceTable, "summary_table", summaryTable)
|
||||||
return nil
|
return nil
|
||||||
@@ -251,7 +287,7 @@ func (c *CronTask) RunVcenterMonthlyAggregate(ctx context.Context, logger *slog.
|
|||||||
firstOfThisMonth := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
|
firstOfThisMonth := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
|
||||||
targetMonth := firstOfThisMonth.AddDate(0, -1, 0)
|
targetMonth := firstOfThisMonth.AddDate(0, -1, 0)
|
||||||
|
|
||||||
monthPrefix := fmt.Sprintf("inventory_daily_%s", targetMonth.Format("200601"))
|
monthPrefix := fmt.Sprintf("inventory_hourly_%s", targetMonth.Format("200601"))
|
||||||
dailyTables, err := report.ListTablesByPrefix(ctx, c.Database, monthPrefix)
|
dailyTables, err := report.ListTablesByPrefix(ctx, c.Database, monthPrefix)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -269,11 +305,14 @@ func (c *CronTask) RunVcenterMonthlyAggregate(ctx context.Context, logger *slog.
|
|||||||
if err := ensureMonthlySummaryTable(ctx, dbConn, monthlyTable); err != nil {
|
if err := ensureMonthlySummaryTable(ctx, dbConn, monthlyTable); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := report.EnsureSnapshotRegistry(ctx, c.Database); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
unionQuery := buildUnionQuery(dailyTables, []string{
|
unionQuery := buildUnionQuery(dailyTables, []string{
|
||||||
`"InventoryId"`, `"Name"`, `"Vcenter"`, `"VmId"`, `"EventKey"`, `"CloudId"`, `"CreationTime"`,
|
`"InventoryId"`, `"Name"`, `"Vcenter"`, `"VmId"`, `"EventKey"`, `"CloudId"`, `"CreationTime"`,
|
||||||
`"DeletionTime"`, `"ResourcePool"`, `"VmType"`, `"Datacenter"`, `"Cluster"`, `"Folder"`,
|
`"DeletionTime"`, `"ResourcePool"`, `"VmType"`, `"Datacenter"`, `"Cluster"`, `"Folder"`,
|
||||||
`"ProvisionedDisk"`, `"InitialVcpus"`, `"InitialRam"`, `"IsTemplate"`, `"PoweredOn"`,
|
`"ProvisionedDisk"`, `"VcpuCount"`, `"RamGB"`, `"IsTemplate"`, `"PoweredOn"`,
|
||||||
`"SrmPlaceholder"`, `"VmUuid"`, `"IsPresent"`,
|
`"SrmPlaceholder"`, `"VmUuid"`, `"IsPresent"`,
|
||||||
})
|
})
|
||||||
if strings.TrimSpace(unionQuery) == "" {
|
if strings.TrimSpace(unionQuery) == "" {
|
||||||
@@ -288,7 +327,7 @@ func (c *CronTask) RunVcenterMonthlyAggregate(ctx context.Context, logger *slog.
|
|||||||
"month", targetMonth.Format("2006-01"),
|
"month", targetMonth.Format("2006-01"),
|
||||||
"vm_count", monthlyTotals.VmCount,
|
"vm_count", monthlyTotals.VmCount,
|
||||||
"vcpu_total", monthlyTotals.VcpuTotal,
|
"vcpu_total", monthlyTotals.VcpuTotal,
|
||||||
"ram_total_mb", monthlyTotals.RamTotal,
|
"ram_total_gb", monthlyTotals.RamTotal,
|
||||||
"disk_total_gb", monthlyTotals.DiskTotal,
|
"disk_total_gb", monthlyTotals.DiskTotal,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -296,18 +335,18 @@ func (c *CronTask) RunVcenterMonthlyAggregate(ctx context.Context, logger *slog.
|
|||||||
insertQuery := fmt.Sprintf(`
|
insertQuery := fmt.Sprintf(`
|
||||||
INSERT INTO %s (
|
INSERT INTO %s (
|
||||||
"InventoryId", "Name", "Vcenter", "VmId", "EventKey", "CloudId", "CreationTime", "DeletionTime",
|
"InventoryId", "Name", "Vcenter", "VmId", "EventKey", "CloudId", "CreationTime", "DeletionTime",
|
||||||
"ResourcePool", "VmType", "Datacenter", "Cluster", "Folder", "ProvisionedDisk", "InitialVcpus",
|
"ResourcePool", "VmType", "Datacenter", "Cluster", "Folder", "ProvisionedDisk", "VcpuCount",
|
||||||
"InitialRam", "IsTemplate", "PoweredOn", "SrmPlaceholder", "VmUuid",
|
"RamGB", "IsTemplate", "PoweredOn", "SrmPlaceholder", "VmUuid",
|
||||||
"AvgVcpus", "AvgRam", "AvgDisk", "AvgIsPresent",
|
"AvgVcpuCount", "AvgRamGB", "AvgProvisionedDisk", "AvgIsPresent",
|
||||||
"PoolTinPct", "PoolBronzePct", "PoolSilverPct", "PoolGoldPct"
|
"PoolTinPct", "PoolBronzePct", "PoolSilverPct", "PoolGoldPct"
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
"InventoryId", "Name", "Vcenter", "VmId", "EventKey", "CloudId", "CreationTime", "DeletionTime",
|
"InventoryId", "Name", "Vcenter", "VmId", "EventKey", "CloudId", "CreationTime", "DeletionTime",
|
||||||
"ResourcePool", "VmType", "Datacenter", "Cluster", "Folder", "ProvisionedDisk", "InitialVcpus",
|
"ResourcePool", "VmType", "Datacenter", "Cluster", "Folder", "ProvisionedDisk", "VcpuCount",
|
||||||
"InitialRam", "IsTemplate", "PoweredOn", "SrmPlaceholder", "VmUuid",
|
"RamGB", "IsTemplate", "PoweredOn", "SrmPlaceholder", "VmUuid",
|
||||||
AVG(CASE WHEN "InitialVcpus" IS NOT NULL THEN "InitialVcpus" END) AS "AvgVcpus",
|
AVG(CASE WHEN "VcpuCount" IS NOT NULL THEN "VcpuCount" END) AS "AvgVcpuCount",
|
||||||
AVG(CASE WHEN "InitialRam" IS NOT NULL THEN "InitialRam" END) AS "AvgRam",
|
AVG(CASE WHEN "RamGB" IS NOT NULL THEN "RamGB" END) AS "AvgRamGB",
|
||||||
AVG(CASE WHEN "ProvisionedDisk" IS NOT NULL THEN "ProvisionedDisk" END) AS "AvgDisk",
|
AVG(CASE WHEN "ProvisionedDisk" IS NOT NULL THEN "ProvisionedDisk" END) AS "AvgProvisionedDisk",
|
||||||
AVG(CASE WHEN "IsPresent" = 'TRUE' THEN 1 ELSE 0 END) AS "AvgIsPresent",
|
AVG(CASE WHEN "IsPresent" = 'TRUE' THEN 1 ELSE 0 END) AS "AvgIsPresent",
|
||||||
100.0 * SUM(CASE WHEN "IsPresent" = 'TRUE' AND LOWER("ResourcePool") = 'tin' THEN 1 ELSE 0 END)
|
100.0 * SUM(CASE WHEN "IsPresent" = 'TRUE' AND LOWER("ResourcePool") = 'tin' THEN 1 ELSE 0 END)
|
||||||
/ NULLIF(SUM(CASE WHEN "IsPresent" = 'TRUE' THEN 1 ELSE 0 END), 0) AS "PoolTinPct",
|
/ NULLIF(SUM(CASE WHEN "IsPresent" = 'TRUE' THEN 1 ELSE 0 END), 0) AS "PoolTinPct",
|
||||||
@@ -322,14 +361,17 @@ FROM (
|
|||||||
) snapshots
|
) snapshots
|
||||||
GROUP BY
|
GROUP BY
|
||||||
"InventoryId", "Name", "Vcenter", "VmId", "EventKey", "CloudId", "CreationTime", "DeletionTime",
|
"InventoryId", "Name", "Vcenter", "VmId", "EventKey", "CloudId", "CreationTime", "DeletionTime",
|
||||||
"ResourcePool", "VmType", "Datacenter", "Cluster", "Folder", "ProvisionedDisk", "InitialVcpus",
|
"ResourcePool", "VmType", "Datacenter", "Cluster", "Folder", "ProvisionedDisk", "VcpuCount",
|
||||||
"InitialRam", "IsTemplate", "PoweredOn", "SrmPlaceholder", "VmUuid";
|
"RamGB", "IsTemplate", "PoweredOn", "SrmPlaceholder", "VmUuid";
|
||||||
`, monthlyTable, unionQuery)
|
`, monthlyTable, unionQuery)
|
||||||
|
|
||||||
if _, err := dbConn.ExecContext(ctx, insertQuery); err != nil {
|
if _, err := dbConn.ExecContext(ctx, insertQuery); err != nil {
|
||||||
c.Logger.Error("failed to aggregate monthly inventory", "error", err, "month", targetMonth.Format("2006-01"))
|
c.Logger.Error("failed to aggregate monthly inventory", "error", err, "month", targetMonth.Format("2006-01"))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := report.RegisterSnapshot(ctx, c.Database, "monthly", monthlyTable, targetMonth); err != nil {
|
||||||
|
c.Logger.Warn("failed to register monthly snapshot", "error", err, "table", monthlyTable)
|
||||||
|
}
|
||||||
|
|
||||||
c.Logger.Debug("Finished monthly inventory aggregation", "summary_table", monthlyTable)
|
c.Logger.Debug("Finished monthly inventory aggregation", "summary_table", monthlyTable)
|
||||||
return nil
|
return nil
|
||||||
@@ -338,15 +380,15 @@ GROUP BY
|
|||||||
// RunSnapshotCleanup drops hourly and daily snapshot tables older than retention.
|
// RunSnapshotCleanup drops hourly and daily snapshot tables older than retention.
|
||||||
func (c *CronTask) RunSnapshotCleanup(ctx context.Context, logger *slog.Logger) error {
|
func (c *CronTask) RunSnapshotCleanup(ctx context.Context, logger *slog.Logger) error {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
hourlyMaxDays := getEnvInt("HOURLY_SNAPSHOT_MAX_AGE_DAYS", 60)
|
hourlyMaxDays := intWithDefault(c.Settings.Values.Settings.HourlySnapshotMaxAgeDays, 60)
|
||||||
dailyMaxMonths := getEnvInt("DAILY_SNAPSHOT_MAX_AGE_MONTHS", 12)
|
dailyMaxMonths := intWithDefault(c.Settings.Values.Settings.DailySnapshotMaxAgeMonths, 12)
|
||||||
|
|
||||||
hourlyCutoff := now.AddDate(0, 0, -hourlyMaxDays)
|
hourlyCutoff := now.AddDate(0, 0, -hourlyMaxDays)
|
||||||
dailyCutoff := now.AddDate(0, -dailyMaxMonths, 0)
|
dailyCutoff := now.AddDate(0, -dailyMaxMonths, 0)
|
||||||
|
|
||||||
dbConn := c.Database.DB()
|
dbConn := c.Database.DB()
|
||||||
|
|
||||||
hourlyTables, err := report.ListTablesByPrefix(ctx, c.Database, "inventory_daily_")
|
hourlyTables, err := report.ListTablesByPrefix(ctx, c.Database, "inventory_hourly_")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -356,7 +398,7 @@ func (c *CronTask) RunSnapshotCleanup(ctx context.Context, logger *slog.Logger)
|
|||||||
if strings.HasPrefix(table, "inventory_daily_summary_") {
|
if strings.HasPrefix(table, "inventory_daily_summary_") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
tableDate, ok := parseSnapshotDate(table, "inventory_daily_", "20060102")
|
tableDate, ok := parseSnapshotDate(table, "inventory_hourly_", "2006010215")
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -365,6 +407,9 @@ func (c *CronTask) RunSnapshotCleanup(ctx context.Context, logger *slog.Logger)
|
|||||||
c.Logger.Error("failed to drop hourly snapshot table", "error", err, "table", table)
|
c.Logger.Error("failed to drop hourly snapshot table", "error", err, "table", table)
|
||||||
} else {
|
} else {
|
||||||
removedHourly++
|
removedHourly++
|
||||||
|
if err := report.DeleteSnapshotRecord(ctx, c.Database, table); err != nil {
|
||||||
|
c.Logger.Warn("failed to remove hourly snapshot registry entry", "error", err, "table", table)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -384,6 +429,9 @@ func (c *CronTask) RunSnapshotCleanup(ctx context.Context, logger *slog.Logger)
|
|||||||
c.Logger.Error("failed to drop daily snapshot table", "error", err, "table", table)
|
c.Logger.Error("failed to drop daily snapshot table", "error", err, "table", table)
|
||||||
} else {
|
} else {
|
||||||
removedDaily++
|
removedDaily++
|
||||||
|
if err := report.DeleteSnapshotRecord(ctx, c.Database, table); err != nil {
|
||||||
|
c.Logger.Warn("failed to remove daily snapshot registry entry", "error", err, "table", table)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -397,8 +445,8 @@ func (c *CronTask) RunSnapshotCleanup(ctx context.Context, logger *slog.Logger)
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func dailyInventoryTableName(t time.Time) (string, error) {
|
func hourlyInventoryTableName(t time.Time) (string, error) {
|
||||||
return safeTableName(fmt.Sprintf("inventory_daily_%s", t.Format("20060102")))
|
return safeTableName(fmt.Sprintf("inventory_hourly_%s", t.Format("2006010215")))
|
||||||
}
|
}
|
||||||
|
|
||||||
func dailySummaryTableName(t time.Time) (string, error) {
|
func dailySummaryTableName(t time.Time) (string, error) {
|
||||||
@@ -435,8 +483,8 @@ func ensureDailyInventoryTable(ctx context.Context, dbConn *sqlx.DB, tableName s
|
|||||||
"Cluster" TEXT,
|
"Cluster" TEXT,
|
||||||
"Folder" TEXT,
|
"Folder" TEXT,
|
||||||
"ProvisionedDisk" REAL,
|
"ProvisionedDisk" REAL,
|
||||||
"InitialVcpus" BIGINT,
|
"VcpuCount" BIGINT,
|
||||||
"InitialRam" BIGINT,
|
"RamGB" BIGINT,
|
||||||
"IsTemplate" TEXT,
|
"IsTemplate" TEXT,
|
||||||
"PoweredOn" TEXT,
|
"PoweredOn" TEXT,
|
||||||
"SrmPlaceholder" TEXT,
|
"SrmPlaceholder" TEXT,
|
||||||
@@ -445,8 +493,14 @@ func ensureDailyInventoryTable(ctx context.Context, dbConn *sqlx.DB, tableName s
|
|||||||
"IsPresent" TEXT NOT NULL
|
"IsPresent" TEXT NOT NULL
|
||||||
);`, tableName)
|
);`, tableName)
|
||||||
|
|
||||||
_, err := dbConn.ExecContext(ctx, ddl)
|
if _, err := dbConn.ExecContext(ctx, ddl); err != nil {
|
||||||
return err
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ensureSnapshotColumns(ctx, dbConn, tableName, []columnDef{
|
||||||
|
{Name: "VcpuCount", Type: "BIGINT"},
|
||||||
|
{Name: "RamGB", Type: "BIGINT"},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func ensureDailySummaryTable(ctx context.Context, dbConn *sqlx.DB, tableName string) error {
|
func ensureDailySummaryTable(ctx context.Context, dbConn *sqlx.DB, tableName string) error {
|
||||||
@@ -465,16 +519,16 @@ func ensureDailySummaryTable(ctx context.Context, dbConn *sqlx.DB, tableName str
|
|||||||
"Cluster" TEXT,
|
"Cluster" TEXT,
|
||||||
"Folder" TEXT,
|
"Folder" TEXT,
|
||||||
"ProvisionedDisk" REAL,
|
"ProvisionedDisk" REAL,
|
||||||
"InitialVcpus" BIGINT,
|
"VcpuCount" BIGINT,
|
||||||
"InitialRam" BIGINT,
|
"RamGB" BIGINT,
|
||||||
"IsTemplate" TEXT,
|
"IsTemplate" TEXT,
|
||||||
"PoweredOn" TEXT,
|
"PoweredOn" TEXT,
|
||||||
"SrmPlaceholder" TEXT,
|
"SrmPlaceholder" TEXT,
|
||||||
"VmUuid" TEXT,
|
"VmUuid" TEXT,
|
||||||
"SamplesPresent" BIGINT NOT NULL,
|
"SamplesPresent" BIGINT NOT NULL,
|
||||||
"AvgVcpus" REAL,
|
"AvgVcpuCount" REAL,
|
||||||
"AvgRam" REAL,
|
"AvgRamGB" REAL,
|
||||||
"AvgDisk" REAL,
|
"AvgProvisionedDisk" REAL,
|
||||||
"AvgIsPresent" REAL,
|
"AvgIsPresent" REAL,
|
||||||
"PoolTinPct" REAL,
|
"PoolTinPct" REAL,
|
||||||
"PoolBronzePct" REAL,
|
"PoolBronzePct" REAL,
|
||||||
@@ -487,9 +541,9 @@ func ensureDailySummaryTable(ctx context.Context, dbConn *sqlx.DB, tableName str
|
|||||||
}
|
}
|
||||||
|
|
||||||
return ensureSnapshotColumns(ctx, dbConn, tableName, []columnDef{
|
return ensureSnapshotColumns(ctx, dbConn, tableName, []columnDef{
|
||||||
{Name: "AvgVcpus", Type: "REAL"},
|
{Name: "AvgVcpuCount", Type: "REAL"},
|
||||||
{Name: "AvgRam", Type: "REAL"},
|
{Name: "AvgRamGB", Type: "REAL"},
|
||||||
{Name: "AvgDisk", Type: "REAL"},
|
{Name: "AvgProvisionedDisk", Type: "REAL"},
|
||||||
{Name: "AvgIsPresent", Type: "REAL"},
|
{Name: "AvgIsPresent", Type: "REAL"},
|
||||||
{Name: "PoolTinPct", Type: "REAL"},
|
{Name: "PoolTinPct", Type: "REAL"},
|
||||||
{Name: "PoolBronzePct", Type: "REAL"},
|
{Name: "PoolBronzePct", Type: "REAL"},
|
||||||
@@ -514,15 +568,15 @@ func ensureMonthlySummaryTable(ctx context.Context, dbConn *sqlx.DB, tableName s
|
|||||||
"Cluster" TEXT,
|
"Cluster" TEXT,
|
||||||
"Folder" TEXT,
|
"Folder" TEXT,
|
||||||
"ProvisionedDisk" REAL,
|
"ProvisionedDisk" REAL,
|
||||||
"InitialVcpus" BIGINT,
|
"VcpuCount" BIGINT,
|
||||||
"InitialRam" BIGINT,
|
"RamGB" BIGINT,
|
||||||
"IsTemplate" TEXT,
|
"IsTemplate" TEXT,
|
||||||
"PoweredOn" TEXT,
|
"PoweredOn" TEXT,
|
||||||
"SrmPlaceholder" TEXT,
|
"SrmPlaceholder" TEXT,
|
||||||
"VmUuid" TEXT,
|
"VmUuid" TEXT,
|
||||||
"AvgVcpus" REAL,
|
"AvgVcpuCount" REAL,
|
||||||
"AvgRam" REAL,
|
"AvgRamGB" REAL,
|
||||||
"AvgDisk" REAL,
|
"AvgProvisionedDisk" REAL,
|
||||||
"AvgIsPresent" REAL,
|
"AvgIsPresent" REAL,
|
||||||
"PoolTinPct" REAL,
|
"PoolTinPct" REAL,
|
||||||
"PoolBronzePct" REAL,
|
"PoolBronzePct" REAL,
|
||||||
@@ -535,7 +589,10 @@ func ensureMonthlySummaryTable(ctx context.Context, dbConn *sqlx.DB, tableName s
|
|||||||
}
|
}
|
||||||
|
|
||||||
return ensureSnapshotColumns(ctx, dbConn, tableName, []columnDef{
|
return ensureSnapshotColumns(ctx, dbConn, tableName, []columnDef{
|
||||||
{Name: "AvgDisk", Type: "REAL"},
|
{Name: "AvgVcpuCount", Type: "REAL"},
|
||||||
|
{Name: "AvgRamGB", Type: "REAL"},
|
||||||
|
{Name: "AvgProvisionedDisk", Type: "REAL"},
|
||||||
|
{Name: "AvgIsPresent", Type: "REAL"},
|
||||||
{Name: "PoolTinPct", Type: "REAL"},
|
{Name: "PoolTinPct", Type: "REAL"},
|
||||||
{Name: "PoolBronzePct", Type: "REAL"},
|
{Name: "PoolBronzePct", Type: "REAL"},
|
||||||
{Name: "PoolSilverPct", Type: "REAL"},
|
{Name: "PoolSilverPct", Type: "REAL"},
|
||||||
@@ -622,8 +679,8 @@ func snapshotTotalsForTable(ctx context.Context, dbConn *sqlx.DB, table string)
|
|||||||
query := fmt.Sprintf(`
|
query := fmt.Sprintf(`
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(DISTINCT "VmId") AS vm_count,
|
COUNT(DISTINCT "VmId") AS vm_count,
|
||||||
COALESCE(SUM(CASE WHEN "InitialVcpus" IS NOT NULL THEN "InitialVcpus" ELSE 0 END), 0) AS vcpu_total,
|
COALESCE(SUM(CASE WHEN "VcpuCount" IS NOT NULL THEN "VcpuCount" ELSE 0 END), 0) AS vcpu_total,
|
||||||
COALESCE(SUM(CASE WHEN "InitialRam" IS NOT NULL THEN "InitialRam" ELSE 0 END), 0) AS ram_total,
|
COALESCE(SUM(CASE WHEN "RamGB" IS NOT NULL THEN "RamGB" ELSE 0 END), 0) AS ram_total,
|
||||||
COALESCE(SUM(CASE WHEN "ProvisionedDisk" IS NOT NULL THEN "ProvisionedDisk" ELSE 0 END), 0) AS disk_total
|
COALESCE(SUM(CASE WHEN "ProvisionedDisk" IS NOT NULL THEN "ProvisionedDisk" ELSE 0 END), 0) AS disk_total
|
||||||
FROM %s
|
FROM %s
|
||||||
WHERE "IsPresent" = 'TRUE'
|
WHERE "IsPresent" = 'TRUE'
|
||||||
@@ -640,8 +697,8 @@ func snapshotTotalsForUnion(ctx context.Context, dbConn *sqlx.DB, unionQuery str
|
|||||||
query := fmt.Sprintf(`
|
query := fmt.Sprintf(`
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(DISTINCT "VmId") AS vm_count,
|
COUNT(DISTINCT "VmId") AS vm_count,
|
||||||
COALESCE(SUM(CASE WHEN "InitialVcpus" IS NOT NULL THEN "InitialVcpus" ELSE 0 END), 0) AS vcpu_total,
|
COALESCE(SUM(CASE WHEN "VcpuCount" IS NOT NULL THEN "VcpuCount" ELSE 0 END), 0) AS vcpu_total,
|
||||||
COALESCE(SUM(CASE WHEN "InitialRam" IS NOT NULL THEN "InitialRam" ELSE 0 END), 0) AS ram_total,
|
COALESCE(SUM(CASE WHEN "RamGB" IS NOT NULL THEN "RamGB" ELSE 0 END), 0) AS ram_total,
|
||||||
COALESCE(SUM(CASE WHEN "ProvisionedDisk" IS NOT NULL THEN "ProvisionedDisk" ELSE 0 END), 0) AS disk_total
|
COALESCE(SUM(CASE WHEN "ProvisionedDisk" IS NOT NULL THEN "ProvisionedDisk" ELSE 0 END), 0) AS disk_total
|
||||||
FROM (
|
FROM (
|
||||||
%s
|
%s
|
||||||
@@ -694,13 +751,8 @@ func nullFloat64ToFloat(value sql.NullFloat64) float64 {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func getEnvInt(key string, fallback int) int {
|
func intWithDefault(value int, fallback int) int {
|
||||||
raw := strings.TrimSpace(os.Getenv(key))
|
if value <= 0 {
|
||||||
if raw == "" {
|
|
||||||
return fallback
|
|
||||||
}
|
|
||||||
value, err := strconv.Atoi(raw)
|
|
||||||
if err != nil || value < 0 {
|
|
||||||
return fallback
|
return fallback
|
||||||
}
|
}
|
||||||
return value
|
return value
|
||||||
@@ -731,8 +783,8 @@ func snapshotFromVM(vmObject *mo.VirtualMachine, vc *vcenter.Vcenter, snapshotTi
|
|||||||
if !vmObject.Config.CreateDate.IsZero() {
|
if !vmObject.Config.CreateDate.IsZero() {
|
||||||
row.CreationTime = sql.NullInt64{Int64: vmObject.Config.CreateDate.Unix(), Valid: true}
|
row.CreationTime = sql.NullInt64{Int64: vmObject.Config.CreateDate.Unix(), Valid: true}
|
||||||
}
|
}
|
||||||
row.InitialVcpus = sql.NullInt64{Int64: int64(vmObject.Config.Hardware.NumCPU), Valid: vmObject.Config.Hardware.NumCPU > 0}
|
row.VcpuCount = sql.NullInt64{Int64: int64(vmObject.Config.Hardware.NumCPU), Valid: vmObject.Config.Hardware.NumCPU > 0}
|
||||||
row.InitialRam = sql.NullInt64{Int64: int64(vmObject.Config.Hardware.MemoryMB), Valid: vmObject.Config.Hardware.MemoryMB > 0}
|
row.RamGB = sql.NullInt64{Int64: int64(vmObject.Config.Hardware.MemoryMB) / 1024, Valid: vmObject.Config.Hardware.MemoryMB > 0}
|
||||||
|
|
||||||
totalDiskBytes := int64(0)
|
totalDiskBytes := int64(0)
|
||||||
for _, device := range vmObject.Config.Hardware.Device {
|
for _, device := range vmObject.Config.Hardware.Device {
|
||||||
@@ -774,11 +826,11 @@ func snapshotFromVM(vmObject *mo.VirtualMachine, vc *vcenter.Vcenter, snapshotTi
|
|||||||
if !row.ProvisionedDisk.Valid {
|
if !row.ProvisionedDisk.Valid {
|
||||||
row.ProvisionedDisk = inv.ProvisionedDisk
|
row.ProvisionedDisk = inv.ProvisionedDisk
|
||||||
}
|
}
|
||||||
if !row.InitialVcpus.Valid {
|
if !row.VcpuCount.Valid {
|
||||||
row.InitialVcpus = inv.InitialVcpus
|
row.VcpuCount = inv.InitialVcpus
|
||||||
}
|
}
|
||||||
if !row.InitialRam.Valid {
|
if !row.RamGB.Valid && inv.InitialRam.Valid {
|
||||||
row.InitialRam = inv.InitialRam
|
row.RamGB = sql.NullInt64{Int64: inv.InitialRam.Int64 / 1024, Valid: inv.InitialRam.Int64 > 0}
|
||||||
}
|
}
|
||||||
if row.IsTemplate == "" {
|
if row.IsTemplate == "" {
|
||||||
row.IsTemplate = boolStringFromInterface(inv.IsTemplate)
|
row.IsTemplate = boolStringFromInterface(inv.IsTemplate)
|
||||||
@@ -837,8 +889,8 @@ func snapshotFromInventory(inv queries.Inventory, snapshotTime time.Time) invent
|
|||||||
Cluster: inv.Cluster,
|
Cluster: inv.Cluster,
|
||||||
Folder: inv.Folder,
|
Folder: inv.Folder,
|
||||||
ProvisionedDisk: inv.ProvisionedDisk,
|
ProvisionedDisk: inv.ProvisionedDisk,
|
||||||
InitialVcpus: inv.InitialVcpus,
|
VcpuCount: inv.InitialVcpus,
|
||||||
InitialRam: inv.InitialRam,
|
RamGB: sql.NullInt64{Int64: inv.InitialRam.Int64 / 1024, Valid: inv.InitialRam.Valid && inv.InitialRam.Int64 > 0},
|
||||||
IsTemplate: boolStringFromInterface(inv.IsTemplate),
|
IsTemplate: boolStringFromInterface(inv.IsTemplate),
|
||||||
PoweredOn: boolStringFromInterface(inv.PoweredOn),
|
PoweredOn: boolStringFromInterface(inv.PoweredOn),
|
||||||
SrmPlaceholder: boolStringFromInterface(inv.SrmPlaceholder),
|
SrmPlaceholder: boolStringFromInterface(inv.SrmPlaceholder),
|
||||||
@@ -851,8 +903,8 @@ func insertDailyInventoryRow(ctx context.Context, dbConn *sqlx.DB, tableName str
|
|||||||
query := fmt.Sprintf(`
|
query := fmt.Sprintf(`
|
||||||
INSERT INTO %s (
|
INSERT INTO %s (
|
||||||
"InventoryId", "Name", "Vcenter", "VmId", "EventKey", "CloudId", "CreationTime", "DeletionTime",
|
"InventoryId", "Name", "Vcenter", "VmId", "EventKey", "CloudId", "CreationTime", "DeletionTime",
|
||||||
"ResourcePool", "VmType", "Datacenter", "Cluster", "Folder", "ProvisionedDisk", "InitialVcpus",
|
"ResourcePool", "VmType", "Datacenter", "Cluster", "Folder", "ProvisionedDisk", "VcpuCount",
|
||||||
"InitialRam", "IsTemplate", "PoweredOn", "SrmPlaceholder", "VmUuid", "SnapshotTime", "IsPresent"
|
"RamGB", "IsTemplate", "PoweredOn", "SrmPlaceholder", "VmUuid", "SnapshotTime", "IsPresent"
|
||||||
)
|
)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
|
||||||
`, tableName)
|
`, tableName)
|
||||||
@@ -874,8 +926,8 @@ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
|
|||||||
row.Cluster,
|
row.Cluster,
|
||||||
row.Folder,
|
row.Folder,
|
||||||
row.ProvisionedDisk,
|
row.ProvisionedDisk,
|
||||||
row.InitialVcpus,
|
row.VcpuCount,
|
||||||
row.InitialRam,
|
row.RamGB,
|
||||||
row.IsTemplate,
|
row.IsTemplate,
|
||||||
row.PoweredOn,
|
row.PoweredOn,
|
||||||
row.SrmPlaceholder,
|
row.SrmPlaceholder,
|
||||||
|
|||||||
@@ -262,6 +262,11 @@ func (c *CronTask) AddVmToInventory(vmObject *mo.VirtualMachine, vc *vcenter.Vce
|
|||||||
return errors.New("can't process empty vm object")
|
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")
|
c.Logger.Debug("found VM")
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -83,6 +83,14 @@ func (c *CronTask) RunVmCheck(ctx context.Context, logger *slog.Logger) error {
|
|||||||
continue
|
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")
|
//c.Logger.Debug("found VM")
|
||||||
srmPlaceholder = "FALSE" // Default assumption
|
srmPlaceholder = "FALSE" // Default assumption
|
||||||
//prettyPrint(vmObject)
|
//prettyPrint(vmObject)
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -30,6 +29,7 @@ type Vcenter struct {
|
|||||||
type VcenterLogin struct {
|
type VcenterLogin struct {
|
||||||
Username string
|
Username string
|
||||||
Password string
|
Password string
|
||||||
|
Insecure bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type VmProperties struct {
|
type VmProperties struct {
|
||||||
@@ -51,13 +51,6 @@ func New(logger *slog.Logger, creds *VcenterLogin) *Vcenter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (v *Vcenter) Login(vUrl string) error {
|
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")
|
|
||||||
|
|
||||||
// Connect to vCenter
|
// Connect to vCenter
|
||||||
u, err := soap.ParseURL(vUrl)
|
u, err := soap.ParseURL(vUrl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -74,11 +67,7 @@ func (v *Vcenter) Login(vUrl string) error {
|
|||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
if insecureString == "true" {
|
c, err := govmomi.NewClient(v.ctx, u, v.credentials.Insecure)
|
||||||
insecure = true
|
|
||||||
}
|
|
||||||
|
|
||||||
c, err := govmomi.NewClient(v.ctx, u, insecure)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
v.Logger.Error("Unable to connect to vCenter", "error", err)
|
v.Logger.Error("Unable to connect to vCenter", "error", err)
|
||||||
return fmt.Errorf("unable to connect to vCenter : %s", err)
|
return fmt.Errorf("unable to connect to vCenter : %s", err)
|
||||||
|
|||||||
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 {
|
func GetLevel() Level {
|
||||||
level := os.Getenv("LOG_LEVEL")
|
return LevelInfo
|
||||||
return ToLevel(level)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Output represents the log output.
|
// 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 {
|
func GetOutput() Output {
|
||||||
output := os.Getenv("LOG_OUTPUT")
|
return OutputText
|
||||||
return ToOutput(output)
|
|
||||||
}
|
}
|
||||||
|
|||||||
209
main.go
209
main.go
@@ -2,8 +2,8 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -19,7 +19,6 @@ import (
|
|||||||
"vctp/server/router"
|
"vctp/server/router"
|
||||||
|
|
||||||
"github.com/go-co-op/gocron/v2"
|
"github.com/go-co-op/gocron/v2"
|
||||||
"github.com/joho/godotenv"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -34,22 +33,29 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Load data from environment file
|
settingsPath := flag.String("settings", "/etc/dtms/vctp.yml", "Path to settings YAML")
|
||||||
envFilename := utils.GetFilePath(".env")
|
flag.Parse()
|
||||||
err := godotenv.Load(envFilename)
|
|
||||||
if err != nil {
|
|
||||||
panic("Error loading .env file")
|
|
||||||
}
|
|
||||||
|
|
||||||
logger := log.New(
|
bootstrapLogger := log.New(log.LevelInfo, log.OutputText)
|
||||||
log.GetLevel(),
|
|
||||||
log.GetOutput(),
|
|
||||||
)
|
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
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
|
// Configure database
|
||||||
dbDriver := os.Getenv("DB_DRIVER")
|
dbDriver := strings.TrimSpace(s.Values.Settings.DatabaseDriver)
|
||||||
if dbDriver == "" {
|
if dbDriver == "" {
|
||||||
dbDriver = "sqlite"
|
dbDriver = "sqlite"
|
||||||
}
|
}
|
||||||
@@ -57,12 +63,12 @@ func main() {
|
|||||||
if normalizedDriver == "" || normalizedDriver == "sqlite3" {
|
if normalizedDriver == "" || normalizedDriver == "sqlite3" {
|
||||||
normalizedDriver = "sqlite"
|
normalizedDriver = "sqlite"
|
||||||
}
|
}
|
||||||
dbURL := os.Getenv("DB_URL")
|
dbURL := strings.TrimSpace(s.Values.Settings.DatabaseURL)
|
||||||
if dbURL == "" && normalizedDriver == "sqlite" {
|
if dbURL == "" && normalizedDriver == "sqlite" {
|
||||||
dbURL = utils.GetFilePath("db.sqlite3")
|
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 {
|
if err != nil {
|
||||||
logger.Error("Failed to create database", "error", err)
|
logger.Error("Failed to create database", "error", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@@ -70,56 +76,36 @@ func main() {
|
|||||||
defer database.Close()
|
defer database.Close()
|
||||||
//defer database.DB().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)
|
logger.Error("failed to migrate database", "error", err)
|
||||||
os.Exit(1)
|
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
|
// Determine bind IP
|
||||||
bindIP := os.Getenv("BIND_IP")
|
bindIP := strings.TrimSpace(s.Values.Settings.BindIP)
|
||||||
if bindIP == "" {
|
if bindIP == "" {
|
||||||
bindIP = utils.GetOutboundIP().String()
|
bindIP = utils.GetOutboundIP().String()
|
||||||
}
|
}
|
||||||
// Determine bind port
|
// Determine bind port
|
||||||
bindPort := os.Getenv("BIND_PORT")
|
bindPort := s.Values.Settings.BindPort
|
||||||
if bindPort == "" {
|
if bindPort == 0 {
|
||||||
bindPort = "9443"
|
bindPort = 9443
|
||||||
}
|
}
|
||||||
bindAddress := fmt.Sprint(bindIP, ":", bindPort)
|
bindAddress := fmt.Sprint(bindIP, ":", bindPort)
|
||||||
//logger.Info("Will listen on address", "ip", bindIP, "port", bindPort)
|
//logger.Info("Will listen on address", "ip", bindIP, "port", bindPort)
|
||||||
|
|
||||||
// Determine bind disable TLS
|
// Determine bind disable TLS
|
||||||
bindDisableTlsEnv := os.Getenv("BIND_DISABLE_TLS")
|
bindDisableTls = s.Values.Settings.BindDisableTLS
|
||||||
if bindDisableTlsEnv == "true" {
|
|
||||||
bindDisableTls = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get file names for TLS cert/key
|
// Get file names for TLS cert/key
|
||||||
tlsCertFilename := os.Getenv("TLS_CERT_FILE")
|
tlsCertFilename := strings.TrimSpace(s.Values.Settings.TLSCertFilename)
|
||||||
if tlsCertFilename != "" {
|
if tlsCertFilename != "" {
|
||||||
tlsCertFilename = utils.GetFilePath(tlsCertFilename)
|
tlsCertFilename = utils.GetFilePath(tlsCertFilename)
|
||||||
} else {
|
} else {
|
||||||
tlsCertFilename = "./cert.pem"
|
tlsCertFilename = "./cert.pem"
|
||||||
}
|
}
|
||||||
|
|
||||||
tlsKeyFilename := os.Getenv("TLS_KEY_FILE")
|
tlsKeyFilename := strings.TrimSpace(s.Values.Settings.TLSKeyFilename)
|
||||||
if tlsKeyFilename != "" {
|
if tlsKeyFilename != "" {
|
||||||
tlsKeyFilename = utils.GetFilePath(tlsKeyFilename)
|
tlsKeyFilename = utils.GetFilePath(tlsKeyFilename)
|
||||||
} else {
|
} else {
|
||||||
@@ -132,9 +118,9 @@ func main() {
|
|||||||
utils.GenerateCerts(tlsCertFilename, tlsKeyFilename)
|
utils.GenerateCerts(tlsCertFilename, tlsKeyFilename)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load vcenter credentials from .env
|
// Load vcenter credentials from serttings, decrypt if required
|
||||||
a := secrets.New(logger, encryptionKey)
|
a := secrets.New(logger, encryptionKey)
|
||||||
vcEp := os.Getenv("VCENTER_PASSWORD")
|
vcEp := strings.TrimSpace(s.Values.Settings.VcenterPassword)
|
||||||
if len(vcEp) == 0 {
|
if len(vcEp) == 0 {
|
||||||
logger.Error("No vcenter password configured")
|
logger.Error("No vcenter password configured")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@@ -143,13 +129,27 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("failed to decrypt vcenter credentials. Assuming un-encrypted", "error", err)
|
logger.Error("failed to decrypt vcenter credentials. Assuming un-encrypted", "error", err)
|
||||||
vcPass = []byte(vcEp)
|
vcPass = []byte(vcEp)
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
//os.Exit(1)
|
//os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
creds := vcenter.VcenterLogin{
|
creds := vcenter.VcenterLogin{
|
||||||
//insecureString := os.Getenv("VCENTER_INSECURE")
|
Username: strings.TrimSpace(s.Values.Settings.VcenterUsername),
|
||||||
Username: os.Getenv("VCENTER_USERNAME"),
|
|
||||||
Password: string(vcPass),
|
Password: string(vcPass),
|
||||||
|
Insecure: s.Values.Settings.VcenterInsecure,
|
||||||
|
}
|
||||||
|
if creds.Username == "" {
|
||||||
|
logger.Error("No vcenter username configured")
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare the task scheduler
|
// Prepare the task scheduler
|
||||||
@@ -167,83 +167,51 @@ func main() {
|
|||||||
VcCreds: &creds,
|
VcCreds: &creds,
|
||||||
}
|
}
|
||||||
|
|
||||||
cronFrequencyString := os.Getenv("VCENTER_EVENT_POLLING_SECONDS")
|
cronFrequency = durationFromSeconds(s.Values.Settings.VcenterEventPollingSeconds, 60)
|
||||||
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)
|
logger.Debug("Setting VM event polling cronjob frequency to", "frequency", cronFrequency)
|
||||||
|
|
||||||
cronInventoryFrequencyString := os.Getenv("VCENTER_INVENTORY_POLLING_SECONDS")
|
cronInvFrequency = durationFromSeconds(s.Values.Settings.VcenterInventoryPollingSeconds, 7200)
|
||||||
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)
|
logger.Debug("Setting VM inventory polling cronjob frequency to", "frequency", cronInvFrequency)
|
||||||
|
|
||||||
cronSnapshotFrequencyString := os.Getenv("VCENTER_INVENTORY_SNAPSHOT_SECONDS")
|
cronSnapshotFrequency = durationFromSeconds(s.Values.Settings.VcenterInventorySnapshotSeconds, 3600)
|
||||||
if cronSnapshotFrequencyString != "" {
|
|
||||||
cronSnapshotFrequency, err = time.ParseDuration(cronSnapshotFrequencyString)
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Can't convert VCENTER_INVENTORY_SNAPSHOT_SECONDS value to time duration. Defaulting to 3600", "value", cronSnapshotFrequencyString, "error", err)
|
|
||||||
cronSnapshotFrequency = time.Hour
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
cronSnapshotFrequency = time.Hour
|
|
||||||
}
|
|
||||||
logger.Debug("Setting VM inventory snapshot cronjob frequency to", "frequency", cronSnapshotFrequency)
|
logger.Debug("Setting VM inventory snapshot cronjob frequency to", "frequency", cronSnapshotFrequency)
|
||||||
|
|
||||||
cronAggregateFrequencyString := os.Getenv("VCENTER_INVENTORY_AGGREGATE_SECONDS")
|
cronAggregateFrequency = durationFromSeconds(s.Values.Settings.VcenterInventoryAggregateSeconds, 86400)
|
||||||
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)
|
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)
|
// start background processing for events stored in events table
|
||||||
job, err := c.NewJob(
|
startsAt := time.Now().Add(time.Second * 10)
|
||||||
gocron.DurationJob(cronFrequency),
|
job, err := c.NewJob(
|
||||||
gocron.NewTask(func() {
|
gocron.DurationJob(cronFrequency),
|
||||||
ct.RunVmCheck(ctx, logger)
|
gocron.NewTask(func() {
|
||||||
}), gocron.WithSingletonMode(gocron.LimitModeReschedule),
|
ct.RunVmCheck(ctx, logger)
|
||||||
gocron.WithStartAt(gocron.WithStartDateTime(startsAt)),
|
}), gocron.WithSingletonMode(gocron.LimitModeReschedule),
|
||||||
)
|
gocron.WithStartAt(gocron.WithStartDateTime(startsAt)),
|
||||||
if err != nil {
|
)
|
||||||
logger.Error("failed to start event processing cron job", "error", err)
|
if err != nil {
|
||||||
os.Exit(1)
|
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)
|
}
|
||||||
|
logger.Debug("Created event processing cron job", "job", job.ID(), "starting_at", startsAt)
|
||||||
|
*/
|
||||||
|
|
||||||
// start background checks of vcenter inventory
|
// start background checks of vcenter inventory
|
||||||
startsAt2 := time.Now().Add(cronInvFrequency)
|
/*
|
||||||
job2, err := c.NewJob(
|
startsAt2 := time.Now().Add(cronInvFrequency)
|
||||||
gocron.DurationJob(cronInvFrequency),
|
job2, err := c.NewJob(
|
||||||
gocron.NewTask(func() {
|
gocron.DurationJob(cronInvFrequency),
|
||||||
ct.RunVcenterPoll(ctx, logger)
|
gocron.NewTask(func() {
|
||||||
}), gocron.WithSingletonMode(gocron.LimitModeReschedule),
|
ct.RunVcenterPoll(ctx, logger)
|
||||||
gocron.WithStartAt(gocron.WithStartDateTime(startsAt2)),
|
}), gocron.WithSingletonMode(gocron.LimitModeReschedule),
|
||||||
)
|
gocron.WithStartAt(gocron.WithStartDateTime(startsAt2)),
|
||||||
if err != nil {
|
)
|
||||||
logger.Error("failed to start vcenter inventory cron job", "error", err)
|
if err != nil {
|
||||||
os.Exit(1)
|
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)
|
}
|
||||||
|
logger.Debug("Created vcenter inventory cron job", "job", job2.ID(), "starting_at", startsAt2)
|
||||||
|
*/
|
||||||
|
|
||||||
startsAt3 := time.Now().Add(cronSnapshotFrequency)
|
startsAt3 := time.Now().Add(cronSnapshotFrequency)
|
||||||
if cronSnapshotFrequency == time.Hour {
|
if cronSnapshotFrequency == time.Hour {
|
||||||
@@ -292,8 +260,12 @@ func main() {
|
|||||||
}
|
}
|
||||||
logger.Debug("Created vcenter monthly aggregation cron job", "job", job5.ID())
|
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(
|
job6, err := c.NewJob(
|
||||||
gocron.CronJob("0 30 2 * *", false),
|
gocron.CronJob(snapshotCleanupCron, false),
|
||||||
gocron.NewTask(func() {
|
gocron.NewTask(func() {
|
||||||
ct.RunSnapshotCleanup(ctx, logger)
|
ct.RunSnapshotCleanup(ctx, logger)
|
||||||
}), gocron.WithSingletonMode(gocron.LimitModeReschedule),
|
}), gocron.WithSingletonMode(gocron.LimitModeReschedule),
|
||||||
@@ -325,3 +297,10 @@ func main() {
|
|||||||
|
|
||||||
os.Exit(0)
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,8 +10,13 @@ buildtime=$(date +%Y-%m-%dT%T%z)
|
|||||||
#Extract the version from yml
|
#Extract the version from yml
|
||||||
package_version=$(grep 'version:' "$package_name.yml" | awk '{print $2}' | tr -d '"' | sed 's/^v//')
|
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")
|
platforms=("linux/amd64")
|
||||||
|
if [[ "$host_os" == "darwin" && ( "$host_arch" == "x86_64" || "$host_arch" == "amd64" || "$host_arch" == "arm64" ) ]]; then
|
||||||
|
platforms+=("darwin/amd64")
|
||||||
|
fi
|
||||||
|
#platforms=("linux/amd64")
|
||||||
|
|
||||||
echo Building::
|
echo Building::
|
||||||
echo - Version $package_version
|
echo - Version $package_version
|
||||||
@@ -40,6 +45,4 @@ do
|
|||||||
#sha256sum build/${output_name}.gz > build/${output_name}_checksum.txt
|
#sha256sum build/${output_name}.gz > build/${output_name}_checksum.txt
|
||||||
done
|
done
|
||||||
|
|
||||||
nfpm package --config $package_name.yml --packager rpm --target build/
|
|
||||||
|
|
||||||
ls -lah build
|
ls -lah build
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ set -euo pipefail
|
|||||||
# Usage: ./update-swagger-ui.sh [version]
|
# Usage: ./update-swagger-ui.sh [version]
|
||||||
# Example: ./update-swagger-ui.sh v5.17.14
|
# Example: ./update-swagger-ui.sh v5.17.14
|
||||||
# If no version is provided, defaults below is used.
|
# 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"
|
TARGET_DIR="server/router/swagger-ui-dist"
|
||||||
TARBALL_URL="https://github.com/swagger-api/swagger-ui/archive/refs/tags/${VERSION}.tar.gz"
|
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"
|
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#configUrl:[[:space:]]*["'\''"][^"'\''"]*["'\''"]#url: "/swagger.json"#' \
|
||||||
-e 's#url:[[:space:]]*["'\''"][^"'\''"]*["'\''"]#url: "/swagger.json"#' \
|
-e 's#url:[[:space:]]*["'\''"][^"'\''"]*["'\''"]#url: "/swagger.json"#' \
|
||||||
-e 's#urls:[[:space:]]*\[[^]]*\]#url: "/swagger.json"#' \
|
-e 's#urls:[[:space:]]*\[[^]]*\]#url: "/swagger.json"#' \
|
||||||
-e '/url:[[:space:]]*"[^\"]*swagger\.json"[[:space:]]*,?$/a\ validatorUrl: null,' \
|
-e "$append_validator" \
|
||||||
"$INDEX"
|
"$INDEX"
|
||||||
|
|
||||||
echo ">> Done. Files are in ${TARGET_DIR}"
|
echo ">> Done. Files are in ${TARGET_DIR}"
|
||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
|
||||||
"vctp/components/views"
|
"vctp/components/views"
|
||||||
"vctp/internal/report"
|
"vctp/internal/report"
|
||||||
|
|
||||||
@@ -22,7 +21,7 @@ import (
|
|||||||
// @Failure 500 {string} string "Server error"
|
// @Failure 500 {string} string "Server error"
|
||||||
// @Router /snapshots/hourly [get]
|
// @Router /snapshots/hourly [get]
|
||||||
func (h *Handler) SnapshotHourlyList(w http.ResponseWriter, r *http.Request) {
|
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.
|
// 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"
|
// @Failure 500 {string} string "Server error"
|
||||||
// @Router /snapshots/daily [get]
|
// @Router /snapshots/daily [get]
|
||||||
func (h *Handler) SnapshotDailyList(w http.ResponseWriter, r *http.Request) {
|
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.
|
// 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"
|
// @Failure 500 {string} string "Server error"
|
||||||
// @Router /snapshots/monthly [get]
|
// @Router /snapshots/monthly [get]
|
||||||
func (h *Handler) SnapshotMonthlyList(w http.ResponseWriter, r *http.Request) {
|
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.
|
// SnapshotReportDownload streams a snapshot table as XLSX.
|
||||||
@@ -91,28 +90,28 @@ func (h *Handler) SnapshotReportDownload(w http.ResponseWriter, r *http.Request)
|
|||||||
w.Write(reportData)
|
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()
|
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 {
|
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)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
fmt.Fprintf(w, "Unable to list snapshot tables: %s\n", err)
|
fmt.Fprintf(w, "Unable to list snapshot tables: %s\n", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
entries := make([]views.SnapshotEntry, 0, len(tables))
|
entries := make([]views.SnapshotEntry, 0, len(records))
|
||||||
for _, table := range tables {
|
for _, record := range records {
|
||||||
if prefix == "inventory_daily_" && strings.HasPrefix(table, "inventory_daily_summary_") {
|
label := report.FormatSnapshotLabel(snapshotType, record.SnapshotTime, record.TableName)
|
||||||
continue
|
|
||||||
}
|
|
||||||
label := table
|
|
||||||
if parsed, ok := report.FormatSnapshotLabel(prefix, table); ok {
|
|
||||||
label = parsed
|
|
||||||
}
|
|
||||||
entries = append(entries, views.SnapshotEntry{
|
entries = append(entries, views.SnapshotEntry{
|
||||||
Label: label,
|
Label: label,
|
||||||
Link: "/api/report/snapshot?table=" + url.QueryEscape(table),
|
Link: "/api/report/snapshot?table=" + url.QueryEscape(record.TableName),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"vctp/db"
|
"vctp/db"
|
||||||
queries "vctp/db/queries"
|
queries "vctp/db/queries"
|
||||||
models "vctp/server/models"
|
models "vctp/server/models"
|
||||||
@@ -56,6 +57,17 @@ func (h *Handler) VmImport(w http.ResponseWriter, r *http.Request) {
|
|||||||
//prettyPrint(inData)
|
//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()
|
ctx := context.Background()
|
||||||
|
|
||||||
// Query Inventory table for this VM before adding it
|
// Query Inventory table for this VM before adding it
|
||||||
|
|||||||
@@ -441,6 +441,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")
|
//c.Logger.Debug("found VM")
|
||||||
srmPlaceholder = "FALSE" // Default assumption
|
srmPlaceholder = "FALSE" // Default assumption
|
||||||
//prettyPrint(vmObject)
|
//prettyPrint(vmObject)
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ func New(logger *slog.Logger, database db.Database, buildTime string, sha1ver st
|
|||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
mux.Handle("/assets/", middleware.CacheMiddleware(http.FileServer(http.FS(dist.AssetsDir))))
|
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.HandleFunc("/", h.Home)
|
mux.HandleFunc("/", h.Home)
|
||||||
mux.HandleFunc("/api/event/vm/create", h.VmCreateEvent)
|
mux.HandleFunc("/api/event/vm/create", h.VmCreateEvent)
|
||||||
mux.HandleFunc("/api/event/vm/modify", h.VmModifyEvent)
|
mux.HandleFunc("/api/event/vm/modify", h.VmModifyEvent)
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
CPE_OPTS='-config /etc/dtms/vctp.yml -log-level info -log-output text'
|
CPE_OPTS='-settings /etc/dtms/vctp.yml'
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ After=network.target
|
|||||||
Type=simple
|
Type=simple
|
||||||
EnvironmentFile=/etc/default/vctp
|
EnvironmentFile=/etc/default/vctp
|
||||||
User=vctp
|
User=vctp
|
||||||
|
Group=dtms
|
||||||
ExecStart=/usr/bin/vctp-linux-amd64 $CPE_OPTS
|
ExecStart=/usr/bin/vctp-linux-amd64 $CPE_OPTS
|
||||||
ExecStartPost=/usr/bin/sleep 3
|
ExecStartPost=/usr/bin/sleep 3
|
||||||
Restart=always
|
Restart=always
|
||||||
|
|||||||
43
src/vctp.yml
43
src/vctp.yml
@@ -1,25 +1,24 @@
|
|||||||
settings:
|
settings:
|
||||||
data_location: "/var/lib/cbs"
|
log_level: "info"
|
||||||
kickstart_location: "/var/lib/cbs/ks"
|
log_output: "text"
|
||||||
database_filename: "/var/lib/cbs/cbs.db"
|
database_driver: "sqlite"
|
||||||
|
database_url: "/var/lib/vctp/db.sqlite3"
|
||||||
bind_ip:
|
bind_ip:
|
||||||
bind_port: 443
|
bind_port: 9443
|
||||||
bind_disable_tls: false
|
bind_disable_tls: false
|
||||||
tls_cert_filename: "/etc/dtms/cbs.crt"
|
tls_cert_filename: "/etc/dtms/vctp.crt"
|
||||||
tls_key_filename: "/etc/dtms/cbs.key"
|
tls_key_filename: "/etc/dtms/vctp.key"
|
||||||
tftp_root_directory: "/var/lib/tftpboot"
|
vcenter_username: ""
|
||||||
tftp_images_subdirectory: "images"
|
vcenter_password: ""
|
||||||
replacements:
|
vcenter_insecure: false
|
||||||
omapi:
|
vcenter_event_polling_seconds: 60
|
||||||
key_name: "OMAPI"
|
vcenter_inventory_polling_seconds: 7200
|
||||||
key_secret:
|
vcenter_inventory_snapshot_seconds: 3600
|
||||||
special_files:
|
vcenter_inventory_aggregate_seconds: 86400
|
||||||
ldap_groups:
|
hourly_snapshot_max_age_days: 60
|
||||||
ldap_bind_address: ""
|
daily_snapshot_max_age_months: 12
|
||||||
ldap_base_dn: ""
|
snapshot_cleanup_cron: "30 2 * * *"
|
||||||
ldap_trust_cert_file: ""
|
tenants_to_filter:
|
||||||
ldap_disable_validation: false
|
node_charge_clusters:
|
||||||
ldap_insecure: false
|
srm_activeactive_vms:
|
||||||
auth_token_lifespan_hours: 2
|
vcenter_addresses:
|
||||||
auth_api_key: ""
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user