1eb8dc403SDave Cobbley# Copyright (C) 2013 Intel Corporation
2eb8dc403SDave Cobbley#
3eb8dc403SDave Cobbley# Released under the MIT license (see COPYING.MIT)
4eb8dc403SDave Cobbley
5eb8dc403SDave Cobbley# This module provides a class for starting qemu images using runqemu.
6eb8dc403SDave Cobbley# It's used by testimage.bbclass.
7eb8dc403SDave Cobbley
8eb8dc403SDave Cobbleyimport subprocess
9eb8dc403SDave Cobbleyimport os
10eb8dc403SDave Cobbleyimport sys
11eb8dc403SDave Cobbleyimport time
12eb8dc403SDave Cobbleyimport signal
13eb8dc403SDave Cobbleyimport re
14eb8dc403SDave Cobbleyimport socket
15eb8dc403SDave Cobbleyimport select
16eb8dc403SDave Cobbleyimport errno
17eb8dc403SDave Cobbleyimport string
18eb8dc403SDave Cobbleyimport threading
19eb8dc403SDave Cobbleyimport codecs
20eb8dc403SDave Cobbleyimport logging
21eb8dc403SDave Cobbleyfrom oeqa.utils.dump import HostDumper
22eb8dc403SDave Cobbley
23eb8dc403SDave Cobbley# Get Unicode non printable control chars
24eb8dc403SDave Cobbleycontrol_range = list(range(0,32))+list(range(127,160))
25eb8dc403SDave Cobbleycontrol_chars = [chr(x) for x in control_range
26eb8dc403SDave Cobbley                if chr(x) not in string.printable]
27eb8dc403SDave Cobbleyre_control_char = re.compile('[%s]' % re.escape("".join(control_chars)))
28eb8dc403SDave Cobbley
29eb8dc403SDave Cobbleyclass QemuRunner:
30eb8dc403SDave Cobbley
31eb8dc403SDave Cobbley    def __init__(self, machine, rootfs, display, tmpdir, deploy_dir_image, logfile, boottime, dump_dir, dump_host_cmds, use_kvm, logger):
32eb8dc403SDave Cobbley
33eb8dc403SDave Cobbley        # Popen object for runqemu
34eb8dc403SDave Cobbley        self.runqemu = None
35eb8dc403SDave Cobbley        # pid of the qemu process that runqemu will start
36eb8dc403SDave Cobbley        self.qemupid = None
37eb8dc403SDave Cobbley        # target ip - from the command line or runqemu output
38eb8dc403SDave Cobbley        self.ip = None
39eb8dc403SDave Cobbley        # host ip - where qemu is running
40eb8dc403SDave Cobbley        self.server_ip = None
41eb8dc403SDave Cobbley        # target ip netmask
42eb8dc403SDave Cobbley        self.netmask = None
43eb8dc403SDave Cobbley
44eb8dc403SDave Cobbley        self.machine = machine
45eb8dc403SDave Cobbley        self.rootfs = rootfs
46eb8dc403SDave Cobbley        self.display = display
47eb8dc403SDave Cobbley        self.tmpdir = tmpdir
48eb8dc403SDave Cobbley        self.deploy_dir_image = deploy_dir_image
49eb8dc403SDave Cobbley        self.logfile = logfile
50eb8dc403SDave Cobbley        self.boottime = boottime
51eb8dc403SDave Cobbley        self.logged = False
52eb8dc403SDave Cobbley        self.thread = None
53eb8dc403SDave Cobbley        self.use_kvm = use_kvm
54eb8dc403SDave Cobbley        self.msg = ''
55eb8dc403SDave Cobbley
56eb8dc403SDave Cobbley        self.runqemutime = 120
57eb8dc403SDave Cobbley        self.qemu_pidfile = 'pidfile_'+str(os.getpid())
58eb8dc403SDave Cobbley        self.host_dumper = HostDumper(dump_host_cmds, dump_dir)
59eb8dc403SDave Cobbley
60eb8dc403SDave Cobbley        self.logger = logger
61eb8dc403SDave Cobbley
62eb8dc403SDave Cobbley    def create_socket(self):
63eb8dc403SDave Cobbley        try:
64eb8dc403SDave Cobbley            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
65eb8dc403SDave Cobbley            sock.setblocking(0)
66eb8dc403SDave Cobbley            sock.bind(("127.0.0.1",0))
67eb8dc403SDave Cobbley            sock.listen(2)
68eb8dc403SDave Cobbley            port = sock.getsockname()[1]
69eb8dc403SDave Cobbley            self.logger.debug("Created listening socket for qemu serial console on: 127.0.0.1:%s" % port)
70eb8dc403SDave Cobbley            return (sock, port)
71eb8dc403SDave Cobbley
72eb8dc403SDave Cobbley        except socket.error:
73eb8dc403SDave Cobbley            sock.close()
74eb8dc403SDave Cobbley            raise
75eb8dc403SDave Cobbley
76eb8dc403SDave Cobbley    def log(self, msg):
77eb8dc403SDave Cobbley        if self.logfile:
78eb8dc403SDave Cobbley            # It is needed to sanitize the data received from qemu
79eb8dc403SDave Cobbley            # because is possible to have control characters
80eb8dc403SDave Cobbley            msg = msg.decode("utf-8", errors='ignore')
81eb8dc403SDave Cobbley            msg = re_control_char.sub('', msg)
82eb8dc403SDave Cobbley            self.msg += msg
83eb8dc403SDave Cobbley            with codecs.open(self.logfile, "a", encoding="utf-8") as f:
84eb8dc403SDave Cobbley                f.write("%s" % msg)
85eb8dc403SDave Cobbley
86eb8dc403SDave Cobbley    def getOutput(self, o):
87eb8dc403SDave Cobbley        import fcntl
88eb8dc403SDave Cobbley        fl = fcntl.fcntl(o, fcntl.F_GETFL)
89eb8dc403SDave Cobbley        fcntl.fcntl(o, fcntl.F_SETFL, fl | os.O_NONBLOCK)
90eb8dc403SDave Cobbley        return os.read(o.fileno(), 1000000).decode("utf-8")
91eb8dc403SDave Cobbley
92eb8dc403SDave Cobbley
93eb8dc403SDave Cobbley    def handleSIGCHLD(self, signum, frame):
94eb8dc403SDave Cobbley        if self.runqemu and self.runqemu.poll():
95eb8dc403SDave Cobbley            if self.runqemu.returncode:
96eb8dc403SDave Cobbley                self.logger.debug('runqemu exited with code %d' % self.runqemu.returncode)
97eb8dc403SDave Cobbley                self.logger.debug("Output from runqemu:\n%s" % self.getOutput(self.runqemu.stdout))
98eb8dc403SDave Cobbley                self.stop()
99eb8dc403SDave Cobbley                self._dump_host()
100eb8dc403SDave Cobbley                raise SystemExit
101eb8dc403SDave Cobbley
102eb8dc403SDave Cobbley    def start(self, qemuparams = None, get_ip = True, extra_bootparams = None, runqemuparams='', launch_cmd=None, discard_writes=True):
103eb8dc403SDave Cobbley        env = os.environ.copy()
104eb8dc403SDave Cobbley        if self.display:
105eb8dc403SDave Cobbley            env["DISPLAY"] = self.display
106eb8dc403SDave Cobbley            # Set this flag so that Qemu doesn't do any grabs as SDL grabs
107eb8dc403SDave Cobbley            # interact badly with screensavers.
108eb8dc403SDave Cobbley            env["QEMU_DONT_GRAB"] = "1"
109eb8dc403SDave Cobbley        if not os.path.exists(self.rootfs):
110eb8dc403SDave Cobbley            self.logger.error("Invalid rootfs %s" % self.rootfs)
111eb8dc403SDave Cobbley            return False
112eb8dc403SDave Cobbley        if not os.path.exists(self.tmpdir):
113eb8dc403SDave Cobbley            self.logger.error("Invalid TMPDIR path %s" % self.tmpdir)
114eb8dc403SDave Cobbley            return False
115eb8dc403SDave Cobbley        else:
116eb8dc403SDave Cobbley            env["OE_TMPDIR"] = self.tmpdir
117eb8dc403SDave Cobbley        if not os.path.exists(self.deploy_dir_image):
118eb8dc403SDave Cobbley            self.logger.error("Invalid DEPLOY_DIR_IMAGE path %s" % self.deploy_dir_image)
119eb8dc403SDave Cobbley            return False
120eb8dc403SDave Cobbley        else:
121eb8dc403SDave Cobbley            env["DEPLOY_DIR_IMAGE"] = self.deploy_dir_image
122eb8dc403SDave Cobbley
123eb8dc403SDave Cobbley        if not launch_cmd:
124eb8dc403SDave Cobbley            launch_cmd = 'runqemu %s %s ' % ('snapshot' if discard_writes else '', runqemuparams)
125eb8dc403SDave Cobbley            if self.use_kvm:
126eb8dc403SDave Cobbley                self.logger.debug('Using kvm for runqemu')
127eb8dc403SDave Cobbley                launch_cmd += ' kvm'
128eb8dc403SDave Cobbley            else:
129eb8dc403SDave Cobbley                self.logger.debug('Not using kvm for runqemu')
130eb8dc403SDave Cobbley            if not self.display:
131eb8dc403SDave Cobbley                launch_cmd += ' nographic'
132eb8dc403SDave Cobbley            launch_cmd += ' %s %s' % (self.machine, self.rootfs)
133eb8dc403SDave Cobbley
134eb8dc403SDave Cobbley        return self.launch(launch_cmd, qemuparams=qemuparams, get_ip=get_ip, extra_bootparams=extra_bootparams, env=env)
135eb8dc403SDave Cobbley
136eb8dc403SDave Cobbley    def launch(self, launch_cmd, get_ip = True, qemuparams = None, extra_bootparams = None, env = None):
137eb8dc403SDave Cobbley        try:
138f86d0556SBrad Bishop            self.threadsock, threadport = self.create_socket()
139eb8dc403SDave Cobbley            self.server_socket, self.serverport = self.create_socket()
140eb8dc403SDave Cobbley        except socket.error as msg:
141eb8dc403SDave Cobbley            self.logger.error("Failed to create listening socket: %s" % msg[1])
142eb8dc403SDave Cobbley            return False
143eb8dc403SDave Cobbley
144eb8dc403SDave Cobbley        bootparams = 'console=tty1 console=ttyS0,115200n8 printk.time=1'
145eb8dc403SDave Cobbley        if extra_bootparams:
146eb8dc403SDave Cobbley            bootparams = bootparams + ' ' + extra_bootparams
147eb8dc403SDave Cobbley
148eb8dc403SDave Cobbley        # Ask QEMU to store the QEMU process PID in file, this way we don't have to parse running processes
149eb8dc403SDave Cobbley        # and analyze descendents in order to determine it.
150eb8dc403SDave Cobbley        if os.path.exists(self.qemu_pidfile):
151eb8dc403SDave Cobbley            os.remove(self.qemu_pidfile)
152eb8dc403SDave Cobbley        self.qemuparams = 'bootparams="{0}" qemuparams="-serial tcp:127.0.0.1:{1} -pidfile {2}"'.format(bootparams, threadport, self.qemu_pidfile)
153eb8dc403SDave Cobbley        if qemuparams:
154eb8dc403SDave Cobbley            self.qemuparams = self.qemuparams[:-1] + " " + qemuparams + " " + '\"'
155eb8dc403SDave Cobbley
156eb8dc403SDave Cobbley        launch_cmd += ' tcpserial=%s %s' % (self.serverport, self.qemuparams)
157eb8dc403SDave Cobbley
158eb8dc403SDave Cobbley        self.origchldhandler = signal.getsignal(signal.SIGCHLD)
159eb8dc403SDave Cobbley        signal.signal(signal.SIGCHLD, self.handleSIGCHLD)
160eb8dc403SDave Cobbley
161eb8dc403SDave Cobbley        self.logger.debug('launchcmd=%s'%(launch_cmd))
162eb8dc403SDave Cobbley
163eb8dc403SDave Cobbley        # FIXME: We pass in stdin=subprocess.PIPE here to work around stty
164eb8dc403SDave Cobbley        # blocking at the end of the runqemu script when using this within
165eb8dc403SDave Cobbley        # oe-selftest (this makes stty error out immediately). There ought
166eb8dc403SDave Cobbley        # to be a proper fix but this will suffice for now.
167eb8dc403SDave Cobbley        self.runqemu = subprocess.Popen(launch_cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=subprocess.PIPE, preexec_fn=os.setpgrp, env=env)
168eb8dc403SDave Cobbley        output = self.runqemu.stdout
169eb8dc403SDave Cobbley
170eb8dc403SDave Cobbley        #
171eb8dc403SDave Cobbley        # We need the preexec_fn above so that all runqemu processes can easily be killed
172eb8dc403SDave Cobbley        # (by killing their process group). This presents a problem if this controlling
173eb8dc403SDave Cobbley        # process itself is killed however since those processes don't notice the death
174eb8dc403SDave Cobbley        # of the parent and merrily continue on.
175eb8dc403SDave Cobbley        #
176eb8dc403SDave Cobbley        # Rather than hack runqemu to deal with this, we add something here instead.
177eb8dc403SDave Cobbley        # Basically we fork off another process which holds an open pipe to the parent
178eb8dc403SDave Cobbley        # and also is setpgrp. If/when the pipe sees EOF from the parent dieing, it kills
179eb8dc403SDave Cobbley        # the process group. This is like pctrl's PDEATHSIG but for a process group
180eb8dc403SDave Cobbley        # rather than a single process.
181eb8dc403SDave Cobbley        #
182eb8dc403SDave Cobbley        r, w = os.pipe()
183eb8dc403SDave Cobbley        self.monitorpid = os.fork()
184eb8dc403SDave Cobbley        if self.monitorpid:
185eb8dc403SDave Cobbley            os.close(r)
186eb8dc403SDave Cobbley            self.monitorpipe = os.fdopen(w, "w")
187eb8dc403SDave Cobbley        else:
188eb8dc403SDave Cobbley            # child process
189eb8dc403SDave Cobbley            os.setpgrp()
190eb8dc403SDave Cobbley            os.close(w)
191eb8dc403SDave Cobbley            r = os.fdopen(r)
192eb8dc403SDave Cobbley            x = r.read()
193eb8dc403SDave Cobbley            os.killpg(os.getpgid(self.runqemu.pid), signal.SIGTERM)
194eb8dc403SDave Cobbley            sys.exit(0)
195eb8dc403SDave Cobbley
196eb8dc403SDave Cobbley        self.logger.debug("runqemu started, pid is %s" % self.runqemu.pid)
197eb8dc403SDave Cobbley        self.logger.debug("waiting at most %s seconds for qemu pid (%s)" %
198eb8dc403SDave Cobbley                          (self.runqemutime, time.strftime("%D %H:%M:%S")))
199eb8dc403SDave Cobbley        endtime = time.time() + self.runqemutime
200eb8dc403SDave Cobbley        while not self.is_alive() and time.time() < endtime:
201eb8dc403SDave Cobbley            if self.runqemu.poll():
202eb8dc403SDave Cobbley                if self.runqemu.returncode:
203eb8dc403SDave Cobbley                    # No point waiting any longer
204eb8dc403SDave Cobbley                    self.logger.debug('runqemu exited with code %d' % self.runqemu.returncode)
205eb8dc403SDave Cobbley                    self._dump_host()
206eb8dc403SDave Cobbley                    self.logger.debug("Output from runqemu:\n%s" % self.getOutput(output))
207f86d0556SBrad Bishop                    self.stop()
208eb8dc403SDave Cobbley                    return False
209eb8dc403SDave Cobbley            time.sleep(0.5)
210eb8dc403SDave Cobbley
211eb8dc403SDave Cobbley        if not self.is_alive():
212eb8dc403SDave Cobbley            self.logger.error("Qemu pid didn't appear in %s seconds (%s)" %
213eb8dc403SDave Cobbley                              (self.runqemutime, time.strftime("%D %H:%M:%S")))
214eb8dc403SDave Cobbley            # Dump all processes to help us to figure out what is going on...
215eb8dc403SDave Cobbley            ps = subprocess.Popen(['ps', 'axww', '-o', 'pid,ppid,command '], stdout=subprocess.PIPE).communicate()[0]
216eb8dc403SDave Cobbley            processes = ps.decode("utf-8")
217eb8dc403SDave Cobbley            self.logger.debug("Running processes:\n%s" % processes)
218eb8dc403SDave Cobbley            self._dump_host()
219eb8dc403SDave Cobbley            op = self.getOutput(output)
220f86d0556SBrad Bishop            self.stop()
221eb8dc403SDave Cobbley            if op:
222eb8dc403SDave Cobbley                self.logger.error("Output from runqemu:\n%s" % op)
223eb8dc403SDave Cobbley            else:
224eb8dc403SDave Cobbley                self.logger.error("No output from runqemu.\n")
225eb8dc403SDave Cobbley            return False
226eb8dc403SDave Cobbley
227eb8dc403SDave Cobbley        # We are alive: qemu is running
228eb8dc403SDave Cobbley        out = self.getOutput(output)
229eb8dc403SDave Cobbley        netconf = False # network configuration is not required by default
230eb8dc403SDave Cobbley        self.logger.debug("qemu started in %s seconds - qemu procces pid is %s (%s)" %
231eb8dc403SDave Cobbley                          (time.time() - (endtime - self.runqemutime),
232eb8dc403SDave Cobbley                           self.qemupid, time.strftime("%D %H:%M:%S")))
233eb8dc403SDave Cobbley        if get_ip:
234eb8dc403SDave Cobbley            cmdline = ''
235eb8dc403SDave Cobbley            with open('/proc/%s/cmdline' % self.qemupid) as p:
236eb8dc403SDave Cobbley                cmdline = p.read()
237eb8dc403SDave Cobbley                # It is needed to sanitize the data received
238eb8dc403SDave Cobbley                # because is possible to have control characters
239eb8dc403SDave Cobbley                cmdline = re_control_char.sub(' ', cmdline)
240eb8dc403SDave Cobbley            try:
241f86d0556SBrad Bishop                ips = re.findall(r"((?:[0-9]{1,3}\.){3}[0-9]{1,3})", cmdline.split("ip=")[1])
242eb8dc403SDave Cobbley                self.ip = ips[0]
243eb8dc403SDave Cobbley                self.server_ip = ips[1]
244eb8dc403SDave Cobbley                self.logger.debug("qemu cmdline used:\n{}".format(cmdline))
245eb8dc403SDave Cobbley            except (IndexError, ValueError):
246eb8dc403SDave Cobbley                # Try to get network configuration from runqemu output
247f86d0556SBrad Bishop                match = re.match(r'.*Network configuration: ([0-9.]+)::([0-9.]+):([0-9.]+)$.*',
248eb8dc403SDave Cobbley                                 out, re.MULTILINE|re.DOTALL)
249eb8dc403SDave Cobbley                if match:
250eb8dc403SDave Cobbley                    self.ip, self.server_ip, self.netmask = match.groups()
251eb8dc403SDave Cobbley                    # network configuration is required as we couldn't get it
252eb8dc403SDave Cobbley                    # from the runqemu command line, so qemu doesn't run kernel
253eb8dc403SDave Cobbley                    # and guest networking is not configured
254eb8dc403SDave Cobbley                    netconf = True
255eb8dc403SDave Cobbley                else:
256eb8dc403SDave Cobbley                    self.logger.error("Couldn't get ip from qemu command line and runqemu output! "
257eb8dc403SDave Cobbley                                 "Here is the qemu command line used:\n%s\n"
258eb8dc403SDave Cobbley                                 "and output from runqemu:\n%s" % (cmdline, out))
259eb8dc403SDave Cobbley                    self._dump_host()
260eb8dc403SDave Cobbley                    self.stop()
261eb8dc403SDave Cobbley                    return False
262eb8dc403SDave Cobbley
263eb8dc403SDave Cobbley        self.logger.debug("Target IP: %s" % self.ip)
264eb8dc403SDave Cobbley        self.logger.debug("Server IP: %s" % self.server_ip)
265eb8dc403SDave Cobbley
266f86d0556SBrad Bishop        self.thread = LoggingThread(self.log, self.threadsock, self.logger)
267eb8dc403SDave Cobbley        self.thread.start()
268eb8dc403SDave Cobbley        if not self.thread.connection_established.wait(self.boottime):
269eb8dc403SDave Cobbley            self.logger.error("Didn't receive a console connection from qemu. "
270eb8dc403SDave Cobbley                         "Here is the qemu command line used:\n%s\nand "
271eb8dc403SDave Cobbley                         "output from runqemu:\n%s" % (cmdline, out))
272eb8dc403SDave Cobbley            self.stop_thread()
273eb8dc403SDave Cobbley            return False
274eb8dc403SDave Cobbley
275eb8dc403SDave Cobbley        self.logger.debug("Output from runqemu:\n%s", out)
276eb8dc403SDave Cobbley        self.logger.debug("Waiting at most %d seconds for login banner (%s)" %
277eb8dc403SDave Cobbley                          (self.boottime, time.strftime("%D %H:%M:%S")))
278eb8dc403SDave Cobbley        endtime = time.time() + self.boottime
279eb8dc403SDave Cobbley        socklist = [self.server_socket]
280eb8dc403SDave Cobbley        reachedlogin = False
281eb8dc403SDave Cobbley        stopread = False
282eb8dc403SDave Cobbley        qemusock = None
283eb8dc403SDave Cobbley        bootlog = b''
284eb8dc403SDave Cobbley        data = b''
285eb8dc403SDave Cobbley        while time.time() < endtime and not stopread:
286eb8dc403SDave Cobbley            try:
287eb8dc403SDave Cobbley                sread, swrite, serror = select.select(socklist, [], [], 5)
288eb8dc403SDave Cobbley            except InterruptedError:
289eb8dc403SDave Cobbley                continue
290eb8dc403SDave Cobbley            for sock in sread:
291eb8dc403SDave Cobbley                if sock is self.server_socket:
292eb8dc403SDave Cobbley                    qemusock, addr = self.server_socket.accept()
293eb8dc403SDave Cobbley                    qemusock.setblocking(0)
294eb8dc403SDave Cobbley                    socklist.append(qemusock)
295eb8dc403SDave Cobbley                    socklist.remove(self.server_socket)
296eb8dc403SDave Cobbley                    self.logger.debug("Connection from %s:%s" % addr)
297eb8dc403SDave Cobbley                else:
298eb8dc403SDave Cobbley                    data = data + sock.recv(1024)
299eb8dc403SDave Cobbley                    if data:
300eb8dc403SDave Cobbley                        bootlog += data
301eb8dc403SDave Cobbley                        data = b''
302eb8dc403SDave Cobbley                        if b' login:' in bootlog:
303eb8dc403SDave Cobbley                            self.server_socket = qemusock
304eb8dc403SDave Cobbley                            stopread = True
305eb8dc403SDave Cobbley                            reachedlogin = True
306eb8dc403SDave Cobbley                            self.logger.debug("Reached login banner in %s seconds (%s)" %
307eb8dc403SDave Cobbley                                              (time.time() - (endtime - self.boottime),
308eb8dc403SDave Cobbley                                              time.strftime("%D %H:%M:%S")))
309eb8dc403SDave Cobbley                    else:
310eb8dc403SDave Cobbley                        # no need to check if reachedlogin unless we support multiple connections
311eb8dc403SDave Cobbley                        self.logger.debug("QEMU socket disconnected before login banner reached. (%s)" %
312eb8dc403SDave Cobbley                                          time.strftime("%D %H:%M:%S"))
313eb8dc403SDave Cobbley                        socklist.remove(sock)
314eb8dc403SDave Cobbley                        sock.close()
315eb8dc403SDave Cobbley                        stopread = True
316eb8dc403SDave Cobbley
317eb8dc403SDave Cobbley
318eb8dc403SDave Cobbley        if not reachedlogin:
319eb8dc403SDave Cobbley            if time.time() >= endtime:
320eb8dc403SDave Cobbley                self.logger.debug("Target didn't reach login banner in %d seconds (%s)" %
321eb8dc403SDave Cobbley                                  (self.boottime, time.strftime("%D %H:%M:%S")))
322eb8dc403SDave Cobbley            tail = lambda l: "\n".join(l.splitlines()[-25:])
323eb8dc403SDave Cobbley            # in case bootlog is empty, use tail qemu log store at self.msg
324eb8dc403SDave Cobbley            lines = tail(bootlog if bootlog else self.msg)
325eb8dc403SDave Cobbley            self.logger.debug("Last 25 lines of text:\n%s" % lines)
326eb8dc403SDave Cobbley            self.logger.debug("Check full boot log: %s" % self.logfile)
327eb8dc403SDave Cobbley            self._dump_host()
328eb8dc403SDave Cobbley            self.stop()
329eb8dc403SDave Cobbley            return False
330eb8dc403SDave Cobbley
331eb8dc403SDave Cobbley        # If we are not able to login the tests can continue
332eb8dc403SDave Cobbley        try:
333eb8dc403SDave Cobbley            (status, output) = self.run_serial("root\n", raw=True)
334f86d0556SBrad Bishop            if re.search(r"root@[a-zA-Z0-9\-]+:~#", output):
335eb8dc403SDave Cobbley                self.logged = True
336eb8dc403SDave Cobbley                self.logger.debug("Logged as root in serial console")
337eb8dc403SDave Cobbley                if netconf:
338eb8dc403SDave Cobbley                    # configure guest networking
339eb8dc403SDave Cobbley                    cmd = "ifconfig eth0 %s netmask %s up\n" % (self.ip, self.netmask)
340eb8dc403SDave Cobbley                    output = self.run_serial(cmd, raw=True)[1]
341f86d0556SBrad Bishop                    if re.search(r"root@[a-zA-Z0-9\-]+:~#", output):
342eb8dc403SDave Cobbley                        self.logger.debug("configured ip address %s", self.ip)
343eb8dc403SDave Cobbley                    else:
344eb8dc403SDave Cobbley                        self.logger.debug("Couldn't configure guest networking")
345eb8dc403SDave Cobbley            else:
346eb8dc403SDave Cobbley                self.logger.debug("Couldn't login into serial console"
347eb8dc403SDave Cobbley                            " as root using blank password")
348eb8dc403SDave Cobbley        except:
349eb8dc403SDave Cobbley            self.logger.debug("Serial console failed while trying to login")
350eb8dc403SDave Cobbley        return True
351eb8dc403SDave Cobbley
352eb8dc403SDave Cobbley    def stop(self):
353eb8dc403SDave Cobbley        if hasattr(self, "origchldhandler"):
354eb8dc403SDave Cobbley            signal.signal(signal.SIGCHLD, self.origchldhandler)
355*1a4b7ee2SBrad Bishop        self.stop_thread()
356*1a4b7ee2SBrad Bishop        self.stop_qemu_system()
357eb8dc403SDave Cobbley        if self.runqemu:
358eb8dc403SDave Cobbley            if hasattr(self, "monitorpid"):
359eb8dc403SDave Cobbley                os.kill(self.monitorpid, signal.SIGKILL)
360eb8dc403SDave Cobbley                self.logger.debug("Sending SIGTERM to runqemu")
361eb8dc403SDave Cobbley                try:
362eb8dc403SDave Cobbley                    os.killpg(os.getpgid(self.runqemu.pid), signal.SIGTERM)
363eb8dc403SDave Cobbley                except OSError as e:
364eb8dc403SDave Cobbley                    if e.errno != errno.ESRCH:
365eb8dc403SDave Cobbley                        raise
366eb8dc403SDave Cobbley            endtime = time.time() + self.runqemutime
367eb8dc403SDave Cobbley            while self.runqemu.poll() is None and time.time() < endtime:
368eb8dc403SDave Cobbley                time.sleep(1)
369eb8dc403SDave Cobbley            if self.runqemu.poll() is None:
370eb8dc403SDave Cobbley                self.logger.debug("Sending SIGKILL to runqemu")
371eb8dc403SDave Cobbley                os.killpg(os.getpgid(self.runqemu.pid), signal.SIGKILL)
372f86d0556SBrad Bishop            self.runqemu.stdin.close()
373f86d0556SBrad Bishop            self.runqemu.stdout.close()
374eb8dc403SDave Cobbley            self.runqemu = None
375f86d0556SBrad Bishop
376eb8dc403SDave Cobbley        if hasattr(self, 'server_socket') and self.server_socket:
377eb8dc403SDave Cobbley            self.server_socket.close()
378eb8dc403SDave Cobbley            self.server_socket = None
379f86d0556SBrad Bishop        if hasattr(self, 'threadsock') and self.threadsock:
380f86d0556SBrad Bishop            self.threadsock.close()
381f86d0556SBrad Bishop            self.threadsock = None
382eb8dc403SDave Cobbley        self.qemupid = None
383eb8dc403SDave Cobbley        self.ip = None
384eb8dc403SDave Cobbley        if os.path.exists(self.qemu_pidfile):
385eb8dc403SDave Cobbley            os.remove(self.qemu_pidfile)
386f86d0556SBrad Bishop        if self.monitorpipe:
387f86d0556SBrad Bishop            self.monitorpipe.close()
388eb8dc403SDave Cobbley
389eb8dc403SDave Cobbley    def stop_qemu_system(self):
390eb8dc403SDave Cobbley        if self.qemupid:
391eb8dc403SDave Cobbley            try:
392eb8dc403SDave Cobbley                # qemu-system behaves well and a SIGTERM is enough
393eb8dc403SDave Cobbley                os.kill(self.qemupid, signal.SIGTERM)
394eb8dc403SDave Cobbley            except ProcessLookupError as e:
395*1a4b7ee2SBrad Bishop                self.logger.warning('qemu-system ended unexpectedly')
396eb8dc403SDave Cobbley
397eb8dc403SDave Cobbley    def stop_thread(self):
398eb8dc403SDave Cobbley        if self.thread and self.thread.is_alive():
399eb8dc403SDave Cobbley            self.thread.stop()
400eb8dc403SDave Cobbley            self.thread.join()
401eb8dc403SDave Cobbley
402eb8dc403SDave Cobbley    def restart(self, qemuparams = None):
403eb8dc403SDave Cobbley        self.logger.debug("Restarting qemu process")
404eb8dc403SDave Cobbley        if self.runqemu.poll() is None:
405eb8dc403SDave Cobbley            self.stop()
406eb8dc403SDave Cobbley        if self.start(qemuparams):
407eb8dc403SDave Cobbley            return True
408eb8dc403SDave Cobbley        return False
409eb8dc403SDave Cobbley
410eb8dc403SDave Cobbley    def is_alive(self):
411*1a4b7ee2SBrad Bishop        if not self.runqemu or self.runqemu.poll() is not None:
412eb8dc403SDave Cobbley            return False
413eb8dc403SDave Cobbley        if os.path.isfile(self.qemu_pidfile):
414eb8dc403SDave Cobbley            f = open(self.qemu_pidfile, 'r')
415eb8dc403SDave Cobbley            qemu_pid = f.read()
416eb8dc403SDave Cobbley            f.close()
417eb8dc403SDave Cobbley            qemupid = int(qemu_pid)
418eb8dc403SDave Cobbley            if os.path.exists("/proc/" + str(qemupid)):
419eb8dc403SDave Cobbley                self.qemupid = qemupid
420eb8dc403SDave Cobbley                return True
421eb8dc403SDave Cobbley        return False
422eb8dc403SDave Cobbley
423eb8dc403SDave Cobbley    def run_serial(self, command, raw=False, timeout=5):
424eb8dc403SDave Cobbley        # We assume target system have echo to get command status
425eb8dc403SDave Cobbley        if not raw:
426eb8dc403SDave Cobbley            command = "%s; echo $?\n" % command
427eb8dc403SDave Cobbley
428eb8dc403SDave Cobbley        data = ''
429eb8dc403SDave Cobbley        status = 0
430eb8dc403SDave Cobbley        self.server_socket.sendall(command.encode('utf-8'))
431eb8dc403SDave Cobbley        start = time.time()
432eb8dc403SDave Cobbley        end = start + timeout
433eb8dc403SDave Cobbley        while True:
434eb8dc403SDave Cobbley            now = time.time()
435eb8dc403SDave Cobbley            if now >= end:
436eb8dc403SDave Cobbley                data += "<<< run_serial(): command timed out after %d seconds without output >>>\r\n\r\n" % timeout
437eb8dc403SDave Cobbley                break
438eb8dc403SDave Cobbley            try:
439eb8dc403SDave Cobbley                sread, _, _ = select.select([self.server_socket],[],[], end - now)
440eb8dc403SDave Cobbley            except InterruptedError:
441eb8dc403SDave Cobbley                continue
442eb8dc403SDave Cobbley            if sread:
443eb8dc403SDave Cobbley                answer = self.server_socket.recv(1024)
444eb8dc403SDave Cobbley                if answer:
445eb8dc403SDave Cobbley                    data += answer.decode('utf-8')
446eb8dc403SDave Cobbley                    # Search the prompt to stop
447f86d0556SBrad Bishop                    if re.search(r"[a-zA-Z0-9]+@[a-zA-Z0-9\-]+:~#", data):
448eb8dc403SDave Cobbley                        break
449eb8dc403SDave Cobbley                else:
450eb8dc403SDave Cobbley                    raise Exception("No data on serial console socket")
451eb8dc403SDave Cobbley
452eb8dc403SDave Cobbley        if data:
453eb8dc403SDave Cobbley            if raw:
454eb8dc403SDave Cobbley                status = 1
455eb8dc403SDave Cobbley            else:
456eb8dc403SDave Cobbley                # Remove first line (command line) and last line (prompt)
457eb8dc403SDave Cobbley                data = data[data.find('$?\r\n')+4:data.rfind('\r\n')]
458eb8dc403SDave Cobbley                index = data.rfind('\r\n')
459eb8dc403SDave Cobbley                if index == -1:
460eb8dc403SDave Cobbley                    status_cmd = data
461eb8dc403SDave Cobbley                    data = ""
462eb8dc403SDave Cobbley                else:
463eb8dc403SDave Cobbley                    status_cmd = data[index+2:]
464eb8dc403SDave Cobbley                    data = data[:index]
465eb8dc403SDave Cobbley                if (status_cmd == "0"):
466eb8dc403SDave Cobbley                    status = 1
467eb8dc403SDave Cobbley        return (status, str(data))
468eb8dc403SDave Cobbley
469eb8dc403SDave Cobbley
470eb8dc403SDave Cobbley    def _dump_host(self):
471eb8dc403SDave Cobbley        self.host_dumper.create_dir("qemu")
472*1a4b7ee2SBrad Bishop        self.logger.warning("Qemu ended unexpectedly, dump data from host"
473eb8dc403SDave Cobbley                " is in %s" % self.host_dumper.dump_dir)
474eb8dc403SDave Cobbley        self.host_dumper.dump_host()
475eb8dc403SDave Cobbley
476eb8dc403SDave Cobbley# This class is for reading data from a socket and passing it to logfunc
477eb8dc403SDave Cobbley# to be processed. It's completely event driven and has a straightforward
478eb8dc403SDave Cobbley# event loop. The mechanism for stopping the thread is a simple pipe which
479eb8dc403SDave Cobbley# will wake up the poll and allow for tearing everything down.
480eb8dc403SDave Cobbleyclass LoggingThread(threading.Thread):
481eb8dc403SDave Cobbley    def __init__(self, logfunc, sock, logger):
482eb8dc403SDave Cobbley        self.connection_established = threading.Event()
483eb8dc403SDave Cobbley        self.serversock = sock
484eb8dc403SDave Cobbley        self.logfunc = logfunc
485eb8dc403SDave Cobbley        self.logger = logger
486eb8dc403SDave Cobbley        self.readsock = None
487eb8dc403SDave Cobbley        self.running = False
488eb8dc403SDave Cobbley
489eb8dc403SDave Cobbley        self.errorevents = select.POLLERR | select.POLLHUP | select.POLLNVAL
490eb8dc403SDave Cobbley        self.readevents = select.POLLIN | select.POLLPRI
491eb8dc403SDave Cobbley
492eb8dc403SDave Cobbley        threading.Thread.__init__(self, target=self.threadtarget)
493eb8dc403SDave Cobbley
494eb8dc403SDave Cobbley    def threadtarget(self):
495eb8dc403SDave Cobbley        try:
496eb8dc403SDave Cobbley            self.eventloop()
497eb8dc403SDave Cobbley        finally:
498eb8dc403SDave Cobbley            self.teardown()
499eb8dc403SDave Cobbley
500eb8dc403SDave Cobbley    def run(self):
501eb8dc403SDave Cobbley        self.logger.debug("Starting logging thread")
502eb8dc403SDave Cobbley        self.readpipe, self.writepipe = os.pipe()
503eb8dc403SDave Cobbley        threading.Thread.run(self)
504eb8dc403SDave Cobbley
505eb8dc403SDave Cobbley    def stop(self):
506eb8dc403SDave Cobbley        self.logger.debug("Stopping logging thread")
507eb8dc403SDave Cobbley        if self.running:
508eb8dc403SDave Cobbley            os.write(self.writepipe, bytes("stop", "utf-8"))
509eb8dc403SDave Cobbley
510eb8dc403SDave Cobbley    def teardown(self):
511eb8dc403SDave Cobbley        self.logger.debug("Tearing down logging thread")
512eb8dc403SDave Cobbley        self.close_socket(self.serversock)
513eb8dc403SDave Cobbley
514eb8dc403SDave Cobbley        if self.readsock is not None:
515eb8dc403SDave Cobbley            self.close_socket(self.readsock)
516eb8dc403SDave Cobbley
517eb8dc403SDave Cobbley        self.close_ignore_error(self.readpipe)
518eb8dc403SDave Cobbley        self.close_ignore_error(self.writepipe)
519eb8dc403SDave Cobbley        self.running = False
520eb8dc403SDave Cobbley
521eb8dc403SDave Cobbley    def eventloop(self):
522eb8dc403SDave Cobbley        poll = select.poll()
523eb8dc403SDave Cobbley        event_read_mask = self.errorevents | self.readevents
524eb8dc403SDave Cobbley        poll.register(self.serversock.fileno())
525eb8dc403SDave Cobbley        poll.register(self.readpipe, event_read_mask)
526eb8dc403SDave Cobbley
527eb8dc403SDave Cobbley        breakout = False
528eb8dc403SDave Cobbley        self.running = True
529eb8dc403SDave Cobbley        self.logger.debug("Starting thread event loop")
530eb8dc403SDave Cobbley        while not breakout:
531eb8dc403SDave Cobbley            events = poll.poll()
532eb8dc403SDave Cobbley            for event in events:
533eb8dc403SDave Cobbley                # An error occurred, bail out
534eb8dc403SDave Cobbley                if event[1] & self.errorevents:
535eb8dc403SDave Cobbley                    raise Exception(self.stringify_event(event[1]))
536eb8dc403SDave Cobbley
537eb8dc403SDave Cobbley                # Event to stop the thread
538eb8dc403SDave Cobbley                if self.readpipe == event[0]:
539eb8dc403SDave Cobbley                    self.logger.debug("Stop event received")
540eb8dc403SDave Cobbley                    breakout = True
541eb8dc403SDave Cobbley                    break
542eb8dc403SDave Cobbley
543eb8dc403SDave Cobbley                # A connection request was received
544eb8dc403SDave Cobbley                elif self.serversock.fileno() == event[0]:
545eb8dc403SDave Cobbley                    self.logger.debug("Connection request received")
546eb8dc403SDave Cobbley                    self.readsock, _ = self.serversock.accept()
547eb8dc403SDave Cobbley                    self.readsock.setblocking(0)
548eb8dc403SDave Cobbley                    poll.unregister(self.serversock.fileno())
549eb8dc403SDave Cobbley                    poll.register(self.readsock.fileno(), event_read_mask)
550eb8dc403SDave Cobbley
551eb8dc403SDave Cobbley                    self.logger.debug("Setting connection established event")
552eb8dc403SDave Cobbley                    self.connection_established.set()
553eb8dc403SDave Cobbley
554eb8dc403SDave Cobbley                # Actual data to be logged
555eb8dc403SDave Cobbley                elif self.readsock.fileno() == event[0]:
556eb8dc403SDave Cobbley                    data = self.recv(1024)
557eb8dc403SDave Cobbley                    self.logfunc(data)
558eb8dc403SDave Cobbley
559eb8dc403SDave Cobbley    # Since the socket is non-blocking make sure to honor EAGAIN
560eb8dc403SDave Cobbley    # and EWOULDBLOCK.
561eb8dc403SDave Cobbley    def recv(self, count):
562eb8dc403SDave Cobbley        try:
563eb8dc403SDave Cobbley            data = self.readsock.recv(count)
564eb8dc403SDave Cobbley        except socket.error as e:
565eb8dc403SDave Cobbley            if e.errno == errno.EAGAIN or e.errno == errno.EWOULDBLOCK:
566eb8dc403SDave Cobbley                return ''
567eb8dc403SDave Cobbley            else:
568eb8dc403SDave Cobbley                raise
569eb8dc403SDave Cobbley
570eb8dc403SDave Cobbley        if data is None:
571eb8dc403SDave Cobbley            raise Exception("No data on read ready socket")
572eb8dc403SDave Cobbley        elif not data:
573eb8dc403SDave Cobbley            # This actually means an orderly shutdown
574eb8dc403SDave Cobbley            # happened. But for this code it counts as an
575eb8dc403SDave Cobbley            # error since the connection shouldn't go away
576eb8dc403SDave Cobbley            # until qemu exits.
577eb8dc403SDave Cobbley            raise Exception("Console connection closed unexpectedly")
578eb8dc403SDave Cobbley
579eb8dc403SDave Cobbley        return data
580eb8dc403SDave Cobbley
581eb8dc403SDave Cobbley    def stringify_event(self, event):
582eb8dc403SDave Cobbley        val = ''
583eb8dc403SDave Cobbley        if select.POLLERR == event:
584eb8dc403SDave Cobbley            val = 'POLLER'
585eb8dc403SDave Cobbley        elif select.POLLHUP == event:
586eb8dc403SDave Cobbley            val = 'POLLHUP'
587eb8dc403SDave Cobbley        elif select.POLLNVAL == event:
588eb8dc403SDave Cobbley            val = 'POLLNVAL'
589eb8dc403SDave Cobbley        return val
590eb8dc403SDave Cobbley
591eb8dc403SDave Cobbley    def close_socket(self, sock):
592eb8dc403SDave Cobbley        sock.shutdown(socket.SHUT_RDWR)
593eb8dc403SDave Cobbley        sock.close()
594eb8dc403SDave Cobbley
595eb8dc403SDave Cobbley    def close_ignore_error(self, fd):
596eb8dc403SDave Cobbley        try:
597eb8dc403SDave Cobbley            os.close(fd)
598eb8dc403SDave Cobbley        except OSError:
599eb8dc403SDave Cobbley            pass
600