+def dictize(obj):
+ """A helper method, to make namedtuple-like object accessible as dict.
+
+ If the object is namedtuple-like, its _asdict() form is returned,
+ but in the returned object __getitem__ method is wrapped
+ to dictize also any items returned.
+ If the object does not have _asdict, it will be returned without any change.
+ Integer keys still access the object as tuple.
+
+ A more useful version would be to keep obj mostly as a namedtuple,
+ just add getitem for string keys. Unfortunately, namedtuple inherits
+ from tuple, including its read-only __getitem__ attribute,
+ so we cannot monkey-patch it.
+
+ TODO: Create a proxy for named tuple to allow that.
+
+ :param obj: Arbitrary object to dictize.
+ :type obj: object
+ :returns: Dictized object.
+ :rtype: same as obj type or collections.OrderedDict
+ """
+ if not hasattr(obj, u"_asdict"):
+ return obj
+ overriden = UserDict(obj._asdict())
+ old_get = overriden.__getitem__
+ new_get = lambda self, key: dictize(old_get(self, key))
+ overriden.__getitem__ = new_get
+ return overriden
+
+
+class PapiSocketExecutor:
+ """Methods for executing VPP Python API commands on forwarded socket.
+
+ Previously, we used an implementation with single client instance
+ and connection being handled by a resource manager.
+ On "with" statement, the instance connected, and disconnected
+ on exit from the "with" block.
+ This was limiting (no nested with blocks) and mainly it was slow:
+ 0.7 seconds per disconnect cycle on Skylake, more than 3 second on Taishan.
+
+ The currently used implementation caches the connected client instances,
+ providing speedup and making "with" blocks unnecessary.
+ But with many call sites, "with" blocks are still the main usage pattern.
+ Documentation still lists that as the intended pattern.
+
+ As a downside, clients need to be explicitly told to disconnect
+ before VPP restart.
+ There is some amount of retries and disconnects on disconnect
+ (so unresponsive VPPs do not breach test much more than needed),
+ but it is hard to verify all that works correctly.
+ Especially, if Robot crashes, files and ssh processes may leak.
+
+ Delay for accepting socket connection is 10s.
+ TODO: Decrease 10s to value that is long enough for creating connection
+ and short enough to not affect performance.
+
+ The current implementation downloads and parses .api.json files only once
+ and caches client instances for reuse.
+ Cleanup metadata is added as additional attributes
+ directly to client instances.
+
+ The current implementation seems to run into read error occasionally.
+ Not sure if the error is in Python code on Robot side, ssh forwarding,
+ or socket handling at VPP side. Anyway, reconnect after some sleep
+ seems to help, hoping repeated command execution does not lead to surprises.
+ The reconnection is logged at WARN level, so it is prominently shown
+ in log.html, so we can see how frequently it happens.
+
+ TODO: Support handling of retval!=0 without try/except in caller.
+
+ Note: Use only with "with" statement, e.g.:
+
+ cmd = 'show_version'
+ with PapiSocketExecutor(node) as papi_exec:
+ reply = papi_exec.add(cmd).get_reply(err_msg)
+
+ This class processes two classes of VPP PAPI methods:
+ 1. Simple request / reply: method='request'.
+ 2. Dump functions: method='dump'.
+
+ Note that access to VPP stats over socket is not supported yet.
+
+ The recommended ways of use are (examples):
+
+ 1. Simple request / reply
+
+ a. One request with no arguments:
+
+ cmd = 'show_version'
+ with PapiSocketExecutor(node) as papi_exec:
+ reply = papi_exec.add(cmd).get_reply(err_msg)
+
+ b. Three requests with arguments, the second and the third ones are the same
+ but with different arguments.
+
+ with PapiSocketExecutor(node) as papi_exec:
+ replies = papi_exec.add(cmd1, **args1).add(cmd2, **args2).\
+ add(cmd2, **args3).get_replies(err_msg)
+
+ 2. Dump functions
+
+ cmd = 'sw_interface_rx_placement_dump'
+ with PapiSocketExecutor(node) as papi_exec:
+ details = papi_exec.add(cmd, sw_if_index=ifc['vpp_sw_index']).\
+ get_details(err_msg)
+ """
+
+ # Class cache for reuse between instances.
+ api_root_dir = None
+ """We copy .api json files and PAPI code from DUT to robot machine.
+ This class variable holds temporary directory once created.
+ When python exits, the directory is deleted, so no downloaded file leaks.
+ The value will be set to TemporaryDirectory class instance (not string path)
+ to ensure deletion at exit."""
+ api_json_path = None
+ """String path to .api.json files, a directory somewhere in api_root_dir."""
+ api_package_path = None
+ """String path to PAPI code, a different directory under api_root_dir."""
+ crc_checker = None
+ """Accesses .api.json files at creation, caching speeds up accessing it."""
+ reusable_vpp_client_list = list()
+ """Each connection needs a separate client instance,
+ and each client instance creation needs to parse all .api files,
+ which takes time. If a client instance disconnects, it is put here,
+ so on next connect we can reuse intead of creating new."""
+ conn_cache = dict()
+ """Mapping from node key to connected client instance."""
+
+ def __init__(self, node, remote_vpp_socket=Constants.SOCKSVR_PATH):
+ """Store the given arguments, declare managed variables.
+
+ :param node: Node to connect to and forward unix domain socket from.
+ :param remote_vpp_socket: Path to remote socket to tunnel to.
+ :type node: dict
+ :type remote_vpp_socket: str
+ """