isc_dhcpd.chart.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. # -*- coding: utf-8 -*-
  2. # Description: isc dhcpd lease netdata python.d module
  3. # Author: l2isbad
  4. from time import mktime, strptime, gmtime, time
  5. from os import stat, access, R_OK
  6. from os.path import isfile
  7. try:
  8. from ipaddress import ip_network, ip_address
  9. HAVE_IPADDRESS = True
  10. except ImportError:
  11. HAVE_IPADDRESS = False
  12. try:
  13. from itertools import filterfalse
  14. except ImportError:
  15. from itertools import ifilterfalse as filterfalse
  16. from bases.FrameworkServices.SimpleService import SimpleService
  17. priority = 60000
  18. retries = 60
  19. update_every = 5
  20. ORDER = ['pools_utilization', 'pools_active_leases', 'leases_total', 'parse_time', 'leases_size']
  21. CHARTS = {
  22. 'pools_utilization': {
  23. 'options': [None, 'Pools Utilization', 'used in percent', 'utilization',
  24. 'isc_dhcpd.utilization', 'line'],
  25. 'lines': []},
  26. 'pools_active_leases': {
  27. 'options': [None, 'Active Leases', 'leases per pool', 'active leases',
  28. 'isc_dhcpd.active_leases', 'line'],
  29. 'lines': []},
  30. 'leases_total': {
  31. 'options': [None, 'Total All Pools', 'number', 'active leases',
  32. 'isc_dhcpd.leases_total', 'line'],
  33. 'lines': [['leases_total', 'leases', 'absolute']]},
  34. 'parse_time': {
  35. 'options': [None, 'Parse Time', 'ms', 'parse stats',
  36. 'isc_dhcpd.parse_time', 'line'],
  37. 'lines': [['parse_time', 'time', 'absolute']]},
  38. 'leases_size': {
  39. 'options': [None, 'Dhcpd Leases File Size', 'kilobytes',
  40. 'parse stats', 'isc_dhcpd.leases_size', 'line'],
  41. 'lines': [['leases_size', 'size', 'absolute', 1, 1024]]}}
  42. class Service(SimpleService):
  43. def __init__(self, configuration=None, name=None):
  44. SimpleService.__init__(self, configuration=configuration, name=name)
  45. self.leases_path = self.configuration.get('leases_path', '/var/lib/dhcp/dhcpd.leases')
  46. self.order = ORDER
  47. self.definitions = CHARTS
  48. self.pools = dict()
  49. # Will work only with 'default' db-time-format (weekday year/month/day hour:minute:second)
  50. # TODO: update algorithm to parse correctly 'local' db-time-format
  51. # Also only ipv4 supported
  52. def check(self):
  53. if not HAVE_IPADDRESS:
  54. self.error('\'python-ipaddress\' module is needed')
  55. return False
  56. if not (isfile(self.leases_path) and access(self.leases_path, R_OK)):
  57. self.error('Make sure leases_path is correct and leases log file is readable by netdata')
  58. return False
  59. if not self.configuration.get('pools'):
  60. self.error('Pools are not defined')
  61. return False
  62. if not isinstance(self.configuration['pools'], dict):
  63. self.error('Invalid \'pools\' format')
  64. return False
  65. for pool in self.configuration['pools']:
  66. try:
  67. net = ip_network(u'%s' % self.configuration['pools'][pool])
  68. self.pools[pool] = dict(net=net, num_hosts=net.num_addresses - 2)
  69. except ValueError as error:
  70. self.error('%s removed, error: %s' % (self.configuration['pools'][pool], error))
  71. if not self.pools:
  72. return False
  73. self.create_charts()
  74. return True
  75. def _get_raw_data(self):
  76. """
  77. Parses log file
  78. :return: tuple(
  79. [ipaddress, lease end time, ...],
  80. time to parse leases file
  81. )
  82. """
  83. try:
  84. with open(self.leases_path) as leases:
  85. time_start = time()
  86. part1 = filterfalse(find_lease, leases)
  87. part2 = filterfalse(find_ends, leases)
  88. result = dict(zip(part1, part2))
  89. time_end = time()
  90. file_parse_time = round((time_end - time_start) * 1000)
  91. return result, file_parse_time
  92. except (OSError, IOError) as error:
  93. self.error("Failed to parse leases file:", str(error))
  94. return None
  95. def _get_data(self):
  96. """
  97. :return: dict
  98. """
  99. raw_data = self._get_raw_data()
  100. if not raw_data:
  101. return None
  102. raw_leases, parse_time = raw_data[0], raw_data[1]
  103. # Result: {ipaddress: end lease time, ...}
  104. active_leases, to_netdata = list(), dict()
  105. current_time = mktime(gmtime())
  106. for ip, lease_end_time in raw_leases.items():
  107. # Result: [active binding, active binding....]. (Expire time (ends date;) - current time > 0)
  108. if binding_active(lease_end_time=lease_end_time[7:-2],
  109. current_time=current_time):
  110. active_leases.append(ip_address(u'%s' % ip[6:-3]))
  111. for pool in self.pools:
  112. dim_id = pool.replace('.', '_')
  113. pool_leases_count = len([ip for ip in active_leases if ip in self.pools[pool]['net']])
  114. to_netdata[dim_id + '_active_leases'] = pool_leases_count
  115. to_netdata[dim_id + '_utilization'] = float(pool_leases_count) / self.pools[pool]['num_hosts'] * 10000
  116. to_netdata['leases_total'] = len(active_leases)
  117. to_netdata['leases_size'] = stat(self.leases_path)[6]
  118. to_netdata['parse_time'] = parse_time
  119. return to_netdata
  120. def create_charts(self):
  121. for pool in self.pools:
  122. dim, dim_id = pool, pool.replace('.', '_')
  123. self.definitions['pools_utilization']['lines'].append([dim_id + '_utilization',
  124. dim, 'absolute', 1, 100])
  125. self.definitions['pools_active_leases']['lines'].append([dim_id + '_active_leases',
  126. dim, 'absolute'])
  127. def binding_active(lease_end_time, current_time):
  128. # lease_end_time might be epoch
  129. if lease_end_time.startswith('epoch'):
  130. epoch = int(lease_end_time.split()[1].replace(';',''))
  131. return epoch - current_time > 0
  132. # max. int for lease-time causes lease to expire in year 2038.
  133. # dhcpd puts 'never' in the ends section of active lease
  134. elif lease_end_time == 'never':
  135. return True
  136. else:
  137. return mktime(strptime(lease_end_time, '%w %Y/%m/%d %H:%M:%S')) - current_time > 0
  138. def find_lease(value):
  139. return value[0:3] != 'lea'
  140. def find_ends(value):
  141. return value[2:6] != 'ends'