Add Atlas media and storage services

This commit is contained in:
Fabio Scotto di Santolo
2026-09-10 22:27:33 +02:00
parent 569e6ef24e
commit 64aebe8c34
34 changed files with 1127 additions and 209 deletions

View File

@@ -6,6 +6,29 @@ effective_username: "{{ server_username }}"
effective_user_group: "{{ server_user_group }}"
effective_user_home: "{{ server_user_home }}"
server_container_stack_dir: /opt/docker/server
server_atlas_music_enabled: false
server_atlas_sftp_remote_name: atlas
server_atlas_sftp_host: CHANGEME_ATLAS_WIREGUARD_IP
server_atlas_sftp_port: 22
server_atlas_sftp_username: admin
server_atlas_sftp_remote_path: /pool/media/music
server_atlas_sftp_private_key: "{{ vault_prometheus_atlas_sftp_private_key | default('') }}"
server_atlas_sftp_private_key_file: /etc/rclone/atlas_ed25519
server_atlas_sftp_known_host: ""
server_atlas_sftp_known_hosts_file: /etc/rclone/known_hosts
server_atlas_wireguard_unit: ""
server_rclone_config_dir: /etc/rclone
server_rclone_config_file: /etc/rclone/rclone.conf
server_rclone_music_service: rclone-music.service
server_rclone_music_mountpoint: /mnt/music_atlas
server_rclone_music_cache_dir: /var/cache/rclone-music
server_navidrome_data_dir: /opt/navidrome/data
server_navidrome_quadlet_dir: "{{ server_user_home }}/.config/containers/systemd"
server_navidrome_env_dir: "{{ server_user_home }}/.config/navidrome"
server_navidrome_env_file: "{{ server_navidrome_env_dir }}/navidrome.env"
server_navidrome_image: docker.io/deluan/navidrome:latest
server_navidrome_port: 4533
server_navidrome_db_password: "{{ vault_navidrome_db_password | default('') }}"
ai_agents: {}
vim_plugins_enabled: false
@@ -62,11 +85,7 @@ server_directories:
owner: root
group: root
mode: "0755"
- path: /opt/navidrome/data
owner: "{{ server_username }}"
group: "{{ server_user_group }}"
mode: "0755"
- path: /opt/music
- path: "{{ server_navidrome_data_dir }}"
owner: "{{ server_username }}"
group: "{{ server_user_group }}"
mode: "0755"

View File

@@ -1,10 +1,10 @@
---
# Replace every CHANGEME value before enabling Atlas storage management.
# Keep Atlas management gates disabled until the NAS and required Vault inputs are ready.
hostname: atlas
ansible_host: CHANGEME_ATLAS_HOST
ansible_host: 192.168.178.55
ansible_user: "{{ atlas_connection_username }}"
ansible_port: 22
ansible_ssh_private_key_file: CHANGEME_ATLAS_SSH_PRIVATE_KEY_PATH
ansible_ssh_private_key_file: /home/fscotto/.ssh/id_ed25519
atlas_admin_username: admin
atlas_connection_username: "{{ atlas_admin_username }}"
@@ -15,46 +15,67 @@ atlas_admin_gid: 1000
atlas_admin_ssh_keys: "{{ vault_atlas_authorized_ssh_keys | default([]) }}"
atlas_admin_password_hash: "{{ vault_atlas_admin_password_hash | default('!') }}"
atlas_lan_subnet: CHANGEME_LAN_SUBNET
atlas_lan_subnet: 192.168.178.0/24
atlas_aegis_ip: 192.168.178.54
atlas_manage_firewall: false
atlas_firewalld_zone: public
atlas_zfs_pool: CHANGEME_ZFS_POOL
atlas_zfs_pool: zpool
atlas_zfs_dataset_work: work
atlas_zfs_dataset_archive: archive
atlas_zfs_dataset_music: media/music
atlas_zfs_dataset_syncthing: syncthing
atlas_zfs_dataset_backup_prometheus: backup_prometheus
atlas_zfs_dataset_icloud_photos: icloud_photos
atlas_mount_root: /CHANGEME_ATLAS_MOUNT_ROOT
atlas_zfs_dataset_icloud_photos: media/icloud_photos
atlas_zfs_dataset_service_backups: backups/services
atlas_mount_root: /zpool
atlas_manage_storage: false
atlas_manage_media_stack: false
rocky_manage_openzfs_repo: true
rocky_manage_syncthing_binary: true
rocky_manage_podman: true
rocky_podman_packages:
- podman
host_packages:
- cockpit
- nfs-utils
- policycoreutils-python-utils
- samba
- samba-client
- samba-common-tools
- zfs
atlas_nfs_exports:
- path: "{{ atlas_work_mountpoint }}"
clients: "{{ atlas_lan_subnet }}(rw,sync,no_subtree_check,root_squash)"
- path: "{{ atlas_icloud_photos_mountpoint }}"
client: "{{ atlas_aegis_ip }}"
options:
- rw
- sync
- no_subtree_check
- all_squash
- "anonuid={{ atlas_immich_uid }}"
- "anongid={{ atlas_immich_gid }}"
atlas_samba_share_name: work
atlas_samba_share_name: Archive
atlas_samba_workgroup: WORKGROUP
atlas_samba_valid_users:
- "{{ atlas_admin_username }}"
atlas_samba_password: "{{ vault_atlas_samba_password | default('') }}"
# Append any pre-existing laptop Unix account here and keep its Samba password in Vault.
atlas_samba_accounts:
- username: "{{ atlas_admin_username }}"
password: "{{ vault_atlas_samba_password | default('') }}"
atlas_samba_valid_users: "{{ atlas_samba_accounts | map(attribute='username') | list }}"
atlas_immich_db_password: "{{ vault_atlas_immich_db_password | default('') }}"
atlas_firewalld_rich_rules:
- 'rule family="ipv4" source address="{{ atlas_lan_subnet }}" service name="ssh" accept'
- 'rule family="ipv4" source address="{{ atlas_lan_subnet }}" service name="cockpit" accept'
- 'rule family="ipv4" source address="{{ atlas_lan_subnet }}" port port="2049" protocol="tcp" accept'
- 'rule family="ipv4" source address="{{ atlas_aegis_ip }}" service name="nfs" accept'
- 'rule family="ipv4" source address="{{ atlas_lan_subnet }}" service name="samba" accept'
- 'rule family="ipv4" source address="{{ atlas_lan_subnet }}" service name="http" accept'
- 'rule family="ipv4" source address="{{ atlas_lan_subnet }}" service name="https" accept'
host_enabled_services:
- sshd
- cockpit.socket
- nfs-server
- smb
- zfs.target

