blob: 8178ea834128d0a17705625da1ca604bf1778a47 [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
31 VM_DIR = '/var/run/cros_vm'
32
33
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -070034 def __init__(self, image_path=None, qemu_path=None, enable_kvm=True,
35 ssh_port=SSH_PORT, dry_run=False):
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070036 """Initialize VM.
37
38 Args:
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070039 image_path: path of vm image.
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -070040 qemu_path: path to qemu binary.
41 enable_kvm: enable kvm (kernel support for virtualization).
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 Bhandarkard8d19292016-05-03 14:32:58 -070048 self.image_path = image_path
49 self.ssh_port = ssh_port
50 self.dry_run = dry_run
51
52 self.pidfile = os.path.join(self.VM_DIR, 'kvm.pid')
53 self.kvm_monitor = os.path.join(self.VM_DIR, 'kvm.monitor')
54 self.kvm_pipe_in = '%s.in' % self.kvm_monitor # to KVM
55 self.kvm_pipe_out = '%s.out' % self.kvm_monitor # from KVM
56 self.kvm_serial = '%s.serial' % self.kvm_monitor
57
58 # TODO(achuith): support nographics, snapshot, mem_path, usb_passthrough,
59 # moblab, etc.
60
61 @staticmethod
62 def _CleanupFiles(recreate):
63 """Cleanup VM_DIR.
64
65 Args:
66 recreate: recreate VM_DIR.
67 """
68 cros_build_lib.SudoRunCommand(['rm', '-rf', VM.VM_DIR])
69 if recreate:
70 cros_build_lib.SudoRunCommand(['mkdir', VM.VM_DIR])
71 cros_build_lib.SudoRunCommand(['chmod', '777', VM.VM_DIR])
72
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070073 def PerformAction(self, start=False, stop=False, cmd=None):
74 """Performs an action, one of start, stop, or run a command in the VM.
75
76 Args:
77 start: start the VM.
78 stop: stop the VM.
79 cmd: list or scalar command to run in the VM.
80
81 Returns:
82 cmd output.
83 """
84
85 if not start and not stop and not cmd:
86 raise VMError('Must specify one of start, stop, or cmd.')
87 if start:
88 self.Start()
89 if stop:
90 self.Stop()
91 if cmd:
92 return self.RemoteCommand(cmd.split())
93
94 def Start(self):
95 """Start the VM."""
96
97 self.Stop()
98
Achuith Bhandarkar022d69c2016-10-05 14:28:14 -070099 logging.debug('Start VM')
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700100 if not self.qemu_path:
101 self.qemu_path = osutils.Which('qemu-system-x86_64')
102 if not self.qemu_path:
103 raise VMError('qemu not found.')
104 logging.debug('qemu path=%s', self.qemu_path)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700105
106 if not self.image_path:
107 self.image_path = os.environ.get('VM_IMAGE_PATH', '')
108 logging.debug('vm image path=%s', self.image_path)
109 if not self.image_path or not os.path.exists(self.image_path):
110 raise VMError('VM image path %s does not exist.' % self.image_path)
111
112 self._CleanupFiles(recreate=True)
113 open(self.kvm_serial, 'w')
114 for pipe in [self.kvm_pipe_in, self.kvm_pipe_out]:
115 os.mkfifo(pipe, 0600)
116
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700117 args = [self.qemu_path, '-m', '2G', '-smp', '4', '-vga', 'cirrus',
118 '-daemonize',
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700119 '-pidfile', self.pidfile,
120 '-chardev', 'pipe,id=control_pipe,path=%s' % self.kvm_monitor,
121 '-serial', 'file:%s' % self.kvm_serial,
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700122 '-mon', 'chardev=control_pipe',
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700123 '-net', 'nic,model=virtio',
124 '-net', 'user,hostfwd=tcp::%d-:22' % self.ssh_port,
125 '-drive', 'file=%s,index=0,media=disk,cache=unsafe'
126 % self.image_path]
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700127 if self.enable_kvm:
128 args.append('-enable-kvm')
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700129 logging.info(' '.join(args))
130 logging.info('Pid file: %s', self.pidfile)
131 if not self.dry_run:
132 cros_build_lib.SudoRunCommand(args)
133
134 def _GetVMPid(self):
135 """Get the pid of the VM.
136
137 Returns:
138 pid of the VM.
139 """
140 if not os.path.exists(self.VM_DIR):
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700141 logging.debug('%s not present.', self.VM_DIR)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700142 return 0
143
144 if not os.path.exists(self.pidfile):
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700145 logging.info('%s does not exist.', self.pidfile)
146 return 0
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700147
148 pid = cros_build_lib.SudoRunCommand(['cat', self.pidfile],
149 redirect_stdout=True).output.rstrip()
150 if not pid.isdigit():
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700151 logging.error('%s in %s is not a pid.', pid, self.pidfile)
152 return 0
153
Achuith Bhandarkar022d69c2016-10-05 14:28:14 -0700154 return int(pid)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700155
156 def IsRunning(self):
157 """Returns True if there's a running VM.
158
159 Returns:
160 True if there's a running VM.
161 """
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700162 pid = self._GetVMPid()
Achuith Bhandarkar022d69c2016-10-05 14:28:14 -0700163 if not pid:
164 return False
165
166 # Make sure the process actually exists.
167 res = cros_build_lib.SudoRunCommand(['kill', '-0', str(pid)],
168 error_code_ok=True)
169 return res.returncode == 0
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700170
171 def Stop(self):
172 """Stop the VM."""
Achuith Bhandarkar022d69c2016-10-05 14:28:14 -0700173 logging.debug('Stop VM')
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700174
175 pid = self._GetVMPid()
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700176 if pid:
177 logging.info('Killing %d.', pid)
178 if not self.dry_run:
179 cros_build_lib.SudoRunCommand(['kill', '-9', str(pid)],
180 error_code_ok=True)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700181
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700182 self._CleanupFiles(recreate=False)
183
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700184 def WaitForBoot(self, timeout=120, poll_interval=0.1):
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700185 """Wait for the VM to boot up.
186
187 If there is no VM running, start one.
188
189 Args:
190 timeout: maxiumum time to wait before raising an exception.
191 poll_interval: interval between checks.
192 """
193 if not os.path.exists(self.VM_DIR):
194 self.Start()
195
196 start_time = time.time()
197 while time.time() - start_time < timeout:
198 result = self.RemoteCommand(cmd=['echo'])
199 if result.returncode == 255:
200 time.sleep(poll_interval)
201 continue
202 elif result.returncode == 0:
203 return
204 else:
205 break
206 raise VMError('WaitForBoot failed')
207
208 def RemoteCommand(self, cmd):
209 """Run a remote command in the VM.
210
211 Args:
212 cmd: command to run, of list type.
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700213 """
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__)
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700240 parser.add_argument('--start', action='store_true', default=False,
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700241 help='Start the VM.')
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700242 parser.add_argument('--stop', action='store_true', default=False,
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700243 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.')
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700247 parser.add_argument('--qemu-path', type='path',
248 help='Path of qemu binary to launch with --start.')
249 parser.add_argument('--disable-kvm', action='store_true', default=False,
250 help='Disable KVM, use software emulation.')
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700251 parser.add_argument('--ssh-port', type=int, default=VM.SSH_PORT,
252 help='ssh port to communicate with VM.')
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700253 parser.add_argument('--dry-run', action='store_true', default=False,
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700254 help='dry run for debugging.')
255 return parser.parse_args(argv)
256
257
258def main(argv):
259 args = ParseCommandLine(argv)
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700260 vm = VM(image_path=args.image_path,
261 qemu_path=args.qemu_path, enable_kvm=not args.disable_kvm,
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700262 ssh_port=args.ssh_port, dry_run=args.dry_run)
263 vm.PerformAction(start=args.start, stop=args.stop, cmd=args.cmd)