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])