feature: the router keeps its metrics in PostgreSQL, serves them for an OpenTelemetry Collector and pushes them over OTLP.
Release / release (push) Successful in 14s

This commit is contained in:
slawek committed 2026-10-25 14:57:47 +01:00
1 parent 83c1c50d8d
commit 38151bf5be
20 files changed
+1153 -128

No files matched your search

+92 -6
View File
@@ -21,6 +21,7 @@ import json
import os
import pathlib
import re
import secrets
import socket
import subprocess
import sys
@@ -59,6 +60,9 @@ MANAGER_CONFIG = pathlib.Path('/etc/vpn-router-manager/manager.json')
MANAGER_ADMIN = pathlib.Path('/etc/vpn-router-manager/admin.json')
MANAGER_PORT = 7080
#: The password of the metrics service account, which setup makes and keeps (root only).
METRICS_SECRET = pathlib.Path('/etc/vpn-router/metrics.secret')
#: The configuration of Caddy, the HTTPS proxy, which the package's drop-in for caddy.service names;
#: Caddy runs as the user and group CADDY_GROUP, which the caddy package creates.
CADDYFILE = pathlib.Path('/etc/caddy/vpn-router.Caddyfile')
@@ -79,6 +83,7 @@ BLOCKS = (
('# BOOTSTRAP RULES START', '# BOOTSTRAP RULES END'),
('# MANAGER RULES START', '# MANAGER RULES END'),
('# HTTPS RULES START', '# HTTPS RULES END'),
('# METRICS RULES START', '# METRICS RULES END'),
('# ROUTER FORWARD RULES START', '# ROUTER FORWARD RULES END'),
('# ROUTER NAT RULES START', '# ROUTER NAT RULES END'),
('# ROUTER MSS RULES START', '# ROUTER MSS RULES END'),
@@ -223,7 +228,7 @@ def filter_block(ctx):
]
lines += ['# P2S DNS RULES END']
# The web service and the Router Manager choose their interfaces themselves, so their ports are open on both.
# The web service, the Router Manager and the metrics service choose their interfaces themselves, so their ports are open on both.
interfaces = [name for name in (ctx['int_iface'], ctx['wan_iface']) if name]
if ctx['web_enabled'] and interfaces:
lines += [
@@ -250,10 +255,19 @@ def filter_block(ctx):
'# MANAGER RULES END',
]
if vpnrouter.metrics_ready(ctx) and interfaces:
lines += [
'',
'# METRICS RULES START',
*(f'-A ufw-before-input -i {name} -p tcp --dport {ctx["metrics_port"]} -j ACCEPT' for name in interfaces),
'# METRICS RULES END',
]
# Let's Encrypt reaches the HTTP challenge on the external interface; the HTTPS ports are open on both.
if vpnrouter.https_ready(ctx) and interfaces:
ports = [port for wanted, port in ((ctx['web_enabled'], ctx['https_web_port']),
(MANAGER.exists(), ctx['https_manager_port'])) if wanted]
(MANAGER.exists(), ctx['https_manager_port']),
(vpnrouter.metrics_ready(ctx), ctx['metrics_https_port'])) if wanted]
lines += [
'',
'# HTTPS RULES START',
@@ -439,6 +453,57 @@ def manage_admin():
return ''
def manage_metrics_account(ctx):
"""Keep the service account metrics.service_account names, whose password is in METRICS_SECRET; the problem, or ''.
While metrics are enabled, setup owns the account: it adds it as a service account with a random password when
it is missing, and gives it a new random password, ending any lockout, when METRICS_SECRET is missing or does not
hold its password. A disabled account is left as it is. An account of that name that is not a service account is
left alone and is a problem.
"""
if not vpnrouter.metrics_ready(ctx):
return ''
name = ctx['metrics_service_account']
try:
vpnrouter_users.check_name(name)
try:
password = METRICS_SECRET.read_text(encoding='utf-8').strip()
except OSError:
password = ''
with vpnrouter_users.session() as connection:
try:
current = vpnrouter_users.entry(connection, name)
except vpnrouter_users.Failure as failure:
if failure.status != 3:
raise
current = None
if current is not None and not vpnrouter_users.service(current):
return f'metrics.service_account: {name} is a user, not a service account'
# An administrator who disabled the account meant it; its password is left as it is.
if current is not None and not vpnrouter_users.summary(current)['enabled']:
return ''
valid = False
if current is not None and password:
try:
vpnrouter_users.login(name, password, None)
valid = True
except vpnrouter_users.Failure:
pass
if not valid:
password = secrets.token_urlsafe(32)
METRICS_SECRET.touch(mode=0o600)
METRICS_SECRET.chmod(0o600)
METRICS_SECRET.write_text(password + '\n', encoding='utf-8')
if current is None:
vpnrouter_users.add(connection, name, password, {}, service=True)
else:
vpnrouter_users.passwd(connection, name, password)
vpnrouter_users.unlock(connection, name)
except (OSError, ldap.LDAPError, vpnrouter_users.Failure) as exc:
return f'the metrics service account cannot be kept: {exc}'
return ''
def manage_openvpn(ctx, changed):
"""Run the OpenVPN server when it is configured, and stop it when it is not."""
unit = 'openvpn-server@vpn-router'
@@ -455,11 +520,12 @@ def manage_openvpn(ctx, changed):
def manage_caddy(ctx):
"""Run Caddy, the HTTPS proxy of the web service and the Router Manager, while HTTPS is ready, and stop it when not.
"""Run Caddy, the HTTPS proxy of the web service, the Router Manager and the metrics service, while HTTPS is ready, and stop it when not.
CADDYFILE serves the web service on https.web_port and, while the manager is installed, the
manager on https.manager_port, both for wan.local_fqdn with a certificate from Let's Encrypt,
and both calling the service on its loopback address.
CADDYFILE serves the web service on https.web_port, while the manager is installed the
manager on https.manager_port, and while metrics are enabled the metrics service on
metrics.https_port, all for wan.local_fqdn with a certificate from Let's Encrypt, and all
calling the service on its loopback address.
"""
if not vpnrouter.https_ready(ctx):
vpnrouter.remove(CADDYFILE)
@@ -480,6 +546,20 @@ def manage_caddy(ctx):
run(['systemctl', 'reload', CADDY_UNIT], check=False)
def manage_metrics(ctx):
"""Run the metrics service while metrics are enabled, and stop it when not.
The service reads its settings itself, so it is restarted only to run the code of a new package version, which
every run of setup after an upgrade does.
"""
unit = 'vpn-router-metrics.service'
if not vpnrouter.metrics_ready(ctx):
run(['systemctl', 'disable', '--now', unit], check=False)
return
run(['systemctl', 'enable', unit], check=False)
run(['systemctl', 'restart', unit], check=False)
def run_platform_setup(cfg, ctx):
"""Let the platform module apply its own state, if it defines any."""
module = vpnrouter.platform_module(cfg)
@@ -621,6 +701,10 @@ def main():
failed_step = 'Router Manager admin'
progress('running', failed_step)
problem = manage_admin()
if not problem:
failed_step = 'metrics service account'
progress('running', failed_step)
problem = manage_metrics_account(ctx)
progress('running', 'OpenVPN')
manage_openvpn(ctx, changed)
progress('running', 'HTTPS proxy')
@@ -636,6 +720,8 @@ def main():
refresh_docker_rules()
progress('running', 'web service state')
refresh_web(cfg, ctx)
progress('running', 'metrics service')
manage_metrics(ctx)
if problem:
progress('failed', failed_step, problem)
print(f'vpn-router: error: {problem}', file=sys.stderr)