Skip to content

Commit ade3c74

Browse files
authored
Merge pull request #382 from xcp-ng/gln/optimize-ssh-tests-with-multiplexing-swro
ssh: use ssh multiplexing to speed up the tests
2 parents 999f19e + bf1301e commit ade3c74

3 files changed

Lines changed: 43 additions & 26 deletions

File tree

lib/commands.py

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import base64
22
import logging
3+
import os
4+
import platform
35
import shlex
46
import subprocess
57

@@ -63,7 +65,7 @@ def _ellide_log_lines(log):
6365
return "\n{}".format("\n".join(reduced_message))
6466

6567
def _ssh(hostname_or_ip, cmd, check, simple_output, suppress_fingerprint_warnings,
66-
background, decode, options) -> Union[SSHResult, SSHCommandFailed, str, bytes, None]:
68+
background, decode, options, multiplexing) -> Union[SSHResult, SSHCommandFailed, str, bytes, None]:
6769
opts = list(options)
6870
opts.append('-o "BatchMode yes"')
6971
opts.append('-o "PubkeyAcceptedAlgorithms +ssh-rsa"')
@@ -74,6 +76,18 @@ def _ssh(hostname_or_ip, cmd, check, simple_output, suppress_fingerprint_warning
7476
opts.append('-o "StrictHostKeyChecking no"')
7577
opts.append('-o "LogLevel ERROR"')
7678
opts.append('-o "UserKnownHostsFile /dev/null"')
79+
# ssh multiplexing is not always well supported on windows, so we disable it on that platform.
80+
# It could work with git bash—we might want to check that instead.
81+
# We use the pid in the control path to avoid a race condition on the master socket creation
82+
# when running the tests in parallel. The socket is removed by the ssh client olding the master
83+
# connection when it reaches the timeout.
84+
if multiplexing and platform.system() != "Windows":
85+
opts.append('-o "ControlMaster auto"')
86+
opts.append(f'-o "ControlPath ~/.ssh/control-{os.getpid()}:%h:%p:%r"')
87+
opts.append('-o "ControlPersist 10m"')
88+
opts.append('-o "ServerAliveInterval 10s"')
89+
else:
90+
opts.append('-o "ControlMaster no"')
7791

7892
if isinstance(cmd, str):
7993
command = cmd
@@ -143,47 +157,47 @@ def _ssh(hostname_or_ip, cmd, check, simple_output, suppress_fingerprint_warning
143157
def ssh(hostname_or_ip: HostAddress, cmd: Union[str, List[str]], *, check: bool = True,
144158
simple_output: Literal[True] = True,
145159
suppress_fingerprint_warnings: bool = True, background: Literal[False] = False,
146-
decode: Literal[True] = True, options: List[str] = []) -> str:
160+
decode: Literal[True] = True, options: List[str] = [], multiplexing=True) -> str:
147161
...
148162
@overload
149163
def ssh(hostname_or_ip: HostAddress, cmd: Union[str, List[str]], *, check: bool = True,
150164
simple_output: Literal[True] = True,
151165
suppress_fingerprint_warnings: bool = True, background: Literal[False] = False,
152-
decode: Literal[False], options: List[str] = []) -> bytes:
166+
decode: Literal[False], options: List[str] = [], multiplexing=True) -> bytes:
153167
...
154168
@overload
155169
def ssh(hostname_or_ip: HostAddress, cmd: Union[str, List[str]], *, check: bool = True,
156170
simple_output: Literal[False],
157171
suppress_fingerprint_warnings: bool = True, background: Literal[False] = False,
158-
decode: bool = True, options: List[str] = []) -> SSHResult:
172+
decode: bool = True, options: List[str] = [], multiplexing=True) -> SSHResult:
159173
...
160174
@overload
161175
def ssh(hostname_or_ip: HostAddress, cmd: Union[str, List[str]], *, check: bool = True,
162176
simple_output: Literal[False],
163177
suppress_fingerprint_warnings: bool = True, background: Literal[True],
164-
decode: bool = True, options: List[str] = []) -> None:
178+
decode: bool = True, options: List[str] = [], multiplexing=True) -> None:
165179
...
166180
@overload
167181
def ssh(hostname_or_ip: HostAddress, cmd: Union[str, List[str]], *, check=True,
168182
simple_output: bool = True,
169183
suppress_fingerprint_warnings=True, background: bool = False,
170-
decode: bool = True, options: List[str] = []) \
184+
decode: bool = True, options: List[str] = [], multiplexing=True) \
171185
-> Union[str, bytes, SSHResult, None]:
172186
...
173187
def ssh(hostname_or_ip, cmd, *, check=True, simple_output=True,
174188
suppress_fingerprint_warnings=True,
175-
background=False, decode=True, options=[]):
189+
background=False, decode=True, options=[], multiplexing=True):
176190
result_or_exc = _ssh(hostname_or_ip, cmd, check, simple_output, suppress_fingerprint_warnings,
177-
background, decode, options)
191+
background, decode, options, multiplexing)
178192
if isinstance(result_or_exc, SSHCommandFailed):
179193
raise result_or_exc
180194
else:
181195
return result_or_exc
182196

183197
def ssh_with_result(hostname_or_ip, cmd, suppress_fingerprint_warnings=True,
184-
background=False, decode=True, options=[]) -> SSHResult:
198+
background=False, decode=True, options=[], multiplexing=True) -> SSHResult:
185199
result_or_exc = _ssh(hostname_or_ip, cmd, False, False, suppress_fingerprint_warnings,
186-
background, decode, options)
200+
background, decode, options, multiplexing)
187201
if isinstance(result_or_exc, SSHCommandFailed):
188202
raise result_or_exc
189203
elif isinstance(result_or_exc, SSHResult):

