blob: 6455170ae8c67250645cda30c3a55073ab8d6d8c [file] [log] [blame]
Mike Frysingere58c0e22017-10-04 15:43:30 -04001# -*- coding: utf-8 -*-
Achuith Bhandarkard8d19292016-05-03 14:32:58 -07002# Copyright 2016 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
Mike Frysinger666566c2016-09-21 00:00:21 -04006"""Script for VM Management."""
Achuith Bhandarkard8d19292016-05-03 14:32:58 -07007
8from __future__ import print_function
9
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +010010import argparse
Achuith Bhandarkar22bfedf2017-11-08 11:59:38 +010011import distutils.version
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070012import os
Achuith Bhandarkar22bfedf2017-11-08 11:59:38 +010013import re
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070014
15from chromite.lib import commandline
16from chromite.lib import cros_build_lib
17from chromite.lib import cros_logging as logging
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -070018from chromite.lib import osutils
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070019from chromite.lib import remote_access
Achuith Bhandarkar2f8352f2017-06-02 12:47:18 -070020from chromite.lib import retry_util
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070021
22
23class VMError(Exception):
24 """Exception for VM failures."""
25
26 def __init__(self, message):
27 super(VMError, self).__init__()
28 logging.error(message)
29
30
31class VM(object):
32 """Class for managing a VM."""
33
34 SSH_PORT = 9222
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070035
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +010036 def __init__(self, argv):
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070037 """Initialize VM.
38
39 Args:
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +010040 argv: command line args.
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070041 """
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +010042 opts = self._ParseArgs(argv)
43 opts.Freeze()
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070044
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +010045 self.qemu_path = opts.qemu_path
Achuith Bhandarkar41259652017-11-14 10:31:02 -080046 self.qemu_bios_path = opts.qemu_bios_path
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +010047 self.enable_kvm = opts.enable_kvm
Achuith Bhandarkarf877da22017-09-12 12:27:39 -070048 # We don't need sudo access for software emulation or if /dev/kvm is
49 # writeable.
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +010050 self.use_sudo = self.enable_kvm and not os.access('/dev/kvm', os.W_OK)
51 self.display = opts.display
52 self.image_path = opts.image_path
53 self.ssh_port = opts.ssh_port
54 self.dry_run = opts.dry_run
55
56 self.start = opts.start
57 self.stop = opts.stop
58 self.cmd = opts.args[1:] if opts.cmd else None
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070059
Achuith Bhandarkaree3163d2016-10-19 12:58:35 -070060 self.vm_dir = os.path.join(osutils.GetGlobalTempDir(), 'cros_vm')
61 if os.path.exists(self.vm_dir):
62 # For security, ensure that vm_dir is not a symlink, and is owned by us or
63 # by root.
64 assert not os.path.islink(self.vm_dir), \
65 'VM state dir is misconfigured; please recreate: %s' % self.vm_dir
66 st_uid = os.stat(self.vm_dir).st_uid
67 assert st_uid == 0 or st_uid == os.getuid(), \
68 'VM state dir is misconfigured; please recreate: %s' % self.vm_dir
69
70 self.pidfile = os.path.join(self.vm_dir, 'kvm.pid')
71 self.kvm_monitor = os.path.join(self.vm_dir, 'kvm.monitor')
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070072 self.kvm_pipe_in = '%s.in' % self.kvm_monitor # to KVM
73 self.kvm_pipe_out = '%s.out' % self.kvm_monitor # from KVM
74 self.kvm_serial = '%s.serial' % self.kvm_monitor
75
Achuith Bhandarkar65d1a892017-05-08 14:13:12 -070076 self.remote = remote_access.RemoteDevice(remote_access.LOCALHOST,
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +010077 port=self.ssh_port)
Achuith Bhandarkar65d1a892017-05-08 14:13:12 -070078
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070079 # TODO(achuith): support nographics, snapshot, mem_path, usb_passthrough,
80 # moblab, etc.
81
Achuith Bhandarkaree3163d2016-10-19 12:58:35 -070082 def _RunCommand(self, *args, **kwargs):
83 """Use SudoRunCommand or RunCommand as necessary."""
84 if self.use_sudo:
85 return cros_build_lib.SudoRunCommand(*args, **kwargs)
86 else:
87 return cros_build_lib.RunCommand(*args, **kwargs)
88
89 def _CleanupFiles(self, recreate):
90 """Cleanup vm_dir.
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070091
92 Args:
Achuith Bhandarkaree3163d2016-10-19 12:58:35 -070093 recreate: recreate vm_dir.
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070094 """
Mike Frysinger97080242017-09-13 01:58:45 -040095 osutils.RmDir(self.vm_dir, ignore_missing=True, sudo=self.use_sudo)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070096 if recreate:
Mike Frysinger97080242017-09-13 01:58:45 -040097 osutils.SafeMakedirs(self.vm_dir)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070098
Achuith Bhandarkar22bfedf2017-11-08 11:59:38 +010099 @cros_build_lib.MemoizedSingleCall
100 def QemuVersion(self):
101 """Determine QEMU version."""
102 version_str = self._RunCommand([self.qemu_path, '--version'],
103 capture_output=True).output
104 # version string looks like one of these:
105 # QEMU emulator version 2.0.0 (Debian 2.0.0+dfsg-2ubuntu1.36), Copyright (c)
106 # 2003-2008 Fabrice Bellard
107 #
108 # QEMU emulator version 2.6.0, Copyright (c) 2003-2008 Fabrice Bellard
109 #
110 # qemu-x86_64 version 2.10.1
111 # Copyright (c) 2003-2017 Fabrice Bellard and the QEMU Project developers
112 m = re.search(r"version ([0-9.]+)", version_str)
113 if not m:
114 raise VMError('Unable to determine QEMU version from:\n%s.' % version_str)
115 return m.group(1)
116
117 def _CheckQemuMinVersion(self):
118 """Ensure minimum QEMU version."""
119 min_qemu_version = '2.6.0'
120 logging.info('QEMU version %s', self.QemuVersion())
121 LooseVersion = distutils.version.LooseVersion
122 if LooseVersion(self.QemuVersion()) < LooseVersion(min_qemu_version):
123 raise VMError('QEMU %s is the minimum supported version. You have %s.'
124 % (min_qemu_version, self.QemuVersion()))
125
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +0100126 def Run(self):
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700127 """Performs an action, one of start, stop, or run a command in the VM.
128
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700129 Returns:
130 cmd output.
131 """
132
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +0100133 if not self.start and not self.stop and not self.cmd:
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700134 raise VMError('Must specify one of start, stop, or cmd.')
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +0100135 if self.start:
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700136 self.Start()
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +0100137 if self.cmd:
138 return self.RemoteCommand(self.cmd)
139 if self.stop:
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700140 self.Stop()
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700141
142 def Start(self):
143 """Start the VM."""
144
145 self.Stop()
146
Achuith Bhandarkar022d69c2016-10-05 14:28:14 -0700147 logging.debug('Start VM')
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700148 if not self.qemu_path:
149 self.qemu_path = osutils.Which('qemu-system-x86_64')
150 if not self.qemu_path:
Achuith Bhandarkar41259652017-11-14 10:31:02 -0800151 raise VMError('qemu not found.')
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700152 logging.debug('qemu path=%s', self.qemu_path)
Achuith Bhandarkar41259652017-11-14 10:31:02 -0800153 qemu_args = [self.qemu_path]
154
155 if self.qemu_bios_path and os.path.isdir(self.qemu_bios_path):
156 qemu_args += ['-L', self.qemu_bios_path]
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700157
158 if not self.image_path:
159 self.image_path = os.environ.get('VM_IMAGE_PATH', '')
160 logging.debug('vm image path=%s', self.image_path)
161 if not self.image_path or not os.path.exists(self.image_path):
Achuith Bhandarkar9788efd2017-11-07 12:34:23 +0100162 raise VMError('No VM image path found. '
163 'Use cros chrome-sdk --download-vm.')
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700164
165 self._CleanupFiles(recreate=True)
Mike Frysinger97080242017-09-13 01:58:45 -0400166 # Make sure we can read these files later on by creating them as ourselves.
167 osutils.Touch(self.kvm_serial)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700168 for pipe in [self.kvm_pipe_in, self.kvm_pipe_out]:
169 os.mkfifo(pipe, 0600)
Mike Frysinger97080242017-09-13 01:58:45 -0400170 osutils.Touch(self.pidfile)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700171
Achuith Bhandarkar22bfedf2017-11-08 11:59:38 +0100172 self._CheckQemuMinVersion()
173
Achuith Bhandarkar41259652017-11-14 10:31:02 -0800174 qemu_args += [
175 '-m', '2G', '-smp', '4', '-vga', 'virtio', '-daemonize',
Nicolas Norvez02525112017-11-30 16:29:23 -0800176 '-usbdevice', 'tablet',
Achuith Bhandarkar41259652017-11-14 10:31:02 -0800177 '-pidfile', self.pidfile,
178 '-chardev', 'pipe,id=control_pipe,path=%s' % self.kvm_monitor,
179 '-serial', 'file:%s' % self.kvm_serial,
180 '-mon', 'chardev=control_pipe',
181 # Qemu-vlans are used by qemu to separate out network traffic on the
182 # slirp network bridge. qemu forwards traffic on a slirp vlan to all
183 # ports conected on that vlan. By default, slirp ports are on vlan
184 # 0. We explicitly set a vlan here so that another qemu VM using
185 # slirp doesn't conflict with our network traffic.
186 '-net', 'nic,model=virtio,vlan=%d' % self.ssh_port,
187 '-net', 'user,hostfwd=tcp:127.0.0.1:%d-:22,vlan=%d'
188 % (self.ssh_port, self.ssh_port),
189 '-drive', 'file=%s,index=0,media=disk,cache=unsafe,format=raw'
190 % self.image_path,
191 ]
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700192 if self.enable_kvm:
Achuith Bhandarkar41259652017-11-14 10:31:02 -0800193 qemu_args.append('-enable-kvm')
Achuith Bhandarkarb891adb2016-10-24 18:43:22 -0700194 if not self.display:
Achuith Bhandarkar41259652017-11-14 10:31:02 -0800195 qemu_args.extend(['-display', 'none'])
196 logging.info(cros_build_lib.CmdToStr(qemu_args))
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700197 logging.info('Pid file: %s', self.pidfile)
198 if not self.dry_run:
Achuith Bhandarkar41259652017-11-14 10:31:02 -0800199 self._RunCommand(qemu_args)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700200
201 def _GetVMPid(self):
202 """Get the pid of the VM.
203
204 Returns:
205 pid of the VM.
206 """
Achuith Bhandarkaree3163d2016-10-19 12:58:35 -0700207 if not os.path.exists(self.vm_dir):
208 logging.debug('%s not present.', self.vm_dir)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700209 return 0
210
211 if not os.path.exists(self.pidfile):
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700212 logging.info('%s does not exist.', self.pidfile)
213 return 0
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700214
Mike Frysinger97080242017-09-13 01:58:45 -0400215 pid = osutils.ReadFile(self.pidfile).rstrip()
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700216 if not pid.isdigit():
Mike Frysinger97080242017-09-13 01:58:45 -0400217 # Ignore blank/empty files.
218 if pid:
219 logging.error('%s in %s is not a pid.', pid, self.pidfile)
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700220 return 0
221
Achuith Bhandarkar022d69c2016-10-05 14:28:14 -0700222 return int(pid)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700223
224 def IsRunning(self):
225 """Returns True if there's a running VM.
226
227 Returns:
228 True if there's a running VM.
229 """
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700230 pid = self._GetVMPid()
Achuith Bhandarkar022d69c2016-10-05 14:28:14 -0700231 if not pid:
232 return False
233
234 # Make sure the process actually exists.
Mike Frysinger97080242017-09-13 01:58:45 -0400235 return os.path.isdir('/proc/%i' % pid)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700236
237 def Stop(self):
238 """Stop the VM."""
Achuith Bhandarkar022d69c2016-10-05 14:28:14 -0700239 logging.debug('Stop VM')
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700240
241 pid = self._GetVMPid()
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700242 if pid:
243 logging.info('Killing %d.', pid)
244 if not self.dry_run:
Achuith Bhandarkaree3163d2016-10-19 12:58:35 -0700245 self._RunCommand(['kill', '-9', str(pid)], error_code_ok=True)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700246
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700247 self._CleanupFiles(recreate=False)
248
Achuith Bhandarkar2f8352f2017-06-02 12:47:18 -0700249 def _WaitForProcs(self):
250 """Wait for expected processes to launch."""
251 class _TooFewPidsException(Exception):
252 """Exception for _GetRunningPids to throw."""
253
254 def _GetRunningPids(exe, numpids):
255 pids = self.remote.GetRunningPids(exe, full_path=False)
256 logging.info('%s pids: %s', exe, repr(pids))
257 if len(pids) < numpids:
258 raise _TooFewPidsException()
259
260 def _WaitForProc(exe, numpids):
261 try:
262 retry_util.RetryException(
Achuith Bhandarkar0e7b8502017-06-12 15:32:41 -0700263 exception=_TooFewPidsException,
Achuith Bhandarkar2f8352f2017-06-02 12:47:18 -0700264 max_retry=20,
265 functor=lambda: _GetRunningPids(exe, numpids),
266 sleep=2)
267 except _TooFewPidsException:
268 raise VMError('_WaitForProcs failed: timed out while waiting for '
269 '%d %s processes to start.' % (numpids, exe))
270
271 # We could also wait for session_manager, nacl_helper, etc, but chrome is
272 # the long pole. We expect the parent, 2 zygotes, gpu-process, renderer.
273 # This could potentially break with Mustash.
274 _WaitForProc('chrome', 5)
275
276 def WaitForBoot(self):
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700277 """Wait for the VM to boot up.
278
Achuith Bhandarkar2f8352f2017-06-02 12:47:18 -0700279 Wait for ssh connection to become active, and wait for all expected chrome
280 processes to be launched.
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700281 """
Achuith Bhandarkaree3163d2016-10-19 12:58:35 -0700282 if not os.path.exists(self.vm_dir):
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700283 self.Start()
284
Achuith Bhandarkar2f8352f2017-06-02 12:47:18 -0700285 try:
286 result = retry_util.RetryException(
Achuith Bhandarkar0e7b8502017-06-12 15:32:41 -0700287 exception=remote_access.SSHConnectionError,
Achuith Bhandarkar2f8352f2017-06-02 12:47:18 -0700288 max_retry=10,
289 functor=lambda: self.RemoteCommand(cmd=['echo']),
290 sleep=5)
291 except remote_access.SSHConnectionError:
292 raise VMError('WaitForBoot timed out trying to connect to VM.')
293
294 if result.returncode != 0:
295 raise VMError('WaitForBoot failed: %s.' % result.error)
296
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +0100297 # Chrome can take a while to start with software emulation.
298 if not self.enable_kvm:
299 self._WaitForProcs()
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700300
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +0100301 def RemoteCommand(self, cmd, **kwargs):
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700302 """Run a remote command in the VM.
303
304 Args:
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +0100305 cmd: command to run.
306 kwargs: additional args (see documentation for RemoteDevice.RunCommand).
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700307 """
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700308 if not self.dry_run:
Achuith Bhandarkar65d1a892017-05-08 14:13:12 -0700309 return self.remote.RunCommand(cmd, debug_level=logging.INFO,
310 combine_stdout_stderr=True,
311 log_output=True,
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +0100312 error_code_ok=True,
313 **kwargs)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700314
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +0100315 @staticmethod
316 def _ParseArgs(argv):
317 """Parse a list of args.
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700318
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +0100319 Args:
320 argv: list of command line arguments.
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700321
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +0100322 Returns:
323 List of parsed opts.
324 """
325 parser = commandline.ArgumentParser(description=__doc__)
326 parser.add_argument('--start', action='store_true', default=False,
327 help='Start the VM.')
328 parser.add_argument('--stop', action='store_true', default=False,
329 help='Stop the VM.')
330 parser.add_argument('--image-path', type='path',
331 help='Path to VM image to launch with --start.')
332 parser.add_argument('--qemu-path', type='path',
333 help='Path of qemu binary to launch with --start.')
Achuith Bhandarkar41259652017-11-14 10:31:02 -0800334 parser.add_argument('--qemu-bios-path', type='path',
335 help='Path of directory with qemu bios files.')
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +0100336 parser.add_argument('--disable-kvm', dest='enable_kvm',
337 action='store_false', default=True,
338 help='Disable KVM, use software emulation.')
339 parser.add_argument('--no-display', dest='display',
340 action='store_false', default=True,
341 help='Do not display video output.')
342 parser.add_argument('--ssh-port', type=int, default=VM.SSH_PORT,
343 help='ssh port to communicate with VM.')
344 parser.add_argument('--dry-run', action='store_true', default=False,
345 help='dry run for debugging.')
346 parser.add_argument('--cmd', action='store_true', default=False,
347 help='Run a command in the VM.')
348 parser.add_argument('args', nargs=argparse.REMAINDER,
349 help='Command to run in the VM.')
350 return parser.parse_args(argv)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700351
352def main(argv):
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +0100353 vm = VM(argv)
354 vm.Run()