qemu_target.py 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. # Copyright 2018 The Chromium Authors. All rights reserved.
  2. # Use of this source code is governed by a BSD-style license that can be
  3. # found in the LICENSE file.
  4. """Implements commands for running and interacting with Fuchsia on QEMU."""
  5. import boot_data
  6. import common
  7. import emu_target
  8. import hashlib
  9. import logging
  10. import os
  11. import platform
  12. import qemu_image
  13. import shutil
  14. import subprocess
  15. import sys
  16. import tempfile
  17. from common import GetHostArchFromPlatform, GetEmuRootForPlatform
  18. from common import EnsurePathExists
  19. from qemu_image import ExecQemuImgWithRetry
  20. from target import FuchsiaTargetException
  21. # Virtual networking configuration data for QEMU.
  22. HOST_IP_ADDRESS = '10.0.2.2'
  23. GUEST_MAC_ADDRESS = '52:54:00:63:5e:7b'
  24. # Capacity of the system's blobstore volume.
  25. EXTENDED_BLOBSTORE_SIZE = 2147483648 # 2GB
  26. def GetTargetType():
  27. return QemuTarget
  28. class QemuTarget(emu_target.EmuTarget):
  29. EMULATOR_NAME = 'qemu'
  30. def __init__(self, out_dir, target_cpu, cpu_cores, require_kvm, ram_size_mb,
  31. logs_dir):
  32. super(QemuTarget, self).__init__(out_dir, target_cpu, logs_dir)
  33. self._cpu_cores=cpu_cores
  34. self._require_kvm=require_kvm
  35. self._ram_size_mb=ram_size_mb
  36. self._host_ssh_port = None
  37. @staticmethod
  38. def CreateFromArgs(args):
  39. return QemuTarget(args.out_dir, args.target_cpu, args.cpu_cores,
  40. args.require_kvm, args.ram_size_mb, args.logs_dir)
  41. def _IsKvmEnabled(self):
  42. kvm_supported = sys.platform.startswith('linux') and \
  43. os.access('/dev/kvm', os.R_OK | os.W_OK)
  44. same_arch = \
  45. (self._target_cpu == 'arm64' and platform.machine() == 'aarch64') or \
  46. (self._target_cpu == 'x64' and platform.machine() == 'x86_64')
  47. if kvm_supported and same_arch:
  48. return True
  49. elif self._require_kvm:
  50. if same_arch:
  51. if not os.path.exists('/dev/kvm'):
  52. kvm_error = 'File /dev/kvm does not exist. Please install KVM first.'
  53. else:
  54. kvm_error = 'To use KVM acceleration, add user to the kvm group '\
  55. 'with "sudo usermod -a -G kvm $USER". Log out and back '\
  56. 'in for the change to take effect.'
  57. raise FuchsiaTargetException(kvm_error)
  58. else:
  59. raise FuchsiaTargetException('KVM unavailable when CPU architecture '\
  60. 'of host is different from that of'\
  61. ' target. See --allow-no-kvm.')
  62. else:
  63. return False
  64. def _BuildQemuConfig(self):
  65. boot_data.AssertBootImagesExist(self._GetTargetSdkArch(), 'qemu')
  66. emu_command = [
  67. '-kernel',
  68. EnsurePathExists(
  69. boot_data.GetTargetFile('qemu-kernel.kernel',
  70. self._GetTargetSdkArch(),
  71. boot_data.TARGET_TYPE_QEMU)),
  72. '-initrd',
  73. EnsurePathExists(
  74. boot_data.GetBootImage(self._out_dir, self._GetTargetSdkArch(),
  75. boot_data.TARGET_TYPE_QEMU)),
  76. '-m',
  77. str(self._ram_size_mb),
  78. '-smp',
  79. str(self._cpu_cores),
  80. # Attach the blobstore and data volumes. Use snapshot mode to discard
  81. # any changes.
  82. '-snapshot',
  83. '-drive',
  84. 'file=%s,format=qcow2,if=none,id=blobstore,snapshot=on' %
  85. _EnsureBlobstoreQcowAndReturnPath(self._out_dir,
  86. self._GetTargetSdkArch()),
  87. '-object',
  88. 'iothread,id=iothread0',
  89. '-device',
  90. 'virtio-blk-pci,drive=blobstore,iothread=iothread0',
  91. # Use stdio for the guest OS only; don't attach the QEMU interactive
  92. # monitor.
  93. '-serial',
  94. 'stdio',
  95. '-monitor',
  96. 'none',
  97. ]
  98. # Configure the machine to emulate, based on the target architecture.
  99. if self._target_cpu == 'arm64':
  100. emu_command.extend([
  101. '-machine',
  102. 'virt-2.12,gic-version=host',
  103. ])
  104. else:
  105. emu_command.extend([
  106. '-machine', 'q35',
  107. ])
  108. # Configure virtual network.
  109. netdev_type = 'virtio-net-pci'
  110. netdev_config = 'type=user,id=net0,restrict=off'
  111. self._host_ssh_port = common.GetAvailableTcpPort()
  112. netdev_config += ",hostfwd=tcp::%s-:22" % self._host_ssh_port
  113. emu_command.extend([
  114. '-netdev', netdev_config,
  115. '-device', '%s,netdev=net0,mac=%s' % (netdev_type, GUEST_MAC_ADDRESS),
  116. ])
  117. # Configure the CPU to emulate.
  118. # On Linux, we can enable lightweight virtualization (KVM) if the host and
  119. # guest architectures are the same.
  120. if self._IsKvmEnabled():
  121. kvm_command = ['-enable-kvm', '-cpu']
  122. if self._target_cpu == 'arm64':
  123. kvm_command.append('host')
  124. else:
  125. kvm_command.append('host,migratable=no,+invtsc')
  126. else:
  127. logging.warning('Unable to launch %s with KVM acceleration. '
  128. 'The guest VM will be slow.' % (self.EMULATOR_NAME))
  129. if self._target_cpu == 'arm64':
  130. kvm_command = ['-cpu', 'cortex-a53']
  131. else:
  132. kvm_command = ['-cpu', 'Haswell,+smap,-check,-fsgsbase']
  133. emu_command.extend(kvm_command)
  134. kernel_args = boot_data.GetKernelArgs()
  135. # TERM=dumb tells the guest OS to not emit ANSI commands that trigger
  136. # noisy ANSI spew from the user's terminal emulator.
  137. kernel_args.append('TERM=dumb')
  138. # Construct kernel cmd line
  139. kernel_args.append('kernel.serial=legacy')
  140. # Don't 'reboot' the emulator if the kernel crashes
  141. kernel_args.append('kernel.halt-on-panic=true')
  142. emu_command.extend(['-append', ' '.join(kernel_args)])
  143. return emu_command
  144. def _BuildCommand(self):
  145. if self._target_cpu == 'arm64':
  146. qemu_exec = 'qemu-system-' + 'aarch64'
  147. elif self._target_cpu == 'x64':
  148. qemu_exec = 'qemu-system-' + 'x86_64'
  149. else:
  150. raise Exception('Unknown target_cpu %s:' % self._target_cpu)
  151. qemu_command = [
  152. os.path.join(GetEmuRootForPlatform(self.EMULATOR_NAME), 'bin',
  153. qemu_exec)
  154. ]
  155. qemu_command.extend(self._BuildQemuConfig())
  156. qemu_command.append('-nographic')
  157. return qemu_command
  158. def _Shutdown(self):
  159. if not self._emu_process:
  160. logging.error('%s did not start' % (self.EMULATOR_NAME))
  161. return
  162. returncode = self._emu_process.poll()
  163. if returncode == None:
  164. logging.info('Shutting down %s' % (self.EMULATOR_NAME))
  165. self._emu_process.kill()
  166. elif returncode == 0:
  167. logging.info('%s quit unexpectedly without errors' % self.EMULATOR_NAME)
  168. elif returncode < 0:
  169. logging.error('%s was terminated by signal %d' %
  170. (self.EMULATOR_NAME, -returncode))
  171. else:
  172. logging.error('%s quit unexpectedly with exit code %d' %
  173. (self.EMULATOR_NAME, returncode))
  174. def _HasNetworking(self):
  175. return False
  176. def _IsEmuStillRunning(self):
  177. if not self._emu_process:
  178. return False
  179. return os.waitpid(self._emu_process.pid, os.WNOHANG)[0] == 0
  180. def _GetEndpoint(self):
  181. if not self._IsEmuStillRunning():
  182. raise Exception('%s quit unexpectedly.' % (self.EMULATOR_NAME))
  183. return (self.LOCAL_ADDRESS, self._host_ssh_port)
  184. def _ComputeFileHash(filename):
  185. hasher = hashlib.md5()
  186. with open(filename, 'rb') as f:
  187. buf = f.read(4096)
  188. while buf:
  189. hasher.update(buf)
  190. buf = f.read(4096)
  191. return hasher.hexdigest()
  192. def _EnsureBlobstoreQcowAndReturnPath(out_dir, target_arch):
  193. """Returns a file containing the Fuchsia blobstore in a QCOW format,
  194. with extra buffer space added for growth."""
  195. qimg_tool = os.path.join(common.GetEmuRootForPlatform('qemu'),
  196. 'bin', 'qemu-img')
  197. fvm_tool = common.GetHostToolPathFromPlatform('fvm')
  198. blobstore_path = boot_data.GetTargetFile('storage-full.blk', target_arch,
  199. 'qemu')
  200. qcow_path = os.path.join(out_dir, 'gen', 'blobstore.qcow')
  201. # Check a hash of the blobstore to determine if we can re-use an existing
  202. # extended version of it.
  203. blobstore_hash_path = os.path.join(out_dir, 'gen', 'blobstore.hash')
  204. current_blobstore_hash = _ComputeFileHash(blobstore_path)
  205. if os.path.exists(blobstore_hash_path) and os.path.exists(qcow_path):
  206. if current_blobstore_hash == open(blobstore_hash_path, 'r').read():
  207. return qcow_path
  208. # Add some extra room for growth to the Blobstore volume.
  209. # Fuchsia is unable to automatically extend FVM volumes at runtime so the
  210. # volume enlargement must be performed prior to QEMU startup.
  211. # The 'fvm' tool only supports extending volumes in-place, so make a
  212. # temporary copy of 'blobstore.bin' before it's mutated.
  213. extended_blobstore = tempfile.NamedTemporaryFile()
  214. shutil.copyfile(blobstore_path, extended_blobstore.name)
  215. subprocess.check_call([fvm_tool, extended_blobstore.name, 'extend',
  216. '--length', str(EXTENDED_BLOBSTORE_SIZE),
  217. blobstore_path])
  218. # Construct a QCOW image from the extended, temporary FVM volume.
  219. # The result will be retained in the build output directory for re-use.
  220. qemu_img_cmd = [qimg_tool, 'convert', '-f', 'raw', '-O', 'qcow2',
  221. '-c', extended_blobstore.name, qcow_path]
  222. # TODO(crbug.com/1046861): Remove arm64 call with retries when bug is fixed.
  223. if common.GetHostArchFromPlatform() == 'arm64':
  224. qemu_image.ExecQemuImgWithRetry(qemu_img_cmd)
  225. else:
  226. subprocess.check_call(qemu_img_cmd)
  227. # Write out a hash of the original blobstore file, so that subsequent runs
  228. # can trivially check if a cached extended FVM volume is available for reuse.
  229. with open(blobstore_hash_path, 'w') as blobstore_hash_file:
  230. blobstore_hash_file.write(current_blobstore_hash)
  231. return qcow_path