diff options
Diffstat (limited to 'meta/lib/oeqa/core/target')
-rw-r--r-- | meta/lib/oeqa/core/target/qemu.py | 40 | ||||
-rw-r--r-- | meta/lib/oeqa/core/target/ssh.py | 81 |
2 files changed, 94 insertions, 27 deletions
diff --git a/meta/lib/oeqa/core/target/qemu.py b/meta/lib/oeqa/core/target/qemu.py index 0f29414df5..d93b3ac94a 100644 --- a/meta/lib/oeqa/core/target/qemu.py +++ b/meta/lib/oeqa/core/target/qemu.py | |||
@@ -8,20 +8,21 @@ import os | |||
8 | import sys | 8 | import sys |
9 | import signal | 9 | import signal |
10 | import time | 10 | import time |
11 | import glob | ||
12 | import subprocess | ||
11 | from collections import defaultdict | 13 | from collections import defaultdict |
12 | 14 | ||
13 | from .ssh import OESSHTarget | 15 | from .ssh import OESSHTarget |
14 | from oeqa.utils.qemurunner import QemuRunner | 16 | from oeqa.utils.qemurunner import QemuRunner |
15 | from oeqa.utils.dump import TargetDumper | ||
16 | 17 | ||
17 | supported_fstypes = ['ext3', 'ext4', 'cpio.gz', 'wic'] | 18 | supported_fstypes = ['ext3', 'ext4', 'cpio.gz', 'wic'] |
18 | 19 | ||
19 | class OEQemuTarget(OESSHTarget): | 20 | class OEQemuTarget(OESSHTarget): |
20 | def __init__(self, logger, server_ip, timeout=300, user='root', | 21 | def __init__(self, logger, server_ip, timeout=300, user='root', |
21 | port=None, machine='', rootfs='', kernel='', kvm=False, slirp=False, | 22 | port=None, machine='', rootfs='', kernel='', kvm=False, slirp=False, |
22 | dump_dir='', dump_host_cmds='', display='', bootlog='', | 23 | dump_dir='', display='', bootlog='', |
23 | tmpdir='', dir_image='', boottime=60, serial_ports=2, | 24 | tmpdir='', dir_image='', boottime=60, serial_ports=2, |
24 | boot_patterns = defaultdict(str), ovmf=False, **kwargs): | 25 | boot_patterns = defaultdict(str), ovmf=False, tmpfsdir=None, **kwargs): |
25 | 26 | ||
26 | super(OEQemuTarget, self).__init__(logger, None, server_ip, timeout, | 27 | super(OEQemuTarget, self).__init__(logger, None, server_ip, timeout, |
27 | user, port) | 28 | user, port) |
@@ -35,17 +36,15 @@ class OEQemuTarget(OESSHTarget): | |||
35 | self.ovmf = ovmf | 36 | self.ovmf = ovmf |
36 | self.use_slirp = slirp | 37 | self.use_slirp = slirp |
37 | self.boot_patterns = boot_patterns | 38 | self.boot_patterns = boot_patterns |
39 | self.dump_dir = dump_dir | ||
40 | self.bootlog = bootlog | ||
38 | 41 | ||
39 | self.runner = QemuRunner(machine=machine, rootfs=rootfs, tmpdir=tmpdir, | 42 | self.runner = QemuRunner(machine=machine, rootfs=rootfs, tmpdir=tmpdir, |
40 | deploy_dir_image=dir_image, display=display, | 43 | deploy_dir_image=dir_image, display=display, |
41 | logfile=bootlog, boottime=boottime, | 44 | logfile=bootlog, boottime=boottime, |
42 | use_kvm=kvm, use_slirp=slirp, dump_dir=dump_dir, | 45 | use_kvm=kvm, use_slirp=slirp, dump_dir=dump_dir, logger=logger, |
43 | dump_host_cmds=dump_host_cmds, logger=logger, | ||
44 | serial_ports=serial_ports, boot_patterns = boot_patterns, | 46 | serial_ports=serial_ports, boot_patterns = boot_patterns, |
45 | use_ovmf=ovmf) | 47 | use_ovmf=ovmf, tmpfsdir=tmpfsdir) |
46 | dump_target_cmds = kwargs.get("testimage_dump_target") | ||
47 | self.target_dumper = TargetDumper(dump_target_cmds, dump_dir, self.runner) | ||
48 | self.target_dumper.create_dir("qemu") | ||
49 | 48 | ||
50 | def start(self, params=None, extra_bootparams=None, runqemuparams=''): | 49 | def start(self, params=None, extra_bootparams=None, runqemuparams=''): |
51 | if self.use_slirp and not self.server_ip: | 50 | if self.use_slirp and not self.server_ip: |
@@ -68,7 +67,28 @@ class OEQemuTarget(OESSHTarget): | |||
68 | self.server_ip = self.runner.server_ip | 67 | self.server_ip = self.runner.server_ip |
69 | else: | 68 | else: |
70 | self.stop() | 69 | self.stop() |
71 | raise RuntimeError("FAILED to start qemu - check the task log and the boot log") | 70 | # Display the first 20 lines of top and |
71 | # last 20 lines of the bootlog when the | ||
72 | # target is not being booted up. | ||
73 | topfile = glob.glob(self.dump_dir + "/*_qemu/host_*_top") | ||
74 | msg = "\n\n===== start: snippet =====\n\n" | ||
75 | for f in topfile: | ||
76 | msg += "file: %s\n\n" % f | ||
77 | with open(f) as tf: | ||
78 | for x in range(20): | ||
79 | msg += next(tf) | ||
80 | msg += "\n\n===== end: snippet =====\n\n" | ||
81 | blcmd = ["tail", "-20", self.bootlog] | ||
82 | msg += "===== start: snippet =====\n\n" | ||
83 | try: | ||
84 | out = subprocess.check_output(blcmd, stderr=subprocess.STDOUT, timeout=1).decode('utf-8') | ||
85 | msg += "file: %s\n\n" % self.bootlog | ||
86 | msg += out | ||
87 | except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError) as err: | ||
88 | msg += "Error running command: %s\n%s\n" % (blcmd, err) | ||
89 | msg += "\n\n===== end: snippet =====\n" | ||
90 | |||
91 | raise RuntimeError("FAILED to start qemu - check the task log and the boot log %s" % (msg)) | ||
72 | 92 | ||
73 | def stop(self): | 93 | def stop(self): |
74 | self.runner.stop() | 94 | self.runner.stop() |
diff --git a/meta/lib/oeqa/core/target/ssh.py b/meta/lib/oeqa/core/target/ssh.py index 461448dbc5..09cdd14c75 100644 --- a/meta/lib/oeqa/core/target/ssh.py +++ b/meta/lib/oeqa/core/target/ssh.py | |||
@@ -34,12 +34,17 @@ class OESSHTarget(OETarget): | |||
34 | self.timeout = timeout | 34 | self.timeout = timeout |
35 | self.user = user | 35 | self.user = user |
36 | ssh_options = [ | 36 | ssh_options = [ |
37 | '-o', 'ServerAliveCountMax=2', | ||
38 | '-o', 'ServerAliveInterval=30', | ||
37 | '-o', 'UserKnownHostsFile=/dev/null', | 39 | '-o', 'UserKnownHostsFile=/dev/null', |
38 | '-o', 'StrictHostKeyChecking=no', | 40 | '-o', 'StrictHostKeyChecking=no', |
39 | '-o', 'LogLevel=ERROR' | 41 | '-o', 'LogLevel=ERROR' |
40 | ] | 42 | ] |
43 | scp_options = [ | ||
44 | '-r' | ||
45 | ] | ||
41 | self.ssh = ['ssh', '-l', self.user ] + ssh_options | 46 | self.ssh = ['ssh', '-l', self.user ] + ssh_options |
42 | self.scp = ['scp'] + ssh_options | 47 | self.scp = ['scp'] + ssh_options + scp_options |
43 | if port: | 48 | if port: |
44 | self.ssh = self.ssh + [ '-p', port ] | 49 | self.ssh = self.ssh + [ '-p', port ] |
45 | self.scp = self.scp + [ '-P', port ] | 50 | self.scp = self.scp + [ '-P', port ] |
@@ -67,7 +72,7 @@ class OESSHTarget(OETarget): | |||
67 | 72 | ||
68 | return (status, output) | 73 | return (status, output) |
69 | 74 | ||
70 | def run(self, command, timeout=None): | 75 | def run(self, command, timeout=None, ignore_status=True): |
71 | """ | 76 | """ |
72 | Runs command in target. | 77 | Runs command in target. |
73 | 78 | ||
@@ -86,10 +91,9 @@ class OESSHTarget(OETarget): | |||
86 | else: | 91 | else: |
87 | processTimeout = self.timeout | 92 | processTimeout = self.timeout |
88 | 93 | ||
89 | status, output = self._run(sshCmd, processTimeout, True) | 94 | status, output = self._run(sshCmd, processTimeout, ignore_status) |
90 | self.logger.debug('Command: %s\nOutput: %s\n' % (command, output)) | 95 | self.logger.debug('Command: %s\nStatus: %d Output: %s\n' % (command, status, output)) |
91 | if (status == 255) and (('No route to host') in output): | 96 | |
92 | self.target_dumper.dump_target() | ||
93 | return (status, output) | 97 | return (status, output) |
94 | 98 | ||
95 | def copyTo(self, localSrc, remoteDst): | 99 | def copyTo(self, localSrc, remoteDst): |
@@ -207,27 +211,41 @@ def SSHCall(command, logger, timeout=None, **opts): | |||
207 | def run(): | 211 | def run(): |
208 | nonlocal output | 212 | nonlocal output |
209 | nonlocal process | 213 | nonlocal process |
214 | output_raw = b'' | ||
210 | starttime = time.time() | 215 | starttime = time.time() |
211 | process = subprocess.Popen(command, **options) | 216 | process = subprocess.Popen(command, **options) |
217 | has_timeout = False | ||
212 | if timeout: | 218 | if timeout: |
213 | endtime = starttime + timeout | 219 | endtime = starttime + timeout |
214 | eof = False | 220 | eof = False |
215 | while time.time() < endtime and not eof: | 221 | os.set_blocking(process.stdout.fileno(), False) |
216 | logger.debug('time: %s, endtime: %s' % (time.time(), endtime)) | 222 | while not has_timeout and not eof: |
217 | try: | 223 | try: |
224 | logger.debug('Waiting for process output: time: %s, endtime: %s' % (time.time(), endtime)) | ||
218 | if select.select([process.stdout], [], [], 5)[0] != []: | 225 | if select.select([process.stdout], [], [], 5)[0] != []: |
219 | reader = codecs.getreader('utf-8')(process.stdout, 'ignore') | 226 | # wait a bit for more data, tries to avoid reading single characters |
220 | data = reader.read(1024, 4096) | 227 | time.sleep(0.2) |
228 | data = process.stdout.read() | ||
221 | if not data: | 229 | if not data: |
222 | process.stdout.close() | ||
223 | eof = True | 230 | eof = True |
224 | else: | 231 | else: |
225 | output += data | 232 | output_raw += data |
226 | logger.debug('Partial data from SSH call: %s' % data) | 233 | # ignore errors to capture as much as possible |
234 | logger.debug('Partial data from SSH call:\n%s' % data.decode('utf-8', errors='ignore')) | ||
227 | endtime = time.time() + timeout | 235 | endtime = time.time() + timeout |
228 | except InterruptedError: | 236 | except InterruptedError: |
237 | logger.debug('InterruptedError') | ||
238 | continue | ||
239 | except BlockingIOError: | ||
240 | logger.debug('BlockingIOError') | ||
229 | continue | 241 | continue |
230 | 242 | ||
243 | if time.time() >= endtime: | ||
244 | logger.debug('SSHCall has timeout! Time: %s, endtime: %s' % (time.time(), endtime)) | ||
245 | has_timeout = True | ||
246 | |||
247 | process.stdout.close() | ||
248 | |||
231 | # process hasn't returned yet | 249 | # process hasn't returned yet |
232 | if not eof: | 250 | if not eof: |
233 | process.terminate() | 251 | process.terminate() |
@@ -235,16 +253,42 @@ def SSHCall(command, logger, timeout=None, **opts): | |||
235 | try: | 253 | try: |
236 | process.kill() | 254 | process.kill() |
237 | except OSError: | 255 | except OSError: |
256 | logger.debug('OSError when killing process') | ||
238 | pass | 257 | pass |
239 | endtime = time.time() - starttime | 258 | endtime = time.time() - starttime |
240 | lastline = ("\nProcess killed - no output for %d seconds. Total" | 259 | lastline = ("\nProcess killed - no output for %d seconds. Total" |
241 | " running time: %d seconds." % (timeout, endtime)) | 260 | " running time: %d seconds." % (timeout, endtime)) |
242 | logger.debug('Received data from SSH call %s ' % lastline) | 261 | logger.debug('Received data from SSH call:\n%s ' % lastline) |
243 | output += lastline | 262 | output += lastline |
263 | process.wait() | ||
244 | 264 | ||
245 | else: | 265 | else: |
246 | output = process.communicate()[0].decode('utf-8', errors='ignore') | 266 | output_raw = process.communicate()[0] |
247 | logger.debug('Data from SSH call: %s' % output.rstrip()) | 267 | |
268 | output = output_raw.decode('utf-8', errors='ignore') | ||
269 | logger.debug('Data from SSH call:\n%s' % output.rstrip()) | ||
270 | |||
271 | # timout or not, make sure process exits and is not hanging | ||
272 | if process.returncode == None: | ||
273 | try: | ||
274 | process.wait(timeout=5) | ||
275 | except TimeoutExpired: | ||
276 | try: | ||
277 | process.kill() | ||
278 | except OSError: | ||
279 | logger.debug('OSError') | ||
280 | pass | ||
281 | process.wait() | ||
282 | |||
283 | if has_timeout: | ||
284 | # Version of openssh before 8.6_p1 returns error code 0 when killed | ||
285 | # by a signal, when the timeout occurs we will receive a 0 error | ||
286 | # code because the process is been terminated and it's wrong because | ||
287 | # that value means success, but the process timed out. | ||
288 | # Afterwards, from version 8.6_p1 onwards, the returned code is 255. | ||
289 | # Fix this behaviour by checking the return code | ||
290 | if process.returncode == 0: | ||
291 | process.returncode = 255 | ||
248 | 292 | ||
249 | options = { | 293 | options = { |
250 | "stdout": subprocess.PIPE, | 294 | "stdout": subprocess.PIPE, |
@@ -271,6 +315,9 @@ def SSHCall(command, logger, timeout=None, **opts): | |||
271 | # whilst running and ensure we don't leave a process behind. | 315 | # whilst running and ensure we don't leave a process behind. |
272 | if process.poll() is None: | 316 | if process.poll() is None: |
273 | process.kill() | 317 | process.kill() |
318 | if process.returncode == None: | ||
319 | process.wait() | ||
274 | logger.debug('Something went wrong, killing SSH process') | 320 | logger.debug('Something went wrong, killing SSH process') |
275 | raise | 321 | raise |
276 | return (process.wait(), output.rstrip()) | 322 | |
323 | return (process.returncode, output.rstrip()) | ||