basevm.py 9.6 KB

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