+ First invocation downloads .api.json files from self._node
+ into a temporary directory.
+
+ After first invocation, the result is cached, so other calls are quick.
+ Class variable is used as the cache, but this property is defined as
+ an instance method, so that _node (for api files) is known.
+
+ :returns: Initialized but not connected VPP instance.
+ :rtype: vpp_papi.VPPApiClient
+ """
+ cls = self.__class__
+ if cls.cached_vpp_instance is not None:
+ return cls.cached_vpp_instance
+ tmp_dir = tempfile.mkdtemp(dir="/tmp")
+ package_path = "Not set yet."
+ try:
+ # Pack, copy and unpack Python part of VPP installation from _node.
+ # TODO: Use rsync or recursive version of ssh.scp_node instead?
+ node = self._node
+ exec_cmd_no_error(node, ["rm", "-rf", "/tmp/papi.txz"])
+ # Papi python version depends on OS (and time).
+ # Python 2.7 or 3.4, site-packages or dist-packages.
+ installed_papi_glob = "/usr/lib/python*/*-packages/vpp_papi"
+ # We need to wrap this command in bash, in order to expand globs,
+ # and as ssh does join, the inner command has to be quoted.
+ inner_cmd = " ".join([
+ "tar", "cJf", "/tmp/papi.txz", "--exclude=*.pyc",
+ installed_papi_glob, "/usr/share/vpp/api"])
+ exec_cmd_no_error(node, ["bash", "-c", "'" + inner_cmd + "'"])
+ scp_node(node, tmp_dir + "/papi.txz", "/tmp/papi.txz", get=True)
+ run(["tar", "xf", tmp_dir + "/papi.txz", "-C", tmp_dir])
+ cls.api_json_directory = tmp_dir + "/usr/share/vpp/api"
+ # Perform initial checks before .api.json files are gone,
+ # by accessing the property (which also creates its instance).
+ self.create_crc_checker()
+ # When present locally, we finally can find the installation path.
+ package_path = glob.glob(tmp_dir + installed_papi_glob)[0]
+ # Package path has to be one level above the vpp_papi directory.
+ package_path = package_path.rsplit('/', 1)[0]
+ sys.path.append(package_path)
+ from vpp_papi.vpp_papi import VPPApiClient as vpp_class
+ vpp_class.apidir = cls.api_json_directory
+ # We need to create instance before removing from sys.path.
+ cls.cached_vpp_instance = vpp_class(
+ use_socket=True, server_address="TBD", async_thread=False,
+ read_timeout=14, logger=FilteredLogger(logger, "INFO"))
+ # Cannot use loglevel parameter, robot.api.logger lacks support.
+ # TODO: Stop overriding read_timeout when VPP-1722 is fixed.
+ finally:
+ shutil.rmtree(tmp_dir)
+ if sys.path[-1] == package_path:
+ sys.path.pop()
+ return cls.cached_vpp_instance
+
+ def __enter__(self):
+ """Create a tunnel, connect VPP instance.
+
+ Only at this point a local socket names are created
+ in a temporary directory, because VIRL runs 3 pybots at once,
+ so harcoding local filenames does not work.
+
+ :returns: self
+ :rtype: PapiSocketExecutor
+ """
+ # Parsing takes longer than connecting, prepare instance before tunnel.
+ vpp_instance = self.vpp_instance
+ node = self._node
+ self._temp_dir = tempfile.mkdtemp(dir="/tmp")
+ self._local_vpp_socket = self._temp_dir + "/vpp-api.sock"
+ self._ssh_control_socket = self._temp_dir + "/ssh.sock"
+ ssh_socket = self._ssh_control_socket
+ # Cleanup possibilities.
+ ret_code, _ = run(["ls", ssh_socket], check=False)
+ if ret_code != 2:
+ # This branch never seems to be hit in CI,
+ # but may be useful when testing manually.
+ run(["ssh", "-S", ssh_socket, "-O", "exit", "0.0.0.0"],
+ check=False, log=True)
+ # TODO: Is any sleep necessary? How to prove if not?
+ run(["sleep", "0.1"])
+ run(["rm", "-vrf", ssh_socket])
+ # Even if ssh can perhaps reuse this file,
+ # we need to remove it for readiness detection to work correctly.
+ run(["rm", "-rvf", self._local_vpp_socket])
+ # On VIRL, the ssh user is not added to "vpp" group,
+ # so we need to change remote socket file access rights.
+ exec_cmd_no_error(
+ node, "chmod o+rwx " + self._remote_vpp_socket, sudo=True)
+ # We use sleep command. The ssh command will exit in 10 second,
+ # unless a local socket connection is established,
+ # in which case the ssh command will exit only when
+ # the ssh connection is closed again (via control socket).
+ # The log level is to supress "Warning: Permanently added" messages.
+ ssh_cmd = [
+ "ssh", "-S", ssh_socket, "-M",
+ "-o", "LogLevel=ERROR", "-o", "UserKnownHostsFile=/dev/null",
+ "-o", "StrictHostKeyChecking=no", "-o", "ExitOnForwardFailure=yes",
+ "-L", self._local_vpp_socket + ':' + self._remote_vpp_socket,
+ "-p", str(node['port']), node['username'] + "@" + node['host'],
+ "sleep", "10"]
+ priv_key = node.get("priv_key")
+ if priv_key:
+ # This is tricky. We need a file to pass the value to ssh command.
+ # And we need ssh command, because paramiko does not suport sockets
+ # (neither ssh_socket, nor _remote_vpp_socket).
+ key_file = tempfile.NamedTemporaryFile()
+ key_file.write(priv_key)
+ # Make sure the content is written, but do not close yet.
+ key_file.flush()
+ ssh_cmd[1:1] = ["-i", key_file.name]
+ password = node.get("password")
+ if password:
+ # Prepend sshpass command to set password.
+ ssh_cmd[:0] = ["sshpass", "-p", password]
+ time_stop = time.time() + 10.0
+ # subprocess.Popen seems to be the best way to run commands
+ # on background. Other ways (shell=True with "&" and ssh with -f)
+ # seem to be too dependent on shell behavior.
+ # In particular, -f does NOT return values for run().
+ subprocess.Popen(ssh_cmd)
+ # Check socket presence on local side.
+ while time.time() < time_stop:
+ # It can take a moment for ssh to create the socket file.
+ ret_code, _ = run(["ls", "-l", self._local_vpp_socket], check=False)
+ if not ret_code:
+ break
+ time.sleep(0.1)
+ else:
+ raise RuntimeError("Local side socket has not appeared.")
+ if priv_key:
+ # Socket up means the key has been read. Delete file by closing it.
+ key_file.close()
+ # Everything is ready, set the local socket address and connect.
+ vpp_instance.transport.server_address = self._local_vpp_socket
+ # It seems we can get read error even if every preceding check passed.
+ # Single retry seems to help.
+ for _ in xrange(2):
+ try:
+ vpp_instance.connect_sync("csit_socket")
+ except IOError as err:
+ logger.warn("Got initial connect error {err!r}".format(err=err))
+ vpp_instance.disconnect()
+ else:
+ break
+ else:
+ raise RuntimeError("Failed to connect to VPP over a socket.")
+ return self
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ """Disconnect the vpp instance, tear down the SHH tunnel.
+
+ Also remove the local sockets by deleting the temporary directory.
+ Arguments related to possible exception are entirely ignored.
+ """
+ self.vpp_instance.disconnect()
+ run(["ssh", "-S", self._ssh_control_socket, "-O", "exit", "0.0.0.0"],
+ check=False)
+ shutil.rmtree(self._temp_dir)
+ return
+
+ def add(self, csit_papi_command, history=True, **kwargs):
+ """Add next command to internal command list; return self.
+
+ The argument name 'csit_papi_command' must be unique enough as it cannot
+ be repeated in kwargs.
+ Unless disabled, new entry to papi history is also added at this point.
+
+ Any pending conflicts from .api.json processing are raised.
+ Then the command name is checked for known CRCs.
+ Unsupported commands raise an exception, as CSIT change
+ should not start using messages without making sure which CRCs
+ are supported.
+ Each CRC issue is raised only once, so subsequent tests
+ can raise other issues.
+
+ :param csit_papi_command: VPP API command.
+ :param history: Enable/disable adding command to PAPI command history.
+ :param kwargs: Optional key-value arguments.
+ :type csit_papi_command: str
+ :type history: bool
+ :type kwargs: dict
+ :returns: self, so that method chaining is possible.
+ :rtype: PapiSocketExecutor
+ :raises RuntimeError: If unverified or conflicting CRC is encountered.
+ """
+ if history:
+ PapiHistory.add_to_papi_history(
+ self._node, csit_papi_command, **kwargs)
+ self._api_command_list.append(
+ dict(api_name=csit_papi_command, api_args=kwargs))
+ return self
+
+ def get_replies(self, err_msg="Failed to get replies."):
+ """Get replies from VPP Python API.
+
+ The replies are parsed into dict-like objects,
+ "retval" field is guaranteed to be zero on success.
+
+ :param err_msg: The message used if the PAPI command(s) execution fails.
+ :type err_msg: str
+ :returns: Responses, dict objects with fields due to API and "retval".
+ :rtype: list of dict
+ :raises RuntimeError: If retval is nonzero, parsing or ssh error.
+ """
+ return self._execute(err_msg=err_msg)
+
+ def get_reply(self, err_msg="Failed to get reply."):
+ """Get reply from VPP Python API.
+
+ The reply is parsed into dict-like object,
+ "retval" field is guaranteed to be zero on success.
+
+ TODO: Discuss exception types to raise, unify with inner methods.
+
+ :param err_msg: The message used if the PAPI command(s) execution fails.
+ :type err_msg: str
+ :returns: Response, dict object with fields due to API and "retval".
+ :rtype: dict
+ :raises AssertionError: If retval is nonzero, parsing or ssh error.
+ """
+ replies = self.get_replies(err_msg=err_msg)
+ if len(replies) != 1:
+ raise RuntimeError("Expected single reply, got {replies!r}".format(
+ replies=replies))
+ return replies[0]
+
+ def get_sw_if_index(self, err_msg="Failed to get reply."):
+ """Get sw_if_index from reply from VPP Python API.
+
+ Frequently, the caller is only interested in sw_if_index field
+ of the reply, this wrapper makes such call sites shorter.
+
+ TODO: Discuss exception types to raise, unify with inner methods.
+
+ :param err_msg: The message used if the PAPI command(s) execution fails.
+ :type err_msg: str
+ :returns: Response, sw_if_index value of the reply.
+ :rtype: int
+ :raises AssertionError: If retval is nonzero, parsing or ssh error.
+ """
+ reply = self.get_reply(err_msg=err_msg)
+ logger.info("Getting index from {reply!r}".format(reply=reply))
+ return reply["sw_if_index"]
+
+ def get_details(self, err_msg="Failed to get dump details."):
+ """Get dump details from VPP Python API.
+
+ The details are parsed into dict-like objects.
+ The number of details per single dump command can vary,
+ and all association between details and dumps is lost,
+ so if you care about the association (as opposed to
+ logging everything at once for debugging purposes),
+ it is recommended to call get_details for each dump (type) separately.
+
+ :param err_msg: The message used if the PAPI command(s) execution fails.
+ :type err_msg: str
+ :returns: Details, dict objects with fields due to API without "retval".
+ :rtype: list of dict