Initial commit
This commit is contained in:
commit
c482d75d5f
|
|
@ -0,0 +1,48 @@
|
||||||
|
# Ansible variable files (sensitive / environment-specific)
|
||||||
|
group_vars/
|
||||||
|
host_vars/
|
||||||
|
commands/
|
||||||
|
|
||||||
|
# Inventory files (often environment-specific and not meant for sharing)
|
||||||
|
inventory/*
|
||||||
|
inventory/group_vars/*
|
||||||
|
inventories/*
|
||||||
|
|
||||||
|
# Keep sanitized examples
|
||||||
|
!inventory/
|
||||||
|
!inventory/hosts.example.yml
|
||||||
|
!inventory/group_vars/
|
||||||
|
!inventory/group_vars/*.example.yml
|
||||||
|
|
||||||
|
# Codex/local assistant config
|
||||||
|
.codex/
|
||||||
|
|
||||||
|
# Ansible Vault / secrets
|
||||||
|
*.vault
|
||||||
|
*.secret
|
||||||
|
*.secrets
|
||||||
|
secrets.yml
|
||||||
|
vault.yml
|
||||||
|
**/vault.yml
|
||||||
|
**/secrets.yml
|
||||||
|
|
||||||
|
# Local environment files
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Optional: Ansible retry files
|
||||||
|
*.retry
|
||||||
|
|
||||||
|
# Python/cache
|
||||||
|
__pycache__/
|
||||||
|
.pytest_cache/
|
||||||
|
|
||||||
|
# OS/editor noise
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
- name: Bootstrap SSH access
|
||||||
|
hosts: all
|
||||||
|
become: true
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: Ensure .ssh directory exists
|
||||||
|
file:
|
||||||
|
path: "/home/{{ bootstrap_user }}/.ssh"
|
||||||
|
state: directory
|
||||||
|
owner: "{{ container_user }}"
|
||||||
|
group: "{{ container_group }}"
|
||||||
|
mode: "0700"
|
||||||
|
|
||||||
|
- name: Install authorized key
|
||||||
|
authorized_key:
|
||||||
|
user: "{{ bootstrap_user }}"
|
||||||
|
state: present
|
||||||
|
key: "{{ lookup('file', '~/.ssh/id_ed25519.pub') }}"
|
||||||
|
|
||||||
|
- name: Allow passwordless sudo
|
||||||
|
copy:
|
||||||
|
dest: /etc/sudoers.d/{{ bootstrap_user }}
|
||||||
|
content: "{{ bootstrap_user }} ALL=(ALL) NOPASSWD:ALL\n"
|
||||||
|
mode: "0440"
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
---
|
||||||
|
# Global settings shared by all inventory groups.
|
||||||
|
|
||||||
|
timezone: America/New_York
|
||||||
|
container_user: ansible
|
||||||
|
container_group: ansible
|
||||||
|
container_uid: 1000
|
||||||
|
container_runtime_dir: "/run/user/{{ container_uid }}"
|
||||||
|
|
||||||
|
bootstrap_user: "{{ container_user }}"
|
||||||
|
deployment_directory: "/home/{{ container_user }}/sbots1"
|
||||||
|
|
||||||
|
# Stack directories
|
||||||
|
stack_root: "/home/{{ container_user }}/lab"
|
||||||
|
|
||||||
|
adguard_dir: "{{ stack_root }}/adguard"
|
||||||
|
prowlarr_dir: "{{ stack_root }}/prowlarr"
|
||||||
|
qbittorrent_dir: "{{ stack_root }}/qbittorrent"
|
||||||
|
radarr_dir: "{{ stack_root }}/radarr"
|
||||||
|
sonarr_dir: "{{ stack_root }}/sonarr"
|
||||||
|
bazarr_dir: "{{ stack_root }}/bazarr"
|
||||||
|
caddy_dir: "{{ stack_root }}/caddy"
|
||||||
|
nas01_dir: /mnt/nas01
|
||||||
|
downloads_root: "{{ nas01_dir }}"
|
||||||
|
|
||||||
|
# Container paths
|
||||||
|
container_config_dir: "/home/{{ container_user }}/.config/containers/systemd"
|
||||||
|
|
||||||
|
adguard_base_directories:
|
||||||
|
- "{{ stack_root }}"
|
||||||
|
- "{{ adguard_dir }}/work/data"
|
||||||
|
- "{{ adguard_dir }}/conf"
|
||||||
|
|
||||||
|
servarr_base_directories:
|
||||||
|
- "{{ stack_root }}"
|
||||||
|
- "{{ prowlarr_dir }}/config"
|
||||||
|
- "{{ qbittorrent_dir }}/config"
|
||||||
|
- "{{ radarr_dir }}/config"
|
||||||
|
- "{{ sonarr_dir }}/config"
|
||||||
|
- "{{ bazarr_dir }}/config"
|
||||||
|
|
||||||
|
caddy_base_directories:
|
||||||
|
- "{{ stack_root }}"
|
||||||
|
- "{{ caddy_dir }}/data"
|
||||||
|
- "{{ caddy_dir }}/config/caddy"
|
||||||
|
- "{{ container_config_dir }}"
|
||||||
|
|
||||||
|
storage_tree:
|
||||||
|
- data
|
||||||
|
- data/media/movies
|
||||||
|
- data/media/tv
|
||||||
|
- data/media/music
|
||||||
|
- data/torrents/movies
|
||||||
|
- data/torrents/tv
|
||||||
|
- data/torrents/music
|
||||||
|
- data/torrents/incomplete
|
||||||
|
- data/torrents/complete
|
||||||
|
|
||||||
|
arr_suite:
|
||||||
|
- qbittorrent
|
||||||
|
- prowlarr
|
||||||
|
- radarr
|
||||||
|
- sonarr
|
||||||
|
- bazarr
|
||||||
|
- flaresolverr
|
||||||
|
|
||||||
|
base_firewall_rules:
|
||||||
|
- service: ssh
|
||||||
|
|
||||||
|
adguard_firewall_rules:
|
||||||
|
- port: 53/tcp
|
||||||
|
- port: 53/udp
|
||||||
|
- port: 80/tcp
|
||||||
|
- port: 443/tcp
|
||||||
|
- port: 3000/tcp
|
||||||
|
|
||||||
|
nfs_exports:
|
||||||
|
- path: "{{ nas01_dir }}"
|
||||||
|
clients:
|
||||||
|
- "192.168.1.11(rw,sync,no_subtree_check,fsid=0)"
|
||||||
|
- "192.168.1.12(rw,sync,no_subtree_check,fsid=0)"
|
||||||
|
- "192.168.1.14(rw,sync,no_subtree_check,fsid=0)"
|
||||||
|
|
||||||
|
adguard_admin_user: ansible
|
||||||
|
adguard_admin_password_hash: "replace-with-adguard-bcrypt-hash"
|
||||||
|
|
||||||
|
nfs_server_name: 192.168.1.10
|
||||||
|
nfs_export_location: /
|
||||||
|
nfs_export_path: "{{ nas01_dir }}"
|
||||||
|
nfs_export_type: nfs4
|
||||||
|
nfs_export_options: rw,sync,no_subtree_check,fsid=0
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
---
|
||||||
|
# Gluetun SELinux access
|
||||||
|
selinux_allow_gluetun: true
|
||||||
|
|
||||||
|
# Servarr stack
|
||||||
|
servarr_stack:
|
||||||
|
- src: qbittorrent.container.j2
|
||||||
|
dest: qbittorrent.container
|
||||||
|
- src: prowlarr.container.j2
|
||||||
|
dest: prowlarr.container
|
||||||
|
- src: radarr.container.j2
|
||||||
|
dest: radarr.container
|
||||||
|
- src: sonarr.container.j2
|
||||||
|
dest: sonarr.container
|
||||||
|
- src: bazarr.container.j2
|
||||||
|
dest: bazarr.container
|
||||||
|
- src: flaresolverr.container.j2
|
||||||
|
dest: flaresolverr.container
|
||||||
|
|
||||||
|
# Gluetun setup
|
||||||
|
vpn_provider: mullvad
|
||||||
|
vpn_type: wireguard
|
||||||
|
vpn_countries: "Netherlands,USA,Canada"
|
||||||
|
vpn_private_key: "replace-with-wireguard-private-key"
|
||||||
|
vpn_addresses: "10.0.0.2/32"
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
---
|
||||||
|
# Caddy
|
||||||
|
adguard_domain: "adguard.example.{{ caddy_node }}"
|
||||||
|
adguard_upstream: adguard:80
|
||||||
|
|
||||||
|
qbittorrent_domain: "qbittorrent.example.{{ caddy_node }}"
|
||||||
|
qbittorrent_upstream: host.containers.internal:8080
|
||||||
|
|
||||||
|
prowlarr_domain: "prowlarr.example.{{ caddy_node }}"
|
||||||
|
prowlarr_upstream: host.containers.internal:9696
|
||||||
|
|
||||||
|
radarr_domain: "radarr.example.{{ caddy_node }}"
|
||||||
|
radarr_upstream: host.containers.internal:7878
|
||||||
|
|
||||||
|
sonarr_domain: "sonarr.example.{{ caddy_node }}"
|
||||||
|
sonarr_upstream: host.containers.internal:8989
|
||||||
|
|
||||||
|
bazarr_domain: "bazarr.example.{{ caddy_node }}"
|
||||||
|
bazarr_upstream: host.containers.internal:6767
|
||||||
|
|
||||||
|
caddy_email: "admin@example.{{ caddy_node }}"
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
---
|
||||||
|
# Media host variables go here when the media role needs host-specific settings.
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
---
|
||||||
|
# MergerFS
|
||||||
|
drive_a_path: /mnt/disk1
|
||||||
|
drive_b_path: /mnt/disk2
|
||||||
|
|
||||||
|
mergerfs_path: /mnt/nas01
|
||||||
|
mergerfs_opts: "defaults,allow_other,use_ino,category.create=mfs"
|
||||||
|
|
||||||
|
storage_backends:
|
||||||
|
- "{{ drive_a_path }}"
|
||||||
|
- "{{ drive_b_path }}"
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
---
|
||||||
|
caddy_node: com
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
---
|
||||||
|
caddy_node: test
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
---
|
||||||
|
# Workstation host variables go here when workstation-specific settings are needed.
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
all:
|
||||||
|
children:
|
||||||
|
|
||||||
|
test:
|
||||||
|
hosts:
|
||||||
|
nas-test:
|
||||||
|
ansible_host: 192.168.122.10
|
||||||
|
ansible_user: ansible
|
||||||
|
env: test
|
||||||
|
media-test:
|
||||||
|
ansible_host: 192.168.122.11
|
||||||
|
ansible_user: ansible
|
||||||
|
env: test
|
||||||
|
controller-test:
|
||||||
|
ansible_host: 192.168.122.12
|
||||||
|
ansible_user: ansible
|
||||||
|
env: test
|
||||||
|
bots-test:
|
||||||
|
ansible_host: 192.168.122.13
|
||||||
|
ansible_user: ansible
|
||||||
|
env: test
|
||||||
|
workstation-test:
|
||||||
|
ansible_host: 192.168.122.14
|
||||||
|
ansible_user: ansible
|
||||||
|
env: test
|
||||||
|
|
||||||
|
prod:
|
||||||
|
hosts:
|
||||||
|
nas01:
|
||||||
|
ansible_host: 192.168.1.10
|
||||||
|
ansible_user: ansible
|
||||||
|
env: prod
|
||||||
|
media1:
|
||||||
|
ansible_host: 192.168.1.11
|
||||||
|
ansible_user: ansible
|
||||||
|
env: prod
|
||||||
|
bots1:
|
||||||
|
ansible_host: 192.168.1.12
|
||||||
|
ansible_user: ansible
|
||||||
|
env: prod
|
||||||
|
controller1:
|
||||||
|
ansible_host: 192.168.1.13
|
||||||
|
ansible_user: ansible
|
||||||
|
env: prod
|
||||||
|
workstation1:
|
||||||
|
ansible_host: 192.168.1.14
|
||||||
|
ansible_user: ansible
|
||||||
|
env: prod
|
||||||
|
|
||||||
|
nas:
|
||||||
|
hosts:
|
||||||
|
nas-test:
|
||||||
|
nas01:
|
||||||
|
|
||||||
|
media:
|
||||||
|
hosts:
|
||||||
|
media-test:
|
||||||
|
media1:
|
||||||
|
|
||||||
|
controller:
|
||||||
|
hosts:
|
||||||
|
controller-test:
|
||||||
|
controller1:
|
||||||
|
|
||||||
|
bots:
|
||||||
|
hosts:
|
||||||
|
bots-test:
|
||||||
|
bots1:
|
||||||
|
|
||||||
|
workstation:
|
||||||
|
hosts:
|
||||||
|
workstation-test:
|
||||||
|
workstation1:
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
- name: Storage
|
||||||
|
hosts: nas
|
||||||
|
become: true
|
||||||
|
roles:
|
||||||
|
- base_os
|
||||||
|
- firewall_base
|
||||||
|
- container_runtime
|
||||||
|
- storage_client
|
||||||
|
- nfs_server
|
||||||
|
|
||||||
|
- name: Jellyfin
|
||||||
|
hosts: media
|
||||||
|
become: true
|
||||||
|
roles:
|
||||||
|
- base_os
|
||||||
|
- firewall_base
|
||||||
|
- container_runtime
|
||||||
|
|
||||||
|
- name: Bots
|
||||||
|
hosts: bots
|
||||||
|
become: true
|
||||||
|
roles:
|
||||||
|
- base_os
|
||||||
|
- firewall_base
|
||||||
|
- container_runtime
|
||||||
|
- nfs_client
|
||||||
|
- servarr
|
||||||
|
|
||||||
|
- name: DNS
|
||||||
|
hosts: controller
|
||||||
|
become: true
|
||||||
|
roles:
|
||||||
|
- base_os
|
||||||
|
- firewall_base
|
||||||
|
- container_runtime
|
||||||
|
- adguard
|
||||||
|
- caddy
|
||||||
|
|
||||||
|
- name: Workstation Setup
|
||||||
|
hosts: workstation
|
||||||
|
become: true
|
||||||
|
roles:
|
||||||
|
- base_os
|
||||||
|
- firewall_base
|
||||||
|
- container_runtime
|
||||||
|
- selinux_containers
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
# adguard/meta/main.yml
|
||||||
|
dependencies:
|
||||||
|
- role: selinux_containers
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
---
|
||||||
|
#adguard/tasks/firewall.yml
|
||||||
|
- name: Open required firewall rules
|
||||||
|
become: true
|
||||||
|
ansible.posix.firewalld:
|
||||||
|
port: "{{ item.port | default(omit) }}"
|
||||||
|
service: "{{ item.service | default(omit) }}"
|
||||||
|
permanent: true
|
||||||
|
state: enabled
|
||||||
|
immediate: true
|
||||||
|
loop: "{{ adguard_firewall_rules }}"
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
---
|
||||||
|
#adguard/tasks/main.yml
|
||||||
|
- import_tasks: firewall.yml
|
||||||
|
|
||||||
|
- name: Create stack and config directories
|
||||||
|
file:
|
||||||
|
path: "{{ item }}"
|
||||||
|
state: directory
|
||||||
|
owner: "{{ container_user }}"
|
||||||
|
group: "{{ container_group }}"
|
||||||
|
mode: "0755"
|
||||||
|
recurse: yes
|
||||||
|
loop: "{{ adguard_base_directories }}"
|
||||||
|
|
||||||
|
- name: Directory SELinux requirement
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
selinux_container_paths: "{{ adguard_base_directories }}"
|
||||||
|
|
||||||
|
- import_role:
|
||||||
|
name: selinux_containers
|
||||||
|
tasks_from: labels
|
||||||
|
|
||||||
|
- name: Ensure container app config directories are owned by container UID
|
||||||
|
become: true
|
||||||
|
file:
|
||||||
|
path: "{{ adguard_dir }}/conf"
|
||||||
|
state: directory
|
||||||
|
owner: "{{ container_user }}"
|
||||||
|
group: "{{ container_group }}"
|
||||||
|
recurse: true
|
||||||
|
|
||||||
|
- name: Deploy AdGuard configuration template
|
||||||
|
template:
|
||||||
|
src: AdGuardHome.yaml.j2
|
||||||
|
dest: "{{ stack_root }}/adguard/conf/AdGuardHome.yaml"
|
||||||
|
mode: '0600'
|
||||||
|
|
||||||
|
- name: Deploy AdGuard Quadlet
|
||||||
|
template:
|
||||||
|
src: adguard.container.j2
|
||||||
|
dest: "{{ container_config_dir }}/adguard.container"
|
||||||
|
|
||||||
|
- name: Force systemd reload (blocking)
|
||||||
|
become: true
|
||||||
|
become_user: "{{ container_user }}"
|
||||||
|
command: systemctl --user daemon-reload
|
||||||
|
|
||||||
|
- name: Wait for quadlet generation
|
||||||
|
pause:
|
||||||
|
seconds: 1
|
||||||
|
|
||||||
|
- name: Start and enable AdGuard service
|
||||||
|
become: true
|
||||||
|
become_user: "{{ container_user }}"
|
||||||
|
systemd:
|
||||||
|
name: adguard.service
|
||||||
|
scope: user
|
||||||
|
state: started
|
||||||
|
|
@ -0,0 +1,194 @@
|
||||||
|
http:
|
||||||
|
pprof:
|
||||||
|
port: 6060
|
||||||
|
enabled: false
|
||||||
|
doh:
|
||||||
|
routes:
|
||||||
|
- GET /dns-query
|
||||||
|
- POST /dns-query
|
||||||
|
- GET /dns-query/{ClientID}
|
||||||
|
- POST /dns-query/{ClientID}
|
||||||
|
insecure_enabled: false
|
||||||
|
address: 0.0.0.0:80
|
||||||
|
session_ttl: 720h
|
||||||
|
users:
|
||||||
|
- name: "{{ adguard_admin_user }}"
|
||||||
|
password: "{{ adguard_admin_password_hash }}"
|
||||||
|
auth_attempts: 5
|
||||||
|
block_auth_min: 15
|
||||||
|
http_proxy: ""
|
||||||
|
language: ""
|
||||||
|
theme: auto
|
||||||
|
dns:
|
||||||
|
bind_hosts:
|
||||||
|
- 0.0.0.0
|
||||||
|
port: 53
|
||||||
|
anonymize_client_ip: false
|
||||||
|
ratelimit: 20
|
||||||
|
ratelimit_subnet_len_ipv4: 24
|
||||||
|
ratelimit_subnet_len_ipv6: 56
|
||||||
|
ratelimit_whitelist: []
|
||||||
|
refuse_any: true
|
||||||
|
upstream_dns:
|
||||||
|
- https://dns10.quad9.net/dns-query
|
||||||
|
upstream_dns_file: ""
|
||||||
|
bootstrap_dns:
|
||||||
|
- 9.9.9.10
|
||||||
|
- 149.112.112.10
|
||||||
|
- 2620:fe::10
|
||||||
|
- 2620:fe::fe:10
|
||||||
|
fallback_dns: []
|
||||||
|
upstream_mode: load_balance
|
||||||
|
fastest_timeout: 1s
|
||||||
|
allowed_clients: []
|
||||||
|
disallowed_clients: []
|
||||||
|
blocked_hosts:
|
||||||
|
- version.bind
|
||||||
|
- id.server
|
||||||
|
- hostname.bind
|
||||||
|
trusted_proxies:
|
||||||
|
- 127.0.0.0/8
|
||||||
|
- ::1/128
|
||||||
|
cache_enabled: true
|
||||||
|
cache_size: 4194304
|
||||||
|
cache_ttl_min: 0
|
||||||
|
cache_ttl_max: 0
|
||||||
|
cache_optimistic: false
|
||||||
|
cache_optimistic_answer_ttl: 30s
|
||||||
|
cache_optimistic_max_age: 12h
|
||||||
|
bogus_nxdomain: []
|
||||||
|
aaaa_disabled: false
|
||||||
|
enable_dnssec: false
|
||||||
|
edns_client_subnet:
|
||||||
|
custom_ip: ""
|
||||||
|
enabled: false
|
||||||
|
use_custom: false
|
||||||
|
max_goroutines: 300
|
||||||
|
handle_ddr: true
|
||||||
|
ipset: []
|
||||||
|
ipset_file: ""
|
||||||
|
bootstrap_prefer_ipv6: false
|
||||||
|
upstream_timeout: 10s
|
||||||
|
private_networks: []
|
||||||
|
use_private_ptr_resolvers: true
|
||||||
|
local_ptr_upstreams: []
|
||||||
|
use_dns64: false
|
||||||
|
dns64_prefixes: []
|
||||||
|
serve_http3: false
|
||||||
|
use_http3_upstreams: false
|
||||||
|
serve_plain_dns: true
|
||||||
|
hostsfile_enabled: true
|
||||||
|
pending_requests:
|
||||||
|
enabled: true
|
||||||
|
tls:
|
||||||
|
enabled: false
|
||||||
|
server_name: ""
|
||||||
|
force_https: false
|
||||||
|
port_https: 443
|
||||||
|
port_dns_over_tls: 853
|
||||||
|
port_dns_over_quic: 853
|
||||||
|
port_dnscrypt: 0
|
||||||
|
dnscrypt_config_file: ""
|
||||||
|
certificate_chain: ""
|
||||||
|
private_key: ""
|
||||||
|
certificate_path: ""
|
||||||
|
private_key_path: ""
|
||||||
|
strict_sni_check: false
|
||||||
|
querylog:
|
||||||
|
dir_path: ""
|
||||||
|
ignored: []
|
||||||
|
interval: 2160h
|
||||||
|
size_memory: 1000
|
||||||
|
enabled: true
|
||||||
|
ignored_enabled: false
|
||||||
|
file_enabled: true
|
||||||
|
statistics:
|
||||||
|
dir_path: ""
|
||||||
|
ignored: []
|
||||||
|
interval: 24h
|
||||||
|
enabled: true
|
||||||
|
ignored_enabled: false
|
||||||
|
filters:
|
||||||
|
- enabled: true
|
||||||
|
url: https://adguardteam.github.io/HostlistsRegistry/assets/filter_1.txt
|
||||||
|
name: AdGuard DNS filter
|
||||||
|
id: 1
|
||||||
|
- enabled: false
|
||||||
|
url: https://adguardteam.github.io/HostlistsRegistry/assets/filter_2.txt
|
||||||
|
name: AdAway Default Blocklist
|
||||||
|
id: 2
|
||||||
|
whitelist_filters: []
|
||||||
|
user_rules: []
|
||||||
|
dhcp:
|
||||||
|
enabled: false
|
||||||
|
interface_name: ""
|
||||||
|
local_domain_name: lan
|
||||||
|
dhcpv4:
|
||||||
|
gateway_ip: ""
|
||||||
|
subnet_mask: ""
|
||||||
|
range_start: ""
|
||||||
|
range_end: ""
|
||||||
|
lease_duration: 86400
|
||||||
|
icmp_timeout_msec: 1000
|
||||||
|
options: []
|
||||||
|
dhcpv6:
|
||||||
|
range_start: ""
|
||||||
|
lease_duration: 86400
|
||||||
|
ra_slaac_only: false
|
||||||
|
ra_allow_slaac: false
|
||||||
|
filtering:
|
||||||
|
blocking_ipv4: ""
|
||||||
|
blocking_ipv6: ""
|
||||||
|
blocked_services:
|
||||||
|
schedule:
|
||||||
|
time_zone: UTC
|
||||||
|
ids: []
|
||||||
|
protection_disabled_until: null
|
||||||
|
safe_search:
|
||||||
|
enabled: false
|
||||||
|
bing: true
|
||||||
|
duckduckgo: true
|
||||||
|
ecosia: true
|
||||||
|
google: true
|
||||||
|
pixabay: true
|
||||||
|
yandex: true
|
||||||
|
youtube: true
|
||||||
|
blocking_mode: default
|
||||||
|
parental_block_host: family-block.dns.adguard.com
|
||||||
|
safebrowsing_block_host: standard-block.dns.adguard.com
|
||||||
|
rewrites: []
|
||||||
|
safe_fs_patterns:
|
||||||
|
- /opt/adguardhome/work/userfilters/*
|
||||||
|
safebrowsing_cache_size: 1048576
|
||||||
|
safesearch_cache_size: 1048576
|
||||||
|
parental_cache_size: 1048576
|
||||||
|
cache_time: 30
|
||||||
|
filters_update_interval: 24
|
||||||
|
blocked_response_ttl: 10
|
||||||
|
filtering_enabled: true
|
||||||
|
rewrites_enabled: true
|
||||||
|
parental_enabled: false
|
||||||
|
safebrowsing_enabled: false
|
||||||
|
protection_enabled: true
|
||||||
|
clients:
|
||||||
|
runtime_sources:
|
||||||
|
whois: true
|
||||||
|
arp: true
|
||||||
|
rdns: true
|
||||||
|
dhcp: true
|
||||||
|
hosts: true
|
||||||
|
persistent: []
|
||||||
|
log:
|
||||||
|
enabled: true
|
||||||
|
file: ""
|
||||||
|
max_backups: 0
|
||||||
|
max_size: 100
|
||||||
|
max_age: 3
|
||||||
|
compress: false
|
||||||
|
local_time: false
|
||||||
|
verbose: false
|
||||||
|
os:
|
||||||
|
group: ""
|
||||||
|
user: ""
|
||||||
|
rlimit_nofile: 0
|
||||||
|
schema_version: 34
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
[Unit]
|
||||||
|
Description=AdGuard Home DNS
|
||||||
|
Wants=network-online.target
|
||||||
|
After=network-online.target homelab-network.service
|
||||||
|
Requires=homelab-network.service
|
||||||
|
|
||||||
|
[Container]
|
||||||
|
Image=docker.io/adguard/adguardhome:latest
|
||||||
|
ContainerName=adguard
|
||||||
|
Network=homelab:alias=adguard
|
||||||
|
|
||||||
|
Volume={{ adguard_dir }}/work:/opt/adguardhome/work
|
||||||
|
Volume={{ adguard_dir }}/conf:/opt/adguardhome/conf
|
||||||
|
|
||||||
|
PublishPort=53:53/tcp
|
||||||
|
PublishPort=53:53/udp
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Restart=always
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
#base_os/defaults/main.yml
|
||||||
|
base_os_install_packages:
|
||||||
|
- slirp4netns
|
||||||
|
- fuse-overlayfs
|
||||||
|
- policycoreutils-python-utils
|
||||||
|
- nano
|
||||||
|
- tree
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
---
|
||||||
|
#base_os/tasks/main.yml
|
||||||
|
- import_tasks: time_sync.yml
|
||||||
|
|
||||||
|
- name: Install required base packages
|
||||||
|
become: true
|
||||||
|
dnf:
|
||||||
|
name: "{{ item }}"
|
||||||
|
state: present
|
||||||
|
loop: "{{ base_os_install_packages }}"
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
---
|
||||||
|
#base_os/tasks/time_sync.yml
|
||||||
|
- name: Chrony time sync (dev only)
|
||||||
|
when: env == "test"
|
||||||
|
block:
|
||||||
|
|
||||||
|
- name: Ensure chronyd is running
|
||||||
|
become: true
|
||||||
|
ansible.builtin.service:
|
||||||
|
name: chronyd
|
||||||
|
state: started
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
- name: Wait for chrony to have reachable sources
|
||||||
|
become: true
|
||||||
|
command: chronyc activity
|
||||||
|
register: chrony_activity
|
||||||
|
retries: 20
|
||||||
|
delay: 2
|
||||||
|
until: "'sources online' in chrony_activity.stdout and '0 sources online' not in chrony_activity.stdout"
|
||||||
|
|
||||||
|
- name: Force time step correction
|
||||||
|
become: true
|
||||||
|
command: chronyc -a makestep
|
||||||
|
|
||||||
|
- name: Verify system time is reasonable
|
||||||
|
command: date
|
||||||
|
register: date_check
|
||||||
|
failed_when: "'2026-04-13' in date_check.stdout"
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
# caddy/meta/main.yml
|
||||||
|
dependencies:
|
||||||
|
- role: selinux_containers
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
---
|
||||||
|
#caddy/tasks/main.yml
|
||||||
|
- name: Create stack and config directories
|
||||||
|
file:
|
||||||
|
path: "{{ item }}"
|
||||||
|
state: directory
|
||||||
|
owner: "{{ container_user }}"
|
||||||
|
group: "{{ container_group }}"
|
||||||
|
mode: "0755"
|
||||||
|
recurse: yes
|
||||||
|
loop: "{{ caddy_base_directories }}"
|
||||||
|
|
||||||
|
- name: Base SELinux requirement
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
selinux_container_paths: "{{ caddy_base_directories }}"
|
||||||
|
|
||||||
|
- import_role:
|
||||||
|
name: selinux_containers
|
||||||
|
tasks_from: labels
|
||||||
|
|
||||||
|
- name: Ensure Caddyfile is deployed
|
||||||
|
template:
|
||||||
|
src: Caddyfile.j2
|
||||||
|
dest: "{{ caddy_dir }}/Caddyfile"
|
||||||
|
owner: "{{ container_user }}"
|
||||||
|
group: "{{ container_group }}"
|
||||||
|
mode: "0644"
|
||||||
|
|
||||||
|
- name: Deploy quadlet
|
||||||
|
template:
|
||||||
|
src: caddy.container.j2
|
||||||
|
dest: "{{ container_config_dir }}/caddy.container"
|
||||||
|
|
||||||
|
- name: Force systemd reload (blocking)
|
||||||
|
become: true
|
||||||
|
become_user: "{{ container_user }}"
|
||||||
|
command: systemctl --user daemon-reload
|
||||||
|
|
||||||
|
- name: Wait for quadlet generation
|
||||||
|
pause:
|
||||||
|
seconds: 1
|
||||||
|
|
||||||
|
- name: Start service
|
||||||
|
become: true
|
||||||
|
become_user: "{{ container_user }}"
|
||||||
|
systemd:
|
||||||
|
name: caddy.service
|
||||||
|
scope: user
|
||||||
|
state: started
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
# Adguard
|
||||||
|
{{ adguard_domain }} {
|
||||||
|
tls internal
|
||||||
|
reverse_proxy {{ adguard_upstream }}
|
||||||
|
}
|
||||||
|
|
||||||
|
# QBittorrent
|
||||||
|
{{ qbittorrent_domain }} {
|
||||||
|
tls internal
|
||||||
|
reverse_proxy {{ qbittorrent_upstream }}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Prowlarr
|
||||||
|
{{ prowlarr_domain }} {
|
||||||
|
tls internal
|
||||||
|
reverse_proxy {{ prowlarr_upstream }}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Radarr
|
||||||
|
{{ radarr_domain }} {
|
||||||
|
tls internal
|
||||||
|
reverse_proxy {{ radarr_upstream }}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Sonarr
|
||||||
|
{{ sonarr_domain }} {
|
||||||
|
tls internal
|
||||||
|
reverse_proxy {{ sonarr_upstream }}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Bazarr
|
||||||
|
{{ bazarr_domain }} {
|
||||||
|
tls internal
|
||||||
|
reverse_proxy {{ bazarr_upstream }}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
[Unit]
|
||||||
|
Description=Caddy Reverse Proxy
|
||||||
|
Wants=network-online.target
|
||||||
|
After=network-online.target
|
||||||
|
Requires=homelab-network.service
|
||||||
|
After=homelab-network.service
|
||||||
|
|
||||||
|
[Container]
|
||||||
|
Image=docker.io/caddy:latest
|
||||||
|
ContainerName=caddy
|
||||||
|
Network=homelab
|
||||||
|
|
||||||
|
Volume={{ caddy_dir }}/Caddyfile:/etc/caddy/Caddyfile
|
||||||
|
Volume={{ caddy_dir }}/data:/data
|
||||||
|
Volume={{ caddy_dir }}/config:/config
|
||||||
|
|
||||||
|
PublishPort=80:80/tcp
|
||||||
|
PublishPort=443:443/tcp
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Restart=always
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
#container_runtime/defaults/main.yml
|
||||||
|
base_runtime_install_packages:
|
||||||
|
- podman
|
||||||
|
- podman-docker
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
#container_runtime/tasks/main.yml
|
||||||
|
- name: Install required base packages
|
||||||
|
become: true
|
||||||
|
dnf:
|
||||||
|
name: "{{ item }}"
|
||||||
|
state: present
|
||||||
|
loop: "{{ base_runtime_install_packages }}"
|
||||||
|
|
||||||
|
- name: Enable lingering for rootless containers
|
||||||
|
become: true
|
||||||
|
command: "loginctl enable-linger {{ container_user }}"
|
||||||
|
args:
|
||||||
|
creates: "/var/lib/systemd/linger/{{ container_user }}"
|
||||||
|
|
||||||
|
- name: Allow rootless to bind to low ports
|
||||||
|
become: true
|
||||||
|
sysctl:
|
||||||
|
name: net.ipv4.ip_unprivileged_port_start
|
||||||
|
value: '53'
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: Create stack directories
|
||||||
|
file:
|
||||||
|
path: "{{ item }}"
|
||||||
|
state: directory
|
||||||
|
owner: "{{ container_user }}"
|
||||||
|
group: "{{ container_group }}"
|
||||||
|
mode: "0755"
|
||||||
|
recurse: yes
|
||||||
|
loop:
|
||||||
|
- "{{ stack_root }}"
|
||||||
|
- "{{ container_config_dir }}"
|
||||||
|
|
||||||
|
- name: Configure SELinux container policies
|
||||||
|
ansible.builtin.import_tasks: ../selinux_containers/tasks/main.yml
|
||||||
|
|
||||||
|
- name: Deploy Podman Network Quadlet
|
||||||
|
become: true
|
||||||
|
template:
|
||||||
|
src: homelab.network.j2
|
||||||
|
dest: "{{ container_config_dir }}/homelab.network"
|
||||||
|
mode: "0644"
|
||||||
|
owner: "{{ container_user }}"
|
||||||
|
group: "{{ container_group }}"
|
||||||
|
|
||||||
|
#- name: Force systemd reload (blocking)
|
||||||
|
# become: true
|
||||||
|
# become_user: "{{ container_user }}"
|
||||||
|
# command: systemctl --user daemon-reload
|
||||||
|
|
||||||
|
- name: Force systemd reload (blocking)
|
||||||
|
become: true
|
||||||
|
become_user: "{{ container_user }}"
|
||||||
|
environment:
|
||||||
|
XDG_RUNTIME_DIR: "{{ container_runtime_dir }}"
|
||||||
|
command: systemctl --user daemon-reload
|
||||||
|
|
||||||
|
- name: Start homelab network
|
||||||
|
become: true
|
||||||
|
become_user: "{{ container_user }}"
|
||||||
|
systemd:
|
||||||
|
name: homelab-network.service
|
||||||
|
scope: user
|
||||||
|
state: started
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
[Network]
|
||||||
|
NetworkName=homelab
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
---
|
||||||
|
#firewall_base/tasks/main.yml
|
||||||
|
- name: Install required base packages
|
||||||
|
become: true
|
||||||
|
dnf:
|
||||||
|
name:
|
||||||
|
- firewalld
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: Enable firewalld
|
||||||
|
become: true
|
||||||
|
systemd:
|
||||||
|
name: firewalld
|
||||||
|
enabled: true
|
||||||
|
state: started
|
||||||
|
|
||||||
|
- name: Open required firewall rules
|
||||||
|
become: true
|
||||||
|
ansible.posix.firewalld:
|
||||||
|
port: "{{ item.port | default(omit) }}"
|
||||||
|
service: "{{ item.service | default(omit) }}"
|
||||||
|
permanent: true
|
||||||
|
state: enabled
|
||||||
|
immediate: true
|
||||||
|
loop: "{{ base_firewall_rules }}"
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
nfs_server: "{{ nfs_server_name }}"
|
||||||
|
nfs_export: "{{ nfs_export_location }}"
|
||||||
|
nfs_mount_point: "{{ nfs_export_path }}"
|
||||||
|
nfs_fstype: "{{ nfs_export_type }}"
|
||||||
|
nfs_options: "defaults,_netdev,nofail,x-systemd.automount,x-systemd.requires=network-online.target,x-systemd.after=network-online.target,x-systemd.idle-timeout=60"
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
---
|
||||||
|
#nfs_client/tasks/main.yml
|
||||||
|
- name: Create dummy NAS root for test environment
|
||||||
|
become: true
|
||||||
|
file:
|
||||||
|
path: "{{ nfs_mount_point }}"
|
||||||
|
state: directory
|
||||||
|
owner: "{{ container_user }}"
|
||||||
|
group: "{{ container_group }}"
|
||||||
|
mode: "0755"
|
||||||
|
when: env == "test"
|
||||||
|
|
||||||
|
- name: Create dummy NAS storage tree for test environment
|
||||||
|
become: true
|
||||||
|
file:
|
||||||
|
path: "{{ nfs_mount_point }}/{{ item }}"
|
||||||
|
state: directory
|
||||||
|
owner: "{{ container_user }}"
|
||||||
|
group: "{{ container_group }}"
|
||||||
|
mode: "0775"
|
||||||
|
loop: "{{ storage_tree }}"
|
||||||
|
when: env == "test"
|
||||||
|
|
||||||
|
- name: Set SELinux context for dummy NAS storage in test environment
|
||||||
|
become: true
|
||||||
|
community.general.sefcontext:
|
||||||
|
target: "{{ nfs_mount_point }}(/.*)?"
|
||||||
|
setype: container_file_t
|
||||||
|
state: present
|
||||||
|
when: env == "test"
|
||||||
|
|
||||||
|
- name: Apply SELinux context for dummy NAS storage in test environment
|
||||||
|
become: true
|
||||||
|
command: restorecon -Rv "{{ nfs_mount_point }}"
|
||||||
|
changed_when: false
|
||||||
|
when: env == "test"
|
||||||
|
|
||||||
|
- name: Install required NFS client packages
|
||||||
|
become: true
|
||||||
|
dnf:
|
||||||
|
name: nfs-utils
|
||||||
|
state: present
|
||||||
|
when: env != "test"
|
||||||
|
|
||||||
|
- name: Check whether NFS mount point is already mounted
|
||||||
|
become: true
|
||||||
|
command: findmnt --mountpoint "{{ nfs_mount_point }}"
|
||||||
|
register: nfs_mount_check
|
||||||
|
changed_when: false
|
||||||
|
failed_when: false
|
||||||
|
when: env != "test"
|
||||||
|
|
||||||
|
- name: Create NFS mount point
|
||||||
|
become: true
|
||||||
|
file:
|
||||||
|
path: "{{ nfs_mount_point }}"
|
||||||
|
state: directory
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0755"
|
||||||
|
when:
|
||||||
|
- env != "test"
|
||||||
|
- nfs_mount_check.rc != 0
|
||||||
|
|
||||||
|
- name: Configure NFS mount
|
||||||
|
become: true
|
||||||
|
ansible.posix.mount:
|
||||||
|
path: "{{ nfs_mount_point }}"
|
||||||
|
src: "{{ nfs_server }}:{{ nfs_export }}"
|
||||||
|
fstype: "{{ nfs_fstype }}"
|
||||||
|
opts: "{{ nfs_options }}"
|
||||||
|
state: mounted
|
||||||
|
when: env != "test"
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
#nfw_server/defaults/main.yml
|
||||||
|
nfs_packages:
|
||||||
|
- nfs-utils
|
||||||
|
- nfs-server
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
---
|
||||||
|
#nfs_server/tasks/main.yml
|
||||||
|
- name: Install NFS utilities
|
||||||
|
become: true
|
||||||
|
dnf:
|
||||||
|
name: nfs-utils
|
||||||
|
state: present
|
||||||
|
loop: "{{ nfs_packages }}"
|
||||||
|
|
||||||
|
- name: Build NFS exports entries
|
||||||
|
become: true
|
||||||
|
lineinfile:
|
||||||
|
path: /etc/exports
|
||||||
|
line: "{{ item.path }} {{ item.clients | join(' ') }}"
|
||||||
|
create: true
|
||||||
|
state: present
|
||||||
|
loop: "{{ nfs_exports }}"
|
||||||
|
|
||||||
|
- name: Export NFS shares
|
||||||
|
become: true
|
||||||
|
command: exportfs -ra
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Enable and start NFS server
|
||||||
|
become: true
|
||||||
|
systemd:
|
||||||
|
name: nfs-server
|
||||||
|
enabled: true
|
||||||
|
state: started
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
---
|
||||||
|
# selinux_containers/tasks/labels.yml
|
||||||
|
- name: Ensure SELinux context for container config paths
|
||||||
|
become: true
|
||||||
|
community.general.sefcontext:
|
||||||
|
target: "{{ item }}(/.*)?"
|
||||||
|
setype: container_file_t
|
||||||
|
state: present
|
||||||
|
loop: "{{ selinux_container_paths | default([]) }}"
|
||||||
|
when:
|
||||||
|
- selinux_container_paths is defined
|
||||||
|
- selinux_container_paths | length > 0
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
---
|
||||||
|
#selinux_containers/tasks/main.yml
|
||||||
|
# -----------------------------
|
||||||
|
# Set SELinux
|
||||||
|
# -----------------------------
|
||||||
|
- name: Set SELinux context for stack
|
||||||
|
become: true
|
||||||
|
community.general.sefcontext:
|
||||||
|
target: "{{ stack_root }}(/.*)?"
|
||||||
|
setype: container_file_t
|
||||||
|
state: present
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# APPLY SELinux
|
||||||
|
# -----------------------------
|
||||||
|
- name: Apply SELinux context (stack)
|
||||||
|
become: true
|
||||||
|
command: restorecon -Rv "{{ stack_root }}"
|
||||||
|
changed_when: false
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
---
|
||||||
|
# selinux_containers/tasks/storage.yml
|
||||||
|
- name: Set SELinux context for storage mounts
|
||||||
|
become: true
|
||||||
|
community.general.sefcontext:
|
||||||
|
target: "{{ item }}(/.*)?"
|
||||||
|
setype: container_file_t
|
||||||
|
state: present
|
||||||
|
loop: "{{ storage_backends }}"
|
||||||
|
when: "'nas' in group_names"
|
||||||
|
|
||||||
|
- name: Apply SELinux context (storage)
|
||||||
|
become: true
|
||||||
|
command: restorecon -R -F -v "{{ item }}"
|
||||||
|
loop: "{{ storage_backends }}"
|
||||||
|
changed_when: false
|
||||||
|
when: "'nas' in group_names"
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
---
|
||||||
|
# selinux_containers/tasks/vpn.yml
|
||||||
|
# -----------------------------
|
||||||
|
# SELinux BOOLEANS
|
||||||
|
# -----------------------------
|
||||||
|
- name: Allow containers to use devices (needed for Gluetun / /dev/net/tun)
|
||||||
|
become: true
|
||||||
|
ansible.posix.seboolean:
|
||||||
|
name: container_use_devices
|
||||||
|
state: true
|
||||||
|
persistent: true
|
||||||
|
when: selinux_allow_gluetun | default(false)
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
---
|
||||||
|
#servarr/defaults/main.yml
|
||||||
|
servarr_firewall_rules:
|
||||||
|
- port: 8080/tcp
|
||||||
|
- port: 6767/tcp
|
||||||
|
- port: 7878/tcp
|
||||||
|
- port: 8191/tcp
|
||||||
|
- port: 8989/tcp
|
||||||
|
- port: 9696/tcp
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
# servarr/meta/main.yml
|
||||||
|
dependencies:
|
||||||
|
- role: selinux_containers
|
||||||
|
- role: vpn
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
---
|
||||||
|
#servarr/tasks/firewall.yml
|
||||||
|
- name: Open required firewall rules
|
||||||
|
become: true
|
||||||
|
ansible.posix.firewalld:
|
||||||
|
port: "{{ item.port | default(omit) }}"
|
||||||
|
service: "{{ item.service | default(omit) }}"
|
||||||
|
permanent: true
|
||||||
|
state: enabled
|
||||||
|
immediate: true
|
||||||
|
loop: "{{ servarr_firewall_rules }}"
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
#servarr/tasks/main.yml
|
||||||
|
- import_tasks: firewall.yml
|
||||||
|
|
||||||
|
- name: Create stack and config directories
|
||||||
|
file:
|
||||||
|
path: "{{ item }}"
|
||||||
|
state: directory
|
||||||
|
owner: "{{ container_user }}"
|
||||||
|
group: "{{ container_group }}"
|
||||||
|
mode: "0755"
|
||||||
|
recurse: yes
|
||||||
|
loop: "{{ servarr_base_directories }}"
|
||||||
|
|
||||||
|
- name: Directory SELinux requirement
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
selinux_container_paths: "{{ servarr_base_directories }}"
|
||||||
|
|
||||||
|
- import_role:
|
||||||
|
name: selinux_containers
|
||||||
|
tasks_from: labels
|
||||||
|
|
||||||
|
- name: Ensure systemd directory exists for rootless user
|
||||||
|
file:
|
||||||
|
path: "{{ container_config_dir }}"
|
||||||
|
state: directory
|
||||||
|
mode: '0755'
|
||||||
|
owner: "{{ container_user }}"
|
||||||
|
group: "{{ container_group }}"
|
||||||
|
|
||||||
|
- name: Deploy Quadlet files
|
||||||
|
template:
|
||||||
|
src: "{{ item.src }}"
|
||||||
|
dest: "{{ container_config_dir }}/{{ item.dest }}"
|
||||||
|
loop: "{{ servarr_stack }}"
|
||||||
|
register: quadlets_deployed
|
||||||
|
|
||||||
|
- name: Ensure app config directories are writable
|
||||||
|
become: true
|
||||||
|
file:
|
||||||
|
path: "{{ item }}"
|
||||||
|
state: directory
|
||||||
|
owner: "{{ container_user }}"
|
||||||
|
group: "{{ container_group }}"
|
||||||
|
recurse: true
|
||||||
|
loop:
|
||||||
|
- "{{ radarr_dir }}/config"
|
||||||
|
- "{{ sonarr_dir }}/config"
|
||||||
|
- "{{ qbittorrent_dir }}/config"
|
||||||
|
- "{{ prowlarr_dir }}/config"
|
||||||
|
- "{{ bazarr_dir }}/config"
|
||||||
|
|
||||||
|
#- name: Force systemd reload (blocking)
|
||||||
|
# become: true
|
||||||
|
# become_user: "{{ container_user }}"
|
||||||
|
# command: systemctl --user daemon-reload
|
||||||
|
|
||||||
|
- name: Force systemd reload (blocking)
|
||||||
|
become: true
|
||||||
|
become_user: "{{ container_user }}"
|
||||||
|
environment:
|
||||||
|
XDG_RUNTIME_DIR: "{{ container_runtime_dir }}"
|
||||||
|
command: systemctl --user daemon-reload
|
||||||
|
|
||||||
|
- name: Validate VPN and start arr stack
|
||||||
|
ansible.builtin.import_role:
|
||||||
|
name: vpn_guard
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
[Unit]
|
||||||
|
Description=Bazarr Subtitle Manager
|
||||||
|
Requires=gluetun.service
|
||||||
|
After=gluetun.service
|
||||||
|
|
||||||
|
[Container]
|
||||||
|
ContainerName=bazarr
|
||||||
|
Image=lscr.io/linuxserver/bazarr:latest
|
||||||
|
|
||||||
|
Environment=PUID=0
|
||||||
|
Environment=PGID=0
|
||||||
|
Environment=TZ=America/New_York
|
||||||
|
Network=container:gluetun
|
||||||
|
Volume={{ bazarr_dir }}/config:/config
|
||||||
|
Volume={{ downloads_root }}:/data
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
[Unit]
|
||||||
|
Description=Flaresolverr Cloudflare Solver
|
||||||
|
Requires=gluetun.service
|
||||||
|
After=gluetun.service
|
||||||
|
|
||||||
|
[Container]
|
||||||
|
ContainerName=flaresolverr
|
||||||
|
Image=ghcr.io/flaresolverr/flaresolverr:latest
|
||||||
|
|
||||||
|
Environment=TZ=America/New_York
|
||||||
|
Environment=LOG_LEVEL=info
|
||||||
|
Network=container:gluetun
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
[Unit]
|
||||||
|
Description=Prowlarr Indexer Manager
|
||||||
|
Requires=gluetun.service
|
||||||
|
After=gluetun.service
|
||||||
|
|
||||||
|
[Container]
|
||||||
|
ContainerName=prowlarr
|
||||||
|
Image=lscr.io/linuxserver/prowlarr:latest
|
||||||
|
|
||||||
|
Environment=PUID=0
|
||||||
|
Environment=PGID=0
|
||||||
|
Environment=TZ=America/New_York
|
||||||
|
Network=container:gluetun
|
||||||
|
Volume={{ prowlarr_dir }}/config:/config
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
[Unit]
|
||||||
|
Description=qBittorrent Downloader
|
||||||
|
Requires=gluetun.service
|
||||||
|
After=gluetun.service
|
||||||
|
StartLimitIntervalSec=0
|
||||||
|
|
||||||
|
[Container]
|
||||||
|
Image=lscr.io/linuxserver/qbittorrent:latest
|
||||||
|
ContainerName=qbittorrent
|
||||||
|
|
||||||
|
Environment=PUID=0
|
||||||
|
Environment=PGID=0
|
||||||
|
Environment=TZ={{ timezone }}
|
||||||
|
Environment=WEBUI_PORT=8080
|
||||||
|
Environment=WEBUI_ADDRESS=0.0.0.0
|
||||||
|
|
||||||
|
Network=container:gluetun
|
||||||
|
|
||||||
|
Volume={{ qbittorrent_dir }}/config:/config
|
||||||
|
Volume={{ downloads_root }}:/data
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
[Unit]
|
||||||
|
Description=Radarr Movie Manager
|
||||||
|
Requires=gluetun.service
|
||||||
|
After=gluetun.service
|
||||||
|
|
||||||
|
[Container]
|
||||||
|
ContainerName=radarr
|
||||||
|
Image=lscr.io/linuxserver/radarr:latest
|
||||||
|
|
||||||
|
Environment=PUID=0
|
||||||
|
Environment=PGID=0
|
||||||
|
Environment=TZ=America/New_York
|
||||||
|
|
||||||
|
Network=container:gluetun
|
||||||
|
|
||||||
|
Volume={{ radarr_dir }}/config:/config
|
||||||
|
Volume={{ downloads_root }}:/data
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
[Unit]
|
||||||
|
Description=Sonarr TV Manager
|
||||||
|
Requires=gluetun.service
|
||||||
|
After=gluetun.service
|
||||||
|
|
||||||
|
[Container]
|
||||||
|
ContainerName=sonarr
|
||||||
|
Image=lscr.io/linuxserver/sonarr:latest
|
||||||
|
|
||||||
|
Environment=PUID=0
|
||||||
|
Environment=PGID=0
|
||||||
|
Environment=TZ=America/New_York
|
||||||
|
|
||||||
|
Network=container:gluetun
|
||||||
|
|
||||||
|
Volume={{ sonarr_dir }}/config:/config
|
||||||
|
Volume={{ downloads_root }}:/data
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
#storage_client/defaults/main.yml
|
||||||
|
base_storage_install_packages:
|
||||||
|
- epel-release
|
||||||
|
- fuse
|
||||||
|
- fuse3
|
||||||
|
- policycoreutils-python-utils
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
---
|
||||||
|
#storage_client/tasks/main.yml
|
||||||
|
- name: Install required packages
|
||||||
|
become: true
|
||||||
|
dnf:
|
||||||
|
name: "{{ item }}"
|
||||||
|
state: present
|
||||||
|
loop: "{{ base_storage_install_packages }}"
|
||||||
|
|
||||||
|
- name: Install mergerfs repo package
|
||||||
|
become: true
|
||||||
|
dnf:
|
||||||
|
name: https://github.com/trapexit/mergerfs/releases/download/2.41.1/mergerfs-2.41.1-1.el10.x86_64.rpm
|
||||||
|
disable_gpg_check: true
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: Ensure source data directories exist
|
||||||
|
become: true
|
||||||
|
file:
|
||||||
|
path: "{{ item }}"
|
||||||
|
state: directory
|
||||||
|
owner: "{{ container_user }}"
|
||||||
|
group: "{{ container_group }}"
|
||||||
|
mode: "0755"
|
||||||
|
loop: "{{ storage_backends }}"
|
||||||
|
|
||||||
|
- name: Create mergerfs mountpoint
|
||||||
|
become: true
|
||||||
|
file:
|
||||||
|
path: /mnt/nas01
|
||||||
|
state: directory
|
||||||
|
owner: "{{ container_user }}"
|
||||||
|
group: "{{ container_group }}"
|
||||||
|
mode: "0755"
|
||||||
|
|
||||||
|
- name: Mount mergerfs pool
|
||||||
|
become: true
|
||||||
|
ansible.posix.mount:
|
||||||
|
path: "{{ mergerfs_path }}"
|
||||||
|
src: "{{ drive_a_path }}/data:{{ drive_b_path }}/data"
|
||||||
|
fstype: fuse.mergerfs
|
||||||
|
opts: defaults,allow_other,use_ino,category.create=mfs
|
||||||
|
state: mounted
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
---
|
||||||
|
#vpn/tasks/main.yml
|
||||||
|
- name: Install kernel extra modules for Gluetun firewall
|
||||||
|
become: true
|
||||||
|
dnf:
|
||||||
|
name: kernel-modules-extra
|
||||||
|
state: present
|
||||||
|
register: kernel_modules_extra_install
|
||||||
|
|
||||||
|
- name: Check whether xt_conntrack is available for running kernel
|
||||||
|
become: true
|
||||||
|
command: modinfo xt_conntrack
|
||||||
|
register: xt_conntrack_modinfo
|
||||||
|
changed_when: false
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
- name: Reboot if kernel modules were installed but running kernel cannot find xt_conntrack
|
||||||
|
become: true
|
||||||
|
reboot:
|
||||||
|
msg: "Rebooting to load kernel matching installed kernel-modules-extra for Gluetun firewall"
|
||||||
|
reboot_timeout: 600
|
||||||
|
when:
|
||||||
|
- xt_conntrack_modinfo.rc != 0
|
||||||
|
|
||||||
|
- name: Load xt_conntrack for Gluetun firewall
|
||||||
|
become: true
|
||||||
|
community.general.modprobe:
|
||||||
|
name: xt_conntrack
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: Persist xt_conntrack module
|
||||||
|
become: true
|
||||||
|
copy:
|
||||||
|
dest: /etc/modules-load.d/gluetun.conf
|
||||||
|
mode: "0644"
|
||||||
|
content: |
|
||||||
|
xt_conntrack
|
||||||
|
|
||||||
|
- name: Configure SELinux for Gluetun device access
|
||||||
|
ansible.builtin.import_role:
|
||||||
|
name: selinux_containers
|
||||||
|
tasks_from: vpn
|
||||||
|
|
||||||
|
- name: Deploy Quadlet files
|
||||||
|
template:
|
||||||
|
src: "gluetun.container.j2"
|
||||||
|
dest: "{{ container_config_dir }}/gluetun.container"
|
||||||
|
|
||||||
|
- name: Force systemd reload (blocking)
|
||||||
|
become: true
|
||||||
|
become_user: "{{ container_user }}"
|
||||||
|
environment:
|
||||||
|
XDG_RUNTIME_DIR: "{{ container_runtime_dir }}"
|
||||||
|
command: systemctl --user daemon-reload
|
||||||
|
|
||||||
|
- name: Wait for quadlet generation
|
||||||
|
pause:
|
||||||
|
seconds: 1
|
||||||
|
|
||||||
|
- name: Start vpn
|
||||||
|
become: true
|
||||||
|
become_user: "{{ container_user }}"
|
||||||
|
environment:
|
||||||
|
XDG_RUNTIME_DIR: "{{ container_runtime_dir }}"
|
||||||
|
systemd:
|
||||||
|
name: gluetun.service
|
||||||
|
scope: user
|
||||||
|
state: started
|
||||||
|
|
||||||
|
- name: Wait for Gluetun container to exist
|
||||||
|
become: true
|
||||||
|
become_user: "{{ container_user }}"
|
||||||
|
command: podman container exists gluetun
|
||||||
|
register: gluetun_exists
|
||||||
|
retries: 20
|
||||||
|
delay: 2
|
||||||
|
until: gluetun_exists.rc == 0
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Wait for Gluetun to stabilize
|
||||||
|
pause:
|
||||||
|
seconds: 5
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
[Unit]
|
||||||
|
Description=Gluetun VPN Client
|
||||||
|
Requires=homelab-network.service
|
||||||
|
After=network-online.target homelab-network.service
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Container]
|
||||||
|
Image=docker.io/qmcgaw/gluetun:latest
|
||||||
|
ContainerName=gluetun
|
||||||
|
UserNS=keep-id
|
||||||
|
|
||||||
|
AddCapability=NET_ADMIN
|
||||||
|
AddCapability=NET_RAW
|
||||||
|
AddDevice=/dev/net/tun:/dev/net/tun
|
||||||
|
|
||||||
|
# Port Mappings:
|
||||||
|
# 6767 Bazarr
|
||||||
|
# 7878 Radarr
|
||||||
|
# 8080 qBittorrent
|
||||||
|
# 8191 Flaresolverr
|
||||||
|
# 8989 Sonarr
|
||||||
|
# 9696 Prowlarr
|
||||||
|
|
||||||
|
PublishPort=6767:6767
|
||||||
|
PublishPort=7878:7878
|
||||||
|
PublishPort=8080:8080
|
||||||
|
PublishPort=8191:8191
|
||||||
|
PublishPort=8989:8989
|
||||||
|
PublishPort=9696:9696
|
||||||
|
|
||||||
|
Environment=VPN_SERVICE_PROVIDER=mullvad
|
||||||
|
Environment=VPN_TYPE=wireguard
|
||||||
|
Environment=WIREGUARD_PRIVATE_KEY={{ vpn_private_key }}
|
||||||
|
Environment=WIREGUARD_ADDRESSES={{ vpn_addresses }}
|
||||||
|
|
||||||
|
Environment=FIREWALL_ENABLED=on
|
||||||
|
#Environment=FIREWALL_ENABLED_DISABLING_IT_SHOOTS_YOU_IN_YOUR_FOOT=off
|
||||||
|
Environment=DNS_ENABLED=false
|
||||||
|
Environment=FIREWALL_INPUT_PORTS=6767,7878,8080,8191,8989,9696
|
||||||
|
|
||||||
|
Network=homelab:alias=gluetun
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
---
|
||||||
|
#vpn_guard/tasks/main.yml
|
||||||
|
- name: Get host public IP
|
||||||
|
command: curl -s https://ipinfo.io/ip
|
||||||
|
register: host_ip
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Get VPN public IP (via Gluetun)
|
||||||
|
become: true
|
||||||
|
become_user: "{{ container_user }}"
|
||||||
|
command: podman exec gluetun wget -qO- https://ipinfo.io/ip
|
||||||
|
register: vpn_ip
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Fail if VPN is not active (kill switch check)
|
||||||
|
fail:
|
||||||
|
msg: "VPN is NOT active (host={{ host_ip.stdout }} vpn={{ vpn_ip.stdout }}). Aborting arr stack start."
|
||||||
|
when: host_ip.stdout == vpn_ip.stdout
|
||||||
|
|
||||||
|
#- name: Start arr stack only after VPN validation
|
||||||
|
# become: true
|
||||||
|
# become_user: "{{ container_user }}"
|
||||||
|
# systemd:
|
||||||
|
# name: "{{ item }}.service"
|
||||||
|
# enabled: yes
|
||||||
|
# state: started
|
||||||
|
# scope: user
|
||||||
|
# loop: "{{ arr_suite }}"
|
||||||
|
|
||||||
|
- name: Start arr stack only after VPN validation
|
||||||
|
become: true
|
||||||
|
become_user: "{{ container_user }}"
|
||||||
|
environment:
|
||||||
|
XDG_RUNTIME_DIR: "{{ container_runtime_dir }}"
|
||||||
|
systemd:
|
||||||
|
name: "{{ item }}.service"
|
||||||
|
enabled: yes
|
||||||
|
state: started
|
||||||
|
scope: user
|
||||||
|
loop: "{{ arr_suite }}"
|
||||||
Loading…
Reference in New Issue