isc_dhcpd.chart.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  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. POOL_CIDR = "CIDR"
  41. POOL_IP_RANGE = "IP_RANGE"
  42. POOL_UNKNOWN = "UNKNOWN"
  43. def detect_ip_type(ip):
  44. ip_type = ip.split("-")
  45. if len(ip_type) == 1:
  46. return POOL_CIDR
  47. elif len(ip_type) == 2:
  48. return POOL_IP_RANGE
  49. else:
  50. return POOL_UNKNOWN
  51. class DhcpdLeasesFile:
  52. def __init__(self, path):
  53. self.path = path
  54. self.mod_time = 0
  55. self.size = 0
  56. def is_valid(self):
  57. return os.path.isfile(self.path) and os.access(self.path, os.R_OK)
  58. def is_changed(self):
  59. mod_time = os.path.getmtime(self.path)
  60. if mod_time != self.mod_time:
  61. self.mod_time = mod_time
  62. self.size = int(os.path.getsize(self.path) / 1024)
  63. return True
  64. return False
  65. def get_data(self):
  66. try:
  67. with open(self.path) as leases:
  68. result = defaultdict(dict)
  69. for row in leases:
  70. row = row.strip()
  71. if row.startswith('lease'):
  72. address = row[6:-2]
  73. elif row.startswith('iaaddr'):
  74. address = row[7:-2]
  75. elif row.startswith('ends'):
  76. result[address]['ends'] = row[5:-1]
  77. elif row.startswith('binding state'):
  78. result[address]['state'] = row[14:-1]
  79. return dict((k, v) for k, v in result.items() if len(v) == 2)
  80. except (OSError, IOError):
  81. return None
  82. class Pool:
  83. def __init__(self, name, network):
  84. self.id = re.sub(r'[:/.-]+', '_', name)
  85. self.name = name
  86. self.networks = list()
  87. for network in network.split(" "):
  88. if not network:
  89. continue
  90. ip_type = detect_ip_type(ip=network)
  91. if ip_type == POOL_CIDR:
  92. self.networks.append(PoolCIDR(network=network))
  93. elif ip_type == POOL_IP_RANGE:
  94. self.networks.append(PoolIPRange(ip_range=network))
  95. else:
  96. raise ValueError('Network ({0}) incorrect syntax, expect CIDR or IPRange format.'.format(network))
  97. def num_hosts(self):
  98. return sum([network.num_hosts() for network in self.networks])
  99. def __contains__(self, item):
  100. for network in self.networks:
  101. if item in network:
  102. return True
  103. return False
  104. class PoolCIDR:
  105. def __init__(self, network):
  106. self.network = ipaddress.ip_network(address=u'%s' % network)
  107. def num_hosts(self):
  108. return self.network.num_addresses - 2
  109. def __contains__(self, item):
  110. return item.address in self.network
  111. class PoolIPRange:
  112. def __init__(self, ip_range):
  113. ip_range = ip_range.split("-")
  114. self.networks = list(self._summarize_address_range(ip_range[0], ip_range[1]))
  115. @staticmethod
  116. def ip_address(ip):
  117. return ipaddress.ip_address(u'%s' % ip)
  118. def _summarize_address_range(self, first, last):
  119. address_first = self.ip_address(first)
  120. address_last = self.ip_address(last)
  121. return ipaddress.summarize_address_range(address_first, address_last)
  122. def num_hosts(self):
  123. return sum([network.num_addresses for network in self.networks])
  124. def __contains__(self, item):
  125. for network in self.networks:
  126. if item.address in network:
  127. return True
  128. return False
  129. class Lease:
  130. def __init__(self, address, ends, state):
  131. self.address = ipaddress.ip_address(address=u'%s' % address)
  132. self.ends = ends
  133. self.state = state
  134. def is_active(self, current_time):
  135. # lease_end_time might be epoch
  136. if self.ends.startswith('epoch'):
  137. epoch = int(self.ends.split()[1].replace(';', ''))
  138. return epoch - current_time > 0
  139. # max. int for lease-time causes lease to expire in year 2038.
  140. # dhcpd puts 'never' in the ends section of active lease
  141. elif self.ends == 'never':
  142. return True
  143. return time.mktime(time.strptime(self.ends, '%w %Y/%m/%d %H:%M:%S')) - current_time > 0
  144. def is_valid(self):
  145. return self.state == 'active'
  146. class Service(SimpleService):
  147. def __init__(self, configuration=None, name=None):
  148. SimpleService.__init__(self, configuration=configuration, name=name)
  149. self.order = ORDER
  150. self.definitions = deepcopy(CHARTS)
  151. lease_path = self.configuration.get('leases_path', '/var/lib/dhcp/dhcpd.leases')
  152. self.dhcpd_leases = DhcpdLeasesFile(path=lease_path)
  153. self.pools = list()
  154. self.data = dict()
  155. # Will work only with 'default' db-time-format (weekday year/month/day hour:minute:second)
  156. # TODO: update algorithm to parse correctly 'local' db-time-format
  157. def check(self):
  158. if not HAVE_IP_ADDRESS:
  159. self.error("'python-ipaddress' package is needed")
  160. return False
  161. if not self.dhcpd_leases.is_valid():
  162. self.error("Make sure '{path}' is exist and readable by netdata".format(path=self.dhcpd_leases.path))
  163. return False
  164. pools = self.configuration.get('pools')
  165. if not pools:
  166. self.error('Pools are not defined')
  167. return False
  168. for pool in pools:
  169. try:
  170. new_pool = Pool(name=pool, network=pools[pool])
  171. except ValueError as error:
  172. self.error("'{pool}' was removed, error: {error}".format(pool=pools[pool], error=error))
  173. else:
  174. self.pools.append(new_pool)
  175. self.create_charts()
  176. return bool(self.pools)
  177. def get_data(self):
  178. """
  179. :return: dict
  180. """
  181. if not self.dhcpd_leases.is_changed():
  182. return self.data
  183. raw_leases = self.dhcpd_leases.get_data()
  184. if not raw_leases:
  185. self.data = dict()
  186. return None
  187. active_leases = list()
  188. current_time = time.mktime(time.gmtime())
  189. for address in raw_leases:
  190. try:
  191. new_lease = Lease(address, **raw_leases[address])
  192. except ValueError:
  193. continue
  194. else:
  195. if new_lease.is_active(current_time) and new_lease.is_valid():
  196. active_leases.append(new_lease)
  197. for pool in self.pools:
  198. count = len([ip for ip in active_leases if ip in pool])
  199. self.data[pool.id + '_active_leases'] = count
  200. self.data[pool.id + '_utilization'] = float(count) / pool.num_hosts() * 10000
  201. self.data['leases_size'] = self.dhcpd_leases.size
  202. self.data['leases_total'] = len(active_leases)
  203. return self.data
  204. def create_charts(self):
  205. for pool in self.pools:
  206. dim = [
  207. pool.id + '_utilization',
  208. pool.name,
  209. 'absolute',
  210. 1,
  211. 100,
  212. ]
  213. self.definitions['pools_utilization']['lines'].append(dim)
  214. dim = [
  215. pool.id + '_active_leases',
  216. pool.name,
  217. ]
  218. self.definitions['pools_active_leases']['lines'].append(dim)