-
Notifications
You must be signed in to change notification settings - Fork 45
Add incremental backup and restore #609
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -40,6 +40,7 @@ This creates a timestamped backup directory at `/var/backup/foreman-backup-YYYYM | |
|
|
||
| | Option | Description | | ||
| |--------|-------------| | ||
| | `--base-backup BASE_DIR` | Create an incremental backup based on a base backup. Only files changed since the base backup are included. The base backup directory must contain `.snar` snapshot files (generated by foremanctl backups). | | ||
| | `--skip-pulp-content` | Skip backing up `/var/lib/pulp`. This is for debugging purposes or if you plan to copy `/var/lib/pulp` using other methods such as rsync or shared storage. **Warning:** You will not have a complete backup if you use this option. | | ||
| | `--wait-for-tasks` | Wait for running Foreman and Pulp tasks to complete instead of failing immediately. The backup will poll until all tasks finish before proceeding. | | ||
|
|
||
|
|
@@ -69,6 +70,38 @@ Allow in-progress tasks to complete before starting backup: | |
| foremanctl backup /var/backup --wait-for-tasks | ||
| ``` | ||
|
|
||
| ### Incremental Backup | ||
|
|
||
| Create a full backup first: | ||
|
|
||
| ```bash | ||
| foremanctl backup /var/backup | ||
| # Creates: /var/backup/foreman-backup-20260629T120000/ | ||
| ``` | ||
|
|
||
| Then create incremental backups referencing the base backup: | ||
|
|
||
| ```bash | ||
| foremanctl backup /var/backup --base-backup /var/backup/foreman-backup-20260629T120000 | ||
| # Creates: /var/backup/foreman-backup-20260630T080000/ (incremental) | ||
| ``` | ||
|
|
||
| **How it works:** | ||
| - The incremental backup contains only files changed since the previous backup | ||
| - `.snar` snapshot files track which files were in the previous backup | ||
| - Both `config.snar` (foremanctl state) and `pulp.snar` (Pulp content) are copied from the previous backup | ||
| - Incremental backups are typically much smaller and faster than full backups | ||
|
|
||
| **Typical backup strategy:** | ||
| - Weekly full backup (Sunday) | ||
| - Daily incremental backups (Monday-Saturday) | ||
| - Each incremental references the previous day's backup | ||
|
|
||
| **Important:** | ||
| - You can chain incrementals: full -> inc1 -> inc2 -> inc3 | ||
| - All backups in the chain are required for restore | ||
| - The previous backup must have been created with foremanctl (contains .snar files) | ||
|
|
||
| ## Backup Contents | ||
|
|
||
| ### Databases | ||
|
|
@@ -269,6 +302,35 @@ foremanctl restore /var/backup/foreman-backup-20260617T104115 --validate | |
|
|
||
| This validates the backup and checks system requirements before proceeding. | ||
|
|
||
| ### Restore Incremental Backups | ||
|
|
||
| To restore from an incremental backup, restore each backup in the chain sequentially, starting with the full backup that began the chain: | ||
|
|
||
| ```bash | ||
| # Step 1: Restore the full (base) backup | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The most recent full backup or do you have to start with the first ever full backup?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This would be the first full backup in the incremental chain (the most recent full backup) and the chain will continue with each incremental backup until you run a full backup starting a new chain.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we then word it as such? |
||
| foremanctl restore /var/backup/foreman-backup-20260629T120000 | ||
|
|
||
| # Step 2: Restore the first incremental | ||
| foremanctl restore /var/backup/foreman-backup-20260630T080000 --force | ||
|
|
||
| # Step 3: Restore the second incremental | ||
| foremanctl restore /var/backup/foreman-backup-20260701T080000 --force | ||
| ``` | ||
|
|
||
| **Important:** | ||
| - Incremental backups contain only files changed since the previous backup | ||
| - All backups in the chain must be restored in order (full -> inc1 -> inc2) | ||
| - The restore command automatically detects incremental backups from metadata | ||
| - The `--force` flag is required for subsequent restores since the system is already deployed | ||
|
|
||
| **Automatic validation:** | ||
| The restore command validates the backup chain: | ||
| - Verifies the base backup directory exists | ||
| - Confirms the base backup metadata matches the expected timestamp | ||
| - Warns if the base backup has not been restored yet | ||
|
|
||
| If any validation fails, a clear error message explains which backup is missing or incorrect. | ||
|
|
||
| ## Prerequisites | ||
|
|
||
| Before restoring, ensure: | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -7,6 +7,35 @@ | |||||||||||||||||||||||
| ansible.builtin.set_fact: | ||||||||||||||||||||||||
| backup_dir_full: "{{ backup_dir }}/foreman-backup-{{ backup_timestamp }}" | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| - name: Validate backup_incremental backup parameters | ||||||||||||||||||||||||
| when: backup_incremental is defined and backup_incremental | length > 0 | ||||||||||||||||||||||||
| block: | ||||||||||||||||||||||||
| - name: Check previous backup files exist | ||||||||||||||||||||||||
| ansible.builtin.stat: | ||||||||||||||||||||||||
| path: "{{ backup_incremental }}/{{ item }}" | ||||||||||||||||||||||||
| register: backup_previous_files_check | ||||||||||||||||||||||||
| loop: | ||||||||||||||||||||||||
| - "metadata.yml" | ||||||||||||||||||||||||
| - "config.snar" | ||||||||||||||||||||||||
| - "pulp.snar" | ||||||||||||||||||||||||
| failed_when: false | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| - name: Fail if previous backup is invalid | ||||||||||||||||||||||||
| ansible.builtin.fail: | ||||||||||||||||||||||||
| msg: | | ||||||||||||||||||||||||
| Previous backup directory does not exist or is not a valid foremanctl backup: {{ backup_incremental }} | ||||||||||||||||||||||||
| Missing files: {{ backup_previous_files_check.results | selectattr('stat.exists', 'equalto', false) | map(attribute='item') | list }} | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| The previous backup must contain metadata.yml and .snar files generated by foremanctl. | ||||||||||||||||||||||||
| Create a new full backup first: | ||||||||||||||||||||||||
| foremanctl backup {{ backup_dir }} | ||||||||||||||||||||||||
| when: backup_previous_files_check.results | selectattr('stat.exists', 'equalto', false) | list | length > 0 | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
Comment on lines
+32
to
+33
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. During testing of incremental backup/restore, restoring an incremental backup fails with: Cause: The incremental backup's incremental:
base_backup_dir: /tmp/backup-xyz/foreman-backup-20260819T035145
base_backup_timestamp: '' # <-- EMPTY!
is_incremental: trueThis happens because base_backup_timestamp: "{{ (backup_previous_metadata | default({})).timestamp | default('') }}"But there's no task that actually reads the previous backup's Fix suggestion: Add tasks to read and parse the previous backup's metadata after validation in
Suggested change
With this fix, the incremental backup's metadata correctly stores: base_backup_timestamp: 20260819T054714And the restore pass.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You're correct! When I simplified the validation, I removed the metadata read/parse tasks that metadata.yaml depends on and didn't add it back after moving the validation. |
||||||||||||||||||||||||
| - name: Load previous backup metadata | ||||||||||||||||||||||||
| ansible.builtin.include_vars: | ||||||||||||||||||||||||
| file: "{{ backup_incremental }}/metadata.yml" | ||||||||||||||||||||||||
| name: backup_previous_metadata | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| - name: Ensure backup directory exists | ||||||||||||||||||||||||
| ansible.builtin.file: | ||||||||||||||||||||||||
| path: "{{ backup_dir }}" | ||||||||||||||||||||||||
|
|
@@ -92,25 +121,53 @@ | |||||||||||||||||||||||
| ansible.builtin.include_tasks: | ||||||||||||||||||||||||
| file: database_dumps.yaml | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| - name: Archive foremanctl state on controller | ||||||||||||||||||||||||
| community.general.archive: | ||||||||||||||||||||||||
| path: "{{ obsah_state_path }}" | ||||||||||||||||||||||||
| dest: "{{ obsah_state_path }}/foremanctl-state.tar.gz" | ||||||||||||||||||||||||
| format: gz | ||||||||||||||||||||||||
| - name: Copy pulp.snar from previous backup | ||||||||||||||||||||||||
| ansible.builtin.copy: | ||||||||||||||||||||||||
| src: "{{ backup_incremental }}/pulp.snar" | ||||||||||||||||||||||||
| dest: "{{ backup_dir_full }}/pulp.snar" | ||||||||||||||||||||||||
| remote_src: true | ||||||||||||||||||||||||
| mode: '0644' | ||||||||||||||||||||||||
| when: | ||||||||||||||||||||||||
| - backup_incremental is defined and backup_incremental | length > 0 | ||||||||||||||||||||||||
| - not skip_pulp_content | default(false) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| - name: Fetch config.snar from previous backup to controller | ||||||||||||||||||||||||
| ansible.builtin.fetch: | ||||||||||||||||||||||||
| src: "{{ backup_incremental }}/config.snar" | ||||||||||||||||||||||||
| dest: "{{ obsah_state_path | dirname }}/config.snar" | ||||||||||||||||||||||||
| flat: true | ||||||||||||||||||||||||
| when: backup_incremental is defined and backup_incremental | length > 0 | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| - name: Backup foremanctl state directory # noqa: command-instead-of-module | ||||||||||||||||||||||||
| ansible.builtin.command: | ||||||||||||||||||||||||
| cmd: > | ||||||||||||||||||||||||
| tar -czf "{{ obsah_state_path | dirname }}/foremanctl-state.tar.gz" | ||||||||||||||||||||||||
| --listed-incremental="{{ obsah_state_path | dirname }}/config.snar" | ||||||||||||||||||||||||
| -C "{{ obsah_state_path | dirname }}" | ||||||||||||||||||||||||
| "{{ obsah_state_path | basename }}" | ||||||||||||||||||||||||
| delegate_to: localhost | ||||||||||||||||||||||||
| become: false | ||||||||||||||||||||||||
| changed_when: true | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| - name: Copy foremanctl state archive to target | ||||||||||||||||||||||||
| ansible.builtin.copy: | ||||||||||||||||||||||||
| src: "{{ obsah_state_path }}/foremanctl-state.tar.gz" | ||||||||||||||||||||||||
| src: "{{ obsah_state_path | dirname }}/foremanctl-state.tar.gz" | ||||||||||||||||||||||||
| dest: "{{ backup_dir_full }}/foremanctl-state.tar.gz" | ||||||||||||||||||||||||
| mode: '0644' | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| - name: Clean up foremanctl state archive on controller | ||||||||||||||||||||||||
| - name: Copy config.snar to backup directory | ||||||||||||||||||||||||
| ansible.builtin.copy: | ||||||||||||||||||||||||
| src: "{{ obsah_state_path | dirname }}/config.snar" | ||||||||||||||||||||||||
| dest: "{{ backup_dir_full }}/config.snar" | ||||||||||||||||||||||||
| mode: '0644' | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| - name: Clean up foremanctl state files on controller | ||||||||||||||||||||||||
| ansible.builtin.file: | ||||||||||||||||||||||||
| path: "{{ obsah_state_path }}/foremanctl-state.tar.gz" | ||||||||||||||||||||||||
| path: "{{ item }}" | ||||||||||||||||||||||||
| state: absent | ||||||||||||||||||||||||
| loop: | ||||||||||||||||||||||||
| - "{{ obsah_state_path | dirname }}/foremanctl-state.tar.gz" | ||||||||||||||||||||||||
| - "{{ obsah_state_path | dirname }}/config.snar" | ||||||||||||||||||||||||
| delegate_to: localhost | ||||||||||||||||||||||||
| become: false | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does performing a full backup wipe out the previous incremental backups?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, performing a full backup doesn't delete anything. All backups (full or incremental) are just timestamped directories that coexist independently. You'd need to manually clean up old backups when you no longer need them.