aboutsummaryrefslogtreecommitdiffstats
path: root/tasks
diff options
context:
space:
mode:
authorColin Wilk <colin@wilk.cx>2026-09-01 21:01:05 +0200
committerColin Wilk <colin@wilk.cx>2026-09-01 21:43:57 +0200
commit9c7586c7c3a235672ec6490d1a8bc44a222ce5d1 (patch)
tree8b36c5ee3105da2ae769c0d9cd96683d213c949d /tasks
parentfa137a92e1084a07608a4008ec0cb891baa76774 (diff)
downloadansible-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.yml74
-rw-r--r--tasks/client_create_scripts_each.yml34
-rw-r--r--tasks/client_setup.yml135
-rw-r--r--tasks/main.yml3
-rw-r--r--tasks/validate.yml8
-rw-r--r--tasks/validate_absent.yml10
-rw-r--r--tasks/validate_present.yml104
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