diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index d09ead7..57ad64f 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -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). --- @@ -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 @@ -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 diff --git a/internal/backup/collector_pve.go b/internal/backup/collector_pve.go index f554f11..ae0109b 100644 --- a/internal/backup/collector_pve.go +++ b/internal/backup/collector_pve.go @@ -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, @@ -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") diff --git a/internal/backup/collector_pve_test.go b/internal/backup/collector_pve_test.go index 4c0e309..6d4a808 100644 --- a/internal/backup/collector_pve_test.go +++ b/internal/backup/collector_pve_test.go @@ -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") @@ -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", diff --git a/internal/config/config.go b/internal/config/config.go index a4b250a..8c4cf28 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -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 } @@ -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 + } + + 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 diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 1a6f630..eb26f81 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -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") diff --git a/internal/config/templates/backup.env b/internal/config/templates/backup.env index 9343fb6..f579cff 100644 --- a/internal/config/templates/backup.env +++ b/internal/config/templates/backup.env @@ -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 diff --git a/internal/orchestrator/.backup.lock b/internal/orchestrator/.backup.lock index 1c8685a..e38741a 100644 --- a/internal/orchestrator/.backup.lock +++ b/internal/orchestrator/.backup.lock @@ -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