| #!/usr/bin/python |
| """ |
| Simple script to setup unattended installs on KVM guests. |
| """ |
| # -*- coding: utf-8 -*- |
| import os, sys, shutil, tempfile, re, ConfigParser, glob, inspect |
| import common |
| |
| |
| SCRIPT_DIR = os.path.dirname(sys.modules[__name__].__file__) |
| KVM_TEST_DIR = os.path.abspath(os.path.join(SCRIPT_DIR, "..")) |
| |
| |
| class SetupError(Exception): |
| """ |
| Simple wrapper for the builtin Exception class. |
| """ |
| pass |
| |
| |
| def find_command(cmd): |
| """ |
| Searches for a command on common paths, error if it can't find it. |
| |
| @param cmd: Command to be found. |
| """ |
| for dir in ["/usr/local/sbin", "/usr/local/bin", |
| "/usr/sbin", "/usr/bin", "/sbin", "/bin"]: |
| file = os.path.join(dir, cmd) |
| if os.path.exists(file): |
| return file |
| raise ValueError('Missing command: %s' % cmd) |
| |
| |
| def run(cmd, info=None): |
| """ |
| Run a command and throw an exception if it fails. |
| Optionally, you can provide additional contextual info. |
| |
| @param cmd: Command string. |
| @param reason: Optional string that explains the context of the failure. |
| |
| @raise: SetupError if command fails. |
| """ |
| print "Running '%s'" % cmd |
| cmd_name = cmd.split(' ')[0] |
| find_command(cmd_name) |
| if os.system(cmd): |
| e_msg = 'Command failed: %s' % cmd |
| if info is not None: |
| e_msg += '. %s' % info |
| raise SetupError(e_msg) |
| |
| |
| def cleanup(dir): |
| """ |
| If dir is a mountpoint, do what is possible to unmount it. Afterwards, |
| try to remove it. |
| |
| @param dir: Directory to be cleaned up. |
| """ |
| print "Cleaning up directory %s" % dir |
| if os.path.ismount(dir): |
| os.system('fuser -k %s' % dir) |
| run('umount %s' % dir, info='Could not unmount %s' % dir) |
| if os.path.isdir(dir): |
| shutil.rmtree(dir) |
| |
| |
| def clean_old_image(image): |
| """ |
| Clean a leftover image file from previous processes. If it contains a |
| mounted file system, do the proper cleanup procedures. |
| |
| @param image: Path to image to be cleaned up. |
| """ |
| if os.path.exists(image): |
| mtab = open('/etc/mtab', 'r') |
| mtab_contents = mtab.read() |
| mtab.close() |
| if image in mtab_contents: |
| os.system('fuser -k %s' % image) |
| os.system('umount %s' % image) |
| os.remove(image) |
| |
| |
| class Disk(object): |
| """ |
| Abstract class for Disk objects, with the common methods implemented. |
| """ |
| def __init__(self): |
| self.path = None |
| |
| |
| def setup_answer_file(self, filename, contents): |
| answer_file = open(os.path.join(self.mount, filename), 'w') |
| answer_file.write(contents) |
| answer_file.close() |
| |
| |
| def copy_to(self, src): |
| dst = os.path.join(self.mount, os.path.basename(src)) |
| if os.path.isdir(src): |
| shutil.copytree(src, dst) |
| elif os.path.isfile(src): |
| shutil.copyfile(src, dst) |
| |
| |
| def close(self): |
| os.chmod(self.path, 0755) |
| cleanup(self.mount) |
| print "Disk %s successfuly set" % self.path |
| |
| |
| class FloppyDisk(Disk): |
| """ |
| Represents a 1.44 MB floppy disk. We can copy files to it, and setup it in |
| convenient ways. |
| """ |
| def __init__(self, path): |
| print "Creating floppy unattended image %s" % path |
| try: |
| qemu_img_binary = os.environ['KVM_TEST_qemu_img_binary'] |
| except KeyError: |
| qemu_img_binary = os.path.join(KVM_TEST_DIR, qemu_img_binary) |
| if not os.path.exists(qemu_img_binary): |
| raise SetupError('The qemu-img binary that is supposed to be used ' |
| '(%s) does not exist. Please verify your ' |
| 'configuration' % qemu_img_binary) |
| |
| self.mount = tempfile.mkdtemp(prefix='floppy_', dir='/tmp') |
| self.virtio_mount = None |
| self.path = path |
| clean_old_image(path) |
| if not os.path.isdir(os.path.dirname(path)): |
| os.makedirs(os.path.dirname(path)) |
| |
| try: |
| c_cmd = '%s create -f raw %s 1440k' % (qemu_img_binary, path) |
| run(c_cmd, info='Could not create floppy image') |
| f_cmd = 'mkfs.msdos -s 1 %s' % path |
| run(f_cmd, info='Error formatting floppy image') |
| m_cmd = 'mount -o loop,rw %s %s' % (path, self.mount) |
| run(m_cmd, info='Could not mount floppy image') |
| except: |
| cleanup(self.mount) |
| |
| |
| def _copy_virtio_drivers(self, virtio_floppy): |
| """ |
| Copy the virtio drivers on the virtio floppy to the install floppy. |
| |
| 1) Mount the floppy containing the viostor drivers |
| 2) Copy its contents to the root of the install floppy |
| """ |
| virtio_mount = tempfile.mkdtemp(prefix='virtio_floppy_', dir='/tmp') |
| |
| pwd = os.getcwd() |
| try: |
| m_cmd = 'mount -o loop %s %s' % (virtio_floppy, virtio_mount) |
| run(m_cmd, info='Could not mount virtio floppy driver') |
| os.chdir(virtio_mount) |
| path_list = glob.glob('*') |
| for path in path_list: |
| self.copy_to(path) |
| finally: |
| os.chdir(pwd) |
| cleanup(virtio_mount) |
| |
| |
| def setup_virtio_win2003(self, virtio_floppy, virtio_oemsetup_id): |
| """ |
| Setup the install floppy with the virtio storage drivers, win2003 style. |
| |
| Win2003 and WinXP depend on the file txtsetup.oem file to install |
| the virtio drivers from the floppy, which is a .ini file. |
| Process: |
| |
| 1) Copy the virtio drivers on the virtio floppy to the install floppy |
| 2) Parse the ini file with config parser |
| 3) Modify the identifier of the default session that is going to be |
| executed on the config parser object |
| 4) Re-write the config file to the disk |
| """ |
| self._copy_virtio_drivers(virtio_floppy) |
| txtsetup_oem = os.path.join(self.mount, 'txtsetup.oem') |
| if not os.path.isfile(txtsetup_oem): |
| raise SetupError('File txtsetup.oem not found on the install ' |
| 'floppy. Please verify if your floppy virtio ' |
| 'driver image has this file') |
| parser = ConfigParser.ConfigParser() |
| parser.read(txtsetup_oem) |
| if not parser.has_section('Defaults'): |
| raise SetupError('File txtsetup.oem does not have the session ' |
| '"Defaults". Please check txtsetup.oem') |
| default_driver = parser.get('Defaults', 'SCSI') |
| if default_driver != virtio_oemsetup_id: |
| parser.set('Defaults', 'SCSI', virtio_oemsetup_id) |
| fp = open(txtsetup_oem, 'w') |
| parser.write(fp) |
| fp.close() |
| |
| |
| def setup_virtio_win2008(self, virtio_floppy): |
| """ |
| Setup the install floppy with the virtio storage drivers, win2008 style. |
| |
| Win2008, Vista and 7 require people to point out the path to the drivers |
| on the unattended file, so we just need to copy the drivers to the |
| driver floppy disk. |
| Process: |
| |
| 1) Copy the virtio drivers on the virtio floppy to the install floppy |
| """ |
| self._copy_virtio_drivers(virtio_floppy) |
| |
| |
| class CdromDisk(Disk): |
| """ |
| Represents a CDROM disk that we can master according to our needs. |
| """ |
| def __init__(self, path): |
| print "Creating ISO unattended image %s" % path |
| self.mount = tempfile.mkdtemp(prefix='cdrom_unattended_', dir='/tmp') |
| self.path = path |
| clean_old_image(path) |
| if not os.path.isdir(os.path.dirname(path)): |
| os.makedirs(os.path.dirname(path)) |
| |
| |
| def close(self): |
| g_cmd = ('mkisofs -o %s -max-iso9660-filenames ' |
| '-relaxed-filenames -D --input-charset iso8859-1 ' |
| '%s' % (self.path, self.mount)) |
| run(g_cmd, info='Could not generate iso with answer file') |
| |
| os.chmod(self.path, 0755) |
| cleanup(self.mount) |
| print "Disk %s successfuly set" % self.path |
| |
| |
| class UnattendedInstall(object): |
| """ |
| Creates a floppy disk image that will contain a config file for unattended |
| OS install. Optionally, sets up a PXE install server using qemu built in |
| TFTP and DHCP servers to install a particular operating system. The |
| parameters to the script are retrieved from environment variables. |
| """ |
| def __init__(self): |
| """ |
| Gets params from environment variables and sets class attributes. |
| """ |
| images_dir = os.path.join(KVM_TEST_DIR, 'images') |
| self.deps_dir = os.path.join(KVM_TEST_DIR, 'deps') |
| self.unattended_dir = os.path.join(KVM_TEST_DIR, 'unattended') |
| |
| attributes = ['kernel_args', 'finish_program', 'cdrom_cd1', |
| 'unattended_file', 'medium', 'url', 'kernel', 'initrd', |
| 'nfs_server', 'nfs_dir', 'pxe_dir', 'pxe_image', |
| 'pxe_initrd', 'install_virtio', 'tftp', |
| 'floppy', 'cdrom_unattended'] |
| for a in attributes: |
| self._setattr(a) |
| |
| if self.install_virtio == 'yes': |
| v_attributes = ['virtio_floppy', 'virtio_storage_path', |
| 'virtio_network_path', 'virtio_oemsetup_id', |
| 'virtio_network_installer'] |
| for va in v_attributes: |
| self._setattr(va) |
| |
| # Silly attribution just to calm pylint down... |
| self.tftp = self.tftp |
| if self.tftp: |
| self.tftp = os.path.join(KVM_TEST_DIR, self.tftp) |
| if not os.path.isdir(self.tftp): |
| os.makedirs(self.tftp) |
| |
| if self.cdrom_cd1: |
| self.cdrom_cd1 = os.path.join(KVM_TEST_DIR, self.cdrom_cd1) |
| self.cdrom_cd1_mount = tempfile.mkdtemp(prefix='cdrom_cd1_', dir='/tmp') |
| if self.medium == 'nfs': |
| self.nfs_mount = tempfile.mkdtemp(prefix='nfs_', dir='/tmp') |
| |
| if self.floppy: |
| self.floppy = os.path.join(KVM_TEST_DIR, self.floppy) |
| if not os.path.isdir(os.path.dirname(self.floppy)): |
| os.makedirs(os.path.dirname(self.floppy)) |
| |
| self.image_path = KVM_TEST_DIR |
| self.kernel_path = os.path.join(self.image_path, self.kernel) |
| self.initrd_path = os.path.join(self.image_path, self.initrd) |
| |
| |
| def _setattr(self, key): |
| """ |
| Populate class attributes with contents of environment variables. |
| |
| Example: KVM_TEST_medium will populate self.medium. |
| |
| @param key: Name of the class attribute we desire to have. |
| """ |
| env_name = 'KVM_TEST_%s' % key |
| value = os.environ.get(env_name, '') |
| setattr(self, key, value) |
| |
| |
| def render_answer_file(self): |
| # Replace KVM_TEST_CDKEY (in the unattended file) with the cdkey |
| # provided for this test and replace the KVM_TEST_MEDIUM with |
| # the tree url or nfs address provided for this test. |
| unattended_contents = open(self.unattended_file).read() |
| dummy_cdkey_re = r'\bKVM_TEST_CDKEY\b' |
| real_cdkey = os.environ.get('KVM_TEST_cdkey') |
| if re.search(dummy_cdkey_re, unattended_contents): |
| if real_cdkey: |
| unattended_contents = re.sub(dummy_cdkey_re, real_cdkey, |
| unattended_contents) |
| else: |
| print ("WARNING: 'cdkey' required but not specified for " |
| "this unattended installation") |
| |
| dummy_medium_re = r'\bKVM_TEST_MEDIUM\b' |
| if self.medium == "cdrom": |
| content = "cdrom" |
| elif self.medium == "url": |
| content = "url --url %s" % self.url |
| elif self.medium == "nfs": |
| content = "nfs --server=%s --dir=%s" % (self.nfs_server, |
| self.nfs_dir) |
| else: |
| raise SetupError("Unexpected installation medium %s" % self.url) |
| |
| unattended_contents = re.sub(dummy_medium_re, content, |
| unattended_contents) |
| |
| def replace_virtio_key(contents, dummy_re, env): |
| """ |
| Replace a virtio dummy string with contents. |
| |
| If install_virtio is not set, replace it with a dummy string. |
| |
| @param contents: Contents of the unattended file |
| @param dummy_re: Regular expression used to search on the. |
| unattended file contents. |
| @param env: Name of the environment variable. |
| """ |
| dummy_path = "C:" |
| driver = os.environ.get(env, '') |
| |
| if re.search(dummy_re, contents): |
| if self.install_virtio == "yes": |
| if driver.endswith("msi"): |
| driver = 'msiexec /passive /package ' + driver |
| else: |
| try: |
| # Let's escape windows style paths properly |
| drive, path = driver.split(":") |
| driver = drive + ":" + re.escape(path) |
| except: |
| pass |
| contents = re.sub(dummy_re, driver, contents) |
| else: |
| contents = re.sub(dummy_re, dummy_path, contents) |
| return contents |
| |
| vdict = {r'\bKVM_TEST_STORAGE_DRIVER_PATH\b': |
| 'KVM_TEST_virtio_storage_path', |
| r'\bKVM_TEST_NETWORK_DRIVER_PATH\b': |
| 'KVM_TEST_virtio_network_path', |
| r'\bKVM_TEST_VIRTIO_NETWORK_INSTALLER\b': |
| 'KVM_TEST_virtio_network_installer_path'} |
| |
| for vkey in vdict: |
| unattended_contents = replace_virtio_key(unattended_contents, |
| vkey, vdict[vkey]) |
| |
| print "Unattended install contents:" |
| print unattended_contents |
| return unattended_contents |
| |
| |
| def setup_boot_disk(self): |
| answer_contents = self.render_answer_file() |
| |
| if self.unattended_file.endswith('.sif'): |
| dest_fname = 'winnt.sif' |
| setup_file = 'winnt.bat' |
| boot_disk = FloppyDisk(self.floppy) |
| boot_disk.setup_answer_file(dest_fname, answer_contents) |
| setup_file_path = os.path.join(self.unattended_dir, setup_file) |
| boot_disk.copy_to(setup_file_path) |
| if self.install_virtio == "yes": |
| boot_disk.setup_virtio_win2003(self.virtio_floppy, |
| self.virtio_oemsetup_id) |
| boot_disk.copy_to(self.finish_program) |
| |
| elif self.unattended_file.endswith('.ks'): |
| # Red Hat kickstart install |
| dest_fname = 'ks.cfg' |
| if self.cdrom_unattended: |
| boot_disk = CdromDisk(self.cdrom_unattended) |
| elif self.floppy: |
| boot_disk = FloppyDisk(self.floppy) |
| else: |
| raise SetupError("Neither cdrom_unattended nor floppy set " |
| "on the config file, please verify") |
| boot_disk.setup_answer_file(dest_fname, answer_contents) |
| |
| elif self.unattended_file.endswith('.xml'): |
| if self.tftp: |
| # SUSE autoyast install |
| dest_fname = "autoinst.xml" |
| if self.cdrom_unattended: |
| boot_disk = CdromDisk(self.cdrom_unattended) |
| elif self.floppy: |
| boot_disk = FloppyDisk(self.floppy) |
| else: |
| raise SetupError("Neither cdrom_unattended nor floppy set " |
| "on the config file, please verify") |
| boot_disk.setup_answer_file(dest_fname, answer_contents) |
| |
| else: |
| # Windows unattended install |
| dest_fname = "autounattend.xml" |
| boot_disk = FloppyDisk(self.floppy) |
| boot_disk.setup_answer_file(dest_fname, answer_contents) |
| if self.install_virtio == "yes": |
| boot_disk.setup_virtio_win2008(self.virtio_floppy) |
| boot_disk.copy_to(self.finish_program) |
| |
| else: |
| raise SetupError('Unknown answer file %s' % |
| self.unattended_file) |
| |
| boot_disk.close() |
| |
| |
| def setup_pxe_boot(self): |
| """ |
| Sets up a PXE boot environment using the built in qemu TFTP server. |
| Copies the PXE Linux bootloader pxelinux.0 from the host (needs the |
| pxelinux package or equivalent for your distro), and vmlinuz and |
| initrd.img files from the CD to a directory that qemu will serve trough |
| TFTP to the VM. |
| """ |
| print "Setting up PXE boot using TFTP root %s" % self.tftp |
| |
| pxe_file = None |
| pxe_paths = ['/usr/lib/syslinux/pxelinux.0', |
| '/usr/share/syslinux/pxelinux.0'] |
| for path in pxe_paths: |
| if os.path.isfile(path): |
| pxe_file = path |
| break |
| |
| if not pxe_file: |
| raise SetupError('Cannot find PXE boot loader pxelinux.0. Make ' |
| 'sure pxelinux or equivalent package for your ' |
| 'distro is installed.') |
| |
| pxe_dest = os.path.join(self.tftp, 'pxelinux.0') |
| shutil.copyfile(pxe_file, pxe_dest) |
| |
| try: |
| m_cmd = ('mount -t iso9660 -v -o loop,ro %s %s' % |
| (self.cdrom_cd1, self.cdrom_cd1_mount)) |
| run(m_cmd, info='Could not mount CD image %s.' % self.cdrom_cd1) |
| |
| pxe_dir = os.path.join(self.cdrom_cd1_mount, self.pxe_dir) |
| pxe_image = os.path.join(pxe_dir, self.pxe_image) |
| pxe_initrd = os.path.join(pxe_dir, self.pxe_initrd) |
| |
| if not os.path.isdir(pxe_dir): |
| raise SetupError('The ISO image does not have a %s dir. The ' |
| 'script assumes that the cd has a %s dir ' |
| 'where to search for the vmlinuz image.' % |
| (self.pxe_dir, self.pxe_dir)) |
| |
| if not os.path.isfile(pxe_image) or not os.path.isfile(pxe_initrd): |
| raise SetupError('The location %s is lacking either a vmlinuz ' |
| 'or a initrd.img file. Cannot find a PXE ' |
| 'image to proceed.' % self.pxe_dir) |
| |
| tftp_image = os.path.join(self.tftp, 'vmlinuz') |
| tftp_initrd = os.path.join(self.tftp, 'initrd.img') |
| shutil.copyfile(pxe_image, tftp_image) |
| shutil.copyfile(pxe_initrd, tftp_initrd) |
| |
| finally: |
| cleanup(self.cdrom_cd1_mount) |
| |
| pxe_config_dir = os.path.join(self.tftp, 'pxelinux.cfg') |
| if not os.path.isdir(pxe_config_dir): |
| os.makedirs(pxe_config_dir) |
| pxe_config_path = os.path.join(pxe_config_dir, 'default') |
| |
| pxe_config = open(pxe_config_path, 'w') |
| pxe_config.write('DEFAULT pxeboot\n') |
| pxe_config.write('TIMEOUT 20\n') |
| pxe_config.write('PROMPT 0\n') |
| pxe_config.write('LABEL pxeboot\n') |
| pxe_config.write(' KERNEL vmlinuz\n') |
| pxe_config.write(' APPEND initrd=initrd.img %s\n' % |
| self.kernel_args) |
| pxe_config.close() |
| |
| print "PXE boot successfuly set" |
| |
| |
| def setup_url(self): |
| """ |
| Download the vmlinuz and initrd.img from URL. |
| """ |
| print "Downloading the vmlinuz and initrd.img" |
| os.chdir(self.image_path) |
| |
| kernel_fetch_cmd = "wget -q %s/isolinux/%s" % (self.url, self.kernel) |
| initrd_fetch_cmd = "wget -q %s/isolinux/%s" % (self.url, self.initrd) |
| |
| if os.path.exists(self.kernel): |
| os.unlink(self.kernel) |
| if os.path.exists(self.initrd): |
| os.unlink(self.initrd) |
| |
| run(kernel_fetch_cmd, info="Could not fetch vmlinuz from %s" % self.url) |
| run(initrd_fetch_cmd, info=("Could not fetch initrd.img from %s" % |
| self.url)) |
| print "Download of vmlinuz and initrd.img finished" |
| |
| |
| def setup_nfs(self): |
| """ |
| Copy the vmlinuz and initrd.img from nfs. |
| """ |
| print "Copying the vmlinuz and initrd.img from nfs" |
| |
| m_cmd = ("mount %s:%s %s -o ro" % |
| (self.nfs_server, self.nfs_dir, self.nfs_mount)) |
| run(m_cmd, info='Could not mount nfs server') |
| |
| try: |
| kernel_fetch_cmd = ("cp %s/isolinux/%s %s" % |
| (self.nfs_mount, self.kernel, self.image_path)) |
| run(kernel_fetch_cmd, info=("Could not copy the vmlinuz from %s" % |
| self.nfs_mount)) |
| initrd_fetch_cmd = ("cp %s/isolinux/%s %s" % |
| (self.nfs_mount, self.initrd, self.image_path)) |
| run(initrd_fetch_cmd, info=("Could not copy the initrd.img from " |
| "%s" % self.nfs_mount)) |
| finally: |
| cleanup(self.nfs_mount) |
| |
| |
| def setup(self): |
| """ |
| Configure the environment for unattended install. |
| |
| Uses an appropriate strategy according to each install model. |
| """ |
| print "Starting unattended install setup" |
| print |
| |
| print "Variables set:" |
| for member in inspect.getmembers(self): |
| name, value = member |
| attribute = getattr(self, name) |
| if not (name.startswith("__") or callable(attribute) or not value): |
| print " %s: %s" % (name, value) |
| print |
| |
| if self.unattended_file and (self.floppy or self.cdrom_unattended): |
| self.setup_boot_disk() |
| if self.medium == "cdrom": |
| if self.tftp: |
| self.setup_pxe_boot() |
| elif self.medium == "url": |
| self.setup_url() |
| elif self.medium == "nfs": |
| self.setup_nfs() |
| else: |
| raise SetupError("Unexpected installation method %s" % |
| self.medium) |
| print "Unattended install setup finished successfuly" |
| |
| |
| if __name__ == "__main__": |
| os_install = UnattendedInstall() |
| os_install.setup() |