Report: Add vsap
[csit.git] / resources / tools / papi / vpp_papi_provider.py
1 #!/usr/bin/env python3
2
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
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     The reply contains single item covering all paths.
213
214     :param args: Command line arguments passed to VPP PAPI Provider.
215     :type args: ArgumentParser
216     :returns: JSON formatted string.
217     :rtype: str
218     :raises RuntimeError: If PAPI command error occurs.
219     """
220
221     try:
222         stats = VPPStats(args.socket)
223     except Exception as err:
224         raise RuntimeError(f"PAPI init failed:\n{err!r}")
225
226     paths = json.loads(args.data)
227     directory = stats.ls(paths)
228     reply = [stats.dump(directory)]
229
230     try:
231         return json.dumps(reply)
232     except UnicodeDecodeError as err:
233         raise RuntimeError(f"PAPI reply {reply} error:\n{err!r}")
234
235
236 def process_stats_request(args):
237     """Process the VPP Stats requests.
238
239     :param args: Command line arguments passed to VPP PAPI Provider.
240     :type args: ArgumentParser
241     :returns: JSON formatted string.
242     :rtype: str
243     :raises RuntimeError: If PAPI command error occurs.
244     """
245
246     try:
247         stats = VPPStats(args.socket)
248     except Exception as err:
249         raise RuntimeError(f"PAPI init failed:\n{err!r}")
250
251     try:
252         json_data = json.loads(args.data)
253     except ValueError as err:
254         raise RuntimeError(f"Input json string is invalid:\n{err!r}")
255
256     papi_fn = getattr(stats, json_data[u"api_name"])
257     reply = papi_fn(**json_data.get(u"api_args", {}))
258
259     return json.dumps(reply)
260
261
262 def main():
263     """Main function for the Python API provider.
264     """
265
266     # The functions which process different types of VPP Python API methods.
267     process_request = dict(
268         request=process_json_request,
269         dump=process_json_request,
270         stats=process_stats,
271         stats_request=process_stats_request
272     )
273
274     parser = argparse.ArgumentParser(
275         formatter_class=argparse.RawDescriptionHelpFormatter,
276         description=__doc__
277     )
278     parser.add_argument(
279         u"-m", u"--method", required=True,
280         choices=[str(key) for key in process_request.keys()],
281         help=u"Specifies the VPP API methods: "
282              u"1. request - simple request / reply; "
283              u"2. dump - dump function;"
284              u"3. stats - VPP statistics."
285     )
286     parser.add_argument(
287         u"-d", u"--data", required=True,
288         help=u"If the method is 'request' or 'dump', data is a JSON string "
289              u"(list) containing API name(s) and its/their input argument(s). "
290              u"If the method is 'stats', data is a JSON string containing t"
291              u"he list of path(s) to the required data."
292     )
293     parser.add_argument(
294         u"-s", u"--socket", default=u"/var/run/vpp/stats.sock",
295         help=u"A file descriptor over the VPP stats Unix domain socket. "
296              u"It is used only if method=='stats'."
297     )
298
299     args = parser.parse_args()
300
301     return process_request[args.method](args)
302
303
304 if __name__ == u"__main__":
305     sys.stdout.write(main())
306     sys.stdout.flush()
307     sys.exit(0)