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