View File

@@ -7,11 +7,19 @@ ansible_ssh_private_key_file: /home/fscotto/.ssh/id_ed25519
server_username: rocky
server_duckdns_domain: fscotto
server_atlas_music_enabled: false
server_atlas_sftp_host: CHANGEME_ATLAS_WIREGUARD_IP
server_atlas_sftp_username: admin
server_atlas_sftp_known_host: CHANGEME_ATLAS_SSH_HOST_KEY
server_atlas_wireguard_unit: wg-quick@wg0.service
server_atlas_sftp_private_key: "{{ vault_prometheus_atlas_sftp_private_key | default('') }}"
host_packages:
- cockpit
- cockpit-navigator
- cockpit-podman
- fuse3
- rclone
host_enabled_services:
- cockpit.socket

View File

@@ -8,20 +8,36 @@ atlas_admin_gid: 1000
atlas_admin_ssh_keys: []
atlas_admin_password_hash: "!"
atlas_immich_username: immich
atlas_immich_group: immich
atlas_immich_uid: 1100
atlas_immich_gid: 1100
atlas_immich_supplementary_groups:
- video
- render
atlas_lan_subnet: CHANGEME_LAN_SUBNET
atlas_aegis_ip: CHANGEME_AEGIS_IP
atlas_manage_firewall: false
atlas_firewalld_zone: public
atlas_zfs_pool: CHANGEME_ZFS_POOL
atlas_zfs_dataset_work: work
atlas_zfs_dataset_archive: archive
atlas_zfs_dataset_music: media/music
atlas_zfs_dataset_syncthing: syncthing
atlas_zfs_dataset_backup_prometheus: backup_prometheus
atlas_zfs_dataset_icloud_photos: icloud_photos
atlas_zfs_dataset_icloud_photos: media/icloud_photos
atlas_zfs_dataset_service_backups: backups/services
atlas_zfs_service_backups_refreservation: 500G
atlas_mount_root: /CHANGEME_ATLAS_MOUNT_ROOT
atlas_archive_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_archive }}"
atlas_music_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_music }}"
atlas_work_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_work }}"
atlas_syncthing_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_syncthing }}"
atlas_backup_prometheus_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_backup_prometheus }}"
atlas_icloud_photos_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_icloud_photos }}"
atlas_service_backups_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_service_backups }}"
atlas_syncthing_config_dir: "{{ atlas_admin_home }}/.local/state/syncthing"
atlas_syncthing_default_dir: "{{ atlas_syncthing_mountpoint }}/Sync"
@@ -34,18 +50,59 @@ atlas_45drives_packages:
- cockpit-zfs
- cockpit-scheduler
atlas_nfs_exports: []
atlas_samba_share_name: work
atlas_nfs_exports:
- path: "{{ atlas_icloud_photos_mountpoint }}"
client: "{{ atlas_aegis_ip }}"
options:
- rw
- sync
- no_subtree_check
- all_squash
- "anonuid={{ atlas_immich_uid }}"
- "anongid={{ atlas_immich_gid }}"
atlas_samba_share_name: Archive
atlas_samba_workgroup: WORKGROUP
atlas_samba_valid_users: []
atlas_samba_password: ""
atlas_samba_password_marker: /var/lib/samba/private/.atlas-password.sha256
atlas_samba_accounts: []
atlas_samba_password_marker_dir: /var/lib/samba/private/ansible-passwords
atlas_firewalld_rich_rules: []
atlas_firewalld_restricted_services:
- ssh
- cockpit
- nfs
- samba
- http
- https
atlas_selinux_booleans:
- samba_export_all_rw
- nfs_export_all_rw
atlas_manage_media_stack: false
atlas_container_quadlet_dir: /etc/containers/systemd
atlas_container_data_root: /var/lib/atlas-containers
atlas_npm_data_dir: "{{ atlas_container_data_root }}/npm/data"
atlas_npm_letsencrypt_dir: "{{ atlas_container_data_root }}/npm/letsencrypt"
atlas_npm_admin_bind_address: 127.0.0.1
atlas_timezone: Europe/Rome
atlas_immich_upload_dir: "{{ atlas_container_data_root }}/immich/upload"
atlas_immich_model_cache_dir: "{{ atlas_container_data_root }}/immich/model-cache"
atlas_immich_machine_learning_config_dir: "{{ atlas_container_data_root }}/immich/machine-learning-config"
atlas_immich_machine_learning_cache_dir: "{{ atlas_container_data_root }}/immich/machine-learning-cache"
atlas_immich_redis_data_dir: "{{ atlas_container_data_root }}/immich/redis"
atlas_immich_postgres_data_dir: "{{ atlas_container_data_root }}/immich/postgres"
atlas_npm_image: docker.io/jc21/nginx-proxy-manager:latest
atlas_immich_version: release
atlas_immich_server_image: "ghcr.io/immich-app/immich-server:{{ atlas_immich_version }}"
atlas_immich_machine_learning_image: "ghcr.io/immich-app/immich-machine-learning:{{ atlas_immich_version }}-openvino"
atlas_immich_redis_image: docker.io/valkey/valkey:9
atlas_immich_postgres_image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0
atlas_immich_db_username: postgres
atlas_immich_db_name: immich
atlas_immich_db_password: "{{ vault_atlas_immich_db_password | default('') }}"
atlas_media_quadlet_services:
- atlas-immich-redis.service
- atlas-immich-postgres.service
- atlas-immich-machine-learning.service
- atlas-immich-server.service
- atlas-npm.service

