Skip to content

Add incremental backup and restore#609

Open
Chyenne8 wants to merge 2 commits into
theforeman:masterfrom
Chyenne8:incremental-backup-restore
Open

Add incremental backup and restore#609
Chyenne8 wants to merge 2 commits into
theforeman:masterfrom
Chyenne8:incremental-backup-restore

Conversation

@Chyenne8

@Chyenne8 Chyenne8 commented Jun 30, 2026

Copy link
Copy Markdown

Summary

Implements full incremental backup/restore capability using GNU tar's --listed-incremental mechanism. Reduces backup time and storage for large Pulp deployments by only backing up changed files.

Depends on: #549 (restore command)

Changes

  • Add --incremental parameter to backup command
  • Modified tar commands to use --listed-incremental with .snar snapshot files
  • .config.snar tracks foremanctl state changes
  • .pulp.snar tracks Pulp content changes
  • Add --chain flag to restore command for automatic backup chain handling
  • Auto-detect incremental backups from metadata
  • Validate backup chain completeness before restore
  • Enhanced error messages suggest alternative backups when .snar missing

Usage

Create full backup (generates .snar files):

foremanctl backup /backups

Create incremental backup:

foremanctl backup /backups --incremental /backups/foreman-backup-20260629T120000

Restore with auto-chain (recommended):

foremanctl restore /backups/foreman-backup-20260701T080000 --chain

Manual chain restore:

foremanctl restore /backups/foreman-backup-20260629T120000
foremanctl restore /backups/foreman-backup-20260630T080000 --force
foremanctl restore /backups/foreman-backup-20260701T080000 --force

How to test this pull request

Get a foremanctl box with normal deploy. On this box, clone foremanctl repo and checkout this branch:

cd /root/foremanctl
source .venv/bin/activate
export OBSAH_STATE=/var/lib/foremanctl

Test incremental backup:

# Create full backup
./foremanctl backup /backups

# Verify .snar files created
ls -la /backups/foreman-backup-*/.snar

# Create incremental backup
./foremanctl backup /backups --incremental /backups/foreman-backup-*

# Verify incremental is smaller
du -sh /backups/foreman-backup-*

Test incremental restore with --chain:

# Restore incremental with auto-chain
./foremanctl restore /backups/foreman-backup-* --chain --validate

Implements offline backup restore functionality with the following features:

Validation:
- Metadata-driven validation for different flavors
- Verifies backup directory structure and required files
- Validates database dumps and foremanctl state archive exist
- Supports dry-run mode to validate without restoring

Restore Process:
- Stops all Foreman services cleanly
- Restores databases using pg_restore with postgres user
- Fixes database ownership after restore
- Restores Pulp content and encryption keys
- Restores foremanctl state (OAuth keys, passwords, configuration)
- Integrates deploy roles to regenerate configuration
- Verifies services after deployment

Error Handling:
- Rescue block stops services on failure
- Leaves system in safe stopped state for investigation
- Provides clear recovery instructions

Implementation Details:
- Derives expected files from backup metadata instead of hardcoding
- Uses regex append for file list generation
- Removes state tracking variables for simplicity
- No debug messages per codebase patterns
- Explicit PostgreSQL stop to handle systemd dependencies

Documentation:
- Comprehensive user guide at docs/user/restore.md
- Covers usage, prerequisites, troubleshooting, and best practices
@Chyenne8 Chyenne8 force-pushed the incremental-backup-restore branch from 5ebe24f to 2ecaa0d Compare June 30, 2026 17:47
Implements full incremental backup/restore capability using GNU tar's
--listed-incremental mechanism. Reduces backup time and storage for large
Pulp deployments by only backing up changed files.

Backup features:

Add --incremental CLI parameter:
  foremanctl backup /backups --incremental /backups/foreman-backup-TIMESTAMP

Implementation:
- Modified tar commands to use --listed-incremental with .snar snapshot files
- .config.snar tracks foremanctl state changes
- .pulp.snar tracks Pulp content changes
- Replaced community.general.archive with raw tar for --listed-incremental support
- Copy .snar files from previous backup before tar runs
- Validate previous backup exists and contains .snar files
- Enhanced error messages suggest alternative backups when .snar missing
- Metadata records: is_incremental, base_backup_dir, base_backup_timestamp

Error handling automation:
- Scans parent directory for backups with .snar support
- Lists available alternatives with type and timestamp
- Provides ready-to-use command with suggested backup

Restore features:

Auto-detect incremental backups from metadata:
- Parse metadata.yml to identify backup type
- Display full vs incremental in validation output

Backup chain validation:
- Verify base backup directory exists
- Confirm base backup metadata matches expected timestamp
- Check if base backup already restored via .last_restore_timestamp
- Fail with clear instructions if chain incomplete

Add --restore-chain for automatic chain restore:
  foremanctl restore /backups/foreman-backup-TIMESTAMP --restore-chain

Chain restore automation:
- Recursively walks metadata to build full dependency chain
- Restores backups in chronological order (full -> inc1 -> inc2)
- Databases restored only from full backup
- Incrementals apply file changes using --listed-incremental=/dev/null
- Eliminates manual multi-step restore process

Handle .snar files during restore:
- Detect .snar presence to identify incremental archives
- Use tar --listed-incremental=/dev/null for extraction
- Fall back to unarchive module for full backups

Usage examples:

Create full backup (generates .snar files):
  foremanctl backup /backups

Create incremental backup:
  foremanctl backup /backups --incremental /backups/foreman-backup-20260629T120000

Restore with auto-chain (recommended):
  foremanctl restore /backups/foreman-backup-20260701T080000 --restore-chain

Manual chain restore:
  foremanctl restore /backups/foreman-backup-20260629T120000
  foremanctl restore /backups/foreman-backup-20260630T080000 --force
  foremanctl restore /backups/foreman-backup-20260701T080000 --force

Validate incremental backup:
  foremanctl restore /backups/foreman-backup-TIMESTAMP --validate

Testing:

Backup:
- Full backup creates .config.snar and .pulp.snar files
- Incremental backup significantly smaller than full
- Error with suggestions when .snar files missing
- Metadata correctly tracks incremental status

Restore:
- Incremental detected from metadata
- Chain validation prevents incomplete restores
- --restore-chain automatically handles dependencies
- Clear errors for missing base backups

Files modified:

Backup:
- src/playbooks/backup/metadata.obsah.yaml
- src/roles/backup/tasks/main.yaml
- src/roles/backup/tasks/pulp_content.yaml
- src/roles/backup/tasks/metadata.yaml
- docs/user/backup.md

Restore:
- src/playbooks/restore/metadata.obsah.yaml
- src/roles/restore/tasks/validate.yaml
- src/roles/restore/tasks/main.yaml
- src/roles/restore/tasks/restore_pulp_content.yaml
- src/roles/restore/tasks/restore_foremanctl_state.yaml
- src/roles/restore/tasks/restore_databases.yaml
- src/roles/restore/tasks/restore_chain.yaml (new)
- src/roles/restore/tasks/restore_chain_walk.yaml (new)
- src/roles/restore/tasks/restore_single.yaml (new)
- docs/user/restore.md
@Chyenne8 Chyenne8 force-pushed the incremental-backup-restore branch from 2ecaa0d to bd5f415 Compare June 30, 2026 17:55
@sjha4

sjha4 commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

I like the idea here: Restore with auto-chain (recommended): We however do not have this in the foreman-maintain implementation..I can test it out and we can take a call with the team if it makes sense to have this.

@sjha4

sjha4 commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

One note about: ./foremanctl backup /backups --incremental /backups/foreman-backup-*

There is some difference between command structure with foreman-maintain for incremental backups here: https://docs.theforeman.org/nightly/Administering_Project/index-katello.html#Performing_an_Incremental_Backup_admin

We'd like to stay close to the foreman-maintain command.

@sjha4

sjha4 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

This PR adds incremental backup support to the offline backup playbook (via a new --incremental option and tar --listed-incremental/.snar files) and introduces a new restore role, playbook, and CLI metadata capable of restoring full backups or chained incremental backups. Restore logic includes validation, chain discovery/ordering, database/Pulp/foremanctl-state restoration, and post-restore verification. Documentation for both backup and restore workflows is added/updated.

Estimated code review effort: 4 (Complex) | ~75 minutes

Possibly related PRs

  • theforeman/foremanctl#507: Both PRs modify the offline backup playbook metadata (src/playbooks/backup/metadata.obsah.yaml), with this PR extending it for the --incremental option.

Suggested reviewers: ehelms, ianballou

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding incremental backup and restore support.
Description check ✅ Passed The description is directly related to the changeset and accurately describes the backup and restore enhancements.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 9

