blob: b32486b05deed97b8c59a7b442ee01a1be37dccd [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
15from chromite.lib import remote_access
16
17
18class VMError(Exception):
19 """Exception for VM failures."""
20
21 def __init__(self, message):
22 super(VMError, self).__init__()
23 logging.error(message)
24
25
26class VM(object):
27 """Class for managing a VM."""
28
29 SSH_PORT = 9222
30 VM_DIR = '/var/run/cros_vm'
31
32
33 def __init__(self, kvm_path=None, image_path=None, ssh_port=SSH_PORT,
34 dry_run=False):
35 """Initialize VM.
36
37 Args:
38 kvm_path: path to kvm binary.
39 image_path: path of vm image.
40 ssh_port: ssh port to use.
41 dry_run: disable VM commands.
42 """
43
44 self.kvm_path = kvm_path
45 self.image_path = image_path
46 self.ssh_port = ssh_port
47 self.dry_run = dry_run
48
49 self.pidfile = os.path.join(self.VM_DIR, 'kvm.pid')
50 self.kvm_monitor = os.path.join(self.VM_DIR, 'kvm.monitor')
51 self.kvm_pipe_in = '%s.in' % self.kvm_monitor # to KVM
52 self.kvm_pipe_out = '%s.out' % self.kvm_monitor # from KVM
53 self.kvm_serial = '%s.serial' % self.kvm_monitor
54
55 # TODO(achuith): support nographics, snapshot, mem_path, usb_passthrough,
56 # moblab, etc.
57
58 @staticmethod
59 def _CleanupFiles(recreate):
60 """Cleanup VM_DIR.
61
62 Args:
63 recreate: recreate VM_DIR.
64 """
65 cros_build_lib.SudoRunCommand(['rm', '-rf', VM.VM_DIR])
66 if recreate:
67 cros_build_lib.SudoRunCommand(['mkdir', VM.VM_DIR])
68 cros_build_lib.SudoRunCommand(['chmod', '777', VM.VM_DIR])
69
70 @staticmethod
71 def _FindKVMBinary():
72 """Returns path to KVM binary.
73
74 Returns:
75 KVM binary path.
76 """
77
78 for exe in ['kvm', 'qemu-kvm', 'qemu-system-x86_64']:
79 try:
80 return cros_build_lib.RunCommand(['which', exe],
81 redirect_stdout=True).output.rstrip()
82 except cros_build_lib.RunCommandError:
83 raise VMError('KVM path not found.')
84
85 def PerformAction(self, start=False, stop=False, cmd=None):
86 """Performs an action, one of start, stop, or run a command in the VM.
87
88 Args:
89 start: start the VM.
90 stop: stop the VM.
91 cmd: list or scalar command to run in the VM.
92
93 Returns:
94 cmd output.
95 """
96
97 if not start and not stop and not cmd:
98 raise VMError('Must specify one of start, stop, or cmd.')
99 if start:
100 self.Start()
101 if stop:
102 self.Stop()
103 if cmd:
104 return self.RemoteCommand(cmd.split())
105
106 def Start(self):
107 """Start the VM."""
108
109 self.Stop()
110
111 if not self.kvm_path:
112 self.kvm_path = self._FindKVMBinary()
113 logging.debug('kvm path=%s', self.kvm_path)
114
115 if not self.image_path:
116 self.image_path = os.environ.get('VM_IMAGE_PATH', '')
117 logging.debug('vm image path=%s', self.image_path)
118 if not self.image_path or not os.path.exists(self.image_path):
119 raise VMError('VM image path %s does not exist.' % self.image_path)
120
121 self._CleanupFiles(recreate=True)
122 open(self.kvm_serial, 'w')
123 for pipe in [self.kvm_pipe_in, self.kvm_pipe_out]:
124 os.mkfifo(pipe, 0600)
125
126 args = [self.kvm_path, '-m', '2G', '-smp', '4', '-vga', 'cirrus',
127 '-pidfile', self.pidfile,
128 '-chardev', 'pipe,id=control_pipe,path=%s' % self.kvm_monitor,
129 '-serial', 'file:%s' % self.kvm_serial,
130 '-mon', 'chardev=control_pipe', '-daemonize',
131 '-net', 'nic,model=virtio',
132 '-net', 'user,hostfwd=tcp::%d-:22' % self.ssh_port,
133 '-drive', 'file=%s,index=0,media=disk,cache=unsafe'
134 % self.image_path]
135 logging.info(' '.join(args))
136 logging.info('Pid file: %s', self.pidfile)
137 if not self.dry_run:
138 cros_build_lib.SudoRunCommand(args)
139
140 def _GetVMPid(self):
141 """Get the pid of the VM.
142
143 Returns:
144 pid of the VM.
145 """
146 if not os.path.exists(self.VM_DIR):
147 logging.info('No VM running.')
148 return 0
149
150 if not os.path.exists(self.pidfile):
151 raise VMError('%s does not exist.' % self.pidfile)
152
153 pid = cros_build_lib.SudoRunCommand(['cat', self.pidfile],
154 redirect_stdout=True).output.rstrip()
155 if not pid.isdigit():
156 raise VMError('%s in %s is not a pid.' % (pid, self.pidfile))
157 return pid
158
159 def IsRunning(self):
160 """Returns True if there's a running VM.
161
162 Returns:
163 True if there's a running VM.
164 """
165 try:
166 pid = self._GetVMPid()
167 except VMError:
168 return False
169 return bool(pid)
170
171 def Stop(self):
172 """Stop the VM."""
173
174 pid = self._GetVMPid()
175 if not pid:
176 return
177
178 logging.info('Killing %s.', pid)
179 if not self.dry_run:
180 cros_build_lib.SudoRunCommand(['kill', '-9', pid])
181 self._CleanupFiles(recreate=False)
182
183 def WaitForBoot(self, timeout=10, poll_interval=0.1):
184 """Wait for the VM to boot up.
185
186 If there is no VM running, start one.
187
188 Args:
189 timeout: maxiumum time to wait before raising an exception.
190 poll_interval: interval between checks.
191 """
192 if not os.path.exists(self.VM_DIR):
193 self.Start()
194
195 start_time = time.time()
196 while time.time() - start_time < timeout:
197 result = self.RemoteCommand(cmd=['echo'])
198 if result.returncode == 255:
199 time.sleep(poll_interval)
200 continue
201 elif result.returncode == 0:
202 return
203 else:
204 break
205 raise VMError('WaitForBoot failed')
206
207 def RemoteCommand(self, cmd):
208 """Run a remote command in the VM.
209
210 Args:
211 cmd: command to run, of list type.
212 verbose: verbose logging output.
213 """
214 if not isinstance(cmd, list):
215 raise VMError('cmd must be a list.')
216
217 args = ['ssh', '-o', 'UserKnownHostsFile=/dev/null',
218 '-o', 'StrictHostKeyChecking=no',
219 '-i', remote_access.TEST_PRIVATE_KEY,
220 '-p', str(self.ssh_port), 'root@localhost']
221 args.extend(cmd)
222
223 if not self.dry_run:
224 return cros_build_lib.RunCommand(args, redirect_stdout=True,
225 combine_stdout_stderr=True,
226 log_output=True,
227 error_code_ok=True)
228
229
230def ParseCommandLine(argv):
231 """Parse the command line.
232
233 Args:
234 argv: Command arguments.
235
236 Returns:
237 List of parsed args.
238 """
239 parser = commandline.ArgumentParser(description=__doc__)
240 parser.add_argument('--start', action='store_true',
241 help='Start the VM.')
242 parser.add_argument('--stop', action='store_true',
243 help='Stop the VM.')
244 parser.add_argument('--cmd', help='Run this command in the VM.')
245 parser.add_argument('--image-path', type='path',
246 help='Path to VM image to launch with --start.')
247 parser.add_argument('--kvm-path', type='path',
248 help='Path of kvm binary to launch with --start.')
249 parser.add_argument('--ssh-port', type=int, default=VM.SSH_PORT,
250 help='ssh port to communicate with VM.')
251 parser.add_argument('--dry-run', action='store_true',
252 help='dry run for debugging.')
253 return parser.parse_args(argv)
254
255
256def main(argv):
257 args = ParseCommandLine(argv)
258 vm = VM(kvm_path=args.kvm_path, image_path=args.image_path,
259 ssh_port=args.ssh_port, dry_run=args.dry_run)
260 vm.PerformAction(start=args.start, stop=args.stop, cmd=args.cmd)