From 83f8a1b694c2c7d4d876f3b0fb2602ca58a77d68 Mon Sep 17 00:00:00 2001 From: Julian Rother Date: Thu, 31 Jul 2025 03:29:10 +0200 Subject: [PATCH] Initial commit --- defaults/main.yml | 5 ++ files/prometheus-ruckus-exporter | 73 ++++++++++++++++++++++++ files/prometheus-ruckus-exporter.service | 33 +++++++++++ handlers/main.yml | 6 ++ tasks/main.yml | 36 ++++++++++++ templates/prometheus-ruckus-exporter.j2 | 3 + 6 files changed, 156 insertions(+) create mode 100644 defaults/main.yml create mode 100755 files/prometheus-ruckus-exporter create mode 100644 files/prometheus-ruckus-exporter.service create mode 100644 handlers/main.yml create mode 100644 tasks/main.yml create mode 100644 templates/prometheus-ruckus-exporter.j2 diff --git a/defaults/main.yml b/defaults/main.yml new file mode 100644 index 0000000..2e621a1 --- /dev/null +++ b/defaults/main.yml @@ -0,0 +1,5 @@ +prometheus_ruckus_exporter_config: + RUCKUS_EXPORTER_PORT: "9076" + #RUCKUS_EXPORTER_HOSTS: 192.168.0.23 192.168.0.42 + #RUCKUS_EXPORTER_USER: admin + #RUCKUS_EXPORTER_PASSWORD: secret diff --git a/files/prometheus-ruckus-exporter b/files/prometheus-ruckus-exporter new file mode 100755 index 0000000..241e98a --- /dev/null +++ b/files/prometheus-ruckus-exporter @@ -0,0 +1,73 @@ +#!/usr/bin/python3 +import time +import os +import asyncio +import sys + +sys.path.append('/usr/local/lib/python-aioruckus') + +from prometheus_client import start_http_server, PROCESS_COLLECTOR, PLATFORM_COLLECTOR, GC_COLLECTOR +from prometheus_client.core import GaugeMetricFamily, CounterMetricFamily, StateSetMetricFamily, InfoMetricFamily, REGISTRY +from prometheus_client.registry import Collector + +from aioruckus import AjaxSession + + +async def collect_async(host, user, password): + async with AjaxSession.async_create(host, user, password) as session: + metrics = [] + def reg(metric): + metrics.append(metric) + return metric + + base_label_names = ('source',) + base_label_values = (host,) + client_label_names = base_label_names + ('ap_mac', 'ap_name', 'bssid', 'channel', 'essid', 'ip', 'mac', 'name', 'vlan', 'radio_type') + ruckus_client_radio_signal_db = reg(GaugeMetricFamily('ruckus_client_radio_signal_db', 'Client radio signal in dB', labels=client_label_names)) + ruckus_client_noise_db = reg(GaugeMetricFamily('ruckus_client_noise_db', 'Client noise floor in dB', labels=client_label_names)) + ruckus_client_receive_bytes_total = reg(CounterMetricFamily('ruckus_client_receive_bytes_total', 'Client total received bytes', labels=client_label_names)) + ruckus_client_receive_packets_total = reg(CounterMetricFamily('ruckus_client_receive_packets_total', 'Client total received packets', labels=client_label_names)) + ruckus_client_transmit_bytes_total = reg(CounterMetricFamily('ruckus_client_transmit_bytes_total', 'Client total transmitted bytes', labels=client_label_names)) + ruckus_client_transmit_packets_total = reg(CounterMetricFamily('ruckus_client_transmit_packets_total', 'Client total transmitted packets', labels=client_label_names)) + + device_info_label_names = base_label_names + ('name', 'mac', 'model', 'serial', 'ip', 'version') + ruckus_device_info = reg(InfoMetricFamily('ruckus_device', 'Device info', labels=device_info_label_names)) + device_label_names = base_label_names + ('name', 'mac') + ruckus_device_uptime_seconds = reg(CounterMetricFamily('ruckus_device_uptime_seconds', 'Device uptime in seconds', labels=device_label_names)) + + clients = await session.api.get_active_clients(interval_stats=True) + for client in clients: + client_label_values = base_label_values + (client['ap'], client['ap-name'], client['vap-mac'], client['channel'], client['ssid'], client.get('ip', ''), client['mac'], client.get('hostname', ''), client.get('vlan', ''), client['radio-type']) + ruckus_client_radio_signal_db.add_metric(client_label_values, float(client['received-signal-strength'])) + ruckus_client_noise_db.add_metric(client_label_values, float(client['noise-floor'])) + ruckus_client_receive_bytes_total.add_metric(client_label_values, float(client['total-rx-bytes'])) + ruckus_client_receive_packets_total.add_metric(client_label_values, float(client['total-rx-pkts'])) + ruckus_client_transmit_bytes_total.add_metric(client_label_values, float(client['total-tx-bytes'])) + ruckus_client_transmit_packets_total.add_metric(client_label_values, float(client['total-tx-pkts'])) + + devices = await session.api.get_ap_stats() + for device in devices: + device_info_label_values = base_label_values + (device['ap-name'], device['mac'], device['model'], device['serial-number'], device['ip'], device['firmware-version']) + ruckus_device_info.add_metric(device_info_label_values, {}) + device_label_values = base_label_values + (device['ap-name'], device['mac']) + ruckus_device_uptime_seconds.add_metric(device_label_values, float(device['uptime'])) + + return metrics + + +class CustomCollector(Collector): + def collect(self): + for host in os.environ['RUCKUS_EXPORTER_HOSTS'].split(' '): + yield from asyncio.run(collect_async(host, os.environ['RUCKUS_EXPORTER_USER'], os.environ['RUCKUS_EXPORTER_PASSWORD'])) + + +REGISTRY.unregister(PROCESS_COLLECTOR) +REGISTRY.unregister(PLATFORM_COLLECTOR) +REGISTRY.unregister(GC_COLLECTOR) +REGISTRY.register(CustomCollector()) + + +if __name__ == '__main__': + start_http_server(addr='127.0.0.1', port=int(os.environ['RUCKUS_EXPORTER_PORT'])) + while True: + time.sleep(100) diff --git a/files/prometheus-ruckus-exporter.service b/files/prometheus-ruckus-exporter.service new file mode 100644 index 0000000..b9cacbf --- /dev/null +++ b/files/prometheus-ruckus-exporter.service @@ -0,0 +1,33 @@ +[Unit] +Description=Prometheus exporter for Ruckus Unleashed Wifi metrics +Requires=network-online.target +After=network-online.target + +[Service] +DynamicUser=yes +User=prometheus-ruckus-exporter +Restart=on-failure +EnvironmentFile=-/etc/default/prometheus-ruckus-exporter +ExecStart=/usr/local/sbin/prometheus-ruckus-exporter + +LockPersonality=yes +MemoryDenyWriteExecute=yes +NoNewPrivileges=true +PrivateDevices=yes +PrivateTmp=disconnected +PrivateUsers=yes +ProcSubset=pid +ProtectClock=yes +ProtectControlGroups=strict +ProtectHome=yes +ProtectHostname=yes +ProtectKernelLogs=yes +ProtectKernelModules=yes +ProtectKernelTunables=yes +ProtectProc=invisible +ProtectSystem=strict +RestrictNamespaces=yes +RestrictRealtime=yes + +[Install] +WantedBy=multi-user.target diff --git a/handlers/main.yml b/handlers/main.yml new file mode 100644 index 0000000..64e626a --- /dev/null +++ b/handlers/main.yml @@ -0,0 +1,6 @@ +- name: Restart prometheus-ruckus-exporter + ansible.builtin.systemd_service: + name: prometheus-ruckus-exporter + daemon_reload: true + state: restarted + enabled: yes diff --git a/tasks/main.yml b/tasks/main.yml new file mode 100644 index 0000000..0467742 --- /dev/null +++ b/tasks/main.yml @@ -0,0 +1,36 @@ +- name: Install dependencies + ansible.builtin.apt: + pkg: + - python3-aiohttp + - python3-xmltodict + - python3-cryptography + +- name: Clone aioruckus library + ansible.builtin.git: + dest: /usr/local/lib/python-aioruckus + repo: https://github.com/ms264556/aioruckus.git + accept_hostkey: true + update: true + version: a639df7585f29aaddc24ebffb9fa0c497d0a55d3 + +- name: Update config + ansible.builtin.template: + src: prometheus-ruckus-exporter.j2 + dest: /etc/default/prometheus-ruckus-exporter + mode: "0640" + notify: + - Restart prometheus-ruckus-exporter + +- name: Copy ruckus-exporter script + ansible.builtin.copy: + src: prometheus-ruckus-exporter + dest: /usr/local/sbin/prometheus-ruckus-exporter + mode: "0755" + notify: Restart prometheus-ruckus-exporter + +- name: Create ruckus-exporter systemd unit + ansible.builtin.copy: + src: prometheus-ruckus-exporter.service + dest: /etc/systemd/system/prometheus-ruckus-exporter.service + notify: Restart prometheus-ruckus-exporter + diff --git a/templates/prometheus-ruckus-exporter.j2 b/templates/prometheus-ruckus-exporter.j2 new file mode 100644 index 0000000..51514e5 --- /dev/null +++ b/templates/prometheus-ruckus-exporter.j2 @@ -0,0 +1,3 @@ +{% for key, value in prometheus_ruckus_exporter_config.items() %} +{{ key }}="{{ value }}" +{% endfor %}