194 lines
5.8 KiB
Python
Executable File
194 lines
5.8 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
"""Munin plugin to monitor Kea DHCP server.
|
|
|
|
=head1 NAME
|
|
|
|
kea - monitor Kea DHCP server
|
|
|
|
=head1 APPLICABLE SYSTEMS
|
|
|
|
Systems with Kea DHCP4 or DHCP6 server running.
|
|
See https://www.isc.org/kea/ for more information.
|
|
|
|
=head1 CONFIGURATION
|
|
|
|
This shows the default configuration of this plugin. You can override
|
|
the control socket URLs. Note that you most probably need to configure
|
|
"user root" to be able to talk to Kea socket.
|
|
|
|
[kea]
|
|
user root
|
|
env.url4 /run/kea/kea4-ctrl-socket
|
|
env.url6 /run/kea/kea6-ctrl-socket
|
|
|
|
If you have Kea Control Agent running you can also use it's HTTP interface.
|
|
|
|
[kea]
|
|
env.url4 http://localhost:8000/
|
|
env.url6 http://localhost:8000/
|
|
|
|
=head1 AUTHOR
|
|
|
|
Kim B. Heino <b@bbbs.net>
|
|
|
|
=head1 LICENSE
|
|
|
|
GPLv2
|
|
|
|
=head1 MAGIC MARKERS
|
|
|
|
#%# family=auto
|
|
#%# capabilities=autoconf
|
|
|
|
=cut
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import socket
|
|
import sys
|
|
import unicodedata
|
|
import requests
|
|
|
|
|
|
def safe_label(name):
|
|
"""Return safe label name."""
|
|
# Convert ä->a as isalpha('ä') is true
|
|
value = unicodedata.normalize('NFKD', name)
|
|
value = value.encode('ASCII', 'ignore').decode('utf-8')
|
|
|
|
# Remove non-alphanumeric chars
|
|
value = ''.join(char.lower() if char.isalnum() else '_' for char in value)
|
|
|
|
# Add leading "_" if it starts with number
|
|
if value[:1].isnumeric():
|
|
return f'_{value}'
|
|
return value
|
|
|
|
|
|
def kea_talk(url, command, ipv):
|
|
"""Send single command to Kea and return json reply."""
|
|
try:
|
|
if url.startswith('http'): # HTTP to Control Agent
|
|
response = requests.post(url, timeout=30, json={
|
|
'command': command,
|
|
'service': [f'dhcp{ipv}'],
|
|
})
|
|
data = response.json()
|
|
else: # Unix socket to daemon
|
|
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
|
|
sock.connect(url)
|
|
sock.send(f'{{"command": "{command}"}}'.encode('utf-8'))
|
|
data = json.loads(sock.recv(65535).decode('utf-8'))
|
|
except (OSError, ValueError, TypeError):
|
|
return {}
|
|
if 'arguments' not in data:
|
|
return {}
|
|
return data
|
|
|
|
|
|
def read_data():
|
|
"""Get config and statistics from Kea."""
|
|
subnets = {}
|
|
stats = {}
|
|
for ipv in (4, 6):
|
|
# Get configuration and statistics
|
|
url = os.getenv(f'url{ipv}', f'/run/kea/kea{ipv}-ctrl-socket')
|
|
conf = kea_talk(url, 'config-get', ipv)
|
|
stats[ipv] = stat = kea_talk(url, 'statistic-get-all', ipv)
|
|
if not conf or not stat:
|
|
continue
|
|
|
|
# Parse subnets
|
|
for subnet in conf['arguments'][f'Dhcp{ipv}'][f'subnet{ipv}']:
|
|
subid = subnet['id']
|
|
key = 'addresses' if ipv == 4 else 'nas'
|
|
used = stat['arguments'][f'subnet[{subid}].assigned-{key}'][0][0]
|
|
subnets[subnet['subnet']] = used
|
|
|
|
return subnets, stats
|
|
|
|
|
|
def config(data):
|
|
"""Print plugin config."""
|
|
subnets, stats = data
|
|
print('multigraph kea_usage')
|
|
print('graph_title Kea subnet usage')
|
|
print('graph_category network')
|
|
print('graph_vlabel leases')
|
|
print('graph_args --lower-limit 0 --base 1000')
|
|
for subnet in subnets:
|
|
label = safe_label(subnet)
|
|
print(f'{label}.label Subnet {subnet}')
|
|
|
|
if stats[4]:
|
|
print('multigraph kea_dhcp4_rate')
|
|
print('graph_title Kea DCHP4 rate')
|
|
print('graph_category network')
|
|
print('graph_vlabel packets / ${graph_period}')
|
|
print('graph_args --lower-limit 0 --base 1000')
|
|
for description in ('Discover received',
|
|
'Offer sent',
|
|
'Request received',
|
|
'ACK sent',
|
|
'NAK sent'):
|
|
label = description.split()[0].lower()
|
|
print(f'{label}.label {description}')
|
|
print(f'{label}.type DERIVE')
|
|
print(f'{label}.min 0')
|
|
|
|
if stats[6]:
|
|
print('multigraph kea_dhcp6_rate')
|
|
print('graph_title Kea DCHP6 rate')
|
|
print('graph_category network')
|
|
print('graph_vlabel packets / ${graph_period}')
|
|
print('graph_args --lower-limit 0 --base 1000')
|
|
for description in ('Solicit received',
|
|
'Renew received',
|
|
'Advertise sent',
|
|
'Request received',
|
|
'Reply sent'):
|
|
label = description.split()[0].lower()
|
|
print(f'{label}.label {description}')
|
|
print(f'{label}.type DERIVE')
|
|
print(f'{label}.min 0')
|
|
|
|
if os.environ.get('MUNIN_CAP_DIRTYCONFIG') == '1':
|
|
fetch(data)
|
|
|
|
|
|
def fetch(data):
|
|
"""Print values."""
|
|
subnets, stats = data
|
|
print('multigraph kea_usage')
|
|
for subnet, used in subnets.items():
|
|
print(f'{safe_label(subnet)}.value {used}')
|
|
|
|
if stats[4]:
|
|
stat = stats[4]['arguments']
|
|
print('multigraph kea_dhcp4_rate')
|
|
print(f'discover.value {stat["pkt4-discover-received"][0][0]}')
|
|
print(f'offer.value {stat["pkt4-offer-sent"][0][0]}')
|
|
print(f'request.value {stat["pkt4-request-received"][0][0]}')
|
|
print(f'ack.value {stat["pkt4-ack-sent"][0][0]}')
|
|
print(f'nak.value {stat["pkt4-nak-sent"][0][0]}')
|
|
|
|
if stats[6]:
|
|
stat = stats[6]['arguments']
|
|
print('multigraph kea_dhcp6_rate')
|
|
print(f'solicit.value {stat["pkt6-solicit-received"][0][0]}')
|
|
print(f'renew.value {stat["pkt6-renew-received"][0][0]}')
|
|
print(f'advertise.value {stat["pkt6-advertise-sent"][0][0]}')
|
|
print(f'request.value {stat["pkt6-request-received"][0][0]}')
|
|
print(f'reply.value {stat["pkt6-reply-sent"][0][0]}')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
if len(sys.argv) > 1 and sys.argv[1] == 'autoconf':
|
|
print('yes' if read_data()[0] else 'no (no Kea statistics)')
|
|
elif len(sys.argv) > 1 and sys.argv[1] == 'config':
|
|
config(read_data())
|
|
else:
|
|
fetch(read_data())
|