View File

@@ -32,3 +32,15 @@
ansible.builtin.systemd:
name: atlas-syncthing
state: restarted
- name: Restart Atlas media Quadlets
ansible.builtin.systemd:
name: "{{ item }}"
state: restarted
daemon_reload: true
loop: "{{ atlas_media_quadlet_services }}"
loop_control:
label: "{{ item }}"
when:
- atlas_manage_media_stack | bool
- not ansible_check_mode

View File

@@ -6,6 +6,7 @@
- atlas_admin_username != 'CHANGEME_ATLAS_ADMIN'
- (atlas_admin_ssh_keys | default([])) | length > 0
- atlas_admin_password_hash not in ['', '!', '*']
- "'wheel' not in atlas_immich_supplementary_groups"
fail_msg: >-
Define atlas_admin_username, vault_atlas_authorized_ssh_keys and
vault_atlas_admin_password_hash before applying the Atlas profile.
@@ -31,6 +32,41 @@
state: present
no_log: true
- name: Create Immich primary group
tags: [atlas, accounts, containers, immich]
ansible.builtin.group:
name: "{{ atlas_immich_group }}"
gid: "{{ atlas_immich_gid }}"
state: present
- name: Ensure Immich hardware-access groups exist
tags: [atlas, accounts, containers, immich]
ansible.builtin.group:
name: "{{ item }}"
state: present
loop: "{{ atlas_immich_supplementary_groups }}"
loop_control:
label: "{{ item }}"
- name: Create unprivileged Immich account
tags: [atlas, accounts, containers, immich]
ansible.builtin.user:
name: "{{ atlas_immich_username }}"
uid: "{{ atlas_immich_uid }}"
group: "{{ atlas_immich_group }}"
groups: "{{ atlas_immich_supplementary_groups }}"
append: false
home: /nonexistent
create_home: false
shell: /sbin/nologin
system: true
state: present
- name: Read Immich hardware-access group IDs
tags: [atlas, accounts, containers, immich]
ansible.builtin.getent:
database: group
- name: Grant Atlas administrator passwordless sudo
tags: [atlas, services]
ansible.builtin.copy:

View File

@@ -0,0 +1,140 @@
---
- name: Require completed Atlas media-stack configuration
tags: [atlas, containers, immich]
ansible.builtin.assert:
that:
- atlas_manage_storage | bool
- atlas_zfs_pool != 'CHANGEME_ZFS_POOL'
- atlas_mount_root != '/CHANGEME_ATLAS_MOUNT_ROOT'
- atlas_immich_db_password | length > 0
- atlas_npm_admin_bind_address == '127.0.0.1'
fail_msg: >-
Enable and configure Atlas storage, provide vault_atlas_immich_db_password,
and keep the NPM administration port bound to loopback before enabling the media stack.
no_log: true
when: atlas_manage_media_stack | bool
- name: Check Atlas Intel graphics device
tags: [atlas, containers, immich]
ansible.builtin.stat:
path: /dev/dri
register: atlas_dri_device
when: atlas_manage_media_stack | bool
- name: Require Atlas Intel graphics device
tags: [atlas, containers, immich]
ansible.builtin.assert:
that:
- atlas_dri_device.stat.isdir | default(false)
fail_msg: /dev/dri is required for Immich QuickSync and OpenVINO acceleration.
when: atlas_manage_media_stack | bool
- name: Create Atlas rootful Quadlet directory
tags: [atlas, containers]
ansible.builtin.file:
path: "{{ atlas_container_quadlet_dir }}"
state: directory
owner: root
group: root
mode: "0755"
when: atlas_manage_media_stack | bool
- name: Create Atlas Immich configuration directory
tags: [atlas, containers, immich]
ansible.builtin.file:
path: /etc/immich
state: directory
owner: root
group: "{{ atlas_immich_group }}"
mode: "0750"
when: atlas_manage_media_stack | bool
- name: Create Atlas Immich persistent directories
tags: [atlas, containers, immich]
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: "{{ atlas_immich_username }}"
group: "{{ atlas_immich_group }}"
mode: "0750"
loop:
- "{{ atlas_immich_upload_dir }}"
- "{{ atlas_immich_model_cache_dir }}"
- "{{ atlas_immich_machine_learning_config_dir }}"
- "{{ atlas_immich_machine_learning_cache_dir }}"
- "{{ atlas_immich_redis_data_dir }}"
- "{{ atlas_immich_postgres_data_dir }}"
loop_control:
label: "{{ item }}"
when: atlas_manage_media_stack | bool
- name: Create Atlas NPM persistent directories
tags: [atlas, containers, npm]
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: root
group: root
mode: "0750"
loop:
- "{{ atlas_npm_data_dir }}"
- "{{ atlas_npm_letsencrypt_dir }}"
loop_control:
label: "{{ item }}"
when: atlas_manage_media_stack | bool
- name: Allow confined containers to use Atlas graphics devices
tags: [atlas, containers, immich]
ansible.posix.seboolean:
name: container_use_devices
state: true
persistent: true
when:
- atlas_manage_media_stack | bool
- (ansible_facts['selinux'] | default({})).get('status', 'disabled') == 'enabled'
- name: Render Vault-backed Immich environment
tags: [atlas, containers, immich]
ansible.builtin.template:
src: immich.env.j2
dest: /etc/immich/immich.env
owner: root
group: "{{ atlas_immich_group }}"
mode: "0640"
no_log: true
diff: false
notify: Restart Atlas media Quadlets
when: atlas_manage_media_stack | bool
- name: Render Atlas media Quadlets
tags: [atlas, containers]
ansible.builtin.template:
src: "{{ item }}.j2"
dest: "{{ atlas_container_quadlet_dir }}/{{ item }}"
owner: root
group: root
mode: "0644"
loop:
- atlas-media.network
- atlas-immich-redis.container
- atlas-immich-postgres.container
- atlas-immich-machine-learning.container
- atlas-immich-server.container
- atlas-npm.container
loop_control:
label: "{{ item }}"
notify: Restart Atlas media Quadlets
when: atlas_manage_media_stack | bool
- name: Start Atlas media Quadlet services
tags: [atlas, containers, services]
ansible.builtin.systemd:
name: "{{ item }}"
state: started
daemon_reload: true
loop: "{{ atlas_media_quadlet_services }}"
loop_control:
label: "{{ item }}"
when:
- atlas_manage_media_stack | bool
- not ansible_check_mode

