# -*- coding: utf-8 -*- # Description: isc dhcpd lease netdata python.d module # Author: l2isbad from time import mktime, strptime, gmtime, time from os import stat, access, R_OK from os.path import isfile try: from ipaddress import ip_network, ip_address have_ipaddress = True except ImportError: have_ipaddress = False try: from itertools import filterfalse except ImportError: from itertools import ifilterfalse as filterfalse from base import SimpleService priority = 60000 retries = 60 update_every = 5 ORDER = ['pools_utilization', 'pools_active_leases', 'leases_total', 'parse_time', 'leases_size'] CHARTS = { 'pools_utilization': { 'options': [None, 'Pools Utilization', 'used in percent', 'utilization', 'isc_dhcpd.utilization', 'line'], 'lines': []}, 'pools_active_leases': { 'options': [None, 'Active Leases', 'leases per pool', 'active leases', 'isc_dhcpd.active_leases', 'line'], 'lines': []}, 'leases_total': { 'options': [None, 'Total All Pools', 'number', 'active leases', 'isc_dhcpd.leases_total', 'line'], 'lines': [['leases_total', 'leases', 'absolute']]}, 'parse_time': { 'options': [None, 'Parse Time', 'ms', 'parse stats', 'isc_dhcpd.parse_time', 'line'], 'lines': [['parse_time', 'time', 'absolute']]}, 'leases_size': { 'options': [None, 'Dhcpd Leases File Size', 'kilobytes', 'parse stats', 'isc_dhcpd.leases_size', 'line'], 'lines': [['leases_size', 'size', 'absolute', 1, 1024]]}} class Service(SimpleService): def __init__(self, configuration=None, name=None): SimpleService.__init__(self, configuration=configuration, name=name) self.leases_path = self.configuration.get('leases_path', '/var/lib/dhcp/dhcpd.leases') self.order = ORDER self.definitions = CHARTS self.pools = dict() # Will work only with 'default' db-time-format (weekday year/month/day hour:minute:second) # TODO: update algorithm to parse correctly 'local' db-time-format # (epoch ; # :: ) # Also only ipv4 supported def check(self): if not have_ipaddress: self.error('\'python-ipaddress\' module is needed') return False if not (isfile(self.leases_path) and access(self.leases_path, R_OK)): self.error('Make sure leases_path is correct and leases log file is readable by netdata') return False if not self.configuration.get('pools'): self.error('Pools are not defined') return False if not isinstance(self.configuration['pools'], dict): self.error('Invalid \'pools\' format') return False for pool in self.configuration['pools']: try: net = ip_network(u'%s' % self.configuration['pools'][pool]) self.pools[pool] = dict(net=net, num_hosts=net.num_addresses - 2) except ValueError as error: self.error('%s removed, error: %s' % (self.configuration['pools'][pool], error)) if not self.pools: return False self.create_charts() return True def _get_raw_data(self): """ Parses log file :return: tuple( [ipaddress, lease end time, ...], time to parse leases file ) """ try: with open(self.leases_path) as leases: time_start = time() part1 = filterfalse(find_lease, leases) part2 = filterfalse(find_ends, leases) result = dict(zip(part1, part2)) time_end = time() file_parse_time = round((time_end - time_start) * 1000) return result, file_parse_time except (OSError, IOError) as error: self.error("Failed to parse leases file:", str(error)) return None def _get_data(self): """ :return: dict """ raw_data = self._get_raw_data() if not raw_data: return None raw_leases, parse_time = raw_data[0], raw_data[1] # Result: {ipaddress: end lease time, ...} active_leases, to_netdata = list(), dict() current_time = mktime(gmtime()) for ip, lease_end_time in raw_leases.items(): # Result: [active binding, active binding....]. (Expire time (ends date;) - current time > 0) if binding_active(lease_end_time=lease_end_time[7:-2], current_time=current_time): active_leases.append(ip_address(u'%s' % ip[6:-3])) for pool in self.pools: dim_id = pool.replace('.', '_') pool_leases_count = len([ip for ip in active_leases if ip in self.pools[pool]['net']]) to_netdata[dim_id + '_active_leases'] = pool_leases_count to_netdata[dim_id + '_utilization'] = float(pool_leases_count) / self.pools[pool]['num_hosts'] * 10000 to_netdata['leases_total'] = len(active_leases) to_netdata['leases_size'] = stat(self.leases_path)[6] to_netdata['parse_time'] = parse_time return to_netdata def create_charts(self): for pool in self.pools: dim, dim_id = pool, pool.replace('.', '_') self.definitions['pools_utilization']['lines'].append([dim_id + '_utilization', dim, 'absolute', 1, 100]) self.definitions['pools_active_leases']['lines'].append([dim_id + '_active_leases', dim, 'absolute']) def binding_active(lease_end_time, current_time): return mktime(strptime(lease_end_time, '%w %Y/%m/%d %H:%M:%S')) - current_time > 0 def find_lease(value): return value[0:3] != 'lea' def find_ends(value): return value[2:6] != 'ends'