Add --memory switch
[ci.git] / htest / vm / qemu.py
blob3b7dad72751cc9fdfc7cab6ec0967bfeb0a2f4b9
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
35 from PIL import Image
37 from htest.utils import retries, format_command, format_command_pipe
38 from htest.vm.controller import VMController
40 class QemuVMController(VMController):
41 """
42 QEMU VM controller.
43 """
45 config = {
46 'amd64': [
47 'qemu-system-x86_64',
48 '-cdrom', '{BOOT}',
49 '-m', '{MEMORY}',
50 '-usb',
52 'arm32/integratorcp': [
53 'qemu-system-arm',
54 '-M', 'integratorcp',
55 '-usb',
56 '-kernel', '{BOOT}',
57 '-m', '{MEMORY}',
59 'ia32': [
60 'qemu-system-i386',
61 '-cdrom', '{BOOT}',
62 '-m', '{MEMORY}',
63 '-usb',
65 'ppc32': [
66 'qemu-system-ppc',
67 '-usb',
68 '-boot', 'd',
69 '-cdrom', '{BOOT}',
70 '-m', '{MEMORY}',
74 def __init__(self, arch, name, boot_image):
75 VMController.__init__(self, 'QEMU-' + arch)
76 self.arch = arch
77 self.booted = False
78 self.name = name
79 self.boot_image = boot_image
80 self.logger = logging.getLogger('qemu-{}'.format(name))
82 def is_supported(arch):
83 return arch in QemuVMController.config
85 def _get_image_dimensions(self, filename):
86 im = Image.open(filename)
87 width, height = im.size
88 im.close()
89 return ( width, height )
91 def _check_is_up(self):
92 if not self.booted:
93 raise Exception("Machine not launched")
95 def _send_command(self, command):
96 self._check_is_up()
97 self.logger.debug("Sending command '{}'".format(command))
98 command = command + '\n'
99 self.monitor.sendall(command.encode('utf-8'))
101 def _run_command(self, command):
102 proc = subprocess.Popen(command)
103 proc.wait()
104 if proc.returncode != 0:
105 raise Exception("Command {} failed.".format(command))
107 def _run_pipe(self, commands):
108 self.logger.debug("Running pipe {}".format(format_command_pipe(commands)))
109 procs = []
110 for command in commands:
111 inp = None
112 if len(procs) > 0:
113 inp = procs[-1].stdout
114 proc = subprocess.Popen(command, stdout=subprocess.PIPE, stdin=inp)
115 procs.append(proc)
116 procs[-1].communicate()
119 def boot(self, **kwargs):
120 self.monitor_file = self.get_temp('monitor')
121 cmd = []
122 for opt in QemuVMController.config[self.arch]:
123 if opt == '{BOOT}':
124 opt = self.boot_image
125 elif opt == '{MEMORY}':
126 opt = '{}'.format(self.memory)
127 cmd.append(opt)
128 cmd.append('-monitor')
129 cmd.append('unix:{},server,nowait'.format(self.monitor_file))
130 self.logger.debug("Starting QEMU: {}".format(format_command(cmd)))
132 self.proc = subprocess.Popen(cmd)
133 self.monitor = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
134 for xxx in retries(timeout=30, interval=2, name="ctl-socket", message="Failed to connect to QEMU control socket."):
135 try:
136 self.monitor.connect(self.monitor_file)
137 break
138 except FileNotFoundError:
139 pass
140 except ConnectionRefusedError:
141 pass
142 if self.proc.poll():
143 raise Exception("QEMU not started, aborting.")
145 self.booted = True
146 self.logger.info("Machine started.")
148 # Skip past GRUB
149 self.type('\n')
151 uspace_booted = False
152 for xxx in retries(timeout=3*60, interval=5, name="vterm", message="Failed to boot into userspace"):
153 self.vterm = []
154 self.capture_vterm()
155 for l in self.vterm:
156 if l.find('to see a few survival tips') != -1:
157 uspace_booted = True
158 break
159 if uspace_booted:
160 break
162 assert uspace_booted
163 self.full_vterm = self.vterm
165 self.logger.info("Machine booted into userspace.")
167 return
169 def capture_vterm_impl(self):
170 screenshot_full = self.get_temp('screen-full.ppm')
171 screenshot_term = self.get_temp('screen-term.png')
172 screenshot_text = self.get_temp('screen-term.txt')
174 self._send_command('screendump ' + screenshot_full)
176 for xxx in retries(timeout=5, interval=1, name="scrdump", message="Failed to capture screen"):
177 try:
178 self._run_command([
179 'convert',
180 screenshot_full,
181 '-crop', '640x480+4+24',
182 '+repage',
183 '-colors', '2',
184 '-monochrome',
185 screenshot_term
187 break
188 except:
189 pass
191 width, height = self._get_image_dimensions(screenshot_term)
192 cols = width // 8
193 rows = height // 16
194 self._run_pipe([
196 'convert',
197 screenshot_term,
198 '-crop', '{}x{}'.format(cols * 8, rows * 16),
199 '+repage',
200 '-crop', '8x16',
201 '+repage',
202 '+adjoin',
203 'txt:-',
206 'sed',
207 '-e', 's|[0-9]*,[0-9]*: ([^)]*)[ ]*#\\([0-9A-Fa-f]\\{6\\}\\).*|\\1|',
208 '-e', 's:^#.*:@:',
209 '-e', 's#000000#0#g',
210 '-e', 's#FFFFFF#F#',
212 [ 'tee', self.get_temp('1.txt') ],
214 'sed',
215 '-e', ':a',
216 '-e', 'N;s#\\n##;s#^@##;/@$/{s#@$##p;d}',
217 '-e', 't a',
219 [ 'tee', self.get_temp('2.txt') ],
221 'sed',
222 '-f', 'ocr.sed',
225 'sed',
226 '/../s#.*#?#',
228 [ 'tee', self.get_temp('3.txt') ],
230 'paste',
231 '-sd', '',
234 'fold',
235 '-w', '{}'.format(cols),
237 [ 'tee', self.get_temp('4.txt') ],
239 'head',
240 '-n', '{}'.format(rows),
243 'tee',
244 screenshot_text,
248 self.screenshot_filename = screenshot_full
250 with open(screenshot_text, 'r') as f:
251 lines = [ l.strip('\n') for l in f.readlines() ]
252 self.logger.debug("Captured text:")
253 for l in lines:
254 self.logger.debug("| " + l)
255 return lines
257 def terminate(self):
258 if not self.booted:
259 return
260 self._send_command('quit')
261 VMController.terminate(self)
263 def type(self, what):
264 translations = {
265 ' ': 'spc',
266 '.': 'dot',
267 '-': 'minus',
268 '/': 'slash',
269 '\n': 'ret',
270 '_': 'shift-minus',
272 for letter in what:
273 if letter in translations:
274 letter = translations[letter]
275 self._send_command('sendkey ' + letter)
276 pass