1import logging, time, socket, re, os, shutil, tempfile, glob, ConfigParser 2import xml.dom.minidom 3from autotest_lib.client.common_lib import error 4from autotest_lib.client.bin import utils 5from autotest_lib.client.virt import virt_vm, virt_utils 6 7 8@error.context_aware 9def cleanup(dir): 10 """ 11 If dir is a mountpoint, do what is possible to unmount it. Afterwards, 12 try to remove it. 13 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) 22 23 24@error.context_aware 25def 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. 29 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) 41 42 43class Disk(object): 44 """ 45 Abstract class for Disk objects, with the common methods implemented. 46 """ 47 def __init__(self): 48 self.path = None 49 50 51 def get_answer_file_path(self, filename): 52 return os.path.join(self.mount, filename) 53 54 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) 62 63 64 def close(self): 65 os.chmod(self.path, 0755) 66 cleanup(self.mount) 67 logging.debug("Disk %s successfuly set", self.path) 68 69 70class 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)) 85 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 96 97 98 def _copy_virtio_drivers(self, virtio_floppy): 99 """ 100 Copy the virtio drivers on the virtio floppy to the install floppy. 101 102 1) Mount the floppy containing the viostor drivers 103 2) Copy its contents to the root of the install floppy 104 """ 105 virtio_mount = tempfile.mkdtemp(prefix='virtio_floppy_', 106 dir=self.tmpdir) 107 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) 119 120 121 def setup_virtio_win2003(self, virtio_floppy, virtio_oemsetup_id): 122 """ 123 Setup the install floppy with the virtio storage drivers, win2003 style. 124 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: 128 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 134 """ 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() 152 153 154 def setup_virtio_win2008(self, virtio_floppy): 155 """ 156 Setup the install floppy with the virtio storage drivers, win2008 style. 157 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: 163 164 1) Copy the virtio drivers on the virtio floppy to the install floppy, 165 if there is one available 166 """ 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") 171 172 173class CdromDisk(Disk): 174 """ 175 Represents a CDROM disk that we can master according to our needs. 176 """ 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)) 183 184 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) 192 193 os.chmod(self.path, 0755) 194 cleanup(self.mount) 195 logging.debug("unattended install CD image %s successfuly created", 196 self.path) 197 198 199class UnattendedInstallConfig(object): 200 """ 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. 204 """ 205 def __init__(self, test, params): 206 """ 207 Sets class atributes from test parameters. 208 209 @param test: KVM test object. 210 @param params: Dictionary with test parameters. 211 """ 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') 216 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'] 222 223 for a in attributes: 224 setattr(self, a, params.get(a, '')) 225 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, '')) 232 233 self.tmpdir = test.tmpdir 234 235 if getattr(self, 'unattended_file'): 236 self.unattended_file = os.path.join(root_dir, self.unattended_file) 237 238 if getattr(self, 'finish_program'): 239 self.finish_program = os.path.join(root_dir, self.finish_program) 240 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) 245 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) 253 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)) 258 259 self.image_path = os.path.dirname(self.kernel) 260 261 262 def answer_kickstart(self, answer_path): 263 """ 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. 267 268 @return: Answer file contents 269 """ 270 contents = open(self.unattended_file).read() 271 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) 276 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) 287 288 contents = re.sub(dummy_medium_re, content, contents) 289 290 logging.debug("Unattended install contents:") 291 for line in contents.splitlines(): 292 logging.debug(line) 293 294 utils.open_write_close(answer_path, contents) 295 296 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") 306 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') 313 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' 319 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) 325 326 # Now, writing the in memory config state to the unattended file 327 fp = open(answer_path, 'w') 328 parser.write(fp) 329 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() 337 338 339 def answer_windows_xml(self, answer_path): 340 doc = xml.dom.minidom.parse(self.unattended_file) 341 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") 352 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) 369 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 387 388 contents = doc.toxml() 389 logging.debug("Unattended install contents:") 390 for line in contents.splitlines(): 391 logging.debug(line) 392 393 fp = open(answer_path, 'w') 394 doc.writexml(fp) 395 396 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) 400 401 contents = doc.toxml() 402 logging.debug("Unattended install contents:") 403 for line in contents.splitlines(): 404 logging.debug(line) 405 406 fp = open(answer_path, 'w') 407 doc.writexml(fp) 408 409 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) 424 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) 438 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) 453 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) 461 462 if self.install_virtio == "yes": 463 boot_disk.setup_virtio_win2008(self.virtio_floppy) 464 boot_disk.copy_to(self.finish_program) 465 466 else: 467 raise ValueError('Unknown answer file type: %s' % 468 self.unattended_file) 469 470 boot_disk.close() 471 472 473 @error.context_aware 474 def setup_cdrom(self): 475 """ 476 Mount cdrom and copy vmlinuz and initrd.img. 477 """ 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) 483 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) 497 498 499 @error.context_aware 500 def setup_url(self): 501 """ 502 Download the vmlinuz and initrd.img from URL. 503 """ 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)) 510 511 if os.path.exists(self.kernel): 512 os.remove(self.kernel) 513 if os.path.exists(self.initrd): 514 os.remove(self.initrd) 515 516 utils.run(kernel_fetch_cmd) 517 utils.run(initrd_fetch_cmd) 518 519 520 def setup_nfs(self): 521 """ 522 Copy the vmlinuz and initrd.img from nfs. 523 """ 524 error.context("copying the vmlinuz and initrd.img from NFS share") 525 526 m_cmd = ("mount %s:%s %s -o ro" % 527 (self.nfs_server, self.nfs_dir, self.nfs_mount)) 528 utils.run(m_cmd) 529 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) 541 542 543 def setup(self): 544 """ 545 Configure the environment for unattended install. 546 547 Uses an appropriate strategy according to each install model. 548 """ 549 logging.info("Starting unattended install setup") 550 virt_utils.display_attributes(self) 551 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) 564 565 566@error.context_aware 567def run_unattended_install(test, params, env): 568 """ 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. 572 573 @param test: KVM test object. 574 @param params: Dictionary with the test parameters. 575 @param env: Dictionary with test environment. 576 """ 577 unattended_install_config = UnattendedInstallConfig(test, params) 578 unattended_install_config.setup() 579 vm = env.get_vm(params["main_vm"]) 580 vm.create() 581 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"))) 585 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") 590 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") 594 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 613 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") 633 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) 637 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") 644