Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • infrastruktur/ansible-warpzone
  • specki/ansible-warpzone
2 results
Show changes
Showing
with 1075 additions and 10 deletions
version: '2.4'
services:
app:
image: traefik:v2.6.1
image: traefik:v3.0
restart: always
ports:
- "80:80"
......@@ -16,9 +14,9 @@ services:
- "/srv/traefik/dynamic:/etc/traefik/dynamic:ro"
- "/srv/traefik/acme.json:/acme.json"
- "/var/run/docker.sock:/var/run/docker.sock"
{% if certFile is defined %}
- "{{ basedir }}/{{ certFile }}:/{{ certFile }}:ro"
- "{{ basedir }}/{{ keyFile }}:/{{ keyFile }}:ro"
{% if selfSignedCN is defined %}
- "{{ basedir }}/cert.pem:/cert.pem:ro"
- "{{ basedir }}/cert.key:/cert.key:ro"
{% endif %}
networks:
- default
......
......@@ -2,19 +2,18 @@
# TLS Options
tls:
{% if certFile is defined %}
{% if selfSignedCN is defined %}
# use local certificate
certificates:
- certFile: "/{{ certFile }}"
keyFile: "/{{ keyFile }}"
- certFile: "/cert.pem"
keyFile: "/cert.key"
{% endif %}
options:
default:
sniStrict: true
preferServerCipherSuites: true
minVersion: "VersionTLS12"
curvePreferences:
- "secp521r1"
......
......@@ -73,6 +73,8 @@ log:
format: "common"
{% if selfSignedCN is not defined %}
# get certificates from letsEncrypt
certificatesResolvers:
letsencrypt:
......@@ -80,3 +82,5 @@ certificatesResolvers:
email: "{{ letsencrypt_notification_email }}"
storage: "/acme.json"
tlsChallenge: true
{% endif %}
---
- include_tasks: ../functions/get_secret.yml
with_items:
- { path: "{{ basedir }}/matrix_notification_access_token", length: -1 }
- name: "create folder struct for {{ servicename }}"
file:
path: "{{ item }}"
state: "directory"
with_items:
- "{{ basedir }}"
- name: Konfig-Dateien erstellen (base,graphite)
template:
src: "{{ item }}"
dest: "{{ basedir }}/{{ item }}"
with_items:
- docker-compose.yml
register: config_files
- name: "stop {{ servicename }} docker"
community.docker.docker_compose_v2:
project_src: "{{ basedir }}"
state: absent
when: config_files.changed
- name: "start {{ servicename }} docker"
community.docker.docker_compose_v2:
project_src: "{{ basedir }}"
state: present
services:
app:
image: containrrr/watchtower:latest
restart: always
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
TZ: "Europe/Berlin"
#WATCHTOWER_RUN_ONCE: "true"
WATCHTOWER_SCHEDULE: "0 0 5 * * *"
WATCHTOWER_CLEANUP: "true"
WATCHTOWER_NOTIFICATION_REPORT: "true"
WATCHTOWER_NOTIFICATION_URL: >
matrix://:{{ matrix_notification_access_token }}@{{ matrix.domain }}/?rooms={{ matrix.notifications_room_id }}
WATCHTOWER_NOTIFICATION_TEMPLATE: | {% raw %}
{{- if .Report -}}
{{- with .Report -}}
{{- if ( or .Updated .Failed ) -}}
{% endraw %}Watchtower @ {{ inventory_hostname }} {%raw %} {{"\n"}}
{{len .Scanned}} Scanned, {{len .Updated}} Updated, {{len .Failed}} Failed
{{- range .Updated}}
- {{.Name}} ({{.ImageName}}): {{.CurrentImageID.ShortID}} updated to {{.LatestImageID.ShortID}}
{{- end -}}
{{- range .Skipped}}
- {{.Name}} ({{.ImageName}}): {{.State}}: {{.Error}}
{{- end -}}
{{- range .Failed}}
- {{.Name}} ({{.ImageName}}): {{.State}}: {{.Error}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- end -}}
{% endraw %}
---
# Die Wireguard Keys müssen vorher erstellt werden
# wg genkey | tee privatekey | wg pubkey > publickey
- include_tasks: ../functions/get_secret.yml
with_items:
- { path: /etc/wireguard/privatekey, length: -1 }
- name: "Install Wireguard Packages"
apt:
state: present
name:
- iptables
- wireguard
- wireguard-tools
- name: "Create folders"
file:
path: "{{ item }}"
state: directory
owner: root
group: root
with_items:
- "/etc/wireguard/"
- name: "Enable IPv4 forwarding"
ansible.posix.sysctl:
name: net.ipv4.ip_forward
value: '1'
sysctl_set: true
state: present
reload: true
- name: "Create config files for wg0"
template:
src: "{{ inventory_hostname }}.conf"
dest: "/etc/wireguard/wg0.conf"
# more info: https://www.ivpn.net/knowledgebase/linux/linux-autostart-wireguard-in-systemd/
- name: "Enable systemd service for wg0"
ansible.builtin.systemd:
name: "wg-quick@wg0"
enabled: true
masked: no
- name: "Reload systemd service"
ansible.builtin.systemd:
daemon_reload: true
- name: "Stop systemd service for wg0"
ansible.builtin.systemd:
name: "wg-quick@wg0"
state: stopped
- name: "Start systemd service for wg0"
ansible.builtin.systemd:
name: "wg-quick@wg0"
state: started
- name: "Cron Job to hold tunnel open"
ansible.builtin.cron:
name: "wireguard-keepalive"
minute: "*/5"
job: "ping -c 1 -q 10.43.1.1 > /dev/null"
when: inventory_hostname == 'carrot'
[Interface]
PrivateKey = {{ privatekey }}
Address = 10.43.1.2
ListenPort = 51821
PostUp = iptables -t nat -I POSTROUTING -s 10.43.1.1 -o eth0 -j MASQUERADE
# PostUp = ip6tables -t nat -I POSTROUTING -o eth0 -j MASQUERADE
PreDown = iptables -t nat -D POSTROUTING -s 10.43.1.1 -o eth0 -j MASQUERADE
# PreDown = ip6tables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
[Peer]
PublicKey = Ir90KFkQqGIedB7ST7zIRGQyd7Ip11fn2rnuIHdF3m0=
Endpoint = {{ hostvars['webserver'].ext_ip4 }}:51821
AllowedIPs = 10.43.1.1, 10.42.1.1, 10.44.0.0/24
[Interface]
PrivateKey = {{ privatekey }}
Address = 10.43.1.1
ListenPort = 51821
[Peer]
PublicKey = 9FLaGBXWjInPv4PFRuAJPPrPWruzocVrXg9lsmwGdX4=
AllowedIPs = 10.43.1.2, 192.168.0.0/24, 10.0.0.0/22
---
- include_tasks: ../functions/get_secret.yml
with_items:
- { path: "/etc/kea/kea_api_password", length: 22 }
- { path: "/etc/kea/kea_ddns_key", length: 44 }
- name: "Instaliere debian Pakete"
apt:
update_cache: yes
state: present
name:
- bind9
- name: "Set owner for bind config directory"
file:
path: "/etc/bind"
state: directory
owner: "bind"
group: "bind"
- name: "Copy Service Config Files"
template:
src: "{{ item }}"
dest: "/etc/bind/{{ item }}"
with_items:
- db.0.0.10.rev
- db.1.0.10.rev
- db.2.0.10.rev
- db.3.0.10.rev
- db.warpzone.lan
- named.conf.local
- named.conf.options
- name: "Purge DDNS Updates"
ansible.builtin.file:
path: /etc/bind/{{ item }}
state: absent
with_items:
- db.0.0.10.rev.jnl
- db.1.0.10.rev.jnl
- db.2.0.10.rev.jnl
- db.3.0.10.rev.jnl
- db.warpzone.lan.jnl
- name: "Enable and restart named.service"
systemd:
name: "named.service"
state: restarted
enabled: True
- name: "Get all active leases from Kea"
uri:
url: "http://127.0.0.1:8000"
method: POST
user: "kea-api"
password: "{{ kea_api_password }}"
body_format: json
body:
command: "lease4-get-all"
service: ["dhcp4"]
headers:
Content-Type: "application/json"
register: all_leases
- name: "Display number of leases found"
debug:
msg: "Found {{ all_leases.json[0].arguments.leases | length }} active leases"
when:
- all_leases.json[0].arguments.leases is defined
- name: "Force DDNS update for each lease"
uri:
url: "http://127.0.0.1:8000"
method: POST
user: "kea-api"
password: "{{ kea_api_password }}"
body_format: json
body:
command: "lease4-resend-ddns"
service: ["dhcp4"]
arguments:
ip-address: "{{ item['ip-address'] }}"
headers:
Content-Type: "application/json"
loop: "{{ all_leases.json[0].arguments.leases }}"
register: ddns_results
when:
- all_leases.json[0].arguments.leases is defined
- item.hostname is defined
- item.hostname != ""
- item['ip-address'] is defined
- item['ip-address'] != ""
$TTL 604800
@ IN SOA ns.warpzone.lan. webmaster.warpzone.lan. (
2 ; Serial
604800 ; Refresh
86400 ; Retry
2419200 ; Expire
604800 ) ; Negative Cache TTL
;
@ IN NS ns.warpzone.lan.
; PTR records for 10.0.0.x will be dynamically updated by Kea
$TTL 604800
@ IN SOA ns.warpzone.lan. webmaster.warpzone.lan. (
2 ; Serial
604800 ; Refresh
86400 ; Retry
2419200 ; Expire
604800 ) ; Negative Cache TTL
;
@ IN NS ns.warpzone.lan.
; PTR records for 10.0.1.x will be dynamically updated by Kea
$TTL 604800
@ IN SOA ns.warpzone.lan. webmaster.warpzone.lan. (
2 ; Serial
604800 ; Refresh
86400 ; Retry
2419200 ; Expire
604800 ) ; Negative Cache TTL
;
@ IN NS ns.warpzone.lan.
; PTR records for 10.0.2.x will be dynamically updated by Kea
$TTL 604800
@ IN SOA ns.warpzone.lan. webmaster.warpzone.lan. (
2 ; Serial
604800 ; Refresh
86400 ; Retry
2419200 ; Expire
604800 ) ; Negative Cache TTL
;
@ IN NS ns.warpzone.lan.
; PTR records for 10.0.3.x will be dynamically updated by Kea
; zonefile for warpzone.lan.1200
; TTL min. 300
$TTL 300
@ IN SOA ns.warpzone.lan. webmaster.warpzone.ms. (
{{ ansible_date_time.epoch }} ; Serial
1200 ; Refresh 1200 - 43200
120 ; Retry 120 -7200
604800 ; Expire 604800 - 2419200
3600 ) ; Negative Cache TTL 3600 - 86400
@ IN A {{ int_ip4 }}
@ IN AAAA {{ ext_ip6 }}
; Nameserver
@ IN NS ns.warpzone.lan.
ns IN A {{ int_ip4 }}
; Manual Static entries
{% for host in groups['all'] %}
{% if hostvars[host].int_ip4 is defined %}
{% if hostvars[host].int_ip4.startswith('10.0.0') or hostvars[host].int_ip4.startswith('192.168.0') %}
; Internal Host {{ host }}
{{ host }} IN A {{ hostvars[host].int_ip4 }}
{% if hostvars[host].webserver_domains is defined %}
{% for domain in hostvars[host].webserver_domains %}
{% if domain.endswith('warpzone.lan') %}
{{ domain | replace('.warpzone.lan','') }} IN A {{ hostvars[host].int_ip4 }}
{% endif %}
{% endfor %}
{% endif %}
{% endif %}
{% endif %}
{% endfor %}
//
// This file contains the own local DNS server configuration.
// This is where you declare the zones associated with this server's domain(s).
// Consider adding the 1918 zones here, if they are not used in your organization
// include "/etc/bind/zones.rfc1918";
//
// ----------------------- Zones -----------------------
// Forward-Lookup
zone "warpzone.lan" {
type master;
file "/etc/bind/db.warpzone.lan";
check-names ignore;
allow-query { any; };
allow-update { key "kea-ddns-key"; };
};
// Reverse-Lookup für 10.0.0.0/22 (jeweils /24-Zonen)
zone "0.0.10.in-addr.arpa" {
type master;
file "/etc/bind/db.0.0.10.rev";
check-names ignore;
allow-query { any; };
allow-update { key "kea-ddns-key"; };
};
zone "1.0.10.in-addr.arpa" {
type master;
file "/etc/bind/db.1.0.10.rev";
check-names ignore;
allow-query { any; };
allow-update { key "kea-ddns-key"; };
};
zone "2.0.10.in-addr.arpa" {
type master;
file "/etc/bind/db.2.0.10.rev";
check-names ignore;
allow-query { any; };
allow-update { key "kea-ddns-key"; };
};
zone "3.0.10.in-addr.arpa" {
type master;
file "/etc/bind/db.3.0.10.rev";
check-names ignore;
allow-query { any; };
allow-update { key "kea-ddns-key"; };
};
// ----------------------- Zones -----------------------
key "kea-ddns-key" {
algorithm hmac-sha256;
secret "{{ kea_ddns_key }}";
};
// This file contains all the configuration options for the DNS server.
// If there is a firewall between you and nameservers you want
// to talk to, you may need to fix the firewall to allow multiple
// ports to talk. See http://www.kb.cert.org/vuls/id/800113
// If your ISP provided one or more IP addresses for stable
// nameservers, you probably want to use them as forwarders.
// Uncomment the following block, and insert the addresses replacing
// the all-0's placeholder.
//========================================================================
// If BIND logs error messages about the root key being expired,
// you will need to update your keys. See https://www.isc.org/bind-keys
//========================================================================
//************************************************************************************
// ACL's define a address_match_list e.g. IP address(es), which can then be referenced
// (used) in a number of statements and the view clause(s). acl's MUST be defined before
// they are referenced in any statement or clause. For this reason they are usually defined
// first in the named.conf file. 'acl-name' is an arbitrary (but unique) quoted string defining
// the specific list. The 'acl-name' is the method used to subsequently reference the particular
// list. Any number of acl's may be defined.
/*
* Deny transfers by default except for the listed hosts.
* If we have other name servers, place them here.
*/
acl "acl_trusted_transfer" {
none;
};
//********************************************************************************
options {
/*
* Is a quoted string defining the absolute path for the server e.g. "/var/named".
* All subsequent relative paths use this base directory. If no directory options
* is specified the directory from which BIND was loaded is used.
*/
directory "/var/cache/bind";
/*
* Is a quoted string and allows you to define where the pid (Process Identifier)
* used by BIND is written. If not present it is distribution or OS specific
* typically /var/run/named.pid or /etc/named.pid. It may be defined using an
* absolute path or relative to the directory parameter.
*/
pid-file "/var/run/named/named.pid";
/*
* Specifies the string that will be returned to a version.bind query when using
* the chaos class only. version_string is a quoted string, for example, "get lost"
* or something equally to the point. We tend to use it in all named.conf files to
* avoid giving out a version number such that an attacker can exploit known
* version-specific weaknesses.
*/
version "not currently available";
/*
* Turns on BIND to listen for IPv6 queries. If this statement is not present and the
* server supports IPv6 (only or in dual stack mode) the server will listen for IPv6 on
* port 53 on all server interfaces. If the OS supports RFC 3493 and RFC 3542 compliant
* IPv6 sockets and the address_match_list uses the special any name then a single listen
* is issued to the wildcard address. If the OS does not support this feature a socket is
* opened for every required address and port. The port default is 53.
* Multiple listen-on-v6 statements are allowed.
*/
listen-on-v6 { any; };
/* Defines the port and IP address(es) on which BIND will listen for incoming queries.
* The default is port 53 on all server interfaces.
* Multiple listen-on statements are allowed.
*/
listen-on { any; };
/* Notify behaviour is applicable to both master zones (with 'type master;')
* and slave zones (with 'type slave;') and if set to 'yes' (the default) then,
* when a zone is loaded or changed, for example, after a zone transfer, NOTIFY
* messages are sent to the name servers defined in the NS records for the zone
* (except itself and the 'Primary Master' name server defined in the SOA record)
* and to any IPs listed in any also-notify statement.
* If set to 'no' NOTIFY messages are not sent.
* If set to 'explicit' NOTIFY is only sent to those IP(s) listed in an also-notify statement.
*/
notify no;
/*
* Defines an match list of IP address(es) which are allowed
* to issue queries to the server.
* Only trusted addresses are allowed to perform queries.
* We will allow anyone to query our master zones below.
* This prevent becoming a public free DNS server.
*/
allow-query {
any;
};
/*
* Defines an match list of IP address(es) which are allowed to
* issue queries that access the local query cache.
* Only trusted addresses are allowed to use query cache.
*/
allow-query-cache {
any;
};
/*
* Defines a match list of IP address(es) which are allowed to
* issue recursive queries to the server.
* Only trusted addresses are allowed to use recursion.
*/
allow-recursion {
any;
};
/*
* Dfines a match list e.g. IP address(es) that are allowed to transfer
* the zone information from the server (master or slave for the zone).
* The default behaviour is to allow zone transfers to any host.
*/
allow-transfer {
none;
};
/*
* Defines an match list of host IP address(es) that are allowed
* to submit dynamic updates for master zones, and thus this
* statement enables Dynamic DNS.
*/
allow-update {
none;
};
/*
* Indicates that a resolver (a caching or caching-only name server) will attempt
* to validate replies from DNSSEC enabled (signed) zones. To perform this task
* the server alos needs either a valid trusted-keys clause (containing one or more
* trusted-anchors or a managed-keys clause.
* Since 9.5 the default value is dnssec-validation yes;
*/
dnssec-validation auto;
/*
* If auth-nxdomain is 'yes' allows the server to answer authoritatively
* (the AA bit is set) when returning NXDOMAIN (domain does not exist) answers,
* if 'no' (the default) the server will not answer authoritatively.
*/
auth-nxdomain no; # conform to RFC1035
/*
* By default empty-zones-enable is set to 'yes' which means that
* reverse queries for IPv4 and IPv6 addresses covered by RFCs 1918,
* 4193, 5737 and 6598 (as well as IPv6 local address (locally assigned),
* IPv6 link local addresses, the IPv6 loopback address and the IPv6 unknown address)
* but which is not not covered by a locally defined zone clause will automatically
* return an NXDOMAIN response from the local name server. This prevents reverse map queries
* to such addresses escaping to the DNS hierarchy where they are simply noise and increase
* the already high level of query pollution caused by mis-configuration.
*/
empty-zones-enable yes;
/*
* If recursion is set to 'yes' (the default) the server will always provide
* recursive query behaviour if requested by the client (resolver).
* If set to 'no' the server will only provide iterative query behaviour -
* normally resulting in a referral. If the answer to the query already
* exists in the cache it will be returned irrespective of the value of this statement.
* This statement essentially controls caching behaviour in the server.
*/
recursion yes;
/*
* additional-from-auth and additional-from-cache control the behaviour when
* zones have additional (out-of-zone) data or when following CNAME or DNAME records.
* These options are for used for configuring authoritative-only (non-caching) servers
* and are only effective if recursion no is specified in a global options clause or
* in a view clause. The default in both cases is yes.
*/
//additional-from-auth no;
//additional-from-cache no;
/*
* Defines a list of IP address(es) and optional port numbers
* to which queries will be forwarded.
*/
forwarders {
// Router DNS
// 10.0.0.1;
// Cloudflare DNS
1.1.1.1;
// Google Public DNS
8.8.8.8;
8.8.4.4;
// OpenDNS
208.67.222.222;
208.67.220.220;
};
};
apispec==6.8.2
blinker==1.9.0
certifi==2025.6.15
charset-normalizer==3.4.2
click==8.2.1
Flask==3.1.1
flask-apispec==0.11.4
flask-swagger-ui==5.21.0
idna==3.10
itsdangerous==2.2.0
Jinja2==3.1.6
MarkupSafe==3.0.2
marshmallow==3.26.1
packaging==25.0
requests==2.32.4
urllib3==2.5.0
webargs==8.7.0
Werkzeug==3.1.3
---
- include_tasks: ../functions/get_secret.yml
with_items:
- { path: "/etc/kea/kea_api_password", length: 22 }
- name: "Instaliere debian Pakete"
apt:
update_cache: yes
state: present
name:
- python3
- python3-pip
- python3-virtualenv
- name: "Create dhcpinfo directories"
file:
path: "{{ item }}"
state: directory
with_items:
- "{{ basedir }}"
- "{{ basedir }}/templates"
- "{{ basedir }}/venv"
- name: "copy dhcpinfo files"
template:
src: "{{ item }}"
dest: "{{ basedir }}/{{ item }}"
with_items:
- app.py
- requirements.txt
- templates/leases.html
- name: Installiere Python-Pakete in venv
pip:
requirements: "{{ basedir }}/requirements.txt"
virtualenv: "{{ basedir }}/venv"
- name: "Create Systemd Unit File"
template:
src: "{{ item }}"
dest: "/etc/systemd/system/{{ item }}"
with_items:
- dhcpinfo.service
- name: "Enable and Restart Systemd Service"
systemd:
name: "dhcpinfo.service"
state: restarted
enabled: True
daemon_reload: yes
from flask import Flask, render_template, jsonify, Response, url_for
import requests
import json
from datetime import datetime
import logging
import os
from requests.auth import HTTPBasicAuth
import sys
from marshmallow import Schema, fields
from flask_apispec import FlaskApiSpec, marshal_with, doc
from apispec import APISpec
from apispec.ext.marshmallow import MarshmallowPlugin
from flask_swagger_ui import get_swaggerui_blueprint
# Flask App Setup
app = Flask(__name__)
logging.basicConfig(
level=logging.DEBUG,
stream=sys.stdout,
format='%(asctime)s %(levelname)s %(message)s'
)
# Kea Configuration
KEA_CONTROL_AGENT_URL = "http://127.0.0.1:{{ kea_ctrl_agent_port }}"
KEA_SERVICE = "dhcp4" # oder "dhcp6" für IPv6
KEA_USERNAME = "{{ kea_ctrl_agent_user }}"
KEA_PASSWORD = "{{ kea_api_password }}"
# Web Server Configuration
WEB_HOST = "0.0.0.0"
WEB_PORT = {{ dhcpinfo_web_port }}
WEB_DEBUG = False
# Auto-Refresh Intervall in Sekunden (0 = kein Auto-Refresh)
AUTO_REFRESH = 30
# Marshmallow-Schemas (nur einmal definiert, werden überall verwendet)
class LeaseSchema(Schema):
ip_address = fields.Str(data_key='ip-address')
hw_address = fields.Str(data_key='hw-address')
hostname = fields.Str()
state = fields.Int()
cltt = fields.Int()
valid_lft = fields.Int()
subnet_id = fields.Int(data_key='subnet-id')
class StatusSchema(Schema):
connected = fields.Bool()
message = fields.Str()
url = fields.Str()
service = fields.Str()
username = fields.Str()
timestamp = fields.Str()
class KeaDHCPClient:
def __init__(self, url, service, username=None, password=None):
self.url = url
self.service = service
self.username = username
self.password = password
self.auth = HTTPBasicAuth(username, password) if username and password else None
def send_command(self, command, arguments=None):
"""Sendet einen Befehl an den Kea Control Agent mit Basic Auth"""
payload = {
"command": command,
"service": [self.service]
}
if arguments:
payload["arguments"] = arguments
headers = {'Content-Type': 'application/json'}
try:
# Request mit Basic Authentication
response = requests.post(
self.url,
json=payload,
headers=headers,
auth=self.auth,
timeout=10,
verify=True # SSL-Zertifikat verifizieren bei HTTPS
)
# HTTP-Status prüfen
response.raise_for_status()
# JSON-Response parsen
json_response = response.json()
# Kea-spezifische Fehler prüfen
if isinstance(json_response, list) and len(json_response) > 0:
result_code = json_response[0].get("result", -1)
if result_code != 0:
error_text = json_response[0].get("text", "Unbekannter Kea-Fehler")
logging.error(f"Kea API Fehler (Code {result_code}): {error_text}")
return json_response
except requests.exceptions.HTTPError as e:
if e.response.status_code == 401:
logging.error("Authentifizierung fehlgeschlagen - Benutzername/Passwort prüfen")
elif e.response.status_code == 403:
logging.error("Zugriff verweigert - Benutzer hat keine Berechtigung")
else:
logging.error(f"HTTP-Fehler bei Kea API Request: {e}")
return None
except requests.exceptions.ConnectionError as e:
logging.error(f"Verbindungsfehler zu Kea Control Agent: {e}")
return None
except requests.exceptions.Timeout as e:
logging.error(f"Timeout bei Kea API Request: {e}")
return None
except requests.exceptions.RequestException as e:
logging.error(f"Allgemeiner Fehler bei Kea API Request: {e}")
return None
except json.JSONDecodeError as e:
logging.error(f"Fehler beim Parsen der JSON-Antwort: {e}")
return None
def test_connection(self):
"""Testet die Verbindung und Authentifizierung"""
try:
response = self.send_command("list-commands")
if response and len(response) > 0 and response[0].get("result") == 0:
return True, "Verbindung erfolgreich"
else:
return False, "Kea API Fehler"
except Exception as e:
return False, f"Verbindungstest fehlgeschlagen: {str(e)}"
def get_all_leases(self):
"""Ruft alle DHCP-Leases ab"""
command = "lease4-get-all" if self.service == "dhcp4" else "lease6-get-all"
return self.send_command(command)
def get_server_config(self):
"""Ruft die Server-Konfiguration ab"""
return self.send_command("config-get")
def get_statistics(self):
"""Ruft Server-Statistiken ab"""
return self.send_command("statistic-get-all")
def get_server_info(self):
"""Ruft Server-Informationen ab"""
return self.send_command("build-report")
def format_timestamp(timestamp):
"""Formatiert Unix-Timestamp zu lesbarem Datum"""
if timestamp and timestamp > 0:
return datetime.fromtimestamp(timestamp).strftime('%d.%m.%Y %H:%M:%S')
return "N/A"
def format_lease_state(state):
"""Formatiert Lease-Status"""
states = {
0: "Verfügbar",
1: "Vergeben",
2: "Abgelaufen",
3: "Zurückgegeben"
}
return states.get(state, f"Unbekannt ({state})")
@app.route('/')
def index():
"""Hauptseite mit Lease-Übersicht"""
kea_client = KeaDHCPClient(KEA_CONTROL_AGENT_URL, KEA_SERVICE, KEA_USERNAME, KEA_PASSWORD)
# Verbindung testen
connection_ok, connection_message = kea_client.test_connection()
if not connection_ok:
return render_template('leases.html',
leases=[],
error=f"Verbindungsfehler: {connection_message}",
connection_status="Fehler",
last_update=datetime.now().strftime('%d.%m.%Y %H:%M:%S'))
# Leases abrufen
lease_response = kea_client.get_all_leases()
leases = []
error_message = None
if lease_response and lease_response[0].get("result") == 0:
# Erfolgreich - Leases verarbeiten
lease_data = lease_response[0].get("arguments", {})
leases = lease_data.get("leases", [])
# Leases für die Anzeige formatieren
for lease in leases:
lease['valid_lft_formatted'] = format_timestamp(lease.get('cltt', 0) + lease.get('valid_lft', 0))
lease['cltt_formatted'] = format_timestamp(lease.get('cltt', 0))
lease['state_formatted'] = format_lease_state(lease.get('state', 0))
else:
error_message = "Fehler beim Abrufen der Leases von Kea DHCP Server"
if lease_response:
error_message += f": {lease_response[0].get('text', 'Unbekannter Fehler')}"
# Statistiken abrufen (optional)
stats_response = kea_client.get_statistics()
logging.debug(f"Raw statistics response: {stats_response}")
statistics = {}
if stats_response and stats_response[0].get("result") == 0:
stats_data = stats_response[0].get("arguments", {})
logging.debug(f"Parsed statistics data: {stats_data}")
for stat_name, stat_values in stats_data.items():
if isinstance(stat_values, list) and len(stat_values) > 0:
statistics[stat_name] = stat_values[0][0] # Extract the actual statistic value
else:
logging.warning(f"Unexpected format for statistic {stat_name}: {stat_values}")
# Server-Info abrufen
server_info = {}
info_response = kea_client.get_server_info()
if info_response and info_response[0].get("result") == 0:
server_info = info_response[0].get("arguments", {})
return render_template('leases.html',
leases=leases,
error=error_message,
statistics=statistics,
server_info=server_info,
total_leases=len(leases),
connection_status="Verbunden",
auto_refresh=AUTO_REFRESH,
last_update=datetime.now().strftime('%d.%m.%Y %H:%M:%S'))
@doc(description='Liste aller DHCP-Leases', tags=['leases'])
@marshal_with(LeaseSchema(many=True))
@app.route('/api/leases')
def api_leases():
"""JSON API für Leases"""
kea_client = KeaDHCPClient(KEA_CONTROL_AGENT_URL, KEA_SERVICE, KEA_USERNAME, KEA_PASSWORD)
lease_response = kea_client.get_all_leases()
if lease_response and lease_response[0].get("result") == 0:
lease_data = lease_response[0].get("arguments", {})
return lease_data.get("leases", [])
else:
return {"error": "Fehler beim Abrufen der Leases"}, 500
@doc(description='Status und Verbindung zur Kea API', tags=['status'])
@marshal_with(StatusSchema)
@app.route('/api/status')
def api_status():
"""API-Endpunkt für Verbindungsstatus"""
kea_client = KeaDHCPClient(KEA_CONTROL_AGENT_URL, KEA_SERVICE, KEA_USERNAME, KEA_PASSWORD)
connection_ok, message = kea_client.test_connection()
return {
"connected": connection_ok,
"message": message,
"url": KEA_CONTROL_AGENT_URL,
"service": KEA_SERVICE,
"username": KEA_USERNAME,
"timestamp": datetime.now().isoformat()
}
@app.route('/metrics')
def metrics():
"""Prometheus Metrics Endpoint für Kea Statistiken"""
kea_client = KeaDHCPClient(KEA_CONTROL_AGENT_URL, KEA_SERVICE, KEA_USERNAME, KEA_PASSWORD)
stats_response = kea_client.get_statistics()
output = []
connection_ok, _ = kea_client.test_connection()
output.append(f"kea_connection_ok {1 if connection_ok else 0}")
# Lease-Anzahl ermitteln
lease_response = kea_client.get_all_leases()
total_leases = 0
if lease_response and lease_response[0].get("result") == 0:
lease_data = lease_response[0].get("arguments", {})
total_leases = len(lease_data.get("leases", []))
output.append(f"kea_total_leases {total_leases}")
if stats_response and stats_response[0].get("result") == 0:
stats_data = stats_response[0].get("arguments", {})
for stat_name, stat_values in stats_data.items():
if isinstance(stat_values, list) and len(stat_values) > 0:
value = stat_values[0][0]
prom_name = stat_name.lower().replace('.', '_').replace('-', '_').replace('[', '_').replace(']', '')
output.append(f"kea_{prom_name} {value}")
return '\n'.join(output) + '\n', 200, {'Content-Type': 'text/plain; version=0.0.4; charset=utf-8'}
# OpenAPI/Swagger-Konfiguration
app.config.update({
'APISPEC_SPEC': APISpec(
title='Warpzone DHCP API',
version='1.0.0',
openapi_version='3.0.2',
plugins=[MarshmallowPlugin()],
),
'APISPEC_SWAGGER_URL': '/swagger.yaml',
})
docs = FlaskApiSpec(app)
docs.register(api_leases)
docs.register(api_status)
swaggerui_blueprint = get_swaggerui_blueprint(
'/swagger',
'/swagger.yaml',
config={'app_name': "Warpzone DHCP API"}
)
app.register_blueprint(swaggerui_blueprint, url_prefix='/swagger')
@app.route('/rss')
def rss_feed():
"""RSS Feed für aktuelle DHCP-Leases"""
kea_client = KeaDHCPClient(KEA_CONTROL_AGENT_URL, KEA_SERVICE, KEA_USERNAME, KEA_PASSWORD)
lease_response = kea_client.get_all_leases()
leases = []
if lease_response and lease_response[0].get("result") == 0:
lease_data = lease_response[0].get("arguments", {})
leases = lease_data.get("leases", [])
now = datetime.now().strftime('%a, %d %b %Y %H:%M:%S +0000')
rss_items = []
for lease in leases:
hostname = lease.get('hostname', 'N/A')
ip = lease.get('ip-address', 'N/A')
mac = lease.get('hw-address', 'N/A')
start = format_timestamp(lease.get('cltt', 0))
end = format_timestamp(lease.get('cltt', 0) + lease.get('valid_lft', 0))
rss_items.append(f"""
<item>
<title>DHCP Lease: {hostname} ({ip})</title>
<description>MAC: {mac}, Lease Start: {start}, Lease Ende: {end}</description>
<pubDate>{start}</pubDate>
<guid>{ip}-{mac}</guid>
</item>
""")
rss = f"""<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
<channel>
<title>Warpzone DHCP Leases</title>
<link>{url_for('index', _external=True)}</link>
<description>Aktuelle DHCP-Leases</description>
<lastBuildDate>{now}</lastBuildDate>
{''.join(rss_items)}
</channel>
</rss>"""
return Response(rss, mimetype='application/rss+xml')
if __name__ == '__main__':
# Konfiguration ausgeben (ohne Passwort)
print(f"🚀 Starte Kea DHCP Web Interface")
print(f"🐞 Debug-Modus: {WEB_DEBUG}")
print(f"📡 Kea Control Agent URL: {KEA_CONTROL_AGENT_URL}")
print(f"🔧 Kea Service: {KEA_SERVICE}")
print(f"👤 Benutzername: {KEA_USERNAME}")
print(f"🔒 Passwort: {'*' * len(KEA_PASSWORD) if KEA_PASSWORD else 'NICHT GESETZT'}")
print(f"🔄 Auto-Refresh: {AUTO_REFRESH} Sekunden")
print(f"🌐 Web Interface: http://{WEB_HOST}:{WEB_PORT}")
print(f"📄 Swagger UI: http://{WEB_HOST}:{WEB_PORT}/swagger")
app.run(debug=WEB_DEBUG, host=WEB_HOST, port=WEB_PORT)
[Unit]
Description=DHCP Info Web Interface
After=network.target network-online.target
Wants=network-online.target
Requires=isc-kea-ctrl-agent.service isc-kea-dhcp4-server.service
After=isc-kea-ctrl-agent.service isc-kea-dhcp4-server.service
[Service]
Type=simple
User=root
Group=root
WorkingDirectory=/srv/dhcpinfo
ExecStart={{ basedir }}/venv/bin/python /srv/dhcpinfo/app.py
ExecReload=/bin/kill -HUP $MAINPID
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
SyslogIdentifier=dhcpinfo
[Install]
WantedBy=multi-user.target