test-in-vm: add --disk option
[ci.git] / htest / vm / qemu.py
blob6b2ed03f9c026c9dccfe776ca458025d9e34469c
1 #!/usr/bin/env python3
4 # Copyright (c) 2018 Vojtech Horky
5 # All rights reserved.
7 # Redistribution and use in source and binary forms, with or without
8 # modification, are permitted provided that the following conditions
9 # are met:
11 # - Redistributions of source code must retain the above copyright
12 # notice, this list of conditions and the following disclaimer.
13 # - Redistributions in binary form must reproduce the above copyright
14 # notice, this list of conditions and the following disclaimer in the
15 # documentation and/or other materials provided with the distribution.
16 # - The name of the author may not be used to endorse or promote products
17 # derived from this software without specific prior written permission.
19 # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
20 # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
21 # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
22 # IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
23 # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
24 # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
28 # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31 import subprocess
32 import socket
33 import logging
34 import os
35 import sys
37 from PIL import Image
39 from htest.utils import retries, format_command, format_command_pipe
40 from htest.vm.controller import VMController
42 class QemuVMController(VMController):
43 """
44 QEMU VM controller.
45 """
47 config = {
48 'amd64': [
49 'qemu-system-x86_64',
50 '-cdrom', '{BOOT}',
51 '-boot', 'd',
52 '-m', '{MEMORY}',
53 '-usb',
54 '-device', 'intel-hda', '-device', 'hda-duplex',
56 'arm32/integratorcp': [
57 'qemu-system-arm',
58 '-M', 'integratorcp',
59 '-usb',
60 '-kernel', '{BOOT}',
61 '-m', '{MEMORY}',
63 'ia32': [
64 'qemu-system-i386',
65 '-cdrom', '{BOOT}',
66 '-boot', 'd',
67 '-m', '{MEMORY}',
68 '-usb',
69 '-device', 'intel-hda', '-device', 'hda-duplex',
71 'ppc32': [
72 'qemu-system-ppc',
73 '-usb',
74 '-boot', 'd',
75 '-cdrom', '{BOOT}',
76 '-m', '{MEMORY}',
80 ocr_sed = os.path.join(
81 os.path.dirname(os.path.realpath(sys.argv[0])),
82 'ocr.sed'
85 def __init__(self, arch, name, boot_image, disk_image):
86 VMController.__init__(self, 'QEMU-' + arch)
87 self.arch = arch
88 self.booted = False
89 self.name = name
90 self.boot_image = boot_image
91 self.disk_image = disk_image
93 def is_supported(arch):
94 return arch in QemuVMController.config
96 def _get_image_dimensions(self, filename):
97 im = Image.open(filename)
98 width, height = im.size
99 im.close()
100 return ( width, height )
102 def _check_is_up(self):
103 if not self.booted:
104 raise Exception("Machine not launched")
106 def _send_command(self, command):
107 self._check_is_up()
108 self.logger.debug("Sending command '{}'".format(command))
109 command = command + '\n'
110 self.monitor.sendall(command.encode('utf-8'))
112 def _run_command(self, command):
113 proc = subprocess.Popen(command)
114 proc.wait()
115 if proc.returncode != 0:
116 raise Exception("Command {} failed.".format(command))
118 def _run_pipe(self, commands):
119 self.logger.debug("Running pipe {}".format(format_command_pipe(commands)))
120 procs = []
121 for command in commands:
122 inp = None
123 if len(procs) > 0:
124 inp = procs[-1].stdout
125 proc = subprocess.Popen(command, stdout=subprocess.PIPE, stdin=inp)
126 procs.append(proc)
127 procs[-1].communicate()
130 def boot(self, **kwargs):
131 self.monitor_file = self.get_temp('monitor')
132 cmd = []
133 for opt in QemuVMController.config[self.arch]:
134 if opt == '{BOOT}':
135 opt = self.boot_image
136 elif opt == '{MEMORY}':
137 opt = '{}'.format(self.memory)
138 cmd.append(opt)
139 if self.disk_image is not None:
140 cmd.append('-drive')
141 cmd.append('file={},index=0,media=disk,format=raw'.format(self.disk_image))
142 if self.is_headless:
143 cmd.append('-display')
144 cmd.append('none')
145 cmd.append('-monitor')
146 cmd.append('unix:{},server,nowait'.format(self.monitor_file))
147 for opt in self.extra_options:
148 cmd.append(opt)
149 self.logger.debug("Starting QEMU: {}".format(format_command(cmd)))
151 self.proc = subprocess.Popen(cmd)
152 self.monitor = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
153 for xxx in retries(timeout=30, interval=2, name="ctl-socket", message="Failed to connect to QEMU control socket."):
154 try:
155 self.monitor.connect(self.monitor_file)
156 break
157 except FileNotFoundError:
158 pass
159 except ConnectionRefusedError:
160 pass
161 if self.proc.poll():
162 raise Exception("QEMU not started, aborting.")
164 self.booted = True
165 self.logger.info("Machine started.")
167 # Skip past GRUB
168 self.type('\n')
170 uspace_booted = False
171 for xxx in retries(timeout=3*60, interval=5, name="vterm", message="Failed to boot into userspace"):
172 self.vterm = []
173 self.capture_vterm()
174 for l in self.vterm:
175 if l.find('to see a few survival tips') != -1:
176 uspace_booted = True
177 break
178 if uspace_booted:
179 break
181 assert uspace_booted
182 self.full_vterm = self.vterm
184 self.logger.info("Machine booted into userspace.")
186 return
188 def capture_vterm_impl(self):
189 screenshot_full = self.get_temp('screen-full.ppm')
190 screenshot_term = self.get_temp('screen-term.png')
191 screenshot_text = self.get_temp('screen-term.txt')
193 try:
194 os.remove(screenshot_full)
195 except IOError as e:
196 pass
198 self._send_command('screendump ' + screenshot_full)
200 for xxx in retries(timeout=10, interval=1, name="scrdump", message="Failed to capture screen"):
201 try:
202 self._run_command([
203 'convert',
204 screenshot_full,
205 '-crop', '640x480+4+24',
206 '+repage',
207 '-colors', '2',
208 '-monochrome',
209 screenshot_term
211 break
212 except:
213 pass
215 width, height = self._get_image_dimensions(screenshot_term)
216 cols = width // 8
217 rows = height // 16
218 self._run_pipe([
220 'convert',
221 screenshot_term,
222 '-crop', '{}x{}'.format(cols * 8, rows * 16),
223 '+repage',
224 '-crop', '8x16',
225 '+repage',
226 '+adjoin',
227 'txt:-',
230 'sed',
231 '-e', 's|[0-9]*,[0-9]*: ([^)]*)[ ]*#\\([0-9A-Fa-f]\\{6\\}\\).*|\\1|',
232 '-e', 's:^#.*:@:',
233 '-e', 's#000000#0#g',
234 '-e', 's#FFFFFF#F#',
236 [ 'tee', self.get_temp('1.txt') ],
238 'sed',
239 '-e', ':a',
240 '-e', 'N;s#\\n##;s#^@##;/@$/{s#@$##p;d}',
241 '-e', 't a',
243 [ 'tee', self.get_temp('2.txt') ],
245 'sed',
246 '-f', QemuVMController.ocr_sed,
249 'sed',
250 '/../s#.*#?#',
252 [ 'tee', self.get_temp('3.txt') ],
254 'paste',
255 '-sd', '',
258 'fold',
259 '-w', '{}'.format(cols),
261 [ 'tee', self.get_temp('4.txt') ],
263 'head',
264 '-n', '{}'.format(rows),
267 'tee',
268 screenshot_text,
272 self.screenshot_filename = screenshot_full
274 with open(screenshot_text, 'r') as f:
275 lines = [ l.strip('\n') for l in f.readlines() ]
276 self.logger.debug("Captured text:")
277 for l in lines:
278 self.logger.debug("| " + l)
279 return lines
281 def terminate(self):
282 if not self.booted:
283 return
284 self._send_command('quit')
285 VMController.terminate(self)
287 def type(self, what):
288 translations = {
289 ' ': 'spc',
290 '.': 'dot',
291 '-': 'minus',
292 '/': 'slash',
293 '\n': 'ret',
294 '_': 'shift-minus',
295 '|': 'shift-backslash',
296 '=': 'equal',
298 for letter in what:
299 if letter.isupper():
300 letter = 'shift-' + letter.lower()
301 if letter in translations:
302 letter = translations[letter]
303 self._send_command('sendkey ' + letter)
304 pass