generate-all-test-cases: separate generic parts of BaseRunner
The `BaseRunner` class represented a base QEMU runner. Rename it to `BaseQEMURunner` and extract parts which are not QEMU specific to a new `BaseRunner` class. This new base class will be later used as a baseline for other types of runners which don't rely on QEMU. Signed-off-by: Tomas Hozza <thozza@redhat.com>
This commit is contained in:
parent
41b1d75187
commit
fb2361907f
1 changed files with 123 additions and 103 deletions
|
|
@ -128,6 +128,109 @@ class RunnerMountPoint:
|
||||||
|
|
||||||
|
|
||||||
class BaseRunner(contextlib.AbstractContextManager):
|
class BaseRunner(contextlib.AbstractContextManager):
|
||||||
|
"""
|
||||||
|
Base class representing a generic runner, which is used for generating image
|
||||||
|
test case definitions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, hostname, username="root", password=None, port=22):
|
||||||
|
self.hostname = hostname
|
||||||
|
self.port = port
|
||||||
|
self.username = username
|
||||||
|
self.password = password
|
||||||
|
self.runner_ready = False
|
||||||
|
|
||||||
|
def run_command(self, command):
|
||||||
|
"""
|
||||||
|
Runs a given command on the Runner over ssh in a blocking fashion.
|
||||||
|
|
||||||
|
Calling this method before is_ready() returned True has undefined
|
||||||
|
behavior.
|
||||||
|
|
||||||
|
Returns stdin, stdout, stderr from the run command.
|
||||||
|
"""
|
||||||
|
ssh = paramiko.SSHClient()
|
||||||
|
# don't ask / fail on unknown remote host fingerprint, just accept any
|
||||||
|
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
try:
|
||||||
|
ssh.connect(self.hostname, self.port, self.username, self.password)
|
||||||
|
ssh_tansport = ssh.get_transport()
|
||||||
|
channel = ssh_tansport.open_session()
|
||||||
|
# don't log commands when the vm is not yet ready for use
|
||||||
|
if self.runner_ready:
|
||||||
|
log.debug("Running on runner: '%s'", command)
|
||||||
|
channel.exec_command(command)
|
||||||
|
stdout = ""
|
||||||
|
stderr = ""
|
||||||
|
# wait for the command to finish
|
||||||
|
while True:
|
||||||
|
while channel.recv_ready():
|
||||||
|
stdout += channel.recv(1024).decode()
|
||||||
|
while channel.recv_stderr_ready():
|
||||||
|
stderr += channel.recv_stderr(1024).decode()
|
||||||
|
if channel.exit_status_ready():
|
||||||
|
break
|
||||||
|
time.sleep(0.01)
|
||||||
|
returncode = channel.recv_exit_status()
|
||||||
|
except Exception as e:
|
||||||
|
# don't log errors when vm is not ready yet, because there are many errors
|
||||||
|
if self.runner_ready:
|
||||||
|
log.error("Running command over SSH failed: %s", str(e))
|
||||||
|
raise e
|
||||||
|
finally:
|
||||||
|
# closes the underlying transport
|
||||||
|
ssh.close()
|
||||||
|
|
||||||
|
return stdout, stderr, returncode
|
||||||
|
|
||||||
|
def run_command_check_call(self, command):
|
||||||
|
"""
|
||||||
|
Runs a command on the runner over SSH in a similar fashion as subprocess.check_call()
|
||||||
|
"""
|
||||||
|
_, _, ret = self.run_command(command)
|
||||||
|
if ret != 0:
|
||||||
|
raise subprocess.CalledProcessError(ret, command)
|
||||||
|
|
||||||
|
def wait_until_ready(self, timeout=None, retry_sec=15):
|
||||||
|
"""
|
||||||
|
Waits for the runner until it is ready for use. This is determined by
|
||||||
|
executing the 'is_ready()' method in a blocking fashion.
|
||||||
|
|
||||||
|
This method is blocking, unless 'timeout' is provided.
|
||||||
|
"""
|
||||||
|
now = time.time()
|
||||||
|
while not self.is_ready():
|
||||||
|
if timeout is not None and time.time() > (now + timeout):
|
||||||
|
raise subprocess.TimeoutExpired("wait_until_ready()", timeout)
|
||||||
|
time.sleep(retry_sec)
|
||||||
|
|
||||||
|
def is_ready(self, command="id"):
|
||||||
|
"""
|
||||||
|
Returns True if the runner is ready to be used, which is determined by
|
||||||
|
running the provided 'command', which must exit with 0 return value.
|
||||||
|
"""
|
||||||
|
if self.runner_ready:
|
||||||
|
return True
|
||||||
|
|
||||||
|
try:
|
||||||
|
# run command to determine if the host is ready for use
|
||||||
|
self.run_command_check_call(command)
|
||||||
|
except (paramiko.ChannelException,
|
||||||
|
paramiko.ssh_exception.NoValidConnectionsError,
|
||||||
|
paramiko.ssh_exception.SSHException,
|
||||||
|
EOFError,
|
||||||
|
socket.timeout,
|
||||||
|
subprocess.CalledProcessError) as _:
|
||||||
|
# ignore all reasonable paramiko exceptions, this is useful when the host is still stating up
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
log.debug("Runner is ready for use")
|
||||||
|
self.runner_ready = True
|
||||||
|
|
||||||
|
return self.runner_ready
|
||||||
|
|
||||||
|
|
||||||
|
class BaseQEMURunner(BaseRunner):
|
||||||
"""
|
"""
|
||||||
Base class representing a QEMU VM runner, which is used for generating image
|
Base class representing a QEMU VM runner, which is used for generating image
|
||||||
test case definitions.
|
test case definitions.
|
||||||
|
|
@ -142,7 +245,8 @@ class BaseRunner(contextlib.AbstractContextManager):
|
||||||
# the actual command to use for running QEMU VM
|
# the actual command to use for running QEMU VM
|
||||||
QEMU_CMD = None
|
QEMU_CMD = None
|
||||||
|
|
||||||
def __init__(self, image, user, passwd, cdrom_iso=None, mount_points=None):
|
def __init__(self, image, username, password, cdrom_iso=None, mount_points=None):
|
||||||
|
super().__init__("localhost", username=username, password=password)
|
||||||
self._check_qemu_bin()
|
self._check_qemu_bin()
|
||||||
|
|
||||||
# path to image to run
|
# path to image to run
|
||||||
|
|
@ -153,16 +257,10 @@ class BaseRunner(contextlib.AbstractContextManager):
|
||||||
self.mount_points = mount_points if mount_points else list()
|
self.mount_points = mount_points if mount_points else list()
|
||||||
# Popen object of the qemu process
|
# Popen object of the qemu process
|
||||||
self.vm = None
|
self.vm = None
|
||||||
self.vm_ready = False
|
|
||||||
# following values are set after the VM is terminated
|
# following values are set after the VM is terminated
|
||||||
self.vm_return_code = None
|
self.vm_return_code = None
|
||||||
self.vm_stdout = None
|
self.vm_stdout = None
|
||||||
self.vm_stderr = None
|
self.vm_stderr = None
|
||||||
# credentials used to SSH to the VM
|
|
||||||
self.vm_user = user
|
|
||||||
self.vm_pass = passwd
|
|
||||||
# port on host to forward the guest's SSH port (22) to
|
|
||||||
self.host_fwd_ssh_port = None
|
|
||||||
|
|
||||||
def _check_qemu_bin(self):
|
def _check_qemu_bin(self):
|
||||||
"""
|
"""
|
||||||
|
|
@ -206,9 +304,9 @@ class BaseRunner(contextlib.AbstractContextManager):
|
||||||
# get a random free TCP port. This should work in majority of cases
|
# get a random free TCP port. This should work in majority of cases
|
||||||
with contextlib.closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock:
|
with contextlib.closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock:
|
||||||
sock.bind(('localhost', 0))
|
sock.bind(('localhost', 0))
|
||||||
self.host_fwd_ssh_port = sock.getsockname()[1]
|
self.port = sock.getsockname()[1]
|
||||||
|
|
||||||
return ["-net", "user,hostfwd=tcp::{}-:22".format(self.host_fwd_ssh_port)]
|
return ["-net", "user,hostfwd=tcp::{}-:22".format(self.port)]
|
||||||
|
|
||||||
def _run_qemu_cmd(self, qemu_cmd):
|
def _run_qemu_cmd(self, qemu_cmd):
|
||||||
"""
|
"""
|
||||||
|
|
@ -251,9 +349,9 @@ class BaseRunner(contextlib.AbstractContextManager):
|
||||||
log.info(
|
log.info(
|
||||||
"Runner started. You can SSH to it once it has been configured:" +\
|
"Runner started. You can SSH to it once it has been configured:" +\
|
||||||
"'ssh %s@localhost -p %d' using password: '%s'",
|
"'ssh %s@localhost -p %d' using password: '%s'",
|
||||||
self.vm_user,
|
self.username,
|
||||||
self.host_fwd_ssh_port,
|
self.port,
|
||||||
self.vm_pass
|
self.password
|
||||||
)
|
)
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
|
|
@ -285,77 +383,14 @@ class BaseRunner(contextlib.AbstractContextManager):
|
||||||
self.vm_return_code, self.vm_stdout, self.vm_stderr)
|
self.vm_return_code, self.vm_stdout, self.vm_stderr)
|
||||||
|
|
||||||
self.vm = None
|
self.vm = None
|
||||||
self.vm_ready = False
|
self.runner_ready = False
|
||||||
|
|
||||||
def run_command(self, command):
|
def is_ready(self, command="ls /var/lib/cloud/instance/boot-finished"):
|
||||||
"""
|
|
||||||
Runs a given command on the VM over ssh in a blocking fashion.
|
|
||||||
|
|
||||||
Calling this method before is_ready() returned True has undefined
|
|
||||||
behavior.
|
|
||||||
|
|
||||||
Returns stdin, stdout, stderr from the run command.
|
|
||||||
"""
|
|
||||||
ssh = paramiko.SSHClient()
|
|
||||||
# don't ask / fail on unknown remote host fingerprint, just accept any
|
|
||||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
|
||||||
try:
|
|
||||||
ssh.connect("localhost", self.host_fwd_ssh_port, self.vm_user, self.vm_pass)
|
|
||||||
ssh_tansport = ssh.get_transport()
|
|
||||||
channel = ssh_tansport.open_session()
|
|
||||||
# don't log commands when the vm is not yet ready for use
|
|
||||||
if self.vm_ready:
|
|
||||||
log.debug("Running on VM: '%s'", command)
|
|
||||||
channel.exec_command(command)
|
|
||||||
stdout = ""
|
|
||||||
stderr = ""
|
|
||||||
# wait for the command to finish
|
|
||||||
while True:
|
|
||||||
while channel.recv_ready():
|
|
||||||
stdout += channel.recv(1024).decode()
|
|
||||||
while channel.recv_stderr_ready():
|
|
||||||
stderr += channel.recv_stderr(1024).decode()
|
|
||||||
if channel.exit_status_ready():
|
|
||||||
break
|
|
||||||
time.sleep(0.01)
|
|
||||||
returncode = channel.recv_exit_status()
|
|
||||||
except Exception as e:
|
|
||||||
# don't log errors when vm is not ready yet, because there are many errors
|
|
||||||
if self.vm_ready:
|
|
||||||
log.error("Running command over ssh failed: %s", str(e))
|
|
||||||
raise e
|
|
||||||
finally:
|
|
||||||
# closes the underlying transport
|
|
||||||
ssh.close()
|
|
||||||
|
|
||||||
return stdout, stderr, returncode
|
|
||||||
|
|
||||||
def run_command_check_call(self, command):
|
|
||||||
"""
|
|
||||||
Runs a command on the VM over ssh in a similar fashion as subprocess.check_call()
|
|
||||||
"""
|
|
||||||
_, _, ret = self.run_command(command)
|
|
||||||
if ret != 0:
|
|
||||||
raise subprocess.CalledProcessError(ret, command)
|
|
||||||
|
|
||||||
def wait_until_ready(self, timeout=None):
|
|
||||||
"""
|
|
||||||
Waits for the VM to be ready for use (cloud-init configuration finished).
|
|
||||||
|
|
||||||
If timeout is provided
|
|
||||||
"""
|
|
||||||
now = time.time()
|
|
||||||
while not self.is_ready():
|
|
||||||
if timeout is not None and time.time() > (now + timeout):
|
|
||||||
raise subprocess.TimeoutExpired("wait_until_ready()", timeout)
|
|
||||||
time.sleep(15)
|
|
||||||
|
|
||||||
def is_ready(self):
|
|
||||||
"""
|
"""
|
||||||
Returns True if the VM is ready to be used.
|
Returns True if the VM is ready to be used.
|
||||||
VM is ready after the cloud-init setup is finished.
|
VM is ready after the cloud-init setup is finished.
|
||||||
"""
|
"""
|
||||||
if self.vm_ready:
|
if self.runner_ready:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# check if the runner didn't terminate unexpectedly before being ready
|
# check if the runner didn't terminate unexpectedly before being ready
|
||||||
|
|
@ -371,22 +406,7 @@ class BaseRunner(contextlib.AbstractContextManager):
|
||||||
qemu_bin = self.QEMU_BIN
|
qemu_bin = self.QEMU_BIN
|
||||||
raise RuntimeError(f"'{qemu_bin}' process ended before being ready to use")
|
raise RuntimeError(f"'{qemu_bin}' process ended before being ready to use")
|
||||||
|
|
||||||
try:
|
return super().is_ready(command)
|
||||||
# cloud-init touches /var/lib/cloud/instance/boot-finished after it finishes
|
|
||||||
self.run_command_check_call("ls /var/lib/cloud/instance/boot-finished")
|
|
||||||
except (paramiko.ChannelException,
|
|
||||||
paramiko.ssh_exception.NoValidConnectionsError,
|
|
||||||
paramiko.ssh_exception.SSHException,
|
|
||||||
EOFError,
|
|
||||||
socket.timeout,
|
|
||||||
subprocess.CalledProcessError) as _:
|
|
||||||
# ignore all reasonable paramiko exceptions, this is useful when the VM is still stating up
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
log.debug("VM is ready for use")
|
|
||||||
self.vm_ready = True
|
|
||||||
|
|
||||||
return self.vm_ready
|
|
||||||
|
|
||||||
def mount_mount_points(self):
|
def mount_mount_points(self):
|
||||||
"""
|
"""
|
||||||
|
|
@ -476,7 +496,7 @@ class BaseRunner(contextlib.AbstractContextManager):
|
||||||
return iso_path
|
return iso_path
|
||||||
|
|
||||||
|
|
||||||
class X86_64Runner(BaseRunner):
|
class X86_64QEMURunner(BaseQEMURunner):
|
||||||
"""
|
"""
|
||||||
VM Runner for x86_64 architecture
|
VM Runner for x86_64 architecture
|
||||||
"""
|
"""
|
||||||
|
|
@ -494,7 +514,7 @@ class X86_64Runner(BaseRunner):
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class Ppc64Runner(BaseRunner):
|
class Ppc64QEMURunner(BaseQEMURunner):
|
||||||
"""
|
"""
|
||||||
VM Runner for ppc64le architecture
|
VM Runner for ppc64le architecture
|
||||||
"""
|
"""
|
||||||
|
|
@ -511,7 +531,7 @@ class Ppc64Runner(BaseRunner):
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class Aarch64Runner(BaseRunner):
|
class Aarch64QEMURunner(BaseQEMURunner):
|
||||||
"""
|
"""
|
||||||
VM Runner for aarch64 architecture
|
VM Runner for aarch64 architecture
|
||||||
"""
|
"""
|
||||||
|
|
@ -535,7 +555,7 @@ class Aarch64Runner(BaseRunner):
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class S390xRunner(BaseRunner):
|
class S390xQEMURunner(BaseQEMURunner):
|
||||||
"""
|
"""
|
||||||
VM Runner for s390x architecture
|
VM Runner for s390x architecture
|
||||||
"""
|
"""
|
||||||
|
|
@ -596,10 +616,10 @@ class TestCaseMatrixGenerator(contextlib.AbstractContextManager):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
ARCH_RUNNER_MAP = {
|
ARCH_RUNNER_MAP = {
|
||||||
"x86_64": X86_64Runner,
|
"x86_64": X86_64QEMURunner,
|
||||||
"aarch64": Aarch64Runner,
|
"aarch64": Aarch64QEMURunner,
|
||||||
"ppc64le": Ppc64Runner,
|
"ppc64le": Ppc64QEMURunner,
|
||||||
"s390x": S390xRunner
|
"s390x": S390xQEMURunner
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, images, ci_userdata, arch_gen_matrix, output, keep_image_info):
|
def __init__(self, images, ci_userdata, arch_gen_matrix, output, keep_image_info):
|
||||||
|
|
@ -756,7 +776,7 @@ class TestCaseMatrixGenerator(contextlib.AbstractContextManager):
|
||||||
"""
|
"""
|
||||||
# use the same CDROM ISO image for all VMs
|
# use the same CDROM ISO image for all VMs
|
||||||
with tempfile.TemporaryDirectory(prefix="osbuild-composer-test-gen-") as tmpdir:
|
with tempfile.TemporaryDirectory(prefix="osbuild-composer-test-gen-") as tmpdir:
|
||||||
cdrom_iso = BaseRunner.prepare_cloud_init_cdrom(self.ci_userdata, tmpdir)
|
cdrom_iso = BaseQEMURunner.prepare_cloud_init_cdrom(self.ci_userdata, tmpdir)
|
||||||
|
|
||||||
# Load user / password from the cloud-init user-data
|
# Load user / password from the cloud-init user-data
|
||||||
if os.path.isdir(self.ci_userdata):
|
if os.path.isdir(self.ci_userdata):
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue