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