🧹 Nitpick comments (4)
src/roles/restore/tasks/validate.yaml (1)

56-60: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated, convoluted incremental-detection logic.

The same fallback expression (incremental.is_incremental | default(incremental | default(false))) is duplicated verbatim in restore_chain_walk.yaml (lines 11-14). Consider extracting this into a small shared include_tasks (e.g. parse_backup_metadata.yaml) that both files call, to avoid the two copies drifting if the metadata schema changes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/roles/restore/tasks/validate.yaml` around lines 56 - 60, The incremental
backup detection logic in the restore metadata parsing is duplicated and should
be centralized. Move the repeated fallback expression used by the restore task’s
set_fact block into a shared include_tasks helper such as
parse_backup_metadata.yaml, then have both validate.yaml and
restore_chain_walk.yaml call that shared task. Keep the logic for
restore_is_incremental, restore_base_backup_dir, and
restore_base_backup_timestamp in one place so schema changes only need to be
updated once.
src/roles/restore/tasks/restore_chain_walk.yaml (2)

16-34: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

No cycle/depth guard on recursive chain walk.

If backup metadata is corrupted (e.g. a base pointing back into a cycle, or an extremely long chain), this recursive include_tasks has no visited-set or max-depth check, risking unbounded recursion/growth of restore_backup_chain. Consider tracking visited directories and failing fast on a repeat.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/roles/restore/tasks/restore_chain_walk.yaml` around lines 16 - 34, The
recursive chain walk in restore_chain_walk.yaml has no protection against cycles
or excessively deep chains, so add a guard before the recursive include in the
restore chain task block. Update the logic around restore_chain_is_incremental,
restore_chain_base_stat, and the include_tasks call to track visited base
directories (or a max depth) and fail fast if a directory repeats or the chain
grows too long. Use the existing restore_backup_chain and restore_chain_current
facts to implement the check, and ensure the recursive restore_chain_walk.yaml
invocation only proceeds when the next base backup has not already been seen.

19-24: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Generic stat failure vs. the friendly chain-error messages elsewhere.

validate.yaml gives detailed "INCOMPLETE BACKUP CHAIN" guidance when a base backup is missing (one level deep). Here, a missing base deeper in the chain fails with a bare stat error, which is less helpful and inconsistent with the PR's stated goal of clearer error messages for missing chain members.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/roles/restore/tasks/restore_chain_walk.yaml` around lines 19 - 24, The
base-backup existence check in the restore chain walk currently fails with a
generic stat error instead of the friendly chain guidance used elsewhere. Update
the task in restore_chain_walk.yaml that uses ansible.builtin.stat and
failed_when so it emits the same “INCOMPLETE BACKUP CHAIN” style message as
validate.yaml when restore_chain_base_stat.stat.exists is false, keeping the
missing-base behavior consistent for deeper chain members.
src/roles/restore/tasks/restore_pulp_content.yaml (1)

54-59: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Recursive file count could be slow on large Pulp media trees.

find with recurse: true walks the entire Pulp media directory just to report a count. For the large Pulp deployments this PR is optimizing for, this can add significant time to every restore.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/roles/restore/tasks/restore_pulp_content.yaml` around lines 54 - 59, The
Count restored media files task in restore_pulp_content.yaml is doing an
expensive recursive walk of the entire media tree via ansible.builtin.find with
recurse:true. Replace that count-only lookup with a cheaper approach that avoids
traversing every file in pulp_storage_path/media, using the
restore_pulp_media_files task as the place to update the logic while still
producing the restored media count needed by the role.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/roles/backup/tasks/main.yaml`:
- Around line 113-224: The incremental backup validation currently always
requires both .config.snar and .pulp.snar, which breaks valid runs when
skip_pulp_content is enabled. Update the .snar check in the backup task flow to
conditionally exclude .pulp.snar whenever the current run skips pulp content,
and make the downstream missing-file/suggestion logic use that filtered list.
Use the existing backup_snar_files_check, skip_pulp_content, and the fail blocks
in main.yaml to keep the behavior aligned with what pulp_content.yaml actually
writes.
- Around line 141-190: The backup suggestion build in
backup_alternatives_with_snar/backup_alternatives_metadata is misaligning paths
and metadata because backup_suggestions filters out failed slurp entries but the
later loop still indexes into the unfiltered path list. Fix this by pairing each
metadata result with its source path in a single pass, or by filtering and
mapping path/content together before building backup_suggestions_formatted, so
the combine step in the set_fact task never relies on idx from a filtered list.
Use the existing tasks around Build suggestions list with backup info and
Combine paths with metadata to keep each metadata.yml matched to the correct
backup directory.

In `@src/roles/restore/tasks/restore_databases.yaml`:
- Around line 2-7: The PostgreSQL admin secret creation task in the restore
workflow is non-idempotent and currently hides real failures. Update the task
around the podman secret creation step so it checks whether
postgresql-admin-password already exists before creating it, or
deletes/recreates it safely when needed; remove the blanket failure suppression
so genuine errors from the secret management step are surfaced. Use the existing
restore_databases task and the secret name postgresql-admin-password to locate
the change.
- Around line 136-151: The restore task in restore_databases.yaml is using
pg_restore with -U item.owner but still setting PGPASSWORD to
postgresql_admin_password, so authentication can fail if the owner password
differs. Update the Restore databases from dump files task to use the correct
password source for each database owner, using the same per-database secret
pattern captured in restore_foremanctl_state.yaml or otherwise ensuring the
password matches item.owner. Also remove the blanket failed_when: false from
this task, or replace it with a condition that only tolerates expected cases, so
failures in ansible.builtin.command are surfaced instead of being silently
reported as success.

In `@src/roles/restore/tasks/restore_foremanctl_state.yaml`:
- Around line 2-6: The restore task currently deletes the target state directory
in a way that breaks chain restores across incremental layers. Update the
restore flow so `restore_foremanctl_state` only clears `obsah_state_path` for
the initial/base restore path, and ensure `restore_single` does not remove the
directory before applying each incremental archive; keep later tar incrementals
applied cumulatively onto the same tree.

In `@src/roles/restore/tasks/restore_single.yaml`:
- Around line 24-27: Chain restore is restoring the wrong database snapshot
because `restore_single_is_first` only runs `restore_databases.yaml` for the
base backup. Update `restore_single.yaml` and the `restore_databases.yaml` flow
so PostgreSQL bootstrap/start-stop still happens once at the beginning of the
chain, but the actual `pg_restore` for the database runs on the last backup
instead of the first. Use the `restore_single_is_first` condition (or a new
end-of-chain check) to ensure later backup DB dumps are applied and the final
restored state matches the latest backup.

In `@src/roles/restore/tasks/validate.yaml`:
- Line 222: The shell command in the restore validation task interpolates
backup_dir directly, which can break or be abused when the path contains spaces
or shell metacharacters. Update the ansible.builtin.shell usage in the validate
task to safely quote backup_dir with the existing du/awk pipeline, or switch to
a safer module if possible. Use the backup_dir variable in this shell step only
through quoting so the restore size check remains functional under become: true.
- Around line 240-251: The Pulp disk-space validation is unreachable because Get
Pulp content archive size uses restore_pulp_archive in its own when condition
before the register value exists. Remove that self-referential guard and gate
the stat task on the appropriate restore/archive prerequisite instead, so
restore_pulp_archive is actually populated; then keep Warn if insufficient Pulp
disk space dependent on restore_pulp_archive.stat.exists and restore_pulp_space
as intended.
- Around line 2-25: The existing-deployment guard in the restore validation task
is too broad and will fail on later members of a restore chain after the first
`restore_single` creates `/var/lib/foremanctl`. Update the `validate.yaml` logic
around `restore_existing_deployment` and the `ansible.builtin.fail` step so this
check only applies to the first backup in a chain (or otherwise skips subsequent
chain members), while still preserving the `force` and `validate` conditions.

---

Nitpick comments:
In `@src/roles/restore/tasks/restore_chain_walk.yaml`:
- Around line 16-34: The recursive chain walk in restore_chain_walk.yaml has no
protection against cycles or excessively deep chains, so add a guard before the
recursive include in the restore chain task block. Update the logic around
restore_chain_is_incremental, restore_chain_base_stat, and the include_tasks
call to track visited base directories (or a max depth) and fail fast if a
directory repeats or the chain grows too long. Use the existing
restore_backup_chain and restore_chain_current facts to implement the check, and
ensure the recursive restore_chain_walk.yaml invocation only proceeds when the
next base backup has not already been seen.
- Around line 19-24: The base-backup existence check in the restore chain walk
currently fails with a generic stat error instead of the friendly chain guidance
used elsewhere. Update the task in restore_chain_walk.yaml that uses
ansible.builtin.stat and failed_when so it emits the same “INCOMPLETE BACKUP
CHAIN” style message as validate.yaml when restore_chain_base_stat.stat.exists
is false, keeping the missing-base behavior consistent for deeper chain members.

