vpp_papi_provider.py revision d68951ac
1#!/usr/bin/env python3
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
16r"""CSIT PAPI Provider
17
18TODO: Add description.
19
20Examples:
21---------
22
23Request/reply or dump:
24
25    vpp_papi_provider.py \
26        --method request \
27        --data '[{"api_name": "show_version", "api_args": {}}]'
28
29VPP-stats:
30
31    vpp_papi_provider.py \
32        --method stats \
33        --data '[["^/if", "/err/ip4-input", "/sys/node/ip4-input"], ["^/if"]]'
34"""
35
36import argparse
37import json
38import os
39import sys
40
41
42# Client name
43CLIENT_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
51try:
52    do_import = bool(not os.getenv(u"NO_VPP_PAPI") == u"1")
53except KeyError:
54    do_import = True
55
56if 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 VPP
71        from vpp_papi.vpp_stats import VPPStats
72    else:
73        raise RuntimeError(u"vpp_papi module not found")
74
75
76def _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
132def 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 = VPP()
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
209def 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
239def 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
265def 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
307if __name__ == u"__main__":
308    sys.stdout.write(main())
309    sys.stdout.flush()
310    sys.exit(0)
311