lib/host.py

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
strtobool,
3030
to_xapi_bool,
3131
wait_for,
32-
wait_for_not,
3332
)
3433
from lib.netutil import wrap_ip
3534
from lib.pif import PIF
@@ -92,38 +91,39 @@ def __str__(self):
9291
@overload
9392
def ssh(self, cmd: Union[str, List[str]], *, check: bool = True, simple_output: Literal[True] = True,
9493
suppress_fingerprint_warnings: bool = True, background: Literal[False] = False,
95-
decode: Literal[True] = True) -> str:
94+
decode: Literal[True] = True, multiplexing=True) -> str:
9695
...
9796

9897
@overload
9998
def ssh(self, cmd: Union[str, List[str]], *, check: bool = True, simple_output: Literal[True] = True,
10099
suppress_fingerprint_warnings: bool = True, background: Literal[False] = False,
101-
decode: Literal[False]) -> bytes:
100+
decode: Literal[False], multiplexing=True) -> bytes:
102101
...
103102

104103
@overload
105104
def ssh(self, cmd: Union[str, List[str]], *, check: bool = True, simple_output: Literal[False],
106105
suppress_fingerprint_warnings: bool = True, background: Literal[False] = False,
107-
decode: bool = True) -> commands.SSHResult:
106+
decode: bool = True, multiplexing=True) -> commands.SSHResult:
108107
...
109108

110109
@overload
111110
def ssh(self, cmd: Union[str, List[str]], *, check: bool = True, simple_output: bool = True,
112111
suppress_fingerprint_warnings: bool = True, background: Literal[True],
113-
decode: bool = True) -> None:
112+
decode: bool = True, multiplexing=True) -> None:
114113
...
115114

116115
@overload
117116
def ssh(self, cmd: Union[str, List[str]], *, check: bool = True, simple_output: bool = True,
118-
suppress_fingerprint_warnings: bool = True, background: bool = False, decode: bool = True) \
117+
suppress_fingerprint_warnings: bool = True, background: bool = False, decode: bool = True,
118+
multiplexing=True) \
119119
-> Union[str, bytes, commands.SSHResult, None]:
120120
...
121121

122122
def ssh(self, cmd, *, check=True, simple_output=True, suppress_fingerprint_warnings=True,
123-
background=False, decode=True):
123+
background=False, decode=True, multiplexing=True):
124124
return commands.ssh(self.hostname_or_ip, cmd, check=check, simple_output=simple_output,
125125
suppress_fingerprint_warnings=suppress_fingerprint_warnings,
126-
background=background, decode=decode)
126+
background=background, decode=decode, multiplexing=multiplexing)
127127

128128
def ssh_with_result(self, cmd) -> commands.SSHResult:
129129
# doesn't raise if the command's return is nonzero, unless there's a SSH error
@@ -550,14 +550,11 @@ def yum_restore_saved_state(self):
550550

551551
def reboot(self, verify=False):
552552
logging.info("Reboot host %s" % self)
553-
try:
554-
self.ssh(['reboot'])
555-
except commands.SSHCommandFailed as e:
556-
# ssh connection may get killed by the reboot and terminate with an error code
557-
if "closed by remote host" not in e.stdout:
558-
raise
553+
# Running `reboot` directly immediately disconnects the ssh session and makes the ssh client return with an
554+
# error code. Instead, we schedule the reboot a few seconds later to let the ssh command return properly.
555+
self.ssh(['systemd-run --on-active=2s reboot'])
559556
if verify:
560-
wait_for_not(self.is_enabled, "Wait for host down")
557+
wait_for(lambda: os.system(f"ping -c1 {self.hostname_or_ip} > /dev/null 2>&1"), "Wait for host down")
561558
wait_for(lambda: not os.system(f"ping -c1 {self.hostname_or_ip} > /dev/null 2>&1"),
562559
"Wait for host up", timeout_secs=10 * 60, retry_delay_secs=10)
563560
wait_for(lambda: not os.system(f"nc -zw5 {self.hostname_or_ip} 22"),

tests/storage/fsp/conftest.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import pytest
22

3+
import time
4+
35
# Explicitly import package-scoped fixtures (see explanation in pkgfixtures.py)
6+
from lib.host import Host
47
from pkgfixtures import host_with_saved_yum_state_toolstack_restart
58

69
FSP_REPO_NAME = 'runx'
@@ -18,9 +21,12 @@ def host_with_runx_repo(host_with_saved_yum_state_toolstack_restart):
1821
host.remove_xcpng_repo(FSP_REPO_NAME)
1922

2023
@pytest.fixture(scope='package')
21-
def host_with_fsp(host_with_runx_repo):
24+
def host_with_fsp(host_with_runx_repo: Host):
2225
host = host_with_runx_repo
2326
host.yum_install(FSP_PACKAGES)
27+
# fsp is not listed by xe sm-list until it's actually used, so just wait a few seconds instead
28+
# wait_for(lambda: host.xe('sm-list', {'type': 'fsp'}).strip() != '', "Wait for fsp to be available")
29+
time.sleep(3)
2430
yield host
2531
# teardown: nothing to do, done by host_with_saved_yum_state.
2632

0 commit comments

Comments
 (0)