In `@src/roles/restore/tasks/restore_pulp_content.yaml`:
- Around line 54-59: The Count restored media files task in
restore_pulp_content.yaml is doing an expensive recursive walk of the entire
media tree via ansible.builtin.find with recurse:true. Replace that count-only
lookup with a cheaper approach that avoids traversing every file in
pulp_storage_path/media, using the restore_pulp_media_files task as the place to
update the logic while still producing the restored media count needed by the
role.

In `@src/roles/restore/tasks/validate.yaml`:
- Around line 56-60: The incremental backup detection logic in the restore
metadata parsing is duplicated and should be centralized. Move the repeated
fallback expression used by the restore task’s set_fact block into a shared
include_tasks helper such as parse_backup_metadata.yaml, then have both
validate.yaml and restore_chain_walk.yaml call that shared task. Keep the logic
for restore_is_incremental, restore_base_backup_dir, and
restore_base_backup_timestamp in one place so schema changes only need to be
updated once.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 90c6b150-79b6-4003-aba5-5b21db4f9dd7

📥 Commits

Reviewing files that changed from the base of the PR and between e5e86e2 and bd5f415.

📒 Files selected for processing (19)
  • docs/user/backup.md
  • docs/user/restore.md
  • src/playbooks/backup/metadata.obsah.yaml
  • src/playbooks/restore/metadata.obsah.yaml
  • src/playbooks/restore/restore.yaml
  • src/roles/backup/tasks/database_dumps.yaml
  • src/roles/backup/tasks/main.yaml
  • src/roles/backup/tasks/metadata.yaml
  • src/roles/backup/tasks/pulp_content.yaml
  • src/roles/restore/defaults/main.yaml
  • src/roles/restore/tasks/main.yaml
  • src/roles/restore/tasks/prepare_system.yaml
  • src/roles/restore/tasks/restore_chain.yaml
  • src/roles/restore/tasks/restore_chain_walk.yaml
  • src/roles/restore/tasks/restore_databases.yaml
  • src/roles/restore/tasks/restore_foremanctl_state.yaml
  • src/roles/restore/tasks/restore_pulp_content.yaml
  • src/roles/restore/tasks/restore_single.yaml
  • src/roles/restore/tasks/validate.yaml

Comment on lines +113 to +224
- name: Check for required .snar files in previous backup
ansible.builtin.stat:
path: "{{ incremental }}/{{ item }}"
register: backup_snar_files_check
loop:
- ".config.snar"
- ".pulp.snar"
failed_when: false

- name: Find alternative backups with .snar files if current is missing
when: backup_snar_files_check.results | selectattr('stat.exists', 'equalto', false) | list | length > 0
block:
- name: Find all backup directories in parent directory
ansible.builtin.find:
paths: "{{ incremental | dirname }}"
file_type: directory
patterns: "foreman-backup-*"
register: backup_alternative_dirs
failed_when: false

- name: Check for .snar files in alternative backups
ansible.builtin.stat:
path: "{{ item.path }}/.config.snar"
register: backup_alternative_snar_check
loop: "{{ backup_alternative_dirs.files | default([]) }}"
failed_when: false
when: backup_alternative_dirs.matched | default(0) > 0

- name: Build list of backups with .snar support
ansible.builtin.set_fact:
backup_alternatives_with_snar: >-
{{
backup_alternative_snar_check.results | default([])
| selectattr('stat.exists', 'defined')
| selectattr('stat.exists', 'equalto', true)
| map(attribute='item')
| map(attribute='path')
| list
}}
when: backup_alternative_snar_check.results | default([]) | length > 0

- name: Read metadata for alternative backups
ansible.builtin.slurp:
path: "{{ item }}/metadata.yml"
register: backup_alternatives_metadata
loop: "{{ backup_alternatives_with_snar | default([]) }}"
failed_when: false
when: backup_alternatives_with_snar | default([]) | length > 0

- name: Build suggestions list with backup info
ansible.builtin.set_fact:
backup_suggestions: >-
{{
backup_alternatives_metadata.results | default([])
| selectattr('content', 'defined')
| map(attribute='content')
| map('b64decode')
| map('from_yaml')
| map('combine', {'path': ''})
| list
}}
when: backup_alternatives_metadata.results | default([]) | length > 0

- name: Combine paths with metadata
ansible.builtin.set_fact:
backup_suggestions_formatted: >-
{{
backup_suggestions_formatted | default([]) +
[{
'path': backup_alternatives_with_snar[idx],
'timestamp': item.timestamp,
'type': 'Full' if not (item.incremental.is_incremental | default(item.incremental | default(false)) | bool) else 'Incremental'
}]
}}
loop: "{{ backup_suggestions | default([]) }}"
loop_control:
index_var: idx
when: backup_suggestions | default([]) | length > 0

- name: Fail with suggestions if alternatives exist
ansible.builtin.fail:
msg: |
Incremental backup requested, but previous backup is missing .snar files.
Previous backup: {{ incremental }}
Missing files: {{ backup_snar_files_check.results | selectattr('stat.exists', 'equalto', false) | map(attribute='item') | list }}

This usually means the previous backup was not created with incremental support.
You must use a backup created with foremanctl >= 2.x as the base for incremental backups.

Available backups with incremental support:
{% for suggestion in backup_suggestions_formatted | default([]) | sort(attribute='timestamp', reverse=true) %}
- {{ suggestion.path }} ({{ suggestion.type }}, {{ suggestion.timestamp }})
{% endfor %}

Retry with one of the above backups, for example:
foremanctl backup {{ backup_dir }} --incremental {{ (backup_suggestions_formatted | default([]) | sort(attribute='timestamp', reverse=true) | first).path }}
when: backup_suggestions_formatted | default([]) | length > 0

- name: Fail without suggestions if no alternatives
ansible.builtin.fail:
msg: |
Incremental backup requested, but previous backup is missing .snar files.
Previous backup: {{ incremental }}
Missing files: {{ backup_snar_files_check.results | selectattr('stat.exists', 'equalto', false) | map(attribute='item') | list }}

This usually means the previous backup was not created with incremental support.
You must use a backup created with foremanctl >= 2.x as the base for incremental backups.

No other backups with incremental support were found in {{ incremental | dirname }}.
Create a new full backup first:
foremanctl backup {{ backup_dir }}
when: backup_suggestions_formatted | default([]) | length == 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

.snar validation ignores skip_pulp_content, causing spurious incremental-backup failures.

The check at Lines 113-120 always verifies both .config.snar and .pulp.snar exist in the previous backup, regardless of whether the current run also passes --skip-pulp-content. Since pulp_content.yaml only writes .pulp.snar when: not skip_pulp_content (main.yaml Line 250), any prior backup that used --skip-pulp-content will never have a .pulp.snar. If a user consistently runs --skip-pulp-content --incremental PREV_DIR, this legitimate combination will trigger the "find alternative backups" workflow and can hard-fail the backup (Lines 192-224) even though .pulp.snar is irrelevant for this run.

🛠️ Proposed fix: skip the `.pulp.snar` check when pulp content is skipped
         - name: Check for required .snar files in previous backup
           ansible.builtin.stat:
             path: "{{ incremental }}/{{ item }}"
           register: backup_snar_files_check
