+ 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=u"/tmp")
+ self._local_vpp_socket = self._temp_dir + u"/vpp-api.sock"
+ self._ssh_control_socket = self._temp_dir + u"/ssh.sock"
+ ssh_socket = self._ssh_control_socket
+ # Cleanup possibilities.
+ ret_code, _ = run([u"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(
+ [u"ssh", u"-S", ssh_socket, u"-O", u"exit", u"0.0.0.0"],
+ check=False, log=True
+ )
+ # TODO: Is any sleep necessary? How to prove if not?
+ run([u"sleep", u"0.1"])
+ run([u"rm", u"-vrf", ssh_socket])
+ # Even if ssh can perhaps reuse this file,
+ # we need to remove it for readiness detection to work correctly.
+ run([u"rm", u"-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, u"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 = [
+ u"ssh", u"-S", ssh_socket, u"-M",
+ u"-o", u"LogLevel=ERROR", u"-o", u"UserKnownHostsFile=/dev/null",
+ u"-o", u"StrictHostKeyChecking=no",
+ u"-o", u"ExitOnForwardFailure=yes",
+ u"-L", self._local_vpp_socket + u":" + self._remote_vpp_socket,
+ u"-p", str(node[u"port"]), node[u"username"] + u"@" + node[u"host"],
+ u"sleep", u"10"
+ ]
+ priv_key = node.get(u"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 support 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] = [u"-i", key_file.name]
+ password = node.get(u"password")
+ if password:
+ # Prepend sshpass command to set password.
+ ssh_cmd[:0] = [u"sshpass", u"-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(
+ [u"ls", u"-l", self._local_vpp_socket], check=False
+ )
+ if not ret_code:
+ break
+ time.sleep(0.1)
+ else:
+ raise RuntimeError(u"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 range(2):
+ try:
+ vpp_instance.connect_sync(u"csit_socket")
+ except (IOError, struct.error) as err:
+ logger.warn(f"Got initial connect error {err!r}")
+ vpp_instance.disconnect()
+ else:
+ break
+ else:
+ raise RuntimeError(u"Failed to connect to VPP over a socket.")
+ return self