Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ BACKUP_PATH=${BASE_DIR}/backup
LOG_PATH=${BASE_DIR}/log
```

**Path resolution**: `${BASE_DIR}` expands automatically. Use absolute paths or relative to `BASE_DIR`.
**Path resolution**: `${BASE_DIR}` expands automatically. Scalar string values also support `$VAR` / `${VAR}` expansion (config keys first, then environment variables).

---

Expand Down Expand Up @@ -930,7 +930,7 @@ CEPH_CONFIG_PATH=/etc/ceph # Ceph config directory
BACKUP_VM_CONFIGS=true # VM/CT config files
```

**Note (PVE snapshot behavior)**: ProxSave snapshots `PVE_CONFIG_PATH` for completeness. When a PVE feature is disabled, proxsave also excludes its well-known files from that snapshot to avoid “still included via full directory copy” surprises (e.g. `qemu-server/` + `lxc/` for `BACKUP_VM_CONFIGS=false`, `firewall/` + `host.fw` for `BACKUP_PVE_FIREWALL=false`, `user.cfg`/`acl.cfg`/`domains.cfg` for `BACKUP_PVE_ACL=false`, `jobs.cfg` + `vzdump.cron` for `BACKUP_PVE_JOBS=false`, `corosync.conf` (and `config.db` capture) for `BACKUP_CLUSTER_CONFIG=false`).
**Note (PVE snapshot behavior)**: ProxSave snapshots `PVE_CONFIG_PATH` for completeness. When a PVE feature is disabled, proxsave also excludes its well-known files from that snapshot to avoid “still included via full directory copy” surprises (e.g. `qemu-server/` + `lxc/` for `BACKUP_VM_CONFIGS=false`, `firewall/` + `host.fw` for `BACKUP_PVE_FIREWALL=false`, `user.cfg`/`domains.cfg` for `BACKUP_PVE_ACL=false` (ACLs are stored in `user.cfg` on PVE), `jobs.cfg` + `vzdump.cron` for `BACKUP_PVE_JOBS=false`, `corosync.conf` (and `config.db` capture) for `BACKUP_CLUSTER_CONFIG=false`).

### PBS-Specific