-          loop:
-            - ".config.snar"
-            - ".pulp.snar"
+          loop: >-
+            {{
+              ['.config.snar'] +
+              ([] if (skip_pulp_content | default(false)) else ['.pulp.snar'])
+            }}
           failed_when: false
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Check for required .snar files in previous backup
ansible.builtin.stat:
path: "{{ incremental }}/{{ item }}"
register: backup_snar_files_check
loop:
- ".config.snar"
- ".pulp.snar"
failed_when: false
- name: Find alternative backups with .snar files if current is missing
when: backup_snar_files_check.results | selectattr('stat.exists', 'equalto', false) | list | length > 0
block:
- name: Find all backup directories in parent directory
ansible.builtin.find:
paths: "{{ incremental | dirname }}"
file_type: directory
patterns: "foreman-backup-*"
register: backup_alternative_dirs
failed_when: false
- name: Check for .snar files in alternative backups
ansible.builtin.stat:
path: "{{ item.path }}/.config.snar"
register: backup_alternative_snar_check
loop: "{{ backup_alternative_dirs.files | default([]) }}"
failed_when: false
when: backup_alternative_dirs.matched | default(0) > 0
- name: Build list of backups with .snar support
ansible.builtin.set_fact:
backup_alternatives_with_snar: >-
{{
backup_alternative_snar_check.results | default([])
| selectattr('stat.exists', 'defined')
| selectattr('stat.exists', 'equalto', true)
| map(attribute='item')
| map(attribute='path')
| list
}}
when: backup_alternative_snar_check.results | default([]) | length > 0
- name: Read metadata for alternative backups
ansible.builtin.slurp:
path: "{{ item }}/metadata.yml"
register: backup_alternatives_metadata
loop: "{{ backup_alternatives_with_snar | default([]) }}"
failed_when: false
when: backup_alternatives_with_snar | default([]) | length > 0
- name: Build suggestions list with backup info
ansible.builtin.set_fact:
backup_suggestions: >-
{{
backup_alternatives_metadata.results | default([])
| selectattr('content', 'defined')
| map(attribute='content')
| map('b64decode')
| map('from_yaml')
| map('combine', {'path': ''})
| list
}}
when: backup_alternatives_metadata.results | default([]) | length > 0
- name: Combine paths with metadata
ansible.builtin.set_fact:
backup_suggestions_formatted: >-
{{
backup_suggestions_formatted | default([]) +
[{
'path': backup_alternatives_with_snar[idx],
'timestamp': item.timestamp,
'type': 'Full' if not (item.incremental.is_incremental | default(item.incremental | default(false)) | bool) else 'Incremental'
}]
}}
loop: "{{ backup_suggestions | default([]) }}"
loop_control:
index_var: idx
when: backup_suggestions | default([]) | length > 0
- name: Fail with suggestions if alternatives exist
ansible.builtin.fail:
msg: |
Incremental backup requested, but previous backup is missing .snar files.
Previous backup: {{ incremental }}
Missing files: {{ backup_snar_files_check.results | selectattr('stat.exists', 'equalto', false) | map(attribute='item') | list }}
This usually means the previous backup was not created with incremental support.
You must use a backup created with foremanctl >= 2.x as the base for incremental backups.
Available backups with incremental support:
{% for suggestion in backup_suggestions_formatted | default([]) | sort(attribute='timestamp', reverse=true) %}
- {{ suggestion.path }} ({{ suggestion.type }}, {{ suggestion.timestamp }})
{% endfor %}
Retry with one of the above backups, for example:
foremanctl backup {{ backup_dir }} --incremental {{ (backup_suggestions_formatted | default([]) | sort(attribute='timestamp', reverse=true) | first).path }}
when: backup_suggestions_formatted | default([]) | length > 0
- name: Fail without suggestions if no alternatives
ansible.builtin.fail:
msg: |
Incremental backup requested, but previous backup is missing .snar files.
Previous backup: {{ incremental }}
Missing files: {{ backup_snar_files_check.results | selectattr('stat.exists', 'equalto', false) | map(attribute='item') | list }}
This usually means the previous backup was not created with incremental support.
You must use a backup created with foremanctl >= 2.x as the base for incremental backups.
No other backups with incremental support were found in {{ incremental | dirname }}.
Create a new full backup first:
foremanctl backup {{ backup_dir }}
when: backup_suggestions_formatted | default([]) | length == 0
- name: Check for required .snar files in previous backup
ansible.builtin.stat:
path: "{{ incremental }}/{{ item }}"
register: backup_snar_files_check
loop: >-
{{
['.config.snar'] +
([] if (skip_pulp_content | default(false)) else ['.pulp.snar'])
}}
failed_when: false
- name: Find alternative backups with .snar files if current is missing
when: backup_snar_files_check.results | selectattr('stat.exists', 'equalto', false) | list | length > 0
block:
- name: Find all backup directories in parent directory
ansible.builtin.find:
paths: "{{ incremental | dirname }}"
file_type: directory
patterns: "foreman-backup-*"
register: backup_alternative_dirs
failed_when: false
- name: Check for .snar files in alternative backups
ansible.builtin.stat:
path: "{{ item.path }}/.config.snar"
register: backup_alternative_snar_check
loop: "{{ backup_alternative_dirs.files | default([]) }}"
failed_when: false
when: backup_alternative_dirs.matched | default(0) > 0
- name: Build list of backups with .snar support
ansible.builtin.set_fact:
backup_alternatives_with_snar: >-
{{
backup_alternative_snar_check.results | default([])
| selectattr('stat.exists', 'defined')
| selectattr('stat.exists', 'equalto', true)
| map(attribute='item')
| map(attribute='path')
| list
}}
when: backup_alternative_snar_check.results | default([]) | length > 0
- name: Read metadata for alternative backups
ansible.builtin.slurp:
path: "{{ item }}/metadata.yml"
register: backup_alternatives_metadata
loop: "{{ backup_alternatives_with_snar | default([]) }}"
failed_when: false
when: backup_alternatives_with_snar | default([]) | length > 0
- name: Build suggestions list with backup info
ansible.builtin.set_fact:
backup_suggestions: >-
{{
backup_alternatives_metadata.results | default([])
| selectattr('content', 'defined')
| map(attribute='content')
| map('b64decode')
| map('from_yaml')
| map('combine', {'path': ''})
| list
}}
when: backup_alternatives_metadata.results | default([]) | length > 0
- name: Combine paths with metadata
ansible.builtin.set_fact:
backup_suggestions_formatted: >-
{{
backup_suggestions_formatted | default([]) +
[{
'path': backup_alternatives_with_snar[idx],
'timestamp': item.timestamp,
'type': 'Full' if not (item.incremental.is_incremental | default(item.incremental | default(false)) | bool) else 'Incremental'
}]
}}
loop: "{{ backup_suggestions | default([]) }}"
loop_control:
index_var: idx
when: backup_suggestions | default([]) | length > 0
- name: Fail with suggestions if alternatives exist
ansible.builtin.fail:
msg: |
Incremental backup requested, but previous backup is missing .snar files.
Previous backup: {{ incremental }}
Missing files: {{ backup_snar_files_check.results | selectattr('stat.exists', 'equalto', false) | map(attribute='item') | list }}
This usually means the previous backup was not created with incremental support.
You must use a backup created with foremanctl >= 2.x as the base for incremental backups.
Available backups with incremental support:
{% for suggestion in backup_suggestions_formatted | default([]) | sort(attribute='timestamp', reverse=true) %}
- {{ suggestion.path }} ({{ suggestion.type }}, {{ suggestion.timestamp }})
{% endfor %}
Retry with one of the above backups, for example:
foremanctl backup {{ backup_dir }} --incremental {{ (backup_suggestions_formatted | default([]) | sort(attribute='timestamp', reverse=true) | first).path }}
when: backup_suggestions_formatted | default([]) | length > 0
- name: Fail without suggestions if no alternatives
ansible.builtin.fail:
msg: |
Incremental backup requested, but previous backup is missing .snar files.
Previous backup: {{ incremental }}
Missing files: {{ backup_snar_files_check.results | selectattr('stat.exists', 'equalto', false) | map(attribute='item') | list }}
This usually means the previous backup was not created with incremental support.
You must use a backup created with foremanctl >= 2.x as the base for incremental backups.
No other backups with incremental support were found in {{ incremental | dirname }}.
Create a new full backup first:
foremanctl backup {{ backup_dir }}
when: backup_suggestions_formatted | default([]) | length == 0
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/roles/backup/tasks/main.yaml` around lines 113 - 224, The incremental
backup validation currently always requires both .config.snar and .pulp.snar,
which breaks valid runs when skip_pulp_content is enabled. Update the .snar
check in the backup task flow to conditionally exclude .pulp.snar whenever the
current run skips pulp content, and make the downstream missing-file/suggestion
logic use that filtered list. Use the existing backup_snar_files_check,
skip_pulp_content, and the fail blocks in main.yaml to keep the behavior aligned
with what pulp_content.yaml actually writes.

Comment on lines +141 to +190
- name: Build list of backups with .snar support
ansible.builtin.set_fact:
backup_alternatives_with_snar: >-
{{
backup_alternative_snar_check.results | default([])
| selectattr('stat.exists', 'defined')
| selectattr('stat.exists', 'equalto', true)
| map(attribute='item')
| map(attribute='path')
| list
}}
when: backup_alternative_snar_check.results | default([]) | length > 0

- name: Read metadata for alternative backups
ansible.builtin.slurp:
path: "{{ item }}/metadata.yml"
register: backup_alternatives_metadata
loop: "{{ backup_alternatives_with_snar | default([]) }}"
failed_when: false
when: backup_alternatives_with_snar | default([]) | length > 0

- name: Build suggestions list with backup info
ansible.builtin.set_fact:
backup_suggestions: >-
{{
backup_alternatives_metadata.results | default([])
| selectattr('content', 'defined')
| map(attribute='content')
| map('b64decode')
| map('from_yaml')
| map('combine', {'path': ''})
| list
}}
when: backup_alternatives_metadata.results | default([]) | length > 0

- name: Combine paths with metadata
ansible.builtin.set_fact:
backup_suggestions_formatted: >-
{{
backup_suggestions_formatted | default([]) +
[{
'path': backup_alternatives_with_snar[idx],
'timestamp': item.timestamp,
'type': 'Full' if not (item.incremental.is_incremental | default(item.incremental | default(false)) | bool) else 'Incremental'
}]
}}
loop: "{{ backup_suggestions | default([]) }}"
loop_control:
index_var: idx
when: backup_suggestions | default([]) | length > 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Index misalignment when building alternative-backup suggestions.

backup_suggestions (Lines 162-174) is built by filtering backup_alternatives_metadata.results with selectattr('content', 'defined'), which drops entries whose metadata.yml slurp failed. The subsequent loop at Lines 176-190 then indexes into backup_alternatives_with_snar[idx] using the filtered list's position (idx), but backup_alternatives_with_snar is the unfiltered list of paths. Whenever any alternative directory lacks a valid metadata.yml, every suggestion after that point gets paired with the wrong path, producing a misleading "Available backups with incremental support" message and a bogus suggested retry command (Line 208).

🛠️ Proposed fix: pair path and content in a single pass, avoiding index lookup
-            - name: Build suggestions list with backup info
-              ansible.builtin.set_fact:
-                backup_suggestions: >-
-                  {{
-                    backup_alternatives_metadata.results | default([])
-                    | selectattr('content', 'defined')
-                    | map(attribute='content')
-                    | map('b64decode')
-                    | map('from_yaml')
-                    | map('combine', {'path': ''})
-                    | list
-                  }}
-              when: backup_alternatives_metadata.results | default([]) | length > 0
-
-            - name: Combine paths with metadata
-              ansible.builtin.set_fact:
-                backup_suggestions_formatted: >-
-                  {{
-                    backup_suggestions_formatted | default([]) +
-                    [{
-                      'path': backup_alternatives_with_snar[idx],
-                      'timestamp': item.timestamp,
-                      'type': 'Full' if not (item.incremental.is_incremental | default(item.incremental | default(false)) | bool) else 'Incremental'
-                    }]
-                  }}
-              loop: "{{ backup_suggestions | default([]) }}"
-              loop_control:
-                index_var: idx
-              when: backup_suggestions | default([]) | length > 0
+            - name: Combine paths with metadata
+              ansible.builtin.set_fact:
+                backup_suggestions_formatted: >-
+                  {{
+                    backup_suggestions_formatted | default([]) +
+                    [{
+                      'path': item.item,
+                      'timestamp': (item.content | b64decode | from_yaml).timestamp,
+                      'type': 'Full' if not ((item.content | b64decode | from_yaml).incremental.is_incremental | default((item.content | b64decode | from_yaml).incremental | default(false)) | bool) else 'Incremental'
+                    }]
+                  }}
+              loop: "{{ backup_alternatives_metadata.results | default([]) | selectattr('content', 'defined') | list }}"
+              when: backup_alternatives_metadata.results | default([]) | length > 0
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Build list of backups with .snar support
ansible.builtin.set_fact:
backup_alternatives_with_snar: >-
{{
backup_alternative_snar_check.results | default([])
| selectattr('stat.exists', 'defined')
| selectattr('stat.exists', 'equalto', true)
| map(attribute='item')
| map(attribute='path')
| list
}}
when: backup_alternative_snar_check.results | default([]) | length > 0
- name: Read metadata for alternative backups
ansible.builtin.slurp:
path: "{{ item }}/metadata.yml"
register: backup_alternatives_metadata
loop: "{{ backup_alternatives_with_snar | default([]) }}"
failed_when: false
when: backup_alternatives_with_snar | default([]) | length > 0
- name: Build suggestions list with backup info
ansible.builtin.set_fact:
backup_suggestions: >-
{{
backup_alternatives_metadata.results | default([])
| selectattr('content', 'defined')
| map(attribute='content')
| map('b64decode')
| map('from_yaml')
| map('combine', {'path': ''})
| list
}}
when: backup_alternatives_metadata.results | default([]) | length > 0
- name: Combine paths with metadata
ansible.builtin.set_fact:
backup_suggestions_formatted: >-
{{
backup_suggestions_formatted | default([]) +
[{
'path': backup_alternatives_with_snar[idx],
'timestamp': item.timestamp,
'type': 'Full' if not (item.incremental.is_incremental | default(item.incremental | default(false)) | bool) else 'Incremental'
}]
}}
loop: "{{ backup_suggestions | default([]) }}"
loop_control:
index_var: idx
when: backup_suggestions | default([]) | length > 0
- name: Build list of backups with .snar support
ansible.builtin.set_fact:
backup_alternatives_with_snar: >-
{{
backup_alternative_snar_check.results | default([])
| selectattr('stat.exists', 'defined')
| selectattr('stat.exists', 'equalto', true)
| map(attribute='item')
| map(attribute='path')
| list
}}
when: backup_alternative_snar_check.results | default([]) | length > 0
- name: Read metadata for alternative backups
ansible.builtin.slurp:
path: "{{ item }}/metadata.yml"
register: backup_alternatives_metadata
loop: "{{ backup_alternatives_with_snar | default([]) }}"
failed_when: false
when: backup_alternatives_with_snar | default([]) | length > 0
- name: Combine paths with metadata
ansible.builtin.set_fact:
backup_suggestions_formatted: >-
{{
backup_suggestions_formatted | default([]) +
[{
'path': item.item,
'timestamp': (item.content | b64decode | from_yaml).timestamp,
'type': 'Full' if not ((item.content | b64decode | from_yaml).incremental.is_incremental | default((item.content | b64decode | from_yaml).incremental | default(false)) | bool) else 'Incremental'
}]
}}
loop: "{{ backup_alternatives_metadata.results | default([]) | selectattr('content', 'defined') | list }}"
when: backup_alternatives_metadata.results | default([]) | length > 0
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/roles/backup/tasks/main.yaml` around lines 141 - 190, The backup
suggestion build in backup_alternatives_with_snar/backup_alternatives_metadata
is misaligning paths and metadata because backup_suggestions filters out failed
slurp entries but the later loop still indexes into the unfiltered path list.
Fix this by pairing each metadata result with its source path in a single pass,
or by filtering and mapping path/content together before building
backup_suggestions_formatted, so the combine step in the set_fact task never
relies on idx from a filtered list. Use the existing tasks around Build
suggestions list with backup info and Combine paths with metadata to keep each
metadata.yml matched to the correct backup directory.

