From 2957b584917ce81ffaa907e455f013db8c9edb68 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Thu, 20 Nov 2025 21:27:06 +0100 Subject: [PATCH 1/8] feat: Add Ansible, Terraform gitignore entries --- .gitignore | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/.gitignore b/.gitignore index 383de38..6cfd010 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,65 @@ vaultpass /.ansible /temp/ +# Created by https://www.toptal.com/developers/gitignore/api/-f,linux,markdown,ansible,terraform +# Edit at https://www.toptal.com/developers/gitignore?templates=-f,linux,markdown,ansible,terraform + +#!! ERROR: -f is undefined. Use list command to see defined gitignore types !!# + +### Ansible ### +*.retry + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +#!! ERROR: markdown is undefined. Use list command to see defined gitignore types !!# + +### Terraform ### +# Local .terraform directories +**/.terraform/* + +# .tfstate files +*.tfstate +*.tfstate.* + +# Crash log files +crash.log +crash.*.log + +# Exclude all .tfvars files, which are likely to contain sensitive data, such as +# password, private keys, and other secrets. These should not be part of version +# control as they are data points which are potentially sensitive and subject +# to change depending on the environment. +*.tfvars +*.tfvars.json + +# Ignore override files as they are usually used to override resources locally and so +# are not checked in +override.tf +override.tf.json +*_override.tf +*_override.tf.json + +# Include override files you do wish to add to version control using negated pattern +# !example_override.tf + +# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan +# example: *tfplan* + +# Ignore CLI configuration files +.terraformrc +terraform.rc + +# End of https://www.toptal.com/developers/gitignore/api/-f,linux,markdown,ansible,terraform From 40b687a3f370b454d7a80324d6df900f8cdde86f Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Thu, 20 Nov 2025 21:27:06 +0100 Subject: [PATCH 2/8] feat: Create skeleton for terraform provisioning role The terraform module does not expect its file contents (project_path) in the 'files/' folder like the core roles, instead looking for it relative to the _invocation_ pwd. So, for now it just resides in the root level of the repository and may be moved from there to wherever it is more pertinent. Additionally, we check for the existence of the OpenTofu binary (tofu), and prefer that if it exists. Otherwise we fall back to the Terraform binary. --- roles/infrastructure/tasks/main.yaml | 26 ++++++++++++++++++++++++++ site.yaml | 8 +++++++- tofu/main.tf | 5 +++++ 3 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 roles/infrastructure/tasks/main.yaml create mode 100644 tofu/main.tf diff --git a/roles/infrastructure/tasks/main.yaml b/roles/infrastructure/tasks/main.yaml new file mode 100644 index 0000000..1169ce3 --- /dev/null +++ b/roles/infrastructure/tasks/main.yaml @@ -0,0 +1,26 @@ +--- +# role currently only works with opentofu +# Either manually extend to both or just leave out test? +- name: Check if tofu is installed + vars: + terraform_bin: tofu + ansible.builtin.command: + argv: + - which + - "{{ terraform_bin|quote }}" + check_mode: false # run even in check mode + tags: debug + register: tofu_installed + failed_when: false + changed_when: false + +- name: Run terraform + community.general.terraform: + binary_path: "{{ (tofu_installed.rc in [ 0 ]) | ternary('tofu', 'terraform') }}" + project_path: "tofu/" + state: present + register: output + +- name: Debug output + debug: + var: output diff --git a/site.yaml b/site.yaml index 0f89754..b77d574 100644 --- a/site.yaml +++ b/site.yaml @@ -5,7 +5,6 @@ gather_facts: False become: true tags: - - system - bootstrap tasks: - name: check for python @@ -50,6 +49,13 @@ # name: incus-install # tags: incus +- name: Raise infrastructure + hosts: localhost + tags: infrastructure + tasks: + - ansible.builtin.import_role: + name: infrastructure + # ansible-galaxy install geerlingguy.docker - name: Install docker hosts: instance_system diff --git a/tofu/main.tf b/tofu/main.tf new file mode 100644 index 0000000..ed46066 --- /dev/null +++ b/tofu/main.tf @@ -0,0 +1,5 @@ + +output "my_debug_output" { + description = "just debuggin" + value = 42 +} From b855494cf5f14b004f301aa53d78285ee04844d7 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Thu, 20 Nov 2025 22:44:52 +0100 Subject: [PATCH 3/8] feat: Enable ansible pipelining, Disable python warning Pipelining speeds up the playbook execution. It _can_ have some negative effects on 'sudo' execution, and specifically requires `requiretty` not enabled in the sudoers file. Since this seems (by default) to be the case on debian distributions, I am trying to switch to pipelining for the time being. --- ansible.cfg | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ansible.cfg b/ansible.cfg index 8734d30..25b7877 100644 --- a/ansible.cfg +++ b/ansible.cfg @@ -1,4 +1,11 @@ [defaults] remote_tmp = /tmp inventory = inventory + roles_path = .ansible/roles:roles +collections_path = .ansible/collections + +interpreter_python = auto_silent + +[ssh_connection] +pipelining = True From e5feb235dfb146d7d501be0a2ec73fba2f468ca9 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Fri, 21 Nov 2025 19:10:51 +0100 Subject: [PATCH 4/8] feat(arr): Add fanedits directory to jellyfin media --- roles/arr/templates/compose.yaml.jinja | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/roles/arr/templates/compose.yaml.jinja b/roles/arr/templates/compose.yaml.jinja index dde72e6..c7fdde7 100644 --- a/roles/arr/templates/compose.yaml.jinja +++ b/roles/arr/templates/compose.yaml.jinja @@ -31,7 +31,7 @@ services: - UMASK_SET={{ arrstack_umask_set }} volumes: - "{{ arrstack_env_dir }}/config/radarr:/config" - - "/mnt/ext/data/media/movies:/data/media/movies" # FIXME: Find solution + - "/mnt/ext/data/media/movies:/data/media/movies" # FIXME: Find non-hardcoded solution - "{{ arrstack_serve_dir }}/files/usenet:/data/usenet" - "{{ arrstack_serve_dir }}/files/torrent:/data/torrent" restart: unless-stopped @@ -148,6 +148,7 @@ services: - "{{ arrstack_env_dir }}/data/tdarr:/app/server" - "{{ arrstack_serve_dir }}/media/tv:/media/tv" - "/mnt/ext/data/media/movies:/media/movies" # FIXME: To be changed? + - "/mnt/ext/data/media/fanedits:/media/fanedits" # FIXME: Find non-hardcoded solution - "/transcodes:/transcodes" # TODO: Implement dynamic form with variable? restart: unless-stopped devices: @@ -169,6 +170,7 @@ services: - "{{ arrstack_env_dir }}/config/sabnzbd:/config" - "{{ arrstack_serve_dir }}/media/tv:/data/media/tv" - "/mnt/ext/data/media/movies:/data/media/movies" # FIXME: To be changed? + - "/mnt/ext/data/media/fanedits:/data/media/fanedits" # FIXME: Find non-hardcoded solution ports: - 6767:6767 restart: unless-stopped @@ -320,6 +322,7 @@ services: - "{{ arrstack_env_dir }}/config/jellyfin:/config" - "{{ arrstack_env_dir }}/data/jellyfin:/config/data" - "/mnt/ext/data/media/movies:/media/movies" # FIXME: To be changed? + - "/mnt/ext/data/media/fanedits:/media/fanedits" # FIXME: Find non-hardcoded solution - "{{ arrstack_serve_dir }}/media/tv:/media/tv" - "{{ arrstack_serve_dir }}/media/music:/media/music" ports: # FIXME: how to enable discovery behind proxies? From 9de2aaea489499df52e8c6d779a6e801a8dc3b95 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Tue, 25 Nov 2025 21:43:43 +0100 Subject: [PATCH 5/8] feat(arr): Move arrstack container versions into vars --- roles/arr/templates/compose.yaml.jinja | 34 +++++++++++++------------- roles/arr/vars/main.yaml | 20 ++++++++++++++- 2 files changed, 36 insertions(+), 18 deletions(-) diff --git a/roles/arr/templates/compose.yaml.jinja b/roles/arr/templates/compose.yaml.jinja index c7fdde7..1caff10 100644 --- a/roles/arr/templates/compose.yaml.jinja +++ b/roles/arr/templates/compose.yaml.jinja @@ -1,7 +1,7 @@ services: sonarr: container_name: sonarr - image: lscr.io/linuxserver/sonarr:latest + image: "lscr.io/linuxserver/sonarr:{{ arrstack_sonarr_version }}" networks: - caddy environment: @@ -21,7 +21,7 @@ services: radarr: container_name: radarr - image: lscr.io/linuxserver/radarr:latest + image: "lscr.io/linuxserver/radarr:{{ arrstack_radarr_version }}" networks: - caddy environment: @@ -41,7 +41,7 @@ services: lidarr: container_name: lidarr - image: lscr.io/linuxserver/lidarr:latest + image: "lscr.io/linuxserver/lidarr:{{ arrstack_lidarr_version }}" networks: - caddy environment: @@ -66,7 +66,7 @@ services: readarr: container_name: readarr - image: lscr.io/linuxserver/readarr:develop + image: "lscr.io/linuxserver/readarr:{{ arrstack_readarr_version }}" networks: - caddy environment: @@ -86,7 +86,7 @@ services: prowlarr: container_name: prowlarr - image: lscr.io/linuxserver/prowlarr:develop + image: "lscr.io/linuxserver/prowlarr:{{ arrstack_prowlarr_version }}" networks: - caddy environment: @@ -102,7 +102,7 @@ services: caddy.reverse_proxy: "{{ '{{' }}upstreams 9696{{ '}}'}}" beets: - image: lscr.io/linuxserver/beets:latest + image: "lscr.io/linuxserver/beets:{{ arrstack_beets_version }}" container_name: beets networks: - caddy @@ -123,7 +123,7 @@ services: caddy.reverse_proxy: "{{ '{{' }}upstreams 8337{{ '}}'}}" tdarr: - image: ghcr.io/haveagitgat/tdarr + image: "ghcr.io/haveagitgat/tdarr:{{ arrstack_tdarr_version }}" container_name: tdarr networks: - caddy @@ -158,7 +158,7 @@ services: caddy.reverse_proxy: "{{ '{{' }}upstreams 8265{{ '}}'}}" bazarr: - image: lscr.io/linuxserver/bazarr:latest + image: "lscr.io/linuxserver/bazarr:{{ arrstack_bazarr_version }}" container_name: bazarr networks: - caddy @@ -180,7 +180,7 @@ services: sabnzbd: container_name: sabnzbd - image: lscr.io/linuxserver/sabnzbd:latest + image: "lscr.io/linuxserver/sabnzbd:{{ arrstack_sabnzbd_version }}" networks: - caddy environment: @@ -198,7 +198,7 @@ services: vpn: container_name: vpn - image: qmcgaw/gluetun:v3 + image: "qmcgaw/gluetun:{{ arrstack_gluetun_version }}" networks: - caddy environment: @@ -232,7 +232,7 @@ services: caddy: "{{ arrstack_qbit_subdomain }}" caddy.reverse_proxy: "{{ '{{' }}upstreams 8888{{ '}}'}}" qbittorrent: - image: linuxserver/qbittorrent + image: "linuxserver/qbittorrent:{{ arrstack_qbittorrent_version }}" container_name: qbittorrent environment: - PUID={{ arrstack_puid }} @@ -250,7 +250,7 @@ services: restart: unless-stopped homarr: - image: ghcr.io/ajnart/homarr:latest + image: "ghcr.io/ajnart/homarr:{{ arrstack_homarr_version }}" container_name: homarr networks: - caddy @@ -265,7 +265,7 @@ services: caddy.reverse_proxy: "{{ '{{' }}upstreams 7575{{ '}}'}}" jellyseerr: - image: fallenbagel/jellyseerr:latest + image: "fallenbagel/jellyseerr:{{ arrstack_jellyseerr_version }}" container_name: jellyseerr networks: - caddy @@ -283,7 +283,7 @@ services: audiobookshelf: container_name: audiobookshelf - image: ghcr.io/advplyr/audiobookshelf:latest + image: "ghcr.io/advplyr/audiobookshelf:{{ arrstack_audiobookshelf_version }}" networks: - caddy environment: @@ -302,7 +302,7 @@ services: caddy.reverse_proxy: "{{ '{{' }}upstreams 80{{ '}}'}}" jellyfin: - image: lscr.io/linuxserver/jellyfin:latest + image: "lscr.io/linuxserver/jellyfin:{{ arrstack_jellyfin_version }}" container_name: jellyfin networks: - caddy @@ -334,7 +334,7 @@ services: caddy.reverse_proxy: "{{ '{{' }}upstreams 8096{{ '}}'}}" gonic: - image: sentriz/gonic:latest + image: "sentriz/gonic:{{ arrstack_gonic_version }}" networks: - caddy environment: @@ -354,7 +354,7 @@ services: {% if restic_enable|d(False) == True and arrstack_restic_enable|d(False) == True %} backup: - image: mazzolino/restic + image: "mazzolino/restic:{{ arrstack_restic_version }}" hostname: "{{ ansible_hostname | default() }}" environment: TZ: "{{ restic_tz }}" diff --git a/roles/arr/vars/main.yaml b/roles/arr/vars/main.yaml index c17c9bb..d392c4c 100644 --- a/roles/arr/vars/main.yaml +++ b/roles/arr/vars/main.yaml @@ -1,2 +1,20 @@ --- -# vars file for arr + +arrstack_sonarr_version: latest +arrstack_radarr_version: latest +arrstack_lidarr_version: latest +arrstack_readarr_version: develop +arrstack_prowlarr_version: develop +arrstack_beets_version: latest +arrstack_tdarr_version: latest +arrstack_bazarr_version: latest +arrstack_sabnzbd_version: latest +arrstack_gluetun_version: v3 +arrstack_qbittorrent_version: latest +arrstack_homarr_version: latest +arrstack_jellyseerr_version: latest +arrstack_audiobookshelf_version: latest +arrstack_jellyfin_version: latest +arrstack_gonic_version: latest + +arrstack_restic_version: latest # TODO: Should maybe be set in restic role instead? From 7f56c80cf45cb0d1d4f782355c8643068ab85d0b Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Tue, 25 Nov 2025 21:53:49 +0100 Subject: [PATCH 6/8] ref(arr): Pin jellyfin container version Since jellyfin version 10.11.0 is a _massive_ upgrade, including non-backwards compatible db migration, we pin the version for now. See: https://jellyfin.org/posts/jellyfin-release-10.11.0/ --- roles/arr/vars/main.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/arr/vars/main.yaml b/roles/arr/vars/main.yaml index d392c4c..5736b2a 100644 --- a/roles/arr/vars/main.yaml +++ b/roles/arr/vars/main.yaml @@ -14,7 +14,7 @@ arrstack_qbittorrent_version: latest arrstack_homarr_version: latest arrstack_jellyseerr_version: latest arrstack_audiobookshelf_version: latest -arrstack_jellyfin_version: latest +arrstack_jellyfin_version: "10.10.7" arrstack_gonic_version: latest arrstack_restic_version: latest # TODO: Should maybe be set in restic role instead? From ef1823da20a83f7fde05441be7ac3104211d1f27 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Wed, 26 Nov 2025 15:48:37 +0100 Subject: [PATCH 7/8] chore(arr): Pin jellyfin to updated version Moved the jellyfin installation to 10.11.x, so now we should pin it to a minimum of that. Also, since the 'latest' container for the linuxserver container images is still the 10.10.7 container, we can't just use that. So we pin the exact version for now instead. --- roles/arr/vars/main.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/arr/vars/main.yaml b/roles/arr/vars/main.yaml index 5736b2a..8d382a0 100644 --- a/roles/arr/vars/main.yaml +++ b/roles/arr/vars/main.yaml @@ -14,7 +14,7 @@ arrstack_qbittorrent_version: latest arrstack_homarr_version: latest arrstack_jellyseerr_version: latest arrstack_audiobookshelf_version: latest -arrstack_jellyfin_version: "10.10.7" +arrstack_jellyfin_version: "10.11.3" arrstack_gonic_version: latest arrstack_restic_version: latest # TODO: Should maybe be set in restic role instead? From a5a6e297ffade245ff5efdb71e4c551757b3575f Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Fri, 28 Nov 2025 14:06:07 +0100 Subject: [PATCH 8/8] feat(nfs): Restrict server to v4 by default Can be changed with `nfs_v4_only=false` which defaults to true. Information taken from: https://wiki.debian.org/NFSServerSetup and applied directly through Ansible. Currently _irreversible_, meaning once we set the server to v4 only there is NO ansible-supported playbook to reset it to all NFSv2/3/4 versions. Has to be done manually, or could be included as manually-run playbook. --- roles/nfs/defaults/main.yaml | 3 ++ roles/nfs/tasks/main.yaml | 6 +++- roles/nfs/tasks/nfs-v4-only.yaml | 48 ++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 roles/nfs/tasks/nfs-v4-only.yaml diff --git a/roles/nfs/defaults/main.yaml b/roles/nfs/defaults/main.yaml index f8088e6..8accc81 100644 --- a/roles/nfs/defaults/main.yaml +++ b/roles/nfs/defaults/main.yaml @@ -4,3 +4,6 @@ nfs_export_lines: - "/srv/media 192.168.0.0/24(rw,async,no_subtree_check) 100.112.0.0/16(rw,async,no_subtree_check)" - "/srv/files 192.168.0.0/24(rw,async,no_subtree_check) 100.112.0.0/16(rw,async,no_subtree_check)" - "/mnt/ext/data/videos 192.168.0.0/24(rw,async,no_subtree_check) 100.112.0.0/16(rw,async,no_subtree_check)" + +nfs_v4_only: true +nfs_v4_disable_rpcbind_fallback: false # needed by Debian 13 diff --git a/roles/nfs/tasks/main.yaml b/roles/nfs/tasks/main.yaml index 165ea36..478a880 100644 --- a/roles/nfs/tasks/main.yaml +++ b/roles/nfs/tasks/main.yaml @@ -10,7 +10,7 @@ ansible.builtin.template: src: exports.jinja dest: /etc/exports - mode: '0644' + mode: "0644" become: true notify: Reload nfs service @@ -22,6 +22,10 @@ become: true loop: "{{ nfs_export_lines }}" +- name: Disable NFSv2/NFSv3 to leave NFSv4-only server + ansible.builtin.include_tasks: "nfs-v4-only.yaml" + when: "nfs_v4_only" + - name: Enable nfs server unit ansible.builtin.systemd: enabled: true diff --git a/roles/nfs/tasks/nfs-v4-only.yaml b/roles/nfs/tasks/nfs-v4-only.yaml new file mode 100644 index 0000000..5c6bf00 --- /dev/null +++ b/roles/nfs/tasks/nfs-v4-only.yaml @@ -0,0 +1,48 @@ +--- +- name: Configure /etc/default/nfs-common for NFSv4-only + ansible.builtin.lineinfile: + path: /etc/default/nfs-common + regexp: '^(# *)?{{ item.key }}=.*' + line: '{{ item.key }}={{ item.val }}' + loop: + - { key: NEED_STATD, val: '"no"' } + - { key: NEED_IDMAPD, val: '"yes"' } + become: true + notify: Reload nfs service + +- name: Configure /etc/default/nfs-kernel-server for NFSv4-only + ansible.builtin.lineinfile: + path: /etc/default/nfs-kernel-server + regexp: '^(# *)?{{ item.key }}=.*' + line: '{{ item.key }}={{ item.val }}' + create: true # in case the file or the var is missing + loop: + - { key: RPCNFSDOPTS, val: '"--no-nfs-version 2 --no-nfs-version 3"' } + - { key: RPCMOUNTDOPTS, val: '"--manage-gids --no-nfs-version 2 --no-nfs-version 3"' } + become: true + notify: Reload nfs service + + # This _can_ be used on very modern kernels, but disables + # the rpcbind fallback if nfsdctl lockd configuration fails. + # Debian 13 still requires this so it is disabled by default +- name: Mask rpcbind units (not needed for NFSv4) + ansible.builtin.systemd: + name: "{{ item }}" + masked: true + state: stopped + loop: + - rpcbind.service + - rpcbind.socket + become: true + when: "nfs_v4_disable_rpcbind_fallback" + +- name: Unmask rpcbind units to keep as fallback + ansible.builtin.systemd: + name: "{{ item }}" + masked: false + state: started + loop: + - rpcbind.socket + - rpcbind.service + become: true + when: "not nfs_v4_disable_rpcbind_fallback"