View File

@@ -11,5 +11,8 @@
- name: Import Atlas file sharing tasks
ansible.builtin.import_tasks: sharing.yml
- name: Import Atlas media-container tasks
ansible.builtin.import_tasks: containers.yml
- name: Import Atlas Syncthing tasks
ansible.builtin.import_tasks: syncthing.yml

View File

@@ -0,0 +1,64 @@
---
- name: Require an existing Unix account for Atlas Samba
ansible.builtin.getent:
database: passwd
key: "{{ atlas_samba_account.username }}"
- name: Read Atlas Samba account
ansible.builtin.command:
argv:
- pdbedit
- --list
- --user
- "{{ atlas_samba_account.username }}"
register: atlas_samba_account_check
changed_when: false
failed_when: false
- name: Check Atlas Samba password marker
ansible.builtin.stat:
path: "{{ atlas_samba_password_marker_dir }}/{{ atlas_samba_account.username }}.sha256"
register: atlas_samba_password_marker_stat
- name: Read Atlas Samba password marker
ansible.builtin.slurp:
src: "{{ atlas_samba_password_marker_dir }}/{{ atlas_samba_account.username }}.sha256"
register: atlas_samba_password_marker_content
no_log: true
when: atlas_samba_password_marker_stat.stat.exists
- name: Determine whether Atlas Samba credentials must change
ansible.builtin.set_fact:
atlas_samba_password_digest: "{{ atlas_samba_account.password | hash('sha256') }}"
atlas_samba_password_update_required: >-
{{
atlas_samba_account_check.rc != 0
or not atlas_samba_password_marker_stat.stat.exists
or (
atlas_samba_password_marker_content.content | default('')
| b64decode | trim
) != (atlas_samba_account.password | hash('sha256'))
}}
no_log: true
- name: Set Atlas Samba account password
ansible.builtin.command:
argv:
- smbpasswd
- -s
- -a
- "{{ atlas_samba_account.username }}"
stdin: "{{ atlas_samba_account.password }}\n{{ atlas_samba_account.password }}"
changed_when: true
no_log: true
when: atlas_samba_password_update_required | bool
- name: Record managed Atlas Samba password digest
ansible.builtin.copy:
content: "{{ atlas_samba_password_digest }}\n"
dest: "{{ atlas_samba_password_marker_dir }}/{{ atlas_samba_account.username }}.sha256"
owner: root
group: root
mode: "0600"
no_log: true
when: atlas_samba_password_update_required | bool

View File

@@ -10,6 +10,19 @@
notify: Reload NFS exports
when: atlas_manage_storage | bool
- name: Enable Atlas file-sharing services
tags: [atlas, sharing, services]
ansible.builtin.systemd:
name: "{{ item }}"
enabled: true
state: started
loop:
- nfs-server.service
- smb.service
loop_control:
label: "{{ item }}"
when: atlas_manage_storage | bool
- name: Configure Atlas NFSv4-only service
tags: [atlas, sharing]
ansible.builtin.template:
@@ -82,106 +95,50 @@
notify: Restart Samba service
when: atlas_manage_storage | bool
- name: Require Atlas Samba password
- name: Require Vault-backed Atlas Samba accounts
tags: [atlas, sharing]
ansible.builtin.assert:
that:
- atlas_samba_password | length > 0
fail_msg: Define vault_atlas_samba_password before enabling Atlas storage.
- atlas_samba_accounts | length > 0
- >-
atlas_samba_accounts | map(attribute='username') | list
| difference(atlas_samba_valid_users) | length == 0
- >-
atlas_samba_valid_users
| difference(atlas_samba_accounts | map(attribute='username') | list) | length == 0
- atlas_samba_accounts | selectattr('password', 'equalto', '') | list | length == 0
fail_msg: Define every authorized Samba account and its Vault-backed password.
no_log: true
when: atlas_manage_storage | bool
- name: Read Atlas Samba account
tags: [atlas, sharing]
ansible.builtin.command:
argv:
- pdbedit
- --list
- --user
- "{{ atlas_admin_username }}"
register: atlas_samba_account
changed_when: false
failed_when: false
when: atlas_manage_storage | bool
- name: Ensure Atlas Samba private state directory exists
tags: [atlas, sharing]
ansible.builtin.file:
path: "{{ atlas_samba_password_marker | dirname }}"
path: "{{ atlas_samba_password_marker_dir }}"
state: directory
owner: root
group: root
mode: "0700"
when: atlas_manage_storage | bool
- name: Check Atlas Samba password marker
- name: Manage Vault-backed Atlas Samba credentials
tags: [atlas, sharing]
ansible.builtin.stat:
path: "{{ atlas_samba_password_marker }}"
register: atlas_samba_password_marker_stat
when: atlas_manage_storage | bool
- name: Read Atlas Samba password marker
tags: [atlas, sharing]
ansible.builtin.slurp:
src: "{{ atlas_samba_password_marker }}"
register: atlas_samba_password_marker_content
no_log: true
when:
- atlas_manage_storage | bool
- atlas_samba_password_marker_stat.stat.exists
- name: Determine whether Atlas Samba credentials must change
tags: [atlas, sharing]
ansible.builtin.set_fact:
atlas_samba_password_digest: "{{ atlas_samba_password | hash('sha256') }}"
atlas_samba_password_update_required: >-
{{
atlas_samba_account.rc != 0
or not atlas_samba_password_marker_stat.stat.exists
or (
atlas_samba_password_marker_content.content | default('')
| b64decode | trim
) != (atlas_samba_password | hash('sha256'))
}}
ansible.builtin.include_tasks: samba_account.yml
loop: "{{ atlas_samba_accounts }}"
loop_control:
loop_var: atlas_samba_account
label: "{{ atlas_samba_account.username }}"
no_log: true
when: atlas_manage_storage | bool
- name: Set Atlas Samba account password
tags: [atlas, sharing]
ansible.builtin.command:
argv:
- smbpasswd
- -s
- -a
- "{{ atlas_admin_username }}"
stdin: "{{ atlas_samba_password }}\n{{ atlas_samba_password }}"
changed_when: true
no_log: true
when:
- atlas_manage_storage | bool
- atlas_samba_password_update_required | bool
- name: Record managed Atlas Samba password digest
tags: [atlas, sharing]
ansible.builtin.copy:
content: "{{ atlas_samba_password_digest }}\n"
dest: "{{ atlas_samba_password_marker }}"
owner: root
group: root
mode: "0600"
no_log: true
when:
- atlas_manage_storage | bool
- atlas_samba_password_update_required | bool
- name: Require completed Atlas firewall placeholders
tags: [atlas, sharing, services]
ansible.builtin.assert:
that:
- atlas_lan_subnet != 'CHANGEME_LAN_SUBNET'
- atlas_aegis_ip != 'CHANGEME_AEGIS_IP'
- atlas_firewalld_zone | length > 0
fail_msg: Replace the Atlas LAN subnet and firewall zone placeholders.
fail_msg: Replace the Atlas LAN, Aegis and firewall-zone placeholders.
when: atlas_manage_firewall | bool
- name: Apply Atlas firewalld rich rules

