diff options
| author | Colin Wilk <colin@wilk.cx> | 2026-07-02 18:10:04 +0200 |
|---|---|---|
| committer | Colin Wilk <colin@wilk.cx> | 2026-07-02 18:10:04 +0200 |
| commit | 50b914e77695fd1cdb294653f20143747e7e3b01 (patch) | |
| tree | b667145cd04e39abb52d1680fc61a17513ae0af6 /tasks | |
| parent | 66e46df3d2a840bf62ef8084ad171041772db65e (diff) | |
| download | ansible-role-borgbackup-50b914e77695fd1cdb294653f20143747e7e3b01.tar.gz ansible-role-borgbackup-50b914e77695fd1cdb294653f20143747e7e3b01.zip | |
Add state: absent variable
Allow removing configuration for repositories.
Diffstat (limited to 'tasks')
| -rw-r--r-- | tasks/absent.yml | 298 | ||||
| -rw-r--r-- | tasks/client_setup.yml | 21 | ||||
| -rw-r--r-- | tasks/main.yml | 7 |
3 files changed, 319 insertions, 7 deletions
diff --git a/tasks/absent.yml b/tasks/absent.yml new file mode 100644 index 0000000..b1a5592 --- /dev/null +++ b/tasks/absent.yml @@ -0,0 +1,298 @@ +--- +- name: Ensure borg_client_user exists + ansible.builtin.getent: + database: passwd + key: "{{ borg_client_user }}" + become: true + failed_when: false + +- 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 + - ansible_facts.getent_passwd[borg_client_user] is defined + +- 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]', '_') }} + +- name: Compute SSH key path + ansible.builtin.set_fact: + borg_ssh_key_path: >- + {{ borg_client_user_home }}/.ssh/id_{{ borg_ssh_key_type }}{%- if borg_ssh_key_per_repo -%}_borgbackup_{{ borg_ssh_key_identifier }}{%- endif -%} + when: borg_client_user_home is defined + +- name: Read SSH public key + ansible.builtin.slurp: + src: "{{ borg_ssh_key_path }}.pub" + register: ssh_pubkey_slurp + become: true + when: borg_ssh_key_path is defined + failed_when: false + +- name: Check if systemd timer exists + ansible.builtin.stat: + path: /etc/systemd/system/{{ borg_backup_timer_name }}{{ '@' if borg_backup_argument != '' }}{{ borg_backup_argument }}.timer + register: timer_stat + become: true + +- name: Stop systemd timer + ansible.builtin.systemd: + name: "{{ borg_backup_timer_name }}{{ '@' if borg_backup_argument != '' }}{{ borg_backup_argument }}.timer" + state: stopped + enabled: false + become: true + when: 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: Stop systemd service + ansible.builtin.systemd: + name: "{{ borg_backup_service_name }}{{ '@' if borg_backup_argument != '' }}{{ borg_backup_argument }}.service" + state: stopped + enabled: false + become: true + when: 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 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: 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 + 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 }}" + 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 + ansible.builtin.file: + path: "{{ borg_backup_script_location }}" + state: absent + become: true + when: + - base_script_stat.stat.exists + - ('ANSIBLE MANAGED BLOCK' not in (base_script_content.content | b64decode)) + +- name: Remove per-repo SSH private key + ansible.builtin.file: + path: "{{ borg_ssh_key_path }}" + state: absent + become: true + when: + - borg_ssh_key_per_repo + - borg_ssh_key_path is defined + +- name: Remove per-repo SSH public key + ansible.builtin.file: + path: "{{ borg_ssh_key_path }}.pub" + state: absent + become: true + when: + - borg_ssh_key_per_repo + - borg_ssh_key_path is defined + +- name: Read existing authorized_keys on server + ansible.builtin.slurp: + src: "{{ borg_server_user_home }}/.ssh/authorized_keys" + become: true + delegate_to: "{{ borg_server_host }}" + register: auth_keys_slurp + ignore_errors: true + +- name: Update authorized_keys on server + when: + - auth_keys_slurp.content is defined + - ssh_pubkey_slurp.content is defined + block: + - name: Get public key content + ansible.builtin.set_fact: + ssh_public_key: "{{ ssh_pubkey_slurp.content | b64decode | trim }}" + + - name: Normalize SSH public key for matching + ansible.builtin.set_fact: + ssh_public_key_material: >- + {{ + ssh_public_key + | regex_search('^[^ ]+ [^ ]+') + | default(ssh_public_key) + }} + + - name: Parse authorized_keys lines + ansible.builtin.set_fact: + auth_keys_lines: "{{ (auth_keys_slurp.content | b64decode).splitlines() }}" + + - name: Find line matching this host's public key + ansible.builtin.set_fact: + matching_line: >- + {{ + auth_keys_lines + | select('search', ssh_public_key_material | regex_escape) + | first + | default('') + }} + + - name: Extract authorized_keys comment for shared key + ansible.builtin.set_fact: + authorized_keys_comment: "{{ matching_line | regex_search('([^\" ]+@[^\" ]+)$') | default(borg_client_user ~ '@' ~ inventory_hostname) }}" + when: + - not borg_ssh_key_per_repo + - matching_line | length > 0 + + - name: Remove entire authorized_keys line (per-repo key) + ansible.builtin.lineinfile: + path: "{{ borg_server_user_home }}/.ssh/authorized_keys" + regexp: "{{ ssh_public_key_material | regex_escape }}" + state: absent + become: true + delegate_to: "{{ borg_server_host }}" + when: + - borg_ssh_key_per_repo + - matching_line | length > 0 + + - name: Extract repos from authorized_keys line (shared key) + ansible.builtin.set_fact: + existing_repos: >- + {{ + (matching_line | regex_findall('--restrict-to-repository ([^\s"]+)')) + }} + repo_to_delete: "{{ borg_server_user_home }}/{{ borg_repo_name }}" + remaining_repos: >- + {{ + (matching_line | regex_findall('--restrict-to-repository ([^\s"]+)')) + | reject('eq', borg_server_user_home ~ '/' ~ borg_repo_name) + | list + }} + when: + - not borg_ssh_key_per_repo + - matching_line | length > 0 + + - name: Remove entire authorized_keys line (shared key, last repo) + ansible.builtin.lineinfile: + path: "{{ borg_server_user_home }}/.ssh/authorized_keys" + regexp: "{{ ssh_public_key_material | regex_escape }}" + state: absent + become: true + delegate_to: "{{ borg_server_host }}" + when: + - not borg_ssh_key_per_repo + - matching_line | length > 0 + - existing_repos is defined + - repo_to_delete in existing_repos + - remaining_repos is defined + - remaining_repos | length == 0 + + - name: Update authorized_keys keeping other repos (shared key, multiple repos) + ansible.builtin.lineinfile: + path: "{{ borg_server_user_home }}/.ssh/authorized_keys" + search_string: "{{ ssh_public_key_material }}" + line: >- + restrict,command="borg serve + {{ ' --append-only' if ('--append-only' in matching_line) }} + {{ ' --storage-quota ' ~ (matching_line | regex_findall('--storage-quota[= ](\S+)') | first) if (matching_line | regex_search('--storage-quota')) }} + {{ (remaining_repos | map('regex_replace', '^', '--restrict-to-repository ')) | join(' ') }}" + {{ ssh_public_key }} {{ authorized_keys_comment }} + state: present + become: true + delegate_to: "{{ borg_server_host }}" + when: + - not borg_ssh_key_per_repo + - matching_line | length > 0 + - authorized_keys_comment is defined + - existing_repos is defined + - repo_to_delete in existing_repos + - remaining_repos is defined + - remaining_repos | length > 0 + +- name: Delete repository data on server + ansible.builtin.file: + path: "{{ borg_server_user_home }}/{{ borg_repo_name }}" + state: absent + become: true + delegate_to: "{{ borg_server_host }}" + when: + - borg_dangerously_delete_backups | default(false) | bool + - borg_server_user_home | default('') | length > 0 + - borg_repo_name | default('') | length > 0 + +- name: Remove decryption key entry + when: borg_decryption_keys_yaml_path | default('') | length > 0 + block: + - name: Check if decryption keys file exists + ansible.builtin.stat: + path: "{{ borg_decryption_keys_yaml_path }}" + delegate_to: localhost + become: false + register: decryption_keys_stat + + - name: Read existing decryption keys file + ansible.builtin.slurp: + src: "{{ borg_decryption_keys_yaml_path }}" + delegate_to: localhost + become: false + register: decryption_keys_slurp + when: decryption_keys_stat.stat.exists + + - name: Remove entry for this host+repo + vars: + existing_keys: >- + {{ + ((decryption_keys_slurp.content | b64decode | from_yaml) | default({}, true)) + if decryption_keys_slurp.content is defined + else {} + }} + key_name: "{{ inventory_hostname ~ '_' ~ borg_repo_name }}" + ansible.builtin.copy: + content: >- + {{ + existing_keys + | dict2items + | rejectattr('key', '==', key_name) + | items2dict + | to_nice_yaml(indent=2, width=2048) + }} + dest: "{{ borg_decryption_keys_yaml_path }}" + mode: "0600" + delegate_to: localhost + become: false + when: decryption_keys_slurp.content is defined diff --git a/tasks/client_setup.yml b/tasks/client_setup.yml index 5d162ed..ab09926 100644 --- a/tasks/client_setup.yml +++ b/tasks/client_setup.yml @@ -39,10 +39,7 @@ - name: Compute SSH key path ansible.builtin.set_fact: borg_ssh_key_path: >- - {{ borg_client_user_home }}/.ssh/id_{{ borg_ssh_key_type }} - {%- if borg_ssh_key_per_repo -%} - _borgbackup_{{ borg_ssh_key_identifier }} - {%- endif -%} + {{ borg_client_user_home }}/.ssh/id_{{ borg_ssh_key_type }}{%- if borg_ssh_key_per_repo -%}_borgbackup_{{ borg_ssh_key_identifier }}{%- endif -%} - name: Create SSH Directory ansible.builtin.file: @@ -73,6 +70,16 @@ become: true register: ssh_key +- name: Normalize SSH public key for matching + ansible.builtin.set_fact: + ssh_public_key: "{{ ssh_key.public_key | trim }}" + ssh_public_key_material: >- + {{ + (ssh_key.public_key | trim) + | regex_search('^[^ ]+ [^ ]+') + | default(ssh_key.public_key | trim) + }} + - name: Ensure authorized_keys file exists on borg server ansible.builtin.file: path: "{{ borg_server_user_home }}/.ssh/authorized_keys" @@ -97,7 +104,7 @@ existing_line: >- {{ (auth_keys_content.content | b64decode).splitlines() - | select("search", ssh_key.public_key | trim | regex_escape) + | select("search", ssh_public_key_material | regex_escape) | first | default("") }} @@ -149,11 +156,11 @@ - name: Update authorized_keys entry for this host ansible.builtin.lineinfile: path: "{{ borg_server_user_home }}/.ssh/authorized_keys" - search_string: "{{ ssh_key.public_key | trim }}" + search_string: "{{ ssh_public_key_material }}" line: >- restrict,command="borg serve{{ " --append-only" if borg_mode_append_only }}{{ " --storage-quota " ~ borg_storage_quota if borg_storage_quota }} {{ all_repos | map('regex_replace', '^', '--restrict-to-repository ') | join(' ') }}" - {{ ssh_key.public_key | trim }} {{ borg_client_user }}@{{ inventory_hostname }} + {{ ssh_public_key }} {{ borg_client_user }}@{{ inventory_hostname }} state: present become: true delegate_to: "{{ borg_server_host }}" diff --git a/tasks/main.yml b/tasks/main.yml index 7ffef06..6feca79 100644 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -1,9 +1,16 @@ --- - name: Install dependencies ansible.builtin.include_tasks: installation.yml + when: state == "present" - name: Run setup on server ansible.builtin.include_tasks: server_setup.yml + when: state == "present" - name: Run setup on client ansible.builtin.include_tasks: client_setup.yml + when: state == "present" + +- name: Remove borg backup configuration + ansible.builtin.include_tasks: absent.yml + when: state == "absent" |