blob: bcf6d432c343dd7b95451f6d5c2a18d353b098f1 [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
10import time
11
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
17
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
32
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 Bhandarkaree3163d2016-10-19 12:58:35 -070048 # Software emulation doesn't need sudo access.
49 self.use_sudo = enable_kvm
Achuith Bhandarkarb891adb2016-10-24 18:43:22 -070050 self.display = display
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070051 self.image_path = image_path
52 self.ssh_port = ssh_port
53 self.dry_run = dry_run
54
Achuith Bhandarkaree3163d2016-10-19 12:58:35 -070055 self.vm_dir = os.path.join(osutils.GetGlobalTempDir(), 'cros_vm')
56 if os.path.exists(self.vm_dir):
57 # For security, ensure that vm_dir is not a symlink, and is owned by us or
58 # by root.
59 assert not os.path.islink(self.vm_dir), \
60 'VM state dir is misconfigured; please recreate: %s' % self.vm_dir
61 st_uid = os.stat(self.vm_dir).st_uid
62 assert st_uid == 0 or st_uid == os.getuid(), \
63 'VM state dir is misconfigured; please recreate: %s' % self.vm_dir
64
65 self.pidfile = os.path.join(self.vm_dir, 'kvm.pid')
66 self.kvm_monitor = os.path.join(self.vm_dir, 'kvm.monitor')
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070067 self.kvm_pipe_in = '%s.in' % self.kvm_monitor # to KVM
68 self.kvm_pipe_out = '%s.out' % self.kvm_monitor # from KVM
69 self.kvm_serial = '%s.serial' % self.kvm_monitor
70
71 # TODO(achuith): support nographics, snapshot, mem_path, usb_passthrough,
72 # moblab, etc.
73
Achuith Bhandarkaree3163d2016-10-19 12:58:35 -070074
75 def _RunCommand(self, *args, **kwargs):
76 """Use SudoRunCommand or RunCommand as necessary."""
77 if self.use_sudo:
78 return cros_build_lib.SudoRunCommand(*args, **kwargs)
79 else:
80 return cros_build_lib.RunCommand(*args, **kwargs)
81
82 def _CleanupFiles(self, recreate):
83 """Cleanup vm_dir.
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070084
85 Args:
Achuith Bhandarkaree3163d2016-10-19 12:58:35 -070086 recreate: recreate vm_dir.
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070087 """
Achuith Bhandarkaree3163d2016-10-19 12:58:35 -070088 self._RunCommand(['rm', '-rf', self.vm_dir])
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070089 if recreate:
Achuith Bhandarkaree3163d2016-10-19 12:58:35 -070090 self._RunCommand(['mkdir', self.vm_dir])
91 self._RunCommand(['chmod', '777', self.vm_dir])
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070092
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070093 def PerformAction(self, start=False, stop=False, cmd=None):
94 """Performs an action, one of start, stop, or run a command in the VM.
95
96 Args:
97 start: start the VM.
98 stop: stop the VM.
99 cmd: list or scalar command to run in the VM.
100
101 Returns:
102 cmd output.
103 """
104
105 if not start and not stop and not cmd:
106 raise VMError('Must specify one of start, stop, or cmd.')
107 if start:
108 self.Start()
109 if stop:
110 self.Stop()
111 if cmd:
112 return self.RemoteCommand(cmd.split())
113
114 def Start(self):
115 """Start the VM."""
116
117 self.Stop()
118
Achuith Bhandarkar022d69c2016-10-05 14:28:14 -0700119 logging.debug('Start VM')
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700120 if not self.qemu_path:
121 self.qemu_path = osutils.Which('qemu-system-x86_64')
122 if not self.qemu_path:
123 raise VMError('qemu not found.')
124 logging.debug('qemu path=%s', self.qemu_path)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700125
126 if not self.image_path:
127 self.image_path = os.environ.get('VM_IMAGE_PATH', '')
128 logging.debug('vm image path=%s', self.image_path)
129 if not self.image_path or not os.path.exists(self.image_path):
130 raise VMError('VM image path %s does not exist.' % self.image_path)
131
132 self._CleanupFiles(recreate=True)
133 open(self.kvm_serial, 'w')
134 for pipe in [self.kvm_pipe_in, self.kvm_pipe_out]:
135 os.mkfifo(pipe, 0600)
136
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',
144 '-net', 'user,hostfwd=tcp::%d-:22' % self.ssh_port,
145 '-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'])
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700151 logging.info(' '.join(args))
152 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
Achuith Bhandarkaree3163d2016-10-19 12:58:35 -0700170 pid = self._RunCommand(['cat', self.pidfile],
171 redirect_stdout=True).output.rstrip()
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700172 if not pid.isdigit():
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700173 logging.error('%s in %s is not a pid.', pid, self.pidfile)
174 return 0
175
Achuith Bhandarkar022d69c2016-10-05 14:28:14 -0700176 return int(pid)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700177
178 def IsRunning(self):
179 """Returns True if there's a running VM.
180
181 Returns:
182 True if there's a running VM.
183 """
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700184 pid = self._GetVMPid()
Achuith Bhandarkar022d69c2016-10-05 14:28:14 -0700185 if not pid:
186 return False
187
188 # Make sure the process actually exists.
Achuith Bhandarkaree3163d2016-10-19 12:58:35 -0700189 res = self._RunCommand(['kill', '-0', str(pid)], error_code_ok=True)
Achuith Bhandarkar022d69c2016-10-05 14:28:14 -0700190 return res.returncode == 0
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 Bhandarkarb891adb2016-10-24 18:43:22 -0700204 def WaitForBoot(self, timeout=180, poll_interval=1):
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700205 """Wait for the VM to boot up.
206
207 If there is no VM running, start one.
208
209 Args:
210 timeout: maxiumum time to wait before raising an exception.
211 poll_interval: interval between checks.
212 """
Achuith Bhandarkaree3163d2016-10-19 12:58:35 -0700213 if not os.path.exists(self.vm_dir):
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700214 self.Start()
215
216 start_time = time.time()
Achuith Bhandarkarb891adb2016-10-24 18:43:22 -0700217 error = 'timed out after %d sec' % timeout
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700218 while time.time() - start_time < timeout:
219 result = self.RemoteCommand(cmd=['echo'])
220 if result.returncode == 255:
221 time.sleep(poll_interval)
222 continue
223 elif result.returncode == 0:
224 return
225 else:
Achuith Bhandarkarb891adb2016-10-24 18:43:22 -0700226 error = self.error
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700227 break
Achuith Bhandarkarb891adb2016-10-24 18:43:22 -0700228 raise VMError('WaitForBoot failed: %s.' % error)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700229
230 def RemoteCommand(self, cmd):
231 """Run a remote command in the VM.
232
233 Args:
234 cmd: command to run, of list type.
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700235 """
236 if not isinstance(cmd, list):
237 raise VMError('cmd must be a list.')
238
239 args = ['ssh', '-o', 'UserKnownHostsFile=/dev/null',
240 '-o', 'StrictHostKeyChecking=no',
241 '-i', remote_access.TEST_PRIVATE_KEY,
242 '-p', str(self.ssh_port), 'root@localhost']
243 args.extend(cmd)
244
245 if not self.dry_run:
246 return cros_build_lib.RunCommand(args, redirect_stdout=True,
247 combine_stdout_stderr=True,
248 log_output=True,
249 error_code_ok=True)
250
251
252def ParseCommandLine(argv):
253 """Parse the command line.
254
255 Args:
256 argv: Command arguments.
257
258 Returns:
259 List of parsed args.
260 """
261 parser = commandline.ArgumentParser(description=__doc__)
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700262 parser.add_argument('--start', action='store_true', default=False,
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700263 help='Start the VM.')
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700264 parser.add_argument('--stop', action='store_true', default=False,
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700265 help='Stop the VM.')
266 parser.add_argument('--cmd', help='Run this command in the VM.')
267 parser.add_argument('--image-path', type='path',
268 help='Path to VM image to launch with --start.')
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700269 parser.add_argument('--qemu-path', type='path',
270 help='Path of qemu binary to launch with --start.')
Achuith Bhandarkarb891adb2016-10-24 18:43:22 -0700271 parser.add_argument('--disable-kvm', dest='enable_kvm',
272 action='store_false', default=True,
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700273 help='Disable KVM, use software emulation.')
Achuith Bhandarkarb891adb2016-10-24 18:43:22 -0700274 parser.add_argument('--no-display', dest='display',
275 action='store_false', default=True,
276 help='Do not display video output.')
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700277 parser.add_argument('--ssh-port', type=int, default=VM.SSH_PORT,
278 help='ssh port to communicate with VM.')
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700279 parser.add_argument('--dry-run', action='store_true', default=False,
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700280 help='dry run for debugging.')
281 return parser.parse_args(argv)
282
283
284def main(argv):
285 args = ParseCommandLine(argv)
Achuith Bhandarkarb891adb2016-10-24 18:43:22 -0700286 vm = VM(image_path=args.image_path, qemu_path=args.qemu_path,
287 enable_kvm=args.enable_kvm, display=args.display,
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700288 ssh_port=args.ssh_port, dry_run=args.dry_run)
289 vm.PerformAction(start=args.start, stop=args.stop, cmd=args.cmd)