View File

@@ -5,12 +5,52 @@
atlas_zfs_datasets:
- name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_work }}"
mountpoint: "{{ atlas_work_mountpoint }}"
owner: "{{ atlas_admin_username }}"
group: "{{ atlas_admin_group }}"
properties:
mountpoint: "{{ atlas_work_mountpoint }}"
- name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_archive }}"
mountpoint: "{{ atlas_archive_mountpoint }}"
owner: "{{ atlas_admin_username }}"
group: "{{ atlas_admin_group }}"
properties:
compression: zstd
mountpoint: "{{ atlas_archive_mountpoint }}"
- name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_music }}"
mountpoint: "{{ atlas_music_mountpoint }}"
owner: "{{ atlas_admin_username }}"
group: "{{ atlas_admin_group }}"
properties:
compression: lz4
mountpoint: "{{ atlas_music_mountpoint }}"
- name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_syncthing }}"
mountpoint: "{{ atlas_syncthing_mountpoint }}"
owner: "{{ atlas_admin_username }}"
group: "{{ atlas_admin_group }}"
properties:
compression: lz4
mountpoint: "{{ atlas_syncthing_mountpoint }}"
- name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_backup_prometheus }}"
mountpoint: "{{ atlas_backup_prometheus_mountpoint }}"
owner: "{{ atlas_admin_username }}"
group: "{{ atlas_admin_group }}"
properties:
mountpoint: "{{ atlas_backup_prometheus_mountpoint }}"
- name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_icloud_photos }}"
mountpoint: "{{ atlas_icloud_photos_mountpoint }}"
owner: "{{ atlas_immich_username }}"
group: "{{ atlas_immich_group }}"
properties:
compression: lz4
mountpoint: "{{ atlas_icloud_photos_mountpoint }}"
- name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_service_backups }}"
mountpoint: "{{ atlas_service_backups_mountpoint }}"
owner: "{{ atlas_admin_username }}"
group: "{{ atlas_admin_group }}"
properties:
compression: lz4
mountpoint: "{{ atlas_service_backups_mountpoint }}"
refreservation: "{{ atlas_zfs_service_backups_refreservation }}"
- name: Require completed Atlas storage placeholders
tags: [atlas, storage]
@@ -56,8 +96,7 @@
community.general.zfs:
name: "{{ item.name }}"
state: present
extra_zfs_properties:
mountpoint: "{{ item.mountpoint }}"
extra_zfs_properties: "{{ item.properties }}"
loop: "{{ atlas_zfs_datasets }}"
loop_control:
label: "{{ item.name }}"
@@ -68,8 +107,8 @@
ansible.builtin.file:
path: "{{ item.mountpoint }}"
state: directory
owner: "{{ atlas_admin_username }}"
group: "{{ atlas_admin_group }}"
owner: "{{ item.owner }}"
group: "{{ item.group }}"
mode: "0770"
loop: "{{ atlas_zfs_datasets }}"
loop_control:

View File

