@cryptotaxi247 / netdata-1 / commits / b9e9fdbb3

Add new version from l2isbad

Sven Mäder committed Feb 16, 2018 at 15:39 UTC b9e9fdbb3332d716bb8df30fd30947c804dc56a3
1 file changed +242 -239
python.d/ntp.chart.py
+242 -239
@@ -1,12 +1,11 @@
1 # -*- coding: utf-8 -*-
2 # Description: ntp netdata python.d module
3 # Author: Sven Mäder (rda0)
4 +# Author: Ilya Mashchenko (l2isbad)
5
5 -import socket
6 import struct
7 import re
8
9 -from itertools import cycle
9 from bases.FrameworkServices.SocketService import SocketService
10
11 # default module values
@@ -36,321 +35,324 @@ ORDER = [
35 'sys_rootdisp',
36 'sys_stratum',
37 'sys_tc',
39 - 'sys_precision'
38 + 'sys_precision',
39 + 'peer_offset',
40 + 'peer_delay',
41 + 'peer_dispersion',
42 + 'peer_jitter',
43 + 'peer_xleave',
44 + 'peer_rootdelay',
45 + 'peer_rootdisp',
46 + 'peer_stratum',
47 + 'peer_hmode',
48 + 'peer_pmode',
49 + 'peer_hpoll',
50 + 'peer_ppoll',
51 + 'peer_precision'
52 ]
53
54 CHARTS = {
55 'sys_offset': {
44 - 'options': [None, "Combined offset of server relative to this host", "ms", 'system', 'ntp.sys_offset', 'area'],
56 + 'options': [None, 'Combined offset of server relative to this host', 'ms', 'system', 'ntp.sys_offset', 'area'],
57 'lines': [
58 ['offset', 'offset', 'absolute', 1, PRECISION]
59 ]},
60 'sys_jitter': {
49 - 'options': [None, "Combined system jitter and clock jitter", "ms", 'system', 'ntp.sys_jitter', 'line'],
61 + 'options': [None, 'Combined system jitter and clock jitter', 'ms', 'system', 'ntp.sys_jitter', 'line'],
62 'lines': [
63 ['sys_jitter', 'system', 'absolute', 1, PRECISION],
64 ['clk_jitter', 'clock', 'absolute', 1, PRECISION]
65 ]},
66 'sys_frequency': {
55 - 'options': [None, "Frequency offset relative to hardware clock", "ppm", 'system', 'ntp.sys_frequency', 'area'],
67 + 'options': [None, 'Frequency offset relative to hardware clock', 'ppm', 'system', 'ntp.sys_frequency', 'area'],
68 'lines': [
69 ['frequency', 'frequency', 'absolute', 1, PRECISION]
70 ]},
71 'sys_wander': {
60 - 'options': [None, "Clock frequency wander", "ppm", 'system', 'ntp.sys_wander', 'area'],
72 + 'options': [None, 'Clock frequency wander', 'ppm', 'system', 'ntp.sys_wander', 'area'],
73 'lines': [
74 ['clk_wander', 'clock', 'absolute', 1, PRECISION]
75 ]},
76 'sys_rootdelay': {
65 - 'options': [None, "Total roundtrip delay to the primary reference clock", "ms", 'system', 'ntp.sys_rootdelay', 'area'],
77 + 'options': [None, 'Total roundtrip delay to the primary reference clock', 'ms', 'system',
78 + 'ntp.sys_rootdelay', 'area'],
79 'lines': [
80 ['rootdelay', 'delay', 'absolute', 1, PRECISION]
81 ]},
82 'sys_rootdisp': {
70 - 'options': [None, "Total root dispersion to the primary reference clock", "ms", 'system', 'ntp.sys_rootdisp', 'area'],
83 + 'options': [None, 'Total root dispersion to the primary reference clock', 'ms', 'system',
84 + 'ntp.sys_rootdisp', 'area'],
85 'lines': [
86 ['rootdisp', 'dispersion', 'absolute', 1, PRECISION]
87 ]},
88 'sys_stratum': {
75 - 'options': [None, "Stratum (1-15)", "1", 'system', 'ntp.sys_stratum', 'line'],
89 + 'options': [None, 'Stratum (1-15)', '1', 'system', 'ntp.sys_stratum', 'line'],
90 'lines': [
91 ['stratum', 'stratum', 'absolute', 1, PRECISION]
92 ]},
93 'sys_tc': {
80 - 'options': [None, "Time constant and poll exponent (3-17)", "log2 s", 'system', 'ntp.sys_tc', 'line'],
94 + 'options': [None, 'Time constant and poll exponent (3-17)', 'log2 s', 'system', 'ntp.sys_tc', 'line'],
95 'lines': [
96 ['tc', 'current', 'absolute', 1, PRECISION],
97 ['mintc', 'minimum', 'absolute', 1, PRECISION]
98 ]},
99 'sys_precision': {
86 - 'options': [None, "Precision", "log2 s", 'system', 'ntp.sys_precision', 'line'],
100 + 'options': [None, 'Precision', 'log2 s', 'system', 'ntp.sys_precision', 'line'],
101 'lines': [
102 ['precision', 'precision', 'absolute', 1, PRECISION]
103 ]}
104 }
105
92 -# Dynamic charts templates
93 -PEER_PREFIX = 'peer'
94 -
95 -PEER_DIMENSIONS = [
96 - ['offset', 'Filter offset', 'ms'],
97 - ['delay', 'Filter delay', 'ms'],
98 - ['dispersion', 'Filter dispersion', 'ms'],
99 - ['jitter', 'Filter jitter', 'ms'],
100 - ['xleave', 'Interleave delay', 'ms'],
101 - ['rootdelay', 'Total roundtrip delay to the primary reference clock', 'ms'],
102 - ['rootdisp', 'Total root dispersion to the primary reference clock', 'ms'],
103 - ['stratum', 'Stratum (1-15)', '1'],
104 - ['hmode', 'Host mode (1-6)', '1'],
105 - ['pmode', 'Peer mode (1-5)', '1'],
106 - ['hpoll', 'Host poll exponent', 'log2 s'],
107 - ['ppoll', 'Peer poll exponent', 'log2 s'],
108 - ['precision', 'Precision', 'log2 s']
109 -]
110 -
106 +PEER_CHARTS = {
107 + 'peer_offset': {
108 + 'options': [None, 'Filter offset', 'ms', 'peers', 'ntp.peer_offset', 'line'],
109 + 'lines': [
110 + ]},
111 + 'peer_delay': {
112 + 'options': [None, 'Filter delay', 'ms', 'peers', 'ntp.peer_delay', 'line'],
113 + 'lines': [
114 + ]},
115 + 'peer_dispersion': {
116 + 'options': [None, 'Filter dispersion', 'ms', 'peers', 'ntp.peer_dispersion', 'line'],
117 + 'lines': [
118 + ]},
119 + 'peer_jitter': {
120 + 'options': [None, 'Filter jitter', 'ms', 'peers', 'ntp.peer_jitter', 'line'],
121 + 'lines': [
122 + ]},
123 + 'peer_xleave': {
124 + 'options': [None, 'Interleave delay', 'ms', 'peers', 'ntp.peer_xleave', 'line'],
125 + 'lines': [
126 + ]},
127 + 'peer_rootdelay': {
128 + 'options': [None, 'Total roundtrip delay to the primary reference clock', 'ms', 'peers',
129 + 'ntp.peer_rootdelay', 'line'],
130 + 'lines': [
131 + ]},
132 + 'peer_rootdisp': {
133 + 'options': [None, 'Total root dispersion to the primary reference clock', 'ms', 'peers',
134 + 'ntp.peer_rootdisp', 'line'],
135 + 'lines': [
136 + ]},
137 + 'peer_stratum': {
138 + 'options': [None, 'Stratum (1-15)', '1', 'peers', 'ntp.peer_stratum', 'line'],
139 + 'lines': [
140 + ]},
141 + 'peer_hmode': {
142 + 'options': [None, 'Host mode (1-6)', '1', 'peers', 'ntp.peer_hmode', 'line'],
143 + 'lines': [
144 + ]},
145 + 'peer_pmode': {
146 + 'options': [None, 'Peer mode (1-5)', '1', 'peers', 'ntp.peer_pmode', 'line'],
147 + 'lines': [
148 + ]},
149 + 'peer_hpoll': {
150 + 'options': [None, 'Host poll exponent', 'log2 s', 'peers', 'ntp.peer_hpoll', 'line'],
151 + 'lines': [
152 + ]},
153 + 'peer_ppoll': {
154 + 'options': [None, 'Peer poll exponent', 'log2 s', 'peers', 'ntp.peer_ppoll', 'line'],
155 + 'lines': [
156 + ]},
157 + 'peer_precision': {
158 + 'options': [None, 'Precision', 'log2 s', 'peers', 'ntp.peer_precision', 'line'],
159 + 'lines': [
160 + ]}
161 +}
162
112 -class Peer(object):
113 - """
114 - Class to hold peer data required in _get_data
115 - """
116 - def __init__(self, peer_id, name, request):
117 - self.id = peer_id
118 - self.name = name
119 - self.request = request
163
164 +class Base:
165 + regex = re.compile(r'([a-z_]+)=((?:-)?[0-9]+(?:\.[0-9]+)?)')
166
122 -class Service(SocketService):
123 - def __init__(self, configuration=None, name=None):
124 - SocketService.__init__(self, configuration=configuration, name=name)
125 - self.port = 'ntp'
126 - self.dgram_socket = True
127 - self.request_systemvars = None
128 - self.peers = None
129 - self.peer_error = 0
130 - self.regex_srcadr = re.compile(r'srcadr=([A-Za-z0-9.-]+)')
131 - self.regex_data = re.compile(r'([a-z_]+)=([0-9-]+(?:\.[0-9]+)?)(?=,)')
132 - self.order = None
133 - self.definitions = None
134 -
135 - def create_charts(self):
167 + @staticmethod
168 + def get_header(associd=0, operation='readvar'):
169 """
137 - Creates the charts dynamically.
138 - Checks ntp for available peers.
139 - Adds all peers whith valid data.
170 + Constructs the NTP Control Message header:
171 + 0 1 2 3
172 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
173 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
174 + |LI | VN |Mode |R|E|M| OpCode | Sequence Number |
175 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
176 + | Status | Association ID |
177 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
178 + | Offset | Count |
179 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
180 """
141 - # Create systemvars charts
142 - self.order = list(ORDER)
143 - self.definitions = dict(CHARTS)
181 + version = 2
182 + sequence = 1
183 + status = 0
184 + offset = 0
185 + count = 0
186 + header = struct.pack(HEADER_FORMAT, (version << 3 | MODE), OPCODES[operation],
187 + sequence, status, associd, offset, count)
188 + return header
189
145 - # Get peer ids
146 - self.request = self.get_header(0, 'readstat')
147 - peer_ids = self.get_peer_ids(self._get_raw_data(raw=True))
148 -
149 - # Get peers
150 - peers = self.get_peers(peer_ids)
151 -
152 - # Create peer charts
153 - if peers:
154 - charts = dict()
155 -
156 - for dimension in PEER_DIMENSIONS:
157 - chart_id = '_'.join([PEER_PREFIX, dimension[0]])
158 - context = '.'.join(['ntp', chart_id])
159 - title = dimension[1]
160 - units = dimension[2]
161 - lines = list()
162 -
163 - for peer in peers:
164 - unique_dimension_id = '_'.join([peer.name, dimension[0]])
165 - line = [unique_dimension_id, peer.name, 'absolute', 1, PRECISION]
166 - lines.append(line)
167 - charts[chart_id] = dict()
168 - charts[chart_id]['options'] = [None, title, units, 'peers', context, 'line']
169 - charts[chart_id]['lines'] = lines
170 -
171 - self.order += ['_'.join([PEER_PREFIX, d[0]]) for d in PEER_DIMENSIONS]
172 - self.definitions.update(charts)
173 - self.peers = cycle(peers)
174 - else:
175 - self.peers = None
190
177 - def get_peers(self, peer_ids):
191 +class System(Base):
192 + def __init__(self):
193 + self.request = self.get_header()
194 +
195 + def get_data(self, raw):
196 """
179 - Figures out the possible local domain name.
180 - Queries each peer once to get data for charts.
181 - Replace the peer srcadr with the possible hostname.
182 - Returns all peers whith valid data.
197 + Extracts key=value pairs with float/integer from ntp response packet data.
198 """
184 - if peer_ids:
185 - peer_ids.sort()
186 -
187 - domain = None
188 -
189 - # Get the local domain name
190 - if self.peer_names:
191 - try:
192 - hostname = socket.gethostname()
193 - fqdn = socket.getfqdn()
194 - if fqdn.startswith(hostname):
195 - domain = fqdn[len(hostname):]
196 - except socket.error:
197 - self.error('Error getting local domain')
198 -
199 - peers = list()
200 -
201 - # Get peer data
202 - for peer_id in peer_ids:
203 - request = self.get_header(peer_id, 'readvar')
204 - self.request = request
205 - raw = self._get_raw_data()
206 - if not raw:
207 - continue
208 -
209 - data = self.get_data_from_raw(raw)
210 - if not data:
211 - continue
199 + data = dict()
200 + for key, value in self.regex.findall(raw):
201 + data[key] = float(value) * PRECISION
202 + return data
203
213 - match_srcadr = self.regex_srcadr.search(raw)
214 - if not match_srcadr:
215 - continue
204
217 - name = match_srcadr.group(1)
218 - match_peer_filter = self.regex_peer_filter.search(name)
219 - if match_peer_filter:
220 - continue
205 +class Peer(Base):
206 + def __init__(self, idx, name):
207 + self.id = idx
208 + self.real_name = name
209 + self.name = name.replace('.', '_')
210 + self.request = self.get_header(self.id)
211
222 - if domain:
223 - try:
224 - name = socket.gethostbyaddr(name)[0]
212 + def get_data(self, raw):
213 + """
214 + Extracts key=value pairs with float/integer from ntp response packet data.
215 + """
216 + data = dict()
217 + for key, value in self.regex.findall(raw):
218 + dimension = '_'.join([self.name, key])
219 + data[dimension] = float(value) * PRECISION
220 + return data
221
226 - match_peer_filter = self.regex_peer_filter.search(name)
227 - if match_peer_filter:
228 - continue
222 + def rebuild_request(self, new_idx):
223 + self.id = new_idx
224 + self.request = self.get_header(self.id)
225
230 - if len(name) > len(domain) and name.endswith(domain):
231 - name = name[:-len(domain)]
232 - except (IndexError, socket.error):
233 - self.error('Failed to reverse lookup address')
226
235 - name = name.replace('.', '-')
227 +class Service(SocketService):
228 + def __init__(self, configuration=None, name=None):
229 + SocketService.__init__(self, configuration=configuration, name=name)
230 + self.order = list(ORDER)
231 + self.definitions = dict(CHARTS)
232 + self.definitions.update(PEER_CHARTS)
233
237 - peers.append(Peer(peer_id, name, request))
234 + self.port = 'ntp'
235 + self.dgram_socket = True
236 + self.system = System()
237 + self.peers = dict()
238 + self.request = str()
239 + self.retries = 0
240
239 - return peers
241 + peer_filter = self.configuration.get('peer_filter', r'127\..*')
242 + try:
243 + self.peer_filter = re.compile(r'^((0\.0\.0\.0)|({0}))$'.format(peer_filter))
244 + except re.error as error:
245 + self.error('Compile pattern error : {0}'.format(error))
246 + self.peer_filter = None
247
248 def check(self):
249 """
250 Checks if we can get valid systemvars.
251 If not, returns None to disable module.
252 """
246 - self._parse_config()
247 - self.peer_names = self.configuration.get('peer_names', True)
248 - peer_filter_custom = self.configuration.get('peer_filter', r'127\..*')
253
250 - try:
251 - self.regex_peer_filter = re.compile(r'^((0\.0\.0\.0)|({0}))$'.format(peer_filter_custom))
252 - except re.error as error:
253 - self.error('Pattern compile error: {0}'.format(error))
254 + if not self.peer_filter:
255 return None
256
256 - self.request_systemvars = self.get_header(0, 'readvar')
257 - self.request = self.request_systemvars
257 + self._parse_config()
258 +
259 + self.request = self.system.request
260 raw_systemvars = self._get_raw_data()
261
260 - if not self.get_data_from_raw(raw_systemvars):
262 + if not self.system.get_data(raw_systemvars):
263 return None
262 -
263 - self.create_charts()
264 -
264 return True
265
267 - def _get_data(self):
266 + def get_data(self):
267 """
268 Gets systemvars data on each update.
270 - Gets peervars data for only one peer on each update.
271 - Total amount of _get_raw_data invocations per update = 2
269 + Gets peervars data for all peers on each update.
270 """
271 data = dict()
272
275 - self.request = self.request_systemvars
276 - raw_systemvars = self._get_raw_data()
277 - data.update(self.get_data_from_raw(raw_systemvars))
273 + self.request = self.system.request
274 + raw = self._get_raw_data()
275 + if not raw:
276 + return None
277
279 - if self.peers:
280 - peer = next(self.peers)
281 - self.request = peer.request
282 - raw_peervars = self._get_raw_data()
283 - data.update(self.get_data_from_raw(raw_peervars, peer))
278 + data.update(self.system.get_data(raw))
279 +
280 + # TODO: run self.find_new_peers() every N runs?
281 + if not self.peers or self.retries > 8:
282 + self.find_new_peers()
283 + else:
284 + for peer in self.peers.values():
285 + self.request = peer.request
286 + peer_data = peer.get_data(self._get_raw_data())
287 + if peer_data:
288 + data.update(peer_data)
289 + else:
290 + self.retries += 1
291
285 - if not data:
286 - self.error("No data received")
287 - return None
292 + return data or None
293
289 - return data
294 + def find_new_peers(self):
295 + new_peers = dict((p.real_name, p) for p in self.get_peers())
296 + if new_peers:
297
291 - def get_data_from_raw(self, raw, peer=None):
292 - """
293 - Extracts key=value pairs with float/integer from ntp response packet data.
294 - """
295 - data = dict()
296 - try:
297 - data_list = self.regex_data.findall(raw)
298 + peers_to_remove = set(self.peers) - set(new_peers)
299 + peers_to_update = set(self.peers) & set(new_peers)
300 + peers_to_add = set(new_peers) - set(self.peers)
301
299 - for data_point in data_list:
300 - key, value = data_point
301 - if peer:
302 - dimension = '_'.join([peer.name, key])
303 - else:
304 - dimension = key
305 - data[dimension] = int(float(value) * PRECISION)
306 - except (ValueError, AttributeError, TypeError):
307 - self.error("Invalid data received")
308 - return None
302 + for peer_name in peers_to_remove:
303 + self.hide_old_peer_from_charts(self.peers[peer_name])
304 + del self.peers[peer_name]
305
310 - # If peer returns no valid data, probably due to ntpd restart,
311 - # then wait 10 seconds and re-initialize the peers and charts
312 - if not data and peer:
313 - self.error('Peer error: No data received')
314 - self.peer_error += 1
306 + for peer_name in peers_to_update:
307 + if new_peers[peer_name].id != self.peers[peer_name].id:
308 + self.peers[peer_name].rebuild_request(new_peers[peer_name].id)
309
316 - if (self.peer_error * self.update_every) > 10:
317 - self.error('Peer error count exceeded, re-creating charts.')
318 - self.create_charts()
319 - self.peer_error = 0
320 - self.create()
310 + for peer_name in peers_to_add:
311 + self.add_new_peer_to_charts(new_peers[peer_name])
312
322 - return data
313 + self.peers.update(new_peers)
314 + self.retries = 0
315
324 - def get_header(self, associd=0, operation='readvar'):
325 - """
326 - Constructs the NTP Control Message header:
327 - 0 1 2 3
328 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
329 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
330 - |LI | VN |Mode |R|E|M| OpCode | Sequence Number |
331 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
332 - | Status | Association ID |
333 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
334 - | Offset | Count |
335 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
336 - """
337 - try:
338 - opcode = OPCODES[operation]
339 - except KeyError:
340 - self.error('Invalid operation: {0}'.format(operation))
341 - return None
342 - version = 2
343 - sequence = 1
344 - status = 0
345 - offset = 0
346 - count = 0
347 - try:
348 - header = struct.pack(HEADER_FORMAT, (version << 3 | MODE), opcode,
349 - sequence, status, associd, offset, count)
350 - return header
351 - except struct.error:
352 - self.error('error packing header: {0}'.format(struct.error))
353 - return None
316 + def add_new_peer_to_charts(self, peer):
317 + for chart_id in set(self.charts.charts) & set(PEER_CHARTS):
318 + dim_id = peer.name + chart_id[4:]
319 + if dim_id not in self.charts[chart_id]:
320 + self.charts[chart_id].add_dimension([dim_id, peer.real_name, 'absolute', 1, PRECISION])
321 + else:
322 + self.charts[chart_id].hide_dimension(dim_id, reverse=True)
323 +
324 + def hide_old_peer_from_charts(self, peer):
325 + for chart_id in set(self.charts.charts) & set(PEER_CHARTS):
326 + dim_id = peer.name + chart_id[4:]
327 + self.charts[chart_id].hide_dimension(dim_id)
328 +
329 + def get_peers(self):
330 + self.request = Base.get_header(operation='readstat')
331 +
332 + raw_data = self._get_raw_data(raw=True)
333 + if not raw_data:
334 + return list()
335 +
336 + peer_ids = self.get_peer_ids(raw_data)
337 + if not peer_ids:
338 + return list()
339 +
340 + new_peers = list()
341 + for peer_id in peer_ids:
342 + self.request = Base.get_header(peer_id)
343 + raw_peer_data = self._get_raw_data()
344 + if not raw_peer_data:
345 + continue
346 + srcadr = re.search(r'(srcadr)=([^,]+)', raw_peer_data)
347 + if not srcadr:
348 + continue
349 + srcadr = srcadr.group(2)
350 + if self.peer_filter.search(srcadr):
351 + continue
352 +
353 + new_peer = Peer(idx=peer_id, name=srcadr)
354 + new_peers.append(new_peer)
355 + return new_peers
356
357 def get_peer_ids(self, res):
358 """
@@ -358,21 +360,22 @@ class Service(SocketService):
360 Get data length from header
361 Get list of association ids returned in the readstat response
362 """
363 +
364 try:
365 count = struct.unpack(HEADER_FORMAT, res[:HEADER_LEN])[6]
363 - except struct.error:
364 - self.error('error unpacking header: {0}'.format(struct.error))
365 - return list()
366 + except struct.error as error:
367 + self.error('error unpacking header: {0}'.format(error))
368 + return None
369 if not count:
367 - self.debug('empty data field in NTP control packet')
368 - return list()
370 + self.error('empty data field in NTP control packet')
371 + return None
372
373 data_end = HEADER_LEN + count
374 data = res[HEADER_LEN:data_end]
375 data_format = ''.join(['!', 'H' * int(count / 2)])
373 -
376 try:
375 - return list(struct.unpack(data_format, data))[::2]
376 - except struct.error:
377 - self.error('error unpacking data: {0}'.format(struct.error))
378 - return list()
377 + peer_ids = list(struct.unpack(data_format, data))[::2]
378 + except struct.error as error:
379 + self.error('error unpacking data: {0}'.format(error))
380 + return None
381 + return peer_ids