+ :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.
+
+ :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
+ """
+ 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