@@ -0,0 +1,27 @@
# Managed by Ansible. Do not edit manually.
[Unit]
Description=Atlas Immich machine learning
[Container]
ContainerName=atlas-immich-machine-learning
Image={{ atlas_immich_machine_learning_image }}
User={{ atlas_immich_uid }}:{{ atlas_immich_gid }}
{% for group_name in atlas_immich_supplementary_groups %}
GroupAdd={{ ansible_facts['getent_group'][group_name][1] }}
{% endfor %}
AddDevice=/dev/dri
Network=atlas-media.network
NetworkAlias=atlas-immich-machine-learning
Environment=TZ={{ atlas_timezone }}
Volume={{ atlas_immich_model_cache_dir }}:/cache:Z
Volume={{ atlas_immich_machine_learning_config_dir }}:/.config:Z
Volume={{ atlas_immich_machine_learning_cache_dir }}:/.cache:Z
NoNewPrivileges=true
DropCapability=NET_RAW
[Service]
Restart=always
TimeoutStartSec=900
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,22 @@
# Managed by Ansible. Do not edit manually.
[Unit]
Description=Atlas Immich PostgreSQL database
[Container]
ContainerName=atlas-immich-postgres
Image={{ atlas_immich_postgres_image }}
User={{ atlas_immich_uid }}:{{ atlas_immich_gid }}
Network=atlas-media.network
NetworkAlias=atlas-immich-postgres
EnvironmentFile=/etc/immich/immich.env
Volume={{ atlas_immich_postgres_data_dir }}:/var/lib/postgresql/data:Z
ShmSize=128m
NoNewPrivileges=true
DropCapability=NET_RAW
[Service]
Restart=always
TimeoutStartSec=900
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,20 @@
# Managed by Ansible. Do not edit manually.
[Unit]
Description=Atlas Immich Redis-compatible cache
[Container]
ContainerName=atlas-immich-redis
Image={{ atlas_immich_redis_image }}
User={{ atlas_immich_uid }}:{{ atlas_immich_gid }}
Network=atlas-media.network
NetworkAlias=atlas-immich-redis
Volume={{ atlas_immich_redis_data_dir }}:/data:Z
NoNewPrivileges=true
DropCapability=NET_RAW
[Service]
Restart=always
TimeoutStartSec=900
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,29 @@
# Managed by Ansible. Do not edit manually.
[Unit]
Description=Atlas Immich server
Requires=atlas-immich-redis.service atlas-immich-postgres.service atlas-immich-machine-learning.service
After=atlas-immich-redis.service atlas-immich-postgres.service atlas-immich-machine-learning.service
[Container]
ContainerName=atlas-immich-server
Image={{ atlas_immich_server_image }}
User={{ atlas_immich_uid }}:{{ atlas_immich_gid }}
{% for group_name in atlas_immich_supplementary_groups %}
GroupAdd={{ ansible_facts['getent_group'][group_name][1] }}
{% endfor %}
AddDevice=/dev/dri
Network=atlas-media.network
NetworkAlias=atlas-immich-server
EnvironmentFile=/etc/immich/immich.env
Volume={{ atlas_immich_upload_dir }}:/data:Z
Volume={{ atlas_icloud_photos_mountpoint }}:/external/icloud_photos:ro,Z
Volume=/etc/localtime:/etc/localtime:ro
NoNewPrivileges=true
DropCapability=NET_RAW
[Service]
Restart=always
TimeoutStartSec=900
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,3 @@
# Managed by Ansible. Do not edit manually.
[Network]
NetworkName=atlas-media

View File

@@ -0,0 +1,21 @@
# Managed by Ansible. Do not edit manually.
[Unit]
Description=Atlas Nginx Proxy Manager
[Container]
ContainerName=atlas-npm
Image={{ atlas_npm_image }}
Network=atlas-media.network
NetworkAlias=atlas-npm
PublishPort=80:80
PublishPort=443:443
PublishPort={{ atlas_npm_admin_bind_address }}:81:81
Volume={{ atlas_npm_data_dir }}:/data:Z
Volume={{ atlas_npm_letsencrypt_dir }}:/etc/letsencrypt:Z
[Service]
Restart=always
TimeoutStartSec=900
[Install]
WantedBy=multi-user.target

View File

@@ -1,4 +1,4 @@
# Managed by Ansible. Do not edit manually.
{% for export in atlas_nfs_exports %}
{{ export.path }} {{ export.clients }}
{{ export.path }} {{ export.client }}({{ export.options | join(',') }})
{% endfor %}

View File

@@ -0,0 +1,13 @@
# Managed by Ansible. Contains a Vault secret; do not edit or copy.
TZ={{ atlas_timezone | to_json }}
DB_HOSTNAME="atlas-immich-postgres"
DB_USERNAME={{ atlas_immich_db_username | to_json }}
DB_PASSWORD={{ atlas_immich_db_password | to_json }}
DB_DATABASE_NAME={{ atlas_immich_db_name | to_json }}
DB_VECTOR_EXTENSION="vectorchord"
POSTGRES_USER={{ atlas_immich_db_username | to_json }}
POSTGRES_PASSWORD={{ atlas_immich_db_password | to_json }}
POSTGRES_DB={{ atlas_immich_db_name | to_json }}
POSTGRES_INITDB_ARGS="--data-checksums"
REDIS_HOSTNAME="atlas-immich-redis"
IMMICH_MACHINE_LEARNING_URL="http://atlas-immich-machine-learning:3003"

View File

@@ -3,11 +3,12 @@
workgroup = {{ atlas_samba_workgroup }}
security = user
map to guest = Never
server min protocol = SMB2
server min protocol = SMB3
hosts allow = {{ atlas_lan_subnet }}
include = registry
[{{ atlas_samba_share_name }}]
path = {{ atlas_work_mountpoint }}
path = {{ atlas_archive_mountpoint }}
browseable = yes
read only = no
valid users = {{ atlas_samba_valid_users | join(' ') }}

View File

@@ -9,3 +9,29 @@
tags: [services]
ansible.builtin.systemd:
daemon_reload: true
- name: Restart rclone music mount
tags: [services, rclone, navidrome]
ansible.builtin.systemd:
name: "{{ server_rclone_music_service }}"
state: restarted
daemon_reload: true
when:
- server_atlas_music_enabled | bool
- not ansible_check_mode
- name: Restart rootless Navidrome
tags: [services, podman, rclone, navidrome]
become_user: "{{ server_username }}"
ansible.builtin.systemd:
name: navidrome.service
scope: user
state: restarted
daemon_reload: true
environment:
XDG_RUNTIME_DIR: "/run/user/{{ ansible_facts['getent_passwd'][server_username][1] }}"
DBUS_SESSION_BUS_ADDRESS: >-
unix:path=/run/user/{{ ansible_facts['getent_passwd'][server_username][1] }}/bus
when:
- server_atlas_music_enabled | bool
- not ansible_check_mode

View File

@@ -8,15 +8,16 @@
fail_msg: >-
server_firewall_backend must be firewalld for the Rocky server profile.
- name: Require server container secret variables
tags: [dotfiles, dotfiles:server, services]
- name: Require server database secret variables
tags: [dotfiles, dotfiles:server, services, navidrome]
ansible.builtin.assert:
that:
- (vault_navidrome_db_password | default('')) | length > 0
- server_navidrome_db_password | length > 0
- (vault_postgres_root_password | default('')) | length > 0
fail_msg: >-
Server container secrets are missing. Define vault_navidrome_db_password and
vault_postgres_root_password in secrets/vault.yml or another vars source.
Define vault_navidrome_db_password and vault_postgres_root_password in Vault
before rendering the Navidrome database configuration.
no_log: true
- name: Configure DuckDNS updater
tags: [dotfiles, dotfiles:server, duckdns]
@@ -71,6 +72,12 @@
tags: [services, podman]
ansible.builtin.include_tasks: podman-compose.yml
- name: Configure Atlas music mount
ansible.builtin.import_tasks: rclone-music.yml
- name: Configure rootless Navidrome
ansible.builtin.import_tasks: navidrome.yml
- name: Disable SSH root login on server
tags: [services]
ansible.builtin.lineinfile:

