KVM test: A couple of fixes for unattended install
[autotest-zwu.git] / client / tests / kvm / tests / unattended_install.py
blob1ff73afcd835c6c1e9e3e345fcda797666449d06
1 import logging, time, socket, re, os, shutil, tempfile, glob, ConfigParser
2 import xml.dom.minidom
3 from autotest_lib.client.common_lib import error
4 from autotest_lib.client.bin import utils
5 from autotest_lib.client.virt import virt_vm, virt_utils
8 @error.context_aware
9 def cleanup(dir):
10 """
11 If dir is a mountpoint, do what is possible to unmount it. Afterwards,
12 try to remove it.
14 @param dir: Directory to be cleaned up.
15 """
16 error.context("cleaning up unattended install directory %s" % dir)
17 if os.path.ismount(dir):
18 utils.run('fuser -k %s' % dir, ignore_status=True)
19 utils.run('umount %s' % dir)
20 if os.path.isdir(dir):
21 shutil.rmtree(dir)
24 @error.context_aware
25 def clean_old_image(image):
26 """
27 Clean a leftover image file from previous processes. If it contains a
28 mounted file system, do the proper cleanup procedures.
30 @param image: Path to image to be cleaned up.
31 """
32 error.context("cleaning up old leftover image %s" % image)
33 if os.path.exists(image):
34 mtab = open('/etc/mtab', 'r')
35 mtab_contents = mtab.read()
36 mtab.close()
37 if image in mtab_contents:
38 utils.run('fuser -k %s' % image, ignore_status=True)
39 utils.run('umount %s' % image)
40 os.remove(image)
43 class Disk(object):
44 """
45 Abstract class for Disk objects, with the common methods implemented.
46 """
47 def __init__(self):
48 self.path = None
51 def get_answer_file_path(self, filename):
52 return os.path.join(self.mount, filename)
55 def copy_to(self, src):
56 logging.debug("Copying %s to disk image mount", src)
57 dst = os.path.join(self.mount, os.path.basename(src))
58 if os.path.isdir(src):
59 shutil.copytree(src, dst)
60 elif os.path.isfile(src):
61 shutil.copyfile(src, dst)
64 def close(self):
65 os.chmod(self.path, 0755)
66 cleanup(self.mount)
67 logging.debug("Disk %s successfuly set", self.path)
70 class FloppyDisk(Disk):
71 """
72 Represents a 1.44 MB floppy disk. We can copy files to it, and setup it in
73 convenient ways.
74 """
75 @error.context_aware
76 def __init__(self, path, qemu_img_binary, tmpdir):
77 error.context("Creating unattended install floppy image %s" % path)
78 self.tmpdir = tmpdir
79 self.mount = tempfile.mkdtemp(prefix='floppy_', dir=self.tmpdir)
80 self.virtio_mount = None
81 self.path = path
82 clean_old_image(path)
83 if not os.path.isdir(os.path.dirname(path)):
84 os.makedirs(os.path.dirname(path))
86 try:
87 c_cmd = '%s create -f raw %s 1440k' % (qemu_img_binary, path)
88 utils.run(c_cmd)
89 f_cmd = 'mkfs.msdos -s 1 %s' % path
90 utils.run(f_cmd)
91 m_cmd = 'mount -o loop,rw %s %s' % (path, self.mount)
92 utils.run(m_cmd)
93 except error.CmdError, e:
94 cleanup(self.mount)
95 raise
98 def _copy_virtio_drivers(self, virtio_floppy):
99 """
100 Copy the virtio drivers on the virtio floppy to the install floppy.
102 1) Mount the floppy containing the viostor drivers
103 2) Copy its contents to the root of the install floppy
105 virtio_mount = tempfile.mkdtemp(prefix='virtio_floppy_',
106 dir=self.tmpdir)
108 pwd = os.getcwd()
109 try:
110 m_cmd = 'mount -o loop,ro %s %s' % (virtio_floppy, virtio_mount)
111 utils.run(m_cmd)
112 os.chdir(virtio_mount)
113 path_list = glob.glob('*')
114 for path in path_list:
115 self.copy_to(path)
116 finally:
117 os.chdir(pwd)
118 cleanup(virtio_mount)
121 def setup_virtio_win2003(self, virtio_floppy, virtio_oemsetup_id):
123 Setup the install floppy with the virtio storage drivers, win2003 style.
125 Win2003 and WinXP depend on the file txtsetup.oem file to install
126 the virtio drivers from the floppy, which is a .ini file.
127 Process:
129 1) Copy the virtio drivers on the virtio floppy to the install floppy
130 2) Parse the ini file with config parser
131 3) Modify the identifier of the default session that is going to be
132 executed on the config parser object
133 4) Re-write the config file to the disk
135 self._copy_virtio_drivers(virtio_floppy)
136 txtsetup_oem = os.path.join(self.mount, 'txtsetup.oem')
137 if not os.path.isfile(txtsetup_oem):
138 raise IOError('File txtsetup.oem not found on the install '
139 'floppy. Please verify if your floppy virtio '
140 'driver image has this file')
141 parser = ConfigParser.ConfigParser()
142 parser.read(txtsetup_oem)
143 if not parser.has_section('Defaults'):
144 raise ValueError('File txtsetup.oem does not have the session '
145 '"Defaults". Please check txtsetup.oem')
146 default_driver = parser.get('Defaults', 'SCSI')
147 if default_driver != virtio_oemsetup_id:
148 parser.set('Defaults', 'SCSI', virtio_oemsetup_id)
149 fp = open(txtsetup_oem, 'w')
150 parser.write(fp)
151 fp.close()
154 def setup_virtio_win2008(self, virtio_floppy):
156 Setup the install floppy with the virtio storage drivers, win2008 style.
158 Win2008, Vista and 7 require people to point out the path to the drivers
159 on the unattended file, so we just need to copy the drivers to the
160 driver floppy disk. Important to note that it's possible to specify
161 drivers from a CDROM, so the floppy driver copy is optional.
162 Process:
164 1) Copy the virtio drivers on the virtio floppy to the install floppy,
165 if there is one available
167 if os.path.isfile(virtio_floppy):
168 self._copy_virtio_drivers(virtio_floppy)
169 else:
170 logging.debug("No virtio floppy present, not needed for this OS anyway")
173 class CdromDisk(Disk):
175 Represents a CDROM disk that we can master according to our needs.
177 def __init__(self, path, tmpdir):
178 self.mount = tempfile.mkdtemp(prefix='cdrom_unattended_', dir=tmpdir)
179 self.path = path
180 clean_old_image(path)
181 if not os.path.isdir(os.path.dirname(path)):
182 os.makedirs(os.path.dirname(path))
185 @error.context_aware
186 def close(self):
187 error.context("Creating unattended install CD image %s" % self.path)
188 g_cmd = ('mkisofs -o %s -max-iso9660-filenames '
189 '-relaxed-filenames -D --input-charset iso8859-1 '
190 '%s' % (self.path, self.mount))
191 utils.run(g_cmd)
193 os.chmod(self.path, 0755)
194 cleanup(self.mount)
195 logging.debug("unattended install CD image %s successfuly created",
196 self.path)
199 class UnattendedInstallConfig(object):
201 Creates a floppy disk image that will contain a config file for unattended
202 OS install. The parameters to the script are retrieved from environment
203 variables.
205 def __init__(self, test, params):
207 Sets class atributes from test parameters.
209 @param test: KVM test object.
210 @param params: Dictionary with test parameters.
212 root_dir = test.bindir
213 images_dir = os.path.join(root_dir, 'images')
214 self.deps_dir = os.path.join(root_dir, 'deps')
215 self.unattended_dir = os.path.join(root_dir, 'unattended')
217 attributes = ['kernel_args', 'finish_program', 'cdrom_cd1',
218 'unattended_file', 'medium', 'url', 'kernel', 'initrd',
219 'nfs_server', 'nfs_dir', 'install_virtio', 'floppy',
220 'cdrom_unattended', 'boot_path', 'extra_params',
221 'qemu_img_binary', 'cdkey', 'finish_program']
223 for a in attributes:
224 setattr(self, a, params.get(a, ''))
226 if self.install_virtio == 'yes':
227 v_attributes = ['virtio_floppy', 'virtio_storage_path',
228 'virtio_network_path', 'virtio_oemsetup_id',
229 'virtio_network_installer']
230 for va in v_attributes:
231 setattr(self, va, params.get(va, ''))
233 self.tmpdir = test.tmpdir
235 if getattr(self, 'unattended_file'):
236 self.unattended_file = os.path.join(root_dir, self.unattended_file)
238 if getattr(self, 'finish_program'):
239 self.finish_program = os.path.join(root_dir, self.finish_program)
241 if getattr(self, 'qemu_img_binary'):
242 if not os.path.isfile(getattr(self, 'qemu_img_binary')):
243 self.qemu_img_binary = os.path.join(root_dir,
244 self.qemu_img_binary)
246 if getattr(self, 'cdrom_cd1'):
247 self.cdrom_cd1 = os.path.join(root_dir, self.cdrom_cd1)
248 self.cdrom_cd1_mount = tempfile.mkdtemp(prefix='cdrom_cd1_',
249 dir=self.tmpdir)
250 if self.medium == 'nfs':
251 self.nfs_mount = tempfile.mkdtemp(prefix='nfs_',
252 dir=self.tmpdir)
254 if getattr(self, 'floppy'):
255 self.floppy = os.path.join(root_dir, self.floppy)
256 if not os.path.isdir(os.path.dirname(self.floppy)):
257 os.makedirs(os.path.dirname(self.floppy))
259 self.image_path = os.path.dirname(self.kernel)
262 def answer_kickstart(self, answer_path):
264 Replace KVM_TEST_CDKEY (in the unattended file) with the cdkey
265 provided for this test and replace the KVM_TEST_MEDIUM with
266 the tree url or nfs address provided for this test.
268 @return: Answer file contents
270 contents = open(self.unattended_file).read()
272 dummy_cdkey_re = r'\bKVM_TEST_CDKEY\b'
273 if re.search(dummy_cdkey_re, contents):
274 if self.cdkey:
275 contents = re.sub(dummy_cdkey_re, self.cdkey, contents)
277 dummy_medium_re = r'\bKVM_TEST_MEDIUM\b'
278 if self.medium == "cdrom":
279 content = "cdrom"
280 elif self.medium == "url":
281 content = "url --url %s" % self.url
282 elif self.medium == "nfs":
283 content = "nfs --server=%s --dir=%s" % (self.nfs_server,
284 self.nfs_dir)
285 else:
286 raise ValueError("Unexpected installation medium %s" % self.url)
288 contents = re.sub(dummy_medium_re, content, contents)
290 logging.debug("Unattended install contents:")
291 for line in contents.splitlines():
292 logging.debug(line)
294 utils.open_write_close(answer_path, contents)
297 def answer_windows_ini(self, answer_path):
298 parser = ConfigParser.ConfigParser()
299 parser.read(self.unattended_file)
300 # First, replacing the CDKEY
301 if self.cdkey:
302 parser.set('UserData', 'ProductKey', self.cdkey)
303 else:
304 logging.error("Param 'cdkey' required but not specified for "
305 "this unattended installation")
307 # Now, replacing the virtio network driver path, under double quotes
308 if self.install_virtio == 'yes':
309 parser.set('Unattended', 'OemPnPDriversPath',
310 '"%s"' % self.virtio_nework_path)
311 else:
312 parser.remove_option('Unattended', 'OemPnPDriversPath')
314 # Last, replace the virtio installer command
315 if self.install_virtio == 'yes':
316 driver = self.virtio_network_installer_path
317 else:
318 driver = 'dir'
320 dummy_re = 'KVM_TEST_VIRTIO_NETWORK_INSTALLER'
321 installer = parser.get('GuiRunOnce', 'Command0')
322 if dummy_re in installer:
323 installer = re.sub(dummy_re, driver, installer)
324 parser.set('GuiRunOnce', 'Command0', installer)
326 # Now, writing the in memory config state to the unattended file
327 fp = open(answer_path, 'w')
328 parser.write(fp)
330 # Let's read it so we can debug print the contents
331 fp = open(answer_path, 'r')
332 contents = fp.read()
333 logging.debug("Unattended install contents:")
334 for line in contents.splitlines():
335 logging.debug(line)
336 fp.close()
339 def answer_windows_xml(self, answer_path):
340 doc = xml.dom.minidom.parse(self.unattended_file)
342 if self.cdkey:
343 # First, replacing the CDKEY
344 product_key = doc.getElementsByTagName('ProductKey')[0]
345 key = product_key.getElementsByTagName('Key')[0]
346 key_text = key.childNodes[0]
347 assert key_text.nodeType == doc.TEXT_NODE
348 key_text.data = self.cdkey
349 else:
350 logging.error("Param 'cdkey' required but not specified for "
351 "this unattended installation")
353 # Now, replacing the virtio driver paths or removing the entire
354 # component PnpCustomizationsWinPE Element Node
355 if self.install_virtio == 'yes':
356 paths = doc.getElementsByTagName("Path")
357 values = [self.virtio_storage_path, self.virtio_network_path]
358 for path, value in zip(paths, values):
359 path_text = path.childNodes[0]
360 assert key_text.nodeType == doc.TEXT_NODE
361 path_text.data = value
362 else:
363 settings = doc.getElementsByTagName("settings")
364 for s in settings:
365 for c in s.getElementsByTagName("component"):
366 if (c.getAttribute('name') ==
367 "Microsoft-Windows-PnpCustomizationsWinPE"):
368 s.removeChild(c)
370 # Last but not least important, replacing the virtio installer command
371 command_lines = doc.getElementsByTagName("CommandLine")
372 for command_line in command_lines:
373 command_line_text = command_line.childNodes[0]
374 assert command_line_text.nodeType == doc.TEXT_NODE
375 dummy_re = 'KVM_TEST_VIRTIO_NETWORK_INSTALLER'
376 if (self.install_virtio == 'yes' and
377 hasattr(self, 'virtio_network_installer_path')):
378 driver = self.virtio_network_installer_path
379 else:
380 driver = 'dir'
381 if driver.endswith("msi"):
382 driver = 'msiexec /passive /package ' + driver
383 if dummy_re in command_line_text.data:
384 t = command_line_text.data
385 t = re.sub(dummy_re, driver, t)
386 command_line_text.data = t
388 contents = doc.toxml()
389 logging.debug("Unattended install contents:")
390 for line in contents.splitlines():
391 logging.debug(line)
393 fp = open(answer_path, 'w')
394 doc.writexml(fp)
397 def answer_suse_xml(self, answer_path):
398 # There's nothing to replace on SUSE files to date. Yay!
399 doc = xml.dom.minidom.parse(self.unattended_file)
401 contents = doc.toxml()
402 logging.debug("Unattended install contents:")
403 for line in contents.splitlines():
404 logging.debug(line)
406 fp = open(answer_path, 'w')
407 doc.writexml(fp)
410 def setup_boot_disk(self):
411 if self.unattended_file.endswith('.sif'):
412 dest_fname = 'winnt.sif'
413 setup_file = 'winnt.bat'
414 boot_disk = FloppyDisk(self.floppy, self.qemu_img_binary,
415 self.tmpdir)
416 answer_path = boot_disk.get_answer_file_path(dest_fname)
417 self.answer_windows_ini(answer_path)
418 setup_file_path = os.path.join(self.unattended_dir, setup_file)
419 boot_disk.copy_to(setup_file_path)
420 if self.install_virtio == "yes":
421 boot_disk.setup_virtio_win2003(self.virtio_floppy,
422 self.virtio_oemsetup_id)
423 boot_disk.copy_to(self.finish_program)
425 elif self.unattended_file.endswith('.ks'):
426 # Red Hat kickstart install
427 dest_fname = 'ks.cfg'
428 if self.cdrom_unattended:
429 boot_disk = CdromDisk(self.cdrom_unattended, self.tmpdir)
430 elif self.floppy:
431 boot_disk = FloppyDisk(self.floppy, self.qemu_img_binary,
432 self.tmpdir)
433 else:
434 raise ValueError("Neither cdrom_unattended nor floppy set "
435 "on the config file, please verify")
436 answer_path = boot_disk.get_answer_file_path(dest_fname)
437 self.answer_kickstart(answer_path)
439 elif self.unattended_file.endswith('.xml'):
440 if "autoyast" in self.extra_params:
441 # SUSE autoyast install
442 dest_fname = "autoinst.xml"
443 if self.cdrom_unattended:
444 boot_disk = CdromDisk(self.cdrom_unattended, self.tmpdir)
445 elif self.floppy:
446 boot_disk = FloppyDisk(self.floppy, self.qemu_img_binary,
447 self.tmpdir)
448 else:
449 raise ValueError("Neither cdrom_unattended nor floppy set "
450 "on the config file, please verify")
451 answer_path = boot_disk.get_answer_file_path(dest_fname)
452 self.answer_suse_xml(answer_path)
454 else:
455 # Windows unattended install
456 dest_fname = "autounattend.xml"
457 boot_disk = FloppyDisk(self.floppy, self.qemu_img_binary,
458 self.tmpdir)
459 answer_path = boot_disk.get_answer_file_path(dest_fname)
460 self.answer_windows_xml(answer_path)
462 if self.install_virtio == "yes":
463 boot_disk.setup_virtio_win2008(self.virtio_floppy)
464 boot_disk.copy_to(self.finish_program)
466 else:
467 raise ValueError('Unknown answer file type: %s' %
468 self.unattended_file)
470 boot_disk.close()
473 @error.context_aware
474 def setup_cdrom(self):
476 Mount cdrom and copy vmlinuz and initrd.img.
478 error.context("Copying vmlinuz and initrd.img from install cdrom %s" %
479 self.cdrom_cd1)
480 m_cmd = ('mount -t iso9660 -v -o loop,ro %s %s' %
481 (self.cdrom_cd1, self.cdrom_cd1_mount))
482 utils.run(m_cmd)
484 try:
485 if not os.path.isdir(self.image_path):
486 os.makedirs(self.image_path)
487 kernel_fetch_cmd = ("cp %s/%s/%s %s" %
488 (self.cdrom_cd1_mount, self.boot_path,
489 os.path.basename(self.kernel), self.kernel))
490 utils.run(kernel_fetch_cmd)
491 initrd_fetch_cmd = ("cp %s/%s/%s %s" %
492 (self.cdrom_cd1_mount, self.boot_path,
493 os.path.basename(self.initrd), self.initrd))
494 utils.run(initrd_fetch_cmd)
495 finally:
496 cleanup(self.cdrom_cd1_mount)
499 @error.context_aware
500 def setup_url(self):
502 Download the vmlinuz and initrd.img from URL.
504 error.context("downloading vmlinuz and initrd.img from %s" % self.url)
505 os.chdir(self.image_path)
506 kernel_fetch_cmd = "wget -q %s/%s/%s" % (self.url, self.boot_path,
507 os.path.basename(self.kernel))
508 initrd_fetch_cmd = "wget -q %s/%s/%s" % (self.url, self.boot_path,
509 os.path.basename(self.initrd))
511 if os.path.exists(self.kernel):
512 os.remove(self.kernel)
513 if os.path.exists(self.initrd):
514 os.remove(self.initrd)
516 utils.run(kernel_fetch_cmd)
517 utils.run(initrd_fetch_cmd)
520 def setup_nfs(self):
522 Copy the vmlinuz and initrd.img from nfs.
524 error.context("copying the vmlinuz and initrd.img from NFS share")
526 m_cmd = ("mount %s:%s %s -o ro" %
527 (self.nfs_server, self.nfs_dir, self.nfs_mount))
528 utils.run(m_cmd)
530 try:
531 kernel_fetch_cmd = ("cp %s/%s/%s %s" %
532 (self.nfs_mount, self.boot_path,
533 os.path.basename(self.kernel), self.image_path))
534 utils.run(kernel_fetch_cmd)
535 initrd_fetch_cmd = ("cp %s/%s/%s %s" %
536 (self.nfs_mount, self.boot_path,
537 os.path.basename(self.initrd), self.image_path))
538 utils.run(initrd_fetch_cmd)
539 finally:
540 cleanup(self.nfs_mount)
543 def setup(self):
545 Configure the environment for unattended install.
547 Uses an appropriate strategy according to each install model.
549 logging.info("Starting unattended install setup")
550 virt_utils.display_attributes(self)
552 if self.unattended_file and (self.floppy or self.cdrom_unattended):
553 self.setup_boot_disk()
554 if self.medium == "cdrom":
555 if self.kernel and self.initrd:
556 self.setup_cdrom()
557 elif self.medium == "url":
558 self.setup_url()
559 elif self.medium == "nfs":
560 self.setup_nfs()
561 else:
562 raise ValueError("Unexpected installation method %s" %
563 self.medium)
566 @error.context_aware
567 def run_unattended_install(test, params, env):
569 Unattended install test:
570 1) Starts a VM with an appropriated setup to start an unattended OS install.
571 2) Wait until the install reports to the install watcher its end.
573 @param test: KVM test object.
574 @param params: Dictionary with the test parameters.
575 @param env: Dictionary with test environment.
577 unattended_install_config = UnattendedInstallConfig(test, params)
578 unattended_install_config.setup()
579 vm = env.get_vm(params["main_vm"])
580 vm.create()
582 install_timeout = int(params.get("timeout", 3000))
583 post_install_delay = int(params.get("post_install_delay", 0))
584 port = vm.get_port(int(params.get("guest_port_unattended_install")))
586 migrate_background = params.get("migrate_background") == "yes"
587 if migrate_background:
588 mig_timeout = float(params.get("mig_timeout", "3600"))
589 mig_protocol = params.get("migration_protocol", "tcp")
591 logging.info("Waiting for installation to finish. Timeout set to %d s "
592 "(%d min)", install_timeout, install_timeout/60)
593 error.context("waiting for installation to finish")
595 start_time = time.time()
596 while (time.time() - start_time) < install_timeout:
597 try:
598 vm.verify_alive()
599 except virt_vm.VMDeadError, e:
600 if params.get("wait_no_ack", "no") == "yes":
601 break
602 else:
603 raise e
604 vm.verify_kernel_crash()
605 if params.get("wait_no_ack", "no") == "no":
606 client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
607 try:
608 client.connect((vm.get_address(), port))
609 if client.recv(1024) == "done":
610 break
611 except (socket.error, virt_vm.VMAddressError):
612 pass
614 if migrate_background:
615 # Drop the params which may break the migration
616 # Better method is to use dnsmasq to do the
617 # unattended installation
618 if vm.params.get("initrd"):
619 vm.params["initrd"] = None
620 if vm.params.get("kernel"):
621 vm.params["kernel"] = None
622 if vm.params.get("extra_params"):
623 vm.params["extra_params"] = re.sub("--append '.*'", "",
624 vm.params["extra_params"])
625 vm.migrate(timeout=mig_timeout, protocol=mig_protocol)
626 else:
627 time.sleep(1)
628 if params.get("wait_no_ack", "no") == "no":
629 client.close()
630 else:
631 raise error.TestFail("Timeout elapsed while waiting for install to "
632 "finish")
634 time_elapsed = time.time() - start_time
635 logging.info("Guest reported successful installation after %d s (%d min)",
636 time_elapsed, time_elapsed/60)
638 if params.get("shutdown_cleanly", "yes") == "yes":
639 shutdown_cleanly_timeout = int(params.get("shutdown_cleanly_timeout",
640 120))
641 logging.info("Wait for guest to shutdown cleanly")
642 if virt_utils.wait_for(vm.is_dead, shutdown_cleanly_timeout, 1, 1):
643 logging.info("Guest managed to shutdown cleanly")