Comment on lines +2 to +7
- name: Create PostgreSQL admin password secret
ansible.builtin.shell: |
cat {{ obsah_state_path }}/postgresql-admin-password | podman secret create postgresql-admin-password -
when: database_mode == 'internal'
changed_when: true
failed_when: false

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Podman secret creation isn't idempotent and failures are fully hidden.

If postgresql-admin-password secret already exists from a prior run, podman secret create fails, and failed_when: false suppresses this without any remediation — PostgreSQL could then start with a stale/mismatched admin secret.

🔧 Suggested fix
+- name: Remove existing PostgreSQL admin password secret
+  ansible.builtin.command:
+    cmd: podman secret rm postgresql-admin-password
+  when: database_mode == 'internal'
+  changed_when: true
+  failed_when: false
+
 - name: Create PostgreSQL admin password secret
   ansible.builtin.shell: |
     cat {{ obsah_state_path }}/postgresql-admin-password | podman secret create postgresql-admin-password -
   when: database_mode == 'internal'
   changed_when: true
-  failed_when: false
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Create PostgreSQL admin password secret
ansible.builtin.shell: |
cat {{ obsah_state_path }}/postgresql-admin-password | podman secret create postgresql-admin-password -
when: database_mode == 'internal'
changed_when: true
failed_when: false
- name: Remove existing PostgreSQL admin password secret
ansible.builtin.command:
cmd: podman secret rm postgresql-admin-password
when: database_mode == 'internal'
changed_when: true
failed_when: false
- name: Create PostgreSQL admin password secret
ansible.builtin.shell: |
cat {{ obsah_state_path }}/postgresql-admin-password | podman secret create postgresql-admin-password -
when: database_mode == 'internal'
changed_when: true
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/roles/restore/tasks/restore_databases.yaml` around lines 2 - 7, The
PostgreSQL admin secret creation task in the restore workflow is non-idempotent
and currently hides real failures. Update the task around the podman secret
creation step so it checks whether postgresql-admin-password already exists
before creating it, or deletes/recreates it safely when needed; remove the
blanket failure suppression so genuine errors from the secret management step
are surfaced. Use the existing restore_databases task and the secret name
postgresql-admin-password to locate the change.

Comment on lines +136 to +151
- name: Restore databases from dump files
ansible.builtin.command:
cmd: >
pg_restore
-h {{ database_host }}
-p {{ database_port }}
-U {{ item.owner }}
-d {{ item.database_name }}
{{ backup_dir }}/{{ item.dump_file }}
environment:
PGPASSWORD: "{{ postgresql_admin_password }}"
loop: "{{ restore_databases_to_restore }}"
loop_control:
label: "{{ item.dump_file }} -> {{ item.database_name }}"
changed_when: true
failed_when: false

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

pg_restore likely fails auth and the failure is silently swallowed.

pg_restore connects as -U {{ item.owner }} while PGPASSWORD is set to {{ postgresql_admin_password }} (the admin password), not the owner-specific password (captured separately per DB, e.g. <db>-db-password, in restore_foremanctl_state.yaml). Unless the owner role's password happens to equal the admin password, this authentication will fail. Compounding this, failed_when: false means the task always reports success even when the restore never actually ran, leaving the "Restore completed successfully!" message misleading.

🔧 Suggested fix
 - name: Restore databases from dump files
   ansible.builtin.command:
     cmd: >
       pg_restore
       -h {{ database_host }}
       -p {{ database_port }}
-      -U {{ item.owner }}
+      -U postgres
       -d {{ item.database_name }}
       {{ backup_dir }}/{{ item.dump_file }}
   environment:
     PGPASSWORD: "{{ postgresql_admin_password }}"
   loop: "{{ restore_databases_to_restore }}"
   loop_control:
     label: "{{ item.dump_file }} -> {{ item.database_name }}"
   changed_when: true
-  failed_when: false
+  register: restore_pg_restore_result
+  failed_when: restore_pg_restore_result.rc != 0 and 'already exists' not in restore_pg_restore_result.stderr
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Restore databases from dump files
ansible.builtin.command:
cmd: >
pg_restore
-h {{ database_host }}
-p {{ database_port }}
-U {{ item.owner }}
-d {{ item.database_name }}
{{ backup_dir }}/{{ item.dump_file }}
environment:
PGPASSWORD: "{{ postgresql_admin_password }}"
loop: "{{ restore_databases_to_restore }}"
loop_control:
label: "{{ item.dump_file }} -> {{ item.database_name }}"
changed_when: true
failed_when: false
- name: Restore databases from dump files
ansible.builtin.command:
cmd: >
pg_restore
-h {{ database_host }}
-p {{ database_port }}
-U postgres
-d {{ item.database_name }}
{{ backup_dir }}/{{ item.dump_file }}
environment:
PGPASSWORD: "{{ postgresql_admin_password }}"
loop: "{{ restore_databases_to_restore }}"
loop_control:
label: "{{ item.dump_file }} -> {{ item.database_name }}"
changed_when: true
register: restore_pg_restore_result
failed_when: restore_pg_restore_result.rc != 0 and 'already exists' not in restore_pg_restore_result.stderr
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/roles/restore/tasks/restore_databases.yaml` around lines 136 - 151, The
restore task in restore_databases.yaml is using pg_restore with -U item.owner
but still setting PGPASSWORD to postgresql_admin_password, so authentication can
fail if the owner password differs. Update the Restore databases from dump files
task to use the correct password source for each database owner, using the same
per-database secret pattern captured in restore_foremanctl_state.yaml or
otherwise ensuring the password matches item.owner. Also remove the blanket
failed_when: false from this task, or replace it with a condition that only
tolerates expected cases, so failures in ansible.builtin.command are surfaced
instead of being silently reported as success.

Comment on lines +2 to +6
- name: Remove existing foremanctl state directory
ansible.builtin.file:
path: "{{ obsah_state_path }}"
state: absent

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '== files ==\n'
git ls-files 'src/roles/restore/tasks/*' 'src/roles/restore/*' | sed -n '1,200p'

printf '\n== restore_single references ==\n'
rg -n --no-heading 'restore_single|restore_foremanctl_state|obsah_state_path|is_first' src/roles/restore -S

printf '\n== relevant task files (line-numbered excerpts) ==\n'
for f in \
  src/roles/restore/tasks/restore_single.yaml \
  src/roles/restore/tasks/restore_foremanctl_state.yaml
do
  echo "--- $f ---"
  wc -l "$f"
  sed -n '1,220p' "$f" | cat -n
done

Repository: theforeman/foremanctl

Length of output: 2032


🌐 Web query:

GNU tar listed incremental restore extract archives sequence onto same tree do not delete target directory between archives

💡 Result:

When restoring from a sequence of GNU tar incremental archives, it is a fundamental aspect of the restoration process that GNU tar attempts to replicate the exact state of the filesystem as it existed at the time each specific archive was created [1]. By design, during an incremental extraction, GNU tar will delete files from the target directory that were not present in the archive [1]. This behavior is how it achieves the state-restoration required for incremental backups, as it must remove files that were deleted on the source system between backup levels [1]. There is no standard GNU tar option to simply disable this deletion behavior while maintaining the incremental restoration logic for files that were present [1]. To perform a correct incremental restore: 1. You must restore the full (level 0) backup first, followed by each subsequent incremental backup in the exact chronological order in which they were created [1][2]. 2. During the extraction of each archive, use the --listed-incremental=/dev/null option (or simply --incremental / -G) [1][3][4]. This tells tar that the archive contains incremental metadata, allowing it to correctly process the file list without needing an external snapshot file [1]. If you wish to avoid the risk of data loss or "unexpected" deletions during this process, the standard practice is to restore the data into an empty, dedicated target directory rather than an active production tree [1][5]. If you must perform this operation on an existing tree and need to avoid deletions, you would typically need to manually inspect the archive contents or use file system snapshots (such as LVM, ZFS, or Btrfs) to restore to a clean state first [1][2]. The provided GNU tar restoration scripts are designed to handle this orchestration automatically, and it is highly recommended to use them if your backup strategy utilizes them [6][7][8].

Citations:


🌐 Web query:

site:github.com theforeman/foremanctl src/roles/restore/tasks/restore_single.yaml restore_foremanctl_state.yaml

💡 Result:

The files src/roles/restore/tasks/restore_single.yaml and src/roles/restore/tasks/restore_foremanctl_state.yaml are Ansible task files within the theforeman/foremanctl repository, which provides tooling for deploying Foreman and Katello using Podman quadlets and Ansible [1][2][3]. These files are components of the foremanctl restore functionality, introduced to enable restoring Foreman instances from backups [3][4]. 1. src/roles/restore/tasks/restore_single.yaml: This task file was added to handle the restoration of a single backup point [3]. It is used as part of the restore process, particularly when dealing with chains of backups (such as those involving incremental backups) [3][5]. 2. src/roles/restore/tasks/restore_foremanctl_state.yaml: This task file manages the restoration of the foremanctl internal state [3]. It ensures that the state configuration, which is tracked via snapshot files (such as.config.snar in incremental backups), is correctly restored during the process [3][5]. Together, these roles support the foremanctl restore command, which validates backups, restores databases, pulp content, and encryption keys, and redeploys the environment to ensure a healthy state [4]. Incremental backups and the associated restore chain logic further utilize these files to manage full and partial state restorations efficiently [5].

Citations:


🌐 Web query:

"src/roles/restore/tasks/restore_single.yaml" "include_tasks" restore_foremanctl_state.yaml

💡 Result:

The file path src/roles/restore/tasks/restore_single.yaml is part of the foremanctl project [1], a command-line tool used for managing Foreman and Katello as containerized services [2]. The task file src/roles/restore/tasks/restore_single.yaml utilizes the include_tasks directive to call restore_foremanctl_state.yaml [1], which is responsible for restoring specific state components during the foremanctl restore process [1][3]. Foremanctl implements backup and restore functionality for these containerized deployments [3][2]. The restore_foremanctl_state.yaml task specifically handles restoring necessary configuration and state information (such as secrets or specific metadata) required to bring the Foreman instance back to a consistent state [3][4]. This workflow is part of a larger, modular restore process that can handle both full and incremental backups [1][4]. In recent versions of foremanctl, the restore command has been expanded to support: - Automated chain restoration (using the --chain flag) [4]. - Incremental backups enabled by GNU tar's --listed-incremental mechanism [4]. - Validation steps to ensure backup integrity before applying changes to an existing deployment [3][4]. The foremanctl tool is distinct from the older foreman-maintain project, although both serve the purpose of system maintenance [5][2]. Foremanctl is specifically architected for containerized environments using Podman Quadlets [2].

