1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 11:16:47 +00:00

fix(vrtManager): libvirt connections open within a time limit

libvirt has no timeout for opening a connection. Measured in the test
setup: an unreachable host blocked qemu+tcp for 135 s and qemu+ssh for
133 s; a daemon that accepts and never answers blocks forever. Gunicorn
runs sync workers and the instance list connects to every compute, so
one unreachable compute held a worker for minutes per page load.

- A connection is opened in a helper thread, at most one per
  connection at a time. The request waits LIBVIRT_CONNECT_TIMEOUT
  (default 5 s). An open nobody waits for any more keeps running; until
  it ends, requests get its error at once, and if it succeeds later the
  connection is used. A failed open is not repeated for
  LIBVIRT_RETRY_AFTER (default 30 s). A stuck host holds one thread and
  does not keep other hosts from connecting.
- qemu+ssh runs ssh through vrtManager/libvirt-ssh (the URI's command
  parameter), which adds ConnectTimeout=10 and BatchMode=yes, so an
  abandoned ssh process ends and no prompt can hang. The user's ssh
  configuration (aliases, ProxyJump, keys) still applies.
- The close callback acts only for the connection it was registered
  for: a late callback of a replaced connection dropped its successor.
  The reason is reported as text. A connection that closed before it
  was published is an error.
- Compute.status is a bool (it returned the exception object, so the
  "host is down" check in refresh_instance_database never fired); the
  message is Compute.connection_error.

Unit tests cover the timeout, retry delay, late success, single open,
a stuck host next to a healthy one and the close callback. Live tests
(vrtManager/test_live_connection.py), for TCP and SSH computes of the
test setup: an unreachable address (no ssh left after 10 s), an endpoint
that never answers, a slow open completing later, a host that goes
silent on an established connection (keepalive ends calls within 45 s,
the next request reconnects), an ssh_config alias and a ProxyJump host.
This commit is contained in:
catborise 2026-10-04 23:02:58 +03:00
parent 99eadaab31
commit 10b46afd36
9 changed files with 517 additions and 86 deletions

View file

@ -17,12 +17,14 @@ class Compute(Model):
@cached_property
def status(self):
# return connection_manager.host_is_up(self.type, self.hostname)
# TODO: looks like socket has problems connecting via VPN
if isinstance(self.connection, virConnect):
return True
else:
return self.connection
"""True if WebVirtCloud can connect to the host (within the connect
timeout; a host that failed recently is not tried again at once)."""
return isinstance(self.connection, virConnect)
@cached_property
def connection_error(self):
"""Why the host cannot be reached, or None."""
return None if self.status else str(self.connection)
@cached_property
def connection(self):

View file

@ -34,6 +34,7 @@ class OwnershipTestCase(TestCase):
def compute(self, name):
compute = Compute.objects.create(name=name, hostname=f"{name}.example", login="", password="", type=1)
compute.__dict__["proxy"] = MagicMock()
compute.__dict__["status"] = True # reachable
self.on(compute, [])
return compute

View file

@ -40,6 +40,7 @@ class InstanceReconciliationTestCase(TestCase):
mock_proxy = MagicMock()
mock_proxy.wvm.listAllDomains.return_value = [dom1, dom2]
self.compute.__dict__["proxy"] = mock_proxy
self.compute.__dict__["status"] = True # reachable
refresh_instance_database(self.compute)
@ -61,6 +62,7 @@ class InstanceReconciliationTestCase(TestCase):
mock_proxy = MagicMock()
mock_proxy.wvm.listAllDomains.return_value = []
self.compute.__dict__["proxy"] = mock_proxy
self.compute.__dict__["status"] = True # reachable
refresh_instance_database(self.compute)
@ -82,6 +84,7 @@ class InstanceReconciliationTestCase(TestCase):
mock_proxy = MagicMock()
mock_proxy.wvm.listAllDomains.return_value = [dom_renamed]
self.compute.__dict__["proxy"] = mock_proxy
self.compute.__dict__["status"] = True # reachable
refresh_instance_database(self.compute)
@ -105,6 +108,7 @@ class InstanceReconciliationTestCase(TestCase):
mock_proxy = MagicMock()
mock_proxy.wvm.listAllDomains.side_effect = Exception("Libvirt socket connection refused")
self.compute.__dict__["proxy"] = mock_proxy
self.compute.__dict__["status"] = True # reachable
refresh_instance_database(self.compute)
@ -124,6 +128,7 @@ class InstanceReconciliationTestCase(TestCase):
mock_proxy = MagicMock()
mock_proxy.wvm.listAllDomains.return_value = [broken_dom]
self.compute.__dict__["proxy"] = mock_proxy
self.compute.__dict__["status"] = True # reachable
refresh_instance_database(self.compute)

