From 501671770faeb05159409cf9d91af076f18e5ceb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20D=C3=A9siles?= <1536672+cdesiles@users.noreply.github.com> Date: Fri, 18 Sep 2026 15:24:18 +0200 Subject: [PATCH] add deluge role --- roles/deluge/README.md | 54 +++++++++++ roles/deluge/defaults/main.yml | 27 ++++++ roles/deluge/handlers/main.yml | 20 ++++ roles/deluge/meta/main.yml | 5 + roles/deluge/tasks/main.yml | 107 +++++++++++++++++++++ roles/deluge/templates/deluge.service.j2 | 15 +++ roles/deluge/templates/deluge.yaml.j2 | 60 ++++++++++++ roles/deluge/templates/nginx-vhost.conf.j2 | 58 +++++++++++ 8 files changed, 346 insertions(+) create mode 100644 roles/deluge/README.md create mode 100644 roles/deluge/defaults/main.yml create mode 100644 roles/deluge/handlers/main.yml create mode 100644 roles/deluge/meta/main.yml create mode 100644 roles/deluge/tasks/main.yml create mode 100644 roles/deluge/templates/deluge.service.j2 create mode 100644 roles/deluge/templates/deluge.yaml.j2 create mode 100644 roles/deluge/templates/nginx-vhost.conf.j2 diff --git a/roles/deluge/README.md b/roles/deluge/README.md new file mode 100644 index 0000000..1b33347 --- /dev/null +++ b/roles/deluge/README.md @@ -0,0 +1,54 @@ +# Deluge Role + +Deploys [Deluge](https://deluge-torrent.org/) - a lightweight BitTorrent client with a web UI - using Podman with k8s files. The `linuxserver/deluge` image runs both `deluged` and `deluge-web`. + +## Prerequisites + +The `podman` role must have been applied to the host first (shared service, intentionally not declared in `meta/main.yml` to keep deluge deploys fast). Order it before `deluge` in the playbook for fresh installs. + +## Role Variables + +See `defaults/main.yml` for all available variables and their default values. + +## First Login + +The web UI listens on `http://:{{ deluge_web_port }}` with the default credentials `admin` / `deluge`. **Change the password immediately** (Preferences → Interface → Password) — the role does not manage it. + +Set the inbound port to `{{ deluge_torrent_port }}` under Preferences → Network, otherwise Deluge picks a random port and the mapped torrent port is useless. + +## Storage + +- `deluge_config_dir` - torrent state, auth and web settings. Backed up. +- `deluge_downloads_dir` - default save path for completed torrents. User data, not backed up. + +## Ports + +| Port | Protocol | Purpose | +| ---------------------------- | --------- | ------------------------------ | +| `deluge_web_port` (8112) | tcp | Web UI | +| `deluge_torrent_port` (6881) | tcp + udp | Incoming peer traffic, DHT/uTP | + +The thin-client daemon port (58846) is not mapped; add it to the pod manifest if a native Deluge client needs to connect. + +## Nginx Reverse Proxy + +Set `deluge_nginx_enabled: true` and `deluge_nginx_hostname` to expose the web UI over HTTPS. Certificates are provisioned via the shared `nginx` role's certbot tasks. If the hostname is not the apex, point a DNS record at the host first. + +## Backup + +The role exposes `deluge_backup_paths` (config only). Add a job in the host inventory to include it: + +```yaml +backup_jobs: + - name: deluge + repositories: [next_s3] + paths: "{{ deluge_backup_paths }}" +``` + +## Troubleshooting + +```sh +systemctl --user status deluge +journalctl --user -u deluge -f +podman logs deluge +``` diff --git a/roles/deluge/defaults/main.yml b/roles/deluge/defaults/main.yml new file mode 100644 index 0000000..ca93c37 --- /dev/null +++ b/roles/deluge/defaults/main.yml @@ -0,0 +1,27 @@ +--- +# Deluge container image and version +deluge_image: lscr.io/linuxserver/deluge +deluge_version: latest + +# Network +deluge_web_port: 8112 +deluge_torrent_port: 6881 + +# Timezone and log level (error/info/debug) +deluge_timezone: UTC +deluge_loglevel: error + +# Storage. Config holds torrent state, auth and web settings; downloads is the +# default save path. Both must live on local storage, not NFS. +deluge_config_dir: "{{ podman_projects_dir }}/deluge/config" +deluge_downloads_dir: "{{ podman_projects_dir }}/deluge/downloads" + +# Backup manifest - consumed by roles/backup via inventory `backup_jobs`. +# Only the config (torrent state, auth, web settings) is worth backing up; +# downloads are user data, not Ansible-reproducible state. +deluge_backup_paths: + - "{{ deluge_config_dir }}" + +# Nginx reverse proxy configuration +deluge_nginx_enabled: false +deluge_nginx_hostname: deluge.nas.local diff --git a/roles/deluge/handlers/main.yml b/roles/deluge/handlers/main.yml new file mode 100644 index 0000000..f74f92e --- /dev/null +++ b/roles/deluge/handlers/main.yml @@ -0,0 +1,20 @@ +--- +- name: Reload systemd user + ansible.builtin.systemd: + daemon_reload: true + scope: user + become: false + become_user: "{{ ansible_user }}" + +- name: Restart Deluge + ansible.builtin.systemd: + name: deluge.service + state: restarted + scope: user + become: false + become_user: "{{ ansible_user }}" + +- name: Reload nginx + ansible.builtin.systemd: + name: nginx + state: reloaded diff --git a/roles/deluge/meta/main.yml b/roles/deluge/meta/main.yml new file mode 100644 index 0000000..03bf76b --- /dev/null +++ b/roles/deluge/meta/main.yml @@ -0,0 +1,5 @@ +--- +# No meta dependencies: podman is a shared service deployed once via the +# playbook (see playbooks/omega.yml role ordering). Declaring it here +# would re-run it on every deluge deploy. +dependencies: [] diff --git a/roles/deluge/tasks/main.yml b/roles/deluge/tasks/main.yml new file mode 100644 index 0000000..cd90f40 --- /dev/null +++ b/roles/deluge/tasks/main.yml @@ -0,0 +1,107 @@ +--- +- name: Get home directory for {{ ansible_user }} + ansible.builtin.getent: + database: passwd + key: "{{ ansible_user }}" + +- name: Set user home directory fact + ansible.builtin.set_fact: + user_home_dir: "{{ ansible_facts['getent_passwd'][ansible_user][4] }}" + +- name: Create Deluge project directory + ansible.builtin.file: + path: "{{ podman_projects_dir | default('/opt/podman') }}/deluge" + state: directory + owner: "{{ ansible_user }}" + group: "{{ ansible_user }}" + mode: "0755" + +- name: Create Deluge state directories + ansible.builtin.file: + path: "{{ item }}" + state: directory + owner: "{{ ansible_user }}" + group: "{{ ansible_user }}" + mode: "0750" + loop: + - "{{ deluge_config_dir }}" + - "{{ deluge_downloads_dir }}" + +- name: Get current Deluge image ID + ansible.builtin.command: "podman image inspect --format {% raw %}{{.Id}}{% endraw %} {{ deluge_image }}:{{ deluge_version }}" + register: deluge_image_before + failed_when: false # image may not exist yet on first deploy + changed_when: false + become: false + become_user: "{{ ansible_user }}" + +- name: Pull Deluge container image + ansible.builtin.command: "podman pull --quiet {{ deluge_image }}:{{ deluge_version }}" + register: deluge_pull + changed_when: deluge_pull.stdout != deluge_image_before.stdout + become: false + become_user: "{{ ansible_user }}" + notify: Restart Deluge + +- name: Deploy Kubernetes YAML for Deluge + ansible.builtin.template: + src: deluge.yaml.j2 + dest: "{{ podman_projects_dir | default('/opt/podman') }}/deluge/deluge.yaml" + owner: "{{ ansible_user }}" + group: "{{ ansible_user }}" + mode: "0644" + notify: Restart Deluge + +- name: Create systemd user directory for Deluge + ansible.builtin.file: + path: "{{ user_home_dir }}/.config/systemd/user" + state: directory + owner: "{{ ansible_user }}" + group: "{{ ansible_user }}" + mode: "0755" + +- name: Create systemd service for Deluge (user scope) + ansible.builtin.template: + src: deluge.service.j2 + dest: "{{ user_home_dir }}/.config/systemd/user/deluge.service" + owner: "{{ ansible_user }}" + group: "{{ ansible_user }}" + mode: "0644" + notify: Reload systemd user + +- name: Enable lingering for user {{ ansible_user }} + ansible.builtin.command: "loginctl enable-linger {{ ansible_user }}" + changed_when: false + when: ansible_user != 'root' + +- name: Enable and start Deluge service (user scope) + ansible.builtin.systemd: + name: deluge.service + enabled: true + state: started + scope: user + become: false + become_user: "{{ ansible_user }}" + +- name: Provision TLS certificate for Deluge + ansible.builtin.include_tasks: "{{ role_path }}/../nginx/tasks/certbot.yml" + vars: + certbot_hostname: "{{ deluge_nginx_hostname }}" + when: deluge_nginx_enabled + +- name: Deploy nginx vhost configuration for Deluge + ansible.builtin.template: + src: nginx-vhost.conf.j2 + dest: "{{ nginx_conf_dir | default('/etc/nginx/conf.d') }}/deluge.conf" + owner: root + group: root + mode: "0644" + when: deluge_nginx_enabled + notify: Reload nginx + +- name: Remove nginx vhost configuration for Deluge + ansible.builtin.file: + path: "{{ nginx_conf_dir | default('/etc/nginx/conf.d') }}/deluge.conf" + state: absent + when: not deluge_nginx_enabled + notify: Reload nginx diff --git a/roles/deluge/templates/deluge.service.j2 b/roles/deluge/templates/deluge.service.j2 new file mode 100644 index 0000000..7d7e02a --- /dev/null +++ b/roles/deluge/templates/deluge.service.j2 @@ -0,0 +1,15 @@ +[Unit] +Description=Deluge BitTorrent Client + +[Service] +Type=notify +NotifyAccess=all +WorkingDirectory={{ podman_projects_dir | default('/opt/podman') }}/deluge +ExecStart=/usr/bin/podman kube play --replace --service-container=true --network=pasta:--map-host-loopback={{ podman_gw_gateway }} deluge.yaml +ExecStop=/usr/bin/podman kube down deluge.yaml +Restart=on-failure +RestartSec=10 +TimeoutStartSec=180 + +[Install] +WantedBy=default.target diff --git a/roles/deluge/templates/deluge.yaml.j2 b/roles/deluge/templates/deluge.yaml.j2 new file mode 100644 index 0000000..1220240 --- /dev/null +++ b/roles/deluge/templates/deluge.yaml.j2 @@ -0,0 +1,60 @@ +--- +apiVersion: v1 +kind: Pod +metadata: + name: deluge + labels: + app: deluge +spec: + containers: + - name: deluge + image: {{ deluge_image }}:{{ deluge_version }} + ports: + - containerPort: {{ deluge_web_port }} + hostPort: {{ deluge_web_port }} + - containerPort: {{ deluge_torrent_port }} + hostPort: {{ deluge_torrent_port }} + - containerPort: {{ deluge_torrent_port }} + hostPort: {{ deluge_torrent_port }} + protocol: UDP + env: + - name: PUID + value: "{{ ansible_facts['getent_passwd'][ansible_user][1] }}" + - name: PGID + value: "{{ ansible_facts['getent_passwd'][ansible_user][2] }}" + - name: TZ + value: "{{ deluge_timezone }}" + - name: DELUGE_LOGLEVEL + value: "{{ deluge_loglevel }}" + - name: UMASK + value: "022" + volumeMounts: + - name: localtime + mountPath: /etc/localtime + readOnly: true + - name: config + mountPath: /config + - name: downloads + mountPath: /downloads + livenessProbe: + tcpSocket: + port: {{ deluge_web_port }} + initialDelaySeconds: 60 + periodSeconds: 30 + timeoutSeconds: 10 + failureThreshold: 3 + restartPolicy: Never + + volumes: + - name: localtime + hostPath: + path: /etc/localtime + type: File + - name: config + hostPath: + path: {{ deluge_config_dir }} + type: Directory + - name: downloads + hostPath: + path: {{ deluge_downloads_dir }} + type: Directory diff --git a/roles/deluge/templates/nginx-vhost.conf.j2 b/roles/deluge/templates/nginx-vhost.conf.j2 new file mode 100644 index 0000000..73e3cb8 --- /dev/null +++ b/roles/deluge/templates/nginx-vhost.conf.j2 @@ -0,0 +1,58 @@ +# Deluge vhost with Let's Encrypt (Certbot) +# Managed by Ansible - DO NOT EDIT MANUALLY + +server { + listen 80; + listen [::]:80; + server_name {{ deluge_nginx_hostname }}; + + # Certbot webroot for ACME challenges + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + # Redirect to HTTPS + location / { + return 301 https://$server_name$request_uri; + } +} + +server { + listen 443 ssl; + listen [::]:443 ssl; + server_name {{ deluge_nginx_hostname }}; + + # Let's Encrypt certificates (managed by Certbot) + ssl_certificate /etc/letsencrypt/live/{{ deluge_nginx_hostname }}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/{{ deluge_nginx_hostname }}/privkey.pem; + + # SSL configuration + ssl_protocols {{ nginx_ssl_protocols | default('TLSv1.3') }}; + ssl_prefer_server_ciphers on; + +{% if nginx_log_backend | default('journald') == 'journald' %} + access_log syslog:server=unix:/dev/log,nohostname,tag=nginx_deluge; + error_log syslog:server=unix:/dev/log,nohostname,tag=nginx_deluge; +{% else %} + access_log /var/log/nginx/{{ deluge_nginx_hostname }}_access.log main; + error_log /var/log/nginx/{{ deluge_nginx_hostname }}_error.log; +{% endif %} + + client_max_body_size 20M; + + location / { + proxy_pass http://127.0.0.1:{{ deluge_web_port }}; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # WebSocket support for real-time UI updates + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $http_connection; + + proxy_buffering off; + proxy_read_timeout 3600s; + } +}