blob: 503f46d6073bde465848694a3ca39b6008a3e32f [file] [log] [blame]
Achuith Bhandarkard8d19292016-05-03 14:32:58 -07001# Copyright 2016 The Chromium OS 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
Mike Frysinger666566c2016-09-21 00:00:21 -04005"""Script for VM Management."""
Achuith Bhandarkard8d19292016-05-03 14:32:58 -07006
7from __future__ import print_function
8
9import os
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070010
11from chromite.lib import commandline
12from chromite.lib import cros_build_lib
13from chromite.lib import cros_logging as logging
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -070014from chromite.lib import osutils
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070015from chromite.lib import remote_access
Achuith Bhandarkar2f8352f2017-06-02 12:47:18 -070016from chromite.lib import retry_util
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070017
18
19class VMError(Exception):
20 """Exception for VM failures."""
21
22 def __init__(self, message):
23 super(VMError, self).__init__()
24 logging.error(message)
25
26
27class VM(object):
28 """Class for managing a VM."""
29
30 SSH_PORT = 9222
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070031
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -070032 def __init__(self, image_path=None, qemu_path=None, enable_kvm=True,
Achuith Bhandarkarb891adb2016-10-24 18:43:22 -070033 display=True, ssh_port=SSH_PORT, dry_run=False):
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070034 """Initialize VM.
35
36 Args:
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070037 image_path: path of vm image.
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -070038 qemu_path: path to qemu binary.
39 enable_kvm: enable kvm (kernel support for virtualization).
Achuith Bhandarkarb891adb2016-10-24 18:43:22 -070040 display: display video output.
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070041 ssh_port: ssh port to use.
42 dry_run: disable VM commands.
43 """
44
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -070045 self.qemu_path = qemu_path
46 self.enable_kvm = enable_kvm
Achuith Bhandarkaree3163d2016-10-19 12:58:35 -070047 # Software emulation doesn't need sudo access.
48 self.use_sudo = enable_kvm
Achuith Bhandarkarb891adb2016-10-24 18:43:22 -070049 self.display = display
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070050 self.image_path = image_path
51 self.ssh_port = ssh_port
52 self.dry_run = dry_run
53
Achuith Bhandarkaree3163d2016-10-19 12:58:35 -070054 self.vm_dir = os.path.join(osutils.GetGlobalTempDir(), 'cros_vm')
55 if os.path.exists(self.vm_dir):
56 # For security, ensure that vm_dir is not a symlink, and is owned by us or
57 # by root.
58 assert not os.path.islink(self.vm_dir), \
59 'VM state dir is misconfigured; please recreate: %s' % self.vm_dir
60 st_uid = os.stat(self.vm_dir).st_uid
61 assert st_uid == 0 or st_uid == os.getuid(), \
62 'VM state dir is misconfigured; please recreate: %s' % self.vm_dir
63
64 self.pidfile = os.path.join(self.vm_dir, 'kvm.pid')
65 self.kvm_monitor = os.path.join(self.vm_dir, 'kvm.monitor')
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070066 self.kvm_pipe_in = '%s.in' % self.kvm_monitor # to KVM
67 self.kvm_pipe_out = '%s.out' % self.kvm_monitor # from KVM
68 self.kvm_serial = '%s.serial' % self.kvm_monitor
69
Achuith Bhandarkar65d1a892017-05-08 14:13:12 -070070 self.remote = remote_access.RemoteDevice(remote_access.LOCALHOST,
71 port=ssh_port)
72
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070073 # TODO(achuith): support nographics, snapshot, mem_path, usb_passthrough,
74 # moblab, etc.
75
Achuith Bhandarkaree3163d2016-10-19 12:58:35 -070076
77 def _RunCommand(self, *args, **kwargs):
78 """Use SudoRunCommand or RunCommand as necessary."""
79 if self.use_sudo:
80 return cros_build_lib.SudoRunCommand(*args, **kwargs)
81 else:
82 return cros_build_lib.RunCommand(*args, **kwargs)
83
84 def _CleanupFiles(self, recreate):
85 """Cleanup vm_dir.
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070086
87 Args:
Achuith Bhandarkaree3163d2016-10-19 12:58:35 -070088 recreate: recreate vm_dir.
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070089 """
Achuith Bhandarkaree3163d2016-10-19 12:58:35 -070090 self._RunCommand(['rm', '-rf', self.vm_dir])
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070091 if recreate:
Achuith Bhandarkaree3163d2016-10-19 12:58:35 -070092 self._RunCommand(['mkdir', self.vm_dir])
93 self._RunCommand(['chmod', '777', self.vm_dir])
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070094
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070095 def PerformAction(self, start=False, stop=False, cmd=None):
96 """Performs an action, one of start, stop, or run a command in the VM.
97
98 Args:
99 start: start the VM.
100 stop: stop the VM.
101 cmd: list or scalar command to run in the VM.
102
103 Returns:
104 cmd output.
105 """
106
107 if not start and not stop and not cmd:
108 raise VMError('Must specify one of start, stop, or cmd.')
109 if start:
110 self.Start()
111 if stop:
112 self.Stop()
113 if cmd:
114 return self.RemoteCommand(cmd.split())
115
116 def Start(self):
117 """Start the VM."""
118
119 self.Stop()
120
Achuith Bhandarkar022d69c2016-10-05 14:28:14 -0700121 logging.debug('Start VM')
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700122 if not self.qemu_path:
123 self.qemu_path = osutils.Which('qemu-system-x86_64')
124 if not self.qemu_path:
125 raise VMError('qemu not found.')
126 logging.debug('qemu path=%s', self.qemu_path)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700127
128 if not self.image_path:
129 self.image_path = os.environ.get('VM_IMAGE_PATH', '')
130 logging.debug('vm image path=%s', self.image_path)
131 if not self.image_path or not os.path.exists(self.image_path):
132 raise VMError('VM image path %s does not exist.' % self.image_path)
133
134 self._CleanupFiles(recreate=True)
135 open(self.kvm_serial, 'w')
136 for pipe in [self.kvm_pipe_in, self.kvm_pipe_out]:
137 os.mkfifo(pipe, 0600)
138
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700139 args = [self.qemu_path, '-m', '2G', '-smp', '4', '-vga', 'cirrus',
140 '-daemonize',
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700141 '-pidfile', self.pidfile,
142 '-chardev', 'pipe,id=control_pipe,path=%s' % self.kvm_monitor,
143 '-serial', 'file:%s' % self.kvm_serial,
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700144 '-mon', 'chardev=control_pipe',
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700145 '-net', 'nic,model=virtio',
146 '-net', 'user,hostfwd=tcp::%d-:22' % self.ssh_port,
147 '-drive', 'file=%s,index=0,media=disk,cache=unsafe'
148 % self.image_path]
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700149 if self.enable_kvm:
150 args.append('-enable-kvm')
Achuith Bhandarkarb891adb2016-10-24 18:43:22 -0700151 if not self.display:
152 args.extend(['-display', 'none'])
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700153 logging.info(' '.join(args))
154 logging.info('Pid file: %s', self.pidfile)
155 if not self.dry_run:
Achuith Bhandarkaree3163d2016-10-19 12:58:35 -0700156 self._RunCommand(args)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700157
158 def _GetVMPid(self):
159 """Get the pid of the VM.
160
161 Returns:
162 pid of the VM.
163 """
Achuith Bhandarkaree3163d2016-10-19 12:58:35 -0700164 if not os.path.exists(self.vm_dir):
165 logging.debug('%s not present.', self.vm_dir)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700166 return 0
167
168 if not os.path.exists(self.pidfile):
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700169 logging.info('%s does not exist.', self.pidfile)
170 return 0
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700171
Achuith Bhandarkaree3163d2016-10-19 12:58:35 -0700172 pid = self._RunCommand(['cat', self.pidfile],
173 redirect_stdout=True).output.rstrip()
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700174 if not pid.isdigit():
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700175 logging.error('%s in %s is not a pid.', pid, self.pidfile)
176 return 0
177
Achuith Bhandarkar022d69c2016-10-05 14:28:14 -0700178 return int(pid)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700179
180 def IsRunning(self):
181 """Returns True if there's a running VM.
182
183 Returns:
184 True if there's a running VM.
185 """
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700186 pid = self._GetVMPid()
Achuith Bhandarkar022d69c2016-10-05 14:28:14 -0700187 if not pid:
188 return False
189
190 # Make sure the process actually exists.
Achuith Bhandarkaree3163d2016-10-19 12:58:35 -0700191 res = self._RunCommand(['kill', '-0', str(pid)], error_code_ok=True)
Achuith Bhandarkar022d69c2016-10-05 14:28:14 -0700192 return res.returncode == 0
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700193
194 def Stop(self):
195 """Stop the VM."""
Achuith Bhandarkar022d69c2016-10-05 14:28:14 -0700196 logging.debug('Stop VM')
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700197
198 pid = self._GetVMPid()
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700199 if pid:
200 logging.info('Killing %d.', pid)
201 if not self.dry_run:
Achuith Bhandarkaree3163d2016-10-19 12:58:35 -0700202 self._RunCommand(['kill', '-9', str(pid)], error_code_ok=True)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700203
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700204 self._CleanupFiles(recreate=False)
205
Achuith Bhandarkar2f8352f2017-06-02 12:47:18 -0700206 def _WaitForProcs(self):
207 """Wait for expected processes to launch."""
208 class _TooFewPidsException(Exception):
209 """Exception for _GetRunningPids to throw."""
210
211 def _GetRunningPids(exe, numpids):
212 pids = self.remote.GetRunningPids(exe, full_path=False)
213 logging.info('%s pids: %s', exe, repr(pids))
214 if len(pids) < numpids:
215 raise _TooFewPidsException()
216
217 def _WaitForProc(exe, numpids):
218 try:
219 retry_util.RetryException(
Achuith Bhandarkar0e7b8502017-06-12 15:32:41 -0700220 exception=_TooFewPidsException,
Achuith Bhandarkar2f8352f2017-06-02 12:47:18 -0700221 max_retry=20,
222 functor=lambda: _GetRunningPids(exe, numpids),
223 sleep=2)
224 except _TooFewPidsException:
225 raise VMError('_WaitForProcs failed: timed out while waiting for '
226 '%d %s processes to start.' % (numpids, exe))
227
228 # We could also wait for session_manager, nacl_helper, etc, but chrome is
229 # the long pole. We expect the parent, 2 zygotes, gpu-process, renderer.
230 # This could potentially break with Mustash.
231 _WaitForProc('chrome', 5)
232
233 def WaitForBoot(self):
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700234 """Wait for the VM to boot up.
235
Achuith Bhandarkar2f8352f2017-06-02 12:47:18 -0700236 Wait for ssh connection to become active, and wait for all expected chrome
237 processes to be launched.
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700238 """
Achuith Bhandarkaree3163d2016-10-19 12:58:35 -0700239 if not os.path.exists(self.vm_dir):
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700240 self.Start()
241
Achuith Bhandarkar2f8352f2017-06-02 12:47:18 -0700242 try:
243 result = retry_util.RetryException(
Achuith Bhandarkar0e7b8502017-06-12 15:32:41 -0700244 exception=remote_access.SSHConnectionError,
Achuith Bhandarkar2f8352f2017-06-02 12:47:18 -0700245 max_retry=10,
246 functor=lambda: self.RemoteCommand(cmd=['echo']),
247 sleep=5)
248 except remote_access.SSHConnectionError:
249 raise VMError('WaitForBoot timed out trying to connect to VM.')
250
251 if result.returncode != 0:
252 raise VMError('WaitForBoot failed: %s.' % result.error)
253
254 self._WaitForProcs()
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700255
256 def RemoteCommand(self, cmd):
257 """Run a remote command in the VM.
258
259 Args:
260 cmd: command to run, of list type.
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700261 """
262 if not isinstance(cmd, list):
263 raise VMError('cmd must be a list.')
264
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700265 if not self.dry_run:
Achuith Bhandarkar65d1a892017-05-08 14:13:12 -0700266 return self.remote.RunCommand(cmd, debug_level=logging.INFO,
267 combine_stdout_stderr=True,
268 log_output=True,
269 error_code_ok=True)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700270
271def ParseCommandLine(argv):
272 """Parse the command line.
273
274 Args:
275 argv: Command arguments.
276
277 Returns:
278 List of parsed args.
279 """
280 parser = commandline.ArgumentParser(description=__doc__)
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700281 parser.add_argument('--start', action='store_true', default=False,
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700282 help='Start the VM.')
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700283 parser.add_argument('--stop', action='store_true', default=False,
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700284 help='Stop the VM.')
285 parser.add_argument('--cmd', help='Run this command in the VM.')
286 parser.add_argument('--image-path', type='path',
287 help='Path to VM image to launch with --start.')
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700288 parser.add_argument('--qemu-path', type='path',
289 help='Path of qemu binary to launch with --start.')
Achuith Bhandarkarb891adb2016-10-24 18:43:22 -0700290 parser.add_argument('--disable-kvm', dest='enable_kvm',
291 action='store_false', default=True,
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700292 help='Disable KVM, use software emulation.')
Achuith Bhandarkarb891adb2016-10-24 18:43:22 -0700293 parser.add_argument('--no-display', dest='display',
294 action='store_false', default=True,
295 help='Do not display video output.')
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700296 parser.add_argument('--ssh-port', type=int, default=VM.SSH_PORT,
297 help='ssh port to communicate with VM.')
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700298 parser.add_argument('--dry-run', action='store_true', default=False,
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700299 help='dry run for debugging.')
300 return parser.parse_args(argv)
301
302
303def main(argv):
304 args = ParseCommandLine(argv)
Achuith Bhandarkarb891adb2016-10-24 18:43:22 -0700305 vm = VM(image_path=args.image_path, qemu_path=args.qemu_path,
306 enable_kvm=args.enable_kvm, display=args.display,
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700307 ssh_port=args.ssh_port, dry_run=args.dry_run)
308 vm.PerformAction(start=args.start, stop=args.stop, cmd=args.cmd)