vpp-api: Add context to VPPStatsIOError.
[vpp.git] / src / vpp-api / python / vpp_papi / vpp_stats.py
1 #!/usr/bin/env python
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_directory_type_t;
22
23 typedef struct
24 {
25   stat_directory_type_t type;
26   union {
27     uint64_t offset;
28     uint64_t index;
29     uint64_t value;
30   };
31   uint64_t offset_vector;
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     uint64_t error_value;
43     counter_t **simple_counter_vec;
44     vlib_counter_t **combined_counter_vec;
45   };
46 } stat_segment_data_t;
47
48 typedef struct
49 {
50   uint64_t epoch;
51   uint64_t in_progress;
52   uint64_t directory_offset;
53   uint64_t error_offset;
54   uint64_t stats_offset;
55 } stat_segment_shared_header_t;
56
57 typedef struct
58 {
59   uint64_t current_epoch;
60   stat_segment_shared_header_t *shared_header;
61   stat_segment_directory_entry_t *directory_vector;
62   ssize_t memory_size;
63 } stat_client_main_t;
64
65 stat_client_main_t * stat_client_get(void);
66 void stat_client_free(stat_client_main_t * sm);
67 int stat_segment_connect_r (char *socket_name, stat_client_main_t * sm);
68 int stat_segment_connect (char *socket_name);
69 void stat_segment_disconnect_r (stat_client_main_t * sm);
70 void stat_segment_disconnect (void);
71
72 uint32_t *stat_segment_ls_r (uint8_t ** patterns, stat_client_main_t * sm);
73 uint32_t *stat_segment_ls (uint8_t ** pattern);
74 stat_segment_data_t *stat_segment_dump_r (uint32_t * stats, stat_client_main_t * sm);
75 stat_segment_data_t *stat_segment_dump (uint32_t * counter_vec);
76 void stat_segment_data_free (stat_segment_data_t * res);
77
78 double stat_segment_heartbeat_r (stat_client_main_t * sm);
79 double stat_segment_heartbeat (void);
80 int stat_segment_vec_len(void *vec);
81 uint8_t **stat_segment_string_vector(uint8_t **string_vector, char *string);
82 """)
83
84
85 # Utility functions
86 def make_string_vector(api, strings):
87     vec = ffi.NULL
88     if type(strings) is not list:
89         strings = [strings]
90     for s in strings:
91         vec = api.stat_segment_string_vector(vec, ffi.new("char []",
92                                                           s.encode()))
93     return vec
94
95
96 def make_string_list(api, vec):
97     vec_len = api.stat_segment_vec_len(vec)
98     return [ffi.string(vec[i]) for i in range(vec_len)]
99
100
101 # 2-dimensonal array of thread, index
102 def simple_counter_vec_list(api, e):
103     vec = []
104     for thread in range(api.stat_segment_vec_len(e)):
105         len_interfaces = api.stat_segment_vec_len(e[thread])
106         if_per_thread = [e[thread][interfaces]
107                          for interfaces in range(len_interfaces)]
108         vec.append(if_per_thread)
109     return vec
110
111
112 def vlib_counter_dict(c):
113     return {'packets': c.packets,
114             'bytes': c.bytes}
115
116
117 def combined_counter_vec_list(api, e):
118     vec = []
119     for thread in range(api.stat_segment_vec_len(e)):
120         len_interfaces = api.stat_segment_vec_len(e[thread])
121         if_per_thread = [vlib_counter_dict(e[thread][interfaces])
122                          for interfaces in range(len_interfaces)]
123         vec.append(if_per_thread)
124     return vec
125
126
127 def stat_entry_to_python(api, e):
128     # Scalar index
129     if e.type == 1:
130         return e.scalar_value
131     if e.type == 2:
132         return simple_counter_vec_list(api, e.simple_counter_vec)
133     if e.type == 3:
134         return combined_counter_vec_list(api, e.combined_counter_vec)
135     if e.type == 4:
136         return e.error_value
137     return None
138
139
140 class VPPStatsIOError(IOError):
141     message = "Stat segment client connection returned: " \
142               "%(retval)s %(strerror)s."
143
144     strerror = {-1: "Stat client couldn't open socket",
145                 -2: "Stat client socket open but couldn't connect",
146                 -3: "Receiving file descriptor failed",
147                 -4: "mmap fstat failed",
148                 -5: "mmap map failed"
149                 }
150
151     def __init__(self, message=None, **kwargs):
152         if 'retval' in kwargs:
153             self.retval = kwargs['retval']
154             kwargs['strerror'] = self.strerror[int(self.retval)]
155
156         if not message:
157             try:
158                 message = self.message % kwargs
159             except Exception as e:
160                 message = self.message
161         else:
162             message = message % kwargs
163
164         super(VPPStatsIOError, self).__init__(message)
165
166
167 class VPPStatsClientLoadError(RuntimeError):
168     pass
169
170
171 class VPPStats(object):
172     VPPStatsIOError = VPPStatsIOError
173
174     default_socketname = '/var/run/stats.sock'
175     sharedlib_name = 'libvppapiclient.so'
176
177     def __init__(self, socketname=default_socketname, timeout=10):
178         try:
179             self.api = ffi.dlopen(VPPStats.sharedlib_name)
180         except Exception:
181             raise VPPStatsClientLoadError("Could not open: %s" %
182                                           VPPStats.sharedlib_name)
183         self.client = self.api.stat_client_get()
184
185         poll_end_time = time.time() + timeout
186         while time.time() < poll_end_time:
187             rv = self.api.stat_segment_connect_r(socketname.encode(),
188                                                  self.client)
189             if rv == 0:
190                 break
191
192         if rv != 0:
193             raise VPPStatsIOError(retval=rv)
194
195     def heartbeat(self):
196         return self.api.stat_segment_heartbeat_r(self.client)
197
198     def ls(self, patterns):
199         return self.api.stat_segment_ls_r(make_string_vector(self.api,
200                                                              patterns),
201                                           self.client)
202
203     def dump(self, counters):
204         stats = {}
205         rv = self.api.stat_segment_dump_r(counters, self.client)
206         # Raise exception and retry
207         if rv == ffi.NULL:
208             raise VPPStatsIOError()
209         rv_len = self.api.stat_segment_vec_len(rv)
210         for i in range(rv_len):
211             n = ffi.string(rv[i].name).decode()
212             e = stat_entry_to_python(self.api, rv[i])
213             if e is not None:
214                 stats[n] = e
215         return stats
216
217     def get_counter(self, name):
218         retries = 0
219         while True:
220             try:
221                 d = self.ls(name)
222                 s = self.dump(d)
223                 if len(s) > 1:
224                     raise AttributeError('Matches multiple counters {}'
225                                          .format(name))
226                 k, v = s.popitem()
227                 return v
228             except VPPStatsIOError as e:
229                 if retries > 10:
230                     return None
231                 retries += 1
232
233     def disconnect(self):
234         self.api.stat_segment_disconnect_r(self.client)
235         self.api.stat_client_free(self.client)
236
237     def set_errors(self):
238         '''Return all errors counters > 0'''
239         retries = 0
240         while True:
241             try:
242                 error_names = self.ls(['/err/'])
243                 error_counters = self.dump(error_names)
244                 break
245             except VPPStatsIOError as e:
246                 if retries > 10:
247                     return None
248                 retries += 1
249
250         return {k: error_counters[k]
251                 for k in error_counters.keys() if error_counters[k]}
252
253     def set_errors_str(self):
254         '''Return all errors counters > 0 pretty printed'''
255         s = 'ERRORS:\n'
256         error_counters = self.set_errors()
257         for k in sorted(error_counters):
258             s += '{:<60}{:>10}\n'.format(k, error_counters[k])
259         return s