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:
8 # http://www.apache.org/licenses/LICENSE-2.0
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.
18 This module implement Python access to the VPP statistics segment. It
19 accesses the data structures directly in shared memory.
20 VPP uses optimistic locking, so data structures may change underneath
21 us while we are reading. Data is copied out and it's important to
22 spend as little time as possible "holding the lock".
24 Counters are stored in VPP as a two dimensional array.
25 Index by thread and index (typically sw_if_index).
26 Simple counters count only packets, Combined counters count packets
29 Counters can be accessed in either dimension.
30 stat['/if/rx'] - returns 2D lists
31 stat['/if/rx'][0] - returns counters for all interfaces for thread 0
32 stat['/if/rx'][0][1] - returns counter for interface 1 on thread 0
33 stat['/if/rx'][0][1]['packets'] - returns the packet counter
34 for interface 1 on thread 0
35 stat['/if/rx'][:, 1] - returns the counters for interface 1 on all threads
36 stat['/if/rx'][:, 1].packets() - returns the packet counters for
37 interface 1 on all threads
38 stat['/if/rx'][:, 1].sum_packets() - returns the sum of packet counters for
39 interface 1 on all threads
40 stat['/if/rx-miss'][:, 1].sum() - returns the sum of packet counters for
41 interface 1 on all threads for simple counters
48 from struct import Struct
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)])
62 VEC_LEN_FMT = Struct('I')
63 def 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]
67 def 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')
77 '''A class representing a VPP vector'''
79 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
88 if self.vec_start + self.vec_len * self.elementsize >= stats.size:
89 raise ValueError('Vector overruns stats segment')
93 return self.struct.iter_unpack(self.statseg[self.vec_start:self.vec_start +
94 self.elementsize*self.vec_len])
96 def __getitem__(self, index):
97 if index > self.vec_len:
98 raise ValueError('Index beyond end of vector')
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))
107 '''Main class implementing Python access to the VPP statistics segment'''
108 # pylint: disable=too-many-instance-attributes
109 shared_headerfmt = Struct('QPQQPP')
110 default_socketname = '/run/vpp/stats.sock'
112 def __init__(self, socketname=default_socketname, timeout=10):
113 self.socketname = socketname
114 self.timeout = timeout
116 self.lock = StatsLock(self)
117 self.connected = False
120 self.error_vectors = 0
124 '''Connect to stats segment'''
127 sock = socket.socket(socket.AF_UNIX, socket.SOCK_SEQPACKET)
128 sock.connect(self.socketname)
133 stat_result = os.fstat(mfd)
134 self.statseg = mmap.mmap(mfd, stat_result.st_size, mmap.PROT_READ, mmap.MAP_SHARED)
137 self.size = stat_result.st_size
138 if self.version != 2:
139 raise Exception('Incompatbile stat segment version {}'
140 .format(self.version))
143 self.connected = True
145 def disconnect(self):
146 '''Disconnect from stats segment'''
149 self.connected = False
153 '''Get version of stats segment'''
154 return self.shared_headerfmt.unpack_from(self.statseg)[0]
158 '''Get base pointer of stats segment'''
159 return self.shared_headerfmt.unpack_from(self.statseg)[1]
163 '''Get current epoch value from stats segment'''
164 return self.shared_headerfmt.unpack_from(self.statseg)[2]
167 def in_progress(self):
168 '''Get value of in_progress from stats segment'''
169 return self.shared_headerfmt.unpack_from(self.statseg)[3]
172 def directory_vector(self):
173 '''Get pointer of directory vector'''
174 return self.shared_headerfmt.unpack_from(self.statseg)[4]
177 def error_vector(self):
178 '''Get pointer of error vector'''
179 return self.shared_headerfmt.unpack_from(self.statseg)[5]
181 elementfmt = 'IQ128s'
184 '''Refresh directory vector cache (epoch changed)'''
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
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'))
199 def __getitem__(self, item):
200 if not self.connected:
202 if self.last_epoch != self.epoch:
205 return self.directory[item].get_counter(self)
208 return iter(self.directory.items())
210 def set_errors(self):
211 '''Return dictionary of error counters > 0'''
212 if not self.connected:
215 errors = {k:v for k, v in self.directory.items() if k.startswith("/err/")}
218 for k, entry in errors.items():
221 for per_thread in self.error_vectors:
222 total += per_thread[i]
227 def set_errors_str(self):
228 '''Return all errors counters > 0 pretty printed'''
229 error_string = ['ERRORS:']
230 error_counters = self.set_errors()
231 for k in sorted(error_counters):
232 error_string.append('{:<60}{:>10}'.format(k, error_counters[k]))
233 return '%s\n' % '\n'.join(error_string)
235 def get_counter(self, name):
236 '''Alternative call to __getitem__'''
237 return self.__getitem__(name)
239 def get_err_counter(self, name):
240 '''Return a single value (sum of all threads)'''
241 if not self.connected:
243 return sum(self.directory[name].get_counter(self))
245 def ls(self, patterns):
246 '''Returns list of counters matching pattern'''
247 # pylint: disable=invalid-name
248 if not self.connected:
250 if not isinstance(patterns, list):
251 patterns = [patterns]
252 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)]
256 def dump(self, counters):
257 '''Given a list of counters return a dictionary of results'''
258 if not self.connected:
262 result[cnt] = self.__getitem__(cnt)
266 '''Stat segment optimistic locking'''
268 def __init__(self, stats):
273 acquired = self.acquire(blocking=True)
274 assert acquired, "Lock wasn't acquired, but blocking=True"
277 def __exit__(self, exc_type=None, exc_value=None, traceback=None):
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
284 start = time.monotonic()
285 while self.stats.in_progress:
289 if start + time.monotonic() > timeout:
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')
302 class StatsCombinedList(list):
303 '''Column slicing for Combined counters list'''
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])
311 class CombinedList(list):
312 '''Combined Counters 2-dimensional by thread by index of packets/octets'''
315 '''Return column (2nd dimension). Packets for all threads'''
316 return [pair[0] for pair in self]
319 '''Return column (2nd dimension). Octets for all threads'''
320 return [pair[1] for pair in self]
322 def sum_packets(self):
323 '''Return column (2nd dimension). Sum of all packets for all threads'''
324 return sum(self.packets())
326 def sum_octets(self):
327 '''Return column (2nd dimension). Sum of all octets for all threads'''
328 return sum(self.octets())
330 class StatsTuple(tuple):
331 '''A Combined vector tuple (packets, octets)'''
332 def __init__(self, data):
333 self.dictionary = {'packets': data[0], 'bytes': data[1]}
337 return dict.__repr__(self.dictionary)
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)
346 class StatsSimpleList(list):
347 '''Simple Counters 2-dimensional by thread by index of packets'''
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])
355 class SimpleList(list):
363 '''An individual stats entry'''
364 # pylint: disable=unused-argument,no-self-use
366 def __init__(self, stattype, statvalue):
368 self.value = statvalue
371 self.function = self.scalar
373 self.function = self.simple
375 self.function = self.combined
377 self.function = self.error
379 self.function = self.name
381 self.function = self.illegal
383 def illegal(self, stats):
384 '''Invalid or unknown counter type'''
387 def scalar(self, stats):
391 def simple(self, stats):
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)
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)
407 def error(self, stats):
409 counter = SimpleList()
410 for clist in stats.error_vectors:
411 counter.append(clist[self.value])
414 def name(self, stats):
417 for name in StatsVector(stats, self.value, 'P'):
418 counter.append(get_string(stats, name[0]))
421 def get_counter(self, stats):
422 '''Return a list of counters'''
423 return self.function(stats)
425 class TestStats(unittest.TestCase):
426 '''Basic statseg tests'''
429 '''Connect to statseg'''
430 self.stat = VPPStats()
432 self.profile = cProfile.Profile()
433 self.profile.enable()
436 '''Disconnect from statseg'''
437 self.stat.disconnect()
438 profile = Stats(self.profile)
440 profile.sort_stats('cumtime')
441 profile.print_stats()
444 def test_counters(self):
445 '''Test access to statseg'''
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'))
460 def test_column(self):
461 '''Test column slicing'''
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'])
476 def test_error(self):
477 '''Test the error vector'''
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())
484 def test_nat44(self):
485 '''Test the nat counters'''
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())
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)
495 print('Looking up sys node')
496 directory = self.stat.ls(["^/sys/node"])
497 print('Dumping sys node')
498 data = self.stat.dump(directory)
500 directory = self.stat.ls(["^/foobar"])
501 data = self.stat.dump(directory)
504 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'])
511 if __name__ == '__main__':
513 from pstats import Stats