blob: 2f1bc4a1622e6d42c1ff4c2e6c4d168341dc0634 [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
Achuith Bhandarkar022d69c2016-10-05 14:28:14 -0700111 logging.debug('Start VM')
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700112 if not self.kvm_path:
113 self.kvm_path = self._FindKVMBinary()
114 logging.debug('kvm path=%s', self.kvm_path)
115
116 if not self.image_path:
117 self.image_path = os.environ.get('VM_IMAGE_PATH', '')
118 logging.debug('vm image path=%s', self.image_path)
119 if not self.image_path or not os.path.exists(self.image_path):
120 raise VMError('VM image path %s does not exist.' % self.image_path)
121
122 self._CleanupFiles(recreate=True)
123 open(self.kvm_serial, 'w')
124 for pipe in [self.kvm_pipe_in, self.kvm_pipe_out]:
125 os.mkfifo(pipe, 0600)
126
127 args = [self.kvm_path, '-m', '2G', '-smp', '4', '-vga', 'cirrus',
128 '-pidfile', self.pidfile,
129 '-chardev', 'pipe,id=control_pipe,path=%s' % self.kvm_monitor,
130 '-serial', 'file:%s' % self.kvm_serial,
131 '-mon', 'chardev=control_pipe', '-daemonize',
132 '-net', 'nic,model=virtio',
133 '-net', 'user,hostfwd=tcp::%d-:22' % self.ssh_port,
134 '-drive', 'file=%s,index=0,media=disk,cache=unsafe'
135 % self.image_path]
136 logging.info(' '.join(args))
137 logging.info('Pid file: %s', self.pidfile)
138 if not self.dry_run:
139 cros_build_lib.SudoRunCommand(args)
140
141 def _GetVMPid(self):
142 """Get the pid of the VM.
143
144 Returns:
145 pid of the VM.
146 """
147 if not os.path.exists(self.VM_DIR):
148 logging.info('No VM running.')
149 return 0
150
151 if not os.path.exists(self.pidfile):
152 raise VMError('%s does not exist.' % self.pidfile)
153
154 pid = cros_build_lib.SudoRunCommand(['cat', self.pidfile],
155 redirect_stdout=True).output.rstrip()
156 if not pid.isdigit():
157 raise VMError('%s in %s is not a pid.' % (pid, self.pidfile))
Achuith Bhandarkar022d69c2016-10-05 14:28:14 -0700158 return int(pid)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700159
160 def IsRunning(self):
161 """Returns True if there's a running VM.
162
163 Returns:
164 True if there's a running VM.
165 """
166 try:
167 pid = self._GetVMPid()
168 except VMError:
169 return False
Achuith Bhandarkar022d69c2016-10-05 14:28:14 -0700170
171 if not pid:
172 return False
173
174 # Make sure the process actually exists.
175 res = cros_build_lib.SudoRunCommand(['kill', '-0', str(pid)],
176 error_code_ok=True)
177 return res.returncode == 0
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700178
179 def Stop(self):
180 """Stop the VM."""
Achuith Bhandarkar022d69c2016-10-05 14:28:14 -0700181 logging.debug('Stop VM')
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700182
183 pid = self._GetVMPid()
184 if not pid:
185 return
186
Achuith Bhandarkar022d69c2016-10-05 14:28:14 -0700187 logging.info('Killing %d.', pid)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700188 if not self.dry_run:
Achuith Bhandarkar022d69c2016-10-05 14:28:14 -0700189 cros_build_lib.SudoRunCommand(['kill', '-9', str(pid)],
190 error_code_ok=True)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700191 self._CleanupFiles(recreate=False)
192
193 def WaitForBoot(self, timeout=10, poll_interval=0.1):
194 """Wait for the VM to boot up.
195
196 If there is no VM running, start one.
197
198 Args:
199 timeout: maxiumum time to wait before raising an exception.
200 poll_interval: interval between checks.
201 """
202 if not os.path.exists(self.VM_DIR):
203 self.Start()
204
205 start_time = time.time()
206 while time.time() - start_time < timeout:
207 result = self.RemoteCommand(cmd=['echo'])
208 if result.returncode == 255:
209 time.sleep(poll_interval)
210 continue
211 elif result.returncode == 0:
212 return
213 else:
214 break
215 raise VMError('WaitForBoot failed')
216
217 def RemoteCommand(self, cmd):
218 """Run a remote command in the VM.
219
220 Args:
221 cmd: command to run, of list type.
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700222 """
223 if not isinstance(cmd, list):
224 raise VMError('cmd must be a list.')
225
226 args = ['ssh', '-o', 'UserKnownHostsFile=/dev/null',
227 '-o', 'StrictHostKeyChecking=no',
228 '-i', remote_access.TEST_PRIVATE_KEY,
229 '-p', str(self.ssh_port), 'root@localhost']
230 args.extend(cmd)
231
232 if not self.dry_run:
233 return cros_build_lib.RunCommand(args, redirect_stdout=True,
234 combine_stdout_stderr=True,
235 log_output=True,
236 error_code_ok=True)
237
238
239def ParseCommandLine(argv):
240 """Parse the command line.
241
242 Args:
243 argv: Command arguments.
244
245 Returns:
246 List of parsed args.
247 """
248 parser = commandline.ArgumentParser(description=__doc__)
249 parser.add_argument('--start', action='store_true',
250 help='Start the VM.')
251 parser.add_argument('--stop', action='store_true',
252 help='Stop the VM.')
253 parser.add_argument('--cmd', help='Run this command in the VM.')
254 parser.add_argument('--image-path', type='path',
255 help='Path to VM image to launch with --start.')
256 parser.add_argument('--kvm-path', type='path',
257 help='Path of kvm binary to launch with --start.')
258 parser.add_argument('--ssh-port', type=int, default=VM.SSH_PORT,
259 help='ssh port to communicate with VM.')
260 parser.add_argument('--dry-run', action='store_true',
261 help='dry run for debugging.')
262 return parser.parse_args(argv)
263
264
265def main(argv):
266 args = ParseCommandLine(argv)
267 vm = VM(kvm_path=args.kvm_path, image_path=args.image_path,
268 ssh_port=args.ssh_port, dry_run=args.dry_run)
269 vm.PerformAction(start=args.start, stop=args.stop, cmd=args.cmd)