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
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
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
|