Files
homelab-configs/arrs-4k/deploy-arrs-4k.yml
T
tommy 5ba7246b45 4K: pre-create download-client completed category dirs in playbook
radarr-4k/sonarr-4k surfaced 'download client places downloads in X but it
does not appear to exist' health errors: the SABnzbd (movies-4k/tv-4k) and
Transmission (radarr-uhd/sonarr-uhd) completed-category dirs dont exist until
the first grab lands in each category. On a rebuild those errors recur until
downloads happen. Add an idempotent file: task creating all four at
1000:3000 (abc:media) 0775 to match the /data tree.

Root cause of the original root:0770 drift on movies-4k: a manual
docker exec <client> mkdir runs as ROOT in linuxserver images (umask 007),
not as the app user abc. Live dirs normalized via host chown (NFS honors
no_root_squash); both instances now report 0 download-client-dir errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 10:11:18 -05:00

181 lines
7.7 KiB
YAML

---
- name: Deploy 4K arr instances (radarr-4k, sonarr-4k) on media-server
hosts: media-server
become: true
vars:
app_dir: /home/tommy/arrs-4k
tasks:
- name: Create arrs-4k directories
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: tommy
group: tommy
mode: "0755"
loop:
- "{{ app_dir }}"
- "{{ app_dir }}/radarr-4k"
- "{{ app_dir }}/sonarr-4k"
- name: Write docker-compose.yml
ansible.builtin.copy:
dest: "{{ app_dir }}/docker-compose.yml"
owner: tommy
group: tommy
mode: "0644"
content: |
# 4K Quality-First arr instances — SEPARATE from the untouched 1080p stack.
# Host: media-server (.183). Shares /mnt/media/data -> /data (single hardlink tree).
# Roots: radarr-4k -> /data/media/movies-4k ; sonarr-4k -> /data/media/tv-4k
# Routing: Traefik FILE provider on node01 dynamic_conf.yml (NO docker labels):
# radarr-4k.goattw.net -> 192.168.99.183:7879 ; sonarr-4k.goattw.net -> 192.168.99.183:8990
# middlewares: secure-headers@file, authelia-auth@file, crowdsec-bouncer@file
# Tdarr does NOT watch movies-4k/tv-4k (quality-first keeps the grabbed file).
services:
radarr-4k:
image: lscr.io/linuxserver/radarr:latest
container_name: radarr-4k
mem_limit: 1g
cpus: 1.0
environment:
- PUID=1000
- PGID=1000
- TZ=America/Chicago
networks:
- traefik_public
volumes:
- ./radarr-4k:/config
- /mnt/media/data:/data
ports:
- "7879:7878/tcp"
restart: unless-stopped
sonarr-4k:
image: lscr.io/linuxserver/sonarr:latest
container_name: sonarr-4k
mem_limit: 1g
cpus: 1.0
environment:
- PUID=1000
- PGID=1000
- TZ=America/Chicago
networks:
- traefik_public
volumes:
- ./sonarr-4k:/config
- /mnt/media/data:/data
ports:
- "8990:8989/tcp"
restart: unless-stopped
networks:
traefik_public:
external: true
- name: Deploy 4K arrs with docker compose
community.docker.docker_compose_v2:
project_src: "{{ app_dir }}"
state: present
pull: missing
register: r
- name: Result
ansible.builtin.debug:
msg: "arrs-4k {{ 'running (changed)' if r.changed else 'already up (no-op)' }}"
- name: Wait for 4K arr APIs (ports)
ansible.builtin.wait_for:
host: 192.168.99.183
port: "{{ item }}"
timeout: 90
loop: [7879, 8990]
# Pre-create the download-client COMPLETED category dirs so a fresh rebuild
# doesn't surface "download client places downloads in X but it does not
# appear to exist" health errors until the first grab lands in each category.
# Owner 1000:3000 (abc:media) 0775 matches the rest of the /data tree — the
# download-client app processes create category dirs this way on first use;
# a manual `docker exec <client> mkdir` (runs as ROOT in linuxserver images)
# produces root:0770 instead, which is the drift this task prevents.
# become=root chown is honored by the TrueNAS NFS export (no_root_squash).
- name: Ensure 4K download-client completed category dirs (abc:media 0775)
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: "1000"
group: "3000"
mode: "0775"
loop:
- /mnt/media/data/usenet/complete/movies-4k # SABnzbd -> radarr-4k
- /mnt/media/data/usenet/complete/tv-4k # SABnzbd -> sonarr-4k
- /mnt/media/data/torrents/radarr-uhd # Transmission -> radarr-4k
- /mnt/media/data/torrents/sonarr-uhd # Transmission -> sonarr-4k
- name: Install 4K download-client config script
ansible.builtin.copy:
dest: "{{ app_dir }}/ensure-arrs-4k-dl.py"
owner: tommy
group: tommy
mode: "0755"
content: |
#!/usr/bin/env python3
# Idempotent: ensure the 4K arr download clients + sabnzbd categories exist.
# Reads RADARR4K_KEY / SONARR4K_KEY / SAB_KEY from env. Prints CHANGED or NOCHANGE.
import os, json, urllib.request
H = "http://192.168.99.183"
SAB = os.environ["SAB_KEY"]
changed = False
def sab_cats():
return json.load(urllib.request.urlopen(f"{H}:8081/api?mode=get_cats&output=json&apikey={SAB}"))["categories"]
def sab_add(cat):
urllib.request.urlopen(f"{H}:8081/api?mode=set_config&section=categories&keyword={cat}&dir={cat}&apikey={SAB}").read()
def dc_get(port, key):
return json.load(urllib.request.urlopen(urllib.request.Request(f"{H}:{port}/api/v3/downloadclient", headers={"X-Api-Key": key})))
def dc_post(port, key, body):
urllib.request.urlopen(urllib.request.Request(f"{H}:{port}/api/v3/downloadclient",
data=json.dumps(body).encode(), method="POST", headers={"X-Api-Key": key, "Content-Type": "application/json"}))
def sab_body(catf, catv, recent, older):
return {"enable": True, "protocol": "usenet", "priority": 1, "name": "SABnzbd",
"implementation": "Sabnzbd", "configContract": "SabnzbdSettings",
"fields": [{"name": "host", "value": "gluetun"}, {"name": "port", "value": 8080},
{"name": "apiKey", "value": SAB}, {"name": catf, "value": catv},
{"name": recent, "value": -100}, {"name": older, "value": -100}]}
def tr_body(catf, catv):
return {"enable": True, "protocol": "torrent", "priority": 1, "name": "Transmission",
"implementation": "Transmission", "configContract": "TransmissionSettings",
"fields": [{"name": "host", "value": "gluetun"}, {"name": "port", "value": 9091},
{"name": "urlBase", "value": "/transmission/"}, {"name": catf, "value": catv}]}
cats = sab_cats()
for c in ("movies-4k", "tv-4k"):
if c not in cats:
sab_add(c); changed = True
plan = [
(7879, os.environ["RADARR4K_KEY"], [sab_body("movieCategory", "movies-4k", "recentMoviePriority", "olderMoviePriority"),
tr_body("movieCategory", "radarr-uhd")]),
(8990, os.environ["SONARR4K_KEY"], [sab_body("tvCategory", "tv-4k", "recentTvPriority", "olderTvPriority"),
tr_body("tvCategory", "sonarr-uhd")]),
]
for port, key, bodies in plan:
have = {c["name"] for c in dc_get(port, key)}
for b in bodies:
if b["name"] not in have:
dc_post(port, key, b); changed = True
print("CHANGED" if changed else "NOCHANGE")
- name: Ensure 4K download clients + sabnzbd categories (idempotent, cats movies-4k/tv-4k)
ansible.builtin.command: "python3 {{ app_dir }}/ensure-arrs-4k-dl.py"
environment:
RADARR4K_KEY: "{{ vault_radarr4k_apikey }}"
SONARR4K_KEY: "{{ vault_sonarr4k_apikey }}"
SAB_KEY: "{{ vault_sabnzbd_apikey }}"
register: dl_result
changed_when: "'CHANGED' in dl_result.stdout"
no_log: true