--- - 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