View file

@ -214,6 +214,7 @@ class ComputeConcurrencyTestCase(TransactionTestCase):
mock_proxy = MagicMock()
mock_proxy.wvm.listAllDomains.return_value = [mock_dom1, mock_dom2]
compute.proxy = mock_proxy
compute.status = True # reachable
exceptions = []
@ -253,6 +254,7 @@ class ComputeConcurrencyTestCase(TransactionTestCase):
mock_proxy = MagicMock()
mock_proxy.wvm.listAllDomains.return_value = []
compute.proxy = mock_proxy
compute.status = True # reachable
# 1. When lock directory cannot be acquired
with patch("computes.utils._get_lock_directory", return_value=None):

View file

@ -18,7 +18,7 @@
{% for compute in computes %}
{% if compute.status is not True %}
<div class="alert alert-danger alert-dismissible fade show" role="alert">
{% trans 'Problem occurred with host' %} {{ compute.name }}: {{ compute.status }}
{% trans 'Problem occurred with host' %} {{ compute.name }}: {{ compute.connection_error }}
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
{% endif %}

View file

@ -1,6 +1,9 @@
import os
import re
import socket
import threading
import time
from urllib.parse import quote
import libvirt
from django.conf import settings
@ -16,6 +19,20 @@ TLS_PORT = 16514
SSH_PORT = 22
TCP_PORT = 16509
# libvirt has no timeout for opening a connection: an unreachable host
# blocks for the kernel's TCP connect timeout (about 2 minutes), a daemon
# that accepts but never answers blocks forever, and such an open cannot be
# cancelled. Connections are opened in a helper thread, at most one per
# connection at a time, so a stuck host holds one thread and nothing else;
# the caller waits at most CONNECT_TIMEOUT seconds, and a host that failed is
# not tried again for RETRY_AFTER seconds.
CONNECT_TIMEOUT = getattr(settings, "LIBVIRT_CONNECT_TIMEOUT", 5)
RETRY_AFTER = getattr(settings, "LIBVIRT_RETRY_AFTER", 30)
CLOSE_REASONS = {0: "error", 1: "end of file", 2: "keepalive timeout", 3: "client closed"}
# ssh with a connect timeout for qemu+ssh (libvirt runs the "command" binary
# in place of ssh and never passes one itself)
SSH_COMMAND = os.path.join(os.path.dirname(os.path.abspath(__file__)), "libvirt-ssh")
class wvmEventLoop(threading.Thread):
""" Event Loop Class"""
@ -58,6 +75,8 @@ class wvmConnection(object):
self.connection_state_lock = threading.Lock()
self.connection = None
self.last_error = None
self._opening = None # threading.Event of the open in progress
self._retry_at = 0.0 # monotonic time before which no new open is tried
# credentials
self.host = host
@ -66,39 +85,70 @@ class wvmConnection(object):
self.type = conn
def connect(self):
self.connection_state_lock.acquire()
try:
# recheck if we have a connection (it may have been
if not self.connected:
if self.type == CONN_TCP:
self.__connect_tcp()
elif self.type == CONN_SSH:
self.__connect_ssh()
elif self.type == CONN_TLS:
self.__connect_tls()
elif self.type == CONN_SOCKET:
self.__connect_socket()
else:
raise ValueError(f'"{self.type}" is not a valid connection type')
"""Open the connection if needed, waiting at most CONNECT_TIMEOUT.
On failure last_error says why and connected stays False."""
with self.connection_state_lock:
if self.connected:
return
if self._opening is not None and self._opening.abandoned:
# A caller already waited for this open in vain; nobody waits
# again until it ends (one thread per connection, however long).
return
if time.monotonic() < self._retry_at:
return # failed recently: answer with the last error at once
if self._opening is None:
self._opening = threading.Event()
self._opening.abandoned = False
threading.Thread(target=self._open, args=(self._opening,), daemon=True).start()
opening = self._opening
if self.connected:
# do some preprocessing of the connection:
# * set keep alive interval
# * set connection close/fail handler
try:
self.connection.setKeepAlive(
connection_manager.keepalive_interval, connection_manager.keepalive_count
)
try:
self.connection.registerCloseCallback(self.__connection_close_callback, None)
except Exception:
# Temporary fix for libvirt > libvirt-0.10.2-41
pass
except libvirtError as e:
# hypervisor driver does not seem to support persistent connections
self.last_error = str(e)
if not opening.wait(CONNECT_TIMEOUT):
with self.connection_state_lock:
if self._opening is opening: # still running: give up waiting, not the open
self.last_error = f"Connection Failed: {self.host} did not answer within {CONNECT_TIMEOUT} s"
opening.abandoned = True
def _open(self, opening):
"""Helper thread: open, configure and publish the connection."""
connection = error = None
try:
connection = self._open_transport()
if not connection.isAlive(): # closed again before it was published
raise util.OperationError("the connection closed right after opening")
# Drivers without keepalive or close callbacks work without them
try:
connection.setKeepAlive(connection_manager.keepalive_interval, connection_manager.keepalive_count)
except libvirtError:
pass
try:
connection.registerCloseCallback(self.__connection_close_callback, None)
except libvirtError:
pass
except Exception as e: # libvirtError, or anything else from the transport
error = f"Connection Failed: {str(e)}"
finally:
self.connection_state_lock.release()
with self.connection_state_lock:
if error is None:
self.connection = connection
self.last_error = None
self._retry_at = 0.0
else:
self.connection = None
self.last_error = error
self._retry_at = time.monotonic() + RETRY_AFTER
self._opening = None
opening.set()
def _open_transport(self):
if self.type == CONN_TCP:
return self.__connect_tcp()
if self.type == CONN_SSH:
return self.__connect_ssh()
if self.type == CONN_TLS:
return self.__connect_tls()
if self.type == CONN_SOCKET:
return self.__connect_socket()
raise ValueError(f'"{self.type}" is not a valid connection type')
@property
def connected(self):
@ -121,65 +171,30 @@ class wvmConnection(object):
return 0
def __connection_close_callback(self, connection, reason, opaque=None):
self.connection_state_lock.acquire()
try:
# on server shutdown libvirt module gets freed before the close callbacks are called
# so we just check here if it is still present
if libvirt is not None:
self.last_error = reason
with self.connection_state_lock:
# A late callback of a connection that was already replaced must
# not drop its successor.
if connection is not self.connection:
return
self.last_error = f"Connection closed ({CLOSE_REASONS.get(reason, reason)})"
# prevent other threads from using the connection (in the future)
self.connection = None
finally:
self.connection_state_lock.release()
def __connect_tcp(self):
flags = [libvirt.VIR_CRED_AUTHNAME, libvirt.VIR_CRED_PASSPHRASE]
auth = [flags, self.__libvirt_auth_credentials_callback, None]
uri = f"qemu+tcp://{self.host}/system"
try:
self.connection = libvirt.openAuth(uri, auth, 0)
self.last_error = None
except libvirtError as e:
self.last_error = f"Connection Failed: {str(e)}"
self.connection = None
return libvirt.openAuth(f"qemu+tcp://{self.host}/system", auth, 0)
def __connect_ssh(self):
uri = "qemu+ssh://%s@%s/system" % (self.login, self.host)
try:
self.connection = libvirt.open(uri)
self.last_error = None
except libvirtError as e:
self.last_error = f"Connection Failed: {str(e)} --- " + repr(libvirt.virGetLastError())
self.connection = None
return libvirt.open(f"qemu+ssh://{self.login}@{self.host}/system?command={quote(SSH_COMMAND)}")
def __connect_tls(self):
flags = [libvirt.VIR_CRED_AUTHNAME, libvirt.VIR_CRED_PASSPHRASE]
auth = [flags, self.__libvirt_auth_credentials_callback, None]
uri = "qemu+tls://%s@%s/system" % (self.login, self.host)
try:
self.connection = libvirt.openAuth(uri, auth, 0)
self.last_error = None
except libvirtError as e:
self.last_error = f"Connection Failed: {str(e)}"
self.connection = None
return libvirt.openAuth(f"qemu+tls://{self.login}@{self.host}/system", auth, 0)
def __connect_socket(self):
uri = "qemu:///system"
try:
self.connection = libvirt.open(uri)
self.last_error = None
except libvirtError as e:
self.last_error = f"Connection Failed: {str(e)}"
self.connection = None
return libvirt.open("qemu:///system")
def close(self):
"""
@ -286,7 +301,7 @@ class wvmConnectionManager(object):
self._connections_lock.release()
if not connection.connected:
# (re-)connect; the connection's own lock serializes concurrent attempts
# (re-)connect within CONNECT_TIMEOUT; one open per connection at a time
connection.connect()
if connection.connected:

12
vrtManager/libvirt-ssh Executable file
View file

@ -0,0 +1,12 @@
#!/bin/sh
# ssh for libvirt qemu+ssh connections (the URI's "command" parameter).
# libvirt passes no connect timeout, so an unreachable host would keep this
# ssh process waiting for the kernel's TCP timeout. BatchMode makes ssh fail
# instead of waiting for a password or host key prompt nobody can answer.
# The timeout is fixed: libvirt passes this script no environment or extra
# arguments. It bounds only ssh's TCP connect; WebVirtCloud stops waiting
# after LIBVIRT_CONNECT_TIMEOUT on its own, so an abandoned ssh lives at most
# this long.
# Everything else comes from the user's ssh configuration (aliases,
# ProxyJump, keys).
exec ssh -o ConnectTimeout=10 -o BatchMode=yes "$@"

View file

@ -1,17 +1,19 @@
"""Reachability probe and connections that do not block each other."""
import os
import socket
import threading
import time
import unittest
from unittest.mock import patch
from unittest.mock import MagicMock, patch
from django.conf import settings
if not settings.configured:
settings.configure(MAC_OUI="52:54:10")
from vrtManager.connection import CONN_TCP, wvmConnectionManager
import libvirt
from vrtManager.connection import CONN_SSH, CONN_TCP, SSH_COMMAND, wvmConnectionManager
def closed_port():
@ -61,5 +63,148 @@ class ConnectionManagerTestCase(unittest.TestCase):
slow.join()
def alive_connection():
conn = MagicMock()
conn.isAlive.return_value = 1
return conn
class BoundedOpenTestCase(unittest.TestCase):
"""Opening waits at most CONNECT_TIMEOUT; a failed host is not retried
for RETRY_AFTER; one open per connection at a time."""
def setUp(self):
for name, value in (("wvmEventLoop", MagicMock()), ("CONNECT_TIMEOUT", 0.3), ("RETRY_AFTER", 1.0)):
patcher = patch(f"vrtManager.connection.{name}", value)
patcher.start()
self.addCleanup(patcher.stop)
self.manager = wvmConnectionManager()
self.release = threading.Event()
self.addCleanup(self.release.set)
self.calls = 0
def open_auth(self, result):
def open_auth(uri, auth, flags):
self.calls += 1
self.release.wait(5)
if isinstance(result, Exception):
raise result
return result
return patch("vrtManager.connection.libvirt.openAuth", side_effect=open_auth)
def get(self, host="h"):
started = time.monotonic()
try:
return self.manager.get_connection(host, "u", "p", CONN_TCP), time.monotonic() - started
except libvirt.libvirtError as err:
return err, time.monotonic() - started
def test_a_hanging_open_is_given_up_after_the_timeout(self):
with self.open_auth(alive_connection()):
result, took = self.get()
self.assertIsInstance(result, libvirt.libvirtError)
self.assertIn("did not answer within", str(result))
self.assertLess(took, 1.0)
def test_within_retry_after_the_last_error_comes_at_once(self):
with self.open_auth(alive_connection()):
self.get()
result, took = self.get()
self.assertIn("did not answer within", str(result))
self.assertLess(took, 0.1)
self.assertEqual(self.calls, 1)
def test_a_late_success_is_used(self):
conn = alive_connection()
with self.open_auth(conn):
self.get()
self.release.set()
time.sleep(0.1)
result, took = self.get()
self.assertIs(result, conn)
self.assertEqual(self.calls, 1)
def test_a_failed_host_is_tried_again_after_retry_after(self):
self.release.set()
with self.open_auth(libvirt.libvirtError("refused")):
self.get()
self.get()
self.assertEqual(self.calls, 1)
time.sleep(1.1)
self.get()
self.assertEqual(self.calls, 2)
def test_concurrent_requests_share_one_open(self):
with self.open_auth(alive_connection()):
threads = [threading.Thread(target=self.get) for _ in range(5)]
for t in threads:
t.start()
for t in threads:
t.join()
self.assertEqual(self.calls, 1)
def test_a_stuck_host_does_not_keep_other_hosts_from_connecting(self):
healthy = alive_connection()
def open_auth(uri, auth, flags):
if "stuck" in uri:
self.release.wait(5) # an open that never returns
return healthy
with patch("vrtManager.connection.libvirt.openAuth", side_effect=open_auth):
for i in range(12):
self.get(f"stuck{i}")
result, took = self.get("healthy")
self.assertIs(result, healthy)
def test_an_abandoned_open_is_not_waited_for_again_nor_duplicated(self):
conn = alive_connection()
with self.open_auth(conn):
self.get()
time.sleep(1.1) # past RETRY_AFTER, the open still runs
result, took = self.get()
self.assertIsInstance(result, libvirt.libvirtError)
self.assertLess(took, 0.1)
self.release.set() # the host answers at last
time.sleep(0.1)
result, _ = self.get()
self.assertIs(result, conn)
self.assertEqual(self.calls, 1)
def test_a_late_close_callback_does_not_drop_the_new_connection(self):
self.release.set()
old, new = alive_connection(), alive_connection()
with self.open_auth(old):
self.get()
entry = self.manager._search_connection("h", "u", "p", CONN_TCP)
old.isAlive.return_value = 0 # seen dead, reconnected
with self.open_auth(new):
self.assertIs(self.get()[0], new)
entry._wvmConnection__connection_close_callback(old, 1)
self.assertIs(self.get()[0], new)
entry._wvmConnection__connection_close_callback(new, 2)
self.assertIsNone(entry.connection)
self.assertEqual(entry.last_error, "Connection closed (keepalive timeout)")
def test_a_connection_closed_before_it_is_published_is_an_error(self):
self.release.set()
dead = alive_connection()
dead.isAlive.return_value = 0
with self.open_auth(dead):
result, _ = self.get()
self.assertIn("closed right after opening", str(result))
def test_ssh_runs_ssh_with_a_connect_timeout(self):
self.release.set()
with patch("vrtManager.connection.libvirt.open", return_value=alive_connection()) as libvirt_open:
self.manager.get_connection("host", "root", "", CONN_SSH)
uri = libvirt_open.call_args[0][0]
self.assertTrue(uri.startswith("qemu+ssh://root@host/system?command="))
self.assertTrue(os.access(SSH_COMMAND, os.X_OK))
with open(SSH_COMMAND) as script:
self.assertIn("ConnectTimeout", script.read())
if __name__ == "__main__":
unittest.main()

View file

@ -0,0 +1,249 @@
"""Live checks of bounded connection opens against real libvirt.
Runs only with TEST_LIBVIRT_HOST set (a TCP compute, see instances/livetest.py);
TEST_LIBVIRT_SSH_HOST/_LOGIN add a reachable ssh compute. 192.0.2.1 is
TEST-NET-1: SYN packets to it are dropped, as for a host that went away
without a trace.
TEST_LIBVIRT_SSH_ALIASES=1 adds the ssh tests that need these entries in the
ssh configuration of the user running the tests (SSH_HOST is the ssh compute):
Host wvc-alias
HostName SSH_HOST
Host wvc-relay
HostName 127.0.0.1
Port 2222
HostKeyAlias SSH_HOST
Host wvc-silent
HostName 127.0.0.1
Port 2223
TEST_LIBVIRT_SSH_JUMP=1 adds a ProxyJump test through
Host wvc-jump
HostName SSH_HOST
ProxyJump user@another-host
"""
import os
import shutil
import socket
import subprocess
import threading
import time
import unittest
import libvirt
from vrtManager.connection import CONN_SSH, CONN_TCP, CONNECT_TIMEOUT, connection_manager
UNREACHABLE = "192.0.2.1"
KEEPALIVE_DEATH = 45 # keepalive (5, 5) closes a silent connection after ~30 s
def timed(*args):
started = time.monotonic()
try:
return connection_manager.get_connection(*args), time.monotonic() - started
except libvirt.libvirtError as err:
return err, time.monotonic() - started
def ssh_processes(host):
"""ssh client processes (exact name) whose arguments mention host."""
out = subprocess.run(["pgrep", "-a", "-x", "ssh"], capture_output=True, text=True).stdout
return [line for line in out.splitlines() if host in line]
class Relay:
"""TCP relay that can hold all traffic (frozen) and pass it again (thawed);
a frozen relay looks like a host that went silent."""
def __init__(self, target, port=0, frozen=False):
self.forwarding = threading.Event()
if not frozen:
self.forwarding.set()
self.server = socket.socket()
self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.server.bind(("127.0.0.1", port))
self.server.listen(5)
self.port = self.server.getsockname()[1]
self.target = target
self.closed = False
self.sockets = []
threading.Thread(target=self.accept, daemon=True).start()
def accept(self):
while True:
try:
client, _ = self.server.accept()
except OSError:
return
upstream = socket.create_connection(self.target)
self.sockets += [client, upstream]
for a, b in ((client, upstream), (upstream, client)):
threading.Thread(target=self.pump, args=(a, b), daemon=True).start()
def pump(self, src, dst):
try:
while not self.closed:
if not self.forwarding.wait(0.2):
continue
src.settimeout(0.2)
try:
data = src.recv(65536)
except socket.timeout:
continue
if not data:
break
dst.sendall(data)
except OSError:
pass
def freeze(self):
self.forwarding.clear()
time.sleep(0.5) # let a recv that was already waiting run out
def thaw(self):
self.forwarding.set()
def close(self):
"""Stop, and end the relayed connections, as a host that went away would."""
self.closed = True
self.forwarding.set()
try:
self.server.shutdown(socket.SHUT_RDWR) # wakes the accept; close alone does not
except OSError:
pass
self.server.close()
for sock in self.sockets:
sock.close()
@unittest.skipUnless(os.environ.get("TEST_LIBVIRT_HOST"), "Set TEST_LIBVIRT_HOST to run the live libvirt tests")
class LiveBoundedOpenTestCase(unittest.TestCase):
def setUp(self):
self.host = os.environ["TEST_LIBVIRT_HOST"]
self.login = os.environ.get("TEST_LIBVIRT_LOGIN", "")
self.password = os.environ.get("TEST_LIBVIRT_PASSWORD", "")
self.ssh_host = os.environ.get("TEST_LIBVIRT_SSH_HOST")
self.ssh_login = os.environ.get("TEST_LIBVIRT_SSH_LOGIN", "root")
# Opening
def test_unreachable_tcp_host_fails_within_the_timeout_then_at_once(self):
args = (UNREACHABLE, "u", "p", CONN_TCP)
first, took = timed(*args)
self.assertIsInstance(first, libvirt.libvirtError)
self.assertLess(took, CONNECT_TIMEOUT + 1)
second, took = timed(*args)
self.assertIsInstance(second, libvirt.libvirtError)
self.assertLess(took, 0.5)
@unittest.skipUnless(shutil.which("ssh"), "no ssh client")
def test_unreachable_ssh_host_fails_within_the_timeout_and_leaves_no_ssh(self):
result, took = timed(UNREACHABLE, "root", "", CONN_SSH)
self.assertIsInstance(result, libvirt.libvirtError)
self.assertLess(took, CONNECT_TIMEOUT + 1)
time.sleep(12) # the wrapper's ConnectTimeout is 10 s
self.assertEqual(ssh_processes(UNREACHABLE), [], "an ssh process outlived its connect timeout")
def test_a_daemon_that_never_answers_fails_within_the_timeout(self):
silent = socket.socket()
silent.bind(("127.0.0.1", 0))
silent.listen(5) # accepts (kernel backlog), never answers
self.addCleanup(silent.close)
result, took = timed(f"127.0.0.1:{silent.getsockname()[1]}", "u", "p", CONN_TCP)
self.assertIsInstance(result, libvirt.libvirtError)
self.assertLess(took, CONNECT_TIMEOUT + 1)
def check_slow_open_completes_later(self, args, relay):
first, _ = timed(*args)
self.assertIsInstance(first, libvirt.libvirtError)
relay.thaw()
deadline = time.monotonic() + 15
while time.monotonic() < deadline:
conn, _ = timed(*args)
if not isinstance(conn, libvirt.libvirtError):
break
time.sleep(0.5)
self.assertNotIsInstance(conn, libvirt.libvirtError)
self.assertTrue(conn.getVersion())
def test_a_slow_tcp_open_that_completes_later_is_used(self):
relay = Relay((self.host, 16509), frozen=True)
self.addCleanup(relay.close)
self.check_slow_open_completes_later((f"127.0.0.1:{relay.port}", self.login, self.password, CONN_TCP), relay)
# A connection whose host goes silent
def check_silent_host_is_noticed_and_replaced(self, args, relay):
old = connection_manager.get_connection(*args)
relay.freeze()
outcome = []
def call():
try:
outcome.append(old.getVersion())
except libvirt.libvirtError as err:
outcome.append(err)
caller = threading.Thread(target=call, daemon=True)
caller.start()
caller.join(KEEPALIVE_DEATH) # keepalive must end the call by then
if caller.is_alive():
relay.close() # cut the connection so the stuck call returns
self.fail(f"a call on a silent connection still waits after {KEEPALIVE_DEATH} s")
self.assertIsInstance(outcome[0], libvirt.libvirtError)
relay.thaw()
new, _ = timed(*args)
self.assertNotIsInstance(new, libvirt.libvirtError)
self.assertIsNot(new, old)
self.assertTrue(new.getVersion())
def test_a_silent_tcp_host_is_noticed_and_reconnected(self):
relay = Relay((self.host, 16509))
self.addCleanup(relay.close)
self.check_silent_host_is_noticed_and_replaced((f"127.0.0.1:{relay.port}", self.login, self.password, CONN_TCP), relay)
# ssh
@unittest.skipUnless(os.environ.get("TEST_LIBVIRT_SSH_HOST"), "Set TEST_LIBVIRT_SSH_HOST (and _LOGIN) for an ssh compute")
def test_a_reachable_ssh_host_connects_through_the_wrapper(self):
conn, _ = timed(self.ssh_host, self.ssh_login, "", CONN_SSH)
self.assertNotIsInstance(conn, libvirt.libvirtError)
self.assertIn("command=", conn.getURI())
self.assertTrue(conn.getVersion())
@unittest.skipUnless(os.environ.get("TEST_LIBVIRT_SSH_ALIASES"), "Set TEST_LIBVIRT_SSH_ALIASES=1 (see the module docstring)")
def test_an_ssh_config_alias_is_used(self):
conn, _ = timed("wvc-alias", self.ssh_login, "", CONN_SSH)
self.assertNotIsInstance(conn, libvirt.libvirtError)
self.assertTrue(conn.getVersion())
@unittest.skipUnless(os.environ.get("TEST_LIBVIRT_SSH_ALIASES"), "Set TEST_LIBVIRT_SSH_ALIASES=1 (see the module docstring)")
def test_an_ssh_port_that_never_answers_fails_within_the_timeout(self):
silent = socket.socket()
silent.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
silent.bind(("127.0.0.1", 2223))
silent.listen(5) # no ssh banner ever comes
self.addCleanup(silent.close)
result, took = timed("wvc-silent", self.ssh_login, "", CONN_SSH)
self.assertIsInstance(result, libvirt.libvirtError)
self.assertLess(took, CONNECT_TIMEOUT + 1)
@unittest.skipUnless(os.environ.get("TEST_LIBVIRT_SSH_ALIASES"), "Set TEST_LIBVIRT_SSH_ALIASES=1 (see the module docstring)")
def test_a_slow_ssh_open_that_completes_later_is_used(self):
relay = Relay((self.ssh_host, 22), port=2222, frozen=True)
self.addCleanup(relay.close)
self.check_slow_open_completes_later(("wvc-relay", self.ssh_login, "", CONN_SSH), relay)
@unittest.skipUnless(os.environ.get("TEST_LIBVIRT_SSH_ALIASES"), "Set TEST_LIBVIRT_SSH_ALIASES=1 (see the module docstring)")
def test_a_silent_ssh_host_is_noticed_and_reconnected(self):
relay = Relay((self.ssh_host, 22), port=2222)
self.addCleanup(relay.close)
self.check_silent_host_is_noticed_and_replaced(("wvc-relay", self.ssh_login, "", CONN_SSH), relay)
@unittest.skipUnless(os.environ.get("TEST_LIBVIRT_SSH_JUMP"), "Set TEST_LIBVIRT_SSH_JUMP=1 (see the module docstring)")
def test_ssh_through_a_jump_host(self):
conn, _ = timed("wvc-jump", self.ssh_login, "", CONN_SSH)
self.assertNotIsInstance(conn, libvirt.libvirtError)
self.assertTrue(conn.getVersion())