From 1f3370687fac8bd065ac6b905123ece0f69ff187 Mon Sep 17 00:00:00 2001 From: tis24dev Date: Mon, 9 Feb 2026 13:45:56 +0100 Subject: [PATCH 1/2] Add config var expansion, docs and roadmap Implement ${VAR}/$VAR expansion for scalar config values (config keys first, then environment) by adding a configVarExpander with caching and cycle protection; preserve historical BASE_DIR behavior. Update getString/getStringWithFallback to use the new expander and add a unit test to validate resolving config-key references. Update templates/backup.env and docs/CONFIGURATION.md to document the new expansion behavior and clarify that backup.env values are resolved without exporting. Add a new RESTORE_ROADMAP.md documenting restore coverage, and update internal/orchestrator/.backup.lock timestamps. --- docs/CONFIGURATION.md | 4 +- docs/RESTORE_ROADMAP.md | 48 +++++++++++++++++ internal/config/config.go | 80 ++++++++++++++++++++++++++-- internal/config/config_test.go | 27 ++++++++++ internal/config/templates/backup.env | 1 + internal/orchestrator/.backup.lock | 4 +- 6 files changed, 157 insertions(+), 7 deletions(-) create mode 100644 docs/RESTORE_ROADMAP.md diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index d09ead7..d70d486 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). --- @@ -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/docs/RESTORE_ROADMAP.md b/docs/RESTORE_ROADMAP.md new file mode 100644 index 0000000..ae7ff93 --- /dev/null +++ b/docs/RESTORE_ROADMAP.md @@ -0,0 +1,48 @@ +# Restore roadmap (ProxSave) + +This document tracks which Proxmox configuration areas are currently restored by ProxSave, and what still remains. + +## Implemented + +### Common (automatic) +- Filesystem mounts: **Smart `/etc/fstab` merge** (interactive) that adds only safe candidates and normalizes restored entries with `nofail` (and `_netdev` for network mounts). If ProxSave inventory is present in the backup, it can remap unstable `/dev/*` devices (e.g. `/dev/sdb1`) to stable `UUID=`/`PARTUUID=`/`LABEL=` references on the restore host. + +### PBS (automatic) +- Datastores: staged apply (`/etc/proxmox-backup/datastore.cfg`) with safety checks. Datastore definitions are applied even if the underlying mount is offline (PBS will show them as unavailable). If a datastore path looks like a mount root but resolves to the root filesystem, ProxSave applies a temporary mount guard to prevent writes to `/`. Definitions are only deferred when the path is invalid or contains unexpected entries; deferred blocks are written to `/tmp/proxsave/datastore.cfg.deferred.*`. +- Host & Integrations: staged apply (`/etc/proxmox-backup/{node,s3,metricserver,traffic-control}.cfg` + `/etc/proxmox-backup/acme/{accounts,plugins}.cfg`). +- Proxy/SSL: restores PBS proxy config (`/etc/proxmox-backup/proxy.cfg`) and TLS assets (`/etc/proxmox-backup/proxy.{pem,key}` and `/etc/proxmox-backup/ssl/`) when the `ssl` and `pbs_host` categories are selected. +- Maintenance: file-based restore (`/etc/proxmox-backup/maintenance.cfg`). +- Jobs: staged apply (`/etc/proxmox-backup/{sync,verification,prune}.cfg`). +- Remotes: staged apply (`/etc/proxmox-backup/remote.cfg`). +- Tape Backup: staged apply (`/etc/proxmox-backup/{tape,tape-job,media-pool}.cfg` + `/etc/proxmox-backup/tape-encryption-keys.json`). +- Notifications (targets/matchers): staged apply (`/etc/proxmox-backup/notifications.cfg` + `/etc/proxmox-backup/notifications-priv.cfg`). +- Access control (users/realms/ACL + secrets): staged apply (`/etc/proxmox-backup/{user,domains,acl,token}.cfg` + `{shadow.json,token.shadow,tfa.json}` when present). Safety rail: `root@pam` is preserved from the fresh install and forced `Admin` on `/` (propagate). +- TFA/WebAuthn note: TFA is restored 1:1 as part of access control, but some methods (notably WebAuthn) may require re-enrollment if the UI origin (FQDN/hostname or port) changes. In CUSTOM mode, if access control is selected without `network`/`ssl`, ProxSave suggests adding them for best compatibility and logs detected WebAuthn-enrolled users. + +### PVE (automatic) +- Cluster RECOVERY mode: restores full cluster database (`/var/lib/pve-cluster/config.db`). +- Cluster SAFE mode: exports `/etc/pve` and (optionally) applies datacenter objects via `pvesh`/`pveum`: + - Resource mappings (`/cluster/mapping/{pci,usb,dir}`): applied via `pvesh` when present in the backup (recommended before VM/CT apply if guests use `mapping=`). + - Pools (resource pools): applied via `pveum` (merge: create/update definitions and add membership; optional allow-move for guests). + - VM/CT configs (qemu-server + lxc) + - `storage.cfg` + - `datacenter.cfg` +- Offline mount safety: for mount-backed storages in `storage.cfg` (notably `nfs`, `cifs`, `cephfs`, `glusterfs`, and `dir` on dedicated mountpoints), ProxSave applies a temporary mount guard (read-only bind mount; fallback `chattr +i`) when the mountpoint resolves to the root filesystem at restore time (for network storages this is `/mnt/pve/`). This prevents accidental writes to `/` while storage is offline. +- Notifications (targets/matchers): staged parse + apply via `pvesh` (SAFE mode). +- Access control (realms/roles/groups/users/ACL + secrets): staged file-based apply to pmxcfs (`/etc/pve/{user,domains}.cfg` + `/etc/pve/priv/{shadow,token,tfa}.cfg`) for full 1:1 restore. Safety rail: `root@pam` is preserved from the fresh install and forced `Administrator` on `/` (propagate). Cluster backups in SAFE mode: applying access control is opt-in (cluster-wide) with a rollback timer. +- Firewall: staged file-based apply to pmxcfs (`/etc/pve/firewall/*` + `/etc/pve/nodes//host.fw`) with an optional rollback timer to prevent lockouts (apply + commit; auto-rollback if not committed). +- HA: staged file-based apply to pmxcfs (`/etc/pve/ha/{resources,groups,rules}.cfg`) with an optional rollback timer (apply + commit; auto-rollback if not committed). Note: applying HA config can trigger immediate HA actions (start/stop/migrate); rollback restores config files but cannot undo actions already taken by the HA manager. +- SDN: staged file-based apply to pmxcfs (`/etc/pve/sdn/` and `sdn.cfg` when present). Note: restores SDN definitions only; applying network changes remains a separate, explicit SDN step (UI/CLI). +- TFA/WebAuthn note: TFA is restored 1:1 as part of access control, but some methods (notably WebAuthn) may require re-enrollment if the UI origin (FQDN/hostname or port) changes. In CUSTOM mode, if access control is selected without `network`/`ssl`, ProxSave suggests adding them for best compatibility and logs detected WebAuthn-enrolled users. + +### Export-only (manual review) +- `pve_config_export`: full `/etc/pve` (never written to system paths). +- `pbs_config`: full `/etc/proxmox-backup` (never written to system paths). + +## Known gaps / next candidates + +### PBS +- (none currently tracked here) + +### PVE +- Replication 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..affc218 100644 --- a/internal/orchestrator/.backup.lock +++ b/internal/orchestrator/.backup.lock @@ -1,3 +1,3 @@ -pid=227499 +pid=13642 host=pve -time=2026-02-05T20:31:58+01:00 +time=2026-02-09T13:32:56+01:00 From 5c08e9f686cf31a38a510163c24dc1c440595fdb Mon Sep 17 00:00:00 2001 From: tis24dev Date: Mon, 9 Feb 2026 14:37:17 +0100 Subject: [PATCH 2/2] Treat PVE ACLs in user.cfg; remove roadmap Document and enforce that PVE stores ACLs inside user.cfg: update CONFIGURATION note, change collector to record only user.cfg (adjust description), remove acl.cfg from directory excludes, and update tests accordingly. --- docs/CONFIGURATION.md | 2 +- docs/RESTORE_ROADMAP.md | 48 --------------------------- internal/backup/collector_pve.go | 12 ++----- internal/backup/collector_pve_test.go | 2 -- internal/orchestrator/.backup.lock | 4 +-- 5 files changed, 6 insertions(+), 62 deletions(-) delete mode 100644 docs/RESTORE_ROADMAP.md diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index d70d486..57ad64f 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -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 diff --git a/docs/RESTORE_ROADMAP.md b/docs/RESTORE_ROADMAP.md deleted file mode 100644 index ae7ff93..0000000 --- a/docs/RESTORE_ROADMAP.md +++ /dev/null @@ -1,48 +0,0 @@ -# Restore roadmap (ProxSave) - -This document tracks which Proxmox configuration areas are currently restored by ProxSave, and what still remains. - -## Implemented - -### Common (automatic) -- Filesystem mounts: **Smart `/etc/fstab` merge** (interactive) that adds only safe candidates and normalizes restored entries with `nofail` (and `_netdev` for network mounts). If ProxSave inventory is present in the backup, it can remap unstable `/dev/*` devices (e.g. `/dev/sdb1`) to stable `UUID=`/`PARTUUID=`/`LABEL=` references on the restore host. - -### PBS (automatic) -- Datastores: staged apply (`/etc/proxmox-backup/datastore.cfg`) with safety checks. Datastore definitions are applied even if the underlying mount is offline (PBS will show them as unavailable). If a datastore path looks like a mount root but resolves to the root filesystem, ProxSave applies a temporary mount guard to prevent writes to `/`. Definitions are only deferred when the path is invalid or contains unexpected entries; deferred blocks are written to `/tmp/proxsave/datastore.cfg.deferred.*`. -- Host & Integrations: staged apply (`/etc/proxmox-backup/{node,s3,metricserver,traffic-control}.cfg` + `/etc/proxmox-backup/acme/{accounts,plugins}.cfg`). -- Proxy/SSL: restores PBS proxy config (`/etc/proxmox-backup/proxy.cfg`) and TLS assets (`/etc/proxmox-backup/proxy.{pem,key}` and `/etc/proxmox-backup/ssl/`) when the `ssl` and `pbs_host` categories are selected. -- Maintenance: file-based restore (`/etc/proxmox-backup/maintenance.cfg`). -- Jobs: staged apply (`/etc/proxmox-backup/{sync,verification,prune}.cfg`). -- Remotes: staged apply (`/etc/proxmox-backup/remote.cfg`). -- Tape Backup: staged apply (`/etc/proxmox-backup/{tape,tape-job,media-pool}.cfg` + `/etc/proxmox-backup/tape-encryption-keys.json`). -- Notifications (targets/matchers): staged apply (`/etc/proxmox-backup/notifications.cfg` + `/etc/proxmox-backup/notifications-priv.cfg`). -- Access control (users/realms/ACL + secrets): staged apply (`/etc/proxmox-backup/{user,domains,acl,token}.cfg` + `{shadow.json,token.shadow,tfa.json}` when present). Safety rail: `root@pam` is preserved from the fresh install and forced `Admin` on `/` (propagate). -- TFA/WebAuthn note: TFA is restored 1:1 as part of access control, but some methods (notably WebAuthn) may require re-enrollment if the UI origin (FQDN/hostname or port) changes. In CUSTOM mode, if access control is selected without `network`/`ssl`, ProxSave suggests adding them for best compatibility and logs detected WebAuthn-enrolled users. - -### PVE (automatic) -- Cluster RECOVERY mode: restores full cluster database (`/var/lib/pve-cluster/config.db`). -- Cluster SAFE mode: exports `/etc/pve` and (optionally) applies datacenter objects via `pvesh`/`pveum`: - - Resource mappings (`/cluster/mapping/{pci,usb,dir}`): applied via `pvesh` when present in the backup (recommended before VM/CT apply if guests use `mapping=`). - - Pools (resource pools): applied via `pveum` (merge: create/update definitions and add membership; optional allow-move for guests). - - VM/CT configs (qemu-server + lxc) - - `storage.cfg` - - `datacenter.cfg` -- Offline mount safety: for mount-backed storages in `storage.cfg` (notably `nfs`, `cifs`, `cephfs`, `glusterfs`, and `dir` on dedicated mountpoints), ProxSave applies a temporary mount guard (read-only bind mount; fallback `chattr +i`) when the mountpoint resolves to the root filesystem at restore time (for network storages this is `/mnt/pve/`). This prevents accidental writes to `/` while storage is offline. -- Notifications (targets/matchers): staged parse + apply via `pvesh` (SAFE mode). -- Access control (realms/roles/groups/users/ACL + secrets): staged file-based apply to pmxcfs (`/etc/pve/{user,domains}.cfg` + `/etc/pve/priv/{shadow,token,tfa}.cfg`) for full 1:1 restore. Safety rail: `root@pam` is preserved from the fresh install and forced `Administrator` on `/` (propagate). Cluster backups in SAFE mode: applying access control is opt-in (cluster-wide) with a rollback timer. -- Firewall: staged file-based apply to pmxcfs (`/etc/pve/firewall/*` + `/etc/pve/nodes//host.fw`) with an optional rollback timer to prevent lockouts (apply + commit; auto-rollback if not committed). -- HA: staged file-based apply to pmxcfs (`/etc/pve/ha/{resources,groups,rules}.cfg`) with an optional rollback timer (apply + commit; auto-rollback if not committed). Note: applying HA config can trigger immediate HA actions (start/stop/migrate); rollback restores config files but cannot undo actions already taken by the HA manager. -- SDN: staged file-based apply to pmxcfs (`/etc/pve/sdn/` and `sdn.cfg` when present). Note: restores SDN definitions only; applying network changes remains a separate, explicit SDN step (UI/CLI). -- TFA/WebAuthn note: TFA is restored 1:1 as part of access control, but some methods (notably WebAuthn) may require re-enrollment if the UI origin (FQDN/hostname or port) changes. In CUSTOM mode, if access control is selected without `network`/`ssl`, ProxSave suggests adding them for best compatibility and logs detected WebAuthn-enrolled users. - -### Export-only (manual review) -- `pve_config_export`: full `/etc/pve` (never written to system paths). -- `pbs_config`: full `/etc/proxmox-backup` (never written to system paths). - -## Known gaps / next candidates - -### PBS -- (none currently tracked here) - -### PVE -- Replication 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/orchestrator/.backup.lock b/internal/orchestrator/.backup.lock index affc218..e38741a 100644 --- a/internal/orchestrator/.backup.lock +++ b/internal/orchestrator/.backup.lock @@ -1,3 +1,3 @@ -pid=13642 +pid=29780 host=pve -time=2026-02-09T13:32:56+01:00 +time=2026-02-09T14:30:21+01:00