@@ -29,16 +29,41 @@ import tempfile
import shutil
import multiprocessing
import traceback
-
-SSH_KEY = open(os.path.join(os.path.dirname(__file__),
- "..", "keys", "id_rsa")).read()
-SSH_PUB_KEY = open(os.path.join(os.path.dirname(__file__),
- "..", "keys", "id_rsa.pub")).read()
-
+import shlex
+
+SSH_KEY_FILE = os.path.join(os.path.dirname(__file__),
+ "..", "keys", "id_rsa")
+SSH_PUB_KEY_FILE = os.path.join(os.path.dirname(__file__),
+ "..", "keys", "id_rsa.pub")
+
+# This is the standard configuration.
+# Any or all of these can be overridden by
+# passing in a config argument to the VM constructor.
+DEFAULT_CONFIG = {
+ 'cpu' : "max",
+ 'machine' : 'pc',
+ 'guest_user' : "qemu",
+ 'guest_pass' : "qemupass",
+ 'root_pass' : "qemupass",
+ 'ssh_key_file' : SSH_KEY_FILE,
+ 'ssh_pub_key_file': SSH_PUB_KEY_FILE,
+ 'memory' : "4G",
+ 'extra_args' : [],
+ 'qemu_args' : "",
+ 'dns' : "",
+ 'ssh_port' : 0,
+ 'install_cmds' : "",
+ 'boot_dev_type' : "block",
+ 'ssh_timeout' : 1,
+}
+BOOT_DEVICE = {
+ 'block' : "-drive file={},if=none,id=drive0,cache=writeback "\
+ "-device virtio-blk,drive=drive0,bootindex=0",
+ 'scsi' : "-device virtio-scsi-device,id=scsi "\
+ "-drive file={},format=raw,if=none,id=hd0 "\
+ "-device scsi-hd,drive=hd0,bootindex=0",
+}
class BaseVM(object):
- GUEST_USER = "qemu"
- GUEST_PASS = "qemupass"
- ROOT_PASS = "qemupass"
envvars = [
"https_proxy",
@@ -61,19 +86,30 @@ class BaseVM(object):
# 4 is arbitrary, but greater than 2,
# since we found we need to wait more than twice as long.
tcg_ssh_timeout_multiplier = 4
- def __init__(self, debug=False, vcpus=None):
+ def __init__(self, debug=False, vcpus=None, config=None):
self._guest = None
+ # Allow input config to override defaults.
+ self._config = DEFAULT_CONFIG.copy()
+ if config != None:
+ self._config.update(config)
+ self.validate_ssh_keys()
self._tmpdir = os.path.realpath(tempfile.mkdtemp(prefix="vm-test-",
suffix=".tmp",
dir="."))
atexit.register(shutil.rmtree, self._tmpdir)
-
- self._ssh_key_file = os.path.join(self._tmpdir, "id_rsa")
- open(self._ssh_key_file, "w").write(SSH_KEY)
- subprocess.check_call(["chmod", "600", self._ssh_key_file])
-
- self._ssh_pub_key_file = os.path.join(self._tmpdir, "id_rsa.pub")
- open(self._ssh_pub_key_file, "w").write(SSH_PUB_KEY)
+ # Copy the key files to a temporary directory.
+ # Also chmod the key file to agree with ssh requirements.
+ self._config['ssh_key'] = \
+ open(self._config['ssh_key_file']).read().rstrip()
+ self._config['ssh_pub_key'] = \
+ open(self._config['ssh_pub_key_file']).read().rstrip()
+ self._ssh_tmp_key_file = os.path.join(self._tmpdir, "id_rsa")
+ open(self._ssh_tmp_key_file, "w").write(self._config['ssh_key'])
+ subprocess.check_call(["chmod", "600", self._ssh_tmp_key_file])
+
+ self._ssh_tmp_pub_key_file = os.path.join(self._tmpdir, "id_rsa.pub")
+ open(self._ssh_tmp_pub_key_file,
+ "w").write(self._config['ssh_pub_key'])
self.debug = debug
self._stderr = sys.stderr
@@ -82,11 +118,14 @@ class BaseVM(object):
self._stdout = sys.stdout
else:
self._stdout = self._devnull
+ netdev = "user,id=vnet,hostfwd=:127.0.0.1:{}-:22"
self._args = [ \
- "-nodefaults", "-m", "4G",
- "-cpu", "max",
- "-netdev", "user,id=vnet,hostfwd=:127.0.0.1:0-:22" +
- (",ipv6=no" if not self.ipv6 else ""),
+ "-nodefaults", "-m", self._config['memory'],
+ "-cpu", self._config['cpu'],
+ "-netdev",
+ netdev.format(self._config['ssh_port']) +
+ (",ipv6=no" if not self.ipv6 else "") +
+ (",dns=" + self._config['dns'] if self._config['dns'] else ""),
"-device", "virtio-net-pci,netdev=vnet",
"-vnc", "127.0.0.1:0,to=20"]
if vcpus and vcpus > 1:
@@ -97,6 +136,55 @@ class BaseVM(object):
logging.info("KVM not available, not using -enable-kvm")
self._data_args = []
+ if self._config['qemu_args'] != None:
+ qemu_args = self._config['qemu_args']
+ qemu_args = qemu_args.replace('\n',' ').replace('\r','')
+ # shlex groups quoted arguments together
+ # we need this to keep the quoted args together for when
+ # the QEMU command is issued later.
+ args = shlex.split(qemu_args)
+ self._config['extra_args'] = []
+ for arg in args:
+ if arg:
+ # Preserve quotes around arguments.
+ # shlex above takes them out, so add them in.
+ if " " in arg:
+ arg = '"{}"'.format(arg)
+ self._config['extra_args'].append(arg)
+
+ def validate_ssh_keys(self):
+ """Check to see if the ssh key files exist."""
+ if 'ssh_key_file' not in self._config or\
+ not os.path.exists(self._config['ssh_key_file']):
+ raise Exception("ssh key file not found.")
+ if 'ssh_pub_key_file' not in self._config or\
+ not os.path.exists(self._config['ssh_pub_key_file']):
+ raise Exception("ssh pub key file not found.")
+
+ def wait_boot(self, wait_string=None):
+ """Wait for the standard string we expect
+ on completion of a normal boot.
+ The user can also choose to override with an
+ alternate string to wait for."""
+ if wait_string is None:
+ if self.login_prompt is None:
+ raise Exception("self.login_prompt not defined")
+ wait_string = self.login_prompt
+ # Intentionally bump up the default timeout under TCG,
+ # since the console wait below takes longer.
+ timeout = self.socket_timeout
+ if not kvm_available(self.arch):
+ timeout *= 8
+ self.console_init(timeout=timeout)
+ self.console_wait(wait_string)
+
+ def __getattr__(self, name):
+ # Support direct access to config by key.
+ # for example, access self._config['cpu'] by self.cpu
+ if name.lower() in self._config.keys():
+ return self._config[name.lower()]
+ return object.__getattribute__(self, name)
+
def _download_with_cache(self, url, sha256sum=None, sha512sum=None):
def check_sha256sum(fname):
if not sha256sum:
@@ -128,8 +216,9 @@ class BaseVM(object):
"-t",
"-o", "StrictHostKeyChecking=no",
"-o", "UserKnownHostsFile=" + os.devnull,
- "-o", "ConnectTimeout=1",
- "-p", self.ssh_port, "-i", self._ssh_key_file]
+ "-o",
+ "ConnectTimeout={}".format(self._config["ssh_timeout"]),
+ "-p", self.ssh_port, "-i", self._ssh_tmp_key_file]
# If not in debug mode, set ssh to quiet mode to
# avoid printing the results of commands.
if not self.debug:
@@ -178,14 +267,14 @@ class BaseVM(object):
"virtio-blk,drive=%s,serial=%s,bootindex=1" % (name, name)]
def boot(self, img, extra_args=[]):
- args = self._args + [
- "-drive", "file=%s,if=none,id=drive0,cache=writeback" % img,
- "-device", "virtio-blk,drive=drive0,bootindex=0"]
- args += self._data_args + extra_args
+ boot_dev = BOOT_DEVICE[self._config['boot_dev_type']]
+ boot_params = boot_dev.format(img)
+ args = self._args + boot_params.split(' ')
+ args += self._data_args + extra_args + self._config['extra_args']
logging.debug("QEMU args: %s", " ".join(args))
qemu_bin = os.environ.get("QEMU", "qemu-system-" + self.arch)
guest = QEMUMachine(binary=qemu_bin, args=args)
- guest.set_machine('pc')
+ guest.set_machine(self._config['machine'])
guest.set_console()
try:
guest.launch()
@@ -299,7 +388,8 @@ class BaseVM(object):
self.console_send(command)
def console_ssh_init(self, prompt, user, pw):
- sshkey_cmd = "echo '%s' > .ssh/authorized_keys\n" % SSH_PUB_KEY.rstrip()
+ sshkey_cmd = "echo '%s' > .ssh/authorized_keys\n" \
+ % self._config['ssh_pub_key'].rstrip()
self.console_wait_send("login:", "%s\n" % user)
self.console_wait_send("Password:", "%s\n" % pw)
self.console_wait_send(prompt, "mkdir .ssh\n")
@@ -358,23 +448,23 @@ class BaseVM(object):
"local-hostname: {}-guest\n".format(name)])
mdata.close()
udata = open(os.path.join(cidir, "user-data"), "w")
- print("guest user:pw {}:{}".format(self.GUEST_USER,
- self.GUEST_PASS))
+ print("guest user:pw {}:{}".format(self._config['guest_user'],
+ self._config['guest_pass']))
udata.writelines(["#cloud-config\n",
"chpasswd:\n",
" list: |\n",
- " root:%s\n" % self.ROOT_PASS,
- " %s:%s\n" % (self.GUEST_USER,
- self.GUEST_PASS),
+ " root:%s\n" % self._config['root_pass'],
+ " %s:%s\n" % (self._config['guest_user'],
+ self._config['guest_pass']),
" expire: False\n",
"users:\n",
- " - name: %s\n" % self.GUEST_USER,
+ " - name: %s\n" % self._config['guest_user'],
" sudo: ALL=(ALL) NOPASSWD:ALL\n",
" ssh-authorized-keys:\n",
- " - %s\n" % SSH_PUB_KEY,
+ " - %s\n" % self._config['ssh_pub_key'],
" - name: root\n",
" ssh-authorized-keys:\n",
- " - %s\n" % SSH_PUB_KEY,
+ " - %s\n" % self._config['ssh_pub_key'],
"locale: en_US.UTF-8\n"])
proxy = os.environ.get("http_proxy")
if not proxy is None:
@@ -427,15 +517,17 @@ def parse_args(vmcls):
parser.disable_interspersed_args()
return parser.parse_args()
-def main(vmcls):
+def main(vmcls, config=None):
try:
+ if config == None:
+ config = {}
args, argv = parse_args(vmcls)
if not argv and not args.build_qemu and not args.build_image:
print("Nothing to do?")
return 1
logging.basicConfig(level=(logging.DEBUG if args.debug
else logging.WARN))
- vm = vmcls(debug=args.debug, vcpus=args.jobs)
+ vm = vmcls(debug=args.debug, vcpus=args.jobs, config=config)
if args.build_image:
if os.path.exists(args.image) and not args.force:
sys.stderr.writelines(["Image file exists: %s\n" % args.image,