blob: 60a9bcff08d2b74e1da0970dadef1597175ecd39 [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
10import os
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070011
12from chromite.lib import commandline
13from chromite.lib import cros_build_lib
14from chromite.lib import cros_logging as logging
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -070015from chromite.lib import osutils
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070016from chromite.lib import remote_access
Achuith Bhandarkar2f8352f2017-06-02 12:47:18 -070017from chromite.lib import retry_util
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070018
19
20class VMError(Exception):
21 """Exception for VM failures."""
22
23 def __init__(self, message):
24 super(VMError, self).__init__()
25 logging.error(message)
26
27
28class VM(object):
29 """Class for managing a VM."""
30
31 SSH_PORT = 9222
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070032
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -070033 def __init__(self, image_path=None, qemu_path=None, enable_kvm=True,
Achuith Bhandarkarb891adb2016-10-24 18:43:22 -070034 display=True, ssh_port=SSH_PORT, dry_run=False):
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070035 """Initialize VM.
36
37 Args:
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070038 image_path: path of vm image.
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -070039 qemu_path: path to qemu binary.
40 enable_kvm: enable kvm (kernel support for virtualization).
Achuith Bhandarkarb891adb2016-10-24 18:43:22 -070041 display: display video output.
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070042 ssh_port: ssh port to use.
43 dry_run: disable VM commands.
44 """
45
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -070046 self.qemu_path = qemu_path
47 self.enable_kvm = 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.
50 self.use_sudo = enable_kvm and not os.access('/dev/kvm', os.W_OK)
Achuith Bhandarkarb891adb2016-10-24 18:43:22 -070051 self.display = display
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070052 self.image_path = image_path
53 self.ssh_port = ssh_port
54 self.dry_run = dry_run
55
Achuith Bhandarkaree3163d2016-10-19 12:58:35 -070056 self.vm_dir = os.path.join(osutils.GetGlobalTempDir(), 'cros_vm')
57 if os.path.exists(self.vm_dir):
58 # For security, ensure that vm_dir is not a symlink, and is owned by us or
59 # by root.
60 assert not os.path.islink(self.vm_dir), \
61 'VM state dir is misconfigured; please recreate: %s' % self.vm_dir
62 st_uid = os.stat(self.vm_dir).st_uid
63 assert st_uid == 0 or st_uid == os.getuid(), \
64 'VM state dir is misconfigured; please recreate: %s' % self.vm_dir
65
66 self.pidfile = os.path.join(self.vm_dir, 'kvm.pid')
67 self.kvm_monitor = os.path.join(self.vm_dir, 'kvm.monitor')
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070068 self.kvm_pipe_in = '%s.in' % self.kvm_monitor # to KVM
69 self.kvm_pipe_out = '%s.out' % self.kvm_monitor # from KVM
70 self.kvm_serial = '%s.serial' % self.kvm_monitor
71
Achuith Bhandarkar65d1a892017-05-08 14:13:12 -070072 self.remote = remote_access.RemoteDevice(remote_access.LOCALHOST,
73 port=ssh_port)
74
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070075 # TODO(achuith): support nographics, snapshot, mem_path, usb_passthrough,
76 # moblab, etc.
77
Achuith Bhandarkaree3163d2016-10-19 12:58:35 -070078
79 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 Bhandarkard8d19292016-05-03 14:32:58 -070096 def PerformAction(self, start=False, stop=False, cmd=None):
97 """Performs an action, one of start, stop, or run a command in the VM.
98
99 Args:
100 start: start the VM.
101 stop: stop the VM.
102 cmd: list or scalar command to run in the VM.
103
104 Returns:
105 cmd output.
106 """
107
108 if not start and not stop and not cmd:
109 raise VMError('Must specify one of start, stop, or cmd.')
110 if start:
111 self.Start()
112 if stop:
113 self.Stop()
114 if cmd:
115 return self.RemoteCommand(cmd.split())
116
117 def Start(self):
118 """Start the VM."""
119
120 self.Stop()
121
Achuith Bhandarkar022d69c2016-10-05 14:28:14 -0700122 logging.debug('Start VM')
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700123 if not self.qemu_path:
124 self.qemu_path = osutils.Which('qemu-system-x86_64')
125 if not self.qemu_path:
126 raise VMError('qemu not found.')
127 logging.debug('qemu path=%s', self.qemu_path)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700128
129 if not self.image_path:
130 self.image_path = os.environ.get('VM_IMAGE_PATH', '')
131 logging.debug('vm image path=%s', self.image_path)
132 if not self.image_path or not os.path.exists(self.image_path):
133 raise VMError('VM image path %s does not exist.' % self.image_path)
134
135 self._CleanupFiles(recreate=True)
Mike Frysinger97080242017-09-13 01:58:45 -0400136 # Make sure we can read these files later on by creating them as ourselves.
137 osutils.Touch(self.kvm_serial)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700138 for pipe in [self.kvm_pipe_in, self.kvm_pipe_out]:
139 os.mkfifo(pipe, 0600)
Mike Frysinger97080242017-09-13 01:58:45 -0400140 osutils.Touch(self.pidfile)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700141
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700142 args = [self.qemu_path, '-m', '2G', '-smp', '4', '-vga', 'cirrus',
143 '-daemonize',
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700144 '-pidfile', self.pidfile,
145 '-chardev', 'pipe,id=control_pipe,path=%s' % self.kvm_monitor,
146 '-serial', 'file:%s' % self.kvm_serial,
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700147 '-mon', 'chardev=control_pipe',
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700148 '-net', 'nic,model=virtio',
Nicolas Norvez80329de2017-03-27 14:32:24 -0700149 '-net', 'user,hostfwd=tcp:127.0.0.1:%d-:22' % self.ssh_port,
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700150 '-drive', 'file=%s,index=0,media=disk,cache=unsafe'
151 % self.image_path]
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700152 if self.enable_kvm:
153 args.append('-enable-kvm')
Achuith Bhandarkarb891adb2016-10-24 18:43:22 -0700154 if not self.display:
155 args.extend(['-display', 'none'])
Mike Frysinger97080242017-09-13 01:58:45 -0400156 logging.info(cros_build_lib.CmdToStr(args))
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700157 logging.info('Pid file: %s', self.pidfile)
158 if not self.dry_run:
Achuith Bhandarkaree3163d2016-10-19 12:58:35 -0700159 self._RunCommand(args)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700160
161 def _GetVMPid(self):
162 """Get the pid of the VM.
163
164 Returns:
165 pid of the VM.
166 """
Achuith Bhandarkaree3163d2016-10-19 12:58:35 -0700167 if not os.path.exists(self.vm_dir):
168 logging.debug('%s not present.', self.vm_dir)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700169 return 0
170
171 if not os.path.exists(self.pidfile):
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700172 logging.info('%s does not exist.', self.pidfile)
173 return 0
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700174
Mike Frysinger97080242017-09-13 01:58:45 -0400175 pid = osutils.ReadFile(self.pidfile).rstrip()
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700176 if not pid.isdigit():
Mike Frysinger97080242017-09-13 01:58:45 -0400177 # Ignore blank/empty files.
178 if pid:
179 logging.error('%s in %s is not a pid.', pid, self.pidfile)
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700180 return 0
181
Achuith Bhandarkar022d69c2016-10-05 14:28:14 -0700182 return int(pid)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700183
184 def IsRunning(self):
185 """Returns True if there's a running VM.
186
187 Returns:
188 True if there's a running VM.
189 """
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700190 pid = self._GetVMPid()
Achuith Bhandarkar022d69c2016-10-05 14:28:14 -0700191 if not pid:
192 return False
193
194 # Make sure the process actually exists.
Mike Frysinger97080242017-09-13 01:58:45 -0400195 return os.path.isdir('/proc/%i' % pid)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700196
197 def Stop(self):
198 """Stop the VM."""
Achuith Bhandarkar022d69c2016-10-05 14:28:14 -0700199 logging.debug('Stop VM')
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700200
201 pid = self._GetVMPid()
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700202 if pid:
203 logging.info('Killing %d.', pid)
204 if not self.dry_run:
Achuith Bhandarkaree3163d2016-10-19 12:58:35 -0700205 self._RunCommand(['kill', '-9', str(pid)], error_code_ok=True)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700206
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700207 self._CleanupFiles(recreate=False)
208
Achuith Bhandarkar2f8352f2017-06-02 12:47:18 -0700209 def _WaitForProcs(self):
210 """Wait for expected processes to launch."""
211 class _TooFewPidsException(Exception):
212 """Exception for _GetRunningPids to throw."""
213
214 def _GetRunningPids(exe, numpids):
215 pids = self.remote.GetRunningPids(exe, full_path=False)
216 logging.info('%s pids: %s', exe, repr(pids))
217 if len(pids) < numpids:
218 raise _TooFewPidsException()
219
220 def _WaitForProc(exe, numpids):
221 try:
222 retry_util.RetryException(
Achuith Bhandarkar0e7b8502017-06-12 15:32:41 -0700223 exception=_TooFewPidsException,
Achuith Bhandarkar2f8352f2017-06-02 12:47:18 -0700224 max_retry=20,
225 functor=lambda: _GetRunningPids(exe, numpids),
226 sleep=2)
227 except _TooFewPidsException:
228 raise VMError('_WaitForProcs failed: timed out while waiting for '
229 '%d %s processes to start.' % (numpids, exe))
230
231 # We could also wait for session_manager, nacl_helper, etc, but chrome is
232 # the long pole. We expect the parent, 2 zygotes, gpu-process, renderer.
233 # This could potentially break with Mustash.
234 _WaitForProc('chrome', 5)
235
236 def WaitForBoot(self):
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700237 """Wait for the VM to boot up.
238
Achuith Bhandarkar2f8352f2017-06-02 12:47:18 -0700239 Wait for ssh connection to become active, and wait for all expected chrome
240 processes to be launched.
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700241 """
Achuith Bhandarkaree3163d2016-10-19 12:58:35 -0700242 if not os.path.exists(self.vm_dir):
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700243 self.Start()
244
Achuith Bhandarkar2f8352f2017-06-02 12:47:18 -0700245 try:
246 result = retry_util.RetryException(
Achuith Bhandarkar0e7b8502017-06-12 15:32:41 -0700247 exception=remote_access.SSHConnectionError,
Achuith Bhandarkar2f8352f2017-06-02 12:47:18 -0700248 max_retry=10,
249 functor=lambda: self.RemoteCommand(cmd=['echo']),
250 sleep=5)
251 except remote_access.SSHConnectionError:
252 raise VMError('WaitForBoot timed out trying to connect to VM.')
253
254 if result.returncode != 0:
255 raise VMError('WaitForBoot failed: %s.' % result.error)
256
257 self._WaitForProcs()
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700258
259 def RemoteCommand(self, cmd):
260 """Run a remote command in the VM.
261
262 Args:
263 cmd: command to run, of list type.
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700264 """
265 if not isinstance(cmd, list):
266 raise VMError('cmd must be a list.')
267
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700268 if not self.dry_run:
Achuith Bhandarkar65d1a892017-05-08 14:13:12 -0700269 return self.remote.RunCommand(cmd, debug_level=logging.INFO,
270 combine_stdout_stderr=True,
271 log_output=True,
272 error_code_ok=True)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700273
274def ParseCommandLine(argv):
275 """Parse the command line.
276
277 Args:
278 argv: Command arguments.
279
280 Returns:
281 List of parsed args.
282 """
283 parser = commandline.ArgumentParser(description=__doc__)
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700284 parser.add_argument('--start', action='store_true', default=False,
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700285 help='Start the VM.')
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700286 parser.add_argument('--stop', action='store_true', default=False,
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700287 help='Stop the VM.')
288 parser.add_argument('--cmd', help='Run this command in the VM.')
289 parser.add_argument('--image-path', type='path',
290 help='Path to VM image to launch with --start.')
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700291 parser.add_argument('--qemu-path', type='path',
292 help='Path of qemu binary to launch with --start.')
Achuith Bhandarkarb891adb2016-10-24 18:43:22 -0700293 parser.add_argument('--disable-kvm', dest='enable_kvm',
294 action='store_false', default=True,
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700295 help='Disable KVM, use software emulation.')
Achuith Bhandarkarb891adb2016-10-24 18:43:22 -0700296 parser.add_argument('--no-display', dest='display',
297 action='store_false', default=True,
298 help='Do not display video output.')
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700299 parser.add_argument('--ssh-port', type=int, default=VM.SSH_PORT,
300 help='ssh port to communicate with VM.')
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700301 parser.add_argument('--dry-run', action='store_true', default=False,
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700302 help='dry run for debugging.')
303 return parser.parse_args(argv)
304
305
306def main(argv):
307 args = ParseCommandLine(argv)
Achuith Bhandarkarb891adb2016-10-24 18:43:22 -0700308 vm = VM(image_path=args.image_path, qemu_path=args.qemu_path,
309 enable_kvm=args.enable_kvm, display=args.display,
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700310 ssh_port=args.ssh_port, dry_run=args.dry_run)
311 vm.PerformAction(start=args.start, stop=args.stop, cmd=args.cmd)