Expand Down Expand Up @@ -990,6 +990,8 @@ SYSTEM_ROOT_PREFIX= # Optional alternate root for system collecti
# Use this to point the collector at a chroot/test fixture without touching the host FS.
```

**Note**: `${PVE_CONFIG_PATH}` (and other `${VAR}` references) are resolved from the same `backup.env` file too — you don’t need to `export` them.

**Use case**: Working with mounted snapshots or mirrors at non-standard paths.

### System Collectors
Expand Down
12 changes: 3 additions & 9 deletions internal/backup/collector_pve.go
Original file line number Diff line number Diff line change
Expand Up @@ -260,15 +260,9 @@ func (c *Collector) populatePVEManifest() {
}
}

// ACL configuration.
// Access control configuration (PVE stores ACLs inside user.cfg).
record(filepath.Join(pveConfigPath, "user.cfg"), c.config.BackupPVEACL, manifestLogOpts{
description: "User configuration",
disableHint: "BACKUP_PVE_ACL",
log: true,
countNotFound: true,
})
record(filepath.Join(pveConfigPath, "acl.cfg"), c.config.BackupPVEACL, manifestLogOpts{
description: "ACL configuration",
description: "User/ACL configuration",
disableHint: "BACKUP_PVE_ACL",
log: true,
countNotFound: true,
Expand Down Expand Up @@ -394,7 +388,7 @@ func (c *Collector) collectPVEDirectories(ctx context.Context, clustered bool) e
extraExclude = append(extraExclude, "firewall", "host.fw")
}
if !c.config.BackupPVEACL {
extraExclude = append(extraExclude, "user.cfg", "acl.cfg", "domains.cfg")
extraExclude = append(extraExclude, "user.cfg", "domains.cfg")
}
if !c.config.BackupPVEJobs {
extraExclude = append(extraExclude, "jobs.cfg", "vzdump.cron")
Expand Down
2 changes: 0 additions & 2 deletions internal/backup/collector_pve_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -756,7 +756,6 @@ func TestCollectPVEDirectoriesExcludesDisabledPVEConfigFiles(t *testing.T) {
mustWrite(filepath.Join(pveRoot, "dummy.cfg"), "ok")
mustWrite(filepath.Join(pveRoot, "corosync.conf"), "corosync")
mustWrite(filepath.Join(pveRoot, "user.cfg"), "user")
mustWrite(filepath.Join(pveRoot, "acl.cfg"), "acl")
mustWrite(filepath.Join(pveRoot, "domains.cfg"), "domains")
mustWrite(filepath.Join(pveRoot, "jobs.cfg"), "jobs")
mustWrite(filepath.Join(pveRoot, "vzdump.cron"), "cron")
Expand Down Expand Up @@ -787,7 +786,6 @@ func TestCollectPVEDirectoriesExcludesDisabledPVEConfigFiles(t *testing.T) {
for _, excluded := range []string{
"corosync.conf",
"user.cfg",
"acl.cfg",
"domains.cfg",
"jobs.cfg",
"vzdump.cron",
Expand Down
80 changes: 76 additions & 4 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -724,8 +724,9 @@ func (c *Config) parseSystemSettings() {
// Helper methods per ottenere valori tipizzati

func (c *Config) getString(key, defaultValue string) string {
if val, ok := c.raw[key]; ok {
return expandEnvVars(val)
upperKey := strings.ToUpper(key)
if val, ok := c.raw[upperKey]; ok {
return c.expandConfigVars(val)
}
return defaultValue
}
Expand Down Expand Up @@ -901,10 +902,81 @@ func expandEnvVars(s string) string {
return result
}

type configVarExpander struct {
raw map[string]string
cache map[string]string
inProgress map[string]bool
}

func newConfigVarExpander(raw map[string]string) *configVarExpander {
return &configVarExpander{
raw: raw,
cache: make(map[string]string),
inProgress: make(map[string]bool),
}
}

func (e *configVarExpander) expand(s string) string {
return os.Expand(s, func(key string) string {
return e.resolve(key)
})
}

func (e *configVarExpander) resolve(key string) string {
key = strings.TrimSpace(key)
if key == "" {
return ""
}
upperKey := strings.ToUpper(key)

if cached, ok := e.cache[upperKey]; ok {
return cached
}
if e.inProgress[upperKey] {
return ""
}
e.inProgress[upperKey] = true
defer delete(e.inProgress, upperKey)

// Keep the historical behavior where BASE_DIR expands even when it's not set
// in the config or environment.
if upperKey == "BASE_DIR" {
if rawVal, ok := e.raw[upperKey]; ok && strings.TrimSpace(rawVal) != "" {
expanded := e.expand(rawVal)
e.cache[upperKey] = expanded
return expanded
}
expanded := defaultBaseDir()
e.cache[upperKey] = expanded
return expanded
}

if rawVal, ok := e.raw[upperKey]; ok {
expanded := e.expand(rawVal)
e.cache[upperKey] = expanded
return expanded
}

if envVal, ok := os.LookupEnv(upperKey); ok {
e.cache[upperKey] = envVal
return envVal
}
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

configVarExpander uppercases variable names before looking them up in the process environment. This prevents expansion of commonly-lowercase env vars (e.g. http_proxy/no_proxy) when referenced as ${http_proxy}. Consider checking the original key with os.LookupEnv(key) as a fallback (while still using uppercased keys for config-file lookups).

Suggested change
}
}
// Fallback: check the original (non-uppercased) key in the environment to
// support commonly-lowercase variables like http_proxy and no_proxy.
if key != upperKey {
if envVal, ok := os.LookupEnv(key); ok {
e.cache[upperKey] = envVal
return envVal
}
}

Copilot uses AI. Check for mistakes.

return ""
}

func (c *Config) expandConfigVars(s string) string {
if strings.IndexByte(s, '$') == -1 {
return s
}
return newConfigVarExpander(c.raw).expand(s)
}

func (c *Config) getStringWithFallback(keys []string, defaultValue string) string {
for _, key := range keys {
if val, ok := c.raw[key]; ok && val != "" {
return expandEnvVars(val)
upperKey := strings.ToUpper(key)
if val, ok := c.raw[upperKey]; ok && val != "" {
return c.expandConfigVars(val)
}
}
return defaultValue
Expand Down
27 changes: 27 additions & 0 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,33 @@ BACKUP_PATH=${BASE_DIR}/backup-data
}
}

func TestLoadConfigExpandsConfigVarReferences(t *testing.T) {
tmpDir := t.TempDir()
configPath := filepath.Join(tmpDir, "config_vars.env")

content := `COROSYNC_CONFIG_PATH=${PVE_CONFIG_PATH}/corosync.conf
PVE_CONFIG_PATH=/etc/pve
`
if err := os.WriteFile(configPath, []byte(content), 0644); err != nil {
t.Fatalf("Failed to create test config: %v", err)
}

cleanup := setBaseDirEnv(t, "/config-vars/base")
defer cleanup()

cfg, err := LoadConfig(configPath)
if err != nil {
t.Fatalf("LoadConfig() error = %v", err)
}

if cfg.PVEConfigPath != "/etc/pve" {
t.Errorf("PVEConfigPath = %q; want %q", cfg.PVEConfigPath, "/etc/pve")
}
if cfg.CorosyncConfigPath != "/etc/pve/corosync.conf" {
t.Errorf("CorosyncConfigPath = %q; want %q", cfg.CorosyncConfigPath, "/etc/pve/corosync.conf")
}
}

func TestRetentionPolicyDefaultAndExplicit(t *testing.T) {
tmpDir := t.TempDir()
configPath := filepath.Join(tmpDir, "retention_policy.env")
Expand Down
1 change: 1 addition & 0 deletions internal/config/templates/backup.env
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,7 @@ PXAR_FILE_INCLUDE_PATTERN= # Space/comma separated patterns to locate PXA
PXAR_FILE_EXCLUDE_PATTERN= # Patterns to exclude while sampling files (e.g. *.tmp, *.lock)

# Override collection paths (use only if directories differ from defaults)
# Note: $VAR / ${VAR} expansion resolves keys from this file too (no need to export).
PVE_CONFIG_PATH=/etc/pve
PVE_CLUSTER_PATH=/var/lib/pve-cluster
COROSYNC_CONFIG_PATH=${PVE_CONFIG_PATH}/corosync.conf
Expand Down
4 changes: 2 additions & 2 deletions internal/orchestrator/.backup.lock
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
pid=227499
pid=29780
host=pve
time=2026-02-05T20:31:58+01:00
time=2026-02-09T14:30:21+01:00
Comment on lines +1 to +3
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This appears to be a runtime lock file (PID/host/timestamp) that was accidentally committed. Keeping it in the repo can confuse users and may cause spurious diffs; it should be removed from version control and added to .gitignore (or generated at runtime under BACKUP_PATH/LOCK_PATH instead).

Copilot uses AI. Check for mistakes.
Loading