blob: 18f13e9c4c0988b91e38783d3def093a55c7f532 [file] [log] [blame]
Renato Botelho do Coutoead1e532019-10-31 13:31:07 -05001#!/usr/bin/env python3
Ole Troane66443c2021-03-18 11:12:01 +01002#
3# Copyright (c) 2021 Cisco and/or its affiliates.
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at:
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15#
Ole Troan73202102018-08-31 00:29:48 +020016
Ole Troane66443c2021-03-18 11:12:01 +010017'''
18This module implement Python access to the VPP statistics segment. It
19accesses the data structures directly in shared memory.
20VPP uses optimistic locking, so data structures may change underneath
21us while we are reading. Data is copied out and it's important to
22spend as little time as possible "holding the lock".
23
24Counters are stored in VPP as a two dimensional array.
25Index by thread and index (typically sw_if_index).
26Simple counters count only packets, Combined counters count packets
27and octets.
28
29Counters can be accessed in either dimension.
30stat['/if/rx'] - returns 2D lists
31stat['/if/rx'][0] - returns counters for all interfaces for thread 0
32stat['/if/rx'][0][1] - returns counter for interface 1 on thread 0
33stat['/if/rx'][0][1]['packets'] - returns the packet counter
34 for interface 1 on thread 0
35stat['/if/rx'][:, 1] - returns the counters for interface 1 on all threads
36stat['/if/rx'][:, 1].packets() - returns the packet counters for
37 interface 1 on all threads
38stat['/if/rx'][:, 1].sum_packets() - returns the sum of packet counters for
39 interface 1 on all threads
40stat['/if/rx-miss'][:, 1].sum() - returns the sum of packet counters for
41 interface 1 on all threads for simple counters
42'''
43
44import os
45import socket
46import array
47import mmap
48from struct import Struct
Paul Vinciguerra56421092018-11-21 16:34:09 -080049import time
Ole Troane66443c2021-03-18 11:12:01 +010050import unittest
51import re
Ole Troan73202102018-08-31 00:29:48 +020052
Ole Troane66443c2021-03-18 11:12:01 +010053def recv_fd(sock):
54 '''Get file descriptor for memory map'''
55 fds = array.array("i") # Array of ints
56 _, ancdata, _, _ = sock.recvmsg(0, socket.CMSG_LEN(4))
57 for cmsg_level, cmsg_type, cmsg_data in ancdata:
58 if cmsg_level == socket.SOL_SOCKET and cmsg_type == socket.SCM_RIGHTS:
59 fds.frombytes(cmsg_data[:len(cmsg_data) - (len(cmsg_data) % fds.itemsize)])
60 return list(fds)[0]
Ole Troan73202102018-08-31 00:29:48 +020061
Ole Troane66443c2021-03-18 11:12:01 +010062VEC_LEN_FMT = Struct('I')
63def get_vec_len(stats, vector_offset):
64 '''Equivalent to VPP vec_len()'''
65 return VEC_LEN_FMT.unpack_from(stats.statseg, vector_offset - 8)[0]
Ole Troan73202102018-08-31 00:29:48 +020066
Ole Troane66443c2021-03-18 11:12:01 +010067def get_string(stats, ptr):
68 '''Get a string from a VPP vector'''
69 namevector = ptr - stats.base
70 namevectorlen = get_vec_len(stats, namevector)
71 if namevector + namevectorlen >= stats.size:
72 raise ValueError('String overruns stats segment')
73 return stats.statseg[namevector:namevector+namevectorlen-1].decode('ascii')
Ole Troan73202102018-08-31 00:29:48 +020074
75
Ole Troane66443c2021-03-18 11:12:01 +010076class StatsVector:
77 '''A class representing a VPP vector'''
Ole Troan73202102018-08-31 00:29:48 +020078
Ole Troane66443c2021-03-18 11:12:01 +010079 def __init__(self, stats, ptr, fmt):
80 self.vec_start = ptr - stats.base
81 self.vec_len = get_vec_len(stats, ptr - stats.base)
82 self.struct = Struct(fmt)
83 self.fmtlen = len(fmt)
84 self.elementsize = self.struct.size
85 self.statseg = stats.statseg
86 self.stats = stats
Ole Troan73202102018-08-31 00:29:48 +020087
Ole Troane66443c2021-03-18 11:12:01 +010088 if self.vec_start + self.vec_len * self.elementsize >= stats.size:
89 raise ValueError('Vector overruns stats segment')
Ole Troan73202102018-08-31 00:29:48 +020090
Ole Troane66443c2021-03-18 11:12:01 +010091 def __iter__(self):
92 with self.stats.lock:
93 return self.struct.iter_unpack(self.statseg[self.vec_start:self.vec_start +
94 self.elementsize*self.vec_len])
Ole Troan73202102018-08-31 00:29:48 +020095
Ole Troane66443c2021-03-18 11:12:01 +010096 def __getitem__(self, index):
97 if index > self.vec_len:
98 raise ValueError('Index beyond end of vector')
99 with self.stats.lock:
100 if self.fmtlen == 1:
101 return self.struct.unpack_from(self.statseg, self.vec_start +
102 (index * self.elementsize))[0]
103 return self.struct.unpack_from(self.statseg, self.vec_start +
104 (index * self.elementsize))
Ole Troan73202102018-08-31 00:29:48 +0200105
Ole Troane66443c2021-03-18 11:12:01 +0100106class VPPStats():
107 '''Main class implementing Python access to the VPP statistics segment'''
108 # pylint: disable=too-many-instance-attributes
109 shared_headerfmt = Struct('QPQQPP')
YohanPipereau71dd9d52019-06-06 16:34:14 +0200110 default_socketname = '/run/vpp/stats.sock'
Paul Vinciguerra6ccc6e92018-11-27 08:15:22 -0800111
112 def __init__(self, socketname=default_socketname, timeout=10):
Paul Vinciguerrae090f4d2019-11-29 17:41:20 -0500113 self.socketname = socketname
114 self.timeout = timeout
Ole Troane66443c2021-03-18 11:12:01 +0100115 self.directory = {}
116 self.lock = StatsLock(self)
Paul Vinciguerrae090f4d2019-11-29 17:41:20 -0500117 self.connected = False
Ole Troane66443c2021-03-18 11:12:01 +0100118 self.size = 0
119 self.last_epoch = 0
120 self.error_vectors = 0
121 self.statseg = 0
Paul Vinciguerrae090f4d2019-11-29 17:41:20 -0500122
123 def connect(self):
Ole Troane66443c2021-03-18 11:12:01 +0100124 '''Connect to stats segment'''
125 if self.connected:
126 return
127 sock = socket.socket(socket.AF_UNIX, socket.SOCK_SEQPACKET)
128 sock.connect(self.socketname)
Paul Vinciguerra56421092018-11-21 16:34:09 -0800129
Ole Troane66443c2021-03-18 11:12:01 +0100130 mfd = recv_fd(sock)
131 sock.close()
Paul Vinciguerra56421092018-11-21 16:34:09 -0800132
Ole Troane66443c2021-03-18 11:12:01 +0100133 stat_result = os.fstat(mfd)
134 self.statseg = mmap.mmap(mfd, stat_result.st_size, mmap.PROT_READ, mmap.MAP_SHARED)
135 socket.close(mfd)
Ole Troan73202102018-08-31 00:29:48 +0200136
Ole Troane66443c2021-03-18 11:12:01 +0100137 self.size = stat_result.st_size
138 if self.version != 2:
139 raise Exception('Incompatbile stat segment version {}'
140 .format(self.version))
Ole Troan73202102018-08-31 00:29:48 +0200141
Ole Troane66443c2021-03-18 11:12:01 +0100142 self.refresh()
143 self.connected = True
Ole Troan233e4682019-05-16 15:01:34 +0200144
Ole Troan73202102018-08-31 00:29:48 +0200145 def disconnect(self):
Ole Troane66443c2021-03-18 11:12:01 +0100146 '''Disconnect from stats segment'''
147 if self.connected:
148 self.statseg.close()
Paul Vinciguerrae090f4d2019-11-29 17:41:20 -0500149 self.connected = False
Ole Troane66443c2021-03-18 11:12:01 +0100150
151 @property
152 def version(self):
153 '''Get version of stats segment'''
154 return self.shared_headerfmt.unpack_from(self.statseg)[0]
155
156 @property
157 def base(self):
158 '''Get base pointer of stats segment'''
159 return self.shared_headerfmt.unpack_from(self.statseg)[1]
160
161 @property
162 def epoch(self):
163 '''Get current epoch value from stats segment'''
164 return self.shared_headerfmt.unpack_from(self.statseg)[2]
165
166 @property
167 def in_progress(self):
168 '''Get value of in_progress from stats segment'''
169 return self.shared_headerfmt.unpack_from(self.statseg)[3]
170
171 @property
172 def directory_vector(self):
173 '''Get pointer of directory vector'''
174 return self.shared_headerfmt.unpack_from(self.statseg)[4]
175
176 @property
177 def error_vector(self):
178 '''Get pointer of error vector'''
179 return self.shared_headerfmt.unpack_from(self.statseg)[5]
180
181 elementfmt = 'IQ128s'
182
183 def refresh(self):
184 '''Refresh directory vector cache (epoch changed)'''
185 directory = {}
186 with self.lock:
187 for direntry in StatsVector(self, self.directory_vector, self.elementfmt):
188 path_raw = direntry[2].find(b'\x00')
189 path = direntry[2][:path_raw].decode('ascii')
190 directory[path] = StatsEntry(direntry[0], direntry[1])
191 self.last_epoch = self.epoch
192 self.directory = directory
193
194 # Cache the error index vectors
195 self.error_vectors = []
196 for threads in StatsVector(self, self.error_vector, 'P'):
197 self.error_vectors.append(StatsVector(self, threads[0], 'Q'))
198
199 def __getitem__(self, item):
200 if not self.connected:
201 self.connect()
202 if self.last_epoch != self.epoch:
203 self.refresh()
204 with self.lock:
205 return self.directory[item].get_counter(self)
206
207 def __iter__(self):
208 return iter(self.directory.items())
Ole Troan73202102018-08-31 00:29:48 +0200209
210 def set_errors(self):
Ole Troane66443c2021-03-18 11:12:01 +0100211 '''Return dictionary of error counters > 0'''
212 if not self.connected:
213 self.connect()
Paul Vinciguerra56421092018-11-21 16:34:09 -0800214
Ole Troane66443c2021-03-18 11:12:01 +0100215 errors = {k:v for k, v in self.directory.items() if k.startswith("/err/")}
216 result = {}
217 with self.lock:
218 for k, entry in errors.items():
219 total = 0
220 i = entry.value
221 for per_thread in self.error_vectors:
222 total += per_thread[i]
223 if total:
224 result[k] = total
225 return result
Ole Troan73202102018-08-31 00:29:48 +0200226
227 def set_errors_str(self):
228 '''Return all errors counters > 0 pretty printed'''
Ole Troane66443c2021-03-18 11:12:01 +0100229 error_string = ['ERRORS:']
Ole Troan73202102018-08-31 00:29:48 +0200230 error_counters = self.set_errors()
231 for k in sorted(error_counters):
Ole Troane66443c2021-03-18 11:12:01 +0100232 error_string.append('{:<60}{:>10}'.format(k, error_counters[k]))
233 return '%s\n' % '\n'.join(error_string)
234
235 def get_counter(self, name):
236 '''Alternative call to __getitem__'''
237 return self.__getitem__(name)
238
239 def get_err_counter(self, name):
240 '''Return a single value (sum of all threads)'''
241 if not self.connected:
242 self.connect()
243 return sum(self.directory[name].get_counter(self))
244
245 def ls(self, patterns):
246 '''Returns list of counters matching pattern'''
247 # pylint: disable=invalid-name
248 if not self.connected:
249 self.connect()
Ole Troan3daca3f2021-03-29 21:12:53 +0200250 if not isinstance(patterns, list):
251 patterns = [patterns]
Ole Troane66443c2021-03-18 11:12:01 +0100252 regex = [re.compile(i) for i in patterns]
253 return [k for k, v in self.directory.items()
254 if any(re.match(pattern, k) for pattern in regex)]
255
256 def dump(self, counters):
257 '''Given a list of counters return a dictionary of results'''
258 if not self.connected:
259 self.connect()
260 result = {}
261 for cnt in counters:
262 result[cnt] = self.__getitem__(cnt)
263 return result
264
265class StatsLock():
266 '''Stat segment optimistic locking'''
267
268 def __init__(self, stats):
269 self.stats = stats
270 self.epoch = 0
271
272 def __enter__(self):
273 acquired = self.acquire(blocking=True)
274 assert acquired, "Lock wasn't acquired, but blocking=True"
275 return self
276
277 def __exit__(self, exc_type=None, exc_value=None, traceback=None):
278 self.release()
279
280 def acquire(self, blocking=True, timeout=-1):
281 '''Acquire the lock. Await in progress to go false. Record epoch.'''
282 self.epoch = self.stats.epoch
283 if timeout > 0:
284 start = time.monotonic()
285 while self.stats.in_progress:
286 if not blocking:
287 time.sleep(0.01)
288 if timeout > 0:
289 if start + time.monotonic() > timeout:
290 return False
291 return True
292
293 def release(self):
294 '''Check if data read while locked is valid'''
295 if self.stats.in_progress or self.stats.epoch != self.epoch:
296 raise IOError('Optimistic lock failed, retry')
297
298 def locked(self):
299 '''Not used'''
300
301
302class StatsCombinedList(list):
303 '''Column slicing for Combined counters list'''
304
305 def __getitem__(self, item):
306 '''Supports partial numpy style 2d support. Slice by column [:,1]'''
307 if isinstance(item, int):
308 return list.__getitem__(self, item)
309 return CombinedList([row[item[1]] for row in self])
310
311class CombinedList(list):
312 '''Combined Counters 2-dimensional by thread by index of packets/octets'''
313
314 def packets(self):
315 '''Return column (2nd dimension). Packets for all threads'''
316 return [pair[0] for pair in self]
317
318 def octets(self):
319 '''Return column (2nd dimension). Octets for all threads'''
320 return [pair[1] for pair in self]
321
322 def sum_packets(self):
323 '''Return column (2nd dimension). Sum of all packets for all threads'''
324 return sum(self.packets())
325
326 def sum_octets(self):
327 '''Return column (2nd dimension). Sum of all octets for all threads'''
328 return sum(self.octets())
329
330class StatsTuple(tuple):
331 '''A Combined vector tuple (packets, octets)'''
332 def __init__(self, data):
333 self.dictionary = {'packets': data[0], 'bytes': data[1]}
334 super().__init__()
335
336 def __repr__(self):
337 return dict.__repr__(self.dictionary)
338
339 def __getitem__(self, item):
340 if isinstance(item, int):
341 return tuple.__getitem__(self, item)
342 if item == 'packets':
343 return tuple.__getitem__(self, 0)
344 return tuple.__getitem__(self, 1)
345
346class StatsSimpleList(list):
347 '''Simple Counters 2-dimensional by thread by index of packets'''
348
349 def __getitem__(self, item):
350 '''Supports partial numpy style 2d support. Slice by column [:,1]'''
351 if isinstance(item, int):
352 return list.__getitem__(self, item)
353 return SimpleList([row[item[1]] for row in self])
354
355class SimpleList(list):
356 '''Simple counter'''
357
358 def sum(self):
359 '''Sum the vector'''
360 return sum(self)
361
362class StatsEntry():
363 '''An individual stats entry'''
364 # pylint: disable=unused-argument,no-self-use
365
366 def __init__(self, stattype, statvalue):
367 self.type = stattype
368 self.value = statvalue
369
370 if stattype == 1:
371 self.function = self.scalar
372 elif stattype == 2:
373 self.function = self.simple
374 elif stattype == 3:
375 self.function = self.combined
376 elif stattype == 4:
377 self.function = self.error
378 elif stattype == 5:
379 self.function = self.name
380 else:
381 self.function = self.illegal
382
383 def illegal(self, stats):
384 '''Invalid or unknown counter type'''
385 return None
386
387 def scalar(self, stats):
388 '''Scalar counter'''
389 return self.value
390
391 def simple(self, stats):
392 '''Simple counter'''
393 counter = StatsSimpleList()
394 for threads in StatsVector(stats, self.value, 'P'):
395 clist = [v[0] for v in StatsVector(stats, threads[0], 'Q')]
396 counter.append(clist)
397 return counter
398
399 def combined(self, stats):
400 '''Combined counter'''
401 counter = StatsCombinedList()
402 for threads in StatsVector(stats, self.value, 'P'):
403 clist = [StatsTuple(cnt) for cnt in StatsVector(stats, threads[0], 'QQ')]
404 counter.append(clist)
405 return counter
406
407 def error(self, stats):
408 '''Error counter'''
409 counter = SimpleList()
410 for clist in stats.error_vectors:
411 counter.append(clist[self.value])
412 return counter
413
414 def name(self, stats):
415 '''Name counter'''
416 counter = []
417 for name in StatsVector(stats, self.value, 'P'):
418 counter.append(get_string(stats, name[0]))
419 return counter
420
421 def get_counter(self, stats):
422 '''Return a list of counters'''
423 return self.function(stats)
424
425class TestStats(unittest.TestCase):
426 '''Basic statseg tests'''
427
428 def setUp(self):
429 '''Connect to statseg'''
430 self.stat = VPPStats()
431 self.stat.connect()
432 self.profile = cProfile.Profile()
433 self.profile.enable()
434
435 def tearDown(self):
436 '''Disconnect from statseg'''
437 self.stat.disconnect()
438 profile = Stats(self.profile)
439 profile.strip_dirs()
440 profile.sort_stats('cumtime')
441 profile.print_stats()
442 print("\n--->>>")
443
444 def test_counters(self):
445 '''Test access to statseg'''
446
447 print('/err/abf-input-ip4/missed', self.stat['/err/abf-input-ip4/missed'])
448 print('/sys/heartbeat', self.stat['/sys/heartbeat'])
449 print('/if/names', self.stat['/if/names'])
450 print('/if/rx-miss', self.stat['/if/rx-miss'])
451 print('/if/rx-miss', self.stat['/if/rx-miss'][1])
452 print('/nat44-ed/out2in/slowpath/drops', self.stat['/nat44-ed/out2in/slowpath/drops'])
453 print('Set Errors', self.stat.set_errors())
454 with self.assertRaises(KeyError):
455 print('NO SUCH COUNTER', self.stat['foobar'])
456 print('/if/rx', self.stat.get_counter('/if/rx'))
457 print('/err/ethernet-input/no error',
458 self.stat.get_err_counter('/err/ethernet-input/no error'))
459
460 def test_column(self):
461 '''Test column slicing'''
462
463 print('/if/rx-miss', self.stat['/if/rx-miss'])
464 print('/if/rx', self.stat['/if/rx']) # All interfaces for thread #1
465 print('/if/rx thread #1', self.stat['/if/rx'][0]) # All interfaces for thread #1
466 print('/if/rx thread #1, interface #1',
467 self.stat['/if/rx'][0][1]) # All interfaces for thread #1
468 print('/if/rx if_index #1', self.stat['/if/rx'][:, 1])
469 print('/if/rx if_index #1 packets', self.stat['/if/rx'][:, 1].packets())
470 print('/if/rx if_index #1 packets', self.stat['/if/rx'][:, 1].sum_packets())
471 print('/if/rx if_index #1 packets', self.stat['/if/rx'][:, 1].octets())
472 print('/if/rx-miss', self.stat['/if/rx-miss'])
473 print('/if/rx-miss if_index #1 packets', self.stat['/if/rx-miss'][:, 1].sum())
474 print('/if/rx if_index #1 packets', self.stat['/if/rx'][0][1]['packets'])
475
476 def test_error(self):
477 '''Test the error vector'''
478
479 print('/err/ethernet-input', self.stat['/err/ethernet-input/no error'])
480 print('/err/nat44-ei-ha/pkts-processed', self.stat['/err/nat44-ei-ha/pkts-processed'])
481 print('/err/ethernet-input', self.stat.get_err_counter('/err/ethernet-input/no error'))
482 print('/err/ethernet-input', self.stat['/err/ethernet-input/no error'].sum())
483
484 def test_nat44(self):
485 '''Test the nat counters'''
486
487 print('/nat44-ei/ha/del-event-recv', self.stat['/nat44-ei/ha/del-event-recv'])
488 print('/err/nat44-ei-ha/pkts-processed', self.stat['/err/nat44-ei-ha/pkts-processed'].sum())
489
490 def test_legacy(self):
491 '''Legacy interface'''
492 directory = self.stat.ls(["^/if", "/err/ip4-input", "/sys/node/ip4-input"])
493 data = self.stat.dump(directory)
494 print(data)
495 print('Looking up sys node')
496 directory = self.stat.ls(["^/sys/node"])
497 print('Dumping sys node')
498 data = self.stat.dump(directory)
499 print(data)
500 directory = self.stat.ls(["^/foobar"])
501 data = self.stat.dump(directory)
502 print(data)
503
Ole Troan3daca3f2021-03-29 21:12:53 +0200504 def test_sys_nodes(self):
505 '''Test /sys/nodes'''
506 counters = self.stat.ls('^/sys/node')
507 print('COUNTERS:', counters)
508 print('/sys/node', self.stat.dump(counters))
509 print('/net/route/to', self.stat['/net/route/to'])
510
Ole Troane66443c2021-03-18 11:12:01 +0100511if __name__ == '__main__':
512 import cProfile
513 from pstats import Stats
514
515 unittest.main()