feat: Add infrastructure roles for multi-tenant architecture
Add new Ansible roles and configuration for the edge proxy and private network architecture: ## New Roles: - **edge-traefik**: Edge reverse proxy that routes to private clients - Dynamic routing configuration for multiple clients - SSL termination at the edge - Routes traffic to private IPs (10.0.0.x) - **nat-gateway**: NAT/gateway configuration for edge server - IP forwarding and masquerading - Allows private network clients to access internet - iptables rules for Docker integration - **diun**: Docker Image Update Notifier - Monitors containers for available updates - Email notifications via Mailgun - Per-client configuration - **kuma**: Uptime monitoring integration - Registers HTTP monitors for client services - Automated monitor creation via API - Checks Authentik, Nextcloud, Collabora endpoints ## New Playbooks: - **setup-edge.yml**: Configure edge server with proxy and NAT ## Configuration: - **host_vars**: Per-client Ansible configuration (valk, white) - SSH bastion configuration for private IPs - Client-specific secrets file references This enables the scalable multi-tenant architecture where: - Edge server has public IP and routes traffic - Client servers use private IPs only (cost savings) - All traffic flows through edge proxy with SSL termination 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
f40acee0a3
commit
13685eb454
19 changed files with 752 additions and 0 deletions
11
ansible/host_vars/valk.yml
Normal file
11
ansible/host_vars/valk.yml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
# valk server - behind edge proxy (private network only)
|
||||
|
||||
# SSH via edge server as bastion/jump host
|
||||
ansible_host: 10.0.0.41
|
||||
ansible_ssh_common_args: '-o ProxyCommand="ssh -i ../keys/ssh/edge -W %h:%p -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null root@78.47.191.38" -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no'
|
||||
|
||||
# Client identification
|
||||
client_name: valk
|
||||
client_domain: valk.vrije.cloud
|
||||
client_secrets_file: valk.sops.yaml
|
||||
11
ansible/host_vars/white.yml
Normal file
11
ansible/host_vars/white.yml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
# White server - behind edge proxy
|
||||
# Note: Currently has public IP for initial setup
|
||||
|
||||
# SSH directly via public IP (temporary)
|
||||
ansible_host: 159.69.182.238
|
||||
|
||||
# Client identification
|
||||
client_name: white
|
||||
client_domain: white.vrije.cloud
|
||||
client_secrets_file: white.sops.yaml
|
||||
20
ansible/playbooks/setup-edge.yml
Normal file
20
ansible/playbooks/setup-edge.yml
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
# Setup Edge Server
|
||||
# Configures the edge server with Traefik reverse proxy
|
||||
|
||||
- name: Setup edge server
|
||||
hosts: edge
|
||||
become: yes
|
||||
|
||||
roles:
|
||||
- role: common
|
||||
tags: [common, setup]
|
||||
|
||||
- role: docker
|
||||
tags: [docker, setup]
|
||||
|
||||
- role: nat-gateway
|
||||
tags: [nat, gateway]
|
||||
|
||||
- role: edge-traefik
|
||||
tags: [traefik, edge]
|
||||
28
ansible/roles/diun/defaults/main.yml
Normal file
28
ansible/roles/diun/defaults/main.yml
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
# Diun default configuration
|
||||
diun_version: "latest"
|
||||
diun_schedule: "0 6 * * *" # Daily at 6am UTC
|
||||
diun_log_level: "info"
|
||||
diun_watch_workers: 10
|
||||
|
||||
# Notification configuration
|
||||
diun_notif_enabled: true
|
||||
diun_notif_type: "webhook" # Options: webhook, slack, discord, email, gotify
|
||||
diun_webhook_endpoint: "" # Set per environment or via secrets
|
||||
diun_webhook_method: "POST"
|
||||
diun_webhook_headers: {}
|
||||
|
||||
# Optional: Slack notification
|
||||
diun_slack_webhook_url: ""
|
||||
|
||||
# Optional: Email notification (Mailgun)
|
||||
# Note: Uses per-client SMTP credentials from mailgun role
|
||||
diun_email_enabled: true
|
||||
diun_smtp_host: "smtp.eu.mailgun.org"
|
||||
diun_smtp_port: 587
|
||||
diun_smtp_from: "{{ client_name }}@mg.vrije.cloud"
|
||||
diun_smtp_to: "pieter@postxsociety.org"
|
||||
|
||||
# Which containers to watch
|
||||
diun_watch_all: true
|
||||
diun_exclude_containers: []
|
||||
5
ansible/roles/diun/handlers/main.yml
Normal file
5
ansible/roles/diun/handlers/main.yml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
- name: Restart Diun
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: /opt/docker/diun
|
||||
state: restarted
|
||||
57
ansible/roles/diun/tasks/main.yml
Normal file
57
ansible/roles/diun/tasks/main.yml
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
---
|
||||
- name: Set SMTP credentials from mailgun role facts or client_secrets
|
||||
set_fact:
|
||||
diun_smtp_username_final: "{{ mailgun_smtp_user | default(client_secrets.mailgun_smtp_user | default(client_name ~ '@mg.vrije.cloud')) }}"
|
||||
diun_smtp_password_final: "{{ mailgun_smtp_password | default(client_secrets.mailgun_smtp_password | default('')) }}"
|
||||
when: mailgun_smtp_user is defined or client_secrets.mailgun_smtp_user is defined or client_name is defined
|
||||
no_log: true
|
||||
|
||||
- name: Create monitoring Docker network
|
||||
community.docker.docker_network:
|
||||
name: monitoring
|
||||
state: present
|
||||
|
||||
- name: Create Diun directory
|
||||
file:
|
||||
path: /opt/docker/diun
|
||||
state: directory
|
||||
mode: '0755'
|
||||
|
||||
- name: Create Diun data directory
|
||||
file:
|
||||
path: /opt/docker/diun/data
|
||||
state: directory
|
||||
mode: '0755'
|
||||
|
||||
- name: Deploy Diun configuration
|
||||
template:
|
||||
src: diun.yml.j2
|
||||
dest: /opt/docker/diun/diun.yml
|
||||
mode: '0644'
|
||||
notify: Restart Diun
|
||||
|
||||
- name: Deploy Diun docker-compose.yml
|
||||
template:
|
||||
src: docker-compose.yml.j2
|
||||
dest: /opt/docker/diun/docker-compose.yml
|
||||
mode: '0644'
|
||||
notify: Restart Diun
|
||||
|
||||
- name: Start Diun container
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: /opt/docker/diun
|
||||
state: present
|
||||
pull: always
|
||||
register: diun_deploy
|
||||
|
||||
- name: Wait for Diun to be healthy
|
||||
shell: docker inspect --format='{{"{{"}} .State.Status {{"}}"}}' diun
|
||||
register: diun_status
|
||||
until: diun_status.stdout == "running"
|
||||
retries: 5
|
||||
delay: 3
|
||||
changed_when: false
|
||||
|
||||
- name: Display Diun status
|
||||
debug:
|
||||
msg: "Diun is {{ diun_status.stdout }} on {{ inventory_hostname }}"
|
||||
58
ansible/roles/diun/templates/diun.yml.j2
Normal file
58
ansible/roles/diun/templates/diun.yml.j2
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
---
|
||||
# Diun configuration for {{ inventory_hostname }}
|
||||
# Documentation: https://crazymax.dev/diun/
|
||||
|
||||
db:
|
||||
path: /data/diun.db
|
||||
|
||||
watch:
|
||||
workers: {{ diun_watch_workers }}
|
||||
schedule: "{{ diun_schedule }}"
|
||||
firstCheckNotif: false
|
||||
|
||||
defaults:
|
||||
watchRepo: true
|
||||
notifyOn:
|
||||
- new
|
||||
- update
|
||||
|
||||
providers:
|
||||
docker:
|
||||
watchByDefault: {{ diun_watch_all | lower }}
|
||||
{% if diun_exclude_containers | length > 0 %}
|
||||
excludeContainers:
|
||||
{% for container in diun_exclude_containers %}
|
||||
- {{ container }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
notif:
|
||||
{% if diun_notif_enabled and diun_notif_type == 'webhook' and diun_webhook_endpoint %}
|
||||
webhook:
|
||||
endpoint: {{ diun_webhook_endpoint }}
|
||||
method: {{ diun_webhook_method }}
|
||||
timeout: 10s
|
||||
{% if diun_webhook_headers | length > 0 %}
|
||||
headers:
|
||||
{% for key, value in diun_webhook_headers.items() %}
|
||||
{{ key }}: {{ value }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if diun_slack_webhook_url %}
|
||||
slack:
|
||||
webhookURL: {{ diun_slack_webhook_url }}
|
||||
{% endif %}
|
||||
|
||||
{% if diun_email_enabled and diun_smtp_username_final is defined and diun_smtp_password_final is defined and diun_smtp_password_final != '' %}
|
||||
mail:
|
||||
host: {{ diun_smtp_host }}
|
||||
port: {{ diun_smtp_port }}
|
||||
ssl: false
|
||||
insecureSkipVerify: false
|
||||
username: {{ diun_smtp_username_final }}
|
||||
password: {{ diun_smtp_password_final }}
|
||||
from: {{ diun_smtp_from }}
|
||||
to: {{ diun_smtp_to }}
|
||||
{% endif %}
|
||||
24
ansible/roles/diun/templates/docker-compose.yml.j2
Normal file
24
ansible/roles/diun/templates/docker-compose.yml.j2
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
version: '3.8'
|
||||
|
||||
services:
|
||||
diun:
|
||||
image: crazymax/diun:{{ diun_version }}
|
||||
container_name: diun
|
||||
restart: unless-stopped
|
||||
command: serve
|
||||
volumes:
|
||||
- "./data:/data"
|
||||
- "./diun.yml:/diun.yml:ro"
|
||||
- "/var/run/docker.sock:/var/run/docker.sock:ro"
|
||||
environment:
|
||||
- TZ=UTC
|
||||
- LOG_LEVEL={{ diun_log_level }}
|
||||
labels:
|
||||
- "diun.enable=true"
|
||||
networks:
|
||||
- monitoring
|
||||
|
||||
networks:
|
||||
monitoring:
|
||||
name: monitoring
|
||||
external: true
|
||||
13
ansible/roles/edge-traefik/defaults/main.yml
Normal file
13
ansible/roles/edge-traefik/defaults/main.yml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
---
|
||||
# Edge Traefik Default Variables
|
||||
# This Traefik instance acts as a reverse proxy for private network clients
|
||||
|
||||
traefik_version: "v3.3"
|
||||
traefik_network: "web"
|
||||
traefik_docker_socket: "/var/run/docker.sock"
|
||||
traefik_acme_email: "admin@vrije.cloud"
|
||||
traefik_acme_staging: false
|
||||
traefik_dashboard_enabled: false
|
||||
|
||||
# Backend client servers (populated from inventory)
|
||||
backend_clients: []
|
||||
7
ansible/roles/edge-traefik/handlers/main.yml
Normal file
7
ansible/roles/edge-traefik/handlers/main.yml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
# Edge Traefik Handlers
|
||||
|
||||
- name: Restart Traefik
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: /opt/docker/traefik
|
||||
state: restarted
|
||||
60
ansible/roles/edge-traefik/tasks/main.yml
Normal file
60
ansible/roles/edge-traefik/tasks/main.yml
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
---
|
||||
# Edge Traefik Installation Tasks
|
||||
# Sets up Traefik as edge reverse proxy for private network clients
|
||||
|
||||
- name: Ensure Traefik configuration directory exists
|
||||
file:
|
||||
path: /opt/docker/traefik
|
||||
state: directory
|
||||
mode: '0755'
|
||||
tags: [traefik, edge]
|
||||
|
||||
- name: Create Let's Encrypt storage directory
|
||||
file:
|
||||
path: /opt/docker/traefik/letsencrypt
|
||||
state: directory
|
||||
mode: '0600'
|
||||
tags: [traefik, edge]
|
||||
|
||||
- name: Create Traefik log directory
|
||||
file:
|
||||
path: /var/log/traefik
|
||||
state: directory
|
||||
mode: '0755'
|
||||
tags: [traefik, edge]
|
||||
|
||||
- name: Deploy Traefik static configuration
|
||||
template:
|
||||
src: traefik.yml.j2
|
||||
dest: /opt/docker/traefik/traefik.yml
|
||||
mode: '0644'
|
||||
notify: Restart Traefik
|
||||
tags: [traefik, edge, config]
|
||||
|
||||
- name: Deploy Traefik dynamic configuration (routing rules)
|
||||
template:
|
||||
src: dynamic.yml.j2
|
||||
dest: /opt/docker/traefik/dynamic.yml
|
||||
mode: '0644'
|
||||
notify: Restart Traefik
|
||||
tags: [traefik, edge, config]
|
||||
|
||||
- name: Deploy Traefik Docker Compose file
|
||||
template:
|
||||
src: docker-compose.yml.j2
|
||||
dest: /opt/docker/traefik/docker-compose.yml
|
||||
mode: '0644'
|
||||
tags: [traefik, edge]
|
||||
|
||||
- name: Start Traefik container
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: /opt/docker/traefik
|
||||
state: present
|
||||
tags: [traefik, edge]
|
||||
|
||||
- name: Wait for Traefik to be ready
|
||||
wait_for:
|
||||
port: 443
|
||||
delay: 5
|
||||
timeout: 60
|
||||
tags: [traefik, edge]
|
||||
24
ansible/roles/edge-traefik/templates/docker-compose.yml.j2
Normal file
24
ansible/roles/edge-traefik/templates/docker-compose.yml.j2
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# Edge Traefik Docker Compose
|
||||
# Managed by Ansible - do not edit manually
|
||||
|
||||
services:
|
||||
traefik:
|
||||
image: traefik:{{ traefik_version }}
|
||||
container_name: traefik
|
||||
restart: unless-stopped
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
{% if traefik_dashboard_enabled %}
|
||||
- "8080:8080"
|
||||
{% endif %}
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- ./traefik.yml:/etc/traefik/traefik.yml:ro
|
||||
- ./dynamic.yml:/etc/traefik/dynamic.yml:ro
|
||||
- ./letsencrypt:/letsencrypt
|
||||
- /var/log/traefik:/var/log/traefik
|
||||
labels:
|
||||
- "traefik.enable=false"
|
||||
97
ansible/roles/edge-traefik/templates/dynamic.yml.j2
Normal file
97
ansible/roles/edge-traefik/templates/dynamic.yml.j2
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
# Edge Traefik Dynamic Configuration
|
||||
# Managed by Ansible - do not edit manually
|
||||
# Routes traffic to backend servers on private network
|
||||
|
||||
http:
|
||||
# Routers for white client
|
||||
routers:
|
||||
white-auth:
|
||||
rule: "Host(`auth.white.vrije.cloud`)"
|
||||
service: white-auth
|
||||
entryPoints:
|
||||
- websecure
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
|
||||
white-nextcloud:
|
||||
rule: "Host(`nextcloud.white.vrije.cloud`)"
|
||||
service: white-nextcloud
|
||||
entryPoints:
|
||||
- websecure
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
|
||||
white-collabora:
|
||||
rule: "Host(`office.white.vrije.cloud`)"
|
||||
service: white-collabora
|
||||
entryPoints:
|
||||
- websecure
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
|
||||
valk-auth:
|
||||
rule: "Host(`auth.valk.vrije.cloud`)"
|
||||
service: valk-auth
|
||||
entryPoints:
|
||||
- websecure
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
|
||||
valk-nextcloud:
|
||||
rule: "Host(`nextcloud.valk.vrije.cloud`)"
|
||||
service: valk-nextcloud
|
||||
entryPoints:
|
||||
- websecure
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
|
||||
valk-collabora:
|
||||
rule: "Host(`office.valk.vrije.cloud`)"
|
||||
service: valk-collabora
|
||||
entryPoints:
|
||||
- websecure
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
|
||||
# Services (backend servers)
|
||||
services:
|
||||
white-auth:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "https://10.0.0.40:443"
|
||||
serversTransport: insecureTransport
|
||||
|
||||
white-nextcloud:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "https://10.0.0.40:443"
|
||||
serversTransport: insecureTransport
|
||||
|
||||
white-collabora:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "https://10.0.0.40:443"
|
||||
serversTransport: insecureTransport
|
||||
|
||||
valk-auth:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "https://10.0.0.41:443"
|
||||
serversTransport: insecureTransport
|
||||
|
||||
valk-nextcloud:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "https://10.0.0.41:443"
|
||||
serversTransport: insecureTransport
|
||||
|
||||
valk-collabora:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "https://10.0.0.41:443"
|
||||
serversTransport: insecureTransport
|
||||
|
||||
# Server transport (allow self-signed certs from backends)
|
||||
serversTransports:
|
||||
insecureTransport:
|
||||
insecureSkipVerify: true
|
||||
47
ansible/roles/edge-traefik/templates/traefik.yml.j2
Normal file
47
ansible/roles/edge-traefik/templates/traefik.yml.j2
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
# Edge Traefik Static Configuration
|
||||
# Managed by Ansible - do not edit manually
|
||||
# This configuration proxies to backend servers on private network
|
||||
|
||||
api:
|
||||
dashboard: {{ traefik_dashboard_enabled | lower }}
|
||||
{% if traefik_dashboard_enabled %}
|
||||
insecure: false
|
||||
{% endif %}
|
||||
|
||||
entryPoints:
|
||||
web:
|
||||
address: ":80"
|
||||
http:
|
||||
redirections:
|
||||
entryPoint:
|
||||
to: websecure
|
||||
scheme: https
|
||||
|
||||
websecure:
|
||||
address: ":443"
|
||||
http:
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
|
||||
providers:
|
||||
# File provider for static backend configurations
|
||||
file:
|
||||
filename: /etc/traefik/dynamic.yml
|
||||
watch: true
|
||||
|
||||
certificatesResolvers:
|
||||
letsencrypt:
|
||||
acme:
|
||||
email: {{ traefik_acme_email }}
|
||||
storage: /letsencrypt/acme.json
|
||||
{% if traefik_acme_staging %}
|
||||
caServer: https://acme-staging-v02.api.letsencrypt.org/directory
|
||||
{% endif %}
|
||||
httpChallenge:
|
||||
entryPoint: web
|
||||
|
||||
log:
|
||||
level: INFO
|
||||
|
||||
accessLog:
|
||||
filePath: /var/log/traefik/access.log
|
||||
41
ansible/roles/kuma/defaults/main.yml
Normal file
41
ansible/roles/kuma/defaults/main.yml
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
---
|
||||
# Uptime Kuma monitoring registration
|
||||
kuma_enabled: true
|
||||
kuma_url: "https://status.vrije.cloud"
|
||||
|
||||
# Authentication options:
|
||||
# Option 1: Username/Password (required for Socket.io API used by Python library)
|
||||
kuma_username: "" # Set this for automated registration
|
||||
kuma_password: "" # Set this for automated registration
|
||||
|
||||
# Option 2: API Key (only for REST endpoints like /metrics, not for monitor management)
|
||||
kuma_api_key: "uk1_H2YjQsSG8em8GG9G9c0arQogSizXI1CRPNgTEUlU"
|
||||
|
||||
# Monitors to create for each client
|
||||
kuma_monitors:
|
||||
- name: "{{ client_name }} - Authentik SSO"
|
||||
type: "http"
|
||||
url: "https://auth.{{ client_domain }}"
|
||||
method: "GET"
|
||||
interval: 60
|
||||
maxretries: 3
|
||||
retry_interval: 60
|
||||
expected_status: "200,302"
|
||||
|
||||
- name: "{{ client_name }} - Nextcloud"
|
||||
type: "http"
|
||||
url: "https://nextcloud.{{ client_domain }}"
|
||||
method: "GET"
|
||||
interval: 60
|
||||
maxretries: 3
|
||||
retry_interval: 60
|
||||
expected_status: "200,302"
|
||||
|
||||
- name: "{{ client_name }} - Collabora Office"
|
||||
type: "http"
|
||||
url: "https://office.{{ client_domain }}"
|
||||
method: "GET"
|
||||
interval: 60
|
||||
maxretries: 3
|
||||
retry_interval: 60
|
||||
expected_status: "200"
|
||||
49
ansible/roles/kuma/tasks/main.yml
Normal file
49
ansible/roles/kuma/tasks/main.yml
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
---
|
||||
# Register client services with Uptime Kuma monitoring
|
||||
# Uses uptime-kuma-api Python library with Socket.io
|
||||
|
||||
- name: Set Kuma credentials from shared secrets
|
||||
set_fact:
|
||||
kuma_username: "{{ shared_secrets.kuma_username | default('') }}"
|
||||
kuma_password: "{{ shared_secrets.kuma_password | default('') }}"
|
||||
when: shared_secrets is defined
|
||||
|
||||
- name: Check if Kuma monitoring is enabled
|
||||
set_fact:
|
||||
kuma_registration_enabled: "{{ (kuma_enabled | bool) and (kuma_url | length > 0) and (kuma_username | length > 0) and (kuma_password | length > 0) }}"
|
||||
|
||||
- name: Kuma registration block
|
||||
when: kuma_registration_enabled
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
block:
|
||||
- name: Ensure uptime-kuma-api Python package is installed
|
||||
pip:
|
||||
name: uptime-kuma-api
|
||||
state: present
|
||||
|
||||
- name: Create Kuma registration script
|
||||
template:
|
||||
src: register_monitors.py.j2
|
||||
dest: /tmp/kuma_register_{{ client_name }}.py
|
||||
mode: '0700'
|
||||
|
||||
- name: Register monitors with Uptime Kuma
|
||||
command: "{{ ansible_playbook_python }} /tmp/kuma_register_{{ client_name }}.py"
|
||||
register: kuma_result
|
||||
changed_when: "'Added' in kuma_result.stdout or 'Updated' in kuma_result.stdout"
|
||||
failed_when: kuma_result.rc != 0
|
||||
|
||||
- name: Display Kuma registration result
|
||||
debug:
|
||||
msg: "{{ kuma_result.stdout_lines }}"
|
||||
|
||||
- name: Cleanup registration script
|
||||
file:
|
||||
path: /tmp/kuma_register_{{ client_name }}.py
|
||||
state: absent
|
||||
|
||||
- name: Skip Kuma registration message
|
||||
debug:
|
||||
msg: "Kuma monitoring registration skipped (not enabled or missing credentials)"
|
||||
when: not kuma_registration_enabled
|
||||
128
ansible/roles/kuma/templates/register_monitors.py.j2
Normal file
128
ansible/roles/kuma/templates/register_monitors.py.j2
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Uptime Kuma Monitor Registration Script
|
||||
Auto-generated for client: {{ client_name }}
|
||||
"""
|
||||
|
||||
import sys
|
||||
from uptime_kuma_api import UptimeKumaApi, MonitorType
|
||||
|
||||
# Configuration
|
||||
KUMA_URL = "{{ kuma_url }}"
|
||||
KUMA_USERNAME = "{{ kuma_username | default('') }}"
|
||||
KUMA_PASSWORD = "{{ kuma_password | default('') }}"
|
||||
CLIENT_NAME = "{{ client_name }}"
|
||||
CLIENT_DOMAIN = "{{ client_domain }}"
|
||||
|
||||
# Monitor definitions
|
||||
MONITORS = {{ kuma_monitors | to_json }}
|
||||
|
||||
# Monitor type mapping
|
||||
TYPE_MAP = {
|
||||
"http": MonitorType.HTTP,
|
||||
"https": MonitorType.HTTP,
|
||||
"ping": MonitorType.PING,
|
||||
"tcp": MonitorType.PORT,
|
||||
"dns": MonitorType.DNS,
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
"""Register monitors with Uptime Kuma"""
|
||||
|
||||
# Check if credentials are provided
|
||||
if not KUMA_USERNAME or not KUMA_PASSWORD:
|
||||
print("⚠️ Kuma registration skipped: No credentials provided")
|
||||
print("")
|
||||
print("To enable automated monitor registration, add to your secrets:")
|
||||
print(" kuma_username: your_username")
|
||||
print(" kuma_password: your_password")
|
||||
print("")
|
||||
print("Note: API keys (uk1_*) are only for REST endpoints, not monitor management")
|
||||
print("Manual registration required at: https://status.vrije.cloud")
|
||||
sys.exit(0) # Exit with success (not a failure, just skipped)
|
||||
|
||||
try:
|
||||
# Connect to Uptime Kuma (Socket.io connection)
|
||||
print(f"🔌 Connecting to Uptime Kuma at {KUMA_URL}...")
|
||||
api = UptimeKumaApi(KUMA_URL)
|
||||
|
||||
# Login with username/password
|
||||
print(f"🔐 Authenticating as {KUMA_USERNAME}...")
|
||||
api.login(KUMA_USERNAME, KUMA_PASSWORD)
|
||||
|
||||
# Get existing monitors
|
||||
print("📋 Fetching existing monitors...")
|
||||
existing_monitors = api.get_monitors()
|
||||
existing_names = {m['name']: m['id'] for m in existing_monitors}
|
||||
|
||||
# Register each monitor
|
||||
added_count = 0
|
||||
updated_count = 0
|
||||
skipped_count = 0
|
||||
|
||||
for monitor_config in MONITORS:
|
||||
monitor_name = monitor_config['name']
|
||||
monitor_type_str = monitor_config.get('type', 'http').lower()
|
||||
monitor_type = TYPE_MAP.get(monitor_type_str, MonitorType.HTTP)
|
||||
|
||||
# Build monitor parameters
|
||||
params = {
|
||||
'type': monitor_type,
|
||||
'name': monitor_name,
|
||||
'interval': monitor_config.get('interval', 60),
|
||||
'maxretries': monitor_config.get('maxretries', 3),
|
||||
'retryInterval': monitor_config.get('retry_interval', 60),
|
||||
}
|
||||
|
||||
# Add type-specific parameters
|
||||
if monitor_type == MonitorType.HTTP:
|
||||
params['url'] = monitor_config['url']
|
||||
params['method'] = monitor_config.get('method', 'GET')
|
||||
if 'expected_status' in monitor_config:
|
||||
params['accepted_statuscodes'] = monitor_config['expected_status'].split(',')
|
||||
elif monitor_type == MonitorType.PING:
|
||||
params['hostname'] = monitor_config.get('hostname', monitor_config.get('url', ''))
|
||||
|
||||
# Check if monitor already exists
|
||||
if monitor_name in existing_names:
|
||||
print(f"⚠️ Monitor '{monitor_name}' already exists (ID: {existing_monitors[monitor_name]})")
|
||||
print(f" Skipping (update not implemented)")
|
||||
skipped_count += 1
|
||||
else:
|
||||
print(f"➕ Adding monitor: {monitor_name}")
|
||||
try:
|
||||
result = api.add_monitor(**params)
|
||||
print(f" ✓ Added (ID: {result.get('monitorID', 'unknown')})")
|
||||
added_count += 1
|
||||
except Exception as e:
|
||||
print(f" ✗ Failed: {e}")
|
||||
|
||||
# Disconnect
|
||||
api.disconnect()
|
||||
|
||||
# Summary
|
||||
print("")
|
||||
print("=" * 60)
|
||||
print(f"📊 Registration Summary for {CLIENT_NAME}:")
|
||||
print(f" Added: {added_count}")
|
||||
print(f" Skipped (already exist): {skipped_count}")
|
||||
print(f" Total monitors: {len(MONITORS)}")
|
||||
print("=" * 60)
|
||||
|
||||
if added_count > 0:
|
||||
print(f"✅ Successfully registered {added_count} new monitor(s)")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ ERROR: Failed to register monitors: {e}")
|
||||
print("")
|
||||
print("Troubleshooting:")
|
||||
print(f" 1. Verify Kuma is accessible: {KUMA_URL}")
|
||||
print(" 2. Check username/password are correct")
|
||||
print(" 3. Ensure uptime-kuma-api Python package is installed")
|
||||
print(" 4. Check network connectivity from deployment machine")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
6
ansible/roles/nat-gateway/handlers/main.yml
Normal file
6
ansible/roles/nat-gateway/handlers/main.yml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
# NAT Gateway Handlers
|
||||
|
||||
- name: Save iptables rules
|
||||
shell: |
|
||||
iptables-save > /etc/iptables/rules.v4
|
||||
66
ansible/roles/nat-gateway/tasks/main.yml
Normal file
66
ansible/roles/nat-gateway/tasks/main.yml
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
---
|
||||
# NAT Gateway Configuration
|
||||
# Enables internet access for private network clients via edge server
|
||||
|
||||
- name: Enable IP forwarding
|
||||
sysctl:
|
||||
name: net.ipv4.ip_forward
|
||||
value: '1'
|
||||
state: present
|
||||
reload: yes
|
||||
tags: [nat, gateway]
|
||||
|
||||
- name: Install iptables-persistent
|
||||
apt:
|
||||
name: iptables-persistent
|
||||
state: present
|
||||
update_cache: yes
|
||||
tags: [nat, gateway]
|
||||
|
||||
- name: Configure NAT (masquerading) for private network
|
||||
iptables:
|
||||
table: nat
|
||||
chain: POSTROUTING
|
||||
out_interface: eth0
|
||||
source: 10.0.0.0/16
|
||||
jump: MASQUERADE
|
||||
comment: NAT for private network clients
|
||||
notify: Save iptables rules
|
||||
tags: [nat, gateway]
|
||||
|
||||
- name: Allow forwarding from private network (in DOCKER-USER chain)
|
||||
iptables:
|
||||
chain: DOCKER-USER
|
||||
in_interface: enp7s0
|
||||
out_interface: eth0
|
||||
source: 10.0.0.0/16
|
||||
jump: ACCEPT
|
||||
comment: Allow forwarding from private network
|
||||
notify: Save iptables rules
|
||||
tags: [nat, gateway]
|
||||
|
||||
- name: Allow established connections back to private network (in DOCKER-USER chain)
|
||||
iptables:
|
||||
chain: DOCKER-USER
|
||||
in_interface: eth0
|
||||
out_interface: enp7s0
|
||||
ctstate: ESTABLISHED,RELATED
|
||||
jump: ACCEPT
|
||||
comment: Allow established connections to private network
|
||||
notify: Save iptables rules
|
||||
tags: [nat, gateway]
|
||||
|
||||
- name: Return from DOCKER-USER chain for other traffic
|
||||
iptables:
|
||||
chain: DOCKER-USER
|
||||
jump: RETURN
|
||||
comment: Let Docker handle other traffic
|
||||
notify: Save iptables rules
|
||||
tags: [nat, gateway]
|
||||
|
||||
- name: Save iptables rules
|
||||
shell: |
|
||||
iptables-save > /etc/iptables/rules.v4
|
||||
args:
|
||||
creates: /etc/iptables/rules.v4
|
||||
tags: [nat, gateway]
|
||||
Loading…
Add table
Reference in a new issue