blob: 7c4e9b136d8c55295ea97d0509f188190fef00a6 [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 Bhandarkard8d19292016-05-03 14:32:58 -070011import os
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070012
13from chromite.lib import commandline
14from chromite.lib import cros_build_lib
15from chromite.lib import cros_logging as logging
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -070016from chromite.lib import osutils
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070017from chromite.lib import remote_access
Achuith Bhandarkar2f8352f2017-06-02 12:47:18 -070018from chromite.lib import retry_util
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070019
20
21class VMError(Exception):
22 """Exception for VM failures."""
23
24 def __init__(self, message):
25 super(VMError, self).__init__()
26 logging.error(message)
27
28
29class VM(object):
30 """Class for managing a VM."""
31
32 SSH_PORT = 9222
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070033
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +010034 def __init__(self, argv):
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070035 """Initialize VM.
36
37 Args:
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +010038 argv: command line args.
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070039 """
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +010040 opts = self._ParseArgs(argv)
41 opts.Freeze()
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070042
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +010043 self.qemu_path = opts.qemu_path
44 self.enable_kvm = opts.enable_kvm
Achuith Bhandarkarf877da22017-09-12 12:27:39 -070045 # We don't need sudo access for software emulation or if /dev/kvm is
46 # writeable.
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +010047 self.use_sudo = self.enable_kvm and not os.access('/dev/kvm', os.W_OK)
48 self.display = opts.display
49 self.image_path = opts.image_path
50 self.ssh_port = opts.ssh_port
51 self.dry_run = opts.dry_run
52
53 self.start = opts.start
54 self.stop = opts.stop
55 self.cmd = opts.args[1:] if opts.cmd else None
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070056
Achuith Bhandarkaree3163d2016-10-19 12:58:35 -070057 self.vm_dir = os.path.join(osutils.GetGlobalTempDir(), 'cros_vm')
58 if os.path.exists(self.vm_dir):
59 # For security, ensure that vm_dir is not a symlink, and is owned by us or
60 # by root.
61 assert not os.path.islink(self.vm_dir), \
62 'VM state dir is misconfigured; please recreate: %s' % self.vm_dir
63 st_uid = os.stat(self.vm_dir).st_uid
64 assert st_uid == 0 or st_uid == os.getuid(), \
65 'VM state dir is misconfigured; please recreate: %s' % self.vm_dir
66
67 self.pidfile = os.path.join(self.vm_dir, 'kvm.pid')
68 self.kvm_monitor = os.path.join(self.vm_dir, 'kvm.monitor')
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070069 self.kvm_pipe_in = '%s.in' % self.kvm_monitor # to KVM
70 self.kvm_pipe_out = '%s.out' % self.kvm_monitor # from KVM
71 self.kvm_serial = '%s.serial' % self.kvm_monitor
72
Achuith Bhandarkar65d1a892017-05-08 14:13:12 -070073 self.remote = remote_access.RemoteDevice(remote_access.LOCALHOST,
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +010074 port=self.ssh_port)
Achuith Bhandarkar65d1a892017-05-08 14:13:12 -070075
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070076 # TODO(achuith): support nographics, snapshot, mem_path, usb_passthrough,
77 # moblab, etc.
78
Achuith Bhandarkaree3163d2016-10-19 12:58:35 -070079 def _RunCommand(self, *args, **kwargs):
80 """Use SudoRunCommand or RunCommand as necessary."""
81 if self.use_sudo:
82 return cros_build_lib.SudoRunCommand(*args, **kwargs)
83 else:
84 return cros_build_lib.RunCommand(*args, **kwargs)
85
86 def _CleanupFiles(self, recreate):
87 """Cleanup vm_dir.
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070088
89 Args:
Achuith Bhandarkaree3163d2016-10-19 12:58:35 -070090 recreate: recreate vm_dir.
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070091 """
Mike Frysinger97080242017-09-13 01:58:45 -040092 osutils.RmDir(self.vm_dir, ignore_missing=True, sudo=self.use_sudo)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070093 if recreate:
Mike Frysinger97080242017-09-13 01:58:45 -040094 osutils.SafeMakedirs(self.vm_dir)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070095
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +010096 def Run(self):
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070097 """Performs an action, one of start, stop, or run a command in the VM.
98
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070099 Returns:
100 cmd output.
101 """
102
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +0100103 if not self.start and not self.stop and not self.cmd:
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700104 raise VMError('Must specify one of start, stop, or cmd.')
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +0100105 if self.start:
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700106 self.Start()
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +0100107 if self.cmd:
108 return self.RemoteCommand(self.cmd)
109 if self.stop:
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700110 self.Stop()
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700111
112 def Start(self):
113 """Start the VM."""
114
115 self.Stop()
116
Achuith Bhandarkar022d69c2016-10-05 14:28:14 -0700117 logging.debug('Start VM')
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700118 if not self.qemu_path:
119 self.qemu_path = osutils.Which('qemu-system-x86_64')
120 if not self.qemu_path:
121 raise VMError('qemu not found.')
122 logging.debug('qemu path=%s', self.qemu_path)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700123
124 if not self.image_path:
125 self.image_path = os.environ.get('VM_IMAGE_PATH', '')
126 logging.debug('vm image path=%s', self.image_path)
127 if not self.image_path or not os.path.exists(self.image_path):
128 raise VMError('VM image path %s does not exist.' % self.image_path)
129
130 self._CleanupFiles(recreate=True)
Mike Frysinger97080242017-09-13 01:58:45 -0400131 # Make sure we can read these files later on by creating them as ourselves.
132 osutils.Touch(self.kvm_serial)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700133 for pipe in [self.kvm_pipe_in, self.kvm_pipe_out]:
134 os.mkfifo(pipe, 0600)
Mike Frysinger97080242017-09-13 01:58:45 -0400135 osutils.Touch(self.pidfile)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700136
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700137 args = [self.qemu_path, '-m', '2G', '-smp', '4', '-vga', 'cirrus',
138 '-daemonize',
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700139 '-pidfile', self.pidfile,
140 '-chardev', 'pipe,id=control_pipe,path=%s' % self.kvm_monitor,
141 '-serial', 'file:%s' % self.kvm_serial,
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700142 '-mon', 'chardev=control_pipe',
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700143 '-net', 'nic,model=virtio',
Nicolas Norvez80329de2017-03-27 14:32:24 -0700144 '-net', 'user,hostfwd=tcp:127.0.0.1:%d-:22' % self.ssh_port,
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700145 '-drive', 'file=%s,index=0,media=disk,cache=unsafe'
146 % self.image_path]
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700147 if self.enable_kvm:
148 args.append('-enable-kvm')
Achuith Bhandarkarb891adb2016-10-24 18:43:22 -0700149 if not self.display:
150 args.extend(['-display', 'none'])
Mike Frysinger97080242017-09-13 01:58:45 -0400151 logging.info(cros_build_lib.CmdToStr(args))
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700152 logging.info('Pid file: %s', self.pidfile)
153 if not self.dry_run:
Achuith Bhandarkaree3163d2016-10-19 12:58:35 -0700154 self._RunCommand(args)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700155
156 def _GetVMPid(self):
157 """Get the pid of the VM.
158
159 Returns:
160 pid of the VM.
161 """
Achuith Bhandarkaree3163d2016-10-19 12:58:35 -0700162 if not os.path.exists(self.vm_dir):
163 logging.debug('%s not present.', self.vm_dir)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700164 return 0
165
166 if not os.path.exists(self.pidfile):
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700167 logging.info('%s does not exist.', self.pidfile)
168 return 0
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700169
Mike Frysinger97080242017-09-13 01:58:45 -0400170 pid = osutils.ReadFile(self.pidfile).rstrip()
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700171 if not pid.isdigit():
Mike Frysinger97080242017-09-13 01:58:45 -0400172 # Ignore blank/empty files.
173 if pid:
174 logging.error('%s in %s is not a pid.', pid, self.pidfile)
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700175 return 0
176
Achuith Bhandarkar022d69c2016-10-05 14:28:14 -0700177 return int(pid)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700178
179 def IsRunning(self):
180 """Returns True if there's a running VM.
181
182 Returns:
183 True if there's a running VM.
184 """
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700185 pid = self._GetVMPid()
Achuith Bhandarkar022d69c2016-10-05 14:28:14 -0700186 if not pid:
187 return False
188
189 # Make sure the process actually exists.
Mike Frysinger97080242017-09-13 01:58:45 -0400190 return os.path.isdir('/proc/%i' % pid)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700191
192 def Stop(self):
193 """Stop the VM."""
Achuith Bhandarkar022d69c2016-10-05 14:28:14 -0700194 logging.debug('Stop VM')
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700195
196 pid = self._GetVMPid()
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700197 if pid:
198 logging.info('Killing %d.', pid)
199 if not self.dry_run:
Achuith Bhandarkaree3163d2016-10-19 12:58:35 -0700200 self._RunCommand(['kill', '-9', str(pid)], error_code_ok=True)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700201
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700202 self._CleanupFiles(recreate=False)
203
Achuith Bhandarkar2f8352f2017-06-02 12:47:18 -0700204 def _WaitForProcs(self):
205 """Wait for expected processes to launch."""
206 class _TooFewPidsException(Exception):
207 """Exception for _GetRunningPids to throw."""
208
209 def _GetRunningPids(exe, numpids):
210 pids = self.remote.GetRunningPids(exe, full_path=False)
211 logging.info('%s pids: %s', exe, repr(pids))
212 if len(pids) < numpids:
213 raise _TooFewPidsException()
214
215 def _WaitForProc(exe, numpids):
216 try:
217 retry_util.RetryException(
Achuith Bhandarkar0e7b8502017-06-12 15:32:41 -0700218 exception=_TooFewPidsException,
Achuith Bhandarkar2f8352f2017-06-02 12:47:18 -0700219 max_retry=20,
220 functor=lambda: _GetRunningPids(exe, numpids),
221 sleep=2)
222 except _TooFewPidsException:
223 raise VMError('_WaitForProcs failed: timed out while waiting for '
224 '%d %s processes to start.' % (numpids, exe))
225
226 # We could also wait for session_manager, nacl_helper, etc, but chrome is
227 # the long pole. We expect the parent, 2 zygotes, gpu-process, renderer.
228 # This could potentially break with Mustash.
229 _WaitForProc('chrome', 5)
230
231 def WaitForBoot(self):
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700232 """Wait for the VM to boot up.
233
Achuith Bhandarkar2f8352f2017-06-02 12:47:18 -0700234 Wait for ssh connection to become active, and wait for all expected chrome
235 processes to be launched.
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700236 """
Achuith Bhandarkaree3163d2016-10-19 12:58:35 -0700237 if not os.path.exists(self.vm_dir):
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700238 self.Start()
239
Achuith Bhandarkar2f8352f2017-06-02 12:47:18 -0700240 try:
241 result = retry_util.RetryException(
Achuith Bhandarkar0e7b8502017-06-12 15:32:41 -0700242 exception=remote_access.SSHConnectionError,
Achuith Bhandarkar2f8352f2017-06-02 12:47:18 -0700243 max_retry=10,
244 functor=lambda: self.RemoteCommand(cmd=['echo']),
245 sleep=5)
246 except remote_access.SSHConnectionError:
247 raise VMError('WaitForBoot timed out trying to connect to VM.')
248
249 if result.returncode != 0:
250 raise VMError('WaitForBoot failed: %s.' % result.error)
251
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +0100252 # Chrome can take a while to start with software emulation.
253 if not self.enable_kvm:
254 self._WaitForProcs()
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700255
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +0100256 def RemoteCommand(self, cmd, **kwargs):
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700257 """Run a remote command in the VM.
258
259 Args:
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +0100260 cmd: command to run.
261 kwargs: additional args (see documentation for RemoteDevice.RunCommand).
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700262 """
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700263 if not self.dry_run:
Achuith Bhandarkar65d1a892017-05-08 14:13:12 -0700264 return self.remote.RunCommand(cmd, debug_level=logging.INFO,
265 combine_stdout_stderr=True,
266 log_output=True,
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +0100267 error_code_ok=True,
268 **kwargs)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700269
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +0100270 @staticmethod
271 def _ParseArgs(argv):
272 """Parse a list of args.
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700273
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +0100274 Args:
275 argv: list of command line arguments.
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700276
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +0100277 Returns:
278 List of parsed opts.
279 """
280 parser = commandline.ArgumentParser(description=__doc__)
281 parser.add_argument('--start', action='store_true', default=False,
282 help='Start the VM.')
283 parser.add_argument('--stop', action='store_true', default=False,
284 help='Stop the VM.')
285 parser.add_argument('--image-path', type='path',
286 help='Path to VM image to launch with --start.')
287 parser.add_argument('--qemu-path', type='path',
288 help='Path of qemu binary to launch with --start.')
289 parser.add_argument('--disable-kvm', dest='enable_kvm',
290 action='store_false', default=True,
291 help='Disable KVM, use software emulation.')
292 parser.add_argument('--no-display', dest='display',
293 action='store_false', default=True,
294 help='Do not display video output.')
295 parser.add_argument('--ssh-port', type=int, default=VM.SSH_PORT,
296 help='ssh port to communicate with VM.')
297 parser.add_argument('--dry-run', action='store_true', default=False,
298 help='dry run for debugging.')
299 parser.add_argument('--cmd', action='store_true', default=False,
300 help='Run a command in the VM.')
301 parser.add_argument('args', nargs=argparse.REMAINDER,
302 help='Command to run in the VM.')
303 return parser.parse_args(argv)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700304
305def main(argv):
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +0100306 vm = VM(argv)
307 vm.Run()