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