generate excel worksheets when data is available instead of on-demand
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2026-01-15 08:43:31 +11:00
parent 434c7136e9
commit 8b2c8ae85d
7 changed files with 67 additions and 1 deletions

View File

@@ -6,6 +6,8 @@ import (
"database/sql" "database/sql"
"fmt" "fmt"
"log/slog" "log/slog"
"os"
"path/filepath"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@@ -637,6 +639,29 @@ func CreateTableReport(logger *slog.Logger, Database db.Database, ctx context.Co
return buffer.Bytes(), nil return buffer.Bytes(), nil
} }
// SaveTableReport renders a table report and writes it to the destination directory with a .xlsx extension.
func SaveTableReport(logger *slog.Logger, Database db.Database, ctx context.Context, tableName, destDir string) (string, error) {
if err := db.ValidateTableName(tableName); err != nil {
return "", err
}
if strings.TrimSpace(destDir) == "" {
return "", fmt.Errorf("destination directory is empty")
}
if err := os.MkdirAll(destDir, 0o755); err != nil {
return "", fmt.Errorf("failed to create reports directory: %w", err)
}
data, err := CreateTableReport(logger, Database, ctx, tableName)
if err != nil {
return "", err
}
filename := filepath.Join(destDir, fmt.Sprintf("%s.xlsx", tableName))
if err := os.WriteFile(filename, data, 0o644); err != nil {
return "", err
}
return filename, nil
}
func addTotalsChartSheet(logger *slog.Logger, database db.Database, ctx context.Context, xlsx *excelize.File, tableName string) { func addTotalsChartSheet(logger *slog.Logger, database db.Database, ctx context.Context, xlsx *excelize.File, tableName string) {
if strings.HasPrefix(tableName, "inventory_daily_summary_") { if strings.HasPrefix(tableName, "inventory_daily_summary_") {
suffix := strings.TrimPrefix(tableName, "inventory_daily_summary_") suffix := strings.TrimPrefix(tableName, "inventory_daily_summary_")

View File

@@ -40,6 +40,7 @@ type SettingsYML struct {
HourlySnapshotMaxAgeDays int `yaml:"hourly_snapshot_max_age_days"` HourlySnapshotMaxAgeDays int `yaml:"hourly_snapshot_max_age_days"`
DailySnapshotMaxAgeMonths int `yaml:"daily_snapshot_max_age_months"` DailySnapshotMaxAgeMonths int `yaml:"daily_snapshot_max_age_months"`
SnapshotCleanupCron string `yaml:"snapshot_cleanup_cron"` SnapshotCleanupCron string `yaml:"snapshot_cleanup_cron"`
ReportsDir string `yaml:"reports_dir"`
HourlyJobTimeoutSeconds int `yaml:"hourly_job_timeout_seconds"` HourlyJobTimeoutSeconds int `yaml:"hourly_job_timeout_seconds"`
HourlySnapshotTimeoutSeconds int `yaml:"hourly_snapshot_timeout_seconds"` HourlySnapshotTimeoutSeconds int `yaml:"hourly_snapshot_timeout_seconds"`
DailyJobTimeoutSeconds int `yaml:"daily_job_timeout_seconds"` DailyJobTimeoutSeconds int `yaml:"daily_job_timeout_seconds"`

View File

@@ -168,6 +168,10 @@ func (c *CronTask) RunVcenterSnapshotHourly(ctx context.Context, logger *slog.Lo
c.Logger.Warn("failed to register hourly snapshot", "error", err, "table", tableName) c.Logger.Warn("failed to register hourly snapshot", "error", err, "table", tableName)
} }
if err := c.generateReport(ctx, tableName); err != nil {
c.Logger.Warn("failed to generate hourly report", "error", err, "table", tableName)
}
c.Logger.Debug("Finished hourly vcenter snapshot", "vcenter_count", len(c.Settings.Values.Settings.VcenterAddresses), "table", tableName, "row_count", rowCount) c.Logger.Debug("Finished hourly vcenter snapshot", "vcenter_count", len(c.Settings.Values.Settings.VcenterAddresses), "table", tableName, "row_count", rowCount)
return nil return nil
} }
@@ -312,6 +316,10 @@ func (c *CronTask) aggregateDailySummary(ctx context.Context, targetTime time.Ti
c.Logger.Warn("failed to register daily snapshot", "error", err, "table", summaryTable) c.Logger.Warn("failed to register daily snapshot", "error", err, "table", summaryTable)
} }
if err := c.generateReport(ctx, summaryTable); err != nil {
c.Logger.Warn("failed to generate daily report", "error", err, "table", summaryTable)
}
c.Logger.Debug("Finished daily inventory aggregation", "summary_table", summaryTable) c.Logger.Debug("Finished daily inventory aggregation", "summary_table", summaryTable)
return nil return nil
} }
@@ -431,6 +439,10 @@ func (c *CronTask) aggregateMonthlySummary(ctx context.Context, targetMonth time
c.Logger.Warn("failed to register monthly snapshot", "error", err, "table", monthlyTable) c.Logger.Warn("failed to register monthly snapshot", "error", err, "table", monthlyTable)
} }
if err := c.generateReport(ctx, monthlyTable); err != nil {
c.Logger.Warn("failed to generate monthly report", "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
} }
@@ -718,6 +730,21 @@ func normalizeResourcePool(value string) string {
} }
} }
func (c *CronTask) reportsDir() string {
if c.Settings != nil && c.Settings.Values != nil {
if dir := strings.TrimSpace(c.Settings.Values.Settings.ReportsDir); dir != "" {
return dir
}
}
return "/var/lib/vctp/reports"
}
func (c *CronTask) generateReport(ctx context.Context, tableName string) error {
dest := c.reportsDir()
_, err := report.SaveTableReport(c.Logger, c.Database, ctx, tableName, dest)
return err
}
func snapshotFromVM(vmObject *mo.VirtualMachine, vc *vcenter.Vcenter, snapshotTime time.Time, inv *queries.Inventory, hostLookup map[string]vcenter.HostLookup, folderLookup vcenter.FolderLookup, rpLookup map[string]string) (inventorySnapshotRow, error) { func snapshotFromVM(vmObject *mo.VirtualMachine, vc *vcenter.Vcenter, snapshotTime time.Time, inv *queries.Inventory, hostLookup map[string]vcenter.HostLookup, folderLookup vcenter.FolderLookup, rpLookup map[string]string) (inventorySnapshotRow, error) {
if vmObject == nil { if vmObject == nil {
return inventorySnapshotRow{}, fmt.Errorf("missing VM object") return inventorySnapshotRow{}, fmt.Errorf("missing VM object")

View File

@@ -111,7 +111,7 @@ func (h *Handler) renderSnapshotList(w http.ResponseWriter, r *http.Request, sna
label := report.FormatSnapshotLabel(snapshotType, record.SnapshotTime, record.TableName) label := report.FormatSnapshotLabel(snapshotType, record.SnapshotTime, record.TableName)
entries = append(entries, views.SnapshotEntry{ entries = append(entries, views.SnapshotEntry{
Label: label, Label: label,
Link: "/api/report/snapshot?table=" + url.QueryEscape(record.TableName), Link: "/reports/" + url.PathEscape(record.TableName) + ".xlsx",
Count: record.SnapshotCount, Count: record.SnapshotCount,
}) })
} }

View File

@@ -5,6 +5,8 @@ import (
"log/slog" "log/slog"
"net/http" "net/http"
"net/http/pprof" "net/http/pprof"
"os"
"path/filepath"
"vctp/db" "vctp/db"
"vctp/dist" "vctp/dist"
"vctp/internal/secrets" "vctp/internal/secrets"
@@ -28,10 +30,19 @@ func New(logger *slog.Logger, database db.Database, buildTime string, sha1ver st
mux := http.NewServeMux() mux := http.NewServeMux()
reportsDir := settings.Values.Settings.ReportsDir
if reportsDir == "" {
reportsDir = "/var/lib/vctp/reports"
}
if err := os.MkdirAll(reportsDir, 0o755); err != nil {
logger.Warn("failed to create reports directory", "error", err, "path", reportsDir)
}
mux.Handle("/assets/", middleware.CacheMiddleware(http.FileServer(http.FS(dist.AssetsDir)))) mux.Handle("/assets/", middleware.CacheMiddleware(http.FileServer(http.FS(dist.AssetsDir))))
mux.Handle("/favicon.ico", 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-16x16.png", middleware.CacheMiddleware(http.FileServer(http.FS(dist.AssetsDir))))
mux.Handle("/favicon-32x32.png", middleware.CacheMiddleware(http.FileServer(http.FS(dist.AssetsDir)))) mux.Handle("/favicon-32x32.png", middleware.CacheMiddleware(http.FileServer(http.FS(dist.AssetsDir))))
mux.Handle("/reports/", http.StripPrefix("/reports/", http.FileServer(http.Dir(filepath.Clean(reportsDir)))))
mux.HandleFunc("/", h.Home) mux.HandleFunc("/", 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)

View File

@@ -20,6 +20,7 @@ getent passwd "$USER" >/dev/null || useradd -r -g "$GROUP" -m -s /bin/bash -c "v
# create vctp data directory if it doesn't exist # create vctp data directory if it doesn't exist
[ -d /var/lib/vctp ] || mkdir -p /var/lib/vctp [ -d /var/lib/vctp ] || mkdir -p /var/lib/vctp
[ -d /var/lib/vctp/reports ] || mkdir -p /var/lib/vctp/reports
# set user ownership on vctp data directory if not already done # set user ownership on vctp data directory if not already done
[ "$(stat -c "%U" /var/lib/vctp)" = "$USER" ] || chown -R "$USER" /var/lib/vctp [ "$(stat -c "%U" /var/lib/vctp)" = "$USER" ] || chown -R "$USER" /var/lib/vctp

View File

@@ -3,6 +3,7 @@ settings:
log_output: "text" log_output: "text"
database_driver: "sqlite" database_driver: "sqlite"
database_url: "/var/lib/vctp/db.sqlite3" database_url: "/var/lib/vctp/db.sqlite3"
reports_dir: /var/lib/vctp/reports
bind_ip: bind_ip:
bind_port: 9443 bind_port: 9443
bind_disable_tls: false bind_disable_tls: false