View File

@@ -0,0 +1,114 @@
---
- name: Read server account details for rootless Navidrome
tags: [services, podman, rclone, navidrome]
ansible.builtin.getent:
database: passwd
key: "{{ server_username }}"
when: server_atlas_music_enabled | bool
- name: Check for a running legacy rootful Navidrome container
tags: [services, podman, navidrome]
ansible.builtin.command:
argv:
- podman
- container
- inspect
- --format
- "{{ '{{.State.Running}}' }}"
- navidrome
register: server_legacy_navidrome_container
changed_when: false
failed_when: false
check_mode: false
when: server_atlas_music_enabled | bool
- name: Refuse concurrent legacy and rootless Navidrome
tags: [services, podman, navidrome]
ansible.builtin.assert:
that:
- >-
server_legacy_navidrome_container.rc != 0
or server_legacy_navidrome_container.stdout | trim != 'true'
fail_msg: >-
Stop the legacy rootful Navidrome container before enabling the rootless Quadlet.
The playbook does not remove the old container or its data automatically.
when: server_atlas_music_enabled | bool
- name: Enable lingering for rootless Navidrome
tags: [services, podman, navidrome]
ansible.builtin.command:
argv:
- loginctl
- enable-linger
- "{{ server_username }}"
creates: "/var/lib/systemd/linger/{{ server_username }}"
when: server_atlas_music_enabled | bool
- name: Start server user systemd manager
tags: [services, podman, navidrome]
ansible.builtin.systemd:
name: "user@{{ ansible_facts['getent_passwd'][server_username][1] }}.service"
state: started
when:
- server_atlas_music_enabled | bool
- not ansible_check_mode
- name: Create rootless Navidrome Quadlet directory
tags: [services, podman, navidrome]
ansible.builtin.file:
path: "{{ server_navidrome_quadlet_dir }}"
state: directory
owner: "{{ server_username }}"
group: "{{ server_user_group }}"
mode: "0700"
when: server_atlas_music_enabled | bool
- name: Create private rootless Navidrome configuration directory
tags: [services, podman, navidrome]
ansible.builtin.file:
path: "{{ server_navidrome_env_dir }}"
state: directory
owner: "{{ server_username }}"
group: "{{ server_user_group }}"
mode: "0700"
when: server_atlas_music_enabled | bool
- name: Render Vault-backed rootless Navidrome environment
tags: [services, podman, navidrome]
ansible.builtin.template:
src: navidrome.env.j2
dest: "{{ server_navidrome_env_file }}"
owner: "{{ server_username }}"
group: "{{ server_user_group }}"
mode: "0600"
no_log: true
diff: false
notify: Restart rootless Navidrome
when: server_atlas_music_enabled | bool
- name: Render rootless Navidrome Quadlet
tags: [services, podman, navidrome]
ansible.builtin.template:
src: navidrome.container.j2
dest: "{{ server_navidrome_quadlet_dir }}/navidrome.container"
owner: "{{ server_username }}"
group: "{{ server_user_group }}"
mode: "0644"
notify: Restart rootless Navidrome
when: server_atlas_music_enabled | bool
- name: Start rootless Navidrome Quadlet
tags: [services, podman, navidrome]
become_user: "{{ server_username }}"
ansible.builtin.systemd:
name: navidrome.service
scope: user
state: started
daemon_reload: true
environment:
XDG_RUNTIME_DIR: "/run/user/{{ ansible_facts['getent_passwd'][server_username][1] }}"
DBUS_SESSION_BUS_ADDRESS: >-
unix:path=/run/user/{{ ansible_facts['getent_passwd'][server_username][1] }}/bus
when:
- server_atlas_music_enabled | bool
- not ansible_check_mode

View File

@@ -0,0 +1,105 @@
---
- name: Require completed Atlas SFTP mount configuration
tags: [services, rclone, navidrome]
ansible.builtin.assert:
that:
- server_atlas_sftp_host != 'CHANGEME_ATLAS_WIREGUARD_IP'
- server_atlas_sftp_known_host != 'CHANGEME_ATLAS_SSH_HOST_KEY'
- server_atlas_sftp_known_host | length > 0
- server_atlas_sftp_private_key | length > 0
- server_atlas_sftp_remote_path == '/pool/media/music'
fail_msg: >-
Define the Atlas WireGuard address, pinned SSH host key and Vault-backed
SFTP private key before enabling the music mount.
no_log: true
when: server_atlas_music_enabled | bool
- name: Create private rclone configuration directory
tags: [services, rclone, navidrome]
ansible.builtin.file:
path: "{{ server_rclone_config_dir }}"
state: directory
owner: root
group: "{{ server_user_group }}"
mode: "0750"
when: server_atlas_music_enabled | bool
- name: Install Vault-backed Atlas SFTP private key
tags: [services, rclone, navidrome]
ansible.builtin.copy:
content: "{{ server_atlas_sftp_private_key | trim }}\n"
dest: "{{ server_atlas_sftp_private_key_file }}"
owner: "{{ server_username }}"
group: "{{ server_user_group }}"
mode: "0600"
no_log: true
diff: false
notify:
- Restart rclone music mount
- Restart rootless Navidrome
when: server_atlas_music_enabled | bool
- name: Install pinned Atlas SSH host key
tags: [services, rclone, navidrome]
ansible.builtin.copy:
content: "{{ server_atlas_sftp_known_host | trim }}\n"
dest: "{{ server_atlas_sftp_known_hosts_file }}"
owner: "{{ server_username }}"
group: "{{ server_user_group }}"
mode: "0640"
notify:
- Restart rclone music mount
- Restart rootless Navidrome
when: server_atlas_music_enabled | bool
- name: Render Atlas SFTP rclone configuration
tags: [services, rclone, navidrome]
ansible.builtin.template:
src: rclone.conf.j2
dest: "{{ server_rclone_config_file }}"
owner: "{{ server_username }}"
group: "{{ server_user_group }}"
mode: "0600"
notify:
- Restart rclone music mount
- Restart rootless Navidrome
when: server_atlas_music_enabled | bool
- name: Create rclone music directories
tags: [services, rclone, navidrome]
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: "{{ server_username }}"
group: "{{ server_user_group }}"
mode: "0750"
loop:
- "{{ server_rclone_music_mountpoint }}"
- "{{ server_rclone_music_cache_dir }}"
loop_control:
label: "{{ item }}"
when: server_atlas_music_enabled | bool
- name: Render rclone music system service
tags: [services, rclone, navidrome]
ansible.builtin.template:
src: rclone-music.service.j2
dest: "/etc/systemd/system/{{ server_rclone_music_service }}"
owner: root
group: root
mode: "0644"
notify:
- Restart rclone music mount
- Restart rootless Navidrome
when: server_atlas_music_enabled | bool
- name: Enable and start rclone music mount
tags: [services, rclone, navidrome]
ansible.builtin.systemd:
name: "{{ server_rclone_music_service }}"
enabled: true
state: started
daemon_reload: true
when:
- server_atlas_music_enabled | bool
- not ansible_check_mode

