123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278 |
- # 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 emu_target
- import hashlib
- import logging
- import os
- import platform
- import qemu_image
- import shutil
- import subprocess
- import sys
- import tempfile
- from common import GetHostArchFromPlatform, GetEmuRootForPlatform
- from common import EnsurePathExists
- from qemu_image import ExecQemuImgWithRetry
- from target import FuchsiaTargetException
- # Virtual networking configuration data for QEMU.
- HOST_IP_ADDRESS = '10.0.2.2'
- GUEST_MAC_ADDRESS = '52:54:00:63:5e:7b'
- # Capacity of the system's blobstore volume.
- EXTENDED_BLOBSTORE_SIZE = 2147483648 # 2GB
- def GetTargetType():
- return QemuTarget
- class QemuTarget(emu_target.EmuTarget):
- EMULATOR_NAME = 'qemu'
- def __init__(self, out_dir, target_cpu, cpu_cores, require_kvm, ram_size_mb,
- logs_dir):
- super(QemuTarget, self).__init__(out_dir, target_cpu, logs_dir)
- self._cpu_cores=cpu_cores
- self._require_kvm=require_kvm
- self._ram_size_mb=ram_size_mb
- self._host_ssh_port = None
- @staticmethod
- def CreateFromArgs(args):
- return QemuTarget(args.out_dir, args.target_cpu, args.cpu_cores,
- args.require_kvm, args.ram_size_mb, args.logs_dir)
- def _IsKvmEnabled(self):
- kvm_supported = sys.platform.startswith('linux') and \
- os.access('/dev/kvm', os.R_OK | os.W_OK)
- same_arch = \
- (self._target_cpu == 'arm64' and platform.machine() == 'aarch64') or \
- (self._target_cpu == 'x64' and platform.machine() == 'x86_64')
- if kvm_supported and same_arch:
- return True
- elif self._require_kvm:
- if same_arch:
- if not os.path.exists('/dev/kvm'):
- kvm_error = 'File /dev/kvm does not exist. Please install KVM first.'
- else:
- kvm_error = 'To use KVM acceleration, add user to the kvm group '\
- 'with "sudo usermod -a -G kvm $USER". Log out and back '\
- 'in for the change to take effect.'
- raise FuchsiaTargetException(kvm_error)
- else:
- raise FuchsiaTargetException('KVM unavailable when CPU architecture '\
- 'of host is different from that of'\
- ' target. See --allow-no-kvm.')
- else:
- return False
- def _BuildQemuConfig(self):
- boot_data.AssertBootImagesExist(self._GetTargetSdkArch(), 'qemu')
- emu_command = [
- '-kernel',
- EnsurePathExists(
- boot_data.GetTargetFile('qemu-kernel.kernel',
- self._GetTargetSdkArch(),
- boot_data.TARGET_TYPE_QEMU)),
- '-initrd',
- EnsurePathExists(
- boot_data.GetBootImage(self._out_dir, self._GetTargetSdkArch(),
- boot_data.TARGET_TYPE_QEMU)),
- '-m',
- str(self._ram_size_mb),
- '-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' %
- _EnsureBlobstoreQcowAndReturnPath(self._out_dir,
- self._GetTargetSdkArch()),
- '-object',
- 'iothread,id=iothread0',
- '-device',
- 'virtio-blk-pci,drive=blobstore,iothread=iothread0',
- # Use stdio for the guest OS only; don't attach the QEMU interactive
- # monitor.
- '-serial',
- 'stdio',
- '-monitor',
- 'none',
- ]
- # Configure the machine to emulate, based on the target architecture.
- if self._target_cpu == 'arm64':
- emu_command.extend([
- '-machine',
- 'virt-2.12,gic-version=host',
- ])
- else:
- emu_command.extend([
- '-machine', 'q35',
- ])
- # Configure virtual network.
- netdev_type = 'virtio-net-pci'
- netdev_config = 'type=user,id=net0,restrict=off'
- self._host_ssh_port = common.GetAvailableTcpPort()
- netdev_config += ",hostfwd=tcp::%s-:22" % self._host_ssh_port
- emu_command.extend([
- '-netdev', netdev_config,
- '-device', '%s,netdev=net0,mac=%s' % (netdev_type, GUEST_MAC_ADDRESS),
- ])
- # Configure the CPU to emulate.
- # On Linux, we can enable lightweight virtualization (KVM) if the host and
- # guest architectures are the same.
- if self._IsKvmEnabled():
- kvm_command = ['-enable-kvm', '-cpu']
- if self._target_cpu == 'arm64':
- kvm_command.append('host')
- else:
- kvm_command.append('host,migratable=no,+invtsc')
- else:
- logging.warning('Unable to launch %s with KVM acceleration. '
- 'The guest VM will be slow.' % (self.EMULATOR_NAME))
- if self._target_cpu == 'arm64':
- kvm_command = ['-cpu', 'cortex-a53']
- else:
- kvm_command = ['-cpu', 'Haswell,+smap,-check,-fsgsbase']
- emu_command.extend(kvm_command)
- kernel_args = boot_data.GetKernelArgs()
- # 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')
- # Construct kernel cmd line
- kernel_args.append('kernel.serial=legacy')
- # Don't 'reboot' the emulator if the kernel crashes
- kernel_args.append('kernel.halt-on-panic=true')
- emu_command.extend(['-append', ' '.join(kernel_args)])
- return emu_command
- def _BuildCommand(self):
- if self._target_cpu == 'arm64':
- qemu_exec = 'qemu-system-' + 'aarch64'
- elif self._target_cpu == 'x64':
- qemu_exec = 'qemu-system-' + 'x86_64'
- else:
- raise Exception('Unknown target_cpu %s:' % self._target_cpu)
- qemu_command = [
- os.path.join(GetEmuRootForPlatform(self.EMULATOR_NAME), 'bin',
- qemu_exec)
- ]
- qemu_command.extend(self._BuildQemuConfig())
- qemu_command.append('-nographic')
- return qemu_command
- def _Shutdown(self):
- if not self._emu_process:
- logging.error('%s did not start' % (self.EMULATOR_NAME))
- return
- returncode = self._emu_process.poll()
- if returncode == None:
- logging.info('Shutting down %s' % (self.EMULATOR_NAME))
- self._emu_process.kill()
- elif returncode == 0:
- logging.info('%s quit unexpectedly without errors' % self.EMULATOR_NAME)
- elif returncode < 0:
- logging.error('%s was terminated by signal %d' %
- (self.EMULATOR_NAME, -returncode))
- else:
- logging.error('%s quit unexpectedly with exit code %d' %
- (self.EMULATOR_NAME, returncode))
- def _HasNetworking(self):
- return False
- def _IsEmuStillRunning(self):
- if not self._emu_process:
- return False
- return os.waitpid(self._emu_process.pid, os.WNOHANG)[0] == 0
- def _GetEndpoint(self):
- if not self._IsEmuStillRunning():
- raise Exception('%s quit unexpectedly.' % (self.EMULATOR_NAME))
- return (self.LOCAL_ADDRESS, self._host_ssh_port)
- def _ComputeFileHash(filename):
- hasher = hashlib.md5()
- with open(filename, 'rb') as f:
- buf = f.read(4096)
- while buf:
- hasher.update(buf)
- buf = f.read(4096)
- return hasher.hexdigest()
- def _EnsureBlobstoreQcowAndReturnPath(out_dir, target_arch):
- """Returns a file containing the Fuchsia blobstore in a QCOW format,
- with extra buffer space added for growth."""
- qimg_tool = os.path.join(common.GetEmuRootForPlatform('qemu'),
- 'bin', 'qemu-img')
- fvm_tool = common.GetHostToolPathFromPlatform('fvm')
- blobstore_path = boot_data.GetTargetFile('storage-full.blk', target_arch,
- 'qemu')
- qcow_path = os.path.join(out_dir, 'gen', 'blobstore.qcow')
- # Check a hash of the blobstore to determine if we can re-use an existing
- # extended version of it.
- blobstore_hash_path = os.path.join(out_dir, 'gen', 'blobstore.hash')
- current_blobstore_hash = _ComputeFileHash(blobstore_path)
- if os.path.exists(blobstore_hash_path) and os.path.exists(qcow_path):
- if current_blobstore_hash == open(blobstore_hash_path, 'r').read():
- return qcow_path
- # Add some extra room for growth to the Blobstore volume.
- # Fuchsia is unable to automatically extend FVM volumes at runtime so the
- # volume enlargement must be performed prior to QEMU startup.
- # The 'fvm' tool only supports extending volumes in-place, so make a
- # temporary copy of 'blobstore.bin' before it's mutated.
- extended_blobstore = tempfile.NamedTemporaryFile()
- shutil.copyfile(blobstore_path, extended_blobstore.name)
- subprocess.check_call([fvm_tool, extended_blobstore.name, 'extend',
- '--length', str(EXTENDED_BLOBSTORE_SIZE),
- blobstore_path])
- # Construct a QCOW image from the extended, temporary FVM volume.
- # The result will be retained in the build output directory for re-use.
- qemu_img_cmd = [qimg_tool, 'convert', '-f', 'raw', '-O', 'qcow2',
- '-c', extended_blobstore.name, qcow_path]
- # TODO(crbug.com/1046861): Remove arm64 call with retries when bug is fixed.
- if common.GetHostArchFromPlatform() == 'arm64':
- qemu_image.ExecQemuImgWithRetry(qemu_img_cmd)
- else:
- subprocess.check_call(qemu_img_cmd)
- # Write out a hash of the original blobstore file, so that subsequent runs
- # can trivially check if a cached extended FVM volume is available for reuse.
- with open(blobstore_hash_path, 'w') as blobstore_hash_file:
- blobstore_hash_file.write(current_blobstore_hash)
- return qcow_path
|