blob: 1029d1eb5e84bf4b6536843dee304453bda5c57a [file] [log] [blame]
#!/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()