This commit is contained in:
@@ -6,11 +6,18 @@ import (
|
|||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
"vctp/internal/utils"
|
"vctp/internal/utils"
|
||||||
|
|
||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
postgresURIUserInfoPasswordPattern = regexp.MustCompile(`(?i)(postgres(?:ql)?://[^@/\s]*:)([^@/\s]*)(@)`)
|
||||||
|
postgresKVPasswordPattern = regexp.MustCompile(`(?i)(\bpassword\s*=\s*)(?:'[^']*'|"[^"]*"|[^\s]+)`)
|
||||||
|
)
|
||||||
|
|
||||||
type Settings struct {
|
type Settings struct {
|
||||||
SettingsPath string
|
SettingsPath string
|
||||||
Logger *slog.Logger
|
Logger *slog.Logger
|
||||||
@@ -102,12 +109,24 @@ func (s *Settings) ReadYMLSettings() error {
|
|||||||
if redacted.Settings.EncryptionKey != "" {
|
if redacted.Settings.EncryptionKey != "" {
|
||||||
redacted.Settings.EncryptionKey = "REDACTED"
|
redacted.Settings.EncryptionKey = "REDACTED"
|
||||||
}
|
}
|
||||||
|
if redacted.Settings.DatabaseURL != "" {
|
||||||
|
redacted.Settings.DatabaseURL = redactDatabaseURL(redacted.Settings.DatabaseURL)
|
||||||
|
}
|
||||||
s.Logger.Debug("Updating settings", "settings", redacted)
|
s.Logger.Debug("Updating settings", "settings", redacted)
|
||||||
s.Values = &settings
|
s.Values = &settings
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func redactDatabaseURL(databaseURL string) string {
|
||||||
|
if strings.TrimSpace(databaseURL) == "" {
|
||||||
|
return databaseURL
|
||||||
|
}
|
||||||
|
redacted := postgresURIUserInfoPasswordPattern.ReplaceAllString(databaseURL, `${1}REDACTED${3}`)
|
||||||
|
redacted = postgresKVPasswordPattern.ReplaceAllString(redacted, `${1}REDACTED`)
|
||||||
|
return redacted
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Settings) WriteYMLSettings() error {
|
func (s *Settings) WriteYMLSettings() error {
|
||||||
if s.Values == nil {
|
if s.Values == nil {
|
||||||
return errors.New("settings are not loaded")
|
return errors.New("settings are not loaded")
|
||||||
|
|||||||
29
internal/settings/settings_redaction_test.go
Normal file
29
internal/settings/settings_redaction_test.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package settings
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestRedactDatabaseURL_PostgresURI(t *testing.T) {
|
||||||
|
input := "postgres://vctp_user:Secr3tP%40ss@db-host:5432/vctp?sslmode=disable"
|
||||||
|
got := redactDatabaseURL(input)
|
||||||
|
want := "postgres://vctp_user:REDACTED@db-host:5432/vctp?sslmode=disable"
|
||||||
|
if got != want {
|
||||||
|
t.Fatalf("unexpected redaction result\nwant: %s\ngot: %s", want, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRedactDatabaseURL_PostgresKeyValue(t *testing.T) {
|
||||||
|
input := "host=db-host port=5432 dbname=vctp user=vctp_user password='P@ss:w0rd#%' sslmode=disable"
|
||||||
|
got := redactDatabaseURL(input)
|
||||||
|
want := "host=db-host port=5432 dbname=vctp user=vctp_user password=REDACTED sslmode=disable"
|
||||||
|
if got != want {
|
||||||
|
t.Fatalf("unexpected redaction result\nwant: %s\ngot: %s", want, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRedactDatabaseURL_UnchangedWhenNoPassword(t *testing.T) {
|
||||||
|
input := "host=db-host port=5432 dbname=vctp user=vctp_user sslmode=disable"
|
||||||
|
got := redactDatabaseURL(input)
|
||||||
|
if got != input {
|
||||||
|
t.Fatalf("expected input to remain unchanged\nwant: %s\ngot: %s", input, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -315,21 +315,59 @@ LIMIT 1
|
|||||||
}
|
}
|
||||||
nextSnapshotRows.Close()
|
nextSnapshotRows.Close()
|
||||||
}
|
}
|
||||||
nextPresence := make(map[string]struct{})
|
|
||||||
|
// Build per-vCenter snapshot timelines from observed VM samples so deletion
|
||||||
|
// inference is only based on times where that vCenter actually reported data.
|
||||||
|
vcenterTimeSet := make(map[string]map[int64]struct{}, 8)
|
||||||
|
for _, v := range aggMap {
|
||||||
|
if v.key.Vcenter == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
set := vcenterTimeSet[v.key.Vcenter]
|
||||||
|
if set == nil {
|
||||||
|
set = make(map[int64]struct{}, len(v.seen))
|
||||||
|
vcenterTimeSet[v.key.Vcenter] = set
|
||||||
|
}
|
||||||
|
for t := range v.seen {
|
||||||
|
if t > 0 {
|
||||||
|
set[t] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vcenterSnapTimes := make(map[string][]int64, len(vcenterTimeSet))
|
||||||
|
for vcenter, set := range vcenterTimeSet {
|
||||||
|
times := make([]int64, 0, len(set))
|
||||||
|
for t := range set {
|
||||||
|
times = append(times, t)
|
||||||
|
}
|
||||||
|
sort.Slice(times, func(i, j int) bool { return times[i] < times[j] })
|
||||||
|
vcenterSnapTimes[vcenter] = times
|
||||||
|
}
|
||||||
|
|
||||||
|
nextPresenceByVcenter := make(map[string]map[string]struct{}, 8)
|
||||||
if nextSnapshotTable != "" && db.TableExists(ctx, dbConn, nextSnapshotTable) {
|
if nextSnapshotTable != "" && db.TableExists(ctx, dbConn, nextSnapshotTable) {
|
||||||
rows, err := querySnapshotRows(ctx, dbConn, nextSnapshotTable, []string{"VmId", "VmUuid", "Name"}, `"Vcenter" = ?`, c.Settings.Values.Settings.VcenterAddresses[0])
|
rows, err := querySnapshotRows(ctx, dbConn, nextSnapshotTable, []string{"Vcenter", "VmId", "VmUuid", "Name"}, "")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
|
var vcenter string
|
||||||
var vmId, vmUuid, name sql.NullString
|
var vmId, vmUuid, name sql.NullString
|
||||||
if err := rows.Scan(&vmId, &vmUuid, &name); err == nil {
|
if err := rows.Scan(&vcenter, &vmId, &vmUuid, &name); err == nil {
|
||||||
if vmId.Valid {
|
if strings.TrimSpace(vcenter) == "" {
|
||||||
nextPresence["id:"+vmId.String] = struct{}{}
|
continue
|
||||||
}
|
}
|
||||||
if vmUuid.Valid {
|
vcPresence := nextPresenceByVcenter[vcenter]
|
||||||
nextPresence["uuid:"+vmUuid.String] = struct{}{}
|
if vcPresence == nil {
|
||||||
|
vcPresence = make(map[string]struct{}, 1024)
|
||||||
|
nextPresenceByVcenter[vcenter] = vcPresence
|
||||||
}
|
}
|
||||||
if name.Valid {
|
if vmId.Valid && strings.TrimSpace(vmId.String) != "" {
|
||||||
nextPresence["name:"+name.String] = struct{}{}
|
vcPresence["id:"+strings.TrimSpace(vmId.String)] = struct{}{}
|
||||||
|
}
|
||||||
|
if vmUuid.Valid && strings.TrimSpace(vmUuid.String) != "" {
|
||||||
|
vcPresence["uuid:"+strings.TrimSpace(vmUuid.String)] = struct{}{}
|
||||||
|
}
|
||||||
|
if name.Valid && strings.TrimSpace(name.String) != "" {
|
||||||
|
vcPresence["name:"+strings.ToLower(strings.TrimSpace(name.String))] = struct{}{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -337,23 +375,24 @@ LIMIT 1
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var maxSnap int64
|
|
||||||
if len(snapTimes) > 0 {
|
|
||||||
maxSnap = snapTimes[len(snapTimes)-1]
|
|
||||||
}
|
|
||||||
|
|
||||||
inferredDeletions := 0
|
inferredDeletions := 0
|
||||||
for _, v := range aggMap {
|
for _, v := range aggMap {
|
||||||
if v.deletion != 0 {
|
if v.deletion != 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
vcSnapTimes := vcenterSnapTimes[v.key.Vcenter]
|
||||||
|
// Deletion inference needs meaningful per-vCenter continuity.
|
||||||
|
if len(vcSnapTimes) < 3 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
vcMaxSnap := vcSnapTimes[len(vcSnapTimes)-1]
|
||||||
// Infer deletion only after seeing at least two consecutive absent snapshots after lastSeen.
|
// Infer deletion only after seeing at least two consecutive absent snapshots after lastSeen.
|
||||||
if maxSnap > 0 && len(v.seen) > 0 && v.lastSeen < maxSnap {
|
if vcMaxSnap > 0 && len(v.seen) > 0 && v.lastSeen < vcMaxSnap {
|
||||||
c.Logger.Debug("inferring deletion window", "vm_id", v.key.VmId, "vm_uuid", v.key.VmUuid, "name", v.key.Name, "last_seen", v.lastSeen, "snapshots", len(snapTimes))
|
c.Logger.Debug("inferring deletion window", "vcenter", v.key.Vcenter, "vm_id", v.key.VmId, "vm_uuid", v.key.VmUuid, "name", v.key.Name, "last_seen", v.lastSeen, "snapshots", len(vcSnapTimes))
|
||||||
}
|
}
|
||||||
consecutiveMisses := 0
|
consecutiveMisses := 0
|
||||||
firstMiss := int64(0)
|
firstMiss := int64(0)
|
||||||
for _, t := range snapTimes {
|
for _, t := range vcSnapTimes {
|
||||||
if t <= v.lastSeen {
|
if t <= v.lastSeen {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -374,18 +413,19 @@ LIMIT 1
|
|||||||
}
|
}
|
||||||
if v.deletion == 0 && firstMiss > 0 {
|
if v.deletion == 0 && firstMiss > 0 {
|
||||||
// Not enough consecutive misses within the day; try to use the first snapshot of the next day to confirm.
|
// Not enough consecutive misses within the day; try to use the first snapshot of the next day to confirm.
|
||||||
|
nextPresence := nextPresenceByVcenter[v.key.Vcenter]
|
||||||
if nextSnapshotTable != "" && len(nextPresence) > 0 {
|
if nextSnapshotTable != "" && len(nextPresence) > 0 {
|
||||||
_, presentByID := nextPresence["id:"+v.key.VmId]
|
_, presentByID := nextPresence["id:"+strings.TrimSpace(v.key.VmId)]
|
||||||
_, presentByUUID := nextPresence["uuid:"+v.key.VmUuid]
|
_, presentByUUID := nextPresence["uuid:"+strings.TrimSpace(v.key.VmUuid)]
|
||||||
_, presentByName := nextPresence["name:"+v.key.Name]
|
_, presentByName := nextPresence["name:"+strings.ToLower(strings.TrimSpace(v.key.Name))]
|
||||||
if !presentByID && !presentByUUID && !presentByName {
|
if !presentByID && !presentByUUID && !presentByName {
|
||||||
v.deletion = firstMiss
|
v.deletion = firstMiss
|
||||||
inferredDeletions++
|
inferredDeletions++
|
||||||
c.Logger.Debug("cross-day deletion inferred from next snapshot", "vm_id", v.key.VmId, "vm_uuid", v.key.VmUuid, "name", v.key.Name, "deletion", firstMiss, "next_table", nextSnapshotTable)
|
c.Logger.Debug("cross-day deletion inferred from next snapshot", "vcenter", v.key.Vcenter, "vm_id", v.key.VmId, "vm_uuid", v.key.VmUuid, "name", v.key.Name, "deletion", firstMiss, "next_table", nextSnapshotTable)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if v.deletion == 0 {
|
if v.deletion == 0 {
|
||||||
c.Logger.Debug("pending deletion inference (insufficient consecutive misses)", "vm_id", v.key.VmId, "vm_uuid", v.key.VmUuid, "name", v.key.Name, "last_seen", v.lastSeen, "first_missing_snapshot", firstMiss)
|
c.Logger.Debug("pending deletion inference (insufficient consecutive misses)", "vcenter", v.key.Vcenter, "vm_id", v.key.VmId, "vm_uuid", v.key.VmUuid, "name", v.key.Name, "last_seen", v.lastSeen, "first_missing_snapshot", firstMiss)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user