Media all drives, Parts A-C on Tester 1: S1-S3 proven (hardlink with one bind per drive; today's shape copies); R-1014..R-1019 filed
gates / gates (push) Successful in 7m46s
gates / gates (push) Successful in 7m46s
Two test drives enrolled through the dashboard; no test media in any backup tier (A6, three proofs); Tester 1 follows a credential-free local mirror of the drill catalog (R-992 worked around a third way). The live-fix task may start.
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
== /api/system/info 200
|
||||
{"ok": true, "data": {"sync_status": {"last_sync": "2026-10-14T13:44:42.082522426Z", "last_status": "ok", "syncing": false}, "system": {"total_mem_mb": 4096, "used_mem_mb": 680, "avail_mem_mb": 3416, "mem_percent": 16.6015625, "disk_total_gb": 68.72599792480469, "disk_used_gb": 4.120944976806641, "disk_avail_gb": 61.089439392089844, "disk_percent": 6.319461258646124, "disk_known": true, "hdd_total_gb": 48.914520263671875, "hdd_used_gb": 0.23301315307617188, "hdd_avail_gb": 46.1658821105957, "hdd_percent": 0.5021954763190453, "hdd_configured": true, "hdd_known": true, "cpu_percent": 17.57910597689603, "load_avg_1": 0.21, "load_avg_5": 0.3, "load_avg_15": 0.32, "temperature_celsius": 0}}}
|
||||
== /api/system/memory 200
|
||||
{"data": {"vmid": 9201, "allocated_mb": 4096, "usage_mb": 944, "host_total_mb": 7872, "min_mb": 2048, "max_mb": 5824, "floor_mb": 2048, "running": true}, "ok": true}
|
||||
== /api/disks 200
|
||||
{"data": {"vmid": 9201, "disks": [{"name": "863c67fd-3e21-4d9f-9a74-3c5fa406f2a2", "type": "usb", "state": "attached", "backing_device": "/dev/sdb", "mount_path": "/mnt/adatlemez", "class": "", "role": "user-data", "data_bearing": false, "data_reason": "", "total_bytes": 52521566208, "used_bytes": 250195968, "used_fraction": 0.0047636806375718965, "durable_id": "uuid:863c67fd-3e21-4d9f-9a74-3c5fa406f2a2", "guest_attached": false, "guest_path": "/mnt/felhom-drives/adatlemez", "bound_under_parent": true, "smart": {"health": "UNKNOWN", "model_name": "QEMU QEMU HARDDISK", "temperature_c": 0, "power_on_hours": null, "reallocated_sectors": null, "pending_sectors": null, "offline_uncorrectable": null, "critical_warning": null, "media_errors": null, "percentage_used": null}}, {"name": "local", "type": "local", "state": "attached", "backing_device": "", "mount_path": "", "class": "", "role": "system", "data_bearing": false, "data_reason": "", "total_bytes": 62903865344, "used_bytes": 13649629184, "used_fraction": 0.216991898818217, "durable_id": "path:/var/lib/vz", "guest_attached": false, "backup_target": true, "bound_under_parent": false, "smart": {"health": "UNKNOWN", "model_name": "QEMU QEMU HARDDISK", "temperature_c": 0, "power_on_hours": null, "reallocated_sectors": null, "pending_sectors": null, "offline_uncorrectable": null, "critical_warning": null, "media_errors": null, "percentage_used": null}}, {"name": "local-lvm", "type": "lvmthin", "state": "attached", "backing_device": "", "mount_path": "", "class": "", "role": "system", "data_bearing": false, "data_reason": "", "total_bytes": 121521569792, "used_bytes": 7813836937, "used_fraction": 0.0642999999948519, "durable_id": "pve/data", "guest_attached": false, "bound_under_parent": false, "smart": {"health": "UNKNOWN", "temperature_c": null, "power_on_hours": null, "reallocated_sectors": null, "pending_sectors": null, "offline_uncorrectable": null, "critical_warning": null, "media_errors": null, "percentage_used": null}}, {"name": "felhom-pbs", "type": "pbs", "state": "attached", "backing_device": "", "mount_path": "", "class": "", "role": "backup", "data_bearing": false, "data_reason": "", "total_bytes": 0, "used_bytes": 0, "used_fraction": 0, "durable_id": "10.77.0.1:felhom-offsite#c6:07:28:3f:5b:7b:5a:41:90:28:d7:ca:4f:37:14:70:56:39:2e:2f:0b:71:e8:06:ca:60:4a:d5:56:5f:3c:fd", "guest_attached": false, "bound_under_parent": false, "smart": {"health": "UNKNOWN", "temperature_c": null, "power_on_hours": nul
|
||||
== /api/storage/backup-target 200
|
||||
{"data": {"degraded": true, "known": true, "label": "", "message": "A rendszermentés jelenleg ugyanazon a lemezen van, mint a rendszer — így hibás fájlok ellen véd, lemezhiba ellen nem. Csatlakoztass egy második meghajtót a teljes védelemhez.", "offer_label": "Adatlemez", "offer_message": "Ezt a meghajtót kijelölheted a rendszermentés helyéül — így egy lemezhiba után is vissza tudod állítani a rendszert.", "offer_path": "/mnt/felhom-drives/adatlemez", "target": "local"}, "ok": true}
|
||||
== deployed: [('bookstack', 'running'), ('paperless-ngx', 'running'), ('privatebin', 'running')]
|
||||
@@ -0,0 +1,30 @@
|
||||
2026-10-14T15:57:57+02:00
|
||||
bios: ovmf
|
||||
boot: order=scsi0
|
||||
cores: 4
|
||||
cpu: host
|
||||
efidisk0: nvme-scratch:341/vm-341-disk-0.raw,efitype=4m,pre-enrolled-keys=0,size=528K
|
||||
machine: q35
|
||||
memory: 8192
|
||||
meta: creation-qemu=11.0.0,ctime=1791141969
|
||||
name: night1004-tester1
|
||||
net0: virtio=BC:24:11:AC:E3:F9,bridge=vmbr0
|
||||
onboot: 1
|
||||
ostype: l26
|
||||
scsi0: nvme-scratch:341/vm-341-disk-1.raw,size=200G
|
||||
scsi1: nvme-scratch:341/vm-341-disk-2.raw,size=50G
|
||||
scsihw: virtio-scsi-single
|
||||
smbios1: uuid=451c8d94-abf2-46e4-a278-b579a9f315b5
|
||||
vga: std
|
||||
vmgenid: acdf89db-a810-445c-9d88-7bccd688d3c1
|
||||
Filesystem 1B-blocks Used Available Use% Mounted on
|
||||
/dev/nvme0n1 1006980812800 135772049408 819981512704 15% /mnt/hdd_1
|
||||
total 76833588
|
||||
4 drwxr----- 2 root root 4096 Oct 4 21:26 .
|
||||
4 drwxr-xr-x 5 root root 4096 Oct 14 05:42 ..
|
||||
528 -rw-r----- 1 root root 540672 Oct 14 14:07 vm-341-disk-0.raw
|
||||
75117456 -rw-r----- 1 root root 214748364800 Oct 14 15:57 vm-341-disk-1.raw
|
||||
1715596 -rw-r----- 1 root root 53687091200 Oct 14 15:57 vm-341-disk-2.raw
|
||||
total used free shared buff/cache available
|
||||
Mem: 29994 11576 10048 164 8973 18417
|
||||
Swap: 8191 0 8191
|
||||
@@ -0,0 +1,49 @@
|
||||
2026-10-14T15:57:59+02:00
|
||||
felhom-agent 0.156.1
|
||||
arch: amd64
|
||||
cores: 3
|
||||
features: nesting=1,keyctl=1
|
||||
hookscript: local:snippets/felhom-guest-hook.sh
|
||||
hostname: tester-1
|
||||
memory: 4096
|
||||
mp0: local-lvm:vm-9201-disk-1,mp=/var/lib/felhom,backup=1,size=70G
|
||||
mp8: /mnt/felhom-drives,mp=/mnt/felhom-drives
|
||||
mp9: /var/lib/felhom-agent/guests/9201/bootstrap,mp=/etc/felhom-bootstrap,ro=1
|
||||
net0: name=eth0,bridge=vmbr0,hwaddr=BC:24:11:FF:2F:FC,ip=dhcp,type=veth
|
||||
net1: name=eth1,bridge=vmbr9,hwaddr=BC:24:11:6C:BB:E5,ip=169.254.253.2/30,type=veth
|
||||
onboot: 1
|
||||
ostype: debian
|
||||
rootfs: local-lvm:vm-9201-disk-0,size=32G
|
||||
swap: 512
|
||||
unprivileged: 1
|
||||
NAME SIZE TYPE MOUNTPOINT SERIAL
|
||||
sda 200G disk drive-scsi0
|
||||
|-sda1 1007K part
|
||||
|-sda2 1G part /boot/efi
|
||||
`-sda3 199G part
|
||||
|-pve-swap 7.7G lvm [SWAP]
|
||||
|-pve-root 59.8G lvm /
|
||||
|-pve-data_tmeta 1.2G lvm
|
||||
| `-pve-data-tpool 113.2G lvm
|
||||
| |-pve-data 113.2G lvm
|
||||
| |-pve-vm--9201--disk--0 32G lvm
|
||||
| `-pve-vm--9201--disk--1 70G lvm
|
||||
`-pve-data_tdata 113.2G lvm
|
||||
`-pve-data-tpool 113.2G lvm
|
||||
|-pve-data 113.2G lvm
|
||||
|-pve-vm--9201--disk--0 32G lvm
|
||||
`-pve-vm--9201--disk--1 70G lvm
|
||||
sdb 50G disk /mnt/adatlemez drive-scsi1
|
||||
total used free shared buff/cache available
|
||||
Mem: 7872 2668 2247 105 3333 5203
|
||||
Swap: 7871 0 7871
|
||||
felhom-controller gitea.dooplex.hu/admin/felhom-controller:0.310.0 Up About an hour (healthy)
|
||||
paperless-webserver ghcr.io/paperless-ngx/paperless-ngx:2.20.15 Up 2 hours (healthy)
|
||||
paperless-postgres postgres:18-alpine Up 2 hours (healthy)
|
||||
paperless-redis redis:7-alpine Up 2 hours (healthy)
|
||||
privatebin privatebin/pdo:2.0.6 Up 2 hours (healthy)
|
||||
bookstack lscr.io/linuxserver/bookstack:26.09.1 Up 2 hours (healthy)
|
||||
bookstack-db mariadb:12.3 Up 2 hours (healthy)
|
||||
filebrowser gtstef/filebrowser:1.5.6-stable Up 2 hours (healthy)
|
||||
cloudflared cloudflare/cloudflared:2026.9.3 Up 2 hours (healthy)
|
||||
traefik traefik:v3.7.13 Up 2 hours
|
||||
@@ -0,0 +1,33 @@
|
||||
+ date -Is
|
||||
2026-10-14T15:58:36+02:00
|
||||
+ df -B1G /mnt/hdd_1
|
||||
+ tail -1
|
||||
/dev/nvme0n1 938 127 764 15% /mnt/hdd_1
|
||||
+ qm set 341 --scsi2 nvme-scratch:100,backup=0
|
||||
update VM 341: -scsi2 nvme-scratch:100,backup=0
|
||||
Formatting '/mnt/hdd_1/images/341/vm-341-disk-3.raw', fmt=raw size=107374182400 preallocation=off
|
||||
scsi2: successfully created disk 'nvme-scratch:341/vm-341-disk-3.raw,backup=0,size=100G'
|
||||
+ qm set 341 --scsi3 nvme-scratch:100,backup=0
|
||||
update VM 341: -scsi3 nvme-scratch:100,backup=0
|
||||
Formatting '/mnt/hdd_1/images/341/vm-341-disk-4.raw', fmt=raw size=107374182400 preallocation=off
|
||||
scsi3: successfully created disk 'nvme-scratch:341/vm-341-disk-4.raw,backup=0,size=100G'
|
||||
+ qm config 341
|
||||
+ grep -E '^scsi|^memory|hotplug'
|
||||
memory: 8192
|
||||
scsi0: nvme-scratch:341/vm-341-disk-1.raw,size=200G
|
||||
scsi1: nvme-scratch:341/vm-341-disk-2.raw,size=50G
|
||||
scsi2: nvme-scratch:341/vm-341-disk-3.raw,backup=0,size=100G
|
||||
scsi3: nvme-scratch:341/vm-341-disk-4.raw,backup=0,size=100G
|
||||
scsihw: virtio-scsi-single
|
||||
+ ls -las /mnt/hdd_1/images/341/
|
||||
total 76833596
|
||||
4 drwxr----- 2 root root 4096 Oct 14 15:58 .
|
||||
4 drwxr-xr-x 5 root root 4096 Oct 14 05:42 ..
|
||||
528 -rw-r----- 1 root root 540672 Oct 14 14:07 vm-341-disk-0.raw
|
||||
75117456 -rw-r----- 1 root root 214748364800 Oct 14 15:58 vm-341-disk-1.raw
|
||||
1715596 -rw-r----- 1 root root 53687091200 Oct 14 15:58 vm-341-disk-2.raw
|
||||
4 -rw-r----- 1 root root 107374182400 Oct 14 15:58 vm-341-disk-3.raw
|
||||
4 -rw-r----- 1 root root 107374182400 Oct 14 15:58 vm-341-disk-4.raw
|
||||
+ df -B1G /mnt/hdd_1
|
||||
+ tail -1
|
||||
/dev/nvme0n1 938 127 764 15% /mnt/hdd_1
|
||||
@@ -0,0 +1,5 @@
|
||||
15:59:08 candidates -> 200 ['/dev/sdc', '/dev/sdd']
|
||||
15:59:08 POST /api/storage/init {"device": "/dev/sdc", "fstype": "ext4", "mount_name": "media_a", "label": "Media A", "set_default": false, "confirmed": false, "durable_id": ""} -> 200 {"data": {"phase": "formatting", "started": true}, "ok": true}
|
||||
15:59:10 status -> 200 {"device": "/dev/sdc", "durable_id": "", "error": "", "opsign": "", "phase": "mounting", "reason": "", "started_at": "2026-10-14T13:59:08.76516334Z", "updated_at": "2026-10-14T13:59:09.122147197Z", "where": "/mnt/media_a"}
|
||||
15:59:12 status -> 200 {"device": "/dev/sdc", "durable_id": "", "error": "", "opsign": "", "phase": "done", "reason": "", "started_at": "2026-10-14T13:59:08.76516334Z", "updated_at": "2026-10-14T13:59:10.953891979Z", "where": "/mnt/felhom-drives/media_a"}
|
||||
RESULT done
|
||||
@@ -0,0 +1,5 @@
|
||||
15:59:16 candidates -> 200 ['/dev/sdd']
|
||||
15:59:16 POST /api/storage/init {"device": "/dev/sdd", "fstype": "ext4", "mount_name": "media_b", "label": "Media B", "set_default": false, "confirmed": false, "durable_id": ""} -> 200 {"data": {"phase": "formatting", "started": true}, "ok": true}
|
||||
15:59:18 status -> 200 {"device": "/dev/sdd", "durable_id": "", "error": "", "opsign": "", "phase": "mounting", "reason": "", "started_at": "2026-10-14T13:59:16.535337994Z", "updated_at": "2026-10-14T13:59:16.879143306Z", "where": "/mnt/media_b"}
|
||||
15:59:20 status -> 200 {"device": "/dev/sdd", "durable_id": "", "error": "", "opsign": "", "phase": "done", "reason": "", "started_at": "2026-10-14T13:59:16.535337994Z", "updated_at": "2026-10-14T13:59:18.641630025Z", "where": "/mnt/felhom-drives/media_b"}
|
||||
RESULT done
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
backup-target 200 {"data": {"degraded": true, "known": true, "label": "", "message": "A rendszermentés jelenleg ugyanazon a lemezen van, mint a rendszer — így hibás fájlok ellen véd, lemezhiba ellen nem. Csatlakoztass egy második meghajtót a teljes védelemhez.", "offer_label": "Adatlemez", "offer_message": "Ezt a meghajtót kijelölheted a rendszermentés helyéül — így egy lemezhiba után is vissza tudod állítani a rendszert.", "offer_path": "/mnt/felhom-drives/adatlemez", "target": "local"}, "ok": true}
|
||||
storage page 200 77
|
||||
Tárhely — Felhom.eu
|
||||
Indítópult
|
||||
Vezérlőpult
|
||||
Alkalmazások
|
||||
Tárhely
|
||||
Meghajtók
|
||||
Hálózati tárhely
|
||||
Biztonsági mentés
|
||||
Áttekintés
|
||||
Távoli mentés
|
||||
Alkalmazások
|
||||
Visszaállítás
|
||||
Megosztás
|
||||
Hálózati megosztás
|
||||
Rendszermonitor
|
||||
Hibakeresés
|
||||
Beállítások
|
||||
Rendszer
|
||||
Értesítések
|
||||
Biztonság és hozzáférés
|
||||
0.310.0
|
||||
Magyar
|
||||
English
|
||||
Kijelentkezés ↗
|
||||
Tárhely — Meghajtók
|
||||
Adattárolók
|
||||
Külső meghajtók kezelése alkalmazásadatok tárolásához.
|
||||
Megőrzött adatok
|
||||
Eltávolított alkalmazások lemezen maradt adatai
|
||||
Adatlemez
|
||||
/mnt/felhom-drives/adatlemez
|
||||
Alapértelmezett
|
||||
Aktív
|
||||
0,2 GB / 48,9 GB
|
||||
ext4 · /dev/sdb[/felhom-data] · QEMU HARDDISK
|
||||
1 alkalmazás használja
|
||||
Paperless-ngx
|
||||
6,3 KB
|
||||
Mozgatás
|
||||
Új telepítések letiltása
|
||||
Áthelyezés ide…
|
||||
Media A (/mnt/felhom-drives/media_a)
|
||||
Media B (/mnt/felhom-drives/media_b)
|
||||
Összes adat áthelyezése
|
||||
Végleges leszerelés
|
||||
Media A
|
||||
/mnt/felhom-drives/media_a
|
||||
Aktív
|
||||
0,0 GB / 97,9 GB
|
||||
ext4 · /dev/sdc[/felhom-data] · QEMU HARDDISK
|
||||
Nincs alkalmazás ezen a tárolón
|
||||
Legyen alapértelmezett
|
||||
Új telepítések letiltása
|
||||
Végleges leszerelés
|
||||
Media B
|
||||
/mnt/felhom-drives/media_b
|
||||
Aktív
|
||||
0,0 GB / 97,9 GB
|
||||
ext4 · /dev/sdd[/felhom-data] · QEMU HARDDISK
|
||||
Nincs alkalmazás ezen a tárolón
|
||||
Legyen alapértelmezett
|
||||
Új telepítések letiltása
|
||||
Végleges leszerelés
|
||||
Adatok áthelyezése
|
||||
…
|
||||
Új meghajtó inicializálása
|
||||
Meglévő meghajtó csatolása
|
||||
Nem regisztrált meghajtók
|
||||
az ügynök által észlelt, még nem regisztrált adatmeghajtók
|
||||
Rendszermeghajtók
|
||||
védett — csak operátori aláírással módosíthatók
|
||||
Már csatlakoztatott tárhely hozzáadása kézzel
|
||||
Elérési út
|
||||
Pl. /mnt/hdd_1 — a meghajtónak már csatolva kell lennie
|
||||
Megnevezés (opcionális)
|
||||
Legyen alapértelmezett új telepítéseknél
|
||||
Hozzáadás
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
Wed Oct 14 13:59:31 UTC 2026
|
||||
TARGET SOURCE FSTYPE PROPAGATION
|
||||
/mnt/felhom-drives /dev/mapper/pve-root[/mnt/felhom-drives] ext4 shared,slave
|
||||
|-/mnt/felhom-drives/adatlemez /dev/sdb[/felhom-data] ext4 shared,slave
|
||||
|-/mnt/felhom-drives/media_a /dev/sdc[/felhom-data] ext4 shared,slave
|
||||
`-/mnt/felhom-drives/media_b /dev/sdd[/felhom-data] ext4 shared,slave
|
||||
/mnt/felhom-drives /dev/mapper/pve-root[/mnt/felhom-drives] ext4 shared,slave
|
||||
|-/mnt/felhom-drives/adatlemez /dev/sdb[/felhom-data] ext4 shared,slave
|
||||
|-/mnt/felhom-drives/media_a /dev/sdc[/felhom-data] ext4 shared,slave
|
||||
`-/mnt/felhom-drives/media_b /dev/sdd[/felhom-data] ext4 shared,slave
|
||||
== media_a
|
||||
total 12
|
||||
drwxr-xr-x 3 root root 4096 Oct 14 13:59 .
|
||||
drwxr-xr-x 5 nobody nogroup 4096 Oct 14 13:59 ..
|
||||
drwxrwsr-x 6 root 1000 4096 Oct 14 13:59 userdata
|
||||
/mnt/felhom-drives/media_a
|
||||
/mnt/felhom-drives/media_a/userdata
|
||||
/mnt/felhom-drives/media_a/userdata/documents
|
||||
/mnt/felhom-drives/media_a/userdata/downloads
|
||||
/mnt/felhom-drives/media_a/userdata/media
|
||||
/mnt/felhom-drives/media_a/userdata/media/audiobooks
|
||||
/mnt/felhom-drives/media_a/userdata/media/books
|
||||
/mnt/felhom-drives/media_a/userdata/media/comics
|
||||
/mnt/felhom-drives/media_a/userdata/media/grimmory
|
||||
/mnt/felhom-drives/media_a/userdata/media/metube
|
||||
/mnt/felhom-drives/media_a/userdata/media/movies
|
||||
/mnt/felhom-drives/media_a/userdata/media/music
|
||||
/mnt/felhom-drives/media_a/userdata/media/nzbget
|
||||
/mnt/felhom-drives/media_a/userdata/media/photos
|
||||
/mnt/felhom-drives/media_a/userdata/media/podcasts
|
||||
/mnt/felhom-drives/media_a/userdata/media/torrents
|
||||
/mnt/felhom-drives/media_a/userdata/media/transmission
|
||||
/mnt/felhom-drives/media_a/userdata/media/tv
|
||||
/mnt/felhom-drives/media_a/userdata/media/usenet
|
||||
/mnt/felhom-drives/media_a/userdata/roms
|
||||
== media_b
|
||||
total 12
|
||||
drwxr-xr-x 3 root root 4096 Oct 14 13:59 .
|
||||
drwxr-xr-x 5 nobody nogroup 4096 Oct 14 13:59 ..
|
||||
drwxrwsr-x 6 root 1000 4096 Oct 14 13:59 userdata
|
||||
/mnt/felhom-drives/media_b
|
||||
/mnt/felhom-drives/media_b/userdata
|
||||
/mnt/felhom-drives/media_b/userdata/documents
|
||||
/mnt/felhom-drives/media_b/userdata/downloads
|
||||
/mnt/felhom-drives/media_b/userdata/media
|
||||
/mnt/felhom-drives/media_b/userdata/media/audiobooks
|
||||
/mnt/felhom-drives/media_b/userdata/media/books
|
||||
/mnt/felhom-drives/media_b/userdata/media/comics
|
||||
/mnt/felhom-drives/media_b/userdata/media/grimmory
|
||||
/mnt/felhom-drives/media_b/userdata/media/metube
|
||||
/mnt/felhom-drives/media_b/userdata/media/movies
|
||||
/mnt/felhom-drives/media_b/userdata/media/music
|
||||
/mnt/felhom-drives/media_b/userdata/media/nzbget
|
||||
/mnt/felhom-drives/media_b/userdata/media/photos
|
||||
/mnt/felhom-drives/media_b/userdata/media/podcasts
|
||||
/mnt/felhom-drives/media_b/userdata/media/torrents
|
||||
/mnt/felhom-drives/media_b/userdata/media/transmission
|
||||
/mnt/felhom-drives/media_b/userdata/media/tv
|
||||
/mnt/felhom-drives/media_b/userdata/media/usenet
|
||||
/mnt/felhom-drives/media_b/userdata/roms
|
||||
== adatlemez
|
||||
total 20
|
||||
drwxr-xr-x 5 root root 4096 Oct 4 19:47 .
|
||||
drwxr-xr-x 5 nobody nogroup 4096 Oct 14 13:59 ..
|
||||
drwxr-xr-x 3 root root 4096 Oct 4 19:45 appdata
|
||||
drwxr-xr-x 5 root root 4096 Oct 5 03:30 backups
|
||||
drwxrwsr-x 6 root 1000 4096 Oct 4 19:44 userdata
|
||||
/mnt/felhom-drives/adatlemez
|
||||
/mnt/felhom-drives/adatlemez/appdata
|
||||
/mnt/felhom-drives/adatlemez/appdata/paperless
|
||||
/mnt/felhom-drives/adatlemez/appdata/paperless/export
|
||||
/mnt/felhom-drives/adatlemez/appdata/paperless/media
|
||||
/mnt/felhom-drives/adatlemez/backups
|
||||
/mnt/felhom-drives/adatlemez/backups/offsite-proof
|
||||
/mnt/felhom-drives/adatlemez/backups/primary
|
||||
/mnt/felhom-drives/adatlemez/backups/primary/paperless-ngx
|
||||
/mnt/felhom-drives/adatlemez/backups/secondary
|
||||
/mnt/felhom-drives/adatlemez/backups/secondary/bookstack
|
||||
/mnt/felhom-drives/adatlemez/backups/secondary/privatebin
|
||||
/mnt/felhom-drives/adatlemez/userdata
|
||||
/mnt/felhom-drives/adatlemez/userdata/documents
|
||||
/mnt/felhom-drives/adatlemez/userdata/downloads
|
||||
/mnt/felhom-drives/adatlemez/userdata/media
|
||||
/mnt/felhom-drives/adatlemez/userdata/media/audiobooks
|
||||
/mnt/felhom-drives/adatlemez/userdata/media/books
|
||||
/mnt/felhom-drives/adatlemez/userdata/media/comics
|
||||
/mnt/felhom-drives/adatlemez/userdata/media/grimmory
|
||||
/mnt/felhom-drives/adatlemez/userdata/media/metube
|
||||
/mnt/felhom-drives/adatlemez/userdata/media/movies
|
||||
/mnt/felhom-drives/adatlemez/userdata/media/music
|
||||
/mnt/felhom-drives/adatlemez/userdata/media/nzbget
|
||||
/mnt/felhom-drives/adatlemez/userdata/media/photos
|
||||
/mnt/felhom-drives/adatlemez/userdata/media/podcasts
|
||||
/mnt/felhom-drives/adatlemez/userdata/media/torrents
|
||||
/mnt/felhom-drives/adatlemez/userdata/media/transmission
|
||||
/mnt/felhom-drives/adatlemez/userdata/media/tv
|
||||
/mnt/felhom-drives/adatlemez/userdata/media/usenet
|
||||
/mnt/felhom-drives/adatlemez/userdata/roms
|
||||
2026-10-14T13:59:18.752620558Z /mnt/felhom-drives/adatlemez/userdata->/srv/adatlemez true rprivate; /mnt/sys_drive/felhom-data/userdata/import->/srv/beolvasas true rprivate; /mnt/felhom-drives/media_a/userdata->/srv/media_a true rprivate; /mnt/felhom-drives/media_b/userdata->/srv/media_b true rprivate; /opt/docker/stacks/filebrowser/config.yaml->/home/filebrowser/config.yaml false rprivate; /var/lib/docker/volumes/filebrowser_filebrowser_data/_data->/home/filebrowser/data true ;
|
||||
2026/10/14 13:55:42 [INFO] [scheduler] Job agent-channel-health completed (took 707ms)
|
||||
2026/10/14 13:55:51 [INFO] [stacks] Status refresh: 9 containers across 67 stacks
|
||||
2026/10/14 13:56:00 [INFO] [web] Login from 192.168.0.180
|
||||
2026/10/14 13:56:01 [INFO] [stacks] Status refresh: 9 containers across 67 stacks
|
||||
2026/10/14 13:56:01 [INFO] Health probes: 1 ok (of 1 probed)
|
||||
2026/10/14 13:56:11 [INFO] [stacks] Status refresh: 9 containers across 67 stacks
|
||||
2026/10/14 13:56:21 [INFO] [stacks] Status refresh: 9 containers across 67 stacks
|
||||
2026/10/14 13:56:21 [INFO] Health probes: 2 ok (of 2 probed)
|
||||
2026/10/14 13:56:31 [INFO] [stacks] Status refresh: 9 containers across 67 stacks
|
||||
2026/10/14 13:56:32 [INFO] [web] Login from 192.168.0.180
|
||||
2026/10/14 13:56:41 [INFO] [scheduler] Running job: stack-scan
|
||||
2026/10/14 13:56:41 [INFO] [stacks] Status refresh: 9 containers across 67 stacks
|
||||
2026/10/14 13:56:41 [INFO] [stacks] ScanStacks complete: 67 stacks found (3 deployed, 64 available)
|
||||
2026/10/14 13:56:41 [INFO] [stacks] Status refresh: 9 containers across 67 stacks
|
||||
2026/10/14 13:56:41 [INFO] [scheduler] Job stack-scan completed (took 125ms)
|
||||
2026/10/14 13:56:41 [INFO] [scheduler] Running job: agent-channel-health
|
||||
2026/10/14 13:56:42 [INFO] [scheduler] Job agent-channel-health completed (took 485ms)
|
||||
2026/10/14 13:56:51 [INFO] [stacks] Status refresh: 9 containers across 67 stacks
|
||||
2026/10/14 13:57:01 [INFO] [stacks] Status refresh: 9 containers across 67 stacks
|
||||
2026/10/14 13:57:11 [INFO] [stacks] Status refresh: 9 containers across 67 stacks
|
||||
2026/10/14 13:57:21 [INFO] [stacks] Status refresh: 9 containers across 67 stacks
|
||||
2026/10/14 13:57:31 [INFO] [stacks] Status refresh: 9 containers across 67 stacks
|
||||
2026/10/14 13:57:41 [INFO] [stacks] Status refresh: 9 containers across 67 stacks
|
||||
2026/10/14 13:57:41 [INFO] [scheduler] Running job: agent-channel-health
|
||||
2026/10/14 13:57:42 [INFO] [scheduler] Job agent-channel-health completed (took 473ms)
|
||||
2026/10/14 13:57:51 [INFO] [stacks] Status refresh: 9 containers across 67 stacks
|
||||
2026/10/14 13:58:01 [INFO] [stacks] Status refresh: 9 containers across 67 stacks
|
||||
2026/10/14 13:58:11 [INFO] [stacks] Status refresh: 9 containers across 67 stacks
|
||||
2026/10/14 13:58:21 [INFO] [stacks] Status refresh: 9 containers across 67 stacks
|
||||
2026/10/14 13:58:31 [INFO] [stacks] Status refresh: 9 containers across 67 stacks
|
||||
2026/10/14 13:58:41 [INFO] [scheduler] Running job: stack-scan
|
||||
2026/10/14 13:58:41 [INFO] [stacks] ScanStacks complete: 67 stacks found (3 deployed, 64 available)
|
||||
2026/10/14 13:58:41 [INFO] [stacks] Status refresh: 9 containers across 67 stacks
|
||||
2026/10/14 13:58:41 [INFO] [scheduler] Job stack-scan completed (took 81ms)
|
||||
2026/10/14 13:58:41 [INFO] [stacks] Status refresh: 9 containers across 67 stacks
|
||||
2026/10/14 13:58:41 [INFO] [scheduler] Running job: agent-channel-health
|
||||
2026/10/14 13:58:42 [INFO] [scheduler] Job agent-channel-health completed (took 480ms)
|
||||
2026/10/14 13:58:49 [INFO] [web] Login from 192.168.0.180
|
||||
2026/10/14 13:58:51 [INFO] [stacks] Status refresh: 9 containers across 67 stacks
|
||||
2026/10/14 13:59:01 [INFO] [stacks] Status refresh: 9 containers across 67 stacks
|
||||
2026/10/14 13:59:08 [INFO] [web] Login from 192.168.0.180
|
||||
2026/10/14 13:59:08 [INFO] [web] storage init "/mnt/media_a" started (device /dev/sdc, fs ext4)
|
||||
2026/10/14 13:59:09 [INFO] [web] enroll /dev/sdc: mounting the agent-verified new filesystem 4ebfd36d-87d3-4b63-839e-a5a710fc799a
|
||||
2026/10/14 13:59:10 [INFO] [web] enroll: drive bound into guest: /mnt/media_a
|
||||
2026/10/14 13:59:10 [INFO] [settings] Added storage path: /mnt/felhom-drives/media_a
|
||||
2026/10/14 13:59:10 [INFO] [settings] Settings saved
|
||||
2026/10/14 13:59:10 [INFO] [web] storage init done: /mnt/felhom-drives/media_a registered (2188ms)
|
||||
2026/10/14 13:59:10 [INFO] [stacks] ParseComposeHDDMounts: found 2 HDD mounts for /opt/docker/stacks/paperless-ngx/docker-compose.yml
|
||||
2026/10/14 13:59:11 [INFO] [web] FileBrowser mounts synced (recreated) — 2 storage path(s), config updated
|
||||
2026/10/14 13:59:11 [INFO] [stacks] Status refresh: 9 containers across 67 stacks
|
||||
2026/10/14 13:59:16 [INFO] [web] Login from 192.168.0.180
|
||||
2026/10/14 13:59:16 [INFO] [web] storage init "/mnt/media_b" started (device /dev/sdd, fs ext4)
|
||||
2026/10/14 13:59:16 [INFO] [web] enroll /dev/sdd: mounting the agent-verified new filesystem e15652d5-0aa7-4eb8-93a1-afb2839d527c
|
||||
2026/10/14 13:59:18 [INFO] [web] enroll: drive bound into guest: /mnt/media_b
|
||||
2026/10/14 13:59:18 [INFO] [settings] Added storage path: /mnt/felhom-drives/media_b
|
||||
2026/10/14 13:59:18 [INFO] [settings] Settings saved
|
||||
2026/10/14 13:59:18 [INFO] [web] storage init done: /mnt/felhom-drives/media_b registered (2106ms)
|
||||
2026/10/14 13:59:18 [INFO] [stacks] ParseComposeHDDMounts: found 2 HDD mounts for /opt/docker/stacks/paperless-ngx/docker-compose.yml
|
||||
2026/10/14 13:59:19 [INFO] [web] FileBrowser mounts synced (recreated) — 3 storage path(s), config updated
|
||||
2026/10/14 13:59:21 [INFO] [stacks] Status refresh: 9 containers across 67 stacks
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
2026-10-14T15:59:52+02:00
|
||||
--- cluster backup jobs:
|
||||
[]
|
||||
|
||||
ls: cannot access '/etc/pve/jobs.cfg': No such file or directory
|
||||
scsi2: nvme-scratch:341/vm-341-disk-3.raw,backup=0,size=100G
|
||||
scsi3: nvme-scratch:341/vm-341-disk-4.raw,backup=0,size=100G
|
||||
@@ -0,0 +1,32 @@
|
||||
2026-10-14T15:59:55+02:00
|
||||
mp0: local-lvm:vm-9201-disk-1,mp=/var/lib/felhom,backup=1,size=70G
|
||||
mp8: /mnt/felhom-drives,mp=/mnt/felhom-drives
|
||||
mp9: /var/lib/felhom-agent/guests/9201/bootstrap,mp=/etc/felhom-bootstrap,ro=1
|
||||
rootfs: local-lvm:vm-9201-disk-0,size=32G
|
||||
total 4
|
||||
-rw-r--r-- 1 root root 1495 Oct 14 04:44 lxc-9201.log
|
||||
2026-10-14 04:42:22 INFO: Starting Backup of VM 9201 (lxc)
|
||||
2026-10-14 04:42:22 INFO: including mount point rootfs ('/') in backup
|
||||
2026-10-14 04:42:22 INFO: including mount point mp0 ('/var/lib/felhom') in backup
|
||||
2026-10-14 04:42:22 INFO: excluding bind mount point mp8 ('/mnt/felhom-drives') from backup (not a volume)
|
||||
2026-10-14 04:42:22 INFO: excluding bind mount point mp9 ('/etc/felhom-bootstrap') from backup (not a volume)
|
||||
2026-10-14 04:42:22 INFO: backup mode: snapshot
|
||||
2026-10-14 04:42:24 INFO: creating vzdump archive '/var/lib/vz/dump/vzdump-lxc-9201-2026_10_14-04_42_22.tar.zst'
|
||||
2026-10-14 04:44:52 INFO: Total bytes written: 5447720960 (5.1GiB, 35MiB/s)
|
||||
2026-10-14 04:44:53 INFO: Finished Backup of VM 9201 (00:02:31)
|
||||
total 5245972
|
||||
-rw-r--r-- 1 root root 1495 Oct 14 04:44 vzdump-lxc-9201-2026_10_14-04_42_22.log
|
||||
-rw-r--r-- 1 root root 16 Oct 14 04:44 vzdump-lxc-9201-2026_10_14-04_42_22.tar.zst.notes
|
||||
-rw-r--r-- 1 root root 1575398000 Oct 14 04:44 vzdump-lxc-9201-2026_10_14-04_42_22.tar.zst
|
||||
-rw-r--r-- 1 root root 1495 Oct 13 04:40 vzdump-lxc-9201-2026_10_13-04_38_03.log
|
||||
== UPID:felhom:0014BF84:005ED7D4:6ACBEB45:vzdump:9201:felhom-agent@pve!agent:
|
||||
INFO: starting new backup job: vzdump 9201 --notes-template 'felhom local-api' --compress zstd --mode snapshot --storage felhom-pbs --node felhom
|
||||
INFO: Starting Backup of VM 9201 (lxc)
|
||||
INFO: including mount point rootfs ('/') in backup
|
||||
INFO: including mount point mp0 ('/var/lib/felhom') in backup
|
||||
INFO: excluding bind mount point mp8 ('/mnt/felhom-drives') from backup (not a volume)
|
||||
INFO: excluding bind mount point mp9 ('/etc/felhom-bootstrap') from backup (not a volume)
|
||||
INFO: Starting backup: [tester-1]:ct/9201/2026-10-11T20:02:13Z
|
||||
INFO: Starting backup protocol: Sun Oct 11 22:02:14 2026
|
||||
INFO: Finished Backup of VM 9201 (00:00:58)
|
||||
TASK OK
|
||||
@@ -0,0 +1,50 @@
|
||||
# A6(3) — the PRODUCTION classifier (controller main acbbf4b = 0.310.0 source) over template dirs; nsRoot = drive A
|
||||
# DRILL templates (the layout under test):
|
||||
== /mnt/5_hdd/felhom.eu/drill/app-catalog-drill/templates/sonarr backup-block=false classified-binds=0 hasClassification=false
|
||||
tier2-secondary: hasClassification=false paths=0 skipped=0
|
||||
tier3-offsite: hasClassification=false paths=0 skipped=0
|
||||
== /mnt/5_hdd/felhom.eu/drill/app-catalog-drill/templates/radarr backup-block=false classified-binds=0 hasClassification=false
|
||||
tier2-secondary: hasClassification=false paths=0 skipped=0
|
||||
tier3-offsite: hasClassification=false paths=0 skipped=0
|
||||
== /mnt/5_hdd/felhom.eu/drill/app-catalog-drill/templates/qbittorrent backup-block=true classified-binds=0 hasClassification=true
|
||||
tier2-secondary: hasClassification=true paths=0 skipped=0
|
||||
tier3-offsite: hasClassification=true paths=0 skipped=0
|
||||
== /mnt/5_hdd/felhom.eu/drill/app-catalog-drill/templates/transmission backup-block=true classified-binds=0 hasClassification=true
|
||||
tier2-secondary: hasClassification=true paths=0 skipped=0
|
||||
tier3-offsite: hasClassification=true paths=0 skipped=0
|
||||
== /mnt/5_hdd/felhom.eu/drill/app-catalog-drill/templates/plex backup-block=false classified-binds=0 hasClassification=false
|
||||
tier2-secondary: hasClassification=false paths=0 skipped=0
|
||||
tier3-offsite: hasClassification=false paths=0 skipped=0
|
||||
== /mnt/5_hdd/felhom.eu/drill/app-catalog-drill/templates/jellyfin backup-block=false classified-binds=0 hasClassification=false
|
||||
tier2-secondary: hasClassification=false paths=0 skipped=0
|
||||
tier3-offsite: hasClassification=false paths=0 skipped=0
|
||||
|
||||
# CONTROL 1 — LIVE templates (today's shape):
|
||||
== /mnt/5_hdd/felhom.eu/git/app-catalog-felhom.eu/templates/sonarr backup-block=true classified-binds=2 hasClassification=true
|
||||
bind root=userdata rel="media/tv" ro=false class="excluded" origin=explicit
|
||||
bind root=userdata rel="downloads" ro=false class="excluded" origin=explicit
|
||||
tier2-secondary: hasClassification=true paths=0 skipped=0
|
||||
tier3-offsite: hasClassification=true paths=0 skipped=0
|
||||
== /mnt/5_hdd/felhom.eu/git/app-catalog-felhom.eu/templates/qbittorrent backup-block=true classified-binds=1 hasClassification=true
|
||||
bind root=userdata rel="media/torrents" ro=false class="optional" origin=explicit
|
||||
tier2-secondary: hasClassification=true paths=1 skipped=0
|
||||
CAPTURED /mnt/felhom-drives/media_a/userdata/media/torrents (optional)
|
||||
tier3-offsite: hasClassification=true paths=0 skipped=0
|
||||
== /mnt/5_hdd/felhom.eu/git/app-catalog-felhom.eu/templates/transmission backup-block=true classified-binds=2 hasClassification=true
|
||||
bind root=userdata rel="media/transmission" ro=false class="optional" origin=explicit
|
||||
bind root=userdata rel="media/transmission/watch" ro=false class="mandatory" origin=default_writable
|
||||
tier2-secondary: hasClassification=true paths=1 skipped=0
|
||||
CAPTURED /mnt/felhom-drives/media_a/userdata/media/transmission (optional)
|
||||
tier3-offsite: hasClassification=true paths=1 skipped=0
|
||||
CAPTURED /mnt/felhom-drives/media_a/userdata/media/transmission/watch (mandatory)
|
||||
|
||||
# CONTROL 2 (decoy, must show CAPTURED mandatory) — live sonarr with a backup block that does NOT list its writable ${USERDATA_PATH} binds:
|
||||
== /tmp/claude-1000/-mnt-5-hdd-felhom-eu-git/bf86b82e-7538-4da2-99f7-a6f96249f7fb/scratchpad/decoy/sonarr-unlisted backup-block=true classified-binds=2 hasClassification=true
|
||||
bind root=userdata rel="media/tv" ro=false class="mandatory" origin=default_writable
|
||||
bind root=userdata rel="downloads" ro=false class="mandatory" origin=default_writable
|
||||
tier2-secondary: hasClassification=true paths=2 skipped=0
|
||||
CAPTURED /mnt/felhom-drives/media_a/userdata/downloads (mandatory)
|
||||
CAPTURED /mnt/felhom-drives/media_a/userdata/media/tv (mandatory)
|
||||
tier3-offsite: hasClassification=true paths=2 skipped=0
|
||||
CAPTURED /mnt/felhom-drives/media_a/userdata/downloads (mandatory)
|
||||
CAPTURED /mnt/felhom-drives/media_a/userdata/media/tv (mandatory)
|
||||
@@ -0,0 +1,19 @@
|
||||
# Predictions — written before Part A4/A5/A6 ran (2026-10-14 ~16:00 CEST)
|
||||
|
||||
A4 enrolment
|
||||
- Both disks enrol through /api/storage/init (ext4) and mount at /mnt/felhom-drives/media_a and media_b in the guest.
|
||||
- Neither becomes the Tier-1 backup target: the target role is written ONLY by /api/storage/backup-target/assign
|
||||
(the offer's acceptance), never by registration. The target stays `local`.
|
||||
- Neither becomes the default storage: this run sends set_default=false (the wizard's checkbox is ON by default).
|
||||
|
||||
A5 skeleton and file browser
|
||||
- Both drives get felhom-data/userdata/{media/{movies,tv,music,...},downloads,...}.
|
||||
- FileBrowser's container is recreated and binds both new drives' userdata/.
|
||||
- The dashboard says nothing about apps needing a restart (no such notice exists in v0.310.0).
|
||||
|
||||
A6 backups
|
||||
- (1) demo-hp: scsi2/scsi3 carry backup=0 (read back), and demo-hp has no vzdump job for VM 341 at all.
|
||||
- (2) Tester 1's whole-guest backup (vzdump of 9201) does not include /mnt/felhom-drives: mp8 is a host bind mount,
|
||||
out of vzdump scope (07 §6.1 FACT). The new drives arrive UNDER mp8 (bound_under_parent), so no new mp line.
|
||||
- (3) A plain-host-path bind is not classified (classifyRoot knows only ${..._PATH}), so it is in no app capture set
|
||||
at any tier. An unlisted writable ${USERDATA_PATH} bind WOULD be mandatory (Tier 3 included).
|
||||
@@ -0,0 +1,37 @@
|
||||
bookstack Up 5 minutes (healthy)
|
||||
bookstack-db Up 5 minutes (healthy)
|
||||
cloudflared Up 5 minutes (healthy)
|
||||
felhom-controller Up 5 minutes (healthy)
|
||||
filebrowser Up 5 minutes (healthy)
|
||||
paperless-postgres Up 5 minutes (healthy)
|
||||
paperless-redis Up 5 minutes (healthy)
|
||||
paperless-webserver Up 5 minutes (healthy)
|
||||
privatebin Up 5 minutes (healthy)
|
||||
qbittorrent Up 4 minutes (healthy)
|
||||
radarr Up 4 minutes (healthy)
|
||||
sonarr Up 4 minutes (healthy)
|
||||
traefik Up 5 minutes
|
||||
transmission Up 4 minutes (healthy)
|
||||
TARGET SOURCE
|
||||
/mnt/felhom-drives /dev/mapper/pve-root[/mnt/felhom-drives]
|
||||
|-/mnt/felhom-drives/media_a /dev/sdc[/felhom-data]
|
||||
|-/mnt/felhom-drives/adatlemez /dev/sdb[/felhom-data]
|
||||
`-/mnt/felhom-drives/media_b /dev/sdd[/felhom-data]
|
||||
sonarr sees: 14 14
|
||||
radarr sees: 14 14
|
||||
qbittorrent sees: 14 14
|
||||
transmission sees: 14 14
|
||||
24: repo_url: file:///opt/docker/felhom-controller/drill-catalog.git
|
||||
2026/10/14 15:28:40 [INFO] [stacks] Stack paperless-ngx started successfully (took 11.8s)
|
||||
2026/10/14 15:28:40 [INFO] [gate] boot 1791991571-10166: live bind confirmed — recreating drive-backed app radarr (state=starting) onto /mnt/felhom-drives/media_a
|
||||
2026/10/14 15:28:44 [INFO] [stacks] Stack paperless-ngx post-start status:
|
||||
2026/10/14 15:28:44 [INFO] [stacks] paperless-postgres postgres:18-alpine@sha256:77f585114c32fbca283dc835b0596f4e52b51b4c6662d7810b2f4084f60a1873 running Up 14 seconds (healthy)
|
||||
2026/10/14 15:28:44 [INFO] [stacks] paperless-redis redis:7-alpine@sha256:858f009f9709ce576febc734aa78b8f6d624b82571f9ddb6bda4377c833b3499 running Up 14 seconds (healthy)
|
||||
2026/10/14 15:28:44 [INFO] [stacks] paperless-webserver ghcr.io/paperless-ngx/paperless-ngx:2.20.15@sha256:6c86cad803970ea782683a8e80e7403444c5bf3cf70de63b4d3c8e87500db92f running Up 4 seconds (health: starting)
|
||||
2026/10/14 15:28:46 [INFO] [gate] boot 1791991571-10166: live bind confirmed — recreating drive-backed app sonarr (state=starting) onto /mnt/felhom-drives/media_a
|
||||
2026/10/14 15:28:51 [INFO] [gate] boot 1791991571-10166: live bind confirmed — recreating drive-backed app transmission (state=running) onto /mnt/felhom-drives/media_a
|
||||
2026/10/14 15:28:58 [INFO] [gate] boot 1791991571-10166: live bind confirmed — recreating drive-backed app qbittorrent (state=running) onto /mnt/felhom-drives/media_a
|
||||
2026/10/14 15:29:01 [INFO] [gate] boot 1791991571-10166: re-syncing FileBrowser mounts against the live binds
|
||||
2026/10/14 15:29:01 [INFO] [stacks] ParseComposeHDDMounts: found 2 HDD mounts for /opt/docker/stacks/paperless-ngx/docker-compose.yml
|
||||
2026/10/14 15:33:14 [INFO] [stacks] ParseComposeHDDMounts: found 2 HDD mounts for /opt/docker/stacks/paperless-ngx/docker-compose.yml
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
mirror main 0ad6383 DRILL media-all-drives: six media templates bind every test drive's userdata/media at the same path (Tester 1, 2026-10-14)
|
||||
|
||||
drill repo main: 0ad6383
|
||||
@@ -0,0 +1,22 @@
|
||||
24: repo_url: file:///opt/docker/felhom-controller/drill-catalog.git
|
||||
24c24
|
||||
< repo_url: https://gitea.dooplex.hu/admin/app-catalog-felhom.eu.git
|
||||
---
|
||||
> repo_url: file:///opt/docker/felhom-controller/drill-catalog.git
|
||||
|
||||
2026/10/14 14:04:37 [INFO] [sync] Updated radarr/.felhom.yml
|
||||
2026/10/14 14:04:37 [INFO] [sync] Updated sonarr/docker-compose.yml
|
||||
2026/10/14 14:04:37 [INFO] [sync] Updated sonarr/.felhom.yml
|
||||
2026/10/14 14:04:37 [INFO] [sync] Updated transmission/docker-compose.yml
|
||||
2026/10/14 14:04:37 [INFO] [sync] Updated transmission/.felhom.yml
|
||||
2026/10/14 14:04:37 [INFO] [sync] catalog moved 8ee687e544e0 -> 0ad638315272 (0 new, 6 stack dir(s) changed)
|
||||
2026/10/14 14:04:37 [INFO] [sync] Catalog sync complete
|
||||
2026/10/14 14:04:37 [INFO] [sync] Initial sync: Sablonok frissítve — frissítve: jellyfin, plex, qbittorrent, radarr, sonarr, transmission
|
||||
|
||||
24: repo_url: file:///opt/docker/felhom-controller/drill-catalog.git
|
||||
25: sync_interval: 15m
|
||||
-rw------- 1 root root 3044 Oct 4 19:41 /var/lib/docker/volumes/felhom-controller-data/_data/controller.yaml.pre-media-2026-10-14
|
||||
mirror main 0ad6383 DRILL media-all-drives: six media templates bind every test drive's userdata/media at the same path (Tester 1, 2026-10-14)
|
||||
cache HEAD 0ad6383 DRILL media-all-drives: six media templates bind every test drive's userdata/media at the same path (Tester 1, 2026-10-14)
|
||||
file:///opt/docker/felhom-controller/drill-catalog.git
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
2026-10-14T16:05:05+02:00
|
||||
== control 1: Tester 1 cache carries the drill layout
|
||||
perl: warning: Setting locale failed.
|
||||
perl: warning: Please check that your locale settings:
|
||||
LANGUAGE = (unset),
|
||||
LC_ALL = (unset),
|
||||
LC_CTYPE = "UTF-8",
|
||||
LC_NUMERIC = (unset),
|
||||
LC_COLLATE = (unset),
|
||||
LC_TIME = (unset),
|
||||
LC_MESSAGES = (unset),
|
||||
LC_MONETARY = (unset),
|
||||
LC_ADDRESS = (unset),
|
||||
LC_IDENTIFICATION = (unset),
|
||||
LC_MEASUREMENT = (unset),
|
||||
LC_PAPER = (unset),
|
||||
LC_TELEPHONE = (unset),
|
||||
LC_NAME = (unset),
|
||||
LANG = "en_US.UTF-8"
|
||||
are supported and installed on your system.
|
||||
perl: warning: Falling back to a fallback locale ("en_US.UTF-8").
|
||||
1
|
||||
0ad6383
|
||||
== control 2: the other boxes' caches still follow the live catalog
|
||||
-- demo-hp 9201:
|
||||
8ee687e
|
||||
https://gitea.dooplex.hu/admin/app-catalog-felhom.eu.git
|
||||
0
|
||||
-- demo-hp 9202:
|
||||
8ee687e
|
||||
https://gitea.dooplex.hu/admin/app-catalog-felhom.eu.git
|
||||
0
|
||||
-- N100 (felhom-pve) guest:
|
||||
LANGUAGE = (unset),
|
||||
LANG = "en_US.UTF-8"
|
||||
guest 9201
|
||||
LANGUAGE = (unset),
|
||||
LANG = "en_US.UTF-8"
|
||||
8ee687e
|
||||
https://gitea.dooplex.hu/admin/app-catalog-felhom.eu.git
|
||||
0
|
||||
== control 3: live catalog main
|
||||
8ee687e544e0bc52980458ebf3c86e9484dba1fa refs/heads/main
|
||||
@@ -0,0 +1,38 @@
|
||||
16:05:42 qbittorrent: POST deploy {"DOMAIN": "enkicsifelhom.hu", "SUBDOMAIN": "torrent", "HDD_PATH": "/mnt/felhom-drives/media_a", "QBT_ADMIN_PASSWORD": "<generated, kept in secrets.json>"} -> 202 {"ok": true, "message": "Telepítés elindítva – az állapot a kártyán követhető"}
|
||||
16:05:57 qbittorrent: deployed, state=running, pinned={'qbittorrent': 'qbittorrentofficial/qbittorrent-nox:5.2.4-1'}
|
||||
running /var/lib/docker/volumes/qbittorrent_qbittorrent_config/_data->/config:true /mnt/felhom-drives/media_a/userdata/media->/mnt/felhom-drives/media_a/userdata/media:true /mnt/felhom-drives/media_b/userdata/media->/mnt/felhom-drives/media_b/userdata/media:true
|
||||
2026/10/14 14:05:42 [INFO] [api] Deploy requested for stack: qbittorrent
|
||||
2026/10/14 14:05:42 [INFO] [stacks] Deploying stack qbittorrent with 4 env vars: [DOMAIN, SUBDOMAIN, HDD_PATH, QBT_ADMIN_PASSWORD]
|
||||
2026/10/14 14:05:42 [INFO] Event pushed: app_deploy_started (info) [hu-only] — Alkalmazás telepítése elindult: qBittorrent
|
||||
2026/10/14 14:05:47 [INFO] [stacks] Stack qbittorrent deployed successfully (took 5.5s)
|
||||
2026/10/14 14:05:48 [INFO] Event pushed: app_deployed (info) [hu-only] — Alkalmazás telepítve: qBittorrent
|
||||
2026/10/14 14:05:53 [INFO] [stacks] after_install qbittorrent: qbittorrent [/bin/sh -c curl -s -c /tmp/fc -o /dev/null -X POST -d "username=admin&password=${QBT_ADMIN_PASSWORD}" -H 'Referer: http://127.0.0.1:8080' http://127.0.0.1:8080/api/v2/auth/login && curl -s -b /tmp/fc -o /dev/null -X POST --data-urlencode 'json={"upnp":false,"web_ui_domain_list":"${SUBDOMAIN}.${DOMAIN};qbittorrent;localhost;127.0.0.1"}' -H 'Referer: http://127.0.0.1:8080' http://127.0.0.1:8080/api/v2/app/setPreferences && curl -s -b /tmp/fc http://127.0.0.1:8080/api/v2/app/preferences | grep -q '"upnp":false' && curl -s -o /dev/null -w '%{http_code}' -H 'Host: qbittorrent:8080' http://127.0.0.1:8080/ | grep -qE '^(200|401|403)$' && echo FELHOM_AFTER_INSTALL_OK
|
||||
2026/10/14 14:05:53 [INFO] [stacks] qbittorrent: install hold OPENED by after_install — the app is reached as without a hold
|
||||
|
||||
16:06:01 transmission: POST deploy {"DOMAIN": "enkicsifelhom.hu", "SUBDOMAIN": "transmission", "HDD_PATH": "/mnt/felhom-drives/media_a", "TR_ADMIN_PASSWORD": "<generated, kept in secrets.json>"} -> 202 {"ok": true, "message": "Telepítés elindítva – az állapot a kártyán követhető"}
|
||||
16:06:21 transmission: deployed, state=running, pinned={'transmission': 'lscr.io/linuxserver/transmission:version-4.1.3-r0'}
|
||||
running /var/lib/docker/volumes/transmission_transmission_config/_data->/config:true /mnt/felhom-drives/media_a/userdata/media/transmission/watch->/watch:true /mnt/felhom-drives/media_a/userdata/media->/mnt/felhom-drives/media_a/userdata/media:true /mnt/felhom-drives/media_b/userdata/media->/mnt/felhom-drives/media_b/userdata/media:true
|
||||
2026/10/14 14:06:01 [INFO] [api] Deploy requested for stack: transmission
|
||||
2026/10/14 14:06:01 [INFO] [stacks] Deploying stack transmission with 4 env vars: [DOMAIN, SUBDOMAIN, HDD_PATH, TR_ADMIN_PASSWORD]
|
||||
2026/10/14 14:06:01 [INFO] Event pushed: app_deploy_started (info) [hu-only] — Alkalmazás telepítése elindult: Transmission
|
||||
2026/10/14 14:06:08 [INFO] [stacks] Stack transmission deployed successfully (took 6.7s)
|
||||
2026/10/14 14:06:08 [INFO] Event pushed: app_deployed (info) [hu-only] — Alkalmazás telepítve: Transmission
|
||||
|
||||
16:06:26 sonarr: POST deploy {"DOMAIN": "enkicsifelhom.hu", "SUBDOMAIN": "sonarr", "HDD_PATH": "/mnt/felhom-drives/media_a"} -> 202 {"ok": true, "data": {"warning": "Az alkalmazások csúcsterhelése meghaladhatja a rendelkezésre álló memóriát. Normál használat mellett ez nem okoz problémát."}, "message": "Telepítés elindítva – az ál
|
||||
16:07:01 sonarr: deployed, state=running, pinned={'sonarr': 'lscr.io/linuxserver/sonarr:4.0.20'}
|
||||
running /var/lib/docker/volumes/sonarr_sonarr_config/_data->/config:true /mnt/felhom-drives/media_a/userdata/media->/mnt/felhom-drives/media_a/userdata/media:true /mnt/felhom-drives/media_b/userdata/media->/mnt/felhom-drives/media_b/userdata/media:true
|
||||
2026/10/14 14:06:26 [INFO] [api] Deploy requested for stack: sonarr
|
||||
2026/10/14 14:06:26 [INFO] [stacks] Deploying stack sonarr with 3 env vars: [DOMAIN, SUBDOMAIN, HDD_PATH]
|
||||
2026/10/14 14:06:26 [INFO] Event pushed: app_deploy_started (info) [hu-only] — Alkalmazás telepítése elindult: Sonarr
|
||||
2026/10/14 14:06:38 [INFO] [stacks] Stack sonarr deployed successfully (took 12.3s)
|
||||
2026/10/14 14:06:39 [INFO] Event pushed: app_deployed (info) [hu-only] — Alkalmazás telepítve: Sonarr
|
||||
|
||||
16:07:05 radarr: POST deploy {"DOMAIN": "enkicsifelhom.hu", "SUBDOMAIN": "radarr", "HDD_PATH": "/mnt/felhom-drives/media_a"} -> 202 {"ok": true, "data": {"warning": "Az alkalmazások csúcsterhelése meghaladhatja a rendelkezésre álló memóriát. Normál használat mellett ez nem okoz problémát."}, "message": "Telepítés elindítva – az ál
|
||||
16:07:30 radarr: deployed, state=running, pinned={'radarr': 'lscr.io/linuxserver/radarr:6.4.4'}
|
||||
running /var/lib/docker/volumes/radarr_radarr_config/_data->/config:true /mnt/felhom-drives/media_a/userdata/media->/mnt/felhom-drives/media_a/userdata/media:true /mnt/felhom-drives/media_b/userdata/media->/mnt/felhom-drives/media_b/userdata/media:true
|
||||
2026/10/14 14:07:05 [INFO] [api] Deploy requested for stack: radarr
|
||||
2026/10/14 14:07:05 [INFO] [stacks] Deploying stack radarr with 3 env vars: [DOMAIN, SUBDOMAIN, HDD_PATH]
|
||||
2026/10/14 14:07:05 [INFO] Event pushed: app_deploy_started (info) [hu-only] — Alkalmazás telepítése elindult: Radarr
|
||||
2026/10/14 14:07:12 [INFO] [stacks] Stack radarr deployed successfully (took 6.6s)
|
||||
2026/10/14 14:07:12 [INFO] Event pushed: app_deployed (info) [hu-only] — Alkalmazás telepítve: Radarr
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
sonarr: env SONARR__AUTH__APIKEY=<set> -> status with that key HTTP 200; with a wrong key HTTP 401; key written to config.xml: 0
|
||||
radarr: env RADARR__AUTH__APIKEY=<set> -> status with that key HTTP 200; with a wrong key HTTP 401; key written to config.xml: 0
|
||||
0
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
== qBittorrent http://172.18.0.8:8080 (password: the deploy secret, read from the container's environment)
|
||||
POST http://172.18.0.8:8080/api/v2/auth/login -> 204 ''
|
||||
session cookie name: QBT_SID_8080
|
||||
GET http://172.18.0.8:8080/api/v2/app/preferences -> 200 {add_stopped_enabled, add_to_top_of_queue, add_trackers, add_trackers_enabled, add_trackers_from_url_enabled, add_trackers_url, add_trackers_url_list, alt_dl_limit, alt_up_limit, alternative_webui_enabled, alternative_webui_path, announce_ip, announce_port, announce_to_all_tiers, announce_to_all_trac}
|
||||
before: auto_tmm_enabled=False save_path=/mnt/felhom-drives/media_a/userdata/media/torrents temp_path_enabled=True temp_path=/mnt/felhom-drives/media_a/userdata/media/torrents/.temp
|
||||
POST http://172.18.0.8:8080/api/v2/app/setPreferences -> 200 ''
|
||||
GET http://172.18.0.8:8080/api/v2/app/preferences -> 200 {add_stopped_enabled, add_to_top_of_queue, add_trackers, add_trackers_enabled, add_trackers_from_url_enabled, add_trackers_url, add_trackers_url_list, alt_dl_limit, alt_up_limit, alternative_webui_enabled, alternative_webui_path, announce_ip, announce_port, announce_to_all_tiers, announce_to_all_trac}
|
||||
after : auto_tmm_enabled=True (a category's save path applies only in automatic mode)
|
||||
GET http://172.18.0.8:8080/api/v2/torrents/categories -> 200 {}
|
||||
POST http://172.18.0.8:8080/api/v2/torrents/createCategory -> 200 ''
|
||||
POST http://172.18.0.8:8080/api/v2/torrents/createCategory -> 200 ''
|
||||
POST http://172.18.0.8:8080/api/v2/torrents/createCategory -> 200 ''
|
||||
POST http://172.18.0.8:8080/api/v2/torrents/createCategory -> 200 ''
|
||||
GET http://172.18.0.8:8080/api/v2/torrents/categories -> 200 {movies-drive-a, movies-drive-b, tv-drive-a, tv-drive-b}
|
||||
category movies-drive-a: savePath=/mnt/felhom-drives/media_a/userdata/media/torrents/movies downloadPath=/mnt/felhom-drives/media_a/userdata/media/torrents/.temp
|
||||
category movies-drive-b: savePath=/mnt/felhom-drives/media_b/userdata/media/torrents/movies downloadPath=/mnt/felhom-drives/media_b/userdata/media/torrents/.temp
|
||||
category tv-drive-a: savePath=/mnt/felhom-drives/media_a/userdata/media/torrents/tv downloadPath=/mnt/felhom-drives/media_a/userdata/media/torrents/.temp
|
||||
category tv-drive-b: savePath=/mnt/felhom-drives/media_b/userdata/media/torrents/tv downloadPath=/mnt/felhom-drives/media_b/userdata/media/torrents/.temp
|
||||
== Transmission http://172.18.0.9:9091/transmission/rpc (password: the deploy secret, read from the container's environment)
|
||||
POST http://172.18.0.9:9091/transmission/rpc -> 409 '<h1>409: Conflict</h1><p>Your request had an invalid session_id header.</p><p>To fix this, follow these steps:<ol><li>
|
||||
POST http://172.18.0.9:9091/transmission/rpc -> 200 {arguments, result}
|
||||
before: {"download-dir": "/mnt/felhom-drives/media_a/userdata/media/transmission/complete", "incomplete-dir": "/mnt/felhom-drives/media_a/userdata/media/transmission/incomplete", "incomplete-dir-enabled": true, "rename-partial-files": true}
|
||||
POST http://172.18.0.9:9091/transmission/rpc -> 409 '<h1>409: Conflict</h1><p>Your request had an invalid session_id header.</p><p>To fix this, follow these steps:<ol><li>
|
||||
POST http://172.18.0.9:9091/transmission/rpc -> 200 {arguments, result}
|
||||
POST http://172.18.0.9:9091/transmission/rpc -> 409 '<h1>409: Conflict</h1><p>Your request had an invalid session_id header.</p><p>To fix this, follow these steps:<ol><li>
|
||||
POST http://172.18.0.9:9091/transmission/rpc -> 200 {arguments, result}
|
||||
after : {"download-dir": "/mnt/felhom-drives/media_a/userdata/media/transmission/complete", "incomplete-dir-enabled": false, "rename-partial-files": true} (one incomplete folder for all torrents would be on drive A)
|
||||
POST http://172.18.0.9:9091/transmission/rpc -> 409 '<h1>409: Conflict</h1><p>Your request had an invalid session_id header.</p><p>To fix this, follow these steps:<ol><li>
|
||||
POST http://172.18.0.9:9091/transmission/rpc -> 200 {arguments, result}
|
||||
probe Host: transmission:9091 -> HTTP 200 {'arguments': {'version': '4.1.3 (838877323f)'}, 'result': 'success'}
|
||||
== sonarr http://172.18.0.10:8989/api/v3 (key: <ApiKey> in /config/config.xml, read from the config volume)
|
||||
GET http://172.18.0.10:8989/api/v3/tag -> 200 [list of 0]
|
||||
POST http://172.18.0.10:8989/api/v3/tag -> 201 {id, label}
|
||||
POST http://172.18.0.10:8989/api/v3/tag -> 201 {id, label}
|
||||
tags: {'drive-a': 1, 'drive-b': 2}
|
||||
GET http://172.18.0.10:8989/api/v3/rootfolder -> 200 [list of 0]
|
||||
POST http://172.18.0.10:8989/api/v3/rootfolder -> 201 {accessible, freeSpace, id, path, unmappedFolders}
|
||||
POST http://172.18.0.10:8989/api/v3/rootfolder -> 201 {accessible, freeSpace, id, path, unmappedFolders}
|
||||
GET http://172.18.0.10:8989/api/v3/rootfolder -> 200 [list of 2]
|
||||
root folder: /mnt/felhom-drives/media_a/userdata/media/tv accessible=True freeSpace=99701432320
|
||||
root folder: /mnt/felhom-drives/media_b/userdata/media/tv accessible=True freeSpace=99701567488
|
||||
GET http://172.18.0.10:8989/api/v3/downloadclient/schema -> 200 [list of 18]
|
||||
GET http://172.18.0.10:8989/api/v3/downloadclient -> 200 [list of 0]
|
||||
POST http://172.18.0.10:8989/api/v3/downloadclient -> 201 {configContract, enable, fields, id, implementation, implementationName, infoLink, name, priority, protocol, removeCompletedDownloads, removeFailedDownloads, tags}
|
||||
POST http://172.18.0.10:8989/api/v3/downloadclient -> 400 [list of 1]
|
||||
client Transmission drive-a: REFUSED by sonarr's own connection test: [{"isWarning": false, "propertyName": "TvCategory", "errorMessage": "Cannot use Category and Directory", "attemptedValue": "tv-sonarr", "severity": "error"}]
|
||||
POST http://172.18.0.10:8989/api/v3/downloadclient -> 201 {configContract, enable, fields, id, implementation, implementationName, infoLink, name, priority, protocol, removeCompletedDownloads, removeFailedDownloads, tags}
|
||||
POST http://172.18.0.10:8989/api/v3/downloadclient -> 400 [list of 1]
|
||||
client Transmission drive-b: REFUSED by sonarr's own connection test: [{"isWarning": false, "propertyName": "TvCategory", "errorMessage": "Cannot use Category and Directory", "attemptedValue": "tv-sonarr", "severity": "error"}]
|
||||
GET http://172.18.0.10:8989/api/v3/downloadclient -> 200 [list of 2]
|
||||
client qBittorrent drive-a: impl=QBittorrent enable=True tags=[1] host=qbittorrent category=tv-drive-a directory=None
|
||||
client qBittorrent drive-b: impl=QBittorrent enable=True tags=[2] host=qbittorrent category=tv-drive-b directory=None
|
||||
GET http://172.18.0.10:8989/api/v3/config/mediamanagement -> 200 {autoUnmonitorPreviouslyDownloadedEpisodes, chmodFolder, chownGroup, copyUsingHardlinks, createEmptySeriesFolders, deleteEmptyFolders, downloadPropersAndRepacks, enableMediaInfo, episodeTitleRequired, extraFileExtensions, fileDate, id, importExtraFiles, minimumFreeSpaceWhenImporting, recycleBin, recy}
|
||||
before: minimumFreeSpaceWhenImporting=100 MB, copyUsingHardlinks=True
|
||||
PUT http://172.18.0.10:8989/api/v3/config/mediamanagement/1 -> 202 {autoUnmonitorPreviouslyDownloadedEpisodes, chmodFolder, chownGroup, copyUsingHardlinks, createEmptySeriesFolders, deleteEmptyFolders, downloadPropersAndRepacks, enableMediaInfo, episodeTitleRequired, extraFileExtensions, fileDate, id, importExtraFiles, minimumFreeSpaceWhenImporting, recycleBin, recy}
|
||||
GET http://172.18.0.10:8989/api/v3/config/mediamanagement -> 200 {autoUnmonitorPreviouslyDownloadedEpisodes, chmodFolder, chownGroup, copyUsingHardlinks, createEmptySeriesFolders, deleteEmptyFolders, downloadPropersAndRepacks, enableMediaInfo, episodeTitleRequired, extraFileExtensions, fileDate, id, importExtraFiles, minimumFreeSpaceWhenImporting, recycleBin, recy}
|
||||
after : minimumFreeSpaceWhenImporting=10240 MB, copyUsingHardlinks=True (default kept)
|
||||
== radarr http://172.18.0.11:7878/api/v3 (key: <ApiKey> in /config/config.xml, read from the config volume)
|
||||
GET http://172.18.0.11:7878/api/v3/tag -> 200 [list of 0]
|
||||
POST http://172.18.0.11:7878/api/v3/tag -> 201 {id, label}
|
||||
POST http://172.18.0.11:7878/api/v3/tag -> 201 {id, label}
|
||||
tags: {'drive-a': 1, 'drive-b': 2}
|
||||
GET http://172.18.0.11:7878/api/v3/rootfolder -> 200 [list of 0]
|
||||
POST http://172.18.0.11:7878/api/v3/rootfolder -> 201 {accessible, freeSpace, id, path, unmappedFolders}
|
||||
POST http://172.18.0.11:7878/api/v3/rootfolder -> 201 {accessible, freeSpace, id, path, unmappedFolders}
|
||||
GET http://172.18.0.11:7878/api/v3/rootfolder -> 200 [list of 2]
|
||||
root folder: /mnt/felhom-drives/media_a/userdata/media/movies accessible=True freeSpace=99701432320
|
||||
root folder: /mnt/felhom-drives/media_b/userdata/media/movies accessible=True freeSpace=99701567488
|
||||
GET http://172.18.0.11:7878/api/v3/downloadclient/schema -> 200 [list of 19]
|
||||
GET http://172.18.0.11:7878/api/v3/downloadclient -> 200 [list of 0]
|
||||
POST http://172.18.0.11:7878/api/v3/downloadclient -> 201 {configContract, enable, fields, id, implementation, implementationName, infoLink, name, priority, protocol, removeCompletedDownloads, removeFailedDownloads, tags}
|
||||
POST http://172.18.0.11:7878/api/v3/downloadclient -> 400 [list of 1]
|
||||
client Transmission drive-a: REFUSED by radarr's own connection test: [{"isWarning": false, "propertyName": "MovieCategory", "errorMessage": "Cannot use Category and Directory", "attemptedValue": "radarr", "severity": "error"}]
|
||||
POST http://172.18.0.11:7878/api/v3/downloadclient -> 201 {configContract, enable, fields, id, implementation, implementationName, infoLink, name, priority, protocol, removeCompletedDownloads, removeFailedDownloads, tags}
|
||||
POST http://172.18.0.11:7878/api/v3/downloadclient -> 400 [list of 1]
|
||||
client Transmission drive-b: REFUSED by radarr's own connection test: [{"isWarning": false, "propertyName": "MovieCategory", "errorMessage": "Cannot use Category and Directory", "attemptedValue": "radarr", "severity": "error"}]
|
||||
GET http://172.18.0.11:7878/api/v3/downloadclient -> 200 [list of 2]
|
||||
client qBittorrent drive-a: impl=QBittorrent enable=True tags=[1] host=qbittorrent category=movies-drive-a directory=None
|
||||
client qBittorrent drive-b: impl=QBittorrent enable=True tags=[2] host=qbittorrent category=movies-drive-b directory=None
|
||||
GET http://172.18.0.11:7878/api/v3/config/mediamanagement -> 200 {autoRenameFolders, autoUnmonitorPreviouslyDownloadedMovies, chmodFolder, chownGroup, copyUsingHardlinks, createEmptyMovieFolders, deleteEmptyFolders, downloadPropersAndRepacks, enableMediaInfo, extraFileExtensions, fileDate, id, importExtraFiles, minimumFreeSpaceWhenImporting, pathsDefaultStatic, re}
|
||||
before: minimumFreeSpaceWhenImporting=100 MB, copyUsingHardlinks=True
|
||||
PUT http://172.18.0.11:7878/api/v3/config/mediamanagement/1 -> 202 {autoRenameFolders, autoUnmonitorPreviouslyDownloadedMovies, chmodFolder, chownGroup, copyUsingHardlinks, createEmptyMovieFolders, deleteEmptyFolders, downloadPropersAndRepacks, enableMediaInfo, extraFileExtensions, fileDate, id, importExtraFiles, minimumFreeSpaceWhenImporting, pathsDefaultStatic, re}
|
||||
GET http://172.18.0.11:7878/api/v3/config/mediamanagement -> 200 {autoRenameFolders, autoUnmonitorPreviouslyDownloadedMovies, chmodFolder, chownGroup, copyUsingHardlinks, createEmptyMovieFolders, deleteEmptyFolders, downloadPropersAndRepacks, enableMediaInfo, extraFileExtensions, fileDate, id, importExtraFiles, minimumFreeSpaceWhenImporting, pathsDefaultStatic, re}
|
||||
after : minimumFreeSpaceWhenImporting=10240 MB, copyUsingHardlinks=True (default kept)
|
||||
== done; Transmission reachable by its container name: True
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
== qBittorrent http://172.18.0.8:8080 (password: the deploy secret, read from the container's environment)
|
||||
POST http://172.18.0.8:8080/api/v2/auth/login -> 204 ''
|
||||
session cookie name: QBT_SID_8080
|
||||
GET http://172.18.0.8:8080/api/v2/app/preferences -> 200 {add_stopped_enabled, add_to_top_of_queue, add_trackers, add_trackers_enabled, add_trackers_from_url_enabled, add_trackers_url, add_trackers_url_list, alt_dl_limit, alt_up_limit, alternative_webui_enabled, alternative_webui_path, announce_ip, announce_port, announce_to_all_tiers, announce_to_all_trac}
|
||||
before: auto_tmm_enabled=True save_path=/mnt/felhom-drives/media_a/userdata/media/torrents temp_path_enabled=True temp_path=/mnt/felhom-drives/media_a/userdata/media/torrents/.temp
|
||||
POST http://172.18.0.8:8080/api/v2/app/setPreferences -> 200 ''
|
||||
GET http://172.18.0.8:8080/api/v2/app/preferences -> 200 {add_stopped_enabled, add_to_top_of_queue, add_trackers, add_trackers_enabled, add_trackers_from_url_enabled, add_trackers_url, add_trackers_url_list, alt_dl_limit, alt_up_limit, alternative_webui_enabled, alternative_webui_path, announce_ip, announce_port, announce_to_all_tiers, announce_to_all_trac}
|
||||
after : auto_tmm_enabled=True (a category's save path applies only in automatic mode)
|
||||
GET http://172.18.0.8:8080/api/v2/torrents/categories -> 200 {movies-drive-a, movies-drive-b, tv-drive-a, tv-drive-b}
|
||||
POST http://172.18.0.8:8080/api/v2/torrents/editCategory -> 200 ''
|
||||
POST http://172.18.0.8:8080/api/v2/torrents/editCategory -> 200 ''
|
||||
POST http://172.18.0.8:8080/api/v2/torrents/editCategory -> 200 ''
|
||||
POST http://172.18.0.8:8080/api/v2/torrents/editCategory -> 200 ''
|
||||
GET http://172.18.0.8:8080/api/v2/torrents/categories -> 200 {movies-drive-a, movies-drive-b, tv-drive-a, tv-drive-b}
|
||||
category movies-drive-a: savePath=/mnt/felhom-drives/media_a/userdata/media/torrents/movies downloadPath=/mnt/felhom-drives/media_a/userdata/media/torrents/.temp
|
||||
category movies-drive-b: savePath=/mnt/felhom-drives/media_b/userdata/media/torrents/movies downloadPath=/mnt/felhom-drives/media_b/userdata/media/torrents/.temp
|
||||
category tv-drive-a: savePath=/mnt/felhom-drives/media_a/userdata/media/torrents/tv downloadPath=/mnt/felhom-drives/media_a/userdata/media/torrents/.temp
|
||||
category tv-drive-b: savePath=/mnt/felhom-drives/media_b/userdata/media/torrents/tv downloadPath=/mnt/felhom-drives/media_b/userdata/media/torrents/.temp
|
||||
== Transmission http://172.18.0.9:9091/transmission/rpc (password: the deploy secret, read from the container's environment)
|
||||
POST http://172.18.0.9:9091/transmission/rpc -> 409 '<h1>409: Conflict</h1><p>Your request had an invalid session_id header.</p><p>To fix this, follow these steps:<ol><li>
|
||||
POST http://172.18.0.9:9091/transmission/rpc -> 200 {arguments, result}
|
||||
before: {"download-dir": "/mnt/felhom-drives/media_a/userdata/media/transmission/complete", "incomplete-dir": "/mnt/felhom-drives/media_a/userdata/media/transmission/incomplete", "incomplete-dir-enabled": false, "rename-partial-files": true}
|
||||
POST http://172.18.0.9:9091/transmission/rpc -> 409 '<h1>409: Conflict</h1><p>Your request had an invalid session_id header.</p><p>To fix this, follow these steps:<ol><li>
|
||||
POST http://172.18.0.9:9091/transmission/rpc -> 200 {arguments, result}
|
||||
POST http://172.18.0.9:9091/transmission/rpc -> 409 '<h1>409: Conflict</h1><p>Your request had an invalid session_id header.</p><p>To fix this, follow these steps:<ol><li>
|
||||
POST http://172.18.0.9:9091/transmission/rpc -> 200 {arguments, result}
|
||||
after : {"download-dir": "/mnt/felhom-drives/media_a/userdata/media/transmission/complete", "incomplete-dir-enabled": false, "rename-partial-files": true} (one incomplete folder for all torrents would be on drive A)
|
||||
POST http://172.18.0.9:9091/transmission/rpc -> 409 '<h1>409: Conflict</h1><p>Your request had an invalid session_id header.</p><p>To fix this, follow these steps:<ol><li>
|
||||
POST http://172.18.0.9:9091/transmission/rpc -> 200 {arguments, result}
|
||||
probe Host: transmission:9091 -> HTTP 200 {'arguments': {'version': '4.1.3 (838877323f)'}, 'result': 'success'}
|
||||
== sonarr http://172.18.0.10:8989/api/v3 (key: <ApiKey> in /config/config.xml, read from the config volume)
|
||||
GET http://172.18.0.10:8989/api/v3/tag -> 200 [list of 2]
|
||||
tags: {'drive-a': 1, 'drive-b': 2}
|
||||
GET http://172.18.0.10:8989/api/v3/rootfolder -> 200 [list of 2]
|
||||
GET http://172.18.0.10:8989/api/v3/rootfolder -> 200 [list of 2]
|
||||
root folder: /mnt/felhom-drives/media_a/userdata/media/tv accessible=True freeSpace=99701432320
|
||||
root folder: /mnt/felhom-drives/media_b/userdata/media/tv accessible=True freeSpace=99701567488
|
||||
GET http://172.18.0.10:8989/api/v3/downloadclient/schema -> 200 [list of 18]
|
||||
GET http://172.18.0.10:8989/api/v3/downloadclient -> 200 [list of 2]
|
||||
client qBittorrent drive-a: exists — left as it is
|
||||
POST http://172.18.0.10:8989/api/v3/downloadclient -> 201 {configContract, enable, fields, id, implementation, implementationName, infoLink, name, priority, protocol, removeCompletedDownloads, removeFailedDownloads, tags}
|
||||
client qBittorrent drive-b: exists — left as it is
|
||||
POST http://172.18.0.10:8989/api/v3/downloadclient -> 201 {configContract, enable, fields, id, implementation, implementationName, infoLink, name, priority, protocol, removeCompletedDownloads, removeFailedDownloads, tags}
|
||||
GET http://172.18.0.10:8989/api/v3/downloadclient -> 200 [list of 4]
|
||||
client qBittorrent drive-a: impl=QBittorrent enable=True tags=[1] host=qbittorrent category=tv-drive-a directory=None
|
||||
client qBittorrent drive-b: impl=QBittorrent enable=True tags=[2] host=qbittorrent category=tv-drive-b directory=None
|
||||
client Transmission drive-a: impl=Transmission enable=True tags=[1] host=transmission category= directory=/mnt/felhom-drives/media_a/userdata/media/transmission/tv
|
||||
client Transmission drive-b: impl=Transmission enable=True tags=[2] host=transmission category= directory=/mnt/felhom-drives/media_b/userdata/media/transmission/tv
|
||||
GET http://172.18.0.10:8989/api/v3/config/mediamanagement -> 200 {autoUnmonitorPreviouslyDownloadedEpisodes, chmodFolder, chownGroup, copyUsingHardlinks, createEmptySeriesFolders, deleteEmptyFolders, downloadPropersAndRepacks, enableMediaInfo, episodeTitleRequired, extraFileExtensions, fileDate, id, importExtraFiles, minimumFreeSpaceWhenImporting, recycleBin, recy}
|
||||
before: minimumFreeSpaceWhenImporting=10240 MB, copyUsingHardlinks=True
|
||||
PUT http://172.18.0.10:8989/api/v3/config/mediamanagement/1 -> 202 {autoUnmonitorPreviouslyDownloadedEpisodes, chmodFolder, chownGroup, copyUsingHardlinks, createEmptySeriesFolders, deleteEmptyFolders, downloadPropersAndRepacks, enableMediaInfo, episodeTitleRequired, extraFileExtensions, fileDate, id, importExtraFiles, minimumFreeSpaceWhenImporting, recycleBin, recy}
|
||||
GET http://172.18.0.10:8989/api/v3/config/mediamanagement -> 200 {autoUnmonitorPreviouslyDownloadedEpisodes, chmodFolder, chownGroup, copyUsingHardlinks, createEmptySeriesFolders, deleteEmptyFolders, downloadPropersAndRepacks, enableMediaInfo, episodeTitleRequired, extraFileExtensions, fileDate, id, importExtraFiles, minimumFreeSpaceWhenImporting, recycleBin, recy}
|
||||
after : minimumFreeSpaceWhenImporting=10240 MB, copyUsingHardlinks=True (default kept)
|
||||
== radarr http://172.18.0.11:7878/api/v3 (key: <ApiKey> in /config/config.xml, read from the config volume)
|
||||
GET http://172.18.0.11:7878/api/v3/tag -> 200 [list of 2]
|
||||
tags: {'drive-a': 1, 'drive-b': 2}
|
||||
GET http://172.18.0.11:7878/api/v3/rootfolder -> 200 [list of 2]
|
||||
GET http://172.18.0.11:7878/api/v3/rootfolder -> 200 [list of 2]
|
||||
root folder: /mnt/felhom-drives/media_a/userdata/media/movies accessible=True freeSpace=99701432320
|
||||
root folder: /mnt/felhom-drives/media_b/userdata/media/movies accessible=True freeSpace=99701567488
|
||||
GET http://172.18.0.11:7878/api/v3/downloadclient/schema -> 200 [list of 19]
|
||||
GET http://172.18.0.11:7878/api/v3/downloadclient -> 200 [list of 2]
|
||||
client qBittorrent drive-a: exists — left as it is
|
||||
POST http://172.18.0.11:7878/api/v3/downloadclient -> 201 {configContract, enable, fields, id, implementation, implementationName, infoLink, name, priority, protocol, removeCompletedDownloads, removeFailedDownloads, tags}
|
||||
client qBittorrent drive-b: exists — left as it is
|
||||
POST http://172.18.0.11:7878/api/v3/downloadclient -> 201 {configContract, enable, fields, id, implementation, implementationName, infoLink, name, priority, protocol, removeCompletedDownloads, removeFailedDownloads, tags}
|
||||
GET http://172.18.0.11:7878/api/v3/downloadclient -> 200 [list of 4]
|
||||
client qBittorrent drive-a: impl=QBittorrent enable=True tags=[1] host=qbittorrent category=movies-drive-a directory=None
|
||||
client qBittorrent drive-b: impl=QBittorrent enable=True tags=[2] host=qbittorrent category=movies-drive-b directory=None
|
||||
client Transmission drive-a: impl=Transmission enable=True tags=[1] host=transmission category= directory=/mnt/felhom-drives/media_a/userdata/media/transmission/movies
|
||||
client Transmission drive-b: impl=Transmission enable=True tags=[2] host=transmission category= directory=/mnt/felhom-drives/media_b/userdata/media/transmission/movies
|
||||
GET http://172.18.0.11:7878/api/v3/config/mediamanagement -> 200 {autoRenameFolders, autoUnmonitorPreviouslyDownloadedMovies, chmodFolder, chownGroup, copyUsingHardlinks, createEmptyMovieFolders, deleteEmptyFolders, downloadPropersAndRepacks, enableMediaInfo, extraFileExtensions, fileDate, id, importExtraFiles, minimumFreeSpaceWhenImporting, pathsDefaultStatic, re}
|
||||
before: minimumFreeSpaceWhenImporting=10240 MB, copyUsingHardlinks=True
|
||||
PUT http://172.18.0.11:7878/api/v3/config/mediamanagement/1 -> 202 {autoRenameFolders, autoUnmonitorPreviouslyDownloadedMovies, chmodFolder, chownGroup, copyUsingHardlinks, createEmptyMovieFolders, deleteEmptyFolders, downloadPropersAndRepacks, enableMediaInfo, extraFileExtensions, fileDate, id, importExtraFiles, minimumFreeSpaceWhenImporting, pathsDefaultStatic, re}
|
||||
GET http://172.18.0.11:7878/api/v3/config/mediamanagement -> 200 {autoRenameFolders, autoUnmonitorPreviouslyDownloadedMovies, chmodFolder, chownGroup, copyUsingHardlinks, createEmptyMovieFolders, deleteEmptyFolders, downloadPropersAndRepacks, enableMediaInfo, extraFileExtensions, fileDate, id, importExtraFiles, minimumFreeSpaceWhenImporting, pathsDefaultStatic, re}
|
||||
after : minimumFreeSpaceWhenImporting=10240 MB, copyUsingHardlinks=True (default kept)
|
||||
== done; Transmission reachable by its container name: True
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# Media apps on every drive — the rig on the Tester 1 box (2026-10-14)
|
||||
|
||||
Task: `TASK-media-all-drives` (operator rulings 2026-10-14, `09` §3). Box: Tester 1 (VM 341 on demo-hp, guest 9201,
|
||||
`enkicsifelhom.hu`), controller 0.310.0, agent 0.156.1. Design written from this: `architecture/15-media-library.md`.
|
||||
Predictions were written before each measurement: `A-drives/PREDICTIONS-A4-A6.md`, `S-PREDICTIONS.md`.
|
||||
|
||||
| Part | Result | Evidence |
|
||||
|---|---|---|
|
||||
| A1 | Tester 1 read first: one drive `adatlemez` (default; not the backup target — that is `local`), apps bookstack, paperless-ngx, privatebin; guest 4096 MB (max 5824), host VM 8 GB; off-site on; demo-hp `/mnt/hdd_1` 764 G free of 938 G | `A-drives/A1-*.txt` |
|
||||
| A2 | Two 100 GB disks hot-added as `scsi2`/`scsi3`, `backup=0`, on `nvme-scratch` (`/mnt/hdd_1` root). Worst case (every sparse image full) leaves ≥ 391 G free = 42 %. Memory NOT raised: the six media apps' summed peaks are 1199 MB (`sizing.json`); with the box's three apps and the platform ~2.7 GB < 4096 MB | `A-drives/A2-disks-added.txt` |
|
||||
| A4 | Both enrolled through `POST /api/storage/init` (the wizard's call), ~2 s each: `media_a` (sdc), `media_b` (sdd). Neither became the default (sent `set_default:false`; **the wizard's checkbox is ON by default**) nor the backup target (`target: local` before and after) | `A-drives/A4-*.txt` |
|
||||
| A5 | Skeleton on both (`userdata/{documents,downloads,media/{movies,tv,torrents,transmission,photos,books,…},roms}`); FileBrowser recreated with both drives (`/srv/media_a`, `/srv/media_b`). The dashboard: „Aktív", „Nincs alkalmazás ezen a tárolón" — **nothing about apps that would need a restart** | `A-drives/A5-*.txt` |
|
||||
| A6 | (1) demo-hp has no backup job at all; `backup=0` read back. (2) Tester 1's whole-guest backups — local AND off-site PBS — log „excluding bind mount point mp8 ('/mnt/felhom-drives')". (3) The controller's own classifier over the test templates: no media bind in any capture set (control: the live templates DO capture — and a decoy with unlisted writable binds is captured mandatory) | `A-drives/A6-*.txt` |
|
||||
| B1 | Tester 1 points at a **local mirror** of the drill catalog (`file:///opt/docker/felhom-controller/drill-catalog.git`) — no credential on the box (R-992: a read-only token could not be minted, the 403 is quoted in `tools/repoint.py`). Three controls pass: Tester 1's cache = drill `0ad6383`; demo-hp 9201, 9202 and the N100 guest = live `8ee687e`; live `main` unchanged | `B1-*.txt` |
|
||||
| B2 | Drill commit `0ad6383`: six templates bind `/mnt/felhom-drives/{media_a,media_b}/userdata/media` at the same path (`:ro` for Plex/Jellyfin); images unchanged | `tools/make_drill.py` |
|
||||
| B4 | qBittorrent, Transmission, Sonarr, Radarr deployed through `POST /api/stacks/<app>/deploy`; plain host paths accepted, no warning | `B4-deploy.txt` |
|
||||
| C | `tools/wire.py`: per drive a qBittorrent category, Sonarr/Radarr root folder + tag + two client entries; min free space 10 GB; hardlinks on (default) | `C-wire-run*.txt` |
|
||||
| C | API key: read from `/config/config.xml`; it CAN be set at install: `SONARR__AUTH__APIKEY` / `RADARR__AUTH__APIKEY` env → 200 with that key, 401 with another | `C-apikey-at-install.txt` |
|
||||
| S1 | Today's two-bind shape: `ln: … Cross-device link` (EXDEV) on one filesystem → copy (2 inodes). One bind per drive: link count 2, one inode | `S1-hardlink.txt` |
|
||||
| S2 | One public-domain film (archive.org, CC PD Mark) on drive B: through Transmission and through qBittorrent → **hardlink** in `media_b/…/movies`. archive.org's web seed failed the last piece for qBittorrent; a local peer (the same torrent in Transmission) finished it — recorded | `S2-*.txt` |
|
||||
| S3 | Tag drive-a, no client forced → „Transmission drive-a", hardlink on A. Mismatch (root B, tag A) → download on A, **copy** on B (link count 1 each) | `S3-*.txt` |
|
||||
| — | **17:25 CEST: demo-hp was power-cut by another session's Immich drill** (`plugcut.py --plug hp`) — Tester 1 went down with it and came back by itself; the boot gate recreated the four media apps onto drive A; all test films then removed (0 files left) | `AFTER-power-cut-1725.txt` |
|
||||
@@ -0,0 +1,14 @@
|
||||
# Predictions — written before S2/S3 ran (2026-10-14 ~16:30 CEST)
|
||||
|
||||
S2 (root B, tag drive-b; once through qBittorrent drive-b, once through Transmission drive-b)
|
||||
- Radarr approves the pushed release, sends it to the one enabled drive-b client.
|
||||
- qBittorrent: the torrent lands in media_b/userdata/media/torrents/movies/<item>/ (category save path, automatic mode);
|
||||
incomplete pieces in media_b/.../torrents/.temp. Transmission: in media_b/.../transmission/movies/<item>/.
|
||||
- Radarr imports by HARDLINK into media_b/userdata/media/movies/Night of the Living Dead (1968)/: link count 2, same inode.
|
||||
No remote path mapping is needed, because every container sees the same path.
|
||||
|
||||
S3
|
||||
- tag routing: root A + tag drive-a, no client forced → one of the drive-a clients takes it (never a drive-b client);
|
||||
download on A, hardlink in A's movies.
|
||||
- the mismatch: root B + tag drive-a → download on A, import into B as a COPY (link count 1, two inodes); the A file
|
||||
keeps seeding. Radarr does not refuse it.
|
||||
@@ -0,0 +1,30 @@
|
||||
### PREDICTION: old shape (two binds) -> ln fails EXDEV, mv falls back to copy (link count 1, two inodes); new shape (one bind per drive) -> hardlink, link count 2, same inode
|
||||
### OLD SHAPE (live templates): -v A/media/movies:/media/movies -v A/downloads:/downloads
|
||||
ln-exit=1
|
||||
copied
|
||||
/downloads/s1-probe/film.mkv links=1 inode=4194328 dev=2080
|
||||
/media/movies/s1-probe/film.mkv links=1 inode=4194329 dev=2080
|
||||
### guest view after OLD shape:
|
||||
/mnt/felhom-drives/media_a/userdata/downloads/s1-probe/film.mkv links=1 inode=4194328
|
||||
/mnt/felhom-drives/media_a/userdata/media/movies/s1-probe/film.mkv links=1 inode=4194329
|
||||
### NEW SHAPE: -v A/media:A/media (same path)
|
||||
ln-exit=0
|
||||
/mnt/felhom-drives/media_a/userdata/media/torrents/s1-probe/film.mkv links=2 inode=4194328 dev=2080
|
||||
/mnt/felhom-drives/media_a/userdata/media/movies/s1-probe/film.mkv links=2 inode=4194328 dev=2080
|
||||
### guest view after NEW shape:
|
||||
/mnt/felhom-drives/media_a/userdata/media/torrents/s1-probe/film.mkv links=2 inode=4194328
|
||||
/mnt/felhom-drives/media_a/userdata/media/movies/s1-probe/film.mkv links=2 inode=4194328
|
||||
### cleanup
|
||||
/mnt/felhom-drives/media_a/userdata/downloads:
|
||||
|
||||
/mnt/felhom-drives/media_a/userdata/media/movies:
|
||||
|
||||
/mnt/felhom-drives/media_a/userdata/media/torrents:
|
||||
|
||||
ln: failed to create hard link '/media/movies/f' => '/downloads/s1-probe/f': Cross-device link
|
||||
mv-exit=0
|
||||
/media/movies/f-moved links=1 inode=4194328
|
||||
/mnt/felhom-drives/media_a/userdata/downloads:
|
||||
|
||||
/mnt/felhom-drives/media_a/userdata/media/movies:
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
# ATTEMPT 1 (16:13–16:27 CEST) — stopped by CC. Radarr approved and grabbed (qBittorrent drive-b took it: category
|
||||
# movies-drive-b, save path media_b/.../torrents/movies, incomplete path media_b/.../torrents/.temp — read from
|
||||
# qBittorrent's own torrents/info). The download stalled at 6.09 % (32 MiB): 0 peers, and archive.org's web seed
|
||||
# answered "500 Internal Server Error" (qBittorrent log, URL https://dn721601.ca.archive.org/); stop/start did not
|
||||
# revive it. A plain curl range request to the same file from the same container answered 206 at the same time.
|
||||
# So the source, not the chain, stalled. The run was stopped and retried (below).
|
||||
# ATTEMPT 2 (16:38–17:09 CEST), item night-of-the-living-dead-1968_202508:
|
||||
=== s2-qbit: root=/mnt/felhom-drives/media_b/userdata/media/movies tag=drive-b client=qBittorrent drive-b item=archive.org/night-of-the-living-dead-1968_202508
|
||||
clients: [('qBittorrent drive-a', True, [1]), ('qBittorrent drive-b', True, [2]), ('Transmission drive-a', True, [1]), ('Transmission drive-b', False, [2])]
|
||||
movie added: id=3 'Night of the Living Dead' (1968) path=/mnt/felhom-drives/media_b/userdata/media/movies/Night of the Living Dead (1968) tags=[2] profile=Any
|
||||
release push -> 200 approved=True rejections=[] downloadAllowed=True
|
||||
+11s grabbed: downloadClient=qBittorrent downloadClientName=qBittorrent drive-b downloadId=28766E9244F6920800B29298E9BBF00E41185CBA
|
||||
+61s queue: status=warning trackedState=downloading left=596675329 client=qBittorrent drive-b output=None msgs=[]
|
||||
+121s queue: status=downloading trackedState=downloading left=35585 client=qBittorrent drive-b output=None msgs=[]
|
||||
+181s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-b output=None msgs=[]
|
||||
+241s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-b output=None msgs=[]
|
||||
+302s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-b output=None msgs=[]
|
||||
+362s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-b output=None msgs=[]
|
||||
+422s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-b output=None msgs=[]
|
||||
+482s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-b output=None msgs=[]
|
||||
+542s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-b output=None msgs=[]
|
||||
+602s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-b output=None msgs=[]
|
||||
+662s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-b output=None msgs=[]
|
||||
+722s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-b output=None msgs=[]
|
||||
+782s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-b output=None msgs=[]
|
||||
+842s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-b output=None msgs=[]
|
||||
+902s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-b output=None msgs=[]
|
||||
+962s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-b output=None msgs=[]
|
||||
+1022s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-b output=None msgs=[]
|
||||
+1082s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-b output=None msgs=[]
|
||||
+1142s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-b output=None msgs=[]
|
||||
+1202s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-b output=None msgs=[]
|
||||
+1262s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-b output=None msgs=[]
|
||||
+1322s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-b output=None msgs=[]
|
||||
+1382s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-b output=None msgs=[]
|
||||
+1442s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-b output=None msgs=[]
|
||||
+1502s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-b output=None msgs=[]
|
||||
+1562s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-b output=None msgs=[]
|
||||
+1622s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-b output=None msgs=[]
|
||||
+1682s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-b output=None msgs=[]
|
||||
+1742s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-b output=None msgs=[]
|
||||
+1802s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-b output=None msgs=[]
|
||||
+1862s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-b output=None msgs=[]
|
||||
+1872s history downloadFolderImported: {"downloadClient": "qBittorrent", "downloadClientName": "qBittorrent drive-b", "droppedPath": "/mnt/felhom-drives/media_b/userdata/media/torrents/movies/night-of-the-living-dead-1968_202508/Night of the Living Dead 1968.mp4", "importedPath": "/mnt/felhom-drives/media_b/userdata/media/movies/Night of the Living Dead (1968)/night-of-the-living-dead-1968_202508.mp4", "size": "596645320"}
|
||||
download file: /mnt/felhom-drives/media_b/userdata/media/torrents/movies/night-of-the-living-dead-1968_202508/Night of the Living Dead 1968.mp4 links=2 inode=131097 size=596645320
|
||||
library file : /mnt/felhom-drives/media_b/userdata/media/movies/Night of the Living Dead (1968)/night-of-the-living-dead-1968_202508.mp4 links=2 inode=131097 size=596645320
|
||||
RESULT s2-qbit: HARDLINK (same inode); download on media_b, library on media_b
|
||||
[finished]
|
||||
|
||||
# NOTE on attempt 2: qBittorrent reached 99.994 % at ~16:41 (7.7 MB/s from archive.org's web seed) and then stalled on
|
||||
# the LAST piece (the film's tail + archive.org's three side files: __ia_thumb.jpg, _meta.sqlite, _meta.xml). Its log
|
||||
# shows "Received error message from URL seed ... dn721505.ca.archive.org ... 500 Internal Server Error"; 8 stop/start
|
||||
# retries over 6 min did not help, while curl range requests for the same three files from DooPlex answered 206. To
|
||||
# finish, CC added the SAME public-domain torrent to Transmission (download dir media_b/.../transmission/s2-helper) and
|
||||
# gave qBittorrent Transmission's container address as a peer (POST /api/v2/torrents/addPeers 172.18.0.9:51413) at
|
||||
# 17:03; qBittorrent completed within 40 s and Radarr imported. The chain itself (grab by client, category save path,
|
||||
# move from .temp, hardlink import) is unaffected; the source's web seed was the fault.
|
||||
@@ -0,0 +1,16 @@
|
||||
=== s2-trans: root=/mnt/felhom-drives/media_b/userdata/media/movies tag=drive-b client=Transmission drive-b item=archive.org/night-of-the-living-dead-1968_202508
|
||||
clients: [('qBittorrent drive-a', True, [1]), ('qBittorrent drive-b', False, [2]), ('Transmission drive-a', True, [1]), ('Transmission drive-b', True, [2])]
|
||||
movie added: id=2 'Night of the Living Dead' (1968) path=/mnt/felhom-drives/media_b/userdata/media/movies/Night of the Living Dead (1968) tags=[2] profile=Any
|
||||
release push -> 200 approved=True rejections=[] downloadAllowed=True
|
||||
+13s grabbed: downloadClient=Transmission downloadClientName=Transmission drive-b downloadId=28766E9244F6920800B29298E9BBF00E41185CBA
|
||||
+63s queue: status=downloading trackedState=downloading left=595626753 client=Transmission drive-b output=/mnt/felhom-drives/media_b/userdata/media/transmission/movies/night-of-the-living-dead-1968_202508 msgs=[]
|
||||
+123s queue: status=downloading trackedState=downloading left=328141569 client=Transmission drive-b output=/mnt/felhom-drives/media_b/userdata/media/transmission/movies/night-of-the-living-dead-1968_202508 msgs=[]
|
||||
+183s queue: status=downloading trackedState=downloading left=50383617 client=Transmission drive-b output=/mnt/felhom-drives/media_b/userdata/media/transmission/movies/night-of-the-living-dead-1968_202508 msgs=[]
|
||||
+243s queue: status=downloading trackedState=downloading left=50383617 client=Transmission drive-b output=/mnt/felhom-drives/media_b/userdata/media/transmission/movies/night-of-the-living-dead-1968_202508 msgs=[]
|
||||
+303s queue: status=downloading trackedState=downloading left=35585 client=Transmission drive-b output=/mnt/felhom-drives/media_b/userdata/media/transmission/movies/night-of-the-living-dead-1968_202508 msgs=[]
|
||||
+353s history downloadFolderImported: {"downloadClient": "Transmission", "downloadClientName": "Transmission drive-b", "droppedPath": "/mnt/felhom-drives/media_b/userdata/media/transmission/movies/night-of-the-living-dead-1968_202508/Night of the Living Dead 1968.mp4", "importedPath": "/mnt/felhom-drives/media_b/userdata/media/movies/Night of the Living Dead (1968)/night-of-the-living-dead-1968_202508.mp4", "size": "596645320"}
|
||||
download file: /mnt/felhom-drives/media_b/userdata/media/transmission/movies/night-of-the-living-dead-1968_202508/Night of the Living Dead 1968.mp4 links=2 inode=131097 size=596645320
|
||||
library file : /mnt/felhom-drives/media_b/userdata/media/movies/Night of the Living Dead (1968)/night-of-the-living-dead-1968_202508.mp4 links=2 inode=131097 size=596645320
|
||||
RESULT s2-trans: HARDLINK (same inode); download on media_b, library on media_b
|
||||
[finished]
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
== drive adatlemez (/mnt/felhom-drives/adatlemez/userdata/media)
|
||||
== drive media_a (/mnt/felhom-drives/media_a/userdata/media)
|
||||
== drive media_b (/mnt/felhom-drives/media_b/userdata/media)
|
||||
-- movies/ (1 file(s))
|
||||
596.6 MB links=2 inode=131097 movies/Night of the Living Dead (1968)/night-of-the-living-dead-1968_202508.mp4
|
||||
-- torrents/ (2 file(s))
|
||||
0.0 MB links=1 inode=131104 torrents/movies/.28766e9244f6920800b29298e9bbf00e41185cba.parts
|
||||
596.6 MB links=2 inode=131097 torrents/movies/night-of-the-living-dead-1968_202508/Night of the Living Dead 1968.mp4
|
||||
-- transmission/ (4 file(s))
|
||||
596.6 MB links=1 inode=131100 transmission/s2-helper/night-of-the-living-dead-1968_202508/Night of the Living Dead 1968.mp4
|
||||
0.0 MB links=1 inode=131101 transmission/s2-helper/night-of-the-living-dead-1968_202508/__ia_thumb.jpg
|
||||
0.0 MB links=1 inode=131102 transmission/s2-helper/night-of-the-living-dead-1968_202508/night-of-the-living-dead-1968_202508_meta.sqlite
|
||||
0.0 MB links=1 inode=131103 transmission/s2-helper/night-of-the-living-dead-1968_202508/night-of-the-living-dead-1968_202508_meta.xml
|
||||
|
||||
== 7 file(s) on 3 drive(s)
|
||||
== ONE FILE, TWO NAMES (hardlink — no extra copy):
|
||||
drive media_b inode 131097 (596.6 MB): movies/…/night-of-the-living-dead-1968_202508.mp4 <-> torrents/…/Night of the Living Dead 1968.mp4
|
||||
1 pair(s)
|
||||
== LIBRARY FILES WITH LINK COUNT 1 (a copy, or its download is gone):
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
== drive adatlemez (/mnt/felhom-drives/adatlemez/userdata/media)
|
||||
== drive media_a (/mnt/felhom-drives/media_a/userdata/media)
|
||||
== drive media_b (/mnt/felhom-drives/media_b/userdata/media)
|
||||
-- movies/ (1 file(s))
|
||||
596.6 MB links=2 inode=131097 movies/Night of the Living Dead (1968)/night-of-the-living-dead-1968_202508.mp4
|
||||
-- transmission/ (4 file(s))
|
||||
596.6 MB links=2 inode=131097 transmission/movies/night-of-the-living-dead-1968_202508/Night of the Living Dead 1968.mp4
|
||||
0.0 MB links=1 inode=131098 transmission/movies/night-of-the-living-dead-1968_202508/__ia_thumb.jpg
|
||||
0.0 MB links=1 inode=131099 transmission/movies/night-of-the-living-dead-1968_202508/night-of-the-living-dead-1968_202508_meta.sqlite
|
||||
0.0 MB links=1 inode=131100 transmission/movies/night-of-the-living-dead-1968_202508/night-of-the-living-dead-1968_202508_meta.xml
|
||||
|
||||
== 5 file(s) on 3 drive(s)
|
||||
== ONE FILE, TWO NAMES (hardlink — no extra copy):
|
||||
drive media_b inode 131097 (596.6 MB): movies/…/night-of-the-living-dead-1968_202508.mp4 <-> transmission/…/Night of the Living Dead 1968.mp4
|
||||
1 pair(s)
|
||||
== LIBRARY FILES WITH LINK COUNT 1 (a copy, or its download is gone):
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
=== s3-mis: root=/mnt/felhom-drives/media_b/userdata/media/movies tag=drive-a client=(by tag) item=archive.org/night-of-the-living-dead-1968_202508
|
||||
clients: [('qBittorrent drive-a', True, [1]), ('qBittorrent drive-b', True, [2]), ('Transmission drive-a', True, [1]), ('Transmission drive-b', True, [2])]
|
||||
movie added: id=5 'Night of the Living Dead' (1968) path=/mnt/felhom-drives/media_b/userdata/media/movies/Night of the Living Dead (1968) tags=[1] profile=Any
|
||||
release push -> 200 approved=True rejections=[] downloadAllowed=True
|
||||
+14s grabbed: downloadClient=qBittorrent downloadClientName=qBittorrent drive-a downloadId=28766E9244F6920800B29298E9BBF00E41185CBA
|
||||
+64s queue: status=downloading trackedState=downloading left=595528449 client=qBittorrent drive-a output=None msgs=[]
|
||||
+124s queue: status=downloading trackedState=downloading left=35585 client=qBittorrent drive-a output=None msgs=[]
|
||||
+185s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-a output=None msgs=[]
|
||||
+245s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-a output=None msgs=[]
|
||||
+305s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-a output=None msgs=[]
|
||||
+365s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-a output=None msgs=[]
|
||||
+425s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-a output=None msgs=[]
|
||||
+485s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-a output=None msgs=[]
|
||||
+545s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-a output=None msgs=[]
|
||||
+605s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-a output=None msgs=[]
|
||||
+665s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-a output=None msgs=[]
|
||||
+725s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-a output=None msgs=[]
|
||||
+785s queue: status=warning trackedState=downloading left=35585 client=qBittorrent drive-a output=None msgs=[]
|
||||
+805s history downloadFolderImported: {"downloadClient": "qBittorrent", "downloadClientName": "qBittorrent drive-a", "droppedPath": "/mnt/felhom-drives/media_a/userdata/media/torrents/movies/night-of-the-living-dead-1968_202508/Night of the Living Dead 1968.mp4", "importedPath": "/mnt/felhom-drives/media_b/userdata/media/movies/Night of the Living Dead (1968)/night-of-the-living-dead-1968_202508.mp4", "size": "596645320"}
|
||||
download file: /mnt/felhom-drives/media_a/userdata/media/torrents/movies/night-of-the-living-dead-1968_202508/Night of the Living Dead 1968.mp4 links=1 inode=4194355 size=596645320
|
||||
library file : /mnt/felhom-drives/media_b/userdata/media/movies/Night of the Living Dead (1968)/night-of-the-living-dead-1968_202508.mp4 links=1 inode=131097 size=596645320
|
||||
RESULT s3-mis: COPY (two inodes); download on media_a, library on media_b
|
||||
[finished]
|
||||
|
||||
# NOTE: qBittorrent drive-a stalled on the LAST piece again (archive.org web seed, as in S2 attempt 2). At 17:21 CC added
|
||||
# the same public-domain torrent to Transmission (media_a/.../transmission/s3-helper) and gave qBittorrent its address
|
||||
# as a peer; qBittorrent completed at 17:23 and Radarr imported. Routing and import are unaffected by the helper.
|
||||
# Radarr's own log around the import:
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
=== s3-tag: root=/mnt/felhom-drives/media_a/userdata/media/movies tag=drive-a client=(by tag) item=archive.org/night-of-the-living-dead-1968_202508
|
||||
clients: [('qBittorrent drive-a', True, [1]), ('qBittorrent drive-b', True, [2]), ('Transmission drive-a', True, [1]), ('Transmission drive-b', True, [2])]
|
||||
movie added: id=4 'Night of the Living Dead' (1968) path=/mnt/felhom-drives/media_a/userdata/media/movies/Night of the Living Dead (1968) tags=[1] profile=Any
|
||||
release push -> 200 approved=True rejections=[] downloadAllowed=True
|
||||
+11s grabbed: downloadClient=Transmission downloadClientName=Transmission drive-a downloadId=28766E9244F6920800B29298E9BBF00E41185CBA
|
||||
+61s queue: status=downloading trackedState=downloading left=587385601 client=Transmission drive-a output=/mnt/felhom-drives/media_a/userdata/media/transmission/movies/night-of-the-living-dead-1968_202508 msgs=[]
|
||||
+122s queue: status=downloading trackedState=downloading left=309381889 client=Transmission drive-a output=/mnt/felhom-drives/media_a/userdata/media/transmission/movies/night-of-the-living-dead-1968_202508 msgs=[]
|
||||
+182s queue: status=downloading trackedState=downloading left=52677377 client=Transmission drive-a output=/mnt/felhom-drives/media_a/userdata/media/transmission/movies/night-of-the-living-dead-1968_202508 msgs=[]
|
||||
+242s queue: status=downloading trackedState=downloading left=52677377 client=Transmission drive-a output=/mnt/felhom-drives/media_a/userdata/media/transmission/movies/night-of-the-living-dead-1968_202508 msgs=[]
|
||||
+302s queue: status=downloading trackedState=downloading left=35585 client=Transmission drive-a output=/mnt/felhom-drives/media_a/userdata/media/transmission/movies/night-of-the-living-dead-1968_202508 msgs=[]
|
||||
+362s history downloadFolderImported: {"downloadClient": "Transmission", "downloadClientName": "Transmission drive-a", "droppedPath": "/mnt/felhom-drives/media_a/userdata/media/transmission/movies/night-of-the-living-dead-1968_202508/Night of the Living Dead 1968.mp4", "importedPath": "/mnt/felhom-drives/media_a/userdata/media/movies/Night of the Living Dead (1968)/night-of-the-living-dead-1968_202508.mp4", "size": "596645320"}
|
||||
download file: /mnt/felhom-drives/media_a/userdata/media/transmission/movies/night-of-the-living-dead-1968_202508/Night of the Living Dead 1968.mp4 links=2 inode=4194354 size=596645320
|
||||
library file : /mnt/felhom-drives/media_a/userdata/media/movies/Night of the Living Dead (1968)/night-of-the-living-dead-1968_202508.mp4 links=2 inode=4194354 size=596645320
|
||||
RESULT s3-tag: HARDLINK (same inode); download on media_a, library on media_a
|
||||
[finished]
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
== drive adatlemez (/mnt/felhom-drives/adatlemez/userdata/media)
|
||||
== drive media_a (/mnt/felhom-drives/media_a/userdata/media)
|
||||
-- torrents/ (4 file(s))
|
||||
596.6 MB links=1 inode=4194355 torrents/movies/night-of-the-living-dead-1968_202508/Night of the Living Dead 1968.mp4
|
||||
0.0 MB links=1 inode=4194364 torrents/movies/night-of-the-living-dead-1968_202508/night-of-the-living-dead-1968_202508_meta.xml
|
||||
0.0 MB links=1 inode=4194362 torrents/movies/night-of-the-living-dead-1968_202508/__ia_thumb.jpg
|
||||
0.0 MB links=1 inode=4194363 torrents/movies/night-of-the-living-dead-1968_202508/night-of-the-living-dead-1968_202508_meta.sqlite
|
||||
-- transmission/ (4 file(s))
|
||||
596.6 MB links=1 inode=4194358 transmission/s3-helper/night-of-the-living-dead-1968_202508/Night of the Living Dead 1968.mp4
|
||||
0.0 MB links=1 inode=4194361 transmission/s3-helper/night-of-the-living-dead-1968_202508/night-of-the-living-dead-1968_202508_meta.xml
|
||||
0.0 MB links=1 inode=4194359 transmission/s3-helper/night-of-the-living-dead-1968_202508/__ia_thumb.jpg
|
||||
0.0 MB links=1 inode=4194360 transmission/s3-helper/night-of-the-living-dead-1968_202508/night-of-the-living-dead-1968_202508_meta.sqlite
|
||||
== drive media_b (/mnt/felhom-drives/media_b/userdata/media)
|
||||
-- movies/ (1 file(s))
|
||||
596.6 MB links=1 inode=131097 movies/Night of the Living Dead (1968)/night-of-the-living-dead-1968_202508.mp4
|
||||
|
||||
== 9 file(s) on 3 drive(s)
|
||||
== ONE FILE, TWO NAMES (hardlink — no extra copy):
|
||||
0 pair(s)
|
||||
== LIBRARY FILES WITH LINK COUNT 1 (a copy, or its download is gone):
|
||||
drive media_b: media_b/userdata/media/movies/Night of the Living Dead (1968)/night-of-the-living-dead-1968_202508.mp4 (596.6 MB)
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
#!/usr/bin/env python3
|
||||
"""chain.py <run> — S2/S3: one public-domain film through the WHOLE path, run INSIDE the guest (run_in_guest.py).
|
||||
|
||||
Radarr (movie added with a root folder + tag) -> release push API -> download client (picked by the tag; for S2 the
|
||||
other same-tag client is switched off for the grab so the named one is used) -> download on a drive -> Radarr's
|
||||
completed-download import -> library file. Then the evidence: download path, library path, link counts, inodes,
|
||||
Radarr's history entry.
|
||||
|
||||
Content: „Night of the Living Dead" (1968), public domain (archive.org items carry the CC Public Domain Mark), < 1 GB.
|
||||
|
||||
runs:
|
||||
s2-qbit root B, tag drive-b, only „qBittorrent drive-b" enabled among the drive-b clients
|
||||
s2-trans root B, tag drive-b, only „Transmission drive-b" enabled among the drive-b clients
|
||||
s3-tag root A, tag drive-a, both drive-a clients enabled (the tag alone picks)
|
||||
s3-mis root B, tag drive-a (the mismatch)
|
||||
cleanup delete the movie with its files in Radarr, remove every torrent with its data in both clients
|
||||
Every key/password is read at runtime and redacted from the output.
|
||||
"""
|
||||
import base64, datetime, json, os, re, subprocess, sys, time, urllib.error, urllib.request
|
||||
|
||||
SECRETS = []
|
||||
TMDB = 10331
|
||||
ITEMS = {"s2-qbit": "night-of-the-living-dead-1968_202508", "s2-trans": "night-of-the-living-dead-1968_202508",
|
||||
"s3-tag": "night-of-the-living-dead-1968_202508", "s3-mis": "night-of-the-living-dead-1968_202508"}
|
||||
RUNS = {"s2-qbit": ("media_b", "drive-b", "qBittorrent drive-b"), "s2-trans": ("media_b", "drive-b", "Transmission drive-b"),
|
||||
"s3-tag": ("media_a", "drive-a", None), "s3-mis": ("media_b", "drive-a", None)}
|
||||
|
||||
|
||||
def red(s):
|
||||
s = str(s)
|
||||
for x in SECRETS:
|
||||
if x:
|
||||
s = s.replace(x, "<redacted>")
|
||||
return s
|
||||
|
||||
|
||||
def say(*a):
|
||||
print(red(" ".join(str(x) for x in a)), flush=True)
|
||||
|
||||
|
||||
def sh(c):
|
||||
return subprocess.run(c, shell=True, capture_output=True, text=True).stdout.strip()
|
||||
|
||||
|
||||
def ip(c):
|
||||
return sh(f"docker inspect {c} -f '{{{{range .NetworkSettings.Networks}}}}{{{{.IPAddress}}}} {{{{end}}}}'").split()[0]
|
||||
|
||||
|
||||
def call(method, url, headers=None, data=None, form=False):
|
||||
h = dict(headers or {})
|
||||
if data is not None:
|
||||
if form:
|
||||
data = "&".join(f"{k}={urllib.request.quote(str(v))}" for k, v in data.items())
|
||||
h["Content-Type"] = "application/x-www-form-urlencoded"
|
||||
else:
|
||||
data = json.dumps(data)
|
||||
h["Content-Type"] = "application/json"
|
||||
data = data.encode()
|
||||
req = urllib.request.Request(url, data=data, headers=h, method=method)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=120) as r:
|
||||
code, txt, hd = r.status, r.read().decode("utf-8", "replace"), {k.lower(): v for k, v in r.headers.items()}
|
||||
except urllib.error.HTTPError as e:
|
||||
code, txt, hd = e.code, e.read().decode("utf-8", "replace"), {k.lower(): v for k, v in e.headers.items()}
|
||||
try:
|
||||
body = json.loads(txt) if txt else None
|
||||
except ValueError:
|
||||
body = txt
|
||||
return code, body, hd
|
||||
|
||||
|
||||
key = re.search(r"<ApiKey>([^<]+)</ApiKey>", open("/var/lib/docker/volumes/radarr_radarr_config/_data/config.xml").read()).group(1)
|
||||
SECRETS.append(key)
|
||||
RA = f"http://{ip('radarr')}:7878/api/v3"
|
||||
H = {"X-Api-Key": key}
|
||||
|
||||
|
||||
def ra(method, path, data=None):
|
||||
c, b, _ = call(method, RA + path, H, data)
|
||||
if c >= 300:
|
||||
say(f" !! radarr {method} {path} -> {c} {red(json.dumps(b))[:500]}")
|
||||
return c, b
|
||||
|
||||
|
||||
def movie():
|
||||
c, ms = ra("GET", "/movie")
|
||||
return next((m for m in ms if m.get("tmdbId") == TMDB), None)
|
||||
|
||||
|
||||
def clients():
|
||||
return ra("GET", "/downloadclient")[1]
|
||||
|
||||
|
||||
def set_enable(cl, on):
|
||||
if cl["enable"] != on:
|
||||
cl = dict(cl, enable=on)
|
||||
ra("PUT", f"/downloadclient/{cl['id']}", cl)
|
||||
|
||||
|
||||
def tag_id(label):
|
||||
return next(t["id"] for t in ra("GET", "/tag")[1] if t["label"] == label)
|
||||
|
||||
|
||||
def st(p):
|
||||
try:
|
||||
s = os.stat(p)
|
||||
return f"links={s.st_nlink} inode={s.st_ino} size={s.st_size}"
|
||||
except OSError as e:
|
||||
return f"stat failed: {e}"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- client side (for cleanup and the download path)
|
||||
def qb():
|
||||
QB = f"http://{ip('qbittorrent')}:8080"
|
||||
pw = sh("docker exec qbittorrent printenv QBT_ADMIN_PASSWORD"); SECRETS.append(pw)
|
||||
c, _, hd = call("POST", QB + "/api/v2/auth/login", {"Referer": QB}, {"username": "admin", "password": pw}, form=True)
|
||||
m = re.search(r"((?:QBT_)?SID[A-Za-z0-9_]*)=([^;]+)", hd.get("set-cookie", "")); SECRETS.append(m.group(2))
|
||||
return QB, {"Referer": QB, "Cookie": f"{m.group(1)}={m.group(2)}"}
|
||||
|
||||
|
||||
def tr(method, args=None):
|
||||
TR = f"http://{ip('transmission')}:9091/transmission/rpc"
|
||||
pw = sh("docker exec transmission printenv PASS"); SECRETS.append(pw)
|
||||
h = {"Authorization": "Basic " + base64.b64encode(f"admin:{pw}".encode()).decode()}
|
||||
SECRETS.append(h["Authorization"].split()[1])
|
||||
c, b, hd = call("POST", TR, h, {"method": method, "arguments": args or {}})
|
||||
if c == 409:
|
||||
h["X-Transmission-Session-Id"] = hd.get("x-transmission-session-id", "")
|
||||
c, b, hd = call("POST", TR, h, {"method": method, "arguments": args or {}})
|
||||
return b
|
||||
|
||||
|
||||
def cleanup():
|
||||
m = movie()
|
||||
if m:
|
||||
ra("DELETE", f"/movie/{m['id']}?deleteFiles=true&addImportExclusion=false")
|
||||
say(f"radarr: movie {m['id']} deleted with its files")
|
||||
QB, QH = qb()
|
||||
c, ts, _ = call("GET", QB + "/api/v2/torrents/info", QH)
|
||||
for t in ts or []:
|
||||
call("POST", QB + "/api/v2/torrents/delete", QH, {"hashes": t["hash"], "deleteFiles": "true"}, form=True)
|
||||
say(f"qbittorrent: removed {t['name']} with its data ({t['content_path']})")
|
||||
b = tr("torrent-get", {"fields": ["id", "name", "downloadDir"]})
|
||||
for t in (b or {}).get("arguments", {}).get("torrents", []):
|
||||
tr("torrent-remove", {"ids": [t["id"]], "delete-local-data": True})
|
||||
say(f"transmission: removed {t['name']} with its data ({t['downloadDir']})")
|
||||
for c in ra("GET", "/downloadclient")[1]:
|
||||
set_enable(c, True)
|
||||
say("all download clients enabled again")
|
||||
|
||||
|
||||
def run(name):
|
||||
drive, tag, only = RUNS[name]
|
||||
item = ITEMS[name]
|
||||
root = f"/mnt/felhom-drives/{drive}/userdata/media/movies"
|
||||
say(f"=== {name}: root={root} tag={tag} client={'(by tag)' if not only else only} item=archive.org/{item}")
|
||||
m = movie()
|
||||
if m:
|
||||
sys.exit(say("STOP: the film is already in Radarr — run cleanup first") or 1)
|
||||
cls = clients()
|
||||
if only:
|
||||
for c in cls:
|
||||
if tag_id(tag) in c["tags"]:
|
||||
set_enable(c, c["name"] == only)
|
||||
say(" clients:", [(c["name"], c["enable"], c["tags"]) for c in clients()])
|
||||
c, lk = ra("GET", f"/movie/lookup/tmdb?tmdbId={TMDB}")
|
||||
qp = ra("GET", "/qualityprofile")[1][0]
|
||||
body = dict(lk, qualityProfileId=qp["id"], rootFolderPath=root, monitored=True, tags=[tag_id(tag)],
|
||||
minimumAvailability="released", addOptions={"searchForMovie": False})
|
||||
c, m = ra("POST", "/movie", body)
|
||||
say(f" movie added: id={m['id']} '{m['title']}' ({m['year']}) path={m['path']} tags={m['tags']} profile={qp['name']}")
|
||||
sz = {"night-of-the-living-dead-1968_202312": 550604180, "night-of-the-living-dead-1968_202508": 596600000,
|
||||
"night-of-the-living-dead_202309": 598500000}[item]
|
||||
rel = {"title": "Night.of.the.Living.Dead.1968.480p.WEB-DL.x264-ARCHIVEORG",
|
||||
"downloadUrl": f"https://archive.org/download/{item}/{item}_archive.torrent",
|
||||
"protocol": "torrent", "publishDate": datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
||||
"size": sz, "indexer": "archive.org (release push)"}
|
||||
t0 = time.time()
|
||||
c, d = ra("POST", "/release/push", rel)
|
||||
dd = d[0] if isinstance(d, list) else d
|
||||
say(f" release push -> {c} approved={dd.get('approved')} rejections={dd.get('rejections')} downloadAllowed={dd.get('downloadAllowed')}")
|
||||
grabbed = imported = None
|
||||
for i in range(240):
|
||||
time.sleep(10)
|
||||
c, h = ra("GET", f"/history/movie?movieId={m['id']}")
|
||||
for e in h or []:
|
||||
if e["eventType"] == "grabbed" and not grabbed:
|
||||
grabbed = e
|
||||
say(f" +{int(time.time()-t0)}s grabbed: downloadClient={e['data'].get('downloadClient')} "
|
||||
f"downloadClientName={e['data'].get('downloadClientName')} downloadId={e.get('downloadId')}")
|
||||
if e["eventType"] == "downloadFolderImported":
|
||||
imported = e
|
||||
if imported:
|
||||
break
|
||||
if i % 6 == 5:
|
||||
c, q = ra("GET", f"/queue?movieIds={m['id']}")
|
||||
for r in (q or {}).get("records", []):
|
||||
say(f" +{int(time.time()-t0)}s queue: status={r.get('status')} trackedState={r.get('trackedDownloadState')} "
|
||||
f"left={r.get('sizeleft')} client={r.get('downloadClient')} output={r.get('outputPath')} "
|
||||
f"msgs={[s.get('messages') for s in r.get('statusMessages', [])]}")
|
||||
if not imported:
|
||||
say(f" RESULT {name}: NOT imported within {int(time.time()-t0)}s")
|
||||
return
|
||||
e = imported["data"]
|
||||
say(f" +{int(time.time()-t0)}s history downloadFolderImported: {json.dumps({k: e.get(k) for k in ('downloadClient', 'downloadClientName', 'droppedPath', 'importedPath', 'size')})}")
|
||||
say(f" download file: {e['droppedPath']} {st(e['droppedPath'])}")
|
||||
say(f" library file : {e['importedPath']} {st(e['importedPath'])}")
|
||||
a, b = os.stat(e["droppedPath"]), os.stat(e["importedPath"])
|
||||
say(f" RESULT {name}: {'HARDLINK (same inode)' if (a.st_ino, a.st_dev) == (b.st_ino, b.st_dev) else 'COPY (two inodes)'}"
|
||||
f"; download on {e['droppedPath'].split('/')[3]}, library on {e['importedPath'].split('/')[3]}")
|
||||
for c in clients():
|
||||
set_enable(c, True)
|
||||
|
||||
|
||||
if sys.argv[1] == "cleanup":
|
||||
cleanup()
|
||||
else:
|
||||
run(sys.argv[1])
|
||||
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env python3
|
||||
"""deploy.py <app> [<app> ...] — Part B4: install an app on the Tester 1 box the way the deploy wizard does:
|
||||
GET /api/stacks/<app>/deploy-fields, POST /api/stacks/<app>/deploy, then wait for the deploy's own completion mark
|
||||
(the pin in app.yaml), as box_walk.deploy does.
|
||||
|
||||
Values: DOMAIN, the app's default SUBDOMAIN, HDD_PATH = drive A (the drive the household would pick; the drill
|
||||
templates bind both drives by plain path and do not read it), and a generated value for each password field — kept
|
||||
ONLY in the 0600 file ~/.felhom-tester1/secrets.json (never printed, never in evidence). box_walk.deploy is not used
|
||||
because its deploy_values makes a folder on the scratch box's drive path, which does not exist here.
|
||||
|
||||
Live-probes rule: never send a deploy for an app that is not meant to be installed. This script refuses an app that is
|
||||
already installed and installs only the apps named on the command line.
|
||||
"""
|
||||
import json, os, secrets, sys, time
|
||||
sys.path.insert(0, "/mnt/5_hdd/felhom.eu/git/app-catalog-felhom.eu/scripts")
|
||||
import box_walk as w
|
||||
|
||||
HDD = "/mnt/felhom-drives/media_a"
|
||||
SEC = os.path.expanduser("~/.felhom-tester1/secrets.json")
|
||||
|
||||
|
||||
def load():
|
||||
try:
|
||||
return json.load(open(SEC))
|
||||
except FileNotFoundError:
|
||||
return {}
|
||||
|
||||
|
||||
def save(d):
|
||||
fd = os.open(SEC, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
||||
with os.fdopen(fd, "w") as f:
|
||||
json.dump(d, f)
|
||||
|
||||
|
||||
w.login()
|
||||
for app in sys.argv[1:]:
|
||||
st = w.stack(app)
|
||||
if st.get("deployed"):
|
||||
w.say(f"{app}: already installed — not touched")
|
||||
continue
|
||||
c, d = w.ctl("GET", f"/api/stacks/{app}/deploy-fields")
|
||||
fields = (((d.get("data") or {}).get("metadata") or {}).get("deploy_fields")) or []
|
||||
vals, gen = {"DOMAIN": w.DOMAIN}, []
|
||||
for f in fields:
|
||||
ev, ty = f.get("env_var"), f.get("type")
|
||||
if ev == "DOMAIN":
|
||||
continue
|
||||
if ev == "SUBDOMAIN":
|
||||
vals[ev] = f.get("default") or st.get("meta", {}).get("subdomain") or app
|
||||
elif ev == "HDD_PATH":
|
||||
vals[ev] = HDD
|
||||
elif ty == "password":
|
||||
vals[ev] = "T1-" + secrets.token_urlsafe(15)
|
||||
gen.append(ev)
|
||||
elif f.get("required"):
|
||||
sys.exit(w.say(f"{app}: required field {ev} ({ty}) has no rule here — STOP, not guessing") or 1)
|
||||
s = load(); s[app] = {k: vals[k] for k in gen}; save(s)
|
||||
shown = {k: ("<generated, kept in secrets.json>" if k in gen else v) for k, v in vals.items()}
|
||||
c, d = w.ctl("POST", f"/api/stacks/{app}/deploy", {"values": vals})
|
||||
w.say(f"{app}: POST deploy {json.dumps(shown)} -> {c} {json.dumps(d, ensure_ascii=False)[:200]}")
|
||||
if c != "202":
|
||||
continue
|
||||
for _ in range(120):
|
||||
time.sleep(5)
|
||||
st = w.stack(app)
|
||||
pins = (st.get("app_config") or {}).get("pinned_images")
|
||||
if st.get("deployed") and pins and st.get("state") in ("running", "unhealthy", "degraded"):
|
||||
w.say(f"{app}: deployed, state={st.get('state')}, pinned={pins}")
|
||||
break
|
||||
else:
|
||||
w.say(f"{app}: never became deployed (state={st.get('state')})")
|
||||
print(w.guest(f"docker inspect {app} -f '{{{{.State.Status}}}} {{{{range .Mounts}}}}{{{{.Source}}}}->{{{{.Destination}}}}:{{{{.RW}}}} {{{{end}}}}'; "
|
||||
f"docker logs --since 10m felhom-controller 2>&1 | grep -i '{app}' | grep -v DEBUG | grep -iE 'after_install|deploy|error|warn' | tail -8"))
|
||||
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env python3
|
||||
"""enrol.py <device> <mount_name> <label> — Part A4: enrol one blank disk on the Tester 1 box through the SAME calls
|
||||
the „Új meghajtó inicializálása" wizard makes (storage_init.html): GET /api/disks/candidates, POST /api/storage/init,
|
||||
then poll GET /api/storage/init/status to a terminal phase. set_default is sent FALSE (the wizard's checkbox is
|
||||
checked by default — recorded in the report; this run keeps Tester 1's default drive unchanged).
|
||||
|
||||
Run from DooPlex: SC=~/.felhom-tester1 TARGET=tester-1 python3 enrol.py /dev/sdc media_a "Media A"
|
||||
"""
|
||||
import json, sys, time
|
||||
sys.path.insert(0, "/mnt/5_hdd/felhom.eu/git/app-catalog-felhom.eu/scripts")
|
||||
import box_walk as w
|
||||
|
||||
dev, name, label = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||
w.login()
|
||||
c, d = w.ctl("GET", "/api/disks/candidates")
|
||||
cands = [x["device"] for x in ((d.get("data") or {}).get("initialize") or [])]
|
||||
w.say(f"candidates -> {c} {cands}")
|
||||
if dev not in cands:
|
||||
sys.exit(w.say(f"STOP: {dev} is not offered for initialisation") or 1)
|
||||
body = {"device": dev, "fstype": "ext4", "mount_name": name, "label": label,
|
||||
"set_default": False, "confirmed": False, "durable_id": ""}
|
||||
c, d = w.ctl("POST", "/api/storage/init", body)
|
||||
w.say(f"POST /api/storage/init {json.dumps(body)} -> {c} {json.dumps(d, ensure_ascii=False)}")
|
||||
if c != "200":
|
||||
sys.exit(1)
|
||||
last = None
|
||||
for _ in range(600):
|
||||
time.sleep(2)
|
||||
c, d = w.ctl("GET", "/api/storage/init/status")
|
||||
ph = (d.get("data") or {}).get("phase")
|
||||
if ph != last:
|
||||
w.say(f"status -> {c} {json.dumps(d.get('data'), ensure_ascii=False)}")
|
||||
last = ph
|
||||
if ph in ("done", "failed", "refused", "needs_confirmation"):
|
||||
break
|
||||
print("RESULT", last)
|
||||
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env python3
|
||||
"""make_drill.py — Part B2: rewrite the six media templates IN THE DRILL CATALOG to the layout under test.
|
||||
|
||||
The layout (one bind per drive, the same path inside the container as in the guest, `userdata/media` only):
|
||||
|
||||
/mnt/felhom-drives/<drive>/userdata/media -> /mnt/felhom-drives/<drive>/userdata/media (rw; :ro for Plex/Jellyfin)
|
||||
|
||||
for <drive> in DRIVES. Images are NOT touched. Plain host paths on purpose: the controller's bind classifier knows only
|
||||
the ${..._PATH} roots (stacks/classify_binds.go classifyRoot), so these binds are in no app capture set (A6).
|
||||
|
||||
Because the ${USERDATA_PATH} binds go away, the `.felhom.yml` `backup.userdata` entries and `data_paths` that named them
|
||||
are removed too — appbackup.ValidateBackupSpec rejects an entry that matches no compose bind, and a rejected block
|
||||
degrades the app to the legacy path. qBittorrent's and Transmission's `backup.volumes` (their config) stay.
|
||||
|
||||
Default save paths: drive A's `media/torrents` (qBittorrent, seeded on first boot) and `media/transmission/complete`
|
||||
(Transmission, set by its entrypoint on a fresh config only). Everything else is unchanged.
|
||||
|
||||
python3 make_drill.py [--check] # edits /mnt/5_hdd/felhom.eu/drill/app-catalog-drill/templates in place
|
||||
"""
|
||||
import re, sys
|
||||
|
||||
D = "/mnt/5_hdd/felhom.eu/drill/app-catalog-drill/templates"
|
||||
DRIVES = ("media_a", "media_b")
|
||||
M = {d: f"/mnt/felhom-drives/{d}/userdata/media" for d in DRIVES}
|
||||
A = M["media_a"]
|
||||
|
||||
|
||||
def binds(indent, ro):
|
||||
suf = ":ro" if ro else ""
|
||||
return "".join(f"{indent}- {M[d]}:{M[d]}{suf}\n" for d in DRIVES)
|
||||
|
||||
|
||||
def sub1(s, old, new, app):
|
||||
if old not in s:
|
||||
sys.exit(f"{app}: expected text not found: {old!r}")
|
||||
return s.replace(old, new, 1)
|
||||
|
||||
|
||||
def compose(app):
|
||||
p = f"{D}/{app}/docker-compose.yml"
|
||||
s = open(p).read()
|
||||
if M["media_a"] in s:
|
||||
return f"{app}: compose already in the new layout"
|
||||
i = " "
|
||||
if app == "sonarr":
|
||||
s = sub1(s, f"{i}- ${{USERDATA_PATH}}/media/tv:/media/tv\n{i}- ${{USERDATA_PATH}}/downloads:/downloads\n", binds(i, False), app)
|
||||
elif app == "radarr":
|
||||
s = sub1(s, f"{i}- ${{USERDATA_PATH}}/media/movies:/media/movies\n{i}- ${{USERDATA_PATH}}/downloads:/downloads\n", binds(i, False), app)
|
||||
elif app == "qbittorrent":
|
||||
s = sub1(s, f"{i}- ${{USERDATA_PATH}}/media/torrents:/downloads\n", binds(i, False), app)
|
||||
s = sub1(s, "'Session\\DefaultSavePath=/downloads'", f"'Session\\DefaultSavePath={A}/torrents'", app)
|
||||
s = sub1(s, "'Session\\TempPath=/downloads/.temp'", f"'Session\\TempPath={A}/torrents/.temp'", app)
|
||||
elif app == "transmission":
|
||||
s = sub1(s, f"{i}- ${{USERDATA_PATH}}/media/transmission:/downloads\n", binds(i, False), app)
|
||||
s = sub1(s, f"{i}- ${{USERDATA_PATH}}/media/transmission/watch:/watch\n", f"{i}- {A}/transmission/watch:/watch\n", app)
|
||||
# the default download folders are set ONCE, on a fresh config (a household's later choice is kept)
|
||||
s = sub1(s, """ cp /defaults/settings.json "$$CONF"\n""",
|
||||
f""" cp /defaults/settings.json "$$CONF"\n jq '.["download-dir"] = "{A}/transmission/complete" | .["incomplete-dir"] = "{A}/transmission/incomplete"' "$$CONF" > /tmp/d.json && mv /tmp/d.json "$$CONF"\n""", app)
|
||||
elif app in ("plex", "jellyfin"):
|
||||
s = sub1(s, f"{i}- ${{USERDATA_PATH}}/media:/media:ro\n", binds(i, True), app)
|
||||
if "${USERDATA_PATH}" in re.sub(r"^#.*$", "", s, flags=re.M):
|
||||
sys.exit(f"{app}: a ${{USERDATA_PATH}} bind is left")
|
||||
open(p, "w").write(s)
|
||||
return f"{app}: compose rewritten"
|
||||
|
||||
|
||||
def felhom_yml(app):
|
||||
p = f"{D}/{app}/.felhom.yml"
|
||||
s = open(p).read()
|
||||
before = s
|
||||
# drop backup.userdata (the list under it) — and the whole backup block when nothing else is left in it
|
||||
s = re.sub(r"(^backup:\n(?: volumes:\n(?: .*\n| .*\n| .*\n)*)?) userdata:\n(?: .*\n| .*\n)+", r"\1", s, flags=re.M)
|
||||
s = re.sub(r"^# --- Backup classification[^\n]*\n^backup:\n(?=\n|#|[a-z])", "", s, flags=re.M)
|
||||
# drop data_paths (top level only; the i18n.en copy is display text and names no bind)
|
||||
s = re.sub(r"^# --- Customer-facing folder annotation[^\n]*\n^data_paths:\n(?: .*\n)+", "", s, flags=re.M)
|
||||
if s != before:
|
||||
open(p, "w").write(s)
|
||||
return f"{app}: .felhom.yml backup.userdata/data_paths removed"
|
||||
return f"{app}: .felhom.yml unchanged"
|
||||
|
||||
|
||||
for app in ("sonarr", "radarr", "qbittorrent", "transmission", "plex", "jellyfin"):
|
||||
print(compose(app))
|
||||
print(felhom_yml(app))
|
||||
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env python3
|
||||
"""repoint.py mirror|drill|restore|status — point the Tester 1 box at the drill catalog WITHOUT a credential (R-992).
|
||||
|
||||
WHY A LOCAL MIRROR. `09` §6.5's route needs a catalog credential on the box: the drill repo is private (R-992). This
|
||||
session could not mint a READ-ONLY one — the Gitea credential on DooPlex is itself a token, and Gitea answers 403 to a
|
||||
token asking to list or create tokens (measured 2026-10-14) — and the night of 2026-10-06 put the admin WRITE token
|
||||
into the box's controller.yaml, which this task forbids. The cache-staging workaround of 2026-10-12 does not hold for a
|
||||
test that lasts days: every sync (and every controller start) re-applies the live catalog over the staged templates
|
||||
and over the installed apps' compose files (Syncer.copyTemplates, no deployed check).
|
||||
|
||||
So the drill catalog is MIRRORED onto the box: a bare git repository inside the controller's own data volume, and
|
||||
`git.repo_url: file:///opt/docker/felhom-controller/drill-catalog.git`. The controller's sync runs exactly as for a
|
||||
remote (clone/fetch/reset, the R-615 re-clone on a changed URL); no secret is on the box. DooPlex updates the mirror
|
||||
from the drill repo with a git bundle (`mirror`). The drill repo on Gitea stays the record.
|
||||
|
||||
SC=~/.felhom-tester1 TARGET=tester-1 python3 repoint.py mirror # (re)load the mirror from the drill repo main
|
||||
... repoint.py drill # save controller.yaml (once), set repo_url to the mirror, restart, sync, rescan
|
||||
... repoint.py restore # put the saved controller.yaml back (byte-identical), restart, sync, rescan
|
||||
... repoint.py status
|
||||
"""
|
||||
import os, subprocess, sys, time
|
||||
sys.path.insert(0, "/mnt/5_hdd/felhom.eu/git/app-catalog-felhom.eu/scripts")
|
||||
import box_walk as w
|
||||
|
||||
DRILL = "/mnt/5_hdd/felhom.eu/drill/app-catalog-drill"
|
||||
VOL = "/var/lib/docker/volumes/felhom-controller-data/_data" # guest path of the controller's data volume
|
||||
CVOL = "/opt/docker/felhom-controller" # the same, inside the controller container
|
||||
SAVE = f"{VOL}/controller.yaml.pre-media-2026-10-14"
|
||||
MIRROR_URL = f"file://{CVOL}/drill-catalog.git"
|
||||
|
||||
|
||||
def status():
|
||||
print(w.guest(f"""grep -nE 'repo_url|sync_interval' {VOL}/controller.yaml; ls -l {SAVE} 2>&1 | tail -1
|
||||
docker exec felhom-controller sh -c 'git -C {CVOL}/drill-catalog.git log -1 --format="mirror main %h %s" main 2>&1;
|
||||
git -C {CVOL}/data/catalog-cache log -1 --format="cache HEAD %h %s" 2>&1; git -C {CVOL}/data/catalog-cache remote get-url origin'"""))
|
||||
|
||||
|
||||
def mirror():
|
||||
b = "/tmp/drill-media.bundle"
|
||||
subprocess.run(["git", "-C", DRILL, "bundle", "create", b, "main"], check=True, capture_output=True)
|
||||
head = subprocess.run(["git", "-C", DRILL, "rev-parse", "--short", "main"], capture_output=True, text=True).stdout.strip()
|
||||
ssh = w.ssh_args()
|
||||
# bundle -> PVE host -> guest -> controller volume; the bare repo is made and fetched BY the controller's own user
|
||||
subprocess.run(["scp", "-q", "-o", "StrictHostKeyChecking=accept-new"] + (["-J", w._T["jump"]] if w._T["jump"] else [])
|
||||
+ [b, f"{w._T['ssh']}:{b}"], check=True)
|
||||
subprocess.run(ssh + [f"pct push {w.GUEST} {b} {VOL}/drill-media.bundle && rm -f {b}"], check=True)
|
||||
print(w.guest(f"""set -e
|
||||
docker exec felhom-controller sh -c 'test -d {CVOL}/drill-catalog.git || git init -q --bare -b main {CVOL}/drill-catalog.git
|
||||
git -C {CVOL}/drill-catalog.git fetch -q {CVOL}/drill-media.bundle +main:main && rm -f {CVOL}/drill-media.bundle
|
||||
git -C {CVOL}/drill-catalog.git log -1 --format="mirror main %h %s" main'"""))
|
||||
print("drill repo main:", head)
|
||||
os.remove(b)
|
||||
|
||||
|
||||
def restart_sync():
|
||||
w.guest("docker restart felhom-controller >/dev/null")
|
||||
for _ in range(60):
|
||||
time.sleep(3)
|
||||
try:
|
||||
w.login()
|
||||
break
|
||||
except SystemExit:
|
||||
continue
|
||||
w.sync_rescan()
|
||||
time.sleep(3)
|
||||
print(w.guest("docker logs --since 3m felhom-controller 2>&1 | grep -E '\\[sync\\]' | grep -v DEBUG | tail -8"))
|
||||
|
||||
|
||||
def drill():
|
||||
print(w.guest(f"""set -e
|
||||
test -f {SAVE} || cp -p {VOL}/controller.yaml {SAVE}
|
||||
sed -i -E 's#^(\\s+repo_url: ).*$#\\1{MIRROR_URL}#' {VOL}/controller.yaml
|
||||
grep -n 'repo_url' {VOL}/controller.yaml; diff {SAVE} {VOL}/controller.yaml || true"""))
|
||||
restart_sync()
|
||||
status()
|
||||
|
||||
|
||||
def restore():
|
||||
print(w.guest(f"""set -e
|
||||
cp -p {SAVE} {VOL}/controller.yaml
|
||||
cmp {SAVE} {VOL}/controller.yaml && echo RESTORED-IDENTICAL; grep -n repo_url {VOL}/controller.yaml"""))
|
||||
restart_sync()
|
||||
status()
|
||||
|
||||
|
||||
{"mirror": mirror, "drill": drill, "restore": restore, "status": status}[sys.argv[1]]()
|
||||
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env python3
|
||||
"""run_bg.py start <script.py> <arg> | log <arg> | stop <arg> — run a long chain step DETACHED inside the Tester 1 guest
|
||||
(so a dropped SSH does not kill it), and read its log. The log lives in /root/media-<arg>.log until `stop` removes it
|
||||
together with the script; the caller copies the log into the evidence folder first."""
|
||||
import os, sys
|
||||
sys.path.insert(0, "/mnt/5_hdd/felhom.eu/git/app-catalog-felhom.eu/scripts")
|
||||
import box_walk as w
|
||||
op = sys.argv[1]
|
||||
if op == "start":
|
||||
src = open(os.path.join(os.path.dirname(os.path.abspath(__file__)), sys.argv[2])).read()
|
||||
a = sys.argv[3]
|
||||
print(w.guest(f"cat > /root/.media-{a}.py <<'PYEOF'\n{src}\nPYEOF\n"
|
||||
f"nohup python3 -u /root/.media-{a}.py {a} > /root/media-{a}.log 2>&1 < /dev/null &\necho started"))
|
||||
elif op == "log":
|
||||
print(w.guest(f"cat /root/media-{sys.argv[2]}.log; pgrep -f '/root/.media-{sys.argv[2]}.py' >/dev/null && echo '[still running]' || echo '[finished]'"))
|
||||
elif op == "stop":
|
||||
print(w.guest(f"pkill -f 'python3 -u /root/.media-{sys.argv[2]}.py'; rm -f /root/.media-{sys.argv[2]}.py /root/media-{sys.argv[2]}.log; echo removed"))
|
||||
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env python3
|
||||
"""run_in_guest.py <script.py> [args] — run one of this folder's stdlib scripts INSIDE the Tester 1 guest
|
||||
(SC=~/.felhom-tester1 TARGET=tester-1) and remove it afterwards. Output only; nothing is left on the box."""
|
||||
import os, sys
|
||||
sys.path.insert(0, "/mnt/5_hdd/felhom.eu/git/app-catalog-felhom.eu/scripts")
|
||||
import box_walk as w
|
||||
src = open(os.path.join(os.path.dirname(os.path.abspath(__file__)), sys.argv[1])).read()
|
||||
args = " ".join(sys.argv[2:])
|
||||
print(w.guest(f"cat > /root/.media-tool.py <<'PYEOF'\n{src}\nPYEOF\npython3 /root/.media-tool.py {args} 2>&1; rm -f /root/.media-tool.py", timeout=1800))
|
||||
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env python3
|
||||
"""where.py — S4: where every media file sits, per drive. Run INSIDE the guest (run_in_guest.py where.py).
|
||||
|
||||
For each enrolled drive under /mnt/felhom-drives/*/userdata/media: every regular file (hidden temp folders included)
|
||||
with its size, link count and inode, grouped by top folder (tv, movies, torrents, transmission, ...). Then the pairs
|
||||
that are ONE file (same device + inode = a hardlink: no extra copy) and the library files that are NOT linked to any
|
||||
download (link count 1 = a copy, or a download already removed). Stdlib only; reads, never writes.
|
||||
"""
|
||||
import collections, glob, os
|
||||
|
||||
ROOTS = sorted(glob.glob("/mnt/felhom-drives/*/userdata/media"))
|
||||
by_ino = collections.defaultdict(list)
|
||||
total = 0
|
||||
for root in ROOTS:
|
||||
drive = root.split("/")[3]
|
||||
print(f"== drive {drive} ({root})")
|
||||
for top in sorted(os.listdir(root)):
|
||||
rows = []
|
||||
for dp, dn, fn in os.walk(os.path.join(root, top)):
|
||||
for f in fn:
|
||||
p = os.path.join(dp, f)
|
||||
try:
|
||||
s = os.lstat(p)
|
||||
except OSError as e:
|
||||
rows.append(f" ?? {p}: {e}")
|
||||
continue
|
||||
if not os.path.isfile(p) or os.path.islink(p):
|
||||
continue
|
||||
total += 1
|
||||
by_ino[(s.st_dev, s.st_ino)].append((drive, top, p, s.st_size, s.st_nlink))
|
||||
rows.append(f" {s.st_size/1e6:10.1f} MB links={s.st_nlink} inode={s.st_ino:<10} {os.path.relpath(p, root)}")
|
||||
if rows:
|
||||
print(f" -- {top}/ ({len(rows)} file(s))")
|
||||
print("\n".join(rows))
|
||||
print(f"\n== {total} file(s) on {len(ROOTS)} drive(s)")
|
||||
print("== ONE FILE, TWO NAMES (hardlink — no extra copy):")
|
||||
n = 0
|
||||
for k, v in by_ino.items():
|
||||
if len(v) > 1:
|
||||
n += 1
|
||||
print(f" drive {v[0][0]} inode {k[1]} ({v[0][3]/1e6:.1f} MB): " + " <-> ".join(f"{x[1]}/…/{os.path.basename(x[2])}" for x in v))
|
||||
print(f" {n} pair(s)")
|
||||
print("== LIBRARY FILES WITH LINK COUNT 1 (a copy, or its download is gone):")
|
||||
for k, v in by_ino.items():
|
||||
for d, top, p, size, nl in v:
|
||||
if top in ("tv", "movies") and nl == 1:
|
||||
print(f" drive {d}: {os.path.relpath(p, '/mnt/felhom-drives')} ({size/1e6:.1f} MB)")
|
||||
@@ -0,0 +1,222 @@
|
||||
#!/usr/bin/env python3
|
||||
"""wire.py [-v] — Part C: connect Sonarr and Radarr to qBittorrent and Transmission, ONE ENTRY PER DRIVE, on the box.
|
||||
|
||||
Runs INSIDE the guest (it talks to the containers on the docker network and reads their keys from their own config
|
||||
volumes / environment). Stdlib only. Every key and password is read at runtime and NEVER written to a file or a log:
|
||||
every printed line passes through redact(). `-v` prints every call and the SHAPE of every answer (status, top-level
|
||||
keys / list length) — this is the reference the later link code copies.
|
||||
|
||||
For each drive d in DRIVES, M = /mnt/felhom-drives/<d>/userdata/media (the same path in every container):
|
||||
qBittorrent : categories tv-<d>, movies-<d> -> save path M/torrents/{tv,movies}, incomplete path M/torrents/.temp
|
||||
Sonarr : root folder M/tv; tag <d>; „qBittorrent <d>" (category tv-<d>, tag <d>) and
|
||||
„Transmission <d>" (directory M/transmission/tv, tag <d>)
|
||||
Radarr : the same with movies
|
||||
Sonarr/Radarr: minimum free space when importing = MIN_FREE_MB; „use hardlinks instead of copy" quoted
|
||||
Box-wide, recorded with the reason in the output:
|
||||
qBittorrent : auto_tmm_enabled = true (a category's save path applies ONLY in automatic torrent management;
|
||||
qBittorrent's default is manual, which saves every torrent to the default
|
||||
path = drive A, whatever the category says)
|
||||
Transmission: incomplete-dir-enabled = false (Transmission has ONE incomplete folder for all torrents; on drive A it
|
||||
would make every drive-B download a cross-drive copy at completion)
|
||||
|
||||
Idempotent: an existing tag / root folder / client / category of the same name is left as it is and reported.
|
||||
"""
|
||||
import base64, json, re, subprocess, sys, urllib.error, urllib.parse, urllib.request
|
||||
|
||||
V = "-v" in sys.argv
|
||||
DRIVES = [("drive-a", "media_a"), ("drive-b", "media_b")]
|
||||
MIN_FREE_MB = 10240
|
||||
SECRETS = []
|
||||
|
||||
|
||||
def redact(s):
|
||||
s = str(s)
|
||||
for x in SECRETS:
|
||||
if x:
|
||||
s = s.replace(x, "<redacted>")
|
||||
return s
|
||||
|
||||
|
||||
def say(*a):
|
||||
print(redact(" ".join(str(x) for x in a)), flush=True)
|
||||
|
||||
|
||||
def sh(cmd):
|
||||
return subprocess.run(cmd, shell=True, capture_output=True, text=True).stdout.strip()
|
||||
|
||||
|
||||
def shape(body):
|
||||
if isinstance(body, dict):
|
||||
return "{" + ", ".join(sorted(body.keys()))[:300] + "}"
|
||||
if isinstance(body, list):
|
||||
return f"[list of {len(body)}]"
|
||||
return repr(body)[:120]
|
||||
|
||||
|
||||
def call(method, url, headers=None, data=None, form=False, raw=False, expect=(200, 201, 202, 204)):
|
||||
h = dict(headers or {})
|
||||
if data is not None and not isinstance(data, (bytes, str)):
|
||||
data = urllib.parse.urlencode(data) if form else json.dumps(data)
|
||||
h.setdefault("Content-Type", "application/x-www-form-urlencoded" if form else "application/json")
|
||||
if isinstance(data, str):
|
||||
data = data.encode()
|
||||
req = urllib.request.Request(url, data=data, headers=h, method=method)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
code, txt, hdrs = r.status, r.read().decode("utf-8", "replace"), {k.lower(): v for k, v in r.headers.items()}
|
||||
except urllib.error.HTTPError as e:
|
||||
code, txt, hdrs = e.code, e.read().decode("utf-8", "replace"), {k.lower(): v for k, v in e.headers.items()}
|
||||
try:
|
||||
body = json.loads(txt) if txt and not raw else txt
|
||||
except ValueError:
|
||||
body = txt
|
||||
if V:
|
||||
say(f" {method} {url} -> {code} {shape(body)}")
|
||||
if code not in expect:
|
||||
say(f" !! {method} {url} -> {code}: {redact(txt)[:600]}")
|
||||
return code, body, hdrs
|
||||
|
||||
|
||||
def ip(c):
|
||||
return sh(f"docker inspect {c} -f '{{{{range .NetworkSettings.Networks}}}}{{{{.IPAddress}}}} {{{{end}}}}'").split()[0]
|
||||
|
||||
|
||||
def apikey(app):
|
||||
x = open(f"/var/lib/docker/volumes/{app}_{app}_config/_data/config.xml").read()
|
||||
k = re.search(r"<ApiKey>([^<]+)</ApiKey>", x).group(1)
|
||||
SECRETS.append(k)
|
||||
return k
|
||||
|
||||
|
||||
def env(c, var):
|
||||
v = sh(f"docker exec {c} printenv {var}")
|
||||
SECRETS.append(v)
|
||||
return v
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- qBittorrent
|
||||
QB = f"http://{ip('qbittorrent')}:8080"
|
||||
qpw = env("qbittorrent", "QBT_ADMIN_PASSWORD")
|
||||
say("== qBittorrent", QB, "(password: the deploy secret, read from the container's environment)")
|
||||
c, _, hd = call("POST", QB + "/api/v2/auth/login", {"Referer": QB}, {"username": "admin", "password": qpw}, form=True, raw=True)
|
||||
# qBittorrent 5.x names the session cookie QBT_SID_<port> (older versions: SID) — take whichever it set
|
||||
m = re.search(r"((?:QBT_)?SID[A-Za-z0-9_]*)=([^;]+)", hd.get("set-cookie", ""))
|
||||
if not m:
|
||||
sys.exit(say(f" !! no session cookie; answer headers: {sorted(hd)}") or 1)
|
||||
say(f" session cookie name: {m.group(1)}")
|
||||
SECRETS.append(m.group(2))
|
||||
QH = {"Referer": QB, "Cookie": f"{m.group(1)}={m.group(2)}"}
|
||||
c, prefs, _ = call("GET", QB + "/api/v2/app/preferences", QH)
|
||||
say(f" before: auto_tmm_enabled={prefs.get('auto_tmm_enabled')} save_path={prefs.get('save_path')} "
|
||||
f"temp_path_enabled={prefs.get('temp_path_enabled')} temp_path={prefs.get('temp_path')}")
|
||||
call("POST", QB + "/api/v2/app/setPreferences", QH, {"json": json.dumps({"auto_tmm_enabled": True})}, form=True, raw=True)
|
||||
c, prefs, _ = call("GET", QB + "/api/v2/app/preferences", QH)
|
||||
say(f" after : auto_tmm_enabled={prefs.get('auto_tmm_enabled')} (a category's save path applies only in automatic mode)")
|
||||
c, cats, _ = call("GET", QB + "/api/v2/torrents/categories", QH)
|
||||
for tag, d in DRIVES:
|
||||
M = f"/mnt/felhom-drives/{d}/userdata/media"
|
||||
for kind in ("tv", "movies"):
|
||||
name = f"{kind}-{tag}"
|
||||
form = {"category": name, "savePath": f"{M}/torrents/{kind}", "downloadPathEnabled": "true",
|
||||
"downloadPath": f"{M}/torrents/.temp"}
|
||||
ep = "editCategory" if name in (cats or {}) else "createCategory"
|
||||
call("POST", QB + f"/api/v2/torrents/{ep}", QH, form, form=True, raw=True)
|
||||
c, cats, _ = call("GET", QB + "/api/v2/torrents/categories", QH)
|
||||
for n, v in sorted((cats or {}).items()):
|
||||
say(f" category {n}: savePath={v.get('savePath')} downloadPath={v.get('download_path', v.get('downloadPath'))}")
|
||||
|
||||
# ---------------------------------------------------------------- Transmission
|
||||
TR = f"http://{ip('transmission')}:9091/transmission/rpc"
|
||||
tpw = env("transmission", "PASS")
|
||||
TA = "Basic " + base64.b64encode(f"admin:{tpw}".encode()).decode()
|
||||
SECRETS.append(TA.split()[1])
|
||||
say("== Transmission", TR, "(password: the deploy secret, read from the container's environment)")
|
||||
|
||||
|
||||
def trpc(method, args=None, host=None):
|
||||
h = {"Authorization": TA}
|
||||
if host:
|
||||
h["Host"] = host
|
||||
c, b, hd = call("POST", TR, h, {"method": method, "arguments": args or {}}, expect=(200, 409))
|
||||
if c == 409:
|
||||
h["X-Transmission-Session-Id"] = hd.get("x-transmission-session-id", "")
|
||||
c, b, hd = call("POST", TR, h, {"method": method, "arguments": args or {}}, expect=(200, 421))
|
||||
return c, b
|
||||
|
||||
|
||||
c, b = trpc("session-get", {"fields": ["download-dir", "incomplete-dir", "incomplete-dir-enabled", "rename-partial-files"]})
|
||||
say(f" before: {json.dumps((b or {}).get('arguments'))}")
|
||||
trpc("session-set", {"incomplete-dir-enabled": False})
|
||||
c, b = trpc("session-get", {"fields": ["download-dir", "incomplete-dir-enabled", "rename-partial-files"]})
|
||||
say(f" after : {json.dumps((b or {}).get('arguments'))} (one incomplete folder for all torrents would be on drive A)")
|
||||
# The probe the link code needs: can another container reach Transmission BY NAME? (Host-header allow-list)
|
||||
c, b = trpc("session-get", {"fields": ["version"]}, host="transmission:9091")
|
||||
say(f" probe Host: transmission:9091 -> HTTP {c} {redact(str(b))[:160]}")
|
||||
TR_BY_NAME = c == 200
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- Sonarr / Radarr
|
||||
def arr(app, port, kind, root_kind):
|
||||
base = f"http://{ip(app)}:{port}/api/v3"
|
||||
H = {"X-Api-Key": apikey(app)}
|
||||
say(f"== {app} {base} (key: <ApiKey> in /config/config.xml, read from the config volume)")
|
||||
c, tags, _ = call("GET", base + "/tag", H)
|
||||
have = {t["label"]: t["id"] for t in tags or []}
|
||||
for tag, _d in DRIVES:
|
||||
if tag not in have:
|
||||
c, t, _ = call("POST", base + "/tag", H, {"label": tag})
|
||||
have[tag] = t["id"]
|
||||
say(f" tags: {have}")
|
||||
c, roots, _ = call("GET", base + "/rootfolder", H)
|
||||
rp = {r["path"].rstrip("/") for r in roots or []}
|
||||
for tag, d in DRIVES:
|
||||
p = f"/mnt/felhom-drives/{d}/userdata/media/{root_kind}"
|
||||
if p not in rp:
|
||||
call("POST", base + "/rootfolder", H, {"path": p})
|
||||
c, roots, _ = call("GET", base + "/rootfolder", H)
|
||||
for r in roots or []:
|
||||
say(f" root folder: {r['path']} accessible={r.get('accessible')} freeSpace={r.get('freeSpace')}")
|
||||
c, schema, _ = call("GET", base + "/downloadclient/schema", H)
|
||||
sch = {s["implementation"]: s for s in schema}
|
||||
c, clients, _ = call("GET", base + "/downloadclient", H)
|
||||
names = {x["name"] for x in clients or []}
|
||||
for tag, d in DRIVES:
|
||||
M = f"/mnt/felhom-drives/{d}/userdata/media"
|
||||
for impl, nm, vals in (
|
||||
("QBittorrent", f"qBittorrent {tag}", {"host": "qbittorrent", "port": 8080, "username": "admin",
|
||||
"password": qpw, f"{kind}Category": f"{'tv' if kind == 'tv' else 'movies'}-{tag}"}),
|
||||
("Transmission", f"Transmission {tag}", {"host": "transmission", "port": 9091, "username": "admin",
|
||||
"password": tpw, "urlBase": "/transmission/",
|
||||
# the schema's default category must be CLEARED: Sonarr/Radarr
|
||||
# refuse „Cannot use Category and Directory" (measured, run 1)
|
||||
f"{kind}Category": "",
|
||||
f"{kind}Directory": f"{M}/transmission/{root_kind}"})):
|
||||
if nm in names:
|
||||
say(f" client {nm}: exists — left as it is")
|
||||
continue
|
||||
body = json.loads(json.dumps(sch[impl]))
|
||||
body.update({"name": nm, "enable": True, "tags": [have[tag]], "priority": 1})
|
||||
for f in body["fields"]:
|
||||
if f["name"] in vals:
|
||||
f["value"] = vals[f["name"]]
|
||||
c, r, _ = call("POST", base + "/downloadclient", H, body, expect=(200, 201, 400))
|
||||
if c == 400:
|
||||
say(f" client {nm}: REFUSED by {app}'s own connection test: {redact(json.dumps(r))[:400]}")
|
||||
c, clients, _ = call("GET", base + "/downloadclient", H)
|
||||
for x in clients or []:
|
||||
f = {k["name"]: k.get("value") for k in x["fields"]}
|
||||
say(f" client {x['name']}: impl={x['implementation']} enable={x['enable']} tags={x['tags']} host={f.get('host')}"
|
||||
f" category={f.get(kind + 'Category')} directory={f.get(kind + 'Directory')}")
|
||||
c, mm, _ = call("GET", base + "/config/mediamanagement", H)
|
||||
say(f" before: minimumFreeSpaceWhenImporting={mm.get('minimumFreeSpaceWhenImporting')} MB, "
|
||||
f"copyUsingHardlinks={mm.get('copyUsingHardlinks')}")
|
||||
mm["minimumFreeSpaceWhenImporting"] = MIN_FREE_MB
|
||||
call("PUT", base + f"/config/mediamanagement/{mm['id']}", H, mm)
|
||||
c, mm, _ = call("GET", base + "/config/mediamanagement", H)
|
||||
say(f" after : minimumFreeSpaceWhenImporting={mm.get('minimumFreeSpaceWhenImporting')} MB, "
|
||||
f"copyUsingHardlinks={mm.get('copyUsingHardlinks')} (default kept)")
|
||||
|
||||
|
||||
arr("sonarr", 8989, "tv", "tv")
|
||||
arr("radarr", 7878, "movie", "movies")
|
||||
say("== done; Transmission reachable by its container name:", TR_BY_NAME)
|
||||
Reference in New Issue
Block a user