2 # VM testing base class
4 # Copyright 2017-2019 Red Hat Inc.
7 # Fam Zheng <famz@redhat.com>
8 # Gerd Hoffmann <kraxel@redhat.com>
10 # This code is licensed under the GPL version 2 or later. See
11 # the COPYING file in the top-level directory.
27 import multiprocessing
32 from qemu
.machine
import QEMUMachine
33 from qemu
.utils
import get_info_usernet_hostfwd_port
, kvm_available
35 SSH_KEY_FILE
= os
.path
.join(os
.path
.dirname(__file__
),
36 "..", "keys", "id_rsa")
37 SSH_PUB_KEY_FILE
= os
.path
.join(os
.path
.dirname(__file__
),
38 "..", "keys", "id_rsa.pub")
40 # This is the standard configuration.
41 # Any or all of these can be overridden by
42 # passing in a config argument to the VM constructor.
46 'guest_user' : "qemu",
47 'guest_pass' : "qemupass",
49 'root_pass' : "qemupass",
50 'ssh_key_file' : SSH_KEY_FILE
,
51 'ssh_pub_key_file': SSH_PUB_KEY_FILE
,
58 'boot_dev_type' : "block",
62 'block' : "-drive file={},if=none,id=drive0,cache=writeback "\
63 "-device virtio-blk,drive=drive0,bootindex=0",
64 'scsi' : "-device virtio-scsi-device,id=scsi "\
65 "-drive file={},format=raw,if=none,id=hd0 "\
66 "-device scsi-hd,drive=hd0,bootindex=0",
77 # The script to run in the guest that builds QEMU
79 # The guest name, to be overridden by subclasses
81 # The guest architecture, to be overridden by subclasses
83 # command to halt the guest, can be overridden by subclasses
85 # Time to wait for shutdown to finish.
86 shutdown_timeout_default
= 30
87 # enable IPv6 networking
89 # This is the timeout on the wait for console bytes.
91 # Scale up some timeouts under TCG.
92 # 4 is arbitrary, but greater than 2,
93 # since we found we need to wait more than twice as long.
94 tcg_timeout_multiplier
= 4
95 def __init__(self
, args
, config
=None):
97 self
._genisoimage
= args
.genisoimage
98 self
._build
_path
= args
.build_path
99 self
._efi
_aarch
64 = args
.efi_aarch64
100 self
._source
_path
= args
.source_path
101 # Allow input config to override defaults.
102 self
._config
= DEFAULT_CONFIG
.copy()
104 # 1GB per core, minimum of 4. This is only a default.
105 mem
= max(4, args
.jobs
)
106 self
._config
['memory'] = f
"{mem}G"
109 self
._config
.update(config
)
110 self
.validate_ssh_keys()
111 self
._tmpdir
= os
.path
.realpath(tempfile
.mkdtemp(prefix
="vm-test-",
114 atexit
.register(shutil
.rmtree
, self
._tmpdir
)
115 # Copy the key files to a temporary directory.
116 # Also chmod the key file to agree with ssh requirements.
117 self
._config
['ssh_key'] = \
118 open(self
._config
['ssh_key_file']).read().rstrip()
119 self
._config
['ssh_pub_key'] = \
120 open(self
._config
['ssh_pub_key_file']).read().rstrip()
121 self
._ssh
_tmp
_key
_file
= os
.path
.join(self
._tmpdir
, "id_rsa")
122 open(self
._ssh
_tmp
_key
_file
, "w").write(self
._config
['ssh_key'])
123 subprocess
.check_call(["chmod", "600", self
._ssh
_tmp
_key
_file
])
125 self
._ssh
_tmp
_pub
_key
_file
= os
.path
.join(self
._tmpdir
, "id_rsa.pub")
126 open(self
._ssh
_tmp
_pub
_key
_file
,
127 "w").write(self
._config
['ssh_pub_key'])
129 self
.debug
= args
.debug
130 self
._console
_log
_path
= None
132 self
._console
_log
_path
= \
133 os
.path
.join(os
.path
.expanduser("~/.cache/qemu-vm"),
134 "{}.install.log".format(self
.name
))
135 self
._stderr
= sys
.stderr
136 self
._devnull
= open(os
.devnull
, "w")
138 self
._stdout
= sys
.stdout
140 self
._stdout
= self
._devnull
141 netdev
= "user,id=vnet,hostfwd=:127.0.0.1:{}-:22"
143 "-nodefaults", "-m", self
._config
['memory'],
144 "-cpu", self
._config
['cpu'],
146 netdev
.format(self
._config
['ssh_port']) +
147 (",ipv6=no" if not self
.ipv6
else "") +
148 (",dns=" + self
._config
['dns'] if self
._config
['dns'] else ""),
149 "-device", "virtio-net-pci,netdev=vnet",
150 "-vnc", "127.0.0.1:0,to=20"]
151 if args
.jobs
and args
.jobs
> 1:
152 self
._args
+= ["-smp", "%d" % args
.jobs
]
153 if kvm_available(self
.arch
):
154 self
._shutdown
_timeout
= self
.shutdown_timeout_default
155 self
._args
+= ["-enable-kvm"]
157 logging
.info("KVM not available, not using -enable-kvm")
158 self
._shutdown
_timeout
= \
159 self
.shutdown_timeout_default
* self
.tcg_timeout_multiplier
162 if self
._config
['qemu_args'] != None:
163 qemu_args
= self
._config
['qemu_args']
164 qemu_args
= qemu_args
.replace('\n',' ').replace('\r','')
165 # shlex groups quoted arguments together
166 # we need this to keep the quoted args together for when
167 # the QEMU command is issued later.
168 args
= shlex
.split(qemu_args
)
169 self
._config
['extra_args'] = []
172 # Preserve quotes around arguments.
173 # shlex above takes them out, so add them in.
175 arg
= '"{}"'.format(arg
)
176 self
._config
['extra_args'].append(arg
)
178 def validate_ssh_keys(self
):
179 """Check to see if the ssh key files exist."""
180 if 'ssh_key_file' not in self
._config
or\
181 not os
.path
.exists(self
._config
['ssh_key_file']):
182 raise Exception("ssh key file not found.")
183 if 'ssh_pub_key_file' not in self
._config
or\
184 not os
.path
.exists(self
._config
['ssh_pub_key_file']):
185 raise Exception("ssh pub key file not found.")
187 def wait_boot(self
, wait_string
=None):
188 """Wait for the standard string we expect
189 on completion of a normal boot.
190 The user can also choose to override with an
191 alternate string to wait for."""
192 if wait_string
is None:
193 if self
.login_prompt
is None:
194 raise Exception("self.login_prompt not defined")
195 wait_string
= self
.login_prompt
196 # Intentionally bump up the default timeout under TCG,
197 # since the console wait below takes longer.
198 timeout
= self
.socket_timeout
199 if not kvm_available(self
.arch
):
201 self
.console_init(timeout
=timeout
)
202 self
.console_wait(wait_string
)
204 def _download_with_cache(self
, url
, sha256sum
=None, sha512sum
=None):
205 def check_sha256sum(fname
):
208 checksum
= subprocess
.check_output(["sha256sum", fname
]).split()[0]
209 return sha256sum
== checksum
.decode("utf-8")
211 def check_sha512sum(fname
):
214 checksum
= subprocess
.check_output(["sha512sum", fname
]).split()[0]
215 return sha512sum
== checksum
.decode("utf-8")
217 cache_dir
= os
.path
.expanduser("~/.cache/qemu-vm/download")
218 if not os
.path
.exists(cache_dir
):
219 os
.makedirs(cache_dir
)
220 fname
= os
.path
.join(cache_dir
,
221 hashlib
.sha1(url
.encode("utf-8")).hexdigest())
222 if os
.path
.exists(fname
) and check_sha256sum(fname
) and check_sha512sum(fname
):
224 logging
.debug("Downloading %s to %s...", url
, fname
)
225 subprocess
.check_call(["wget", "-c", url
, "-O", fname
+ ".download"],
226 stdout
=self
._stdout
, stderr
=self
._stderr
)
227 os
.rename(fname
+ ".download", fname
)
230 def _ssh_do(self
, user
, cmd
, check
):
233 "-o", "StrictHostKeyChecking=no",
234 "-o", "UserKnownHostsFile=" + os
.devnull
,
236 "ConnectTimeout={}".format(self
._config
["ssh_timeout"]),
237 "-p", str(self
.ssh_port
), "-i", self
._ssh
_tmp
_key
_file
,
238 "-o", "IdentitiesOnly=yes"]
239 # If not in debug mode, set ssh to quiet mode to
240 # avoid printing the results of commands.
243 for var
in self
.envvars
:
244 ssh_cmd
+= ['-o', "SendEnv=%s" % var
]
245 assert not isinstance(cmd
, str)
246 ssh_cmd
+= ["%s@127.0.0.1" % user
] + list(cmd
)
247 logging
.debug("ssh_cmd: %s", " ".join(ssh_cmd
))
248 r
= subprocess
.call(ssh_cmd
)
250 raise Exception("SSH command failed: %s" % cmd
)
254 return self
._ssh
_do
(self
._config
["guest_user"], cmd
, False)
256 def ssh_root(self
, *cmd
):
257 return self
._ssh
_do
(self
._config
["root_user"], cmd
, False)
259 def ssh_check(self
, *cmd
):
260 self
._ssh
_do
(self
._config
["guest_user"], cmd
, True)
262 def ssh_root_check(self
, *cmd
):
263 self
._ssh
_do
(self
._config
["root_user"], cmd
, True)
265 def build_image(self
, img
):
266 raise NotImplementedError
268 def exec_qemu_img(self
, *args
):
269 cmd
= [os
.environ
.get("QEMU_IMG", "qemu-img")]
270 cmd
.extend(list(args
))
271 subprocess
.check_call(cmd
)
273 def add_source_dir(self
, src_dir
):
274 name
= "data-" + hashlib
.sha1(src_dir
.encode("utf-8")).hexdigest()[:5]
275 tarfile
= os
.path
.join(self
._tmpdir
, name
+ ".tar")
276 logging
.debug("Creating archive %s for src_dir dir: %s", tarfile
, src_dir
)
277 subprocess
.check_call(["./scripts/archive-source.sh", tarfile
],
278 cwd
=src_dir
, stdin
=self
._devnull
,
279 stdout
=self
._stdout
, stderr
=self
._stderr
)
280 self
._data
_args
+= ["-drive",
281 "file=%s,if=none,id=%s,cache=writeback,format=raw" % \
284 "virtio-blk,drive=%s,serial=%s,bootindex=1" % (name
, name
)]
286 def boot(self
, img
, extra_args
=[]):
287 boot_dev
= BOOT_DEVICE
[self
._config
['boot_dev_type']]
288 boot_params
= boot_dev
.format(img
)
289 args
= self
._args
+ boot_params
.split(' ')
290 args
+= self
._data
_args
+ extra_args
+ self
._config
['extra_args']
291 logging
.debug("QEMU args: %s", " ".join(args
))
292 qemu_path
= get_qemu_path(self
.arch
, self
._build
_path
)
294 # Since console_log_path is only set when the user provides the
295 # log_console option, we will set drain_console=True so the
296 # console is always drained.
297 guest
= QEMUMachine(binary
=qemu_path
, args
=args
,
298 console_log
=self
._console
_log
_path
,
300 guest
.set_machine(self
._config
['machine'])
305 logging
.error("Failed to launch QEMU, command line:")
306 logging
.error(" ".join([qemu_path
] + args
))
307 logging
.error("Log:")
308 logging
.error(guest
.get_log())
309 logging
.error("QEMU version >= 2.10 is required")
311 atexit
.register(self
.shutdown
)
313 # Init console so we can start consuming the chars.
315 usernet_info
= guest
.cmd("human-monitor-command",
316 command_line
="info usernet")
317 self
.ssh_port
= get_info_usernet_hostfwd_port(usernet_info
)
318 if not self
.ssh_port
:
319 raise Exception("Cannot find ssh port from 'info usernet':\n%s" % \
322 def console_init(self
, timeout
= None):
324 timeout
= self
.socket_timeout
326 vm
.console_socket
.settimeout(timeout
)
327 self
.console_raw_path
= os
.path
.join(vm
._temp
_dir
,
328 vm
._name
+ "-console.raw")
329 self
.console_raw_file
= open(self
.console_raw_path
, 'wb')
331 def console_log(self
, text
):
332 for line
in re
.split("[\r\n]", text
):
333 # filter out terminal escape sequences
334 line
= re
.sub("\x1b\\[[0-9;?]*[a-zA-Z]", "", line
)
335 line
= re
.sub("\x1b\\([0-9;?]*[a-zA-Z]", "", line
)
336 # replace unprintable chars
337 line
= re
.sub("\x1b", "<esc>", line
)
338 line
= re
.sub("[\x00-\x1f]", ".", line
)
339 line
= re
.sub("[\x80-\xff]", ".", line
)
343 sys
.stderr
.write("con recv: %s\n" % line
)
345 def console_wait(self
, expect
, expectalt
= None):
350 chars
= vm
.console_socket
.recv(1)
351 if self
.console_raw_file
:
352 self
.console_raw_file
.write(chars
)
353 self
.console_raw_file
.flush()
354 except socket
.timeout
:
355 sys
.stderr
.write("console: *** read timeout ***\n")
356 sys
.stderr
.write("console: waiting for: '%s'\n" % expect
)
357 if not expectalt
is None:
358 sys
.stderr
.write("console: waiting for: '%s' (alt)\n" % expectalt
)
359 sys
.stderr
.write("console: line buffer:\n")
360 sys
.stderr
.write("\n")
361 self
.console_log(output
.rstrip())
362 sys
.stderr
.write("\n")
364 output
+= chars
.decode("latin1")
367 if not expectalt
is None and expectalt
in output
:
369 if "\r" in output
or "\n" in output
:
370 lines
= re
.split("[\r\n]", output
)
373 self
.console_log("\n".join(lines
))
375 self
.console_log(output
)
376 if not expectalt
is None and expectalt
in output
:
380 def console_consume(self
):
383 vm
.console_socket
.setblocking(0)
386 chars
= vm
.console_socket
.recv(1)
389 output
+= chars
.decode("latin1")
390 if "\r" in output
or "\n" in output
:
391 lines
= re
.split("[\r\n]", output
)
394 self
.console_log("\n".join(lines
))
396 self
.console_log(output
)
397 vm
.console_socket
.setblocking(1)
399 def console_send(self
, command
):
402 logline
= re
.sub("\n", "<enter>", command
)
403 logline
= re
.sub("[\x00-\x1f]", ".", logline
)
404 sys
.stderr
.write("con send: %s\n" % logline
)
405 for char
in list(command
):
406 vm
.console_socket
.send(char
.encode("utf-8"))
409 def console_wait_send(self
, wait
, command
):
410 self
.console_wait(wait
)
411 self
.console_send(command
)
413 def console_ssh_init(self
, prompt
, user
, pw
):
414 sshkey_cmd
= "echo '%s' > .ssh/authorized_keys\n" \
415 % self
._config
['ssh_pub_key'].rstrip()
416 self
.console_wait_send("login:", "%s\n" % user
)
417 self
.console_wait_send("Password:", "%s\n" % pw
)
418 self
.console_wait_send(prompt
, "mkdir .ssh\n")
419 self
.console_wait_send(prompt
, sshkey_cmd
)
420 self
.console_wait_send(prompt
, "chmod 755 .ssh\n")
421 self
.console_wait_send(prompt
, "chmod 644 .ssh/authorized_keys\n")
423 def console_sshd_config(self
, prompt
):
424 self
.console_wait(prompt
)
425 self
.console_send("echo 'PermitRootLogin yes' >> /etc/ssh/sshd_config\n")
426 for var
in self
.envvars
:
427 self
.console_wait(prompt
)
428 self
.console_send("echo 'AcceptEnv %s' >> /etc/ssh/sshd_config\n" % var
)
430 def print_step(self
, text
):
431 sys
.stderr
.write("### %s ...\n" % text
)
433 def wait_ssh(self
, wait_root
=False, seconds
=300, cmd
="exit 0"):
434 # Allow more time for VM to boot under TCG.
435 if not kvm_available(self
.arch
):
436 seconds
*= self
.tcg_timeout_multiplier
437 starttime
= datetime
.datetime
.now()
438 endtime
= starttime
+ datetime
.timedelta(seconds
=seconds
)
440 while datetime
.datetime
.now() < endtime
:
441 if wait_root
and self
.ssh_root(cmd
) == 0:
444 elif self
.ssh(cmd
) == 0:
447 seconds
= (endtime
- datetime
.datetime
.now()).total_seconds()
448 logging
.debug("%ds before timeout", seconds
)
451 raise Exception("Timeout while waiting for guest ssh")
454 self
._guest
.shutdown(timeout
=self
._shutdown
_timeout
)
457 self
._guest
.wait(timeout
=self
._shutdown
_timeout
)
459 def graceful_shutdown(self
):
460 self
.ssh_root(self
.poweroff
)
461 self
._guest
.wait(timeout
=self
._shutdown
_timeout
)
463 def qmp(self
, *args
, **kwargs
):
464 return self
._guest
.qmp(*args
, **kwargs
)
466 def gen_cloud_init_iso(self
):
468 mdata
= open(os
.path
.join(cidir
, "meta-data"), "w")
469 name
= self
.name
.replace(".","-")
470 mdata
.writelines(["instance-id: {}-vm-0\n".format(name
),
471 "local-hostname: {}-guest\n".format(name
)])
473 udata
= open(os
.path
.join(cidir
, "user-data"), "w")
474 print("guest user:pw {}:{}".format(self
._config
['guest_user'],
475 self
._config
['guest_pass']))
476 udata
.writelines(["#cloud-config\n",
479 " root:%s\n" % self
._config
['root_pass'],
480 " %s:%s\n" % (self
._config
['guest_user'],
481 self
._config
['guest_pass']),
484 " - name: %s\n" % self
._config
['guest_user'],
485 " sudo: ALL=(ALL) NOPASSWD:ALL\n",
486 " ssh-authorized-keys:\n",
487 " - %s\n" % self
._config
['ssh_pub_key'],
489 " ssh-authorized-keys:\n",
490 " - %s\n" % self
._config
['ssh_pub_key'],
491 "locale: en_US.UTF-8\n"])
492 proxy
= os
.environ
.get("http_proxy")
493 if not proxy
is None:
494 udata
.writelines(["apt:\n",
495 " proxy: %s" % proxy
])
497 subprocess
.check_call([self
._genisoimage
, "-output", "cloud-init.iso",
498 "-volid", "cidata", "-joliet", "-rock",
499 "user-data", "meta-data"],
501 stdin
=self
._devnull
, stdout
=self
._stdout
,
503 return os
.path
.join(cidir
, "cloud-init.iso")
505 def get_qemu_packages_from_lcitool_json(self
, json_path
=None):
506 """Parse a lcitool variables json file and return the PKGS list."""
507 if json_path
is None:
508 json_path
= os
.path
.join(
509 os
.path
.dirname(__file__
), "generated", self
.name
+ ".json"
511 with
open(json_path
, "r") as fh
:
512 return json
.load(fh
)["pkgs"]
515 def get_qemu_path(arch
, build_path
=None):
516 """Fetch the path to the qemu binary."""
517 # If QEMU environment variable set, it takes precedence
518 if "QEMU" in os
.environ
:
519 qemu_path
= os
.environ
["QEMU"]
521 qemu_path
= os
.path
.join(build_path
, arch
+ "-softmmu")
522 qemu_path
= os
.path
.join(qemu_path
, "qemu-system-" + arch
)
524 # Default is to use system path for qemu.
525 qemu_path
= "qemu-system-" + arch
528 def get_qemu_version(qemu_path
):
529 """Get the version number from the current QEMU,
530 and return the major number."""
531 output
= subprocess
.check_output([qemu_path
, '--version'])
532 version_line
= output
.decode("utf-8")
533 version_num
= re
.split(r
' |\(', version_line
)[3].split('.')[0]
534 return int(version_num
)
536 def parse_config(config
, args
):
537 """ Parse yaml config and populate our config structure.
538 The yaml config allows the user to override the
539 defaults for VM parameters. In many cases these
540 defaults can be overridden without rebuilding the VM."""
542 config_file
= args
.config
543 elif 'QEMU_CONFIG' in os
.environ
:
544 config_file
= os
.environ
['QEMU_CONFIG']
547 if not os
.path
.exists(config_file
):
548 raise Exception("config file {} does not exist".format(config_file
))
549 # We gracefully handle importing the yaml module
550 # since it might not be installed.
551 # If we are here it means the user supplied a .yml file,
552 # so if the yaml module is not installed we will exit with error.
556 print("The python3-yaml package is needed "\
557 "to support config.yaml files")
558 # Instead of raising an exception we exit to avoid
559 # a raft of messy (expected) errors to stdout.
561 with
open(config_file
) as f
:
562 yaml_dict
= yaml
.safe_load(f
)
564 if 'qemu-conf' in yaml_dict
:
565 config
.update(yaml_dict
['qemu-conf'])
567 raise Exception("config file {} is not valid"\
568 " missing qemu-conf".format(config_file
))
571 def parse_args(vmcls
):
573 def get_default_jobs():
574 if multiprocessing
.cpu_count() > 1:
575 if kvm_available(vmcls
.arch
):
576 return multiprocessing
.cpu_count() // 2
577 elif os
.uname().machine
== "x86_64" and \
578 vmcls
.arch
in ["aarch64", "x86_64", "i386"]:
579 # MTTCG is available on these arches and we can allow
580 # more cores. but only up to a reasonable limit. User
581 # can always override these limits with --jobs.
582 return min(multiprocessing
.cpu_count() // 2, 8)
585 parser
= argparse
.ArgumentParser(
586 formatter_class
=argparse
.ArgumentDefaultsHelpFormatter
,
587 description
="Utility for provisioning VMs and running builds",
588 epilog
="""Remaining arguments are passed to the command.
589 Exit codes: 0 = success, 1 = command line error,
590 2 = environment initialization failed,
591 3 = test command failed""")
592 parser
.add_argument("--debug", "-D", action
="store_true",
593 help="enable debug output")
594 parser
.add_argument("--image", "-i", default
="%s.img" % vmcls
.name
,
595 help="image file name")
596 parser
.add_argument("--force", "-f", action
="store_true",
597 help="force build image even if image exists")
598 parser
.add_argument("--jobs", type=int, default
=get_default_jobs(),
599 help="number of virtual CPUs")
600 parser
.add_argument("--verbose", "-V", action
="store_true",
601 help="Pass V=1 to builds within the guest")
602 parser
.add_argument("--build-image", "-b", action
="store_true",
604 parser
.add_argument("--build-qemu",
605 help="build QEMU from source in guest")
606 parser
.add_argument("--build-target",
607 help="QEMU build target", default
="check")
608 parser
.add_argument("--build-path", default
=None,
609 help="Path of build directory, "\
610 "for using build tree QEMU binary. ")
611 parser
.add_argument("--source-path", default
=None,
612 help="Path of source directory, "\
613 "for finding additional files. ")
614 parser
.add_argument("--interactive", "-I", action
="store_true",
615 help="Interactively run command")
616 parser
.add_argument("--snapshot", "-s", action
="store_true",
617 help="run tests with a snapshot")
618 parser
.add_argument("--genisoimage", default
="genisoimage",
619 help="iso imaging tool")
620 parser
.add_argument("--config", "-c", default
=None,
621 help="Provide config yaml for configuration. "\
622 "See config_example.yaml for example.")
623 parser
.add_argument("--efi-aarch64",
624 default
="/usr/share/qemu-efi-aarch64/QEMU_EFI.fd",
625 help="Path to efi image for aarch64 VMs.")
626 parser
.add_argument("--log-console", action
="store_true",
627 help="Log console to file.")
628 parser
.add_argument("commands", nargs
="*", help="""Remaining
629 commands after -- are passed to command inside the VM""")
631 return parser
.parse_args()
633 def main(vmcls
, config
=None):
636 config
= DEFAULT_CONFIG
637 args
= parse_args(vmcls
)
638 if not args
.commands
and not args
.build_qemu
and not args
.build_image
:
639 print("Nothing to do?")
641 config
= parse_config(config
, args
)
642 logging
.basicConfig(level
=(logging
.DEBUG
if args
.debug
644 vm
= vmcls(args
, config
=config
)
646 if os
.path
.exists(args
.image
) and not args
.force
:
647 sys
.stderr
.writelines(["Image file exists: %s\n" % args
.image
,
648 "Use --force option to overwrite\n"])
650 return vm
.build_image(args
.image
)
652 vm
.add_source_dir(args
.build_qemu
)
653 cmd
= [vm
.BUILD_SCRIPT
.format(
654 configure_opts
= " ".join(args
.commands
),
656 target
=args
.build_target
,
657 verbose
= "V=1" if args
.verbose
else "")]
662 img
+= ",snapshot=on"
665 except Exception as e
:
666 if isinstance(e
, SystemExit) and e
.code
== 0:
668 sys
.stderr
.write("Failed to prepare guest environment\n")
669 traceback
.print_exc()
673 if vm
.ssh(*cmd
) != 0:
678 if not args
.snapshot
:
679 vm
.graceful_shutdown()