| # Copyright 2018 The Chromium Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Implements commands for running and interacting with Fuchsia on QEMU.""" |
| |
| import boot_data |
| import common |
| import logging |
| import target |
| import os |
| import platform |
| import socket |
| import subprocess |
| import sys |
| import tempfile |
| import time |
| |
| from common import GetQemuRootForPlatform, EnsurePathExists |
| |
| |
| # Virtual networking configuration data for QEMU. |
| GUEST_NET = '192.168.3.0/24' |
| GUEST_IP_ADDRESS = '192.168.3.9' |
| HOST_IP_ADDRESS = '192.168.3.2' |
| GUEST_MAC_ADDRESS = '52:54:00:63:5e:7b' |
| |
| |
| class QemuTarget(target.Target): |
| def __init__(self, output_dir, target_cpu, cpu_cores, system_log_file, |
| require_kvm, ram_size_mb=2048): |
| """output_dir: The directory which will contain the files that are |
| generated to support the QEMU deployment. |
| target_cpu: The emulated target CPU architecture. |
| Can be 'x64' or 'arm64'.""" |
| super(QemuTarget, self).__init__(output_dir, target_cpu) |
| self._qemu_process = None |
| self._ram_size_mb = ram_size_mb |
| self._system_log_file = system_log_file |
| self._cpu_cores = cpu_cores |
| self._require_kvm = require_kvm |
| |
| def __enter__(self): |
| return self |
| |
| # Used by the context manager to ensure that QEMU is killed when the Python |
| # process exits. |
| def __exit__(self, exc_type, exc_val, exc_tb): |
| self.Shutdown(); |
| |
| def Start(self): |
| qemu_path = os.path.join(GetQemuRootForPlatform(), 'bin', |
| 'qemu-system-' + self._GetTargetSdkLegacyArch()) |
| kernel_args = boot_data.GetKernelArgs(self._output_dir) |
| |
| # TERM=dumb tells the guest OS to not emit ANSI commands that trigger |
| # noisy ANSI spew from the user's terminal emulator. |
| kernel_args.append('TERM=dumb') |
| |
| # Enable logging to the serial port. This is a temporary fix to investigate |
| # the root cause for https://crbug.com/869753 . |
| kernel_args.append('kernel.serial=legacy') |
| |
| qemu_command = [qemu_path, |
| '-m', str(self._ram_size_mb), |
| '-nographic', |
| '-kernel', EnsurePathExists( |
| boot_data.GetTargetFile(self._GetTargetSdkArch(), |
| 'qemu-kernel.bin')), |
| '-initrd', EnsurePathExists( |
| boot_data.GetBootImage(self._output_dir, self._GetTargetSdkArch())), |
| '-smp', str(self._cpu_cores), |
| |
| # Attach the blobstore and data volumes. Use snapshot mode to discard |
| # any changes. |
| '-snapshot', |
| '-drive', 'file=%s,format=qcow2,if=none,id=blobstore,snapshot=on' % |
| EnsurePathExists( |
| os.path.join(self._output_dir, 'fvm.blk.qcow2')), |
| '-device', 'virtio-blk-pci,drive=blobstore', |
| |
| # Use stdio for the guest OS only; don't attach the QEMU interactive |
| # monitor. |
| '-serial', 'stdio', |
| '-monitor', 'none', |
| |
| '-append', ' '.join(kernel_args) |
| ] |
| |
| # Configure the machine to emulate, based on the target architecture. |
| if self._target_cpu == 'arm64': |
| qemu_command.extend([ |
| '-machine','virt', |
| ]) |
| netdev_type = 'virtio-net-pci' |
| else: |
| qemu_command.extend([ |
| '-machine', 'q35', |
| ]) |
| netdev_type = 'e1000' |
| |
| # Configure the CPU to emulate. |
| # On Linux, we can enable lightweight virtualization (KVM) if the host and |
| # guest architectures are the same. |
| enable_kvm = self._require_kvm or (sys.platform.startswith('linux') and ( |
| (self._target_cpu == 'arm64' and platform.machine() == 'aarch64') or |
| (self._target_cpu == 'x64' and platform.machine() == 'x86_64')) and |
| os.access('/dev/kvm', os.R_OK | os.W_OK)) |
| if enable_kvm: |
| qemu_command.extend(['-enable-kvm', '-cpu', 'host,migratable=no']) |
| else: |
| logging.warning('Unable to launch QEMU with KVM acceleration.') |
| if self._target_cpu == 'arm64': |
| qemu_command.extend(['-cpu', 'cortex-a53']) |
| else: |
| qemu_command.extend(['-cpu', 'Haswell,+smap,-check,-fsgsbase']) |
| |
| # Configure virtual network. It is used in the tests to connect to |
| # testserver running on the host. |
| netdev_config = 'user,id=net0,net=%s,dhcpstart=%s,host=%s' % \ |
| (GUEST_NET, GUEST_IP_ADDRESS, HOST_IP_ADDRESS) |
| |
| self._host_ssh_port = common.GetAvailableTcpPort() |
| netdev_config += ",hostfwd=tcp::%s-:22" % self._host_ssh_port |
| qemu_command.extend([ |
| '-netdev', netdev_config, |
| '-device', '%s,netdev=net0,mac=%s' % (netdev_type, GUEST_MAC_ADDRESS), |
| ]) |
| |
| # We pass a separate stdin stream to qemu. Sharing stdin across processes |
| # leads to flakiness due to the OS prematurely killing the stream and the |
| # Python script panicking and aborting. |
| # The precise root cause is still nebulous, but this fix works. |
| # See crbug.com/741194. |
| logging.debug('Launching QEMU.') |
| logging.debug(' '.join(qemu_command)) |
| |
| # Zircon sends debug logs to serial port (see kernel.serial=legacy flag |
| # above). Serial port is redirected to a file through QEMU stdout. |
| # Unless a |_system_log_file| is explicitly set, we output the kernel serial |
| # log to a temporary file, and print that out if we are unable to connect to |
| # the QEMU guest, to make it easier to diagnose connectivity issues. |
| temporary_system_log_file = None |
| if self._system_log_file: |
| stdout = self._system_log_file |
| stderr = subprocess.STDOUT |
| else: |
| temporary_system_log_file = tempfile.NamedTemporaryFile('w') |
| stdout = temporary_system_log_file |
| stderr = sys.stderr |
| |
| self._qemu_process = subprocess.Popen(qemu_command, stdin=open(os.devnull), |
| stdout=stdout, stderr=stderr) |
| try: |
| self._WaitUntilReady(); |
| except target.FuchsiaTargetException: |
| if temporary_system_log_file: |
| logging.info("Kernel logs:\n" + |
| open(temporary_system_log_file.name, 'r').read()) |
| raise |
| |
| def Shutdown(self): |
| if self._IsQemuStillRunning(): |
| logging.info('Shutting down QEMU.') |
| self._qemu_process.kill() |
| |
| def _IsQemuStillRunning(self): |
| if not self._qemu_process: |
| return False |
| return os.waitpid(self._qemu_process.pid, os.WNOHANG)[0] == 0 |
| |
| def _GetEndpoint(self): |
| if not self._IsQemuStillRunning(): |
| raise Exception('QEMU quit unexpectedly.') |
| return ('localhost', self._host_ssh_port) |
| |
| def _GetSshConfigPath(self): |
| return boot_data.GetSSHConfigPath(self._output_dir) |