Citations:


🌐 Web query:

site:github.com/theforeman/foremanctl "restore_foremanctl_state.yaml" "obsah_state_path" "state: absent"

💡 Result:

The file restore_foremanctl_state.yaml is an Ansible task file within the foremanctl project [1], a tool used to manage Foreman and Katello deployments via Podman Quadlets [2]. The OBSAH_STATE_PATH (or the environment variable OBSAH_STATE) refers to the directory path where foremanctl maintains its internal state, configuration, and data, typically located at /var/lib/foremanctl on production installations [3][4]. Within the context of restore_foremanctl_state.yaml, the configuration state: absent is a standard Ansible module parameter used to ensure that a specified file, directory, or state resource is removed or does not exist on the target system during the restoration process [1][5]. This is common in deployment and restoration playbooks to clear existing, potentially conflicting state files before applying the state defined in the backup archive [5][3]. In summary, this task is part of the automated restoration workflow [1] that ensures the local foremanctl environment is cleaned (state: absent) [1] before it is overwritten with the data preserved from a previous backup [5].

Citations:


src/roles/restore/tasks/restore_foremanctl_state.yaml:2-6 — Only clear obsah_state_path before the base restore, not before every incremental layer.

restore_single.yaml participates in the chain-restore flow, and GNU tar incremental archives need to be applied cumulatively onto the same tree. Removing the directory on each layer drops files restored by earlier archives and leaves later incrementals incomplete.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/roles/restore/tasks/restore_foremanctl_state.yaml` around lines 2 - 6,
The restore task currently deletes the target state directory in a way that
breaks chain restores across incremental layers. Update the restore flow so
`restore_foremanctl_state` only clears `obsah_state_path` for the initial/base
restore path, and ensure `restore_single` does not remove the directory before
applying each incremental archive; keep later tar incrementals applied
cumulatively onto the same tree.

Comment on lines +24 to +27
- name: Restore databases from first backup in chain
ansible.builtin.include_tasks:
file: restore_databases.yaml
when: restore_single_is_first

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🔴 Critical | 🏗️ Heavy lift

Chain restore uses the stale, base-backup database dump instead of the latest one.

Database dumps are full dumps in every backup (only foremanctl state/Pulp content use --listed-incremental). Gating restore_databases.yaml on restore_single_is_first means chain restore always restores the database from the oldest (base) backup, discarding all DB changes captured by later backups in the chain — final state has fresh Pulp/state content but a stale database.

💡 Suggested direction
-    - name: Restore databases from first backup in chain
-      ansible.builtin.include_tasks:
-        file: restore_databases.yaml
-      when: restore_single_is_first
+    - name: Restore databases from latest backup in chain
+      ansible.builtin.include_tasks:
+        file: restore_databases.yaml
+      when: restore_single_index | int == restore_single_total | int

This likely requires moving the PostgreSQL start/stop bootstrapping out of restore_databases.yaml so it still runs once at the start of the chain, while the actual pg_restore step only fires on the last backup.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/roles/restore/tasks/restore_single.yaml` around lines 24 - 27, Chain
restore is restoring the wrong database snapshot because
`restore_single_is_first` only runs `restore_databases.yaml` for the base
backup. Update `restore_single.yaml` and the `restore_databases.yaml` flow so
PostgreSQL bootstrap/start-stop still happens once at the beginning of the
chain, but the actual `pg_restore` for the database runs on the last backup
instead of the first. Use the `restore_single_is_first` condition (or a new
end-of-chain check) to ensure later backup DB dumps are applied and the final
restored state matches the latest backup.

Comment on lines +2 to +25
- name: Check if foremanctl is already deployed
ansible.builtin.stat:
path: /var/lib/foremanctl
register: restore_existing_deployment

- name: Fail if restoring over existing system without --force
ansible.builtin.fail:
msg: |
DESTRUCTIVE OPERATION WARNING:

An existing Foreman deployment was detected on this system.
Restore will PERMANENTLY DELETE all current data including:
- All databases (Foreman, Candlepin, Pulp, etc.)
- All Pulp content
- All configuration

There is NO UNDO operation.

If you are certain you want to proceed, re-run with --force:
foremanctl restore {{ backup_dir }} --force
when:
- restore_existing_deployment.stat.exists
- not force | default(false)
- not validate | default(false)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## Files\n'
git ls-files 'src/roles/restore/tasks/*' 'docs/user/restore.md' | sed 's#^`#-` #'

printf '\n## Search for chain-related gates\n'
rg -n --no-heading 'restore_single_is_first|restore_chain|validate\.yaml|force \| default|validate:|--force' src docs/user/restore.md

printf '\n## Outline relevant task files\n'
for f in src/roles/restore/tasks/validate.yaml src/roles/restore/tasks/restore_chain.yaml src/roles/restore/tasks/restore_single.yaml; do
  echo "\n### $f"
  ast-grep outline "$f" --view expanded || true
done

printf '\n## Read relevant file slices\n'
for f in src/roles/restore/tasks/validate.yaml src/roles/restore/tasks/restore_chain.yaml src/roles/restore/tasks/restore_single.yaml docs/user/restore.md; do
  echo "\n===== $f ====="
  nl -ba "$f" | sed -n '1,220p'
done

Repository: theforeman/foremanctl

Length of output: 2032


🌐 Web query:

theforeman foremanctl src/roles/restore/tasks/restore_chain.yaml restore_single.yaml validate.yaml docs/user/restore.md

💡 Result:

The files you referenced are part of the internal implementation of the restore functionality for foremanctl, a CLI tool for managing containerized Foreman and Katello deployments [1]. These specific Ansible roles and tasks handle the automated restoration of data from backups [2][3]. 1. src/roles/restore/tasks/restore_chain.yaml: This role implements support for restoring backup chains, particularly for incremental backups [2][3]. It allows foremanctl to automatically detect and restore a full backup followed by its dependent incremental backups when the --chain flag is used [3]. 2. src/roles/restore/tasks/restore_single.yaml: This role is used to perform a restore of a specific, single backup archive rather than a chain [2]. 3. src/roles/restore/tasks/validate.yaml: This task file is responsible for pre-restore validation [2]. It ensures the system environment and the backup itself are suitable for restoration [4]. This includes checks for disk space, backup integrity, system requirements, and configuration consistency (e.g., verifying that the backup hostname matches the current system) [4][5]. 4. docs/user/restore.md: This documentation file provides user-facing instructions on how to use the restore command [2][4]. It covers workflows such as running a validation-only restore, executing a full restore (which requires the --force flag on existing deployments), and utilizing the --chain flag for incremental backup restoration [3][4]. In summary, these components form the backend of the foremanctl restore command, which orchestrates the safe recovery of databases, Pulp content, and various system secrets, followed by a redeployment of containerized services [4]. Documentation for these procedures is maintained in the repository to guide users through the process [2][4].

Citations:


🌐 Web query:

site:github.com/theforeman/foremanctl "restore_single_is_first" foremanctl

💡 Result:

The term restore_single_is_first appears to be an internal Ansible variable or task flag associated with the restore logic in the foremanctl tool, rather than a user-facing command line parameter [1]. Foremanctl is a CLI tool for managing Foreman and Katello deployments using Podman and Ansible [2]. Based on the codebase for foremanctl, restore_single.yaml is a dedicated task file used during the restore process [1]. The restore_single_is_first indicator is likely a logic control mechanism used within these internal Ansible roles to manage the sequence of operations when performing a single (non-chained) restoration, ensuring that necessary prerequisites are handled correctly before the core restore tasks are executed [1]. For standard user operations, restoration is typically handled using the foremanctl restore command, with options such as --chain or --restore-chain (in newer versions) for handling incremental backups [3][1]. Users should interact with these high-level CLI flags rather than internal task variables [3][1].

Citations:


🌐 Web query:

site:raw.githubusercontent.com theforeman foremanctl "src/roles/restore/tasks/validate.yaml"

💡 Result:

The specific file path src/roles/restore/tasks/validate.yaml does not appear in the authoritative documentation or source code repositories for either foremanctl or the official Foreman infrastructure maintenance tools [1][2]. Foremanctl is a tool designed to install Foreman using containers deployed as podman quadlets [1]. Separate maintenance tasks, such as backups and restores, are typically handled by the foreman-maintain utility, which is a specialized tool for keeping Foreman and Satellite infrastructure running [2]. If you are encountering a validation task within an Ansible role related to restoration, it is likely part of a custom implementation or an unofficial Ansible collection rather than a core component of the Foreman project. Official Foreman maintenance, including backups and restores, relies on the foreman-maintain utility [2], and troubleshooting or configuration for Foreman components is generally managed through standard configuration files located in /etc/foreman or /etc/foreman-proxy [3].

Citations:


🌐 Web query:

site:raw.githubusercontent.com/theforeman/foremanctl 2ecaa0d7264d0a5ddbe8a3f80ca948e96fa9c966 src/roles/restore/tasks/restore_chain.yaml

💡 Result:

Found it. The file src/roles/restore/tasks/restore_chain.yaml at commit 2ecaa0d7264d0a5ddbe8a3f80ca948e96fa9c966 builds an incremental restore chain: it initializes the chain with the current backup, walks backward to find the full backup, reverses the chain, prints the detected order, and then restores each backup in chronological order. (raw.githubusercontent.com)

Citations:


🌐 Web query:

site:raw.githubusercontent.com/theforeman/foremanctl 2ecaa0d7264d0a5ddbe8a3f80ca948e96fa9c966 src/roles/restore/tasks/restore_single.yaml

💡 Result:

Found it. The file at that commit is a single Ansible task list that:

  • displays restore progress
  • sets backup_dir
  • validates the backup
  • restores foremanctl state
  • prepares the system on the first backup in the chain
  • restores databases on the first backup in the chain
  • restores Pulp content
  • records the successful restore timestamp [1]

[1] https://raw.githubusercontent.com/theforeman/foremanctl/2ecaa0d7264d0a5ddbe8a3f80ca948e96fa9c966/src/roles/restore/tasks/restore_single.yaml


🌐 Web query:

site:raw.githubusercontent.com/theforeman/foremanctl 2ecaa0d7264d0a5ddbe8a3f80ca948e96fa9c966 src/roles/restore/tasks/validate.yaml

💡 Result:

src/roles/restore/tasks/validate.yaml is the restore preflight for foremanctl. It checks for an existing deployment and blocks restore unless --force/validation bypass is used, verifies the backup directory and metadata.yml, parses backup metadata, validates incremental-backup chains, confirms required dump/archive files exist, and warns about disk space and database-mode mismatches. [1]

[1] https://raw.githubusercontent.com/theforeman/foremanctl/2ecaa0d7264d0a5ddbe8a3f80ca948e96fa9c966/src/roles/restore/tasks/validate.yaml


🌐 Web query:

site:github.com/theforeman/foremanctl/pull/609 restore_chain validate restore_single

💡 Result:

The terms restore_chain, validate, and restore_single are associated with backup and restore operations for the Foreman project's management tool, foremanctl, specifically regarding incremental backup support [1]. In the context of foremanctl, these options are used to manage the integrity and sequencing of incremental backup chains: 1. --chain: This flag is used with the restore command to enable automatic backup chain handling, allowing the tool to process a series of incremental backups [1]. 2. --validate: This flag is used in conjunction with the restore and --chain commands to verify the completeness and integrity of a backup chain before the restore operation proceeds [1]. 3. restore_single: While not explicitly defined as a standalone command flag in the documentation snippet, the workflow implies that manual restoration of individual backups can be performed, often requiring the --force flag to override the default chain validation logic when restoring steps out of sequence or without the full chain [1]. For standard operations, it is recommended to use the auto-chain feature, which can be validated as follows [1]: foremanctl restore /backups/foreman-backup-* --chain --validate If manual restoration is required, you would typically execute the restoration for each backup file in the chain sequence, often utilizing the --force flag for subsequent steps [1].

Citations:


Skip the existing-deployment guard for later chain members restore_chain runs restore_single for each backup, so once the first member restores /var/lib/foremanctl, this fail will trip on the next member unless the whole chain is forced. That blocks the documented automatic chain restore path; gate this check to the first backup in the chain.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/roles/restore/tasks/validate.yaml` around lines 2 - 25, The
existing-deployment guard in the restore validation task is too broad and will
fail on later members of a restore chain after the first `restore_single`
creates `/var/lib/foremanctl`. Update the `validate.yaml` logic around
`restore_existing_deployment` and the `ansible.builtin.fail` step so this check
only applies to the first backup in a chain (or otherwise skips subsequent chain
members), while still preserving the `force` and `validate` conditions.

when: database_mode == 'internal'

- name: Get total size of database dumps
ansible.builtin.shell: du -sB1 {{ backup_dir }}/*.dump 2>/dev/null | awk '{sum+=$1} END {print int(sum/1024/1024/1024)+1}'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Unquoted backup_dir interpolated into shell command.

backup_dir is inserted raw into a shell command run under become: true. Special characters (spaces, ;, `, $()) in this admin-supplied path would break or manipulate the command. Recommend quoting or using a safer module.

🛡️ Proposed fix
 - name: Get total size of database dumps
   ansible.builtin.shell: du -sB1 {{ backup_dir }}/*.dump 2>/dev/null | awk '{sum+=$1} END {print int(sum/1024/1024/1024)+1}'
+  args:
+    executable: /bin/bash

Or better: quote the variable, e.g. du -sB1 {{ backup_dir | quote }}/*.dump ....

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/roles/restore/tasks/validate.yaml` at line 222, The shell command in the
restore validation task interpolates backup_dir directly, which can break or be
abused when the path contains spaces or shell metacharacters. Update the
ansible.builtin.shell usage in the validate task to safely quote backup_dir with
the existing du/awk pipeline, or switch to a safer module if possible. Use the
backup_dir variable in this shell step only through quoting so the restore size
check remains functional under become: true.

Comment on lines +240 to +251
- name: Get Pulp content archive size
ansible.builtin.stat:
path: "{{ backup_dir }}/pulp-content.tar.gz"
register: restore_pulp_archive
when: restore_pulp_archive is defined

- name: Warn if insufficient Pulp disk space
ansible.builtin.debug:
msg: "WARNING: Available space ({{ restore_pulp_space.stdout }}GB) may be insufficient for Pulp content ({{ (restore_pulp_archive.stat.size / 1024 / 1024 / 1024) | int + 1 }}GB needed)"
when:
- restore_pulp_archive.stat.exists | default(false)
- restore_pulp_space.stdout | int < (restore_pulp_archive.stat.size / 1024 / 1024 / 1024) | int + 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Dead code: Pulp disk-space check never executes.

when: restore_pulp_archive is defined (line 244) references the same task's own register variable before it has ever been set. On first evaluation this is always Undefined, so the stat task is always skipped, restore_pulp_archive stays undefined, and the follow-up warning at lines 246-251 (restore_pulp_archive.stat.exists | default(false)) silently defaults to false — the insufficient-space warning can never fire.

🐛 Proposed fix
 - name: Get Pulp content archive size
   ansible.builtin.stat:
     path: "{{ backup_dir }}/pulp-content.tar.gz"
   register: restore_pulp_archive
-  when: restore_pulp_archive is defined
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Get Pulp content archive size
ansible.builtin.stat:
path: "{{ backup_dir }}/pulp-content.tar.gz"
register: restore_pulp_archive
when: restore_pulp_archive is defined
- name: Warn if insufficient Pulp disk space
ansible.builtin.debug:
msg: "WARNING: Available space ({{ restore_pulp_space.stdout }}GB) may be insufficient for Pulp content ({{ (restore_pulp_archive.stat.size / 1024 / 1024 / 1024) | int + 1 }}GB needed)"
when:
- restore_pulp_archive.stat.exists | default(false)
- restore_pulp_space.stdout | int < (restore_pulp_archive.stat.size / 1024 / 1024 / 1024) | int + 1
- name: Get Pulp content archive size
ansible.builtin.stat:
path: "{{ backup_dir }}/pulp-content.tar.gz"
register: restore_pulp_archive
- name: Warn if insufficient Pulp disk space
ansible.builtin.debug:
msg: "WARNING: Available space ({{ restore_pulp_space.stdout }}GB) may be insufficient for Pulp content ({{ (restore_pulp_archive.stat.size / 1024 / 1024 / 1024) | int + 1 }}GB needed)"
when:
- restore_pulp_archive.stat.exists | default(false)
- restore_pulp_space.stdout | int < (restore_pulp_archive.stat.size / 1024 / 1024 / 1024) | int + 1
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/roles/restore/tasks/validate.yaml` around lines 240 - 251, The Pulp
disk-space validation is unreachable because Get Pulp content archive size uses
restore_pulp_archive in its own when condition before the register value exists.
Remove that self-referential guard and gate the stat task on the appropriate
restore/archive prerequisite instead, so restore_pulp_archive is actually
populated; then keep Warn if insufficient Pulp disk space dependent on
restore_pulp_archive.stat.exists and restore_pulp_space as intended.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants