Stats: Use the new client class name
[csit.git] / resources / tools / papi / vpp_papi_provider.py
1 #!/usr/bin/env python3
2
3 # Copyright (c) 2020 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
16 r"""CSIT PAPI Provider
17
18 TODO: Add description.
19
20 Examples:
21 ---------
22
23 Request/reply or dump:
24
25     vpp_papi_provider.py \
26         --method request \
27         --data '[{"api_name": "show_version", "api_args": {}}]'
28
29 VPP-stats:
30
31     vpp_papi_provider.py \
32         --method stats \
33         --data '[["^/if", "/err/ip4-input", "/sys/node/ip4-input"], ["^/if"]]'
34 """
35
36 import argparse
37 import json
38 import os
39 import sys
40
41
42 # Client name
43 CLIENT_NAME = u"csit_papi"
44
45
46 # Sphinx creates auto-generated documentation by importing the python source
47 # files and collecting the docstrings from them. The NO_VPP_PAPI flag allows
48 # the vpp_papi_provider.py file to be importable without having to build
49 # the whole vpp api if the user only wishes to generate the test documentation.
50
51 try:
52     do_import = bool(not os.getenv(u"NO_VPP_PAPI") == u"1")
53 except KeyError:
54     do_import = True
55
56 if do_import:
57
58     # Find the directory where the modules are installed. The directory depends
59     # on the OS used.
60     # TODO: Find a better way to import papi modules.
61
62     modules_path = None
63     for root, dirs, files in os.walk(u"/usr/lib"):
64         for name in files:
65             if name == u"vpp_papi.py":
66                 modules_path = os.path.split(root)[0]
67                 break
68     if modules_path:
69         sys.path.append(modules_path)
70         from vpp_papi import VPPApiClient
71         from vpp_papi.vpp_stats import VPPStats
72     else:
73         raise RuntimeError(u"vpp_papi module not found")
74
75
76 def _convert_reply(api_r):
77     """Process API reply / a part of API reply for smooth converting to
78     JSON string.
79
80     It is used only with 'request' and 'dump' methods.
81
82     Apply binascii.hexlify() method for string values.
83
84     TODO: Implement complex solution to process of replies.
85
86     :param api_r: API reply.
87     :type api_r: Vpp_serializer reply object (named tuple)
88     :returns: Processed API reply / a part of API reply.
89     :rtype: dict
90     """
91     unwanted_fields = [u"count", u"index", u"context"]
92
93     def process_value(val):
94         """Process value.
95
96         :param val: Value to be processed.
97         :type val: object
98         :returns: Processed value.
99         :rtype: dict or str or int
100         """
101         if isinstance(val, dict):
102             for val_k, val_v in val.items():
103                 val[str(val_k)] = process_value(val_v)
104             return val
105         elif isinstance(val, list):
106             for idx, val_l in enumerate(val):
107                 val[idx] = process_value(val_l)
108             return val
109         elif isinstance(val, bytes):
110             val.hex()
111         elif hasattr(val, u"__int__"):
112             return int(val)
113         elif hasattr(val, "__str__"):
114             return str(val).encode(encoding=u"utf-8").hex()
115         # Next handles parameters not supporting preferred integer or string
116         # representation to get it logged
117         elif hasattr(val, u"__repr__"):
118             return repr(val)
119         else:
120             return val
121
122     reply_dict = dict()
123     reply_key = repr(api_r).split(u"(")[0]
124     reply_value = dict()
125     for item in dir(api_r):
126         if not item.startswith(u"_") and item not in unwanted_fields:
127             reply_value[item] = process_value(getattr(api_r, item))
128     reply_dict[reply_key] = reply_value
129     return reply_dict
130
131
132 def process_json_request(args):
133     """Process the request/reply and dump classes of VPP API methods.
134
135     :param args: Command line arguments passed to VPP PAPI Provider.
136     :type args: ArgumentParser
137     :returns: JSON formatted string.
138     :rtype: str
139     :raises RuntimeError: If PAPI command error occurs.
140     """
141
142     try:
143         vpp = VPPApiClient()
144     except Exception as err:
145         raise RuntimeError(f"PAPI init failed:\n{err!r}")
146
147     reply = list()
148
149     def process_value(val):
150         """Process value.
151
152         :param val: Value to be processed.
153         :type val: object
154         :returns: Processed value.
155         :rtype: dict or str or int
156         """
157         if isinstance(val, dict):
158             for val_k, val_v in val.items():
159                 val[str(val_k)] = process_value(val_v)
160             return val
161         elif isinstance(val, list):
162             for idx, val_l in enumerate(val):
163                 val[idx] = process_value(val_l)
164             return val
165         elif isinstance(val, str):
166             return bytes.fromhex(val).decode(encoding=u"utf-8")
167         elif isinstance(val, int):
168             return val
169         else:
170             return str(val)
171
172     json_data = json.loads(args.data)
173     vpp.connect(CLIENT_NAME)
174     for data in json_data:
175         api_name = data[u"api_name"]
176         api_args_unicode = data[u"api_args"]
177         api_reply = dict(api_name=api_name)
178         api_args = dict()
179         for a_k, a_v in api_args_unicode.items():
180             api_args[str(a_k)] = process_value(a_v)
181         try:
182             papi_fn = getattr(vpp.api, api_name)
183             rep = papi_fn(**api_args)
184
185             if isinstance(rep, list):
186                 converted_reply = list()
187                 for r in rep:
188                     converted_reply.append(_convert_reply(r))
189             else:
190                 converted_reply = _convert_reply(rep)
191
192             api_reply[u"api_reply"] = converted_reply
193             reply.append(api_reply)
194         except (AttributeError, ValueError) as err:
195             vpp.disconnect()
196             raise RuntimeError(
197                 f"PAPI command {api_name}({api_args}) input error:\n{err!r}"
198             )
199         except Exception as err:
200             vpp.disconnect()
201             raise RuntimeError(
202                 f"PAPI command {api_name}({api_args}) error:\n{err!r}"
203             )
204     vpp.disconnect()
205
206     return json.dumps(reply)
207
208
209 def process_stats(args):
210     """Process the VPP Stats.
211
212     :param args: Command line arguments passed to VPP PAPI Provider.
213     :type args: ArgumentParser
214     :returns: JSON formatted string.
215     :rtype: str
216     :raises RuntimeError: If PAPI command error occurs.
217     """
218
219     try:
220         stats = VPPStats(args.socket)
221     except Exception as err:
222         raise RuntimeError(f"PAPI init failed:\n{err!r}")
223
224     json_data = json.loads(args.data)
225
226     reply = list()
227
228     for path in json_data:
229         directory = stats.ls(path)
230         data = stats.dump(directory)
231         reply.append(data)
232
233     try:
234         return json.dumps(reply)
235     except UnicodeDecodeError as err:
236         raise RuntimeError(f"PAPI reply {reply} error:\n{err!r}")
237
238
239 def process_stats_request(args):
240     """Process the VPP Stats requests.
241
242     :param args: Command line arguments passed to VPP PAPI Provider.
243     :type args: ArgumentParser
244     :returns: JSON formatted string.
245     :rtype: str
246     :raises RuntimeError: If PAPI command error occurs.
247     """
248
249     try:
250         stats = VPPStats(args.socket)
251     except Exception as err:
252         raise RuntimeError(f"PAPI init failed:\n{err!r}")
253
254     try:
255         json_data = json.loads(args.data)
256     except ValueError as err:
257         raise RuntimeError(f"Input json string is invalid:\n{err!r}")
258
259     papi_fn = getattr(stats, json_data[u"api_name"])
260     reply = papi_fn(**json_data.get(u"api_args", {}))
261
262     return json.dumps(reply)
263
264
265 def main():
266     """Main function for the Python API provider.
267     """
268
269     # The functions which process different types of VPP Python API methods.
270     process_request = dict(
271         request=process_json_request,
272         dump=process_json_request,
273         stats=process_stats,
274         stats_request=process_stats_request
275     )
276
277     parser = argparse.ArgumentParser(
278         formatter_class=argparse.RawDescriptionHelpFormatter,
279         description=__doc__
280     )
281     parser.add_argument(
282         u"-m", u"--method", required=True,
283         choices=[str(key) for key in process_request.keys()],
284         help=u"Specifies the VPP API methods: "
285              u"1. request - simple request / reply; "
286              u"2. dump - dump function;"
287              u"3. stats - VPP statistics."
288     )
289     parser.add_argument(
290         u"-d", u"--data", required=True,
291         help=u"If the method is 'request' or 'dump', data is a JSON string "
292              u"(list) containing API name(s) and its/their input argument(s). "
293              u"If the method is 'stats', data is a JSON string containing t"
294              u"he list of path(s) to the required data."
295     )
296     parser.add_argument(
297         u"-s", u"--socket", default=u"/var/run/vpp/stats.sock",
298         help=u"A file descriptor over the VPP stats Unix domain socket. "
299              u"It is used only if method=='stats'."
300     )
301
302     args = parser.parse_args()
303
304     return process_request[args.method](args)
305
306
307 if __name__ == u"__main__":
308     sys.stdout.write(main())
309     sys.stdout.flush()
310     sys.exit(0)