isc_dhcpd.chart.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. # -*- coding: utf-8 -*-
  2. # Description: isc dhcpd lease netdata python.d module
  3. # Author: ilyam8
  4. # SPDX-License-Identifier: GPL-3.0-or-later
  5. import os
  6. import re
  7. import time
  8. try:
  9. import ipaddress
  10. HAVE_IP_ADDRESS = True
  11. except ImportError:
  12. HAVE_IP_ADDRESS = False
  13. from collections import defaultdict
  14. from copy import deepcopy
  15. from bases.FrameworkServices.SimpleService import SimpleService
  16. ORDER = [
  17. 'pools_utilization',
  18. 'pools_active_leases',
  19. 'leases_total',
  20. ]
  21. CHARTS = {
  22. 'pools_utilization': {
  23. 'options': [None, 'Pools Utilization', 'percentage', 'utilization', 'isc_dhcpd.utilization', 'line'],
  24. 'lines': []
  25. },
  26. 'pools_active_leases': {
  27. 'options': [None, 'Active Leases Per Pool', 'leases', 'active leases', 'isc_dhcpd.active_leases', 'line'],
  28. 'lines': []
  29. },
  30. 'leases_total': {
  31. 'options': [None, 'All Active Leases', 'leases', 'active leases', 'isc_dhcpd.leases_total', 'line'],
  32. 'lines': [
  33. ['leases_total', 'leases', 'absolute']
  34. ],
  35. 'variables': [
  36. ['leases_size']
  37. ]
  38. }
  39. }
  40. class DhcpdLeasesFile:
  41. def __init__(self, path):
  42. self.path = path
  43. self.mod_time = 0
  44. self.size = 0
  45. def is_valid(self):
  46. return os.path.isfile(self.path) and os.access(self.path, os.R_OK)
  47. def is_changed(self):
  48. mod_time = os.path.getmtime(self.path)
  49. if mod_time != self.mod_time:
  50. self.mod_time = mod_time
  51. self.size = int(os.path.getsize(self.path) / 1024)
  52. return True
  53. return False
  54. def get_data(self):
  55. try:
  56. with open(self.path) as leases:
  57. result = defaultdict(dict)
  58. for row in leases:
  59. row = row.strip()
  60. if row.startswith('lease'):
  61. address = row[6:-2]
  62. elif row.startswith('iaaddr'):
  63. address = row[7:-2]
  64. elif row.startswith('ends'):
  65. result[address]['ends'] = row[5:-1]
  66. elif row.startswith('binding state'):
  67. result[address]['state'] = row[14:-1]
  68. return dict((k, v) for k, v in result.items() if len(v) == 2)
  69. except (OSError, IOError):
  70. return None
  71. class Pool:
  72. def __init__(self, name, network):
  73. self.id = re.sub(r'[:/.-]+', '_', name)
  74. self.name = name
  75. self.network = ipaddress.ip_network(address=u'%s' % network)
  76. def num_hosts(self):
  77. return self.network.num_addresses - 2
  78. def __contains__(self, item):
  79. return item.address in self.network
  80. class Lease:
  81. def __init__(self, address, ends, state):
  82. self.address = ipaddress.ip_address(address=u'%s' % address)
  83. self.ends = ends
  84. self.state = state
  85. def is_active(self, current_time):
  86. # lease_end_time might be epoch
  87. if self.ends.startswith('epoch'):
  88. epoch = int(self.ends.split()[1].replace(';', ''))
  89. return epoch - current_time > 0
  90. # max. int for lease-time causes lease to expire in year 2038.
  91. # dhcpd puts 'never' in the ends section of active lease
  92. elif self.ends == 'never':
  93. return True
  94. return time.mktime(time.strptime(self.ends, '%w %Y/%m/%d %H:%M:%S')) - current_time > 0
  95. def is_valid(self):
  96. return self.state == 'active'
  97. class Service(SimpleService):
  98. def __init__(self, configuration=None, name=None):
  99. SimpleService.__init__(self, configuration=configuration, name=name)
  100. self.order = ORDER
  101. self.definitions = deepcopy(CHARTS)
  102. lease_path = self.configuration.get('leases_path', '/var/lib/dhcp/dhcpd.leases')
  103. self.dhcpd_leases = DhcpdLeasesFile(path=lease_path)
  104. self.pools = list()
  105. self.data = dict()
  106. # Will work only with 'default' db-time-format (weekday year/month/day hour:minute:second)
  107. # TODO: update algorithm to parse correctly 'local' db-time-format
  108. def check(self):
  109. if not HAVE_IP_ADDRESS:
  110. self.error("'python-ipaddress' package is needed")
  111. return False
  112. if not self.dhcpd_leases.is_valid():
  113. self.error("Make sure '{path}' is exist and readable by netdata".format(path=self.dhcpd_leases.path))
  114. return False
  115. pools = self.configuration.get('pools')
  116. if not pools:
  117. self.error('Pools are not defined')
  118. return False
  119. for pool in pools:
  120. try:
  121. new_pool = Pool(name=pool, network=pools[pool])
  122. except ValueError as error:
  123. self.error("'{pool}' was removed, error: {error}".format(pool=pools[pool], error=error))
  124. else:
  125. self.pools.append(new_pool)
  126. self.create_charts()
  127. return bool(self.pools)
  128. def get_data(self):
  129. """
  130. :return: dict
  131. """
  132. if not self.dhcpd_leases.is_changed():
  133. return self.data
  134. raw_leases = self.dhcpd_leases.get_data()
  135. if not raw_leases:
  136. self.data = dict()
  137. return None
  138. active_leases = list()
  139. current_time = time.mktime(time.gmtime())
  140. for address in raw_leases:
  141. try:
  142. new_lease = Lease(address, **raw_leases[address])
  143. except ValueError:
  144. continue
  145. else:
  146. if new_lease.is_active(current_time) and new_lease.is_valid():
  147. active_leases.append(new_lease)
  148. for pool in self.pools:
  149. count = len([ip for ip in active_leases if ip in pool])
  150. self.data[pool.id + '_active_leases'] = count
  151. self.data[pool.id + '_utilization'] = float(count) / pool.num_hosts() * 10000
  152. self.data['leases_size'] = self.dhcpd_leases.size
  153. self.data['leases_total'] = len(active_leases)
  154. return self.data
  155. def create_charts(self):
  156. for pool in self.pools:
  157. dim = [
  158. pool.id + '_utilization',
  159. pool.name,
  160. 'absolute',
  161. 1,
  162. 100,
  163. ]
  164. self.definitions['pools_utilization']['lines'].append(dim)
  165. dim = [
  166. pool.id + '_active_leases',
  167. pool.name,
  168. ]
  169. self.definitions['pools_active_leases']['lines'].append(dim)