blob: 0b1c701a43083bd568b49f549738ad9c560f7d6d [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:
Ole Troand640ae52021-05-14 14:00:50 +020072 raise IOError('String overruns stats segment')
Ole Troane66443c2021-03-18 11:12:01 +010073 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:
Ole Troand640ae52021-05-14 14:00:50 +020089 raise IOError('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:
Ole Troand640ae52021-05-14 14:00:50 +020098 raise IOError('Index beyond end of vector')
Ole Troane66443c2021-03-18 11:12:01 +010099 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
Ole Troane66443c2021-03-18 11:12:01 +0100120 self.statseg = 0
Paul Vinciguerrae090f4d2019-11-29 17:41:20 -0500121
122 def connect(self):
Ole Troane66443c2021-03-18 11:12:01 +0100123 '''Connect to stats segment'''
124 if self.connected:
125 return
126 sock = socket.socket(socket.AF_UNIX, socket.SOCK_SEQPACKET)
127 sock.connect(self.socketname)
Paul Vinciguerra56421092018-11-21 16:34:09 -0800128
Ole Troane66443c2021-03-18 11:12:01 +0100129 mfd = recv_fd(sock)
130 sock.close()
Paul Vinciguerra56421092018-11-21 16:34:09 -0800131
Ole Troane66443c2021-03-18 11:12:01 +0100132 stat_result = os.fstat(mfd)
133 self.statseg = mmap.mmap(mfd, stat_result.st_size, mmap.PROT_READ, mmap.MAP_SHARED)
Tianyu Li0e79bc12021-04-14 12:36:20 +0800134 os.close(mfd)
Ole Troan73202102018-08-31 00:29:48 +0200135
Ole Troane66443c2021-03-18 11:12:01 +0100136 self.size = stat_result.st_size
137 if self.version != 2:
138 raise Exception('Incompatbile stat segment version {}'
139 .format(self.version))
Ole Troan73202102018-08-31 00:29:48 +0200140
Ole Troane66443c2021-03-18 11:12:01 +0100141 self.refresh()
142 self.connected = True
Ole Troan233e4682019-05-16 15:01:34 +0200143
Ole Troan73202102018-08-31 00:29:48 +0200144 def disconnect(self):
Ole Troane66443c2021-03-18 11:12:01 +0100145 '''Disconnect from stats segment'''
146 if self.connected:
147 self.statseg.close()
Paul Vinciguerrae090f4d2019-11-29 17:41:20 -0500148 self.connected = False
Ole Troane66443c2021-03-18 11:12:01 +0100149
150 @property
151 def version(self):
152 '''Get version of stats segment'''
153 return self.shared_headerfmt.unpack_from(self.statseg)[0]
154
155 @property
156 def base(self):
157 '''Get base pointer of stats segment'''
158 return self.shared_headerfmt.unpack_from(self.statseg)[1]
159
160 @property
161 def epoch(self):
162 '''Get current epoch value from stats segment'''
163 return self.shared_headerfmt.unpack_from(self.statseg)[2]
164
165 @property
166 def in_progress(self):
167 '''Get value of in_progress from stats segment'''
168 return self.shared_headerfmt.unpack_from(self.statseg)[3]
169
170 @property
171 def directory_vector(self):
172 '''Get pointer of directory vector'''
173 return self.shared_headerfmt.unpack_from(self.statseg)[4]
174
Ole Troane66443c2021-03-18 11:12:01 +0100175 elementfmt = 'IQ128s'
176
Arthur de Kerhordb023802021-03-11 10:26:54 -0800177 def refresh(self, blocking=True):
Ole Troane66443c2021-03-18 11:12:01 +0100178 '''Refresh directory vector cache (epoch changed)'''
179 directory = {}
Arthur de Kerhordb023802021-03-11 10:26:54 -0800180 directory_by_idx = {}
181 while True:
182 try:
183 with self.lock:
Arthur de Kerhorc9ae8cf2021-06-24 19:39:44 +0200184 self.last_epoch = self.epoch
Arthur de Kerhordb023802021-03-11 10:26:54 -0800185 for i, direntry in enumerate(StatsVector(self, self.directory_vector, self.elementfmt)):
186 path_raw = direntry[2].find(b'\x00')
187 path = direntry[2][:path_raw].decode('ascii')
188 directory[path] = StatsEntry(direntry[0], direntry[1])
189 directory_by_idx[i] = path
Arthur de Kerhordb023802021-03-11 10:26:54 -0800190 self.directory = directory
191 self.directory_by_idx = directory_by_idx
Arthur de Kerhor905c1532021-06-25 18:03:24 +0200192 return
Arthur de Kerhordb023802021-03-11 10:26:54 -0800193 except IOError:
194 if not blocking:
195 raise
Ole Troane66443c2021-03-18 11:12:01 +0100196
Arthur de Kerhordb023802021-03-11 10:26:54 -0800197 def __getitem__(self, item, blocking=True):
Ole Troane66443c2021-03-18 11:12:01 +0100198 if not self.connected:
199 self.connect()
Arthur de Kerhordb023802021-03-11 10:26:54 -0800200 while True:
201 try:
202 if self.last_epoch != self.epoch:
203 self.refresh(blocking)
204 with self.lock:
Arthur de Kerhor905c1532021-06-25 18:03:24 +0200205 return self.directory[item].get_counter(self)
Arthur de Kerhordb023802021-03-11 10:26:54 -0800206 except IOError:
207 if not blocking:
208 raise
Ole Troane66443c2021-03-18 11:12:01 +0100209
210 def __iter__(self):
211 return iter(self.directory.items())
Ole Troan73202102018-08-31 00:29:48 +0200212
Damjan Marion66c85832022-03-14 13:04:38 +0100213
Arthur de Kerhordb023802021-03-11 10:26:54 -0800214 def set_errors(self, blocking=True):
Ole Troane66443c2021-03-18 11:12:01 +0100215 '''Return dictionary of error counters > 0'''
216 if not self.connected:
217 self.connect()
Paul Vinciguerra56421092018-11-21 16:34:09 -0800218
Damjan Marion66c85832022-03-14 13:04:38 +0100219 errors = {k: v for k, v in self.directory.items()
220 if k.startswith("/err/")}
Ole Troane66443c2021-03-18 11:12:01 +0100221 result = {}
Damjan Marion66c85832022-03-14 13:04:38 +0100222 for k in errors:
Arthur de Kerhordb023802021-03-11 10:26:54 -0800223 try:
Damjan Marion66c85832022-03-14 13:04:38 +0100224 total = self[k].sum()
225 if total:
226 result[k] = total
227 except KeyError:
228 pass
229 return result
Ole Troan73202102018-08-31 00:29:48 +0200230
Arthur de Kerhordb023802021-03-11 10:26:54 -0800231 def set_errors_str(self, blocking=True):
Ole Troan73202102018-08-31 00:29:48 +0200232 '''Return all errors counters > 0 pretty printed'''
Ole Troane66443c2021-03-18 11:12:01 +0100233 error_string = ['ERRORS:']
Arthur de Kerhordb023802021-03-11 10:26:54 -0800234 error_counters = self.set_errors(blocking)
Ole Troan73202102018-08-31 00:29:48 +0200235 for k in sorted(error_counters):
Ole Troane66443c2021-03-18 11:12:01 +0100236 error_string.append('{:<60}{:>10}'.format(k, error_counters[k]))
237 return '%s\n' % '\n'.join(error_string)
238
Arthur de Kerhordb023802021-03-11 10:26:54 -0800239 def get_counter(self, name, blocking=True):
Ole Troane66443c2021-03-18 11:12:01 +0100240 '''Alternative call to __getitem__'''
Arthur de Kerhordb023802021-03-11 10:26:54 -0800241 return self.__getitem__(name, blocking)
Ole Troane66443c2021-03-18 11:12:01 +0100242
Arthur de Kerhordb023802021-03-11 10:26:54 -0800243 def get_err_counter(self, name, blocking=True):
Damjan Marion66c85832022-03-14 13:04:38 +0100244 '''Alternative call to __getitem__'''
245 return self.__getitem__(name, blocking).sum()
Ole Troane66443c2021-03-18 11:12:01 +0100246
247 def ls(self, patterns):
248 '''Returns list of counters matching pattern'''
249 # pylint: disable=invalid-name
250 if not self.connected:
251 self.connect()
Ole Troan3daca3f2021-03-29 21:12:53 +0200252 if not isinstance(patterns, list):
253 patterns = [patterns]
Ole Troane66443c2021-03-18 11:12:01 +0100254 regex = [re.compile(i) for i in patterns]
Ole Troan43eb6082021-08-31 10:47:45 +0200255 if self.last_epoch != self.epoch:
256 self.refresh()
257
Ole Troane66443c2021-03-18 11:12:01 +0100258 return [k for k, v in self.directory.items()
259 if any(re.match(pattern, k) for pattern in regex)]
260
Arthur de Kerhordb023802021-03-11 10:26:54 -0800261 def dump(self, counters, blocking=True):
Ole Troane66443c2021-03-18 11:12:01 +0100262 '''Given a list of counters return a dictionary of results'''
263 if not self.connected:
264 self.connect()
265 result = {}
266 for cnt in counters:
Arthur de Kerhordb023802021-03-11 10:26:54 -0800267 result[cnt] = self.__getitem__(cnt,blocking)
Ole Troane66443c2021-03-18 11:12:01 +0100268 return result
269
270class StatsLock():
271 '''Stat segment optimistic locking'''
272
273 def __init__(self, stats):
274 self.stats = stats
275 self.epoch = 0
276
277 def __enter__(self):
278 acquired = self.acquire(blocking=True)
279 assert acquired, "Lock wasn't acquired, but blocking=True"
280 return self
281
282 def __exit__(self, exc_type=None, exc_value=None, traceback=None):
283 self.release()
284
285 def acquire(self, blocking=True, timeout=-1):
286 '''Acquire the lock. Await in progress to go false. Record epoch.'''
287 self.epoch = self.stats.epoch
288 if timeout > 0:
289 start = time.monotonic()
290 while self.stats.in_progress:
291 if not blocking:
292 time.sleep(0.01)
293 if timeout > 0:
294 if start + time.monotonic() > timeout:
295 return False
296 return True
297
298 def release(self):
299 '''Check if data read while locked is valid'''
300 if self.stats.in_progress or self.stats.epoch != self.epoch:
301 raise IOError('Optimistic lock failed, retry')
302
303 def locked(self):
304 '''Not used'''
305
306
307class StatsCombinedList(list):
308 '''Column slicing for Combined counters list'''
309
310 def __getitem__(self, item):
311 '''Supports partial numpy style 2d support. Slice by column [:,1]'''
312 if isinstance(item, int):
313 return list.__getitem__(self, item)
314 return CombinedList([row[item[1]] for row in self])
315
316class CombinedList(list):
317 '''Combined Counters 2-dimensional by thread by index of packets/octets'''
318
319 def packets(self):
320 '''Return column (2nd dimension). Packets for all threads'''
321 return [pair[0] for pair in self]
322
323 def octets(self):
324 '''Return column (2nd dimension). Octets for all threads'''
325 return [pair[1] for pair in self]
326
327 def sum_packets(self):
328 '''Return column (2nd dimension). Sum of all packets for all threads'''
329 return sum(self.packets())
330
331 def sum_octets(self):
332 '''Return column (2nd dimension). Sum of all octets for all threads'''
333 return sum(self.octets())
334
335class StatsTuple(tuple):
336 '''A Combined vector tuple (packets, octets)'''
337 def __init__(self, data):
338 self.dictionary = {'packets': data[0], 'bytes': data[1]}
339 super().__init__()
340
341 def __repr__(self):
342 return dict.__repr__(self.dictionary)
343
344 def __getitem__(self, item):
345 if isinstance(item, int):
346 return tuple.__getitem__(self, item)
347 if item == 'packets':
348 return tuple.__getitem__(self, 0)
349 return tuple.__getitem__(self, 1)
350
351class StatsSimpleList(list):
352 '''Simple Counters 2-dimensional by thread by index of packets'''
353
354 def __getitem__(self, item):
355 '''Supports partial numpy style 2d support. Slice by column [:,1]'''
356 if isinstance(item, int):
357 return list.__getitem__(self, item)
358 return SimpleList([row[item[1]] for row in self])
359
360class SimpleList(list):
361 '''Simple counter'''
362
363 def sum(self):
364 '''Sum the vector'''
365 return sum(self)
366
367class StatsEntry():
368 '''An individual stats entry'''
369 # pylint: disable=unused-argument,no-self-use
370
371 def __init__(self, stattype, statvalue):
372 self.type = stattype
373 self.value = statvalue
374
375 if stattype == 1:
376 self.function = self.scalar
377 elif stattype == 2:
378 self.function = self.simple
379 elif stattype == 3:
380 self.function = self.combined
381 elif stattype == 4:
Ole Troane66443c2021-03-18 11:12:01 +0100382 self.function = self.name
Damjan Marion66c85832022-03-14 13:04:38 +0100383 elif stattype == 6:
Arthur de Kerhordb023802021-03-11 10:26:54 -0800384 self.function = self.symlink
Ole Troane66443c2021-03-18 11:12:01 +0100385 else:
386 self.function = self.illegal
387
388 def illegal(self, stats):
389 '''Invalid or unknown counter type'''
390 return None
391
392 def scalar(self, stats):
393 '''Scalar counter'''
394 return self.value
395
396 def simple(self, stats):
397 '''Simple counter'''
398 counter = StatsSimpleList()
399 for threads in StatsVector(stats, self.value, 'P'):
400 clist = [v[0] for v in StatsVector(stats, threads[0], 'Q')]
401 counter.append(clist)
402 return counter
403
404 def combined(self, stats):
405 '''Combined counter'''
406 counter = StatsCombinedList()
407 for threads in StatsVector(stats, self.value, 'P'):
408 clist = [StatsTuple(cnt) for cnt in StatsVector(stats, threads[0], 'QQ')]
409 counter.append(clist)
410 return counter
411
Ole Troane66443c2021-03-18 11:12:01 +0100412 def name(self, stats):
413 '''Name counter'''
414 counter = []
415 for name in StatsVector(stats, self.value, 'P'):
Arthur de Kerhordb023802021-03-11 10:26:54 -0800416 if name[0]:
417 counter.append(get_string(stats, name[0]))
Ole Troane66443c2021-03-18 11:12:01 +0100418 return counter
419
Arthur de Kerhordb023802021-03-11 10:26:54 -0800420 SYMLINK_FMT1 = Struct('II')
421 SYMLINK_FMT2 = Struct('Q')
422 def symlink(self, stats):
423 '''Symlink counter'''
424 b = self.SYMLINK_FMT2.pack(self.value)
425 index1, index2 = self.SYMLINK_FMT1.unpack(b)
426 name = stats.directory_by_idx[index1]
427 return stats[name][:,index2]
428
Ole Troane66443c2021-03-18 11:12:01 +0100429 def get_counter(self, stats):
430 '''Return a list of counters'''
Arthur de Kerhordb023802021-03-11 10:26:54 -0800431 if stats:
432 return self.function(stats)
Ole Troane66443c2021-03-18 11:12:01 +0100433
434class TestStats(unittest.TestCase):
435 '''Basic statseg tests'''
436
437 def setUp(self):
438 '''Connect to statseg'''
439 self.stat = VPPStats()
440 self.stat.connect()
441 self.profile = cProfile.Profile()
442 self.profile.enable()
443
444 def tearDown(self):
445 '''Disconnect from statseg'''
446 self.stat.disconnect()
447 profile = Stats(self.profile)
448 profile.strip_dirs()
449 profile.sort_stats('cumtime')
450 profile.print_stats()
451 print("\n--->>>")
452
453 def test_counters(self):
454 '''Test access to statseg'''
455
456 print('/err/abf-input-ip4/missed', self.stat['/err/abf-input-ip4/missed'])
457 print('/sys/heartbeat', self.stat['/sys/heartbeat'])
458 print('/if/names', self.stat['/if/names'])
459 print('/if/rx-miss', self.stat['/if/rx-miss'])
460 print('/if/rx-miss', self.stat['/if/rx-miss'][1])
461 print('/nat44-ed/out2in/slowpath/drops', self.stat['/nat44-ed/out2in/slowpath/drops'])
Ole Troane66443c2021-03-18 11:12:01 +0100462 with self.assertRaises(KeyError):
463 print('NO SUCH COUNTER', self.stat['foobar'])
464 print('/if/rx', self.stat.get_counter('/if/rx'))
Damjan Marion66c85832022-03-14 13:04:38 +0100465 print('/err/ethernet-input/no_error',
466 self.stat.get_counter('/err/ethernet-input/no_error'))
Ole Troane66443c2021-03-18 11:12:01 +0100467
468 def test_column(self):
469 '''Test column slicing'''
470
471 print('/if/rx-miss', self.stat['/if/rx-miss'])
472 print('/if/rx', self.stat['/if/rx']) # All interfaces for thread #1
473 print('/if/rx thread #1', self.stat['/if/rx'][0]) # All interfaces for thread #1
474 print('/if/rx thread #1, interface #1',
475 self.stat['/if/rx'][0][1]) # All interfaces for thread #1
476 print('/if/rx if_index #1', self.stat['/if/rx'][:, 1])
477 print('/if/rx if_index #1 packets', self.stat['/if/rx'][:, 1].packets())
478 print('/if/rx if_index #1 packets', self.stat['/if/rx'][:, 1].sum_packets())
479 print('/if/rx if_index #1 packets', self.stat['/if/rx'][:, 1].octets())
480 print('/if/rx-miss', self.stat['/if/rx-miss'])
481 print('/if/rx-miss if_index #1 packets', self.stat['/if/rx-miss'][:, 1].sum())
482 print('/if/rx if_index #1 packets', self.stat['/if/rx'][0][1]['packets'])
483
Ole Troane66443c2021-03-18 11:12:01 +0100484 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
Arthur de Kerhordb023802021-03-11 10:26:54 -0800511 def test_symlink(self):
512 '''Symbolic links'''
513 print('/interface/local0/rx', self.stat['/interfaces/local0/rx'])
514 print('/sys/nodes/unix-epoll-input', self.stat['/nodes/unix-epoll-input/calls'])
515
Ole Troane66443c2021-03-18 11:12:01 +0100516if __name__ == '__main__':
517 import cProfile
518 from pstats import Stats
519
520 unittest.main()