Pulled in latest vpp_stats.py from upstream after https://gerrit.fd.io/r/c/vpp/+/35640

This commit is contained in:
Pim van Pelt
2022-04-01 13:10:17 +00:00
parent 968c0abe2f
commit c9233749bc
2 changed files with 52 additions and 128 deletions

View File

@ -49,9 +49,7 @@ class MyAgent(agentx.Agent):
self.logger.info("Connecting to VPP Stats Segment") self.logger.info("Connecting to VPP Stats Segment")
vppstat = VPPStats(socketname='/run/vpp/stats.sock', timeout=2) vppstat = VPPStats(socketname='/run/vpp/stats.sock', timeout=2)
if not vppstat.connect(): vppstat.connect()
self.logger.error("Can't connect to VPP Stats API, bailing")
return False
vpp = VPPApi(clientname='vpp-snmp-agent') vpp = VPPApi(clientname='vpp-snmp-agent')
if not vpp.connect(): if not vpp.connect():

View File

@ -40,26 +40,32 @@ from struct import Struct
import time import time
import re import re
def recv_fd(sock):
'''Get file descriptor for memory map'''
fds = array.array("i") # Array of ints
_, ancdata, _, _ = sock.recvmsg(0, socket.CMSG_LEN(4))
for cmsg_level, cmsg_type, cmsg_data in ancdata:
if cmsg_level == socket.SOL_SOCKET and cmsg_type == socket.SCM_RIGHTS:
fds.frombytes(cmsg_data[:len(cmsg_data) - (len(cmsg_data) % fds.itemsize)])
return list(fds)[0]
VEC_LEN_FMT = Struct('I') VEC_LEN_FMT = Struct('I')
def get_vec_len(stats, vector_offset): def get_vec_len(stats, vector_offset):
'''Equivalent to VPP vec_len()''' '''Equivalent to VPP vec_len()'''
return VEC_LEN_FMT.unpack_from(stats.statseg, vector_offset - 8)[0] return VEC_LEN_FMT.unpack_from(stats.statseg, vector_offset - 8)[0]
def get_string(stats, ptr): def get_string(stats, ptr):
'''Get a string from a VPP vector''' '''Get a string from a VPP vector'''
namevector = ptr - stats.base namevector = ptr - stats.base
namevectorlen = get_vec_len(stats, namevector) namevectorlen = get_vec_len(stats, namevector)
if namevector + namevectorlen >= stats.size: if namevector + namevectorlen >= stats.size:
raise IOError('String overruns stats segment') raise IOError('String overruns stats segment')
return stats.statseg[namevector:namevector + namevectorlen - return stats.statseg[namevector:namevector+namevectorlen-1].decode('ascii')
1].decode('ascii')
class StatsVector: class StatsVector:
'''A class representing a VPP vector''' '''A class representing a VPP vector'''
def __init__(self, stats, ptr, fmt): def __init__(self, stats, ptr, fmt):
self.vec_start = ptr - stats.base self.vec_start = ptr - stats.base
self.vec_len = get_vec_len(stats, ptr - stats.base) self.vec_len = get_vec_len(stats, ptr - stats.base)
@ -74,8 +80,7 @@ class StatsVector:
def __iter__(self): def __iter__(self):
with self.stats.lock: with self.stats.lock:
return self.struct.iter_unpack( return self.struct.iter_unpack(self.statseg[self.vec_start:self.vec_start +
self.statseg[self.vec_start:self.vec_start +
self.elementsize*self.vec_len]) self.elementsize*self.vec_len])
def __getitem__(self, index): def __getitem__(self, index):
@ -83,12 +88,10 @@ class StatsVector:
raise IOError('Index beyond end of vector') raise IOError('Index beyond end of vector')
with self.stats.lock: with self.stats.lock:
if self.fmtlen == 1: if self.fmtlen == 1:
return self.struct.unpack_from( return self.struct.unpack_from(self.statseg, self.vec_start +
self.statseg, (index * self.elementsize))[0]
self.vec_start + (index * self.elementsize))[0] return self.struct.unpack_from(self.statseg, self.vec_start +
return self.struct.unpack_from( (index * self.elementsize))
self.statseg, self.vec_start + (index * self.elementsize))
class VPPStats(): class VPPStats():
'''Main class implementing Python access to the VPP statistics segment''' '''Main class implementing Python access to the VPP statistics segment'''
@ -104,43 +107,29 @@ class VPPStats():
self.connected = False self.connected = False
self.size = 0 self.size = 0
self.last_epoch = 0 self.last_epoch = 0
self.error_vectors = 0
self.statseg = 0 self.statseg = 0
def connect(self): def connect(self):
'''Connect to stats segment''' '''Connect to stats segment'''
if self.connected: if self.connected:
return True return
try:
sock = socket.socket(socket.AF_UNIX, socket.SOCK_SEQPACKET) sock = socket.socket(socket.AF_UNIX, socket.SOCK_SEQPACKET)
sock.connect(self.socketname) sock.connect(self.socketname)
except:
return False
# Get file descriptor for memory map mfd = recv_fd(sock)
fds = array.array("i") # Array of ints
_, ancdata, _, _ = sock.recvmsg(0, socket.CMSG_LEN(4))
for cmsg_level, cmsg_type, cmsg_data in ancdata:
if cmsg_level == socket.SOL_SOCKET and cmsg_type == socket.SCM_RIGHTS:
fds.frombytes(cmsg_data[:len(cmsg_data) -
(len(cmsg_data) % fds.itemsize)])
mfd = list(fds)[0]
sock.close() sock.close()
stat_result = os.fstat(mfd) stat_result = os.fstat(mfd)
self.statseg = mmap.mmap(mfd, stat_result.st_size, mmap.PROT_READ, self.statseg = mmap.mmap(mfd, stat_result.st_size, mmap.PROT_READ, mmap.MAP_SHARED)
mmap.MAP_SHARED)
os.close(mfd) os.close(mfd)
self.size = stat_result.st_size self.size = stat_result.st_size
if self.version != 2: if self.version != 2:
raise Exception('Incompatbile stat segment version {}'.format( raise Exception('Incompatbile stat segment version {}'
self.version)) .format(self.version))
return False
self.refresh() self.refresh()
self.connected = True self.connected = True
return True
def disconnect(self): def disconnect(self):
'''Disconnect from stats segment''' '''Disconnect from stats segment'''
@ -173,11 +162,6 @@ class VPPStats():
'''Get pointer of directory vector''' '''Get pointer of directory vector'''
return self.shared_headerfmt.unpack_from(self.statseg)[4] return self.shared_headerfmt.unpack_from(self.statseg)[4]
@property
def error_vector(self):
'''Get pointer of error vector'''
return self.shared_headerfmt.unpack_from(self.statseg)[5]
elementfmt = 'IQ128s' elementfmt = 'IQ128s'
def refresh(self, blocking=True): def refresh(self, blocking=True):
@ -188,21 +172,13 @@ class VPPStats():
try: try:
with self.lock: with self.lock:
self.last_epoch = self.epoch self.last_epoch = self.epoch
for i, direntry in enumerate( for i, direntry in enumerate(StatsVector(self, self.directory_vector, self.elementfmt)):
StatsVector(self, self.directory_vector,
self.elementfmt)):
path_raw = direntry[2].find(b'\x00') path_raw = direntry[2].find(b'\x00')
path = direntry[2][:path_raw].decode('ascii') path = direntry[2][:path_raw].decode('ascii')
directory[path] = StatsEntry(direntry[0], direntry[1]) directory[path] = StatsEntry(direntry[0], direntry[1])
directory_by_idx[i] = path directory_by_idx[i] = path
self.directory = directory self.directory = directory
self.directory_by_idx = directory_by_idx self.directory_by_idx = directory_by_idx
# Cache the error index vectors
self.error_vectors = []
for threads in StatsVector(self, self.error_vector, 'P'):
self.error_vectors.append(
StatsVector(self, threads[0], 'Q'))
return return
except IOError: except IOError:
if not blocking: if not blocking:
@ -224,32 +200,23 @@ class VPPStats():
def __iter__(self): def __iter__(self):
return iter(self.directory.items()) return iter(self.directory.items())
def set_errors(self, blocking=True): def set_errors(self, blocking=True):
'''Return dictionary of error counters > 0''' '''Return dictionary of error counters > 0'''
if not self.connected: if not self.connected:
self.connect() self.connect()
errors = { errors = {k: v for k, v in self.directory.items()
k: v if k.startswith("/err/")}
for k, v in self.directory.items() if k.startswith("/err/")
}
result = {} result = {}
while True: for k in errors:
try: try:
if self.last_epoch != self.epoch: total = self[k].sum()
self.refresh(blocking)
with self.lock:
for k, entry in errors.items():
total = 0
i = entry.value
for per_thread in self.error_vectors:
total += per_thread[i]
if total: if total:
result[k] = total result[k] = total
except KeyError:
pass
return result return result
except IOError:
if not blocking:
raise
def set_errors_str(self, blocking=True): def set_errors_str(self, blocking=True):
'''Return all errors counters > 0 pretty printed''' '''Return all errors counters > 0 pretty printed'''
@ -264,19 +231,8 @@ class VPPStats():
return self.__getitem__(name, blocking) return self.__getitem__(name, blocking)
def get_err_counter(self, name, blocking=True): def get_err_counter(self, name, blocking=True):
'''Return a single value (sum of all threads)''' '''Alternative call to __getitem__'''
if not self.connected: return self.__getitem__(name, blocking).sum()
self.connect()
if name.startswith("/err/"):
while True:
try:
if self.last_epoch != self.epoch:
self.refresh(blocking)
with self.lock:
return sum(self.directory[name].get_counter(self))
except IOError:
if not blocking:
raise
def ls(self, patterns): def ls(self, patterns):
'''Returns list of counters matching pattern''' '''Returns list of counters matching pattern'''
@ -286,10 +242,11 @@ class VPPStats():
if not isinstance(patterns, list): if not isinstance(patterns, list):
patterns = [patterns] patterns = [patterns]
regex = [re.compile(i) for i in patterns] regex = [re.compile(i) for i in patterns]
return [ if self.last_epoch != self.epoch:
k for k, v in self.directory.items() if any( self.refresh()
re.match(pattern, k) for pattern in regex)
] return [k for k, v in self.directory.items()
if any(re.match(pattern, k) for pattern in regex)]
def dump(self, counters, blocking=True): def dump(self, counters, blocking=True):
'''Given a list of counters return a dictionary of results''' '''Given a list of counters return a dictionary of results'''
@ -300,9 +257,9 @@ class VPPStats():
result[cnt] = self.__getitem__(cnt,blocking) result[cnt] = self.__getitem__(cnt,blocking)
return result return result
class StatsLock(): class StatsLock():
'''Stat segment optimistic locking''' '''Stat segment optimistic locking'''
def __init__(self, stats): def __init__(self, stats):
self.stats = stats self.stats = stats
self.epoch = 0 self.epoch = 0
@ -339,15 +296,16 @@ class StatsLock():
class StatsCombinedList(list): class StatsCombinedList(list):
'''Column slicing for Combined counters list''' '''Column slicing for Combined counters list'''
def __getitem__(self, item): def __getitem__(self, item):
'''Supports partial numpy style 2d support. Slice by column [:,1]''' '''Supports partial numpy style 2d support. Slice by column [:,1]'''
if isinstance(item, int): if isinstance(item, int):
return list.__getitem__(self, item) return list.__getitem__(self, item)
return CombinedList([row[item[1]] for row in self]) return CombinedList([row[item[1]] for row in self])
class CombinedList(list): class CombinedList(list):
'''Combined Counters 2-dimensional by thread by index of packets/octets''' '''Combined Counters 2-dimensional by thread by index of packets/octets'''
def packets(self): def packets(self):
'''Return column (2nd dimension). Packets for all threads''' '''Return column (2nd dimension). Packets for all threads'''
return [pair[0] for pair in self] return [pair[0] for pair in self]
@ -364,7 +322,6 @@ class CombinedList(list):
'''Return column (2nd dimension). Sum of all octets for all threads''' '''Return column (2nd dimension). Sum of all octets for all threads'''
return sum(self.octets()) return sum(self.octets())
class StatsTuple(tuple): class StatsTuple(tuple):
'''A Combined vector tuple (packets, octets)''' '''A Combined vector tuple (packets, octets)'''
def __init__(self, data): def __init__(self, data):
@ -381,26 +338,24 @@ class StatsTuple(tuple):
return tuple.__getitem__(self, 0) return tuple.__getitem__(self, 0)
return tuple.__getitem__(self, 1) return tuple.__getitem__(self, 1)
class StatsSimpleList(list): class StatsSimpleList(list):
'''Simple Counters 2-dimensional by thread by index of packets''' '''Simple Counters 2-dimensional by thread by index of packets'''
def __getitem__(self, item): def __getitem__(self, item):
'''Supports partial numpy style 2d support. Slice by column [:,1]''' '''Supports partial numpy style 2d support. Slice by column [:,1]'''
if isinstance(item, int): if isinstance(item, int):
return list.__getitem__(self, item) return list.__getitem__(self, item)
return SimpleList([row[item[1]] for row in self]) return SimpleList([row[item[1]] for row in self])
class SimpleList(list): class SimpleList(list):
'''Simple counter''' '''Simple counter'''
def sum(self): def sum(self):
'''Sum the vector''' '''Sum the vector'''
return sum(self) return sum(self)
class StatsEntry(): class StatsEntry():
'''An individual stats entry''' '''An individual stats entry'''
# pylint: disable=unused-argument,no-self-use # pylint: disable=unused-argument,no-self-use
def __init__(self, stattype, statvalue): def __init__(self, stattype, statvalue):
@ -414,10 +369,8 @@ class StatsEntry():
elif stattype == 3: elif stattype == 3:
self.function = self.combined self.function = self.combined
elif stattype == 4: elif stattype == 4:
self.function = self.error
elif stattype == 5:
self.function = self.name self.function = self.name
elif stattype == 7: elif stattype == 6:
self.function = self.symlink self.function = self.symlink
else: else:
self.function = self.illegal self.function = self.illegal
@ -442,19 +395,10 @@ class StatsEntry():
'''Combined counter''' '''Combined counter'''
counter = StatsCombinedList() counter = StatsCombinedList()
for threads in StatsVector(stats, self.value, 'P'): for threads in StatsVector(stats, self.value, 'P'):
clist = [ clist = [StatsTuple(cnt) for cnt in StatsVector(stats, threads[0], 'QQ')]
StatsTuple(cnt) for cnt in StatsVector(stats, threads[0], 'QQ')
]
counter.append(clist) counter.append(clist)
return counter return counter
def error(self, stats):
'''Error counter'''
counter = SimpleList()
for clist in stats.error_vectors:
counter.append(clist[self.value])
return counter
def name(self, stats): def name(self, stats):
'''Name counter''' '''Name counter'''
counter = [] counter = []
@ -465,7 +409,6 @@ class StatsEntry():
SYMLINK_FMT1 = Struct('II') SYMLINK_FMT1 = Struct('II')
SYMLINK_FMT2 = Struct('Q') SYMLINK_FMT2 = Struct('Q')
def symlink(self, stats): def symlink(self, stats):
'''Symlink counter''' '''Symlink counter'''
b = self.SYMLINK_FMT2.pack(self.value) b = self.SYMLINK_FMT2.pack(self.value)
@ -477,20 +420,3 @@ class StatsEntry():
'''Return a list of counters''' '''Return a list of counters'''
if stats: if stats:
return self.function(stats) return self.function(stats)
#
# stat = VPPStats(socketname='/run/vpp/stats.sock', timeout=2)
# stat.connect()
# print('version ', stat.version)
# print('epoch ', stat.epoch)
# print('/if/names', stat['/if/names'])
#
# for x in range(10):
# idx=2
# print('/if/rx[%s] packets %u octets %u' % (stat['/if/names'][idx], stat['/if/rx'][:, idx].sum_packets(), stat['/if/rx'][:, idx].sum_octets()))
# print('/if/tx[%s] packets %u octets %u' % (stat['/if/names'][idx], stat['/if/tx'][:, idx].sum_packets(), stat['/if/tx'][:, idx].sum_octets()))
# print("")
# time.sleep(10)
#
# stat.disconnect()