]>
Commit | Line | Data |
---|---|---|
ff2ebff0 FZ |
1 | #!/usr/bin/env python |
2 | # | |
3 | # VM testing base class | |
4 | # | |
5 | # Copyright 2017 Red Hat Inc. | |
6 | # | |
7 | # Authors: | |
8 | # Fam Zheng <[email protected]> | |
9 | # | |
10 | # This code is licensed under the GPL version 2 or later. See | |
11 | # the COPYING file in the top-level directory. | |
12 | # | |
13 | ||
f03868bd | 14 | from __future__ import print_function |
ff2ebff0 FZ |
15 | import os |
16 | import sys | |
17 | import logging | |
18 | import time | |
19 | import datetime | |
20 | sys.path.append(os.path.join(os.path.dirname(__file__), "..", "..", "scripts")) | |
21 | from qemu import QEMUMachine | |
22 | import subprocess | |
23 | import hashlib | |
24 | import optparse | |
25 | import atexit | |
26 | import tempfile | |
27 | import shutil | |
28 | import multiprocessing | |
29 | import traceback | |
30 | ||
31 | SSH_KEY = open(os.path.join(os.path.dirname(__file__), | |
32 | "..", "keys", "id_rsa")).read() | |
33 | SSH_PUB_KEY = open(os.path.join(os.path.dirname(__file__), | |
34 | "..", "keys", "id_rsa.pub")).read() | |
35 | ||
36 | class BaseVM(object): | |
37 | GUEST_USER = "qemu" | |
38 | GUEST_PASS = "qemupass" | |
39 | ROOT_PASS = "qemupass" | |
40 | ||
41 | # The script to run in the guest that builds QEMU | |
42 | BUILD_SCRIPT = "" | |
43 | # The guest name, to be overridden by subclasses | |
44 | name = "#base" | |
45 | def __init__(self, debug=False, vcpus=None): | |
46 | self._guest = None | |
47 | self._tmpdir = os.path.realpath(tempfile.mkdtemp(prefix="vm-test-", | |
48 | suffix=".tmp", | |
49 | dir=".")) | |
50 | atexit.register(shutil.rmtree, self._tmpdir) | |
51 | ||
52 | self._ssh_key_file = os.path.join(self._tmpdir, "id_rsa") | |
53 | open(self._ssh_key_file, "w").write(SSH_KEY) | |
54 | subprocess.check_call(["chmod", "600", self._ssh_key_file]) | |
55 | ||
56 | self._ssh_pub_key_file = os.path.join(self._tmpdir, "id_rsa.pub") | |
57 | open(self._ssh_pub_key_file, "w").write(SSH_PUB_KEY) | |
58 | ||
59 | self.debug = debug | |
60 | self._stderr = sys.stderr | |
61 | self._devnull = open(os.devnull, "w") | |
62 | if self.debug: | |
63 | self._stdout = sys.stdout | |
64 | else: | |
65 | self._stdout = self._devnull | |
66 | self._args = [ \ | |
67 | "-nodefaults", "-m", "2G", | |
ff2ebff0 FZ |
68 | "-netdev", "user,id=vnet,hostfwd=:127.0.0.1:0-:22", |
69 | "-device", "virtio-net-pci,netdev=vnet", | |
70 | "-vnc", "127.0.0.1:0,to=20", | |
71 | "-serial", "file:%s" % os.path.join(self._tmpdir, "serial.out")] | |
72 | if vcpus: | |
73 | self._args += ["-smp", str(vcpus)] | |
74 | if os.access("/dev/kvm", os.R_OK | os.W_OK): | |
dcf7ea4a | 75 | self._args += ["-cpu", "host"] |
ff2ebff0 FZ |
76 | self._args += ["-enable-kvm"] |
77 | else: | |
78 | logging.info("KVM not available, not using -enable-kvm") | |
dcf7ea4a | 79 | self._args += ["-cpu", "max"] |
ff2ebff0 FZ |
80 | self._data_args = [] |
81 | ||
82 | def _download_with_cache(self, url, sha256sum=None): | |
83 | def check_sha256sum(fname): | |
84 | if not sha256sum: | |
85 | return True | |
86 | checksum = subprocess.check_output(["sha256sum", fname]).split()[0] | |
87 | return sha256sum == checksum | |
88 | ||
89 | cache_dir = os.path.expanduser("~/.cache/qemu-vm/download") | |
90 | if not os.path.exists(cache_dir): | |
91 | os.makedirs(cache_dir) | |
92 | fname = os.path.join(cache_dir, hashlib.sha1(url).hexdigest()) | |
93 | if os.path.exists(fname) and check_sha256sum(fname): | |
94 | return fname | |
95 | logging.debug("Downloading %s to %s...", url, fname) | |
96 | subprocess.check_call(["wget", "-c", url, "-O", fname + ".download"], | |
97 | stdout=self._stdout, stderr=self._stderr) | |
98 | os.rename(fname + ".download", fname) | |
99 | return fname | |
100 | ||
101 | def _ssh_do(self, user, cmd, check, interactive=False): | |
102 | ssh_cmd = ["ssh", "-q", | |
103 | "-o", "StrictHostKeyChecking=no", | |
104 | "-o", "UserKnownHostsFile=" + os.devnull, | |
105 | "-o", "ConnectTimeout=1", | |
106 | "-p", self.ssh_port, "-i", self._ssh_key_file] | |
107 | if interactive: | |
108 | ssh_cmd += ['-t'] | |
109 | assert not isinstance(cmd, str) | |
110 | ssh_cmd += ["%[email protected]" % user] + list(cmd) | |
111 | logging.debug("ssh_cmd: %s", " ".join(ssh_cmd)) | |
726c9a3b | 112 | r = subprocess.call(ssh_cmd) |
ff2ebff0 FZ |
113 | if check and r != 0: |
114 | raise Exception("SSH command failed: %s" % cmd) | |
115 | return r | |
116 | ||
117 | def ssh(self, *cmd): | |
118 | return self._ssh_do(self.GUEST_USER, cmd, False) | |
119 | ||
120 | def ssh_interactive(self, *cmd): | |
121 | return self._ssh_do(self.GUEST_USER, cmd, False, True) | |
122 | ||
123 | def ssh_root(self, *cmd): | |
124 | return self._ssh_do("root", cmd, False) | |
125 | ||
126 | def ssh_check(self, *cmd): | |
127 | self._ssh_do(self.GUEST_USER, cmd, True) | |
128 | ||
129 | def ssh_root_check(self, *cmd): | |
130 | self._ssh_do("root", cmd, True) | |
131 | ||
132 | def build_image(self, img): | |
133 | raise NotImplementedError | |
134 | ||
135 | def add_source_dir(self, src_dir): | |
136 | name = "data-" + hashlib.sha1(src_dir).hexdigest()[:5] | |
137 | tarfile = os.path.join(self._tmpdir, name + ".tar") | |
138 | logging.debug("Creating archive %s for src_dir dir: %s", tarfile, src_dir) | |
139 | subprocess.check_call(["./scripts/archive-source.sh", tarfile], | |
140 | cwd=src_dir, stdin=self._devnull, | |
141 | stdout=self._stdout, stderr=self._stderr) | |
142 | self._data_args += ["-drive", | |
143 | "file=%s,if=none,id=%s,cache=writeback,format=raw" % \ | |
144 | (tarfile, name), | |
145 | "-device", | |
146 | "virtio-blk,drive=%s,serial=%s,bootindex=1" % (name, name)] | |
147 | ||
148 | def boot(self, img, extra_args=[]): | |
149 | args = self._args + [ | |
150 | "-device", "VGA", | |
151 | "-drive", "file=%s,if=none,id=drive0,cache=writeback" % img, | |
152 | "-device", "virtio-blk,drive=drive0,bootindex=0"] | |
153 | args += self._data_args + extra_args | |
154 | logging.debug("QEMU args: %s", " ".join(args)) | |
155 | qemu_bin = os.environ.get("QEMU", "qemu-system-x86_64") | |
156 | guest = QEMUMachine(binary=qemu_bin, args=args) | |
157 | try: | |
158 | guest.launch() | |
159 | except: | |
160 | logging.error("Failed to launch QEMU, command line:") | |
161 | logging.error(" ".join([qemu_bin] + args)) | |
162 | logging.error("Log:") | |
163 | logging.error(guest.get_log()) | |
164 | logging.error("QEMU version >= 2.10 is required") | |
165 | raise | |
166 | atexit.register(self.shutdown) | |
167 | self._guest = guest | |
168 | usernet_info = guest.qmp("human-monitor-command", | |
169 | command_line="info usernet") | |
170 | self.ssh_port = None | |
171 | for l in usernet_info["return"].splitlines(): | |
172 | fields = l.split() | |
173 | if "TCP[HOST_FORWARD]" in fields and "22" in fields: | |
174 | self.ssh_port = l.split()[3] | |
175 | if not self.ssh_port: | |
176 | raise Exception("Cannot find ssh port from 'info usernet':\n%s" % \ | |
177 | usernet_info) | |
178 | ||
179 | def wait_ssh(self, seconds=120): | |
180 | starttime = datetime.datetime.now() | |
181 | guest_up = False | |
182 | while (datetime.datetime.now() - starttime).total_seconds() < seconds: | |
183 | if self.ssh("exit 0") == 0: | |
184 | guest_up = True | |
185 | break | |
186 | time.sleep(1) | |
187 | if not guest_up: | |
188 | raise Exception("Timeout while waiting for guest ssh") | |
189 | ||
190 | def shutdown(self): | |
191 | self._guest.shutdown() | |
192 | ||
193 | def wait(self): | |
194 | self._guest.wait() | |
195 | ||
196 | def qmp(self, *args, **kwargs): | |
197 | return self._guest.qmp(*args, **kwargs) | |
198 | ||
199 | def parse_args(vm_name): | |
200 | parser = optparse.OptionParser( | |
201 | description="VM test utility. Exit codes: " | |
202 | "0 = success, " | |
203 | "1 = command line error, " | |
204 | "2 = environment initialization failed, " | |
205 | "3 = test command failed") | |
206 | parser.add_option("--debug", "-D", action="store_true", | |
207 | help="enable debug output") | |
208 | parser.add_option("--image", "-i", default="%s.img" % vm_name, | |
209 | help="image file name") | |
210 | parser.add_option("--force", "-f", action="store_true", | |
211 | help="force build image even if image exists") | |
212 | parser.add_option("--jobs", type=int, default=multiprocessing.cpu_count() / 2, | |
213 | help="number of virtual CPUs") | |
214 | parser.add_option("--build-image", "-b", action="store_true", | |
215 | help="build image") | |
216 | parser.add_option("--build-qemu", | |
217 | help="build QEMU from source in guest") | |
218 | parser.add_option("--interactive", "-I", action="store_true", | |
219 | help="Interactively run command") | |
983c2a77 FZ |
220 | parser.add_option("--snapshot", "-s", action="store_true", |
221 | help="run tests with a snapshot") | |
ff2ebff0 FZ |
222 | parser.disable_interspersed_args() |
223 | return parser.parse_args() | |
224 | ||
225 | def main(vmcls): | |
226 | try: | |
227 | args, argv = parse_args(vmcls.name) | |
228 | if not argv and not args.build_qemu and not args.build_image: | |
f03868bd | 229 | print("Nothing to do?") |
ff2ebff0 | 230 | return 1 |
fb3b4e6d EH |
231 | logging.basicConfig(level=(logging.DEBUG if args.debug |
232 | else logging.WARN)) | |
ff2ebff0 FZ |
233 | vm = vmcls(debug=args.debug, vcpus=args.jobs) |
234 | if args.build_image: | |
235 | if os.path.exists(args.image) and not args.force: | |
236 | sys.stderr.writelines(["Image file exists: %s\n" % args.image, | |
237 | "Use --force option to overwrite\n"]) | |
238 | return 1 | |
239 | return vm.build_image(args.image) | |
240 | if args.build_qemu: | |
241 | vm.add_source_dir(args.build_qemu) | |
242 | cmd = [vm.BUILD_SCRIPT.format( | |
243 | configure_opts = " ".join(argv), | |
244 | jobs=args.jobs)] | |
245 | else: | |
246 | cmd = argv | |
983c2a77 FZ |
247 | img = args.image |
248 | if args.snapshot: | |
249 | img += ",snapshot=on" | |
250 | vm.boot(img) | |
ff2ebff0 FZ |
251 | vm.wait_ssh() |
252 | except Exception as e: | |
253 | if isinstance(e, SystemExit) and e.code == 0: | |
254 | return 0 | |
255 | sys.stderr.write("Failed to prepare guest environment\n") | |
256 | traceback.print_exc() | |
257 | return 2 | |
258 | ||
259 | if args.interactive: | |
260 | if vm.ssh_interactive(*cmd) == 0: | |
261 | return 0 | |
262 | vm.ssh_interactive() | |
263 | return 3 | |
264 | else: | |
265 | if vm.ssh(*cmd) != 0: | |
266 | return 3 |