stats: remove offsets on vpp side
[vpp.git] / src / vpp-api / python / vpp_papi / vpp_stats.py
1 #!/usr/bin/env python3
2
3 from __future__ import print_function
4 from cffi import FFI
5 import time
6
7 ffi = FFI()
8 ffi.cdef("""
9 typedef uint64_t counter_t;
10 typedef struct {
11   counter_t packets;
12   counter_t bytes;
13 } vlib_counter_t;
14
15 typedef enum {
16   STAT_DIR_TYPE_ILLEGAL = 0,
17   STAT_DIR_TYPE_SCALAR_INDEX,
18   STAT_DIR_TYPE_COUNTER_VECTOR_SIMPLE,
19   STAT_DIR_TYPE_COUNTER_VECTOR_COMBINED,
20   STAT_DIR_TYPE_ERROR_INDEX,
21   STAT_DIR_TYPE_NAME_VECTOR,
22 } stat_directory_type_t;
23
24 typedef struct
25 {
26   stat_directory_type_t type;
27   union {
28     uint64_t index;
29     uint64_t value;
30     uint64_t *data;
31   };
32   char name[128]; // TODO change this to pointer to "somewhere"
33 } stat_segment_directory_entry_t;
34
35 typedef struct
36 {
37   char *name;
38   stat_directory_type_t type;
39   union
40   {
41     double scalar_value;
42     counter_t *error_vector;
43     counter_t **simple_counter_vec;
44     vlib_counter_t **combined_counter_vec;
45     uint8_t **name_vector;
46   };
47 } stat_segment_data_t;
48
49 typedef struct
50 {
51   uint64_t version;
52   void *base;
53   uint64_t epoch;
54   uint64_t in_progress;
55   stat_segment_directory_entry_t *directory_vector;
56   uint64_t **error_vector;
57 } stat_segment_shared_header_t;
58
59 typedef struct
60 {
61   uint64_t current_epoch;
62   stat_segment_shared_header_t *shared_header;
63   stat_segment_directory_entry_t *directory_vector;
64   ssize_t memory_size;
65 } stat_client_main_t;
66
67 stat_client_main_t * stat_client_get(void);
68 void stat_client_free(stat_client_main_t * sm);
69 int stat_segment_connect_r (char *socket_name, stat_client_main_t * sm);
70 int stat_segment_connect (char *socket_name);
71 void stat_segment_disconnect_r (stat_client_main_t * sm);
72 void stat_segment_disconnect (void);
73
74 uint32_t *stat_segment_ls_r (uint8_t ** patterns, stat_client_main_t * sm);
75 uint32_t *stat_segment_ls (uint8_t ** pattern);
76 stat_segment_data_t *stat_segment_dump_r (uint32_t * stats,
77                                           stat_client_main_t * sm);
78 stat_segment_data_t *stat_segment_dump (uint32_t * counter_vec);
79 void stat_segment_data_free (stat_segment_data_t * res);
80
81 double stat_segment_heartbeat_r (stat_client_main_t * sm);
82 int stat_segment_vec_len(void *vec);
83 uint8_t **stat_segment_string_vector(uint8_t **string_vector, char *string);
84 char *stat_segment_index_to_name_r (uint32_t index, stat_client_main_t * sm);
85 uint64_t stat_segment_version(void);
86 uint64_t stat_segment_version_r(stat_client_main_t *sm);
87 void free(void *ptr);
88 """)  # noqa: E501
89
90
91 # Utility functions
92 def make_string_vector(api, strings):
93     vec = ffi.NULL
94     if type(strings) is not list:
95         strings = [strings]
96     for s in strings:
97         vec = api.stat_segment_string_vector(vec, ffi.new("char []",
98                                                           s.encode('utf-8')))
99     return vec
100
101
102 def make_string_list(api, vec):
103     vec_len = api.stat_segment_vec_len(vec)
104     return [ffi.string(vec[i]) for i in range(vec_len)]
105
106
107 # 2-dimensonal array of thread, index
108 def simple_counter_vec_list(api, e):
109     vec = []
110     for thread in range(api.stat_segment_vec_len(e)):
111         len_interfaces = api.stat_segment_vec_len(e[thread])
112         if_per_thread = [e[thread][interfaces]
113                          for interfaces in range(len_interfaces)]
114         vec.append(if_per_thread)
115     return vec
116
117
118 def vlib_counter_dict(c):
119     return {'packets': c.packets,
120             'bytes': c.bytes}
121
122
123 def combined_counter_vec_list(api, e):
124     vec = []
125     for thread in range(api.stat_segment_vec_len(e)):
126         len_interfaces = api.stat_segment_vec_len(e[thread])
127         if_per_thread = [vlib_counter_dict(e[thread][interfaces])
128                          for interfaces in range(len_interfaces)]
129         vec.append(if_per_thread)
130     return vec
131
132
133 def error_vec_list(api, e):
134     vec = []
135     for thread in range(api.stat_segment_vec_len(e)):
136         vec.append(e[thread])
137     return vec
138
139
140 def name_vec_list(api, e):
141     return [ffi.string(e[i]).decode('utf-8') for i in
142             range(api.stat_segment_vec_len(e)) if e[i] != ffi.NULL]
143
144
145 def stat_entry_to_python(api, e):
146     # Scalar index
147     if e.type == 1:
148         return e.scalar_value
149     if e.type == 2:
150         return simple_counter_vec_list(api, e.simple_counter_vec)
151     if e.type == 3:
152         return combined_counter_vec_list(api, e.combined_counter_vec)
153     if e.type == 4:
154         return error_vec_list(api, e.error_vector)
155     if e.type == 5:
156         return name_vec_list(api, e.name_vector)
157     raise NotImplementedError()
158
159
160 class VPPStatsIOError(IOError):
161     message = "Stat segment client connection returned: " \
162               "%(retval)s %(strerror)s."
163
164     strerror = {-1: "Stat client couldn't open socket",
165                 -2: "Stat client socket open but couldn't connect",
166                 -3: "Receiving file descriptor failed",
167                 -4: "mmap fstat failed",
168                 -5: "mmap map failed"
169                 }
170
171     def __init__(self, message=None, **kwargs):
172         if 'retval' in kwargs:
173             self.retval = kwargs['retval']
174             kwargs['strerror'] = self.strerror[int(self.retval)]
175
176         if not message:
177             try:
178                 message = self.message % kwargs
179             except Exception:
180                 message = self.message
181         else:
182             message = message % kwargs
183
184         super(VPPStatsIOError, self).__init__(message)
185
186
187 class VPPStatsClientLoadError(RuntimeError):
188     pass
189
190
191 class VPPStats(object):
192     VPPStatsIOError = VPPStatsIOError
193
194     default_socketname = '/run/vpp/stats.sock'
195     sharedlib_name = 'libvppapiclient.so'
196
197     def __init__(self, socketname=default_socketname, timeout=10):
198         self.socketname = socketname
199         self.timeout = timeout
200         self.connected = False
201         try:
202             self.api = ffi.dlopen(VPPStats.sharedlib_name)
203         except Exception:
204             raise VPPStatsClientLoadError("Could not open: %s" %
205                                           VPPStats.sharedlib_name)
206
207     def connect(self):
208         self.client = self.api.stat_client_get()
209
210         poll_end_time = time.time() + self.timeout
211         while time.time() < poll_end_time:
212             rv = self.api.stat_segment_connect_r(
213                 self.socketname.encode('utf-8'), self.client)
214             # Break out if success or any other error than "no such file"
215             # (indicating that VPP hasn't started yet)
216             if rv == 0 or ffi.errno != 2:
217                 self.connected = True
218                 break
219
220         if rv != 0:
221             raise VPPStatsIOError(retval=rv)
222
223     def heartbeat(self):
224         if not self.connected:
225             self.connect()
226         return self.api.stat_segment_heartbeat_r(self.client)
227
228     def ls(self, patterns):
229         if not self.connected:
230             self.connect()
231         return self.api.stat_segment_ls_r(make_string_vector(self.api,
232                                                              patterns),
233                                           self.client)
234
235     def lsstr(self, patterns):
236         if not self.connected:
237             self.connect()
238         rv = self.api.stat_segment_ls_r(make_string_vector(self.api,
239                                                            patterns),
240                                         self.client)
241
242         if rv == ffi.NULL:
243             raise VPPStatsIOError()
244         return [ffi.string(self.api.stat_segment_index_to_name_r(
245             rv[i], self.client)).decode('utf-8')
246                 for i in range(self.api.stat_segment_vec_len(rv))]
247
248     def dump(self, counters):
249         if not self.connected:
250             self.connect()
251         stats = {}
252         rv = self.api.stat_segment_dump_r(counters, self.client)
253         # Raise exception and retry
254         if rv == ffi.NULL:
255             raise VPPStatsIOError()
256         rv_len = self.api.stat_segment_vec_len(rv)
257
258         for i in range(rv_len):
259             n = ffi.string(rv[i].name).decode('utf-8')
260             e = stat_entry_to_python(self.api, rv[i])
261             if e is not None:
262                 stats[n] = e
263         return stats
264
265     def get_counter(self, name):
266         retries = 0
267         while True:
268             try:
269                 d = self.ls(name)
270                 s = self.dump(d)
271                 if len(s) > 1:
272                     raise AttributeError('Matches multiple counters {}'
273                                          .format(name))
274                 k, v = s.popitem()
275                 return v
276             except VPPStatsIOError:
277                 if retries > 10:
278                     return None
279                 retries += 1
280
281     def get_err_counter(self, name):
282         """Get an error counter. The errors from each worker thread
283            are summed"""
284         return sum(self.get_counter(name))
285
286     def disconnect(self):
287         try:
288             self.api.stat_segment_disconnect_r(self.client)
289             self.api.stat_client_free(self.client)
290             self.connected = False
291             del self.client
292         except AttributeError:
293             # no need to disconnect if we're not connected
294             pass
295
296     def set_errors(self):
297         '''Return all errors counters > 0'''
298         retries = 0
299         while True:
300             try:
301                 error_names = self.ls(['/err/'])
302                 error_counters = self.dump(error_names)
303                 break
304             except VPPStatsIOError:
305                 if retries > 10:
306                     return None
307                 retries += 1
308
309         return {k: sum(error_counters[k])
310                 for k in error_counters.keys() if sum(error_counters[k])}
311
312     def set_errors_str(self):
313         '''Return all errors counters > 0 pretty printed'''
314         s = ['ERRORS:']
315         error_counters = self.set_errors()
316         for k in sorted(error_counters):
317             s.append('{:<60}{:>10}'.format(k, error_counters[k]))
318         return '%s\n' % '\n'.join(s)