mirror of
https://github.com/fscotto/infra.git
synced 2026-10-06 06:49:49 +00:00
Integrate consistent Nextcloud backups and recovery
This commit is contained in:
@@ -35,6 +35,9 @@
|
||||
- name: Import Atlas offline USB backup tasks
|
||||
ansible.builtin.import_tasks: usb_backup.yml
|
||||
|
||||
- name: Import recurring Nextcloud backup preparation
|
||||
ansible.builtin.import_tasks: nextcloud_backup.yml
|
||||
|
||||
- name: Import Atlas Prometheus backup pull identity tasks
|
||||
ansible.builtin.import_tasks: prometheus_pull_identity.yml
|
||||
|
||||
|
||||
@@ -39,6 +39,10 @@
|
||||
(atlas_nextcloud_users | map(attribute='password') | list) }}
|
||||
no_log: true
|
||||
|
||||
- name: Prepare access to declared existing Archive directories
|
||||
ansible.builtin.include_tasks: nextcloud_external_access.yml
|
||||
when: atlas_nextcloud_external_mounts | length > 0
|
||||
|
||||
- name: Verify the existing application-data parent is mounted
|
||||
community.general.zfs_facts:
|
||||
name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_app_data }}"
|
||||
@@ -197,7 +201,8 @@
|
||||
owner: "{{ atlas_admin_username }}"
|
||||
group: "{{ atlas_admin_group }}"
|
||||
mode: "0644"
|
||||
loop: [atlas-nextcloud-cron.service, atlas-nextcloud-cron.timer]
|
||||
loop: [atlas-nextcloud-cron.service, atlas-nextcloud-cron.timer,
|
||||
atlas-nextcloud-external-scan.service, atlas-nextcloud-external-scan.timer]
|
||||
register: atlas_nextcloud_cron_units
|
||||
|
||||
- name: Manage and verify rootless Nextcloud services
|
||||
@@ -227,7 +232,13 @@
|
||||
scope: user
|
||||
name: "{{ item }}"
|
||||
state: >-
|
||||
{{ 'restarted' if (atlas_nextcloud_quadlets is changed or
|
||||
{{ 'restarted' if (
|
||||
atlas_nextcloud_quadlets.results |
|
||||
selectattr('item', 'equalto', item | replace('.service', '.container')) |
|
||||
selectattr('changed') | list | length > 0 or
|
||||
atlas_nextcloud_quadlets.results |
|
||||
selectattr('item', 'equalto', 'atlas-nextcloud.network') |
|
||||
selectattr('changed') | list | length > 0 or
|
||||
atlas_nextcloud_private_configuration is changed or
|
||||
atlas_nextcloud_secret_files is changed) else 'started' }}
|
||||
loop: "{{ atlas_nextcloud_services }}"
|
||||
@@ -299,6 +310,14 @@
|
||||
state: "{{ 'restarted' if atlas_nextcloud_cron_units is changed else 'started' }}"
|
||||
enabled: true
|
||||
|
||||
- name: Enable periodic targeted Archive discovery
|
||||
ansible.builtin.systemd:
|
||||
scope: user
|
||||
name: atlas-nextcloud-external-scan.timer
|
||||
state: "{{ 'restarted' if atlas_nextcloud_cron_units is changed else 'started' }}"
|
||||
enabled: true
|
||||
when: atlas_nextcloud_external_mounts | length > 0
|
||||
|
||||
- name: Verify ONLYOFFICE local health without publishing the domain
|
||||
ansible.builtin.uri:
|
||||
url: "http://127.0.0.1:{{ atlas_onlyoffice_http_port }}/healthcheck"
|
||||
|
||||
@@ -169,3 +169,18 @@
|
||||
argv: [podman, exec, --user, '33', atlas-nextcloud, php, occ, background:cron]
|
||||
when: atlas_nextcloud_background_mode.stdout | trim != 'cron'
|
||||
changed_when: true
|
||||
|
||||
- name: Enable shipped external storage support when required
|
||||
ansible.builtin.command:
|
||||
argv: [podman, exec, --user, '33', atlas-nextcloud, php, occ, app:enable, files_external]
|
||||
when:
|
||||
- atlas_nextcloud_external_mounts | length > 0
|
||||
- "'files_external' not in (atlas_nextcloud_current_apps.stdout | from_json).enabled"
|
||||
changed_when: true
|
||||
|
||||
- name: Maintain only the declared Archive mounts
|
||||
ansible.builtin.include_tasks: nextcloud_external_mount.yml
|
||||
loop: "{{ atlas_nextcloud_external_mounts }}"
|
||||
loop_control:
|
||||
loop_var: atlas_nextcloud_mount
|
||||
label: "{{ atlas_nextcloud_mount.name }}"
|
||||
|
||||
55
ansible/roles/profile_atlas/tasks/nextcloud_backup.yml
Normal file
55
ansible/roles/profile_atlas/tasks/nextcloud_backup.yml
Normal file
@@ -0,0 +1,55 @@
|
||||
---
|
||||
- name: Manage recurring consistent Nextcloud backup preparation
|
||||
tags: [atlas, nextcloud_backup]
|
||||
when: atlas_manage_nextcloud | bool
|
||||
block:
|
||||
- name: Validate private backup scope and local bundle retention
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- atlas_nextcloud_backup_root == atlas_mount_root ~ '/backup/nextcloud'
|
||||
- atlas_nextcloud_backup_keep | int >= 2
|
||||
- atlas_manage_borg_backup | bool
|
||||
- atlas_manage_usb_backup | bool
|
||||
|
||||
- name: Install recurring backup helper with shell syntax validation
|
||||
ansible.builtin.template:
|
||||
src: atlas-nextcloud-backup.sh.j2
|
||||
dest: /usr/local/sbin/atlas-nextcloud-backup
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0750"
|
||||
validate: /bin/bash -n %s
|
||||
|
||||
- name: Install Nextcloud backup preparation and boot recovery units
|
||||
ansible.builtin.template:
|
||||
src: "{{ item }}.j2"
|
||||
dest: "/etc/systemd/system/{{ item }}"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
loop: [atlas-nextcloud-backup.service, atlas-nextcloud-backup-recovery.service]
|
||||
|
||||
- name: Create backup dependency drop-in directories
|
||||
ansible.builtin.file:
|
||||
path: "/etc/systemd/system/{{ item }}.d"
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0755"
|
||||
loop: [atlas-borg-backup.service, atlas-usb-backup.service]
|
||||
|
||||
- name: Require a fresh consistent bundle before offsite and manual USB backups
|
||||
ansible.builtin.template:
|
||||
src: atlas-nextcloud-backup-dependency.conf.j2
|
||||
dest: "/etc/systemd/system/{{ item }}.d/nextcloud.conf"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
loop: [atlas-borg-backup.service, atlas-usb-backup.service]
|
||||
|
||||
- name: Reload systemd and enable interruption recovery without running a backup
|
||||
ansible.builtin.systemd:
|
||||
daemon_reload: true
|
||||
name: atlas-nextcloud-backup-recovery.service
|
||||
enabled: true
|
||||
when: not ansible_check_mode
|
||||
100
ansible/roles/profile_atlas/tasks/nextcloud_external_access.yml
Normal file
100
ansible/roles/profile_atlas/tasks/nextcloud_external_access.yml
Normal file
@@ -0,0 +1,100 @@
|
||||
---
|
||||
- name: Restrict external storage to explicit Archive directories
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- item.source in [atlas_archive_mountpoint ~ '/Documents', atlas_icloudpd_photos_dir]
|
||||
- item.target is match('^/mnt/archive-[a-z]+$')
|
||||
- item.name is match('^[A-Za-z][A-Za-z ]+$')
|
||||
- item.readonly is boolean
|
||||
- item.user in (atlas_nextcloud_users | map(attribute='username') | list)
|
||||
- item.source != atlas_icloudpd_photos_dir or item.readonly
|
||||
loop: "{{ atlas_nextcloud_external_mounts }}"
|
||||
|
||||
- name: Inspect existing sources without creating or moving data
|
||||
ansible.builtin.stat:
|
||||
path: "{{ item.source }}"
|
||||
follow: false
|
||||
loop: "{{ atlas_nextcloud_external_mounts }}"
|
||||
register: atlas_nextcloud_external_sources
|
||||
|
||||
- name: Refuse missing sources and symlinks
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- item.stat.isdir | default(false)
|
||||
- not (item.stat.islnk | default(false))
|
||||
loop: "{{ atlas_nextcloud_external_sources.results }}"
|
||||
loop_control:
|
||||
label: "{{ item.item.source }}"
|
||||
|
||||
- name: Verify the Archive dataset before modifying its ACL capability
|
||||
community.general.zfs_facts:
|
||||
name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_archive }}"
|
||||
properties: name,mounted,mountpoint
|
||||
register: atlas_nextcloud_external_dataset
|
||||
|
||||
- name: Refuse an absent or unmounted Archive dataset
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- atlas_nextcloud_external_dataset.ansible_facts.ansible_zfs_datasets | length == 1
|
||||
- atlas_nextcloud_external_dataset.ansible_facts.ansible_zfs_datasets[0].mounted == 'yes'
|
||||
- atlas_nextcloud_external_dataset.ansible_facts.ansible_zfs_datasets[0].mountpoint == atlas_archive_mountpoint
|
||||
|
||||
- name: Enable persistent POSIX ACL support on the verified Archive dataset
|
||||
community.general.zfs:
|
||||
name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_archive }}"
|
||||
state: present
|
||||
extra_zfs_properties:
|
||||
acltype: posix
|
||||
|
||||
- name: Derive actual rootless web UID for narrowly scoped Archive ACLs
|
||||
become_user: "{{ atlas_admin_username }}"
|
||||
environment:
|
||||
XDG_RUNTIME_DIR: "/run/user/{{ atlas_admin_uid }}"
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- podman
|
||||
- unshare
|
||||
- python3
|
||||
- -c
|
||||
- >-
|
||||
print(next(int(b)+33-int(a) for a,b,n in
|
||||
(l.split() for l in open('/proc/self/uid_map')) if int(a)<=33<int(a)+int(n)))
|
||||
register: atlas_nextcloud_external_uid
|
||||
changed_when: false
|
||||
check_mode: false
|
||||
|
||||
- name: Grant web user access only inside the declared sources
|
||||
ansible.posix.acl:
|
||||
path: "{{ item.source }}"
|
||||
entity: "{{ atlas_nextcloud_external_uid.stdout | trim }}"
|
||||
etype: user
|
||||
permissions: "{{ 'rX' if item.readonly else 'rwX' }}"
|
||||
recursive: true
|
||||
follow: false
|
||||
state: present
|
||||
loop: "{{ atlas_nextcloud_external_mounts }}"
|
||||
|
||||
- name: Inherit web access on new files and directories
|
||||
ansible.posix.acl:
|
||||
path: "{{ item.source }}"
|
||||
entity: "{{ atlas_nextcloud_external_uid.stdout | trim }}"
|
||||
etype: user
|
||||
permissions: "{{ 'rX' if item.readonly else 'rwX' }}"
|
||||
default: true
|
||||
recursive: true
|
||||
follow: false
|
||||
state: present
|
||||
loop: "{{ atlas_nextcloud_external_mounts }}"
|
||||
|
||||
- name: Preserve administrator access to documents created through Nextcloud
|
||||
ansible.posix.acl:
|
||||
path: "{{ item.source }}"
|
||||
entity: "{{ atlas_admin_uid }}"
|
||||
etype: user
|
||||
permissions: rwX
|
||||
default: true
|
||||
recursive: true
|
||||
follow: false
|
||||
state: present
|
||||
loop: "{{ atlas_nextcloud_external_mounts }}"
|
||||
when: not item.readonly
|
||||
@@ -0,0 +1,94 @@
|
||||
---
|
||||
- name: Inspect current system mounts without exposing credentials
|
||||
ansible.builtin.command:
|
||||
argv: [podman, exec, --user, '33', atlas-nextcloud, php, occ, files_external:list, --output=json]
|
||||
register: atlas_nextcloud_mount_list
|
||||
changed_when: false
|
||||
no_log: true
|
||||
|
||||
- name: Select only the matching mount name
|
||||
ansible.builtin.set_fact:
|
||||
atlas_nextcloud_matching_mounts: >-
|
||||
{{ atlas_nextcloud_mount_list.stdout | from_json |
|
||||
selectattr('mount_point', 'equalto', '/' ~ atlas_nextcloud_mount.name) | list }}
|
||||
no_log: true
|
||||
|
||||
- name: Refuse duplicates or repurposing of existing unrelated storage
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- atlas_nextcloud_matching_mounts | length <= 1
|
||||
- >-
|
||||
atlas_nextcloud_matching_mounts | length == 0 or
|
||||
(atlas_nextcloud_matching_mounts[0].configuration.datadir | default('') == atlas_nextcloud_mount.target
|
||||
and atlas_nextcloud_matching_mounts[0].storage == '\\OC\\Files\\Storage\\Local')
|
||||
fail_msg: Existing storage conflicts with the declared Archive mount; refusing an implicit replacement.
|
||||
|
||||
- name: Create an absent local mount restricted to its declared user
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- podman
|
||||
- exec
|
||||
- --user
|
||||
- '33'
|
||||
- atlas-nextcloud
|
||||
- php
|
||||
- occ
|
||||
- files_external:create
|
||||
- "{{ atlas_nextcloud_mount.name }}"
|
||||
- local
|
||||
- null::null
|
||||
- --config
|
||||
- "datadir={{ atlas_nextcloud_mount.target }}"
|
||||
- --applicable-user
|
||||
- "{{ atlas_nextcloud_mount.user }}"
|
||||
- --output=json
|
||||
when: atlas_nextcloud_matching_mounts | length == 0
|
||||
register: atlas_nextcloud_mount_created
|
||||
changed_when: true
|
||||
|
||||
- name: Record the managed mount ID and options
|
||||
ansible.builtin.set_fact:
|
||||
atlas_nextcloud_mount_id: >-
|
||||
{{ atlas_nextcloud_mount_created.stdout | trim if atlas_nextcloud_matching_mounts | length == 0
|
||||
else atlas_nextcloud_matching_mounts[0].mount_id }}
|
||||
atlas_nextcloud_mount_options: >-
|
||||
{{ {} if atlas_nextcloud_matching_mounts | length == 0 else atlas_nextcloud_matching_mounts[0].options }}
|
||||
|
||||
- name: Restrict the managed mount to exactly its declared user
|
||||
ansible.builtin.command:
|
||||
argv: >-
|
||||
{{ ['podman', 'exec', '--user', '33', 'atlas-nextcloud', 'php', 'occ',
|
||||
'files_external:applicable', atlas_nextcloud_mount_id | string,
|
||||
'--add-user=' ~ atlas_nextcloud_mount.user] +
|
||||
(atlas_nextcloud_matching_mounts[0].applicable_groups |
|
||||
map('regex_replace', '^', '--remove-group=') | list) +
|
||||
(atlas_nextcloud_matching_mounts[0].applicable_users |
|
||||
reject('equalto', atlas_nextcloud_mount.user) |
|
||||
map('regex_replace', '^', '--remove-user=') | list) }}
|
||||
when:
|
||||
- atlas_nextcloud_matching_mounts | length > 0
|
||||
- >-
|
||||
atlas_nextcloud_matching_mounts[0].applicable_groups | length > 0 or
|
||||
atlas_nextcloud_matching_mounts[0].applicable_users != [atlas_nextcloud_mount.user]
|
||||
changed_when: true
|
||||
|
||||
- name: Maintain read-only photos and external change detection
|
||||
ansible.builtin.command:
|
||||
argv: [podman, exec, --user, '33', atlas-nextcloud, php, occ, files_external:option,
|
||||
"{{ atlas_nextcloud_mount_id }}", "{{ item.key }}", "{{ item.value | to_json }}"]
|
||||
loop:
|
||||
- {key: readonly, value: "{{ atlas_nextcloud_mount.readonly }}"}
|
||||
- {key: filesystem_check_changes, value: 1}
|
||||
- {key: enable_sharing, value: false}
|
||||
# Nextcloud persists option values as strings ("1" / "" for booleans).
|
||||
when: >-
|
||||
item.key not in atlas_nextcloud_mount_options or
|
||||
atlas_nextcloud_mount_options[item.key] | string !=
|
||||
(('1' if item.value else '') if item.value is boolean else item.value | string)
|
||||
changed_when: true
|
||||
|
||||
- name: Verify the managed local storage is accessible
|
||||
ansible.builtin.command:
|
||||
argv: [podman, exec, --user, '33', atlas-nextcloud, php, occ, files_external:verify,
|
||||
"{{ atlas_nextcloud_mount_id }}"]
|
||||
changed_when: false
|
||||
@@ -10,6 +10,7 @@
|
||||
properties:
|
||||
compression: zstd
|
||||
mountpoint: "{{ atlas_archive_mountpoint }}"
|
||||
acltype: posix
|
||||
- name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_services }}"
|
||||
mountpoint: "{{ atlas_services_mountpoint }}"
|
||||
owner: "{{ atlas_admin_username }}"
|
||||
|
||||
Reference in New Issue
Block a user