View File

@@ -0,0 +1,21 @@
# Managed by Ansible. Do not edit manually.
[Unit]
Description=Rootless Navidrome music server
[Container]
ContainerName=navidrome
Image={{ server_navidrome_image }}
Network=host
EnvironmentFile={{ server_navidrome_env_file }}
Volume={{ server_navidrome_data_dir }}:/data
Volume={{ server_rclone_music_mountpoint }}:/music:ro
SecurityLabelDisable=true
[Service]
ExecStartPre=/usr/bin/mountpoint -q {{ server_rclone_music_mountpoint }}
Restart=always
RestartSec=10s
TimeoutStartSec=900
[Install]
WantedBy=default.target

View File

@@ -0,0 +1,7 @@
# Managed by Ansible. Contains a Vault secret; do not edit or copy.
ND_DATABASE_URL={{ ('postgres://navidrome:' ~ server_navidrome_db_password ~ '@127.0.0.1:5432/navidrome_db?sslmode=disable') | to_json }}
ND_PORT={{ server_navidrome_port }}
ND_SCANSCHEDULE="1h"
ND_SESSIONTIMEOUT="24h"
ND_ENABLETRANSCODING="true"
ND_LOGLEVEL="info"

View File

@@ -0,0 +1,29 @@
[Unit]
Description=Read-only Atlas music mount via rclone SFTP
Wants=network-online.target
After=network-online.target
{% if server_atlas_wireguard_unit | length > 0 %}
Wants={{ server_atlas_wireguard_unit }}
After={{ server_atlas_wireguard_unit }}
{% endif %}
[Service]
Type=notify
User={{ server_username }}
Group={{ server_user_group }}
ExecStart=/usr/bin/rclone mount \
{{ server_atlas_sftp_remote_name }}:{{ server_atlas_sftp_remote_path }} \
{{ server_rclone_music_mountpoint }} \
--config {{ server_rclone_config_file }} \
--cache-dir {{ server_rclone_music_cache_dir }} \
--read-only \
--vfs-cache-mode full \
--vfs-cache-max-size 15G \
--vfs-read-chunk-size 5M \
--dir-cache-time 24h
ExecStop=-/usr/bin/fusermount3 -uz {{ server_rclone_music_mountpoint }}
Restart=on-failure
RestartSec=10s
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,9 @@
# Managed by Ansible. Authentication uses the separately deployed Vault-backed key.
[{{ server_atlas_sftp_remote_name }}]
type = sftp
host = {{ server_atlas_sftp_host }}
user = {{ server_atlas_sftp_username }}
port = {{ server_atlas_sftp_port }}
key_file = {{ server_atlas_sftp_private_key_file }}
known_hosts_file = {{ server_atlas_sftp_known_hosts_file }}
shell_type = unix

View File

@@ -3,25 +3,6 @@
name: server
services:
navidrome:
image: docker.io/deluan/navidrome:latest
container_name: navidrome
restart: unless-stopped
expose:
- "4533"
environment:
ND_DATABASE_URL: "postgres://navidrome:{{ vault_navidrome_db_password }}@navidromedb:5432/navidrome_db?sslmode=disable"
ND_SESSIONTIMEOUT: 24h
ND_ENABLETRANSCODING: "true"
volumes:
- "/opt/navidrome/data:/data{{ ':' ~ selinux_volume_option if selinux_volume_option else '' }}"
- "/opt/music:/music:ro{{ ',' ~ selinux_volume_option if selinux_volume_option else '' }}"
user: "1000:1000"
networks:
- web
depends_on:
- navidromedb
nginx-proxy-manager:
image: docker.io/jc21/nginx-proxy-manager:latest
container_name: nginx-proxy-manager
@@ -30,6 +11,8 @@ services:
- "80:80"
- "443:443"
- "127.0.0.1:81:81"
extra_hosts:
- "host.containers.internal:host-gateway"
volumes:
- "/opt/npm/data:/data{{ ':' ~ selinux_volume_option if selinux_volume_option else '' }}"
- "/opt/npm/letsencrypt:/etc/letsencrypt{{ ':' ~ selinux_volume_option if selinux_volume_option else '' }}"
@@ -64,11 +47,13 @@ services:
POSTGRES_DB: "navidrome_db"
POSTGRES_USER: "navidrome"
POSTGRES_PASSWORD: "{{ vault_postgres_root_password }}"
volumes:
- "/opt/postgres/data:/var/lib/postgresql/data{{ ':' ~ selinux_volume_option if selinux_volume_option else '' }}"
ports:
- "127.0.0.1:5432:5432"
networks:
- web
gitea:
image: docker.gitea.com/gitea:1.25.2
container_name: gitea