update to reflect 380 prod

This commit is contained in:
Graham McIntire 2026-03-10 10:05:17 -05:00
parent 231c9744aa
commit 47158aa771
No known key found for this signature in database
81 changed files with 1893 additions and 1253 deletions

View file

@ -14,12 +14,12 @@ bind_dnssec_validation: auto
# Secondaries that should receive NOTIFY
bind_also_notify:
- 204.87.183.53 # ns2
- 172.245.56.83 # vm1.w5isp.com
- 172.245.56.83 # git.mcintire.me
bind_allow_transfer:
- 23.128.97.53 # ns-global.kjsl.com
- 204.87.183.53 # ns2
- 172.245.56.83 # vm1.w5isp.com
- 172.245.56.83 # git.mcintire.me
# Zone definitions are loaded from individual files in zones/ directory
bind_zones:

View file

@ -20,6 +20,8 @@ mcintire_me_zone:
ip: 204.110.191.8
- name: git
ip: 172.245.56.83
- name: vm1
ip: 172.245.56.83
other_name_servers:
- ha.mcintire.me. IN CNAME q1l09qiycnaagngmjsg1qas0rhqcfoou.ui.nabu.casa.
- _acme-challenge.ha.mcintire.me. IN CNAME _acme-challenge.q1l09qiycnaagngmjsg1qas0rhqcfoou.ui.nabu.casa.

View file

@ -30,6 +30,8 @@ w5isp_com_zone:
ip: 204.110.191.8
- name: vaprs
ip: 204.110.191.8
- name: aprs
ip: 204.110.191.232
other_name_servers:
- autodiscover.w5isp.com. IN CNAME mail.w5isp.com.
- autoconfig.w5isp.com. IN CNAME mail.w5isp.com.

View file

@ -0,0 +1,7 @@
---
ansible_user: root
ansible_python_interpreter: /usr/bin/python3
ansible_ssh_private_key_file: /Users/graham/.ssh/id_ed25519
ansible_ssh_common_args: '-o StrictHostKeyChecking=accept-new'
firewall_enabled: false

View file

@ -0,0 +1,8 @@
---
firewall_trusted_subnets:
- subnet: 100.64.0.0/10
comment: Tailscale
- subnet: 204.110.188.0/22
comment: VNTX
- subnet: 10.0.0.0/8
comment: Internal

View file

@ -0,0 +1,37 @@
---
ansible_user: graham
ansible_host: 204.110.191.232
ansible_python_interpreter: /usr/bin/python3
ansible_ssh_private_key_file: /Users/graham/.ssh/id_ed25519
ansible_ssh_common_args: '-o StrictHostKeyChecking=accept-new'
firewall_allow_rules:
- port: 10152
proto: tcp
comment: APRS-IS full feed
- port: 10152
proto: udp
comment: APRS-IS full feed UDP
- port: 14580
proto: tcp
comment: APRS-IS client
- port: 14580
proto: udp
comment: APRS-IS client UDP
- port: 8080
proto: udp
comment: APRS-IS UDP submit
- port: 8080
proto: tcp
comment: APRS-IS HTTP upload
- port: 14501
proto: tcp
comment: APRS-IS HTTP status
aprsc_server_id: T2DALLAS
aprsc_passcode: "28302"
aprsc_admin: "Graham McIntire, W5ISP"
aprsc_email: graham@mcintire.me
aprsc_bind_address: 204.110.191.232
aprsc_uplink: hub-rotate.aprs2.net
aprsc_uplink_port: 20152

View file

@ -0,0 +1,142 @@
---
ansible_user: graham
ansible_host: 10.0.15.22
ansible_python_interpreter: /usr/bin/python3
ansible_ssh_private_key_file: /Users/graham/.ssh/id_ed25519
ansible_ssh_common_args: '-o StrictHostKeyChecking=accept-new'
firewall_enabled: false
# Override role defaults - LXC container with default Debian paths
postgresql_version: "17"
postgresql_storage_setup_enabled: false
postgresql_data_directory: /var/lib/postgresql/17/main
postgresql_wal_directory: /var/lib/postgresql/17/main/pg_wal
postgresql_config_directory: /etc/postgresql/17/main
postgresql_backup_enabled: false
# PostgreSQL base config (postgresql.conf)
postgresql_config:
# Connection
listen_addresses: "'*'"
max_connections: 100
port: 5432
# Memory (tuned for 8GB RAM, Intel N100 2 vCPU)
shared_buffers: 2GB
effective_cache_size: 6GB
work_mem: 16MB
maintenance_work_mem: 512MB
huge_pages: try
# NVMe SSD tuning
random_page_cost: 1.1
effective_io_concurrency: 200
maintenance_io_concurrency: 200
# WAL & Checkpoints (write-heavy APRS workload)
wal_buffers: 64MB
max_wal_size: 2GB
min_wal_size: 512MB
wal_compression: zstd
checkpoint_completion_target: 0.9
checkpoint_timeout: 10min
synchronous_commit: off
# Parallelism (constrained by 2 vCPUs)
max_worker_processes: 4
max_parallel_workers: 2
max_parallel_workers_per_gather: 1
max_parallel_maintenance_workers: 1
# Autovacuum (aggressive for high-churn packets table)
autovacuum_vacuum_scale_factor: 0.05
autovacuum_analyze_scale_factor: 0.02
autovacuum_vacuum_cost_limit: 1000
autovacuum_naptime: 30s
# Query planner
default_statistics_target: 200
# Monitoring & Logging
shared_preload_libraries: "'pg_stat_statements'"
pg_stat_statements.track: top
track_io_timing: on
log_min_duration_statement: 500
log_lock_waits: on
log_temp_files: 0
log_autovacuum_min_duration: 250
log_line_prefix: "'%m [%p] %q%u@%d '"
log_timezone: "'Etc/UTC'"
cluster_name: "'17/main'"
# Locale
datestyle: "'iso, mdy'"
timezone: "'Etc/UTC'"
lc_messages: "'C'"
lc_monetary: "'C'"
lc_numeric: "'C'"
lc_time: "'C'"
default_text_search_config: "'pg_catalog.english'"
# SSL (snakeoil certs)
ssl: on
ssl_cert_file: "'/etc/ssl/certs/ssl-cert-snakeoil.pem'"
ssl_key_file: "'/etc/ssl/private/ssl-cert-snakeoil.key'"
# pg_hba.conf entries
postgresql_hba_entries:
- type: local
database: all
user: postgres
auth_method: peer
- type: local
database: all
user: all
auth_method: md5
- type: host
database: all
user: all
address: 127.0.0.1/32
auth_method: scram-sha-256
- type: host
database: all
user: all
address: 0.0.0.0/0
auth_method: md5
- type: host
database: all
user: all
address: "::1/128"
auth_method: scram-sha-256
- type: local
database: replication
user: all
auth_method: peer
- type: host
database: replication
user: all
address: 127.0.0.1/32
auth_method: scram-sha-256
- type: host
database: replication
user: all
address: "::1/128"
auth_method: scram-sha-256
# Database
postgresql_databases:
- name: aprsme_prod
owner: aprs
postgresql_users:
- name: aprs
password: "{{ vault_postgresql_aprs_password | default(omit) }}"
role_attr_flags: CREATEDB
postgresql_extensions:
aprsme_prod:
- citext
- pg_stat_statements
- pg_trgm
- postgis

View file

@ -0,0 +1,150 @@
---
ansible_user: graham
ansible_host: 10.0.15.20
ansible_python_interpreter: /usr/bin/python3
ansible_ssh_private_key_file: /Users/graham/.ssh/id_ed25519
ansible_ssh_common_args: '-o StrictHostKeyChecking=accept-new'
firewall_enabled: false
# Override role defaults - this server uses default Debian paths, not RAID
postgresql_version: "17"
postgresql_storage_setup_enabled: false
postgresql_data_directory: /var/lib/postgresql/17/main
postgresql_wal_directory: /var/lib/postgresql/17/main/pg_wal
postgresql_config_directory: /etc/postgresql/17/main
postgresql_backup_enabled: false
# TimescaleDB
timescaledb_enabled: true
timescaledb_max_background_workers: 16
timescaledb_apt_repo: "deb https://packagecloud.io/timescale/timescaledb/debian/ trixie main"
# PostgreSQL config (matches live server tuning)
postgresql_config:
# Connection
listen_addresses: "'*'"
max_connections: 100
port: 5432
# Memory (tuned for 8GB ARM server)
shared_buffers: 2014MB
work_mem: 5157kB
maintenance_work_mem: 1031576kB
effective_cache_size: 6044MB
# Disk
random_page_cost: 1.1
effective_io_concurrency: 256
# Workers
max_worker_processes: 23
max_parallel_workers: 4
max_parallel_workers_per_gather: 2
# WAL
wal_buffers: 16MB
max_wal_size: 1GB
min_wal_size: 512MB
checkpoint_completion_target: 0.9
# Autovacuum
autovacuum_max_workers: 10
autovacuum_naptime: 10
# Query tuning
default_statistics_target: 100
jit: off
# Compression and locking
default_toast_compression: lz4
max_locks_per_transaction: 128
# Logging
log_line_prefix: "'%m [%p] %q%u@%d '"
log_timezone: "'UTC'"
cluster_name: "'17/main'"
# Locale
datestyle: "'iso, dmy'"
timezone: "'Europe/London'"
lc_messages: "'en_GB.UTF-8'"
lc_monetary: "'en_GB.UTF-8'"
lc_numeric: "'en_GB.UTF-8'"
lc_time: "'en_GB.UTF-8'"
default_text_search_config: "'pg_catalog.english'"
# Extensions
shared_preload_libraries: "'pg_stat_statements,timescaledb'"
# TimescaleDB
timescaledb.max_background_workers: 16
# SSL
ssl: on
ssl_cert_file: "'server.crt'"
ssl_key_file: "'server.key'"
# pg_hba.conf entries (matches live server)
postgresql_hba_entries:
- type: local
database: all
user: postgres
auth_method: peer
- type: local
database: all
user: all
auth_method: peer
- type: host
database: all
user: all
address: 127.0.0.1/32
auth_method: scram-sha-256
- type: host
database: all
user: all
address: "::1/128"
auth_method: scram-sha-256
- type: local
database: replication
user: all
auth_method: peer
- type: host
database: replication
user: all
address: 127.0.0.1/32
auth_method: scram-sha-256
- type: host
database: replication
user: all
address: "::1/128"
auth_method: scram-sha-256
- type: hostssl
database: all
user: all
address: 10.0.8.0/21
auth_method: scram-sha-256
# Databases and users (matches live server)
postgresql_databases:
- name: aprsme_prod
owner: aprsme
- name: gridmap
owner: gridmap
- name: towerops
owner: towerops
postgresql_users:
- name: aprsme
password: "{{ vault_postgresql_aprsme_password | default(omit) }}"
- name: gridmap
password: "{{ vault_postgresql_gridmap_password | default(omit) }}"
- name: towerops
password: "{{ vault_postgresql_towerops_password | default(omit) }}"
postgresql_extensions:
aprsme_prod:
- citext
- pg_stat_statements
- pg_trgm
- postgis

View file

@ -4,3 +4,11 @@ ansible_host: 204.110.191.218
ansible_python_interpreter: /usr/bin/python3
ansible_ssh_private_key_file: /Users/graham/.ssh/ansible
ansible_ssh_common_args: '-o StrictHostKeyChecking=accept-new'
firewall_allow_rules:
- port: 80
proto: tcp
comment: HTTP
- port: 443
proto: tcp
comment: HTTPS

View file

@ -6,6 +6,23 @@ ansible_ssh_common_args: '-o StrictHostKeyChecking=accept-new'
public_ip: 172.245.56.83
dns_backend: bind9
firewall_allow_rules:
- port: 53
proto: tcp
comment: DNS
- port: 53
proto: udp
comment: DNS
- port: 80
proto: tcp
comment: HTTP
- port: 443
proto: tcp
comment: HTTPS
- port: 2222
proto: tcp
comment: Forgejo SSH
# Forgejo runner
forgejo_runner_token: "I7rqgwumFl3BjuXnaEfroto7RHxGhsIUzM0lgMmM"
forgejo_runner_name: "vm1-runner"

View file

@ -0,0 +1,3 @@
---
vault_librenms_db_password: tqEoiUsDyEMBe
vault_librenms_app_key: "base64:2Ro8xSViclOmQjFoXgm13h5Mx6WaLlihe/Q7O9qbReA="

View file

@ -0,0 +1,17 @@
---
ansible_user: graham
ansible_host: 204.110.191.227
ansible_python_interpreter: /usr/bin/python3
ansible_ssh_private_key_file: /Users/graham/.ssh/id_ed25519
ansible_ssh_common_args: '-o StrictHostKeyChecking=accept-new'
firewall_allow_rules:
- port: 80
proto: tcp
comment: HTTP
- port: 443
proto: tcp
comment: HTTPS
- port: 161
proto: udp
comment: SNMP

View file

@ -0,0 +1,6 @@
---
ansible_user: ansible
ansible_host: 204.110.191.229
ansible_python_interpreter: /usr/bin/python3
ansible_ssh_private_key_file: /Users/graham/.ssh/ansible
ansible_ssh_common_args: '-o StrictHostKeyChecking=accept-new'

View file

@ -1,4 +1,27 @@
---
# Host-specific variables for mail.mcintire.me
ansible_user: ansible
ansible_python_interpreter: /usr/bin/python3
ansible_python_interpreter: /usr/bin/python3
firewall_allow_rules:
- port: 25
proto: tcp
comment: SMTP
- port: 80
proto: tcp
comment: HTTP
- port: 443
proto: tcp
comment: HTTPS
- port: 465
proto: tcp
comment: SMTPS
- port: 587
proto: tcp
comment: SMTP Submission
- port: 993
proto: tcp
comment: IMAPS
- port: 995
proto: tcp
comment: POP3S

View file

@ -4,3 +4,11 @@ ansible_host: 204.110.191.241
ansible_python_interpreter: /usr/bin/python3
ansible_ssh_private_key_file: /Users/graham/.ssh/id_ed25519
ansible_ssh_common_args: '-o StrictHostKeyChecking=accept-new'
firewall_allow_rules:
- port: 80
proto: tcp
comment: HTTP
- port: 443
proto: tcp
comment: HTTPS

View file

@ -8,6 +8,14 @@ ansible_python_interpreter: /usr/bin/python3
ansible_ssh_private_key_file: /Users/graham/.ssh/id_ed25519
ansible_ssh_common_args: '-o StrictHostKeyChecking=accept-new'
firewall_allow_rules:
- port: 80
proto: tcp
comment: HTTP
- port: 443
proto: tcp
comment: HTTPS
# Override defaults for NetBox installation
netbox_port: 8001
netbox_secret_key: "{{ vault_netbox_secret_key }}"

View file

@ -5,3 +5,11 @@ ansible_python_interpreter: /usr/bin/python3
ansible_ssh_common_args: '-o StrictHostKeyChecking=accept-new'
public_ip: 204.110.191.222
dns_backend: bind9
firewall_allow_rules:
- port: 53
proto: tcp
comment: DNS
- port: 53
proto: udp
comment: DNS

View file

@ -4,3 +4,17 @@ ansible_host: 204.110.191.248
ansible_python_interpreter: /usr/bin/python3
ansible_ssh_private_key_file: /Users/graham/.ssh/id_ed25519
ansible_ssh_common_args: '-o StrictHostKeyChecking=accept-new'
firewall_allow_rules:
- port: 1812
proto: udp
comment: RADIUS Auth
- port: 1812
proto: tcp
comment: RADIUS Auth
- port: 1813
proto: udp
comment: RADIUS Accounting
- port: 1813
proto: tcp
comment: RADIUS Accounting

View file

@ -0,0 +1,6 @@
---
ansible_user: graham
ansible_host: 204.110.191.240
ansible_python_interpreter: /usr/bin/python3
ansible_ssh_private_key_file: /Users/graham/.ssh/id_ed25519
ansible_ssh_common_args: '-o StrictHostKeyChecking=accept-new'

View file

@ -0,0 +1,6 @@
---
ansible_user: graham
ansible_host: 204.110.191.250
ansible_python_interpreter: /usr/bin/python3
ansible_ssh_private_key_file: /Users/graham/.ssh/id_ed25519
ansible_ssh_common_args: '-o StrictHostKeyChecking=accept-new'

View file

@ -4,3 +4,14 @@ ansible_host: 204.110.191.8
ansible_python_interpreter: /usr/bin/python3
ansible_ssh_private_key_file: /Users/graham/.ssh/ansible
ansible_ssh_common_args: '-o StrictHostKeyChecking=accept-new'
network:
skip: true
firewall_allow_rules:
- port: 80
proto: tcp
comment: HTTP
- port: 443
proto: tcp
comment: HTTPS

View file

@ -0,0 +1,16 @@
---
ansible_user: graham
ansible_host: 204.110.191.231
ansible_python_interpreter: /usr/bin/python3
ansible_ssh_private_key_file: /Users/graham/.ssh/id_ed25519
ansible_ssh_common_args: '-o StrictHostKeyChecking=accept-new'
firewall_allow_rules:
- port: 80
proto: tcp
comment: HTTP
- port: 443
proto: tcp
comment: HTTPS
dokku_domain: staging.towerops.net

View file

@ -1,7 +0,0 @@
---
# Vault file for sync.w5isp.com - Mailcow secrets
vault_mailcow_api_key: "drVv1AtEHuJw5C0+MxlKGGOMYiLx7C6dlfcAFlh2wZY="
vault_mailcow_dbpass: "om30XAgUbHTcPy3uGNzJAp8L+uGvNyoS"
vault_mailcow_dbroot: "jhIXyrY/DOS8wuRPSmCuRR/DqqXzi2Lq"
vault_mailcow_redispass: "9YZCv2mqofPYl7p4ep5DvL2PVBIBhMK/"

View file

@ -1,15 +0,0 @@
---
# Host-specific variables for sync.w5isp.com
ansible_user: ansible
ansible_python_interpreter: /usr/bin/python3
# Mailcow configuration
mailcow_hostname: mail.w5isp.com
mailcow_http_bind: "127.0.0.1"
mailcow_https_bind: "127.0.0.1"
mailcow_api_key: "{{ vault_mailcow_api_key }}"
mailcow_dbpass: "{{ vault_mailcow_dbpass }}"
mailcow_dbroot: "{{ vault_mailcow_dbroot }}"
# Since we're using Caddy as reverse proxy
mailcow_skip_lets_encrypt: "y"

View file

@ -0,0 +1,23 @@
---
ansible_user: graham
ansible_host: 204.110.191.224
ansible_python_interpreter: /usr/bin/python3
ansible_ssh_private_key_file: /Users/graham/.ssh/id_ed25519
ansible_ssh_common_args: '-o StrictHostKeyChecking=accept-new'
firewall_allow_rules:
- port: 80
proto: tcp
comment: HTTP
- port: 443
proto: tcp
comment: HTTPS
- port: 81
proto: tcp
comment: UISP device management
- port: 8089
proto: tcp
comment: UISP HTTPS alt
- port: 2055
proto: udp
comment: NetFlow

View file

@ -4,3 +4,23 @@ ansible_host: 204.110.191.247
ansible_python_interpreter: /usr/bin/python3
ansible_ssh_private_key_file: /Users/graham/.ssh/ansible
ansible_ssh_common_args: '-o StrictHostKeyChecking=accept-new'
firewall_trusted_subnets:
- subnet: 100.64.0.0/10
comment: Tailscale
- subnet: 204.110.188.0/22
comment: VNTX
- subnet: 10.0.0.0/8
comment: Internal
firewall_allow_rules:
- port: 51820
proto: udp
comment: WireGuard
firewall_default_routed: allow
firewall_ip_forward: true
firewall_masquerade_rules:
- source: 10.6.0.0/24
out_interface: eth0
comment: wireguard-nat-rule

View file

@ -1,10 +1,20 @@
[vntx_servers]
#logs.vntx.net
logs.vntx.net
radius.vntx.net
vpn.vntx.net
netbox.vntx.net
#ntp.vntx.net
unimus.vntx.net
monitor.vntx.net
librenms.vntx.net
uisp.vntx.net
[librenms_servers]
librenms.vntx.net
[resolvers]
resolver1.vntx.net
resolver2.vntx.net
[monitoring_servers]
monitor.vntx.net
@ -13,22 +23,40 @@ monitor.vntx.net
skippy.w5isp.com
mail.mcintire.me ansible_host=107.174.178.20
dokku.w5isp.com
camper
aprs.w5isp.com
staging.towerops.net
[dokku_servers]
staging.towerops.net
[aprsc_servers]
aprs.w5isp.com
[caddy_servers]
skippy.w5isp.com
#[syncthing_servers]
#sync.w5isp.com
[bind9_servers]
ns1.as393837.net ansible_host=204.110.191.222
vm1.w5isp.com ansible_host=172.245.56.83
git.mcintire.me ansible_host=172.245.56.83
[dns_servers:children]
bind9_servers
[forgejo_servers]
vm1.w5isp.com
git.mcintire.me
[uisp_servers]
uisp.vntx.net
[proxmox_servers]
node1 ansible_host=10.0.15.101
node2 ansible_host=10.0.15.102
node3 ansible_host=10.0.15.103
[postgresql_servers]
db.mcintire.me ansible_host=10.0.15.20
db.aprs.me ansible_host=10.0.15.22
[home_cluster:children]
proxmox_servers
postgresql_servers

View file

@ -15,12 +15,23 @@
community.general.timezone:
name: "{{ timezone }}"
# Resolver play disabled - no hosts in resolvers group
#- name: Apply resolver config
# hosts: resolvers
# gather_facts: true
# roles:
# - { role: resolvers, become: true }
- name: Apply firewall configuration
hosts: all
become: true
gather_facts: true
tags:
- firewall
roles:
- firewall
- name: Apply resolver config
hosts: resolvers
become: true
gather_facts: true
tags:
- resolvers
roles:
- resolvers
# NS play disabled - replaced by BIND9 play below
#- name: Apply ns config
@ -113,41 +124,6 @@
roles:
- caddy
- name: Configure PostgreSQL servers
hosts: postgresql_servers
become: true
gather_facts: true
tags:
- postgresql
- database
pre_tasks:
- name: Set hostname
hostname:
name: "{{ inventory_hostname }}"
- name: Update /etc/hosts
lineinfile:
path: /etc/hosts
regexp: '^127\.0\.1\.1'
line: "127.0.1.1 {{ inventory_hostname }}"
state: present
- name: Update apt cache
apt:
update_cache: yes
cache_valid_time: 3600
- name: Install Python dependencies for Ansible
apt:
name:
- python3-apt
- python3-psycopg2
state: present
roles:
- raid
- postgresql
- name: Provision BIND9 VM on Proxmox
hosts: localhost
@ -296,3 +272,39 @@
- forgejo
roles:
- forgejo
- name: Configure LibreNMS servers
hosts: librenms_servers
become: yes
gather_facts: true
tags:
- librenms
roles:
- librenms
- name: Configure aprsc servers
hosts: aprsc_servers
become: yes
gather_facts: true
tags:
- aprsc
roles:
- aprsc
- name: Configure UISP servers
hosts: uisp_servers
become: yes
gather_facts: true
tags:
- uisp
roles:
- uisp
- name: Configure Dokku servers
hosts: dokku_servers
become: yes
gather_facts: true
tags:
- dokku
roles:
- dokku

View file

@ -1,6 +1,6 @@
---
# One-time playbook to regenerate SSH host keys on existing hosts
# Usage: ansible-playbook -l "vm1,pangolin,camper,birdnet" regenerate-ssh-keys.yml
# Usage: ansible-playbook -l "vm1,pangolin,birdnet" regenerate-ssh-keys.yml
#
# After running this playbook, you'll need to update your local known_hosts:
# ssh-keygen -R hostname

View file

@ -0,0 +1,35 @@
---
aprsc_server_id: NOCALL
aprsc_passcode: "0"
aprsc_admin: "Server Admin"
aprsc_email: admin@example.com
aprsc_path: /opt/aprsc
aprsc_user: aprsc
# Upstream connection
aprsc_uplink: hub-rotate.aprs2.net
aprsc_uplink_port: 20152
aprsc_uplink_type: full
aprsc_bind_address: ""
# Listeners
aprsc_fullfeed_port: 10152
aprsc_client_port: 14580
aprsc_udpsubmit_port: 8080
aprsc_http_status_port: 14501
aprsc_http_upload_port: 8080
# Timeouts
aprsc_upstream_timeout: 15s
aprsc_client_timeout: 48h
# Logging
aprsc_log_rotate_size: 10
aprsc_log_rotate_count: 5
# Resource limits
aprsc_file_limit: 10000
# APT repository
aprsc_apt_repo: "deb http://aprsc-dist.he.fi/aprsc/apt {{ ansible_distribution_release }} main"

View file

@ -0,0 +1,5 @@
---
- name: restart aprsc
systemd:
name: aprsc
state: restarted

View file

@ -0,0 +1,33 @@
---
- name: Add aprsc APT repository
apt_repository:
repo: "{{ aprsc_apt_repo }}"
state: present
filename: aprsc
- name: Install aprsc
apt:
name: aprsc
state: present
update_cache: true
- name: Deploy aprsc configuration
template:
src: aprsc.conf.j2
dest: "{{ aprsc_path }}/etc/aprsc.conf"
owner: root
group: root
mode: '0644'
notify: restart aprsc
- name: Enable and start aprsc-chroot service
systemd:
name: aprsc-chroot
enabled: true
state: started
- name: Enable and start aprsc service
systemd:
name: aprsc
state: started
enabled: true

View file

@ -0,0 +1,39 @@
# Configuration for aprsc, an APRS-IS server for core servers
# Managed by Ansible
ServerId {{ aprsc_server_id }}
PassCode {{ aprsc_passcode }}
MyAdmin "{{ aprsc_admin }}"
MyEmail {{ aprsc_email }}
### Directories #########
RunDir data
LogRotate {{ aprsc_log_rotate_size }} {{ aprsc_log_rotate_count }}
### Intervals and timers #########
UpstreamTimeout {{ aprsc_upstream_timeout }}
ClientTimeout {{ aprsc_client_timeout }}
### TCP listener ##########
Listen "Full feed" fullfeed tcp :: {{ aprsc_fullfeed_port }} hidden
Listen "" fullfeed udp :: {{ aprsc_fullfeed_port }} hidden
Listen "Client-Defined Filters" igate tcp :: {{ aprsc_client_port }}
Listen "" igate udp :: {{ aprsc_client_port }}
Listen "UDP submit" udpsubmit udp :: {{ aprsc_udpsubmit_port }}
### Uplink configuration ########
Uplink "Core rotate" {{ aprsc_uplink_type }} tcp {{ aprsc_uplink }} {{ aprsc_uplink_port }}
{% if aprsc_bind_address is defined and aprsc_bind_address %}
UplinkBind {{ aprsc_bind_address }}
{% endif %}
### HTTP server ##########
HTTPStatus 0.0.0.0 {{ aprsc_http_status_port }}
HTTPUpload 0.0.0.0 {{ aprsc_http_upload_port }}
### Environment ############
FileLimit {{ aprsc_file_limit }}

View file

@ -65,14 +65,21 @@ base_packages_by_distribution:
AlmaLinux:
- bind-utils
- lsof
- mosh
- nano
- net-tools
- mlocate
- fail2ban
- pv
- selinux-policy
- tar
- unzip
- qemu-guest-agent
- python3-libselinux
# Packages only available on specific AlmaLinux versions
# EL9 has mosh, mlocate, fail2ban via EPEL; EL10 does not yet
base_packages_almalinux_9:
- mosh
- mlocate
- fail2ban
base_packages_almalinux_10:
- plocate

View file

@ -46,3 +46,10 @@
- include_role:
name: alpine
when: ansible_facts['os_family'] == "Alpine"
# Firewall configuration
- include_role:
name: firewall
when: firewall_enabled | default(true)
tags:
- firewall

View file

@ -4,7 +4,9 @@
base_packages_combined: >-
{{ (base_common_packages
+ base_packages_by_os_family.get(ansible_facts['os_family'], [])
+ base_packages_by_distribution.get(ansible_facts['distribution'], [])) | unique }}
+ base_packages_by_distribution.get(ansible_facts['distribution'], [])
+ (base_packages_almalinux_9 if ansible_facts['distribution'] == 'AlmaLinux' and ansible_facts['distribution_major_version'] == '9' else [])
+ (base_packages_almalinux_10 if ansible_facts['distribution'] == 'AlmaLinux' and ansible_facts['distribution_major_version'] == '10' else [])) | unique }}
tags:
- packages
- common

View file

@ -0,0 +1,20 @@
---
dokku_domain: "{{ inventory_hostname }}"
dokku_apt_suite: bookworm # trixie packages may not exist yet
dokku_ssh_keys:
- name: graham
key: "https://github.com/gmcintire.keys"
dokku_postgres_plugin_url: https://github.com/dokku/dokku-postgres.git
dokku_postgres_image: "timescale/timescaledb"
dokku_postgres_image_version: "latest-pg17"
dokku_redis_plugin_url: https://github.com/dokku/dokku-redis.git
dokku_redis_image: "valkey/valkey"
dokku_redis_image_version: "8"
dokku_apps:
- name: towerops
domains:
- staging.towerops.net
postgres:
service_name: towerops-db
redis:
service_name: towerops-redis

View file

@ -0,0 +1,3 @@
---
- name: restart dokku nginx
command: dokku nginx:reload

View file

@ -0,0 +1,321 @@
---
# Install Docker (required by Dokku)
- name: Install prerequisites
apt:
name:
- ca-certificates
- curl
- gnupg
- apt-transport-https
state: present
update_cache: true
- name: Add Docker GPG key
ansible.builtin.get_url:
url: https://download.docker.com/linux/debian/gpg
dest: /usr/share/keyrings/docker-archive-keyring.asc
mode: '0644'
- name: Add Docker repository
ansible.builtin.deb822_repository:
name: docker
types: deb
uris: https://download.docker.com/linux/debian
suites: "{{ ansible_distribution_release }}"
components: stable
architectures: "{{ ansible_architecture | replace('x86_64', 'amd64') }}"
signed_by: /usr/share/keyrings/docker-archive-keyring.asc
state: present
- name: Install Docker packages
apt:
name:
- docker-ce
- docker-ce-cli
- containerd.io
- docker-compose-plugin
- docker-buildx-plugin
state: present
update_cache: true
- name: Ensure Docker service is started and enabled
systemd:
name: docker
state: started
enabled: true
# Install Dokku
- name: Add Dokku GPG key
ansible.builtin.get_url:
url: https://packagecloud.io/dokku/dokku/gpgkey
dest: /usr/share/keyrings/dokku-archive-keyring.asc
mode: '0644'
- name: Add Dokku repository
ansible.builtin.deb822_repository:
name: dokku
types: deb
uris: https://packagecloud.io/dokku/dokku/debian
suites: "{{ dokku_apt_suite }}"
components: main
signed_by: /usr/share/keyrings/dokku-archive-keyring.asc
state: present
- name: Preseed Dokku configuration
ansible.builtin.debconf:
name: dokku
question: "{{ item.question }}"
value: "{{ item.value }}"
vtype: "{{ item.vtype }}"
loop:
- question: dokku/vhost_enable
value: "true"
vtype: boolean
- question: dokku/hostname
value: "{{ dokku_domain }}"
vtype: string
- question: dokku/skip_key_file
value: "true"
vtype: boolean
loop_control:
label: "{{ item.question }}"
- name: Install Dokku
apt:
name: dokku
state: present
update_cache: true
- name: Set Dokku global domain
command: dokku domains:set-global {{ dokku_domain }}
register: dokku_domain_result
changed_when: "'Set' in dokku_domain_result.stdout"
# SSH keys for git push access
- name: Fetch SSH keys
ansible.builtin.uri:
url: "{{ item.key }}"
return_content: true
register: ssh_key_responses
loop: "{{ dokku_ssh_keys }}"
loop_control:
label: "{{ item.name }}"
delegate_to: localhost
become: false
- name: Check existing Dokku SSH keys
command: dokku ssh-keys:list
register: dokku_existing_keys
changed_when: false
failed_when: false
- name: Add SSH keys to Dokku
shell: |
echo '{{ item.content.split("\n") | first }}' | dokku ssh-keys:add {{ item.item.name }}
when: "item.item.name not in dokku_existing_keys.stdout"
loop: "{{ ssh_key_responses.results }}"
loop_control:
label: "{{ item.item.name }}"
# Install dokku-postgres plugin
- name: Check if postgres plugin is installed
command: dokku plugin:list
register: dokku_plugins
changed_when: false
- name: Install dokku-postgres plugin
command: dokku plugin:install {{ dokku_postgres_plugin_url }}
when: "'postgres' not in dokku_plugins.stdout"
- name: Install dokku-redis plugin
command: dokku plugin:install {{ dokku_redis_plugin_url }}
when: "'redis' not in dokku_plugins.stdout"
# Create apps and postgres services
- name: Check existing apps
command: dokku apps:list
register: dokku_apps_list
changed_when: false
- name: Create Dokku apps
command: "dokku apps:create {{ item.name }}"
when: "item.name not in dokku_apps_list.stdout_lines"
loop: "{{ dokku_apps }}"
loop_control:
label: "{{ item.name }}"
- name: Set app domains
shell: |
current=$(dokku domains:report {{ item.name }} --domains-app-vhosts)
for domain in {{ item.domains | join(' ') }}; do
if ! echo "$current" | grep -q "$domain"; then
dokku domains:add {{ item.name }} "$domain"
fi
done
when: item.domains is defined
loop: "{{ dokku_apps }}"
loop_control:
label: "{{ item.name }}"
register: domains_result
changed_when: "'added' in domains_result.stdout | default('')"
- name: Check if app has SSL cert
shell: "dokku certs:report {{ item.name }} --ssl-enabled 2>&1"
loop: "{{ dokku_apps }}"
loop_control:
label: "{{ item.name }}"
register: certs_check
changed_when: false
failed_when: false
- name: Generate self-signed SSL cert for apps
shell: |
mkdir -p /home/dokku/{{ item.item.name }}/tls
openssl req -new -x509 -days 3650 -nodes \
-out /home/dokku/{{ item.item.name }}/tls/server.crt \
-keyout /home/dokku/{{ item.item.name }}/tls/server.key \
-subj "/CN={{ item.item.domains | first }}"
chown -R dokku:dokku /home/dokku/{{ item.item.name }}/tls
chmod 600 /home/dokku/{{ item.item.name }}/tls/server.key
dokku proxy:build-config {{ item.item.name }}
when:
- item.item.domains is defined
- "'true' not in item.stdout"
loop: "{{ certs_check.results }}"
loop_control:
label: "{{ item.item.name }}"
- name: Check existing postgres services
command: dokku postgres:list
register: dokku_postgres_list
changed_when: false
failed_when: false
- name: Create postgres services with TimescaleDB
command: >
dokku postgres:create {{ item.postgres.service_name }}
--image {{ dokku_postgres_image }}
--image-version {{ dokku_postgres_image_version }}
when:
- item.postgres is defined
- "item.postgres.service_name not in dokku_postgres_list.stdout"
loop: "{{ dokku_apps }}"
loop_control:
label: "{{ item.name }}"
register: postgres_create_result
# Handle TimescaleDB SSL cert issue - container may get stuck restarting without certs
- name: Wait for postgres services to stabilize
pause:
seconds: 10
when: postgres_create_result.changed | default(false)
- name: Check postgres service status
command: "dokku postgres:info {{ item.postgres.service_name }}"
when:
- item.postgres is defined
- postgres_create_result.changed | default(false)
loop: "{{ dokku_apps }}"
loop_control:
label: "{{ item.name }}"
register: postgres_info
changed_when: false
failed_when: false
- name: Fix TimescaleDB SSL certs if container is restarting
shell: |
CONTAINER_ID=$(docker ps -a --filter "name=dokku.postgres.{{ item.postgres.service_name }}" --format '{{"{{"}}.ID{{"}}"}}')
if [ -n "$CONTAINER_ID" ]; then
DATA_DIR=$(docker inspect "$CONTAINER_ID" --format '{{"{{" }}range .Mounts{{"}}"}}{{"{{"}}if eq .Destination "/var/lib/postgresql/data"{{"}}"}}{{"{{"}} .Source {{"}}"}}{{"{{"}}end{{"}}"}}{{"{{"}}end{{"}}"}}')
if [ -n "$DATA_DIR" ] && [ ! -f "$DATA_DIR/server.crt" ]; then
openssl req -new -x509 -days 3650 -nodes \
-out "$DATA_DIR/server.crt" \
-keyout "$DATA_DIR/server.key" \
-subj "/CN=dokku-postgres-{{ item.postgres.service_name }}"
chown 999:999 "$DATA_DIR/server.crt" "$DATA_DIR/server.key"
chmod 600 "$DATA_DIR/server.key"
dokku postgres:restart {{ item.postgres.service_name }}
fi
fi
when:
- item.postgres is defined
- postgres_create_result.changed | default(false)
loop: "{{ dokku_apps }}"
loop_control:
label: "{{ item.name }}"
changed_when: false
# Enable TimescaleDB extension
- name: Enable timescaledb extension
shell: |
echo "CREATE EXTENSION IF NOT EXISTS timescaledb;" | dokku postgres:connect {{ item.postgres.service_name }}
when:
- item.postgres is defined
loop: "{{ dokku_apps }}"
loop_control:
label: "{{ item.name }}"
register: timescaledb_ext
changed_when: "'CREATE EXTENSION' in timescaledb_ext.stdout"
failed_when:
- timescaledb_ext.rc != 0
- "'already exists' not in timescaledb_ext.stderr"
# Link postgres to apps
- name: Check postgres links
shell: "dokku postgres:linked {{ item.postgres.service_name }} {{ item.name }} && echo linked || echo not_linked"
when:
- item.postgres is defined
loop: "{{ dokku_apps }}"
loop_control:
label: "{{ item.name }}"
register: postgres_link_check
changed_when: false
failed_when: false
- name: Link postgres to apps
command: "dokku postgres:link {{ item.item.postgres.service_name }} {{ item.item.name }}"
when:
- item.item.postgres is defined
- "'not_linked' in item.stdout"
loop: "{{ postgres_link_check.results }}"
loop_control:
label: "{{ item.item.name }}"
# Redis/Valkey services
- name: Check existing redis services
command: dokku redis:list
register: dokku_redis_list
changed_when: false
failed_when: false
- name: Create redis services with Valkey
command: >
dokku redis:create {{ item.redis.service_name }}
--image {{ dokku_redis_image }}
--image-version {{ dokku_redis_image_version }}
when:
- item.redis is defined
- "item.redis.service_name not in dokku_redis_list.stdout"
loop: "{{ dokku_apps }}"
loop_control:
label: "{{ item.name }}"
- name: Check redis links
shell: "dokku redis:linked {{ item.redis.service_name }} {{ item.name }} && echo linked || echo not_linked"
when:
- item.redis is defined
loop: "{{ dokku_apps }}"
loop_control:
label: "{{ item.name }}"
register: redis_link_check
changed_when: false
failed_when: false
- name: Link redis to apps
command: "dokku redis:link {{ item.item.redis.service_name }} {{ item.item.name }}"
when:
- item.item.redis is defined
- "'not_linked' in item.stdout"
loop: "{{ redis_link_check.results }}"
loop_control:
label: "{{ item.item.name }}"

View file

@ -0,0 +1,38 @@
---
# Trusted subnets - SSH and all ports allowed from these
firewall_trusted_subnets:
- subnet: 100.64.0.0/10
comment: Tailscale
- subnet: 204.110.188.0/22
comment: VNTX
# Default policy
firewall_default_incoming: deny
firewall_default_outgoing: allow
# Per-host rules for publicly exposed services
# Override in host_vars or group_vars as needed
# Example:
# firewall_allow_rules:
# - port: 80
# proto: tcp
# comment: HTTP
# - port: 443
# proto: tcp
# comment: HTTPS
firewall_allow_rules: []
# Routed/forwarding policy (allow, deny, reject)
# Set to allow on hosts that need to forward traffic (e.g. VPN gateways)
firewall_default_routed: deny
# Enable IP forwarding (sysctl net.ipv4.ip_forward)
firewall_ip_forward: false
# NAT masquerade rules for VPN/routing
# Example:
# firewall_masquerade_rules:
# - source: 10.6.0.0/24
# out_interface: eth0
# comment: wireguard-nat-rule
firewall_masquerade_rules: []

View file

@ -0,0 +1,4 @@
---
- name: Reload ufw
ufw:
state: reloaded

View file

@ -0,0 +1,125 @@
---
# Firewall configuration using ufw
# Default deny incoming, allow outgoing
# Trusted subnets get full access
# Per-host rules for publicly exposed services
- name: Install ufw
apt:
name: ufw
state: present
update_cache: true
when: ansible_os_family == "Debian"
- name: Install firewalld
dnf:
name: firewalld
state: present
when: ansible_os_family == "RedHat"
- name: Ensure firewalld is started and enabled
systemd:
name: firewalld
state: started
enabled: true
when: ansible_os_family == "RedHat"
# --- Firewalld (RedHat/AlmaLinux) ---
- name: Allow trusted subnets in firewalld
ansible.posix.firewalld:
zone: public
source: "{{ item.subnet }}"
permanent: true
immediate: true
state: enabled
loop: "{{ firewall_trusted_subnets }}"
when: ansible_os_family == "RedHat"
- name: Allow per-host public services in firewalld
ansible.posix.firewalld:
zone: public
port: "{{ item.port | string }}/{{ item.proto }}"
permanent: true
immediate: true
state: enabled
loop: "{{ firewall_allow_rules }}"
when:
- ansible_os_family == "RedHat"
- firewall_allow_rules | length > 0
# --- UFW (Debian/Ubuntu) ---
- name: Reset ufw to defaults
ufw:
state: reset
when: ansible_os_family == "Debian"
- name: Set default incoming policy to deny
ufw:
direction: incoming
policy: "{{ firewall_default_incoming }}"
when: ansible_os_family == "Debian"
- name: Set default outgoing policy to allow
ufw:
direction: outgoing
policy: "{{ firewall_default_outgoing }}"
when: ansible_os_family == "Debian"
- name: Set default routed policy
ufw:
direction: routed
policy: "{{ firewall_default_routed }}"
when: ansible_os_family == "Debian"
- name: Allow all traffic from trusted subnets
ufw:
rule: allow
from_ip: "{{ item.subnet }}"
comment: "{{ item.comment }}"
loop: "{{ firewall_trusted_subnets }}"
when: ansible_os_family == "Debian"
- name: Allow per-host public services
ufw:
rule: allow
port: "{{ item.port | string }}"
proto: "{{ item.proto }}"
comment: "{{ item.comment }}"
loop: "{{ firewall_allow_rules }}"
when:
- ansible_os_family == "Debian"
- firewall_allow_rules | length > 0
- name: Enable IP forwarding
ansible.posix.sysctl:
name: net.ipv4.ip_forward
value: '1'
sysctl_set: true
state: present
reload: true
when:
- ansible_os_family == "Debian"
- firewall_ip_forward
- name: Configure NAT masquerade rules in ufw before.rules
ansible.builtin.blockinfile:
path: /etc/ufw/before.rules
insertbefore: "\\*filter"
marker: "# {mark} ANSIBLE MANAGED - {{ item.comment }}"
block: |
*nat
:POSTROUTING ACCEPT [0:0]
-A POSTROUTING -s {{ item.source }} -o {{ item.out_interface }} -j MASQUERADE -m comment --comment "{{ item.comment }}"
COMMIT
loop: "{{ firewall_masquerade_rules }}"
when:
- ansible_os_family == "Debian"
- firewall_masquerade_rules | length > 0
notify: Reload ufw
- name: Enable ufw
ufw:
state: enabled
when: ansible_os_family == "Debian"

View file

@ -1,6 +1,6 @@
---
forgejo_home: /opt/forgejo
forgejo_version: "10"
forgejo_version: "15.0-test"
forgejo_http_port: 3000
forgejo_ssh_port: 2222
forgejo_domain: git.mcintire.me
@ -10,7 +10,7 @@ forgejo_root_url: "https://{{ forgejo_domain }}"
forgejo_app_name: "Forgejo"
# Runner settings
forgejo_runner_version: "6.2.2"
forgejo_runner_version: "12.7.1"
forgejo_runner_token: ""
forgejo_runner_name: "default-runner"
forgejo_runner_labels: "docker:docker://node:20-bookworm,ubuntu-latest:docker://ubuntu:latest,ubuntu-22.04:docker://ubuntu:22.04"

View file

@ -1,7 +1,7 @@
---
services:
forgejo:
image: codeberg.org/forgejo/forgejo:{{ forgejo_version }}
image: codeberg.org/forgejo-experimental/forgejo:{{ forgejo_version }}
container_name: forgejo
restart: unless-stopped
environment:

View file

@ -94,7 +94,7 @@
- name: zones.conf
template:
src: templates/etc/icinga2/zones.conf.j2
src: etc/icinga2/zones.conf.j2
dest: /etc/icinga2/zones.conf
owner: nagios
group: nagios
@ -104,7 +104,7 @@
- name: icinga2.conf
template:
src: templates/etc/icinga2/icinga2.conf.j2
src: etc/icinga2/icinga2.conf.j2
dest: /etc/icinga2/icinga2.conf
owner: nagios
group: nagios
@ -114,7 +114,7 @@
- name: checks.conf
template:
src: templates/etc/icinga2/conf.d/checks.conf.j2
src: etc/icinga2/conf.d/checks.conf.j2
dest: /etc/icinga2/conf.d/checks.conf
owner: nagios
group: nagios

View file

@ -122,9 +122,11 @@
owner: root
group: root
mode: 00644
follow: yes
vars:
domain: "{{ network.domain | default('local') }}"
search_domains: "{{ network.search_domains | default([domain]) }}"
nameservers: "{{ network.nameservers | default(['9.9.9.9', '149.112.112.112']) }}"
when: network.skip | default(False) != True
tags:
- network_resolv_conf

View file

@ -0,0 +1 @@
// Managed by Ansible

View file

@ -0,0 +1,58 @@
/**
* Icinga 2 configuration file
* - this is where you define settings for the Icinga application including
* which hosts/services to check.
*
* For an overview of all available configuration options please refer
* to the documentation that is distributed as part of Icinga 2.
*/
/**
* The constants.conf defines global constants.
*/
include "constants.conf"
/**
* The zones.conf defines zones for a cluster setup.
* Not required for single instance setups.
*/
include "zones.conf"
/**
* The Icinga Template Library (ITL) provides a number of useful templates
* and command definitions.
* Common monitoring plugin command definitions are included separately.
*/
include <itl>
include <plugins>
include <plugins-contrib>
include <manubulon>
/**
* This includes the Icinga 2 Windows plugins. These command definitions
* are required on a master node when a client is used as command endpoint.
*/
include <windows-plugins>
/**
* This includes the NSClient++ check commands. These command definitions
* are required on a master node when a client is used as command endpoint.
*/
include <nscp>
/**
* The features-available directory contains a number of configuration
* files for features which can be enabled and disabled using the
* icinga2 feature enable / icinga2 feature disable CLI commands.
* These commands work by creating and removing symbolic links in
* the features-enabled directory.
*/
include "features-enabled/*.conf"
/**
* Although in theory you could define all your objects in this file
* the preferred way is to create separate directories and files in the conf.d
* directory. Each of these files must have the file extension ".conf".
*/
// Disabled by the node setup CLI command
// include_recursive "conf.d"

View file

@ -0,0 +1,14 @@
object Endpoint "{{ inventory_hostname }}" {
}
object Zone "master" {
endpoints = [ "{{ inventory_hostname }}" ]
}
object Zone "global-templates" {
global = true
}
object Zone "director-global" {
global = true
}

View file

@ -0,0 +1,24 @@
---
librenms_user: librenms
librenms_group: librenms
librenms_path: /opt/librenms
librenms_domain: librenms.vntx.net
# PHP
librenms_php_version: "8.4"
librenms_php_fpm_socket: /run/php-fpm-librenms.sock
# PHP-FPM pool settings
librenms_fpm_max_children: 5
librenms_fpm_start_servers: 2
librenms_fpm_min_spare_servers: 1
librenms_fpm_max_spare_servers: 3
# MariaDB
librenms_db_name: librenms
librenms_db_user: librenms
librenms_db_password: "{{ vault_librenms_db_password }}"
librenms_app_key: "{{ vault_librenms_app_key }}"
# Poller
librenms_poller_threads: 16

View file

@ -0,0 +1,29 @@
---
- name: restart php-fpm
service:
name: "php{{ librenms_php_version }}-fpm"
state: restarted
- name: restart caddy
service:
name: caddy
state: restarted
- name: restart snmpd
service:
name: snmpd
state: restarted
- name: restart postfix
service:
name: postfix
state: restarted
- name: reload systemd
systemd:
daemon_reload: true
- name: restart librenms-scheduler
systemd:
name: librenms-scheduler.timer
state: restarted

View file

@ -0,0 +1,241 @@
---
- name: Install MariaDB APT repository
ansible.builtin.deb822_repository:
name: mariadb
types: deb
uris: https://mirror.rackspace.com/mariadb/repo/12.2/debian
suites: trixie
components: main
signed_by: https://mariadb.org/mariadb_release_signing_key.pgp
state: present
- name: Install PHP APT repository (sury)
ansible.builtin.deb822_repository:
name: php
types: deb
uris: https://packages.sury.org/php/
suites: trixie
components: main
signed_by: https://packages.sury.org/php/apt.gpg
state: present
- name: Install required packages
apt:
name:
- acl
- curl
- fping
- git
- graphviz
- imagemagick
- mariadb-client
- mariadb-server
- mtr-tiny
- nmap
- python3-pymysql
- rrdtool
- snmp
- snmpd
- unzip
- whois
- "php{{ librenms_php_version }}-bcmath"
- "php{{ librenms_php_version }}-cli"
- "php{{ librenms_php_version }}-curl"
- "php{{ librenms_php_version }}-fpm"
- "php{{ librenms_php_version }}-gd"
- "php{{ librenms_php_version }}-gmp"
- "php{{ librenms_php_version }}-intl"
- "php{{ librenms_php_version }}-mbstring"
- "php{{ librenms_php_version }}-mysql"
- "php{{ librenms_php_version }}-snmp"
- "php{{ librenms_php_version }}-xml"
- "php{{ librenms_php_version }}-zip"
- postfix
state: present
update_cache: true
- name: Create librenms group
group:
name: "{{ librenms_group }}"
system: true
state: present
- name: Create librenms user
user:
name: "{{ librenms_user }}"
group: "{{ librenms_group }}"
home: "{{ librenms_path }}"
shell: /usr/bin/bash
system: true
create_home: false
state: present
- name: Check if LibreNMS is already installed
stat:
path: "{{ librenms_path }}/lnms"
register: librenms_installed
- name: Clone LibreNMS repository
git:
repo: https://github.com/librenms/librenms.git
dest: "{{ librenms_path }}"
version: master
depth: 1
become_user: "{{ librenms_user }}"
when: not librenms_installed.stat.exists
- name: Set LibreNMS directory ownership
file:
path: "{{ librenms_path }}"
owner: "{{ librenms_user }}"
group: "{{ librenms_group }}"
recurse: true
changed_when: false
- name: Set LibreNMS directory permissions
file:
path: "{{ librenms_path }}"
mode: '0771'
state: directory
- name: Ensure writable directories exist
file:
path: "{{ librenms_path }}/{{ item }}"
owner: "{{ librenms_user }}"
group: "{{ librenms_group }}"
mode: '0775'
state: directory
loop:
- rrd
- storage
- storage/logs
- bootstrap/cache
- name: Set ACLs for LibreNMS directories
acl:
path: "{{ librenms_path }}/{{ item }}"
entry: "default:group::rwx"
state: present
loop:
- rrd
- storage
- storage/logs
- bootstrap/cache
- name: Deploy LibreNMS .env file
template:
src: env.j2
dest: "{{ librenms_path }}/.env"
owner: "{{ librenms_user }}"
group: "{{ librenms_group }}"
mode: '0640'
- name: Configure PHP-FPM pool for LibreNMS
template:
src: php-fpm-librenms.conf.j2
dest: "/etc/php/{{ librenms_php_version }}/fpm/pool.d/librenms.conf"
owner: root
group: root
mode: '0644'
notify: restart php-fpm
- name: Set PHP memory_limit
lineinfile:
path: "/etc/php/{{ librenms_php_version }}/fpm/php.ini"
regexp: '^memory_limit\s*='
line: 'memory_limit = 512M'
notify: restart php-fpm
- name: Configure Caddy for LibreNMS
template:
src: Caddyfile.j2
dest: /etc/caddy/Caddyfile
owner: root
group: root
mode: '0644'
notify: restart caddy
- name: Deploy LibreNMS cron jobs
template:
src: cron-librenms.j2
dest: /etc/cron.d/librenms
owner: root
group: root
mode: '0644'
- name: Deploy LibreNMS scheduler service
template:
src: librenms-scheduler.service.j2
dest: /etc/systemd/system/librenms-scheduler.service
owner: root
group: root
mode: '0644'
notify: reload systemd
- name: Deploy LibreNMS scheduler timer
template:
src: librenms-scheduler.timer.j2
dest: /etc/systemd/system/librenms-scheduler.timer
owner: root
group: root
mode: '0644'
notify:
- reload systemd
- restart librenms-scheduler
- name: Enable and start LibreNMS scheduler timer
systemd:
name: librenms-scheduler.timer
state: started
enabled: true
- name: Configure SNMPD
template:
src: snmpd.conf.j2
dest: /etc/snmp/snmpd.conf
owner: root
group: root
mode: '0600'
notify: restart snmpd
- name: Configure Postfix for local-only delivery
lineinfile:
path: /etc/postfix/main.cf
regexp: "{{ item.regexp }}"
line: "{{ item.line }}"
loop:
- regexp: '^inet_interfaces\s*='
line: 'inet_interfaces = loopback-only'
- regexp: '^mydestination\s*='
line: 'mydestination = $myhostname, localhost.$mydomain, localhost'
notify: restart postfix
- name: Ensure MariaDB is running
service:
name: mariadb
state: started
enabled: true
- name: Ensure Caddy is running
service:
name: caddy
state: started
enabled: true
- name: Ensure PHP-FPM is running
service:
name: "php{{ librenms_php_version }}-fpm"
state: started
enabled: true
- name: Ensure SNMPD is running
service:
name: snmpd
state: started
enabled: true
- name: Ensure Postfix is running
service:
name: postfix
state: started
enabled: true

View file

@ -0,0 +1,16 @@
{{ librenms_domain }} {
root * {{ librenms_path }}/html
encode gzip
php_fastcgi unix/{{ librenms_php_fpm_socket }}
file_server
@dotfiles path */.*
@wellknown path /.well-known/*
handle @wellknown {
file_server
}
respond @dotfiles 403
}

View file

@ -0,0 +1,12 @@
33 */6 * * * {{ librenms_user }} {{ librenms_path }}/cronic {{ librenms_path }}/discovery-wrapper.py 1
*/5 * * * * {{ librenms_user }} {{ librenms_path }}/discovery.php -h new >> /dev/null 2>&1
*/5 * * * * {{ librenms_user }} {{ librenms_path }}/cronic {{ librenms_path }}/poller-wrapper.py {{ librenms_poller_threads }}
* * * * * {{ librenms_user }} {{ librenms_path }}/alerts.php >> /dev/null 2>&1
*/5 * * * * {{ librenms_user }} {{ librenms_path }}/poll-billing.php >> /dev/null 2>&1
01 * * * * {{ librenms_user }} {{ librenms_path }}/billing-calculate.php >> /dev/null 2>&1
*/5 * * * * {{ librenms_user }} {{ librenms_path }}/check-services.php >> /dev/null 2>&1
# Daily maintenance script. DO NOT DISABLE!
19 0 * * * {{ librenms_user }} {{ librenms_path }}/daily.sh >> /dev/null 2>&1

View file

@ -0,0 +1,5 @@
DB_DATABASE={{ librenms_db_name }}
DB_USERNAME={{ librenms_db_user }}
DB_PASSWORD={{ librenms_db_password }}
APP_KEY={{ librenms_app_key }}
NODE_ID={{ librenms_node_id | default('695bd7d80d63b') }}

View file

@ -0,0 +1,11 @@
[Unit]
Description=LibreNMS Scheduler
[Service]
Type=oneshot
StandardOutput=null
StandardError=null
WorkingDirectory={{ librenms_path }}/
ExecStart=/usr/bin/env php artisan schedule:run --no-ansi --no-interaction
User={{ librenms_user }}
Group={{ librenms_group }}

View file

@ -0,0 +1,9 @@
[Unit]
Description=LibreNMS Scheduler
[Timer]
OnCalendar=minutely
AccuracySec=1second
[Install]
WantedBy=timers.target

View file

@ -0,0 +1,14 @@
[librenms]
user = {{ librenms_user }}
group = {{ librenms_group }}
listen = {{ librenms_php_fpm_socket }}
listen.owner = www-data
listen.group = {{ librenms_group }}
pm = dynamic
pm.max_children = {{ librenms_fpm_max_children }}
pm.start_servers = {{ librenms_fpm_start_servers }}
pm.min_spare_servers = {{ librenms_fpm_min_spare_servers }}
pm.max_spare_servers = {{ librenms_fpm_max_spare_servers }}

View file

@ -0,0 +1,3 @@
rocommunity public 127.0.0.1
syslocation "VNTX Data Center"
syscontact admin@vntx.net

View file

@ -1,122 +0,0 @@
---
# PostgreSQL version
postgresql_version: 17
# Password management
use_1password: false # Set to true to use 1Password for password storage
op_account_id: "YOOATCZZSVGH7AD6VABUVPORLI" # Default 1Password account ID
# Storage configuration
postgresql_storage_setup_enabled: false # Set to true to configure storage
postgresql_storage_type: raid0 # Options: single_nvme, raid0
postgresql_raid_device: /dev/md0
postgresql_storage_device: "{{ postgresql_raid_device if postgresql_storage_type == 'raid0' else '/dev/nvme0n1p1' }}"
postgresql_mount_point: /mnt/pgdata
# PostgreSQL configuration
postgresql_data_directory: /mnt/pgdata/{{ postgresql_version }}/main
postgresql_wal_directory: /mnt/pgdata/wal
postgresql_archive_directory: /mnt/pgdata/archive
postgresql_config_directory: /etc/postgresql/{{ postgresql_version }}/main
# Database settings for APRS.me
postgresql_databases:
- name: aprsme_prod
owner: aprsme
postgresql_users:
- name: aprsme
password: "{{ vault_postgresql_aprsme_password }}" # Store in vault
priv: "aprsme_prod:ALL"
# Performance tuning for CM3588 with 16GB RAM
# These are optimized for APRS.me workload
postgresql_config:
# Connection settings
listen_addresses: "'*'"
max_connections: 200 # Increased for 16GB system
# Memory settings (for 16GB RAM system)
shared_buffers: 4GB # 25% of RAM
effective_cache_size: 12GB # 75% of RAM
maintenance_work_mem: 1GB
work_mem: 64MB # Increased for available memory
# Disk settings optimized for RAID0 SSDs
random_page_cost: 1.1 # SSD optimization
effective_io_concurrency: 200 # Modern SSD
max_worker_processes: 4 # Pi 5 has 4 cores
max_parallel_workers_per_gather: 2
max_parallel_workers: 4
max_parallel_maintenance_workers: 2
# Write performance settings
wal_buffers: 32MB
checkpoint_completion_target: 0.9
checkpoint_timeout: 10min
max_wal_size: 4GB
min_wal_size: 512MB
# Logging
log_min_duration_statement: 1000 # Log queries over 1 second
log_checkpoints: on
log_connections: on
log_disconnections: on
log_line_prefix: "'%t [%p]: [%l-1] user=%u,db=%d,app=%a,client=%h '"
log_timezone: 'UTC'
# Query tuning
default_statistics_target: 100
# Autovacuum tuning for high insert rate
autovacuum: on
autovacuum_max_workers: 4
autovacuum_naptime: 30s # More frequent checks
autovacuum_vacuum_threshold: 50
autovacuum_analyze_threshold: 50
autovacuum_vacuum_scale_factor: 0.1 # 10% for more frequent vacuums
autovacuum_analyze_scale_factor: 0.05 # 5% for more frequent analyzes
# Enable query planning statistics
track_activity_query_size: 2048
track_io_timing: on
track_functions: all
# PostGIS specific
postgis.gdal_datapath: "'/usr/share/gdal'"
# Security settings
postgresql_hba_entries:
- type: local
database: all
user: postgres
auth_method: peer
- type: local
database: all
user: all
auth_method: peer
- type: host
database: all
user: all
address: 127.0.0.1/32
auth_method: scram-sha-256
- type: host
database: all
user: all
address: ::1/128
auth_method: scram-sha-256
- type: hostssl
database: aprsme_prod
user: aprsme
address: 10.0.0.0/8 # Adjust for your network
auth_method: scram-sha-256
# Backup settings
postgresql_backup_enabled: true
postgresql_backup_directory: /mnt/pgdata/backups
postgresql_backup_schedule:
minute: "0"
hour: "2"
day: "*"
month: "*"
weekday: "*"

View file

@ -1,26 +0,0 @@
---
- name: restart postgresql
systemd:
name: postgresql
state: restarted
daemon_reload: true
- name: reload postgresql
systemd:
name: postgresql
state: reloaded
- name: restart pgbouncer
systemd:
name: pgbouncer
state: restarted
- name: reload systemd
systemd:
daemon_reload: true
- name: reload sysctl
command: sysctl -p /etc/sysctl.d/30-postgresql.conf
- name: reload udev
command: udevadm control --reload-rules && udevadm trigger

View file

@ -1,55 +0,0 @@
---
# 1Password integration for PostgreSQL passwords
# This file handles password generation and storage in 1Password
- name: Check if 1Password CLI is available
command: which op
register: op_cli_check
changed_when: false
failed_when: false
delegate_to: localhost
- name: Fail if 1Password CLI is not installed
fail:
msg: "1Password CLI (op) is not installed. Please install it first."
when: op_cli_check.rc != 0
- name: Generate password for PostgreSQL user
set_fact:
generated_password: "{{ lookup('password', '/dev/null length=32 chars=ascii_letters,digits') }}"
when: vault_postgresql_aprsme_password is not defined
no_log: true
- name: Create 1Password item for PostgreSQL password
shell: |
op item create \
--category=database \
--title="PostgreSQL {{ item.name }} - {{ ansible_hostname }}" \
--vault="Private" \
username="{{ item.name }}" \
password="{{ generated_password | default(vault_postgresql_aprsme_password) }}" \
hostname="{{ ansible_host }}" \
port="5432" \
database="{{ postgresql_databases[0].name }}" \
--account="{{ op_account_id | default('YOOATCZZSVGH7AD6VABUVPORLI') }}" \
--format=json || \
op item edit \
"PostgreSQL {{ item.name }} - {{ ansible_hostname }}" \
password="{{ generated_password | default(vault_postgresql_aprsme_password) }}" \
--account="{{ op_account_id | default('YOOATCZZSVGH7AD6VABUVPORLI') }}"
delegate_to: localhost
loop: "{{ postgresql_users }}"
when: use_1password | default(false)
no_log: true
register: op_items
- name: Set password fact from 1Password or generated
set_fact:
postgresql_users_with_passwords: >-
{%- set users = [] -%}
{%- for user in postgresql_users -%}
{%- set _ = user.update({'password': generated_password | default(vault_postgresql_aprsme_password)}) -%}
{%- set _ = users.append(user) -%}
{%- endfor -%}
{{ users }}
no_log: true

View file

@ -1,372 +0,0 @@
---
# Set up storage before installing PostgreSQL
- name: Set up storage for PostgreSQL
include_tasks: storage.yml
when: postgresql_storage_setup_enabled | default(false)
- name: Download PostgreSQL GPG key
ansible.builtin.get_url:
url: https://www.postgresql.org/media/keys/ACCC4CF8.asc
dest: /usr/share/keyrings/postgresql-archive-keyring.asc
mode: '0644'
- name: Add PostgreSQL repository
ansible.builtin.deb822_repository:
name: pgdg
types: deb
uris: http://apt.postgresql.org/pub/repos/apt
suites: "{{ ansible_distribution_release }}-pgdg"
components: main
signed_by: /usr/share/keyrings/postgresql-archive-keyring.asc
state: present
- name: Install PostgreSQL and PostGIS
apt:
name:
- postgresql-{{ postgresql_version }}
- postgresql-client-{{ postgresql_version }}
- postgresql-contrib-{{ postgresql_version }}
- postgresql-{{ postgresql_version }}-postgis-3
- postgresql-{{ postgresql_version }}-postgis-3-scripts
- postgis
- python3-psycopg2
- libpq-dev
- pgbouncer
state: present
update_cache: true
- name: Stop PostgreSQL service if running
systemd:
name: postgresql
state: stopped
failed_when: false
- name: Disable PostgreSQL from starting automatically (we'll configure it first)
systemd:
name: postgresql
enabled: false
- name: Remove any default PostgreSQL data directory
file:
path: /var/lib/postgresql/{{ postgresql_version }}/main
state: absent
- name: Install locales package
apt:
name: locales
state: present
- name: Generate en_US.UTF-8 locale
locale_gen:
name: en_US.UTF-8
state: present
- name: Create PostgreSQL config directory structure
file:
path: "{{ item }}"
state: directory
owner: postgres
group: postgres
mode: '0755'
loop:
- /etc/postgresql
- /etc/postgresql/{{ postgresql_version }}
- /etc/postgresql/{{ postgresql_version }}/main
- name: Create PostgreSQL cluster on RAID array
command: |
/usr/lib/postgresql/{{ postgresql_version }}/bin/initdb \
-D {{ postgresql_data_directory }} \
--wal-segsize=64 \
--data-checksums \
--encoding=UTF8 \
--locale=C.UTF-8 \
--lc-collate=C.UTF-8 \
--lc-ctype=C.UTF-8
args:
creates: "{{ postgresql_data_directory }}/PG_VERSION"
become_user: postgres
environment:
PGDATA: "{{ postgresql_data_directory }}"
- name: Set up SSL/TLS
include_tasks: ssl.yml
- name: Configure PostgreSQL
template:
src: postgresql.conf.j2
dest: "{{ postgresql_config_directory }}/postgresql.conf"
owner: postgres
group: postgres
mode: '0644'
notify: restart postgresql
- name: Configure PostgreSQL authentication
template:
src: pg_hba.conf.j2
dest: "{{ postgresql_config_directory }}/pg_hba.conf"
owner: postgres
group: postgres
mode: '0640'
notify: restart postgresql
- name: Create sysctl configuration for PostgreSQL
template:
src: 30-postgresql.conf.j2
dest: /etc/sysctl.d/30-postgresql.conf
mode: '0644'
notify: reload sysctl
- name: Create systemd override directory
file:
path: /etc/systemd/system/postgresql@.service.d
state: directory
mode: '0755'
- name: Configure systemd limits for PostgreSQL
template:
src: postgresql-override.conf.j2
dest: /etc/systemd/system/postgresql@.service.d/override.conf
mode: '0644'
notify:
- reload systemd
- restart postgresql
- name: Check if PostgreSQL cluster is properly initialized
stat:
path: "{{ postgresql_data_directory }}/PG_VERSION"
register: pg_initialized
- name: Reinitialize PostgreSQL if corrupted
block:
- name: Stop PostgreSQL if running
systemd:
name: postgresql@{{ postgresql_version }}-main
state: stopped
failed_when: false
- name: Remove corrupted data directory
file:
path: "{{ postgresql_data_directory }}"
state: absent
- name: Create data directory
file:
path: "{{ postgresql_data_directory }}"
state: directory
owner: postgres
group: postgres
mode: '0700'
- name: Clear WAL directory for reinit
file:
path: "{{ postgresql_wal_directory }}"
state: absent
- name: Create fresh WAL directory
file:
path: "{{ postgresql_wal_directory }}"
state: directory
owner: postgres
group: postgres
mode: '0700'
- name: Initialize new PostgreSQL cluster
command: |
/usr/lib/postgresql/{{ postgresql_version }}/bin/initdb \
-D {{ postgresql_data_directory }} \
--wal-segsize=64 \
--data-checksums \
--encoding=UTF8 \
--locale=C.UTF-8 \
--lc-collate=C.UTF-8 \
--lc-ctype=C.UTF-8 \
--waldir={{ postgresql_wal_directory }}
become_user: postgres
environment:
PGDATA: "{{ postgresql_data_directory }}"
when: pg_initialized.stat.exists and force_reinit | default(false)
- name: Ensure WAL directory is properly configured
block:
- name: Check if WAL directory exists
stat:
path: "{{ postgresql_wal_directory }}"
register: wal_dir_stat
- name: Create WAL directory if missing
file:
path: "{{ postgresql_wal_directory }}"
state: directory
owner: postgres
group: postgres
mode: '0700'
when: not wal_dir_stat.stat.exists
- name: Check if pg_wal is a symlink
stat:
path: "{{ postgresql_data_directory }}/pg_wal"
register: pg_wal_stat
- name: Ensure pg_wal symlink has correct ownership
file:
path: "{{ postgresql_data_directory }}/pg_wal"
owner: postgres
group: postgres
when: pg_wal_stat.stat.exists and pg_wal_stat.stat.islnk
- name: Start and enable PostgreSQL
systemd:
name: postgresql
state: started
enabled: true
daemon_reload: true
- name: Wait for PostgreSQL to start
wait_for:
port: 5432
host: localhost
delay: 5
- name: Handle password management with 1Password
include_tasks: 1password.yml
when: use_1password | default(false)
- name: Create PostgreSQL users
postgresql_user:
name: "{{ item.name }}"
password: "{{ item.password }}"
encrypted: true
state: present
become: true
become_user: postgres
loop: "{{ postgresql_users }}"
no_log: true
- name: Create databases
postgresql_db:
name: "{{ item.name }}"
owner: "{{ item.owner }}"
encoding: UTF-8
lc_collate: en_US.UTF-8
lc_ctype: en_US.UTF-8
template: template0
state: present
become: true
become_user: postgres
loop: "{{ postgresql_databases }}"
- name: Enable PostGIS extension
postgresql_ext:
name: postgis
db: "{{ item.name }}"
state: present
become: true
become_user: postgres
loop: "{{ postgresql_databases }}"
- name: Enable additional extensions
postgresql_ext:
name: "{{ item[1] }}"
db: "{{ item[0].name }}"
state: present
become: true
become_user: postgres
loop: "{{ postgresql_databases | product(['pg_stat_statements', 'btree_gist', 'btree_gin']) | list }}"
- name: Grant privileges
postgresql_privs:
database: "{{ priv_item.split(':')[0] }}"
privs: "{{ priv_item.split(':')[1] }}"
type: database
role: "{{ user.name }}"
state: present
become: true
become_user: postgres
loop: "{{ postgresql_users | selectattr('priv', 'defined') | map(attribute='priv') | list }}"
loop_control:
loop_var: priv_item
vars:
user: "{{ postgresql_users | selectattr('priv', 'equalto', priv_item) | first }}"
- name: Retrieve SCRAM hashes for PgBouncer
postgresql_query:
db: postgres
query: "SELECT rolname, rolpassword FROM pg_authid WHERE rolname = %s"
positional_args:
- "{{ item.name }}"
become: true
become_user: postgres
loop: "{{ postgresql_users }}"
register: scram_hashes
no_log: true
- name: Configure PgBouncer
template:
src: pgbouncer.ini.j2
dest: /etc/pgbouncer/pgbouncer.ini
owner: postgres
group: postgres
mode: '0640'
notify: restart pgbouncer
- name: Configure PgBouncer authentication
template:
src: pgbouncer-userlist.txt.j2
dest: /etc/pgbouncer/userlist.txt
owner: postgres
group: postgres
mode: '0640'
notify: restart pgbouncer
- name: Enable and start PgBouncer
systemd:
name: pgbouncer
state: started
enabled: true
- name: Set up backup script
template:
src: backup-postgresql.sh.j2
dest: /usr/local/bin/backup-postgresql
mode: '0755'
when: postgresql_backup_enabled
- name: Create backup directory
file:
path: "{{ postgresql_backup_directory }}"
state: directory
owner: postgres
group: postgres
mode: '0755'
when: postgresql_backup_enabled
- name: Set up backup cron job
cron:
name: "PostgreSQL backup"
minute: "{{ postgresql_backup_schedule.minute }}"
hour: "{{ postgresql_backup_schedule.hour }}"
day: "{{ postgresql_backup_schedule.day }}"
month: "{{ postgresql_backup_schedule.month }}"
weekday: "{{ postgresql_backup_schedule.weekday }}"
user: postgres
job: "/usr/local/bin/backup-postgresql"
when: postgresql_backup_enabled
- name: Configure log rotation
template:
src: postgresql-logrotate.j2
dest: /etc/logrotate.d/postgresql-custom
mode: '0644'
- name: Install monitoring script
template:
src: check-postgresql.sh.j2
dest: /usr/local/bin/check-postgresql
mode: '0755'
- name: Set up monitoring cron job
cron:
name: "Check PostgreSQL health"
minute: "*/5"
job: "/usr/local/bin/check-postgresql || echo 'PostgreSQL health check failed' | logger -t postgresql-health"

View file

@ -1,70 +0,0 @@
---
# SSL/TLS configuration for PostgreSQL
- name: Install OpenSSL
apt:
name: openssl
state: present
- name: Create SSL directory
file:
path: "{{ postgresql_data_directory }}/ssl"
state: directory
owner: postgres
group: postgres
mode: '0700'
- name: Check if SSL certificate exists
stat:
path: "{{ postgresql_data_directory }}/ssl/server.crt"
register: ssl_cert
- name: Generate private key for PostgreSQL
shell: |
openssl genrsa -out {{ postgresql_data_directory }}/ssl/server.key 4096
chmod 600 {{ postgresql_data_directory }}/ssl/server.key
chown postgres:postgres {{ postgresql_data_directory }}/ssl/server.key
when: not ssl_cert.stat.exists
- name: Generate self-signed certificate
shell: |
openssl req -new -x509 -days 3650 \
-key {{ postgresql_data_directory }}/ssl/server.key \
-out {{ postgresql_data_directory }}/ssl/server.crt \
-subj "/C=US/ST=State/L=City/O=Organization/CN={{ ansible_hostname }}" \
-addext "subjectAltName=DNS:{{ ansible_hostname }},DNS:{{ ansible_fqdn | default(ansible_hostname) }},IP:{{ ansible_default_ipv4.address }},IP:{{ ansible_host }}"
chmod 644 {{ postgresql_data_directory }}/ssl/server.crt
chown postgres:postgres {{ postgresql_data_directory }}/ssl/server.crt
when: not ssl_cert.stat.exists
- name: Create CA certificate (copy server cert as CA for self-signed)
copy:
src: "{{ postgresql_data_directory }}/ssl/server.crt"
dest: "{{ postgresql_data_directory }}/ssl/ca.crt"
remote_src: true
owner: postgres
group: postgres
mode: '0644'
- name: Set proper permissions on SSL files
file:
path: "{{ item.path }}"
owner: postgres
group: postgres
mode: "{{ item.mode }}"
loop:
- { path: "{{ postgresql_data_directory }}/ssl/server.key", mode: '0600' }
- { path: "{{ postgresql_data_directory }}/ssl/server.crt", mode: '0644' }
- { path: "{{ postgresql_data_directory }}/ssl/ca.crt", mode: '0644' }
- name: Create symlinks in PostgreSQL config directory
file:
src: "{{ item.src }}"
dest: "{{ item.dest }}"
state: link
owner: postgres
group: postgres
loop:
- { src: "{{ postgresql_data_directory }}/ssl/server.crt", dest: "{{ postgresql_config_directory }}/server.crt" }
- { src: "{{ postgresql_data_directory }}/ssl/server.key", dest: "{{ postgresql_config_directory }}/server.key" }
- { src: "{{ postgresql_data_directory }}/ssl/ca.crt", dest: "{{ postgresql_config_directory }}/ca.crt" }

View file

@ -1,113 +0,0 @@
---
# Storage setup for PostgreSQL on NVMe drives
- name: Install storage utilities
apt:
name:
- mdadm
- xfsprogs
- nvme-cli
- smartmontools
state: present
update_cache: true
- name: Check for NVMe devices
shell: nvme list -o json
register: nvme_devices
changed_when: false
failed_when: false
- name: Create partition on NVMe drive
parted:
device: /dev/nvme0n1
number: 1
state: present
label: gpt
part_start: 0%
part_end: 100%
fs_type: ext4
- name: Create RAID0 array with single drive (for future expansion)
command: |
mdadm --create {{ postgresql_raid_device }} \
--level=0 \
--raid-devices=1 \
--force \
/dev/nvme0n1p1
args:
creates: "{{ postgresql_raid_device }}"
when: postgresql_storage_type == "raid0"
- name: Wait for RAID array to be ready
wait_for:
path: "{{ postgresql_raid_device }}"
state: present
when: postgresql_storage_type == "raid0"
- name: Create filesystem on storage device
filesystem:
fstype: xfs
dev: "{{ postgresql_storage_device }}"
opts: "-L pgdata"
when: postgresql_storage_device is defined
- name: Get filesystem UUID
command: blkid -s UUID -o value {{ postgresql_storage_device }}
register: fs_uuid
changed_when: false
when: postgresql_storage_device is defined
- name: Create mount points
file:
path: "{{ item }}"
state: directory
owner: root
group: root
mode: '0755'
loop:
- "{{ postgresql_mount_point }}"
- "{{ postgresql_data_directory }}"
- "{{ postgresql_wal_directory }}"
- name: Mount PostgreSQL storage
mount:
path: "{{ postgresql_mount_point }}"
src: "UUID={{ fs_uuid.stdout }}"
fstype: xfs
opts: "noatime,nodiratime"
state: mounted
when: fs_uuid.stdout is defined
- name: Save RAID configuration
shell: mdadm --detail --scan >> /etc/mdadm/mdadm.conf
when: postgresql_storage_type == "raid0"
- name: Update initramfs
command: update-initramfs -u
when: postgresql_storage_type == "raid0"
- name: Set optimal I/O scheduler for NVMe
copy:
content: |
ACTION=="add|change", KERNEL=="nvme[0-9]n[0-9]", ATTR{queue/scheduler}="none"
ACTION=="add|change", KERNEL=="nvme[0-9]n[0-9]", ATTR{queue/nr_requests}="1024"
dest: /etc/udev/rules.d/60-nvme-scheduler.rules
mode: '0644'
notify: reload udev
- name: Configure sysctl for PostgreSQL performance
sysctl:
name: "{{ item.name }}"
value: "{{ item.value }}"
state: present
reload: true
loop:
- { name: 'vm.dirty_background_ratio', value: '5' }
- { name: 'vm.dirty_ratio', value: '10' }
- { name: 'vm.swappiness', value: '10' }
- { name: 'vm.overcommit_memory', value: '2' }
- { name: 'vm.overcommit_ratio', value: '95' }
register: sysctl_result
failed_when:
- sysctl_result.failed
- "'No such file or directory' not in sysctl_result.msg | default('')"

View file

@ -1,34 +0,0 @@
# Kernel parameters for PostgreSQL optimization
# For CM3588 with 16GB RAM and RAID0 SSDs
# Shared memory settings
kernel.shmmax = 17179869184 # 16GB
kernel.shmall = 4194304 # 16GB / 4096 (page size)
kernel.shmmni = 4096
# Semaphores: semmsl, semmns, semopm, semmni
kernel.sem = 250 32000 100 128
# Virtual memory
vm.swappiness = 10
vm.dirty_ratio = 15
vm.dirty_background_ratio = 5
vm.overcommit_memory = 2
vm.overcommit_ratio = 95
# Huge pages (optional, can be enabled if needed)
# vm.nr_hugepages = 4096 # 8GB of huge pages
# Network optimization for 2.5Gbit connection
net.core.rmem_default = 262144
net.core.rmem_max = 4194304
net.core.wmem_default = 262144
net.core.wmem_max = 4194304
net.ipv4.tcp_rmem = 4096 87380 4194304
net.ipv4.tcp_wmem = 4096 65536 4194304
net.ipv4.tcp_keepalive_time = 60
net.ipv4.tcp_keepalive_intvl = 10
net.ipv4.tcp_keepalive_probes = 6
# File system
fs.file-max = 65536

View file

@ -1,48 +0,0 @@
#!/bin/bash
# PostgreSQL backup script for APRS.me
set -euo pipefail
# Configuration
BACKUP_DIR="{{ postgresql_backup_directory }}"
DB_NAME="aprsme_prod"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="${BACKUP_DIR}/${DB_NAME}_${TIMESTAMP}.sql.gz"
KEEP_DAYS=7
# Create backup directory if it doesn't exist
mkdir -p "${BACKUP_DIR}"
# Log backup start
echo "[$(date)] Starting backup of ${DB_NAME}" | logger -t postgresql-backup
# Perform backup with compression
if pg_dump -Fc -Z9 "${DB_NAME}" > "${BACKUP_FILE}.tmp"; then
mv "${BACKUP_FILE}.tmp" "${BACKUP_FILE}"
echo "[$(date)] Backup completed: ${BACKUP_FILE}" | logger -t postgresql-backup
# Calculate backup size
SIZE=$(du -h "${BACKUP_FILE}" | cut -f1)
echo "[$(date)] Backup size: ${SIZE}" | logger -t postgresql-backup
else
echo "[$(date)] Backup failed for ${DB_NAME}" | logger -t postgresql-backup -p user.err
rm -f "${BACKUP_FILE}.tmp"
exit 1
fi
# Clean up old backups
echo "[$(date)] Cleaning up backups older than ${KEEP_DAYS} days" | logger -t postgresql-backup
find "${BACKUP_DIR}" -name "${DB_NAME}_*.sql.gz" -type f -mtime +${KEEP_DAYS} -delete
# Verify backup integrity
if pg_restore -l "${BACKUP_FILE}" > /dev/null 2>&1; then
echo "[$(date)] Backup verification successful" | logger -t postgresql-backup
else
echo "[$(date)] Backup verification failed" | logger -t postgresql-backup -p user.err
exit 1
fi
# Optional: Copy to remote backup location
# rsync -av "${BACKUP_FILE}" user@backup-server:/path/to/backups/
exit 0

View file

@ -1,60 +0,0 @@
#!/bin/bash
# PostgreSQL health check script
set -euo pipefail
# Check if PostgreSQL is running
if ! systemctl is-active --quiet postgresql; then
echo "PostgreSQL service is not running"
exit 1
fi
# Check if we can connect to PostgreSQL
if ! pg_isready -h localhost -p 5432 -U postgres -q; then
echo "Cannot connect to PostgreSQL"
exit 1
fi
# Check database connection
if ! psql -h localhost -p 5432 -U postgres -d aprsme_prod -c "SELECT 1" > /dev/null 2>&1; then
echo "Cannot connect to aprsme_prod database"
exit 1
fi
# Check PgBouncer
if ! pg_isready -h localhost -p 6432 -U postgres -q; then
echo "PgBouncer is not responding"
exit 1
fi
# Check replication lag (if applicable)
LAG=$(psql -h localhost -p 5432 -U postgres -t -c "SELECT EXTRACT(EPOCH FROM (NOW() - pg_last_xact_replay_timestamp()))::INT" 2>/dev/null || echo "0")
if [ "${LAG}" -gt "60" ]; then
echo "Replication lag is too high: ${LAG} seconds"
exit 1
fi
# Check disk space
DISK_USAGE=$(df -h /var/lib/postgresql | tail -1 | awk '{print $5}' | sed 's/%//')
if [ "${DISK_USAGE}" -gt "90" ]; then
echo "Disk usage is critical: ${DISK_USAGE}%"
exit 1
fi
# Check connection count
CONNECTIONS=$(psql -h localhost -p 5432 -U postgres -t -c "SELECT COUNT(*) FROM pg_stat_activity" 2>/dev/null || echo "0")
MAX_CONNECTIONS=$(psql -h localhost -p 5432 -U postgres -t -c "SHOW max_connections" 2>/dev/null || echo "200")
if [ "${CONNECTIONS}" -gt "$(( ${MAX_CONNECTIONS} * 90 / 100 ))" ]; then
echo "Connection count is high: ${CONNECTIONS}/${MAX_CONNECTIONS}"
exit 1
fi
# Check for long running queries
LONG_QUERIES=$(psql -h localhost -p 5432 -U postgres -t -c "SELECT COUNT(*) FROM pg_stat_activity WHERE state != 'idle' AND query_start < NOW() - INTERVAL '5 minutes'" 2>/dev/null || echo "0")
if [ "${LONG_QUERIES}" -gt "5" ]; then
echo "Too many long running queries: ${LONG_QUERIES}"
exit 1
fi
echo "PostgreSQL health check passed"
exit 0

View file

@ -1,9 +0,0 @@
# PostgreSQL Client Authentication Configuration File
# ===================================================
# Managed by Ansible - Do not edit manually
# TYPE DATABASE USER ADDRESS METHOD
{% for entry in postgresql_hba_entries %}
{{ entry.type }} {{ entry.database }} {{ entry.user }} {{ entry.address | default('') }} {{ entry.auth_method }}
{% endfor %}

View file

@ -1,13 +0,0 @@
# PgBouncer userlist file
# Format: "username" "password_or_scram_hash"
# Managed by Ansible
# Note: When using SCRAM-SHA-256, this contains the SCRAM hash from pg_authid
{% for result in scram_hashes.results %}
{% if result.query_result[0].rolpassword %}
"{{ result.query_result[0].rolname }}" "{{ result.query_result[0].rolpassword }}"
{% endif %}
{% endfor %}
# PgBouncer stats user
"pgbouncer" "pgbouncer"

View file

@ -1,73 +0,0 @@
[databases]
# Connection string for aprsme database
aprsme_prod = host=127.0.0.1 port=5432 dbname=aprsme_prod
[pgbouncer]
# Connection settings
listen_addr = *
listen_port = 6432
auth_type = scram-sha-256
auth_file = /etc/pgbouncer/userlist.txt
# Pool settings optimized for APRS.me
pool_mode = transaction
max_client_conn = 1000
default_pool_size = 25
min_pool_size = 5
reserve_pool_size = 5
reserve_pool_timeout = 3
# Performance settings
max_db_connections = 100
max_user_connections = 100
server_lifetime = 3600
server_idle_timeout = 600
server_connect_timeout = 15
server_login_retry = 15
query_timeout = 0
query_wait_timeout = 120
client_idle_timeout = 0
client_login_timeout = 60
# Logging
log_connections = 1
log_disconnections = 1
log_pooler_errors = 1
stats_period = 60
# Admin settings
admin_users = postgres
stats_users = postgres, aprsme
# Security
ignore_startup_parameters = extra_float_digits
# Resource limits
pkt_buf = 4096
max_packet_size = 2147483647
listen_backlog = 128
sbuf_loopcnt = 5
tcp_defer_accept = 0
tcp_socket_buffer = 0
tcp_keepalive = 1
tcp_keepcnt = 3
tcp_keepidle = 60
tcp_keepintvl = 10
# DNS settings
dns_max_ttl = 15
dns_nxdomain_ttl = 15
# TLS settings for client connections
client_tls_sslmode = allow
client_tls_cert_file = /etc/postgresql/{{ postgresql_version }}/main/server.crt
client_tls_key_file = /etc/postgresql/{{ postgresql_version }}/main/server.key
client_tls_ca_file = /etc/postgresql/{{ postgresql_version }}/main/ca.crt
client_tls_protocols = secure
client_tls_ciphers = HIGH:MEDIUM:+3DES:!aNULL
# TLS settings for server connections
server_tls_sslmode = require
server_tls_ca_file = /etc/postgresql/{{ postgresql_version }}/main/ca.crt
server_tls_protocols = secure
server_tls_ciphers = HIGH:MEDIUM:+3DES:!aNULL

View file

@ -1,16 +0,0 @@
/var/log/postgresql/*.log {
daily
rotate 7
missingok
compress
delaycompress
notifempty
create 640 postgres postgres
sharedscripts
postrotate
# Signal PostgreSQL to reopen log files
if [ -f /var/run/postgresql/{{ postgresql_version }}-main.pid ]; then
/usr/bin/pg_ctlcluster {{ postgresql_version }} main reload > /dev/null
fi
endscript
}

View file

@ -1,17 +0,0 @@
[Service]
# Increase limits for PostgreSQL
LimitNOFILE=65536
LimitNPROC=4096
# Disable OOM killer for PostgreSQL
OOMScoreAdjust=-1000
# CPU affinity - use all cores
CPUAffinity=0-7
# Nice level for better priority
Nice=-5
# I/O scheduling class and priority
IOSchedulingClass=best-effort
IOSchedulingPriority=0

View file

@ -1,115 +0,0 @@
# PostgreSQL configuration for APRS.me on CM3588
# Optimized for RK3588 with RAID0 SSDs
#------------------------------------------------------------------------------
# FILE LOCATIONS
#------------------------------------------------------------------------------
data_directory = '{{ postgresql_data_directory }}'
hba_file = '{{ postgresql_config_directory }}/pg_hba.conf'
ident_file = '{{ postgresql_config_directory }}/pg_ident.conf'
external_pid_file = '/var/run/postgresql/{{ postgresql_version }}-main.pid'
#------------------------------------------------------------------------------
# CONNECTIONS AND AUTHENTICATION
#------------------------------------------------------------------------------
{% for key, value in postgresql_config.items() %}
{{ key }} = {{ value }}
{% endfor %}
#------------------------------------------------------------------------------
# RESOURCE USAGE (except WAL)
#------------------------------------------------------------------------------
# These settings are configured above in postgresql_config
#------------------------------------------------------------------------------
# WRITE-AHEAD LOG
#------------------------------------------------------------------------------
# WAL settings configured above
#------------------------------------------------------------------------------
# REPLICATION
#------------------------------------------------------------------------------
wal_level = replica
archive_mode = on
archive_command = 'test ! -f {{ postgresql_archive_directory }}/%f && cp %p {{ postgresql_archive_directory }}/%f'
max_wal_senders = 3
wal_keep_size = 1GB
#------------------------------------------------------------------------------
# QUERY TUNING
#------------------------------------------------------------------------------
# Planner settings for PostGIS and complex queries
enable_partitionwise_join = on
enable_partitionwise_aggregate = on
jit = on
#------------------------------------------------------------------------------
# REPORTING AND LOGGING
#------------------------------------------------------------------------------
# Logging configuration set above
#------------------------------------------------------------------------------
# STATISTICS
#------------------------------------------------------------------------------
# Stats configuration set above
shared_preload_libraries = 'pg_stat_statements'
pg_stat_statements.max = 10000
pg_stat_statements.track = all
#------------------------------------------------------------------------------
# AUTOVACUUM
#------------------------------------------------------------------------------
# Autovacuum settings configured above
#------------------------------------------------------------------------------
# CLIENT CONNECTION DEFAULTS
#------------------------------------------------------------------------------
datestyle = 'iso, mdy'
timezone = 'UTC'
lc_messages = 'en_US.UTF-8'
lc_monetary = 'en_US.UTF-8'
lc_numeric = 'en_US.UTF-8'
lc_time = 'en_US.UTF-8'
default_text_search_config = 'pg_catalog.english'
#------------------------------------------------------------------------------
# LOCK MANAGEMENT
#------------------------------------------------------------------------------
deadlock_timeout = 1s
max_locks_per_transaction = 128
#------------------------------------------------------------------------------
# VERSION/PLATFORM COMPATIBILITY
#------------------------------------------------------------------------------
# No specific settings needed
#------------------------------------------------------------------------------
# ERROR HANDLING
#------------------------------------------------------------------------------
exit_on_error = off
restart_after_crash = on
#------------------------------------------------------------------------------
# SSL/TLS CONFIGURATION
#------------------------------------------------------------------------------
ssl = on
ssl_cert_file = '/etc/postgresql/{{ postgresql_version }}/main/server.crt'
ssl_key_file = '/etc/postgresql/{{ postgresql_version }}/main/server.key'
ssl_ca_file = '/etc/postgresql/{{ postgresql_version }}/main/ca.crt'
ssl_ciphers = 'HIGH:MEDIUM:+3DES:!aNULL'
ssl_prefer_server_ciphers = on
ssl_min_protocol_version = 'TLSv1.2'

View file

@ -1,15 +1,11 @@
---
# Handler for the webtier: handlers are called by other plays.
# See http://docs.ansible.com/playbooks_intro.html for more information about handlers.
- name: Restart unbound
ansible.builtin.service:
name: unbound
state: restarted
become: true
- name: Restart firewalld
ansible.builtin.service:
name: firewalld
state: restarted
become: true

View file

@ -3,14 +3,31 @@
ansible.builtin.dnf:
name:
- unbound
- certbot
state: present
- name: Enable service Unbound and ensure it is not masked
- name: Create systemd override directory for unbound
ansible.builtin.file:
path: /etc/systemd/system/unbound.service.d
state: directory
owner: root
group: root
mode: '0755'
- name: Fix unbound sd_notify failure (disable notify type)
ansible.builtin.copy:
dest: /etc/systemd/system/unbound.service.d/override.conf
content: |
[Service]
Type=simple
owner: root
group: root
mode: '0644'
register: unbound_override
- name: Reload systemd after override change
ansible.builtin.systemd:
name: unbound
enabled: true
masked: false
daemon_reload: true
when: unbound_override.changed
- name: Create Unbound config
ansible.builtin.template:
@ -20,25 +37,18 @@
notify:
- Restart unbound
- name: Enable and start Unbound
ansible.builtin.systemd:
name: unbound
state: started
enabled: true
- name: Permit traffic in default zone for dns service
ansible.posix.firewalld:
zone: public
service: dns
permanent: true
state: enabled
- name: Permit traffic in default zone for http service
ansible.posix.firewalld:
zone: public
service: http
permanent: true
state: enabled
- name: Permit traffic in default zone for https service
ansible.posix.firewalld:
zone: public
service: https
permanent: true
state: enabled
when: ansible_facts.services['firewalld.service'] is defined
notify:
- Restart firewalld

View file

@ -0,0 +1,8 @@
---
uisp_user: unms
uisp_user_uid: 1001
uisp_group: unms
uisp_group_gid: 1001
uisp_home: /home/unms
uisp_app_dir: /home/unms/app
uisp_data_dir: /home/unms/data

View file

@ -0,0 +1,14 @@
---
- name: Reload systemd
ansible.builtin.systemd:
daemon_reload: true
- name: Restart containerd
ansible.builtin.service:
name: containerd
state: restarted
- name: Restart docker
ansible.builtin.service:
name: docker
state: restarted

View file

@ -0,0 +1,103 @@
---
- name: Install Docker prerequisites
ansible.builtin.apt:
name:
- apt-transport-https
- ca-certificates
- curl
- gnupg
state: present
update_cache: yes
- name: Add Docker GPG key
ansible.builtin.get_url:
url: https://download.docker.com/linux/debian/gpg
dest: /etc/apt/keyrings/docker.asc
mode: '0644'
- name: Add Docker APT repository
ansible.builtin.apt_repository:
repo: "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian {{ ansible_distribution_release }} stable"
filename: docker
state: present
- name: Install Docker CE
ansible.builtin.apt:
name:
- docker-ce
- docker-ce-cli
- containerd.io
- docker-buildx-plugin
- docker-compose-plugin
- docker-ce-rootless-extras
state: present
update_cache: yes
- name: Create UISP group
ansible.builtin.group:
name: "{{ uisp_group }}"
gid: "{{ uisp_group_gid }}"
state: present
- name: Create UISP user
ansible.builtin.user:
name: "{{ uisp_user }}"
uid: "{{ uisp_user_uid }}"
group: "{{ uisp_group }}"
groups: docker
append: yes
home: "{{ uisp_home }}"
shell: /bin/sh
state: present
- name: Ensure Docker is running
ansible.builtin.systemd:
name: docker
state: started
enabled: yes
- name: Create containerd killmode override directory
ansible.builtin.file:
path: /usr/lib/systemd/system/containerd.service.d
state: directory
owner: root
group: root
mode: '0755'
- name: Configure containerd KillMode for UISP
ansible.builtin.copy:
dest: /usr/lib/systemd/system/containerd.service.d/unms-killmode.conf
content: |
[Service]
KillMode=mixed
owner: root
group: root
mode: '0644'
notify:
- Reload systemd
- Restart containerd
- name: Ensure UISP app directory exists
ansible.builtin.file:
path: "{{ uisp_app_dir }}"
state: directory
owner: "{{ uisp_user }}"
group: root
mode: '0700'
- name: Ensure UISP data directory exists
ansible.builtin.file:
path: "{{ uisp_data_dir }}"
state: directory
owner: "{{ uisp_user }}"
group: root
mode: '0700'
- name: Deploy UISP auto-update cron job
ansible.builtin.copy:
dest: /etc/cron.d/unms-update
content: |
* * * * * {{ uisp_user }} {{ uisp_app_dir }}/update.sh --cron > /dev/null 2>&1 || true
owner: root
group: root
mode: '0644'

View file

@ -333,7 +333,7 @@ resource "cloudflare_zone_settings_override" "towerops_net" {
always_use_https = "on"
tls_1_3 = "zrt"
zero_rtt = "on"
ssl = "strict"
ssl = "full"
security_header {
enabled = true
@ -397,6 +397,20 @@ resource "cloudflare_ruleset" "towerops_firewall" {
kind = "zone"
phase = "http_request_firewall_custom"
# Allow agent WebSocket connections (must come before block rules)
rules {
action = "skip"
action_parameters {
ruleset = "current"
}
expression = "(http.request.uri.path eq \"/socket/agent/websocket\")"
description = "allow agent websocket connections"
enabled = true
logging {
enabled = false
}
}
rules {
action = "block"
expression = local.firewall_expression

File diff suppressed because one or more lines are too long