aboutsummaryrefslogtreecommitdiffstats
path: root/tasks/client_setup.yml
blob: fc32e03c6d2649d398c78ed8483a8fdcc4443893 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
---
- 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: "{{ 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]', '_') }}"

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

- name: Create SSH Directory
  ansible.builtin.file:
    path: "{{ borg_client_user_home }}/.ssh"
    owner: "{{ borg_client_user }}"
    group: "{{ borg_client_user }}"
    mode: "0700"
    state: directory
  become: true

- name: Add borg server to known_hosts
  ansible.builtin.known_hosts:
    name: "{{ borg_server_host_url }}"
    key: "{{ borg_server_host_url }} {{ borg_server_host_ssh_key }}"
    path: "{{ borg_client_user_home }}/.ssh/known_hosts"
    state: present
  become: true
  become_user: "{{ borg_client_user }}"

- name: Generate SSH keys
  community.crypto.openssh_keypair:
    path: "{{ borg_ssh_key_path }}"
    type: "{{ borg_ssh_key_type }}"
    owner: "{{ borg_client_user }}"
    group: "{{ borg_client_user }}"
    mode: "0600"
    comment: "{{ borg_client_user }}@{{ inventory_hostname }}{% if borg_ssh_key_per_repo %}_{{ borg_ssh_key_identifier }}{% endif %}"
  become: true
  register: ssh_key

- name: Ensure authorized_keys file exists on borg server
  ansible.builtin.file:
    path: "{{ borg_server_user_home }}/.ssh/authorized_keys"
    state: touch
    owner: "{{ borg_server_user }}"
    group: "{{ borg_server_user }}"
    mode: "0600"
    access_time: preserve
    modification_time: preserve
  become: true
  delegate_to: "{{ borg_server_host }}"

- name: Read existing authorized_keys content
  ansible.builtin.slurp:
    src: "{{ borg_server_user_home }}/.ssh/authorized_keys"
  become: true
  delegate_to: "{{ borg_server_host }}"
  register: auth_keys_content

- name: Find existing authorized_keys line for this host
  ansible.builtin.set_fact:
    existing_line: >-
      {{
        (auth_keys_content.content | b64decode).splitlines()
        | select("search", ssh_key.public_key | trim | regex_escape)
        | first
        | default("")
      }}

- name: Detect --append-only setting from existing entry
  ansible.builtin.set_fact:
    existing_append_only: "{{ existing_line is search('--append-only') }}"
  when: existing_line | length > 0

- name: Detect --storage-quota setting from existing entry
  ansible.builtin.set_fact:
    existing_storage_quota: >-
      {{
        (existing_line | regex_findall('--storage-quota[= ](\d+[KMGT]?)')
        | first
        | default(""))
      }}
  when: existing_line | length > 0

- name: Fail if --append-only setting differs from existing entry
  ansible.builtin.fail:
    msg: |
      Inconsistent --append-only setting for host {{ inventory_hostname }}.
      Existing entry has --append-only={{ existing_append_only }}, but current invocation uses --append-only={{ borg_mode_append_only }}.
      All repositories for a host must have the same --append-only setting.
  when:
    - existing_line | length > 0
    - existing_append_only != borg_mode_append_only

- name: Fail if --storage-quota setting differs from existing entry
  ansible.builtin.fail:
    msg: |
      Inconsistent --storage-quota setting for host {{ inventory_hostname }}.
      Existing entry has --storage-quota={{ existing_storage_quota }}, but current invocation uses --storage-quota={{ borg_storage_quota }}.
      All repositories for a host must have the same --storage-quota setting.
  when:
    - existing_line | length > 0
    - existing_storage_quota != (borg_storage_quota | default(""))

- name: Compute all repos for this host
  ansible.builtin.set_fact:
    all_repos: >-
      {{
        (
          (existing_line | regex_findall('--restrict-to-repository ([^\s"]+)'))
          + [borg_server_user_home + '/' + borg_repo_name]
        ) | unique | sort }}

- 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 }}"
    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 }}
    state: present
  become: true
  delegate_to: "{{ borg_server_host }}"

- name: Initialise Borg repository
  ansible.builtin.command: >
    borg init --encryption=repokey
    {{ borg_server_user }}@{{ borg_server_host_url }}:{{ borg_server_user_home }}/{{ borg_repo_name }}
  environment:
    BORG_PASSPHRASE: "{{ borg_passphrase }}"
    BORG_RSH: "{{ ('ssh -i ' ~ borg_ssh_key_path) if borg_ssh_key_per_repo else omit }}"
  become: true
  become_user: "{{ borg_client_user }}"
  register: init_borg_output
  changed_when: init_borg_output.rc == 0
  failed_when:
    - init_borg_output.rc not in [0, 2]
      or (
      init_borg_output.rc == 2
      and 'already exists' not in init_borg_output.stdout
      and 'already exists' not in init_borg_output.stderr
      )

- name: Export decryption keys
  when: borg_decryption_keys_yaml_path | default('') | length > 0
  block:
    - name: Make sure key file exists
      ansible.builtin.file:
        path: "{{ borg_decryption_keys_yaml_path }}"
        state: touch
        mode: "0600"
        access_time: preserve
        modification_time: preserve
      delegate_to: localhost
      become: false

    - name: Read Vars file
      ansible.builtin.include_vars:
        file: "{{ borg_decryption_keys_yaml_path }}"
      register: local

    - name: Add repository encryption keys to ansible repo
      when: not (inventory_hostname ~ '_' ~ borg_repo_name) in local.ansible_facts
      throttle: 1
      block:
        - name: If host new read encryption keys
          ansible.builtin.command: >
            borg key export --paper
            {{ borg_server_user }}@{{ borg_server_host_url }}:{{ borg_server_user_home }}/{{ borg_repo_name }}
          environment:
            BORG_RSH: "{{ ('ssh -i ' ~ borg_ssh_key_path) if borg_ssh_key_per_repo else omit }}"
          become: true
          become_user: "{{ borg_client_user }}"
          register: borg_keys
          changed_when: borg_keys.rc != 0

        - name: If host new add encryption keys to vars
          ansible.builtin.set_fact:
            decryption_keys: "{{ local.ansible_facts | combine({(inventory_hostname ~ '_' ~ borg_repo_name): borg_keys.stdout}) }}"

    - name: Update encryption vars
      ansible.builtin.copy:
        content: "{{ decryption_keys | to_nice_yaml(indent=2, width=2048) }}"
        dest: "{{ borg_decryption_keys_yaml_path }}"
        mode: "0600"
      when: decryption_keys is defined
      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: Configure systemd borg_backup service
  ansible.builtin.template:
    src: borg_backup.service.j2
    dest: /etc/systemd/system/{{ borg_backup_timer_name }}{{ "@" if borg_backup_argument != "" }}{{ borg_backup_argument }}.service
    mode: "0644"
    owner: root
    group: root
  notify: Reload systemd
  become: true

- name: Configure systemd borg_backup timer
  ansible.builtin.template:
    src: borg_backup.timer.j2
    dest: /etc/systemd/system/{{ borg_backup_timer_name }}{{ "@" if borg_backup_argument != "" }}{{ borg_backup_argument }}.timer
    mode: "0644"
    owner: root
    group: root
  notify: Reload systemd
  become: true

- name: Reload systemd now before enabling services
  ansible.builtin.meta: flush_handlers

- name: Enable borg_backup systemd timer
  ansible.builtin.systemd:
    name: "{{ borg_backup_timer_name }}{{ '@' if borg_backup_argument != '' else '' }}{{ borg_backup_argument }}.timer"
    state: started
    enabled: true
  become: true