@cryptotaxi247 / netdata-1 / commits / 6940b8859

isc_dhcpd: ipv6 support and optimization

lgz committed Dec 25, 2017 at 21:16 UTC 6940b88594e4e64186511671d63c86c39bb41d4b
1 file changed +138 -110
python.d/isc_dhcpd.chart.py
+138 -110
@@ -2,18 +2,19 @@
2 # Description: isc dhcpd lease netdata python.d module
3 # Author: l2isbad
4
5 -from time import mktime, strptime, gmtime, time
6 -from os import stat, access, R_OK
7 -from os.path import isfile
8 -try:
9 - from ipaddress import ip_network, ip_address
10 - HAVE_IPADDRESS = True
11 -except ImportError:
12 - HAVE_IPADDRESS = False
5 +import os
6 +import re
7 +import time
8 +
9 +
10 try:
14 - from itertools import filterfalse
11 + import ipaddress
12 + HAVE_IP_ADDRESS = True
13 except ImportError:
16 - from itertools import ifilterfalse as filterfalse
14 + HAVE_IP_ADDRESS = False
15 +
16 +from collections import defaultdict
17 +from copy import deepcopy
18
19 from bases.FrameworkServices.SimpleService import SimpleService
20
@@ -21,146 +22,173 @@ priority = 60000
22 retries = 60
23 update_every = 5
24
24 -ORDER = ['pools_utilization', 'pools_active_leases', 'leases_total', 'parse_time', 'leases_size']
25 +ORDER = ['pools_utilization', 'pools_active_leases', 'leases_total']
26
27 CHARTS = {
28 'pools_utilization': {
28 - 'options': [None, 'Pools Utilization', 'used in percent', 'utilization',
29 + 'options': [None, 'Pools Utilization', '%', 'utilization',
30 'isc_dhcpd.utilization', 'line'],
31 'lines': []},
32 'pools_active_leases': {
32 - 'options': [None, 'Active Leases', 'leases per pool', 'active leases',
33 + 'options': [None, 'Active Leases Per Pool', 'leases', 'active leases',
34 'isc_dhcpd.active_leases', 'line'],
35 'lines': []},
36 'leases_total': {
36 - 'options': [None, 'Total All Pools', 'number', 'active leases',
37 + 'options': [None, 'All Active Leases', 'leases', 'active leases',
38 'isc_dhcpd.leases_total', 'line'],
38 - 'lines': [['leases_total', 'leases', 'absolute']]},
39 - 'parse_time': {
40 - 'options': [None, 'Parse Time', 'ms', 'parse stats',
41 - 'isc_dhcpd.parse_time', 'line'],
42 - 'lines': [['parse_time', 'time', 'absolute']]},
43 - 'leases_size': {
44 - 'options': [None, 'Dhcpd Leases File Size', 'kilobytes',
45 - 'parse stats', 'isc_dhcpd.leases_size', 'line'],
46 - 'lines': [['leases_size', 'size', 'absolute', 1, 1024]]}}
39 + 'lines': [['leases_total', 'leases', 'absolute']],
40 + 'variables': [
41 + ['leases_size']
42 + ]
43 + }
44 +}
45 +
46 +
47 +class DhcpdLeasesFile:
48 + def __init__(self, path):
49 + self.path = path
50 + self.mod_time = 0
51 + self.size = 0
52 +
53 + def is_valid(self):
54 + return os.path.isfile(self.path) and os.access(self.path, os.R_OK)
55 +
56 + def is_changed(self):
57 + mod_time = os.path.getmtime(self.path)
58 + if mod_time != self.mod_time:
59 + self.mod_time = mod_time
60 + self.size = int(os.path.getsize(self.path) / 1024)
61 + return True
62 + return False
63 +
64 + def get_data(self):
65 + try:
66 + with open(self.path) as leases:
67 + result = defaultdict(dict)
68 + for row in leases:
69 + row = row.strip()
70 + if row.startswith('lease'):
71 + address = row[6:-2]
72 + elif row.startswith('iaaddr'):
73 + address = row[7:-2]
74 + elif row.startswith('ends'):
75 + result[address]['ends'] = row[5:-1]
76 + elif row.startswith('binding state'):
77 + result[address]['state'] = row[14:-1]
78 + return result
79 + except (OSError, IOError):
80 + return None
81 +
82 +
83 +class Pool:
84 + def __init__(self, name, network):
85 + self.id = re.sub(r'[:/.-]+', '_', name)
86 + self.name = name
87 + self.network = ipaddress.ip_network(address=u'%s' % network)
88 +
89 + def num_hosts(self):
90 + return self.network.num_addresses - 2
91 +
92 + def __contains__(self, item):
93 + return item.address in self.network
94 +
95 +
96 +class Lease:
97 + def __init__(self, address, ends, state):
98 + self.address = ipaddress.ip_address(address=address)
99 + self.ends = ends
100 + self.state = state
101 +
102 + def is_active(self, current_time):
103 + # lease_end_time might be epoch
104 + if self.ends.startswith('epoch'):
105 + epoch = int(self.ends.split()[1].replace(';', ''))
106 + return epoch - current_time > 0
107 + # max. int for lease-time causes lease to expire in year 2038.
108 + # dhcpd puts 'never' in the ends section of active lease
109 + elif self.ends == 'never':
110 + return True
111 + return time.mktime(time.strptime(self.ends, '%w %Y/%m/%d %H:%M:%S')) - current_time > 0
112 +
113 + def is_valid(self):
114 + return self.state == 'active'
115
116
117 class Service(SimpleService):
118 def __init__(self, configuration=None, name=None):
119 SimpleService.__init__(self, configuration=configuration, name=name)
52 - self.leases_path = self.configuration.get('leases_path', '/var/lib/dhcp/dhcpd.leases')
120 self.order = ORDER
54 - self.definitions = CHARTS
55 - self.pools = dict()
121 + self.definitions = deepcopy(CHARTS)
122 +
123 + lease_path = self.configuration.get('leases_path', '/var/lib/dhcp/dhcpd.leases')
124 + self.dhcpd_leases = DhcpdLeasesFile(path=lease_path)
125 + self.pools = list()
126 + self.data = dict()
127
128 # Will work only with 'default' db-time-format (weekday year/month/day hour:minute:second)
129 # TODO: update algorithm to parse correctly 'local' db-time-format
59 - # Also only ipv4 supported
130
131 def check(self):
62 - if not HAVE_IPADDRESS:
63 - self.error('\'python-ipaddress\' module is needed')
132 + if not HAVE_IP_ADDRESS:
133 + self.error("'python-ipaddress' module is needed")
134 return False
65 - if not (isfile(self.leases_path) and access(self.leases_path, R_OK)):
66 - self.error('Make sure leases_path is correct and leases log file is readable by netdata')
135 +
136 + if not self.dhcpd_leases.is_valid():
137 + self.error("Make sure '{path}' is exist and readable by netdata".format(path=self.dhcpd_leases.path))
138 return False
68 - if not self.configuration.get('pools'):
139 +
140 + pools = self.configuration.get('pools')
141 + if not pools:
142 self.error('Pools are not defined')
143 return False
71 - if not isinstance(self.configuration['pools'], dict):
72 - self.error('Invalid \'pools\' format')
73 - return False
144
75 - for pool in self.configuration['pools']:
145 + for pool in pools:
146 try:
77 - net = ip_network(u'%s' % self.configuration['pools'][pool])
78 - self.pools[pool] = dict(net=net, num_hosts=net.num_addresses - 2)
147 + new_pool = Pool(name=pool, network=pools[pool])
148 except ValueError as error:
80 - self.error('%s removed, error: %s' % (self.configuration['pools'][pool], error))
149 + self.error("'{pool}' was removed, error: {error}".format(pool=pools[pool], error=error))
150 + else:
151 + self.pools.append(new_pool)
152
82 - if not self.pools:
83 - return False
153 self.create_charts()
85 - return True
154 + return bool(self.pools)
155
87 - def _get_raw_data(self):
88 - """
89 - Parses log file
90 - :return: tuple(
91 - [ipaddress, lease end time, ...],
92 - time to parse leases file
93 - )
94 - """
95 - try:
96 - with open(self.leases_path) as leases:
97 - time_start = time()
98 - part1 = filterfalse(find_lease, leases)
99 - part2 = filterfalse(find_ends, leases)
100 - result = dict(zip(part1, part2))
101 - time_end = time()
102 - file_parse_time = round((time_end - time_start) * 1000)
103 - return result, file_parse_time
104 - except (OSError, IOError) as error:
105 - self.error("Failed to parse leases file:", str(error))
106 - return None
107 -
108 - def _get_data(self):
156 + def get_data(self):
157 """
158 :return: dict
159 """
112 - raw_data = self._get_raw_data()
113 - if not raw_data:
114 - return None
160 + if not self.dhcpd_leases.is_changed():
161 + return self.data
162
116 - raw_leases, parse_time = raw_data[0], raw_data[1]
163 + raw_leases = self.dhcpd_leases.get_data()
164 + if not raw_leases:
165 + self.data = dict()
166 + return None
167
118 - # Result: {ipaddress: end lease time, ...}
119 - active_leases, to_netdata = list(), dict()
120 - current_time = mktime(gmtime())
168 + active_leases = list()
169 + current_time = time.mktime(time.gmtime())
170
122 - for ip, lease_end_time in raw_leases.items():
123 - # Result: [active binding, active binding....]. (Expire time (ends date;) - current time > 0)
124 - if binding_active(lease_end_time=lease_end_time[7:-2],
125 - current_time=current_time):
126 - active_leases.append(ip_address(u'%s' % ip[6:-3]))
171 + for address in raw_leases:
172 + try:
173 + new_lease = Lease(address, **raw_leases[address])
174 + except ValueError:
175 + continue
176 + else:
177 + if new_lease.is_active(current_time) and new_lease.is_valid():
178 + active_leases.append(new_lease)
179
180 for pool in self.pools:
129 - dim_id = pool.replace('.', '_')
130 - pool_leases_count = len([ip for ip in active_leases if ip in self.pools[pool]['net']])
131 - to_netdata[dim_id + '_active_leases'] = pool_leases_count
132 - to_netdata[dim_id + '_utilization'] = float(pool_leases_count) / self.pools[pool]['num_hosts'] * 10000
181 + count = len([ip for ip in active_leases if ip in pool])
182 + self.data[pool.id + '_active_leases'] = count
183 + self.data[pool.id + '_utilization'] = float(count) / pool.num_hosts() * 10000
184 +
185 + self.data['leases_size'] = self.dhcpd_leases.size
186 + self.data['leases_total'] = len(active_leases)
187
134 - to_netdata['leases_total'] = len(active_leases)
135 - to_netdata['leases_size'] = stat(self.leases_path)[6]
136 - to_netdata['parse_time'] = parse_time
137 - return to_netdata
188 + return self.data
189
190 def create_charts(self):
191 for pool in self.pools:
141 - dim, dim_id = pool, pool.replace('.', '_')
142 - self.definitions['pools_utilization']['lines'].append([dim_id + '_utilization',
143 - dim, 'absolute', 1, 100])
144 - self.definitions['pools_active_leases']['lines'].append([dim_id + '_active_leases',
145 - dim, 'absolute'])
146 -
147 -
148 -def binding_active(lease_end_time, current_time):
149 - # lease_end_time might be epoch
150 - if lease_end_time.startswith('epoch'):
151 - epoch = int(lease_end_time.split()[1].replace(';',''))
152 - return epoch - current_time > 0
153 - # max. int for lease-time causes lease to expire in year 2038.
154 - # dhcpd puts 'never' in the ends section of active lease
155 - elif lease_end_time == 'never':
156 - return True
157 - else:
158 - return mktime(strptime(lease_end_time, '%w %Y/%m/%d %H:%M:%S')) - current_time > 0
159 -
160 -
161 -def find_lease(value):
162 - return value[0:3] != 'lea'
163 -
164 -
165 -def find_ends(value):
166 - return value[2:6] != 'ends'
192 + self.definitions['pools_utilization']['lines'].append([pool.id + '_utilization', pool.name,
193 + 'absolute', 1, 100])
194 + self.definitions['pools_active_leases']['lines'].append([pool.id + '_active_leases', pool.name])