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