diff options
| author | Colin Wilk <colin@wilk.cx> | 2026-09-01 21:01:05 +0200 |
|---|---|---|
| committer | Colin Wilk <colin@wilk.cx> | 2026-09-01 21:43:57 +0200 |
| commit | 9c7586c7c3a235672ec6490d1a8bc44a222ce5d1 (patch) | |
| tree | 8b36c5ee3105da2ae769c0d9cd96683d213c949d /tasks | |
| parent | fa137a92e1084a07608a4008ec0cb891baa76774 (diff) | |
| download | ansible-role-borgbackup-9c7586c7c3a235672ec6490d1a8bc44a222ce5d1.tar.gz ansible-role-borgbackup-9c7586c7c3a235672ec6490d1a8bc44a222ce5d1.zip | |
Add borg prune and compact jobs
Run repository retention either after a successful backup or from a
dedicated systemd timer. Clean up script generation with templates and
expand molecule test coverage.
BREAKING CHANGE: Aggregate backup scripts are no longer managed, and
state=preset now requires at least one readable included directory.
Diffstat (limited to 'tasks')
| -rw-r--r-- | tasks/absent.yml | 74 | ||||
| -rw-r--r-- | tasks/client_create_scripts_each.yml | 34 | ||||
| -rw-r--r-- | tasks/client_setup.yml | 135 | ||||
| -rw-r--r-- | tasks/main.yml | 3 | ||||
| -rw-r--r-- | tasks/validate.yml | 8 | ||||
| -rw-r--r-- | tasks/validate_absent.yml | 10 | ||||
| -rw-r--r-- | tasks/validate_present.yml | 104 |
7 files changed, 264 insertions, 104 deletions
diff --git a/tasks/absent.yml b/tasks/absent.yml index b1a5592..6afb7f3 100644 --- a/tasks/absent.yml +++ b/tasks/absent.yml @@ -38,6 +38,12 @@ register: timer_stat become: true +- name: Check if prune systemd timer exists + ansible.builtin.stat: + path: /etc/systemd/system/{{ borg_prune_timer_name }}{{ '@' if borg_backup_argument != '' }}{{ borg_backup_argument }}.timer + register: prune_timer_stat + become: true + - name: Stop systemd timer ansible.builtin.systemd: name: "{{ borg_backup_timer_name }}{{ '@' if borg_backup_argument != '' }}{{ borg_backup_argument }}.timer" @@ -46,12 +52,26 @@ become: true when: timer_stat.stat.exists +- name: Stop prune systemd timer + ansible.builtin.systemd: + name: "{{ borg_prune_timer_name }}{{ '@' if borg_backup_argument != '' }}{{ borg_backup_argument }}.timer" + state: stopped + enabled: false + become: true + when: prune_timer_stat.stat.exists + - name: Check if systemd service exists ansible.builtin.stat: path: /etc/systemd/system/{{ borg_backup_service_name }}{{ '@' if borg_backup_argument != '' }}{{ borg_backup_argument }}.service register: service_stat become: true +- name: Check if prune systemd service exists + ansible.builtin.stat: + path: /etc/systemd/system/{{ borg_prune_service_name }}{{ '@' if borg_backup_argument != '' }}{{ borg_backup_argument }}.service + register: prune_service_stat + become: true + - name: Stop systemd service ansible.builtin.systemd: name: "{{ borg_backup_service_name }}{{ '@' if borg_backup_argument != '' }}{{ borg_backup_argument }}.service" @@ -60,59 +80,55 @@ become: true when: service_stat.stat.exists +- name: Stop prune systemd service + ansible.builtin.systemd: + name: "{{ borg_prune_service_name }}{{ '@' if borg_backup_argument != '' }}{{ borg_backup_argument }}.service" + state: stopped + enabled: false + become: true + when: prune_service_stat.stat.exists + - name: Remove systemd timer file ansible.builtin.file: path: /etc/systemd/system/{{ borg_backup_timer_name }}{{ '@' if borg_backup_argument != '' }}{{ borg_backup_argument }}.timer state: absent become: true +- name: Remove prune systemd timer file + ansible.builtin.file: + path: /etc/systemd/system/{{ borg_prune_timer_name }}{{ '@' if borg_backup_argument != '' }}{{ borg_backup_argument }}.timer + state: absent + become: true + - name: Remove systemd service file ansible.builtin.file: path: /etc/systemd/system/{{ borg_backup_service_name }}{{ '@' if borg_backup_argument != '' }}{{ borg_backup_argument }}.service state: absent become: true +- name: Remove prune systemd service file + ansible.builtin.file: + path: /etc/systemd/system/{{ borg_prune_service_name }}{{ '@' if borg_backup_argument != '' }}{{ borg_backup_argument }}.service + state: absent + become: true + - name: Reload systemd daemon ansible.builtin.systemd: daemon_reload: true become: true -- name: Check if base backup script exists - ansible.builtin.stat: - path: "{{ borg_backup_script_location }}" - register: base_script_stat - become: true - -- name: Remove repo-specific backup script +- name: Remove repository-specific backup script ansible.builtin.file: - path: "{{ borg_backup_script_location }}{{ '@' if borg_backup_argument != '' }}{{ borg_backup_argument }}" - state: absent - become: true - when: borg_backup_argument | length > 0 - -- name: Remove block from base backup script - ansible.builtin.blockinfile: - path: "{{ borg_backup_script_location }}" - marker: "## {mark} ANSIBLE MANAGED BLOCK for {{ borg_server_host_url }}/{{ borg_repo_name }}" + path: "{{ borg_backup_script_location }}{{ '@' if borg_backup_argument != '' else '' }}{{ borg_backup_argument }}" state: absent become: true - when: base_script_stat.stat.exists -- name: Read base script content - ansible.builtin.slurp: - src: "{{ borg_backup_script_location }}" - register: base_script_content - become: true - when: base_script_stat.stat.exists - -- name: Remove empty base script +- name: Remove repository-specific prune script ansible.builtin.file: - path: "{{ borg_backup_script_location }}" + path: "{{ borg_prune_script_location }}{{ '@' if borg_backup_argument != '' else '' }}{{ borg_backup_argument }}" state: absent become: true - when: - - base_script_stat.stat.exists - - ('ANSIBLE MANAGED BLOCK' not in (base_script_content.content | b64decode)) + when: borg_prune_script_location | length > 0 - name: Remove per-repo SSH private key ansible.builtin.file: diff --git a/tasks/client_create_scripts_each.yml b/tasks/client_create_scripts_each.yml deleted file mode 100644 index 6056b81..0000000 --- a/tasks/client_create_scripts_each.yml +++ /dev/null @@ -1,34 +0,0 @@ ---- -- name: Create script for automatic borg backup - ansible.builtin.file: - dest: "{{ script_location }}" - state: touch - owner: "{{ borg_client_user }}" - group: "{{ borg_client_user }}" - modification_time: preserve - access_time: preserve - mode: "0711" - become: true - -- name: Insert shebang into backup script - ansible.builtin.lineinfile: - path: "{{ script_location }}" - line: "#!/bin/bash" - insertbefore: BOF - state: present - become: true - -- name: Insert Backup job block into scripts - ansible.builtin.blockinfile: - path: "{{ script_location }}" - marker: "## {mark} ANSIBLE MANAGED BLOCK for {{ borg_server_host_url }}/{{ borg_repo_name }}" - block: | - export BORG_PASSPHRASE={{ borg_passphrase | quote }} - {% if borg_ssh_key_per_repo %} - export BORG_RSH="ssh -i {{ borg_ssh_key_path }}" - {% endif %} - borg create -C {{ borg_compression }}{% if borg_create_additional_arguments %} {{ borg_create_additional_arguments }}{% endif %} \ - {{ borg_server_user }}@{{ borg_server_host_url }}:{{ borg_server_user_home }}/{{ borg_repo_name }}::{{ borg_backup_name_format }} \ - {{ borg_included_dirs | map('quote') | join(' ') }} \ - {% for e in (borg_excluded_dirs | map('quote')) %} --exclude {{ e }} {% endfor %} - become: true diff --git a/tasks/client_setup.yml b/tasks/client_setup.yml index ab09926..2a1389a 100644 --- a/tasks/client_setup.yml +++ b/tasks/client_setup.yml @@ -1,37 +1,3 @@ ---- -- name: Ensure borg_client_user exists - ansible.builtin.getent: - database: passwd - key: "{{ borg_client_user }}" - become: true - -- name: Compute borg_client_user_home if not set - ansible.builtin.set_fact: - borg_client_user_home: "{{ ansible_facts.getent_passwd[borg_client_user][4] }}" - when: borg_client_user_home is not defined - -- name: Validate borg_client_user home exists - ansible.builtin.stat: - path: "{{ borg_client_user_home }}" - register: user_home_stat - become: true - -- name: Fail if borg_client_user home missing - ansible.builtin.fail: - msg: | - Home directory {{ borg_client_user_home }} for user {{ borg_client_user }} does not exist. - Please ensure the user has a valid home directory before running this role. - when: not user_home_stat.stat.exists - -- name: Check readability of included paths - ansible.builtin.stat: - path: "{{ item }}" - loop: "{{ borg_included_dirs }}" - register: included_paths_stat - become: true - become_user: "{{ borg_client_user }}" - when: borg_included_dirs | length > 0 - - name: Compute SSH key identifier ansible.builtin.set_fact: borg_ssh_key_identifier: "{{ (borg_server_host_url ~ '_' ~ borg_repo_name) | regex_replace('[^a-zA-Z0-9]', '_') }}" @@ -230,13 +196,24 @@ delegate_to: localhost become: false -- name: Create backup scripts - ansible.builtin.include_tasks: client_create_scripts_each.yml - loop: - - "{{ borg_backup_script_location }}" - - "{{ borg_backup_script_location }}{{ '@' if borg_backup_argument != '' else '' }}{{ borg_backup_argument }}" - loop_control: - loop_var: script_location +- name: Create repository-specific backup script + ansible.builtin.template: + src: borg_backup_script.j2 + dest: "{{ borg_backup_script_location }}{{ '@' if borg_backup_argument != '' else '' }}{{ borg_backup_argument }}" + owner: "{{ borg_client_user }}" + group: "{{ borg_client_user }}" + mode: "0711" + become: true + +- name: Create repository-specific prune script + ansible.builtin.template: + src: borg_prune_script.j2 + dest: "{{ borg_prune_script_location }}{{ '@' if borg_backup_argument != '' else '' }}{{ borg_backup_argument }}" + owner: "{{ borg_client_user }}" + group: "{{ borg_client_user }}" + mode: "0711" + become: true + when: borg_prune_enabled - name: Configure systemd borg_backup service ansible.builtin.template: @@ -258,6 +235,72 @@ notify: Reload systemd become: true +- name: Configure systemd borg_prune service + ansible.builtin.template: + src: borg_prune.service.j2 + dest: /etc/systemd/system/{{ borg_prune_service_name }}{{ "@" if borg_backup_argument != "" }}{{ borg_backup_argument }}.service + mode: "0644" + owner: root + group: root + notify: Reload systemd + become: true + when: borg_prune_enabled + +- name: Configure systemd borg_prune timer + ansible.builtin.template: + src: borg_prune.timer.j2 + dest: /etc/systemd/system/{{ borg_prune_timer_name }}{{ "@" if borg_backup_argument != "" }}{{ borg_backup_argument }}.timer + mode: "0644" + owner: root + group: root + notify: Reload systemd + become: true + when: + - borg_prune_enabled + - borg_prune_trigger == 'timer' + +- name: Check if stale borg_prune timer exists + ansible.builtin.stat: + path: /etc/systemd/system/{{ borg_prune_timer_name }}{{ "@" if borg_backup_argument != "" }}{{ borg_backup_argument }}.timer + register: stale_prune_timer_stat + become: true + when: not (borg_prune_enabled and borg_prune_trigger == 'timer') + +- name: Disable stale borg_prune timer + ansible.builtin.systemd: + name: "{{ borg_prune_timer_name }}{{ '@' if borg_backup_argument != '' else '' }}{{ borg_backup_argument }}.timer" + state: stopped + enabled: false + become: true + when: + - not (borg_prune_enabled and borg_prune_trigger == 'timer') + - stale_prune_timer_stat.stat.exists + +- name: Remove stale borg_prune timer file + ansible.builtin.file: + path: /etc/systemd/system/{{ borg_prune_timer_name }}{{ "@" if borg_backup_argument != "" }}{{ borg_backup_argument }}.timer + state: absent + notify: Reload systemd + become: true + when: not (borg_prune_enabled and borg_prune_trigger == 'timer') + +- name: Remove stale borg_prune service file + ansible.builtin.file: + path: /etc/systemd/system/{{ borg_prune_service_name }}{{ "@" if borg_backup_argument != "" }}{{ borg_backup_argument }}.service + state: absent + notify: Reload systemd + become: true + when: not borg_prune_enabled + +- name: Remove stale repository-specific prune script + ansible.builtin.file: + path: "{{ borg_prune_script_location }}{{ '@' if borg_backup_argument != '' else '' }}{{ borg_backup_argument }}" + state: absent + become: true + when: + - not borg_prune_enabled + - borg_prune_script_location | length > 0 + - name: Reload systemd now before enabling services ansible.builtin.meta: flush_handlers @@ -267,3 +310,13 @@ state: started enabled: true become: true + +- name: Enable borg_prune systemd timer + ansible.builtin.systemd: + name: "{{ borg_prune_timer_name }}{{ '@' if borg_backup_argument != '' else '' }}{{ borg_backup_argument }}.timer" + state: started + enabled: true + become: true + when: + - borg_prune_enabled + - borg_prune_trigger == 'timer' diff --git a/tasks/main.yml b/tasks/main.yml index 6feca79..47c45a7 100644 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -1,4 +1,7 @@ --- +- name: Validate role configuration + ansible.builtin.include_tasks: validate.yml + - name: Install dependencies ansible.builtin.include_tasks: installation.yml when: state == "present" diff --git a/tasks/validate.yml b/tasks/validate.yml new file mode 100644 index 0000000..04e7401 --- /dev/null +++ b/tasks/validate.yml @@ -0,0 +1,8 @@ +--- +- name: Validate present-state configuration + ansible.builtin.include_tasks: validate_present.yml + when: state == "present" + +- name: Validate absent-state configuration + ansible.builtin.include_tasks: validate_absent.yml + when: state == "absent" diff --git a/tasks/validate_absent.yml b/tasks/validate_absent.yml new file mode 100644 index 0000000..b234107 --- /dev/null +++ b/tasks/validate_absent.yml @@ -0,0 +1,10 @@ +--- +- name: Validate absent-state variables + ansible.builtin.assert: + that: + - borg_repo_name | length > 0 + - borg_server_user_home | length > 0 + - borg_backup_script_location | length > 0 + fail_msg: >- + Invalid configuration for state=absent. Ensure borg_repo_name, + borg_server_user_home, and borg_backup_script_location are set. diff --git a/tasks/validate_present.yml b/tasks/validate_present.yml new file mode 100644 index 0000000..fafa807 --- /dev/null +++ b/tasks/validate_present.yml @@ -0,0 +1,104 @@ +--- +- name: Validate required present-state variables + ansible.builtin.assert: + that: + - borg_repo_name | length > 0 + - borg_server_user | length > 0 + - borg_server_user_home | length > 0 + - borg_backup_script_location | length > 0 + - borg_included_dirs | length > 0 + fail_msg: >- + Invalid configuration for state=present. Ensure borg_repo_name, + borg_server_user, borg_server_user_home, borg_backup_script_location are + set and borg_included_dirs is not empty. + +- name: Validate prune configuration + ansible.builtin.assert: + that: + - not borg_prune_enabled or not borg_mode_append_only + - not borg_prune_enabled or borg_prune_glob_archives | length > 0 + - not borg_prune_enabled or borg_prune_trigger in ['after_backup', 'timer'] + - not borg_prune_enabled or borg_compact_threshold >= 0 + - not borg_prune_enabled or borg_compact_threshold <= 100 + - not borg_prune_enabled or ( + borg_prune_keep_within | length > 0 or + borg_prune_keep_last | length > 0 or + borg_prune_keep_minutely | length > 0 or + borg_prune_keep_hourly | length > 0 or + borg_prune_keep_daily | length > 0 or + borg_prune_keep_weekly | length > 0 or + borg_prune_keep_monthly | length > 0 or + borg_prune_keep_13weekly | length > 0 or + borg_prune_keep_3monthly | length > 0 or + borg_prune_keep_yearly | length > 0 or + borg_prune_additional_arguments | length > 0 + ) + - not borg_prune_enabled or borg_prune_service_name | length > 0 + - not borg_prune_enabled or borg_prune_script_location | length > 0 + - borg_prune_trigger != 'timer' or borg_prune_timer_name | length > 0 + - borg_prune_trigger != 'timer' or borg_prune_systemd_oncalendar | length > 0 + - borg_prune_trigger != 'timer' or borg_prune_systemd_accuracysec | length > 0 + fail_msg: >- + Invalid prune configuration. Prune requires at least one retention rule + from borg_prune_keep_* / borg_prune_keep_within or + borg_prune_additional_arguments, a non-empty archive glob, compact + threshold between 0 and 100, and it is incompatible with + borg_mode_append_only. + +- name: Ensure borg_client_user exists + ansible.builtin.getent: + database: passwd + key: "{{ borg_client_user }}" + become: true + +- name: Compute borg_client_user_home if not set + ansible.builtin.set_fact: + borg_client_user_home: "{{ ansible_facts.getent_passwd[borg_client_user][4] }}" + when: borg_client_user_home is not defined + +- name: Validate borg_client_user home exists + ansible.builtin.stat: + path: "{{ borg_client_user_home }}" + register: user_home_stat + become: true + +- name: Fail if borg_client_user home missing + ansible.builtin.fail: + msg: | + Home directory {{ borg_client_user_home }} for user {{ borg_client_user }} does not exist. + Please ensure the user has a valid home directory before running this role. + when: not user_home_stat.stat.exists + +- name: Check readability of included paths + ansible.builtin.stat: + path: "{{ item }}" + loop: "{{ borg_included_dirs }}" + register: included_paths_stat + become: true + become_user: "{{ borg_client_user }}" + +- name: Fail if included path is unreadable or missing + ansible.builtin.fail: + msg: >- + Included path {{ item.item }} is missing or not accessible by + {{ borg_client_user }}. + when: + - not item.stat.exists or not item.stat.readable + loop: "{{ included_paths_stat.results }}" + +- name: Ensure borg_server_user exists when auto-create disabled + ansible.builtin.getent: + database: passwd + key: "{{ borg_server_user }}" + become: true + delegate_to: "{{ borg_server_host }}" + when: not borg_server_user_create + +- name: Fail if borg_server_user does not exist when auto-create disabled + ansible.builtin.fail: + msg: | + User {{ borg_server_user }} does not exist on {{ borg_server_host }}. + Please create the user before running this role or set borg_server_user_create: true. + when: + - not borg_server_user_create + - ansible_facts.getent_passwd[borg_server_user] is not defined |