PAPI stats: Use list as argument to ls
[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     :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         # The ls method can match multiple patterns,
230         # but we feed it one path at a time anyway, because the caller
231         # expect results in a list, one item per path.
232         # Most VPP versions understand a string is a single pattern,
233         # but some blindly iterate (as if it was a list of chars).
234         directory = stats.ls([path])
235         data = stats.dump(directory)
236         reply.append(data)
237
238     try:
239         return json.dumps(reply)
240     except UnicodeDecodeError as err:
241         raise RuntimeError(f"PAPI reply {reply} error:\n{err!r}")
242
243
244 def process_stats_request(args):
245     """Process the VPP Stats requests.
246
247     :param args: Command line arguments passed to VPP PAPI Provider.
248     :type args: ArgumentParser
249     :returns: JSON formatted string.
250     :rtype: str
251     :raises RuntimeError: If PAPI command error occurs.
252     """
253
254     try:
255         stats = VPPStats(args.socket)
256     except Exception as err:
257         raise RuntimeError(f"PAPI init failed:\n{err!r}")
258
259     try:
260         json_data = json.loads(args.data)
261     except ValueError as err:
262         raise RuntimeError(f"Input json string is invalid:\n{err!r}")
263
264     papi_fn = getattr(stats, json_data[u"api_name"])
265     reply = papi_fn(**json_data.get(u"api_args", {}))
266
267     return json.dumps(reply)
268
269
270 def main():
271     """Main function for the Python API provider.
272     """
273
274     # The functions which process different types of VPP Python API methods.
275     process_request = dict(
276         request=process_json_request,
277         dump=process_json_request,
278         stats=process_stats,
279         stats_request=process_stats_request
280     )
281
282     parser = argparse.ArgumentParser(
283         formatter_class=argparse.RawDescriptionHelpFormatter,
284         description=__doc__
285     )
286     parser.add_argument(
287         u"-m", u"--method", required=True,
288         choices=[str(key) for key in process_request.keys()],
289         help=u"Specifies the VPP API methods: "
290              u"1. request - simple request / reply; "
291              u"2. dump - dump function;"
292              u"3. stats - VPP statistics."
293     )
294     parser.add_argument(
295         u"-d", u"--data", required=True,
296         help=u"If the method is 'request' or 'dump', data is a JSON string "
297              u"(list) containing API name(s) and its/their input argument(s). "
298              u"If the method is 'stats', data is a JSON string containing t"
299              u"he list of path(s) to the required data."
300     )
301     parser.add_argument(
302         u"-s", u"--socket", default=u"/var/run/vpp/stats.sock",
303         help=u"A file descriptor over the VPP stats Unix domain socket. "
304              u"It is used only if method=='stats'."
305     )
306
307     args = parser.parse_args()
308
309     return process_request[args.method](args)
310
311
312 if __name__ == u"__main__":
313     sys.stdout.write(main())
314     sys.stdout.flush()
315     sys.exit(0)