blob: 1ff73afcd835c6c1e9e3e345fcda797666449d06 [file] [log] [blame]
import logging, time, socket, re, os, shutil, tempfile, glob, ConfigParser
import xml.dom.minidom
from autotest_lib.client.common_lib import error
from autotest_lib.client.bin import utils
from autotest_lib.client.virt import virt_vm, virt_utils
@error.context_aware
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.
"""
error.context("cleaning up unattended install directory %s" % dir)
if os.path.ismount(dir):
utils.run('fuser -k %s' % dir, ignore_status=True)
utils.run('umount %s' % dir)
if os.path.isdir(dir):
shutil.rmtree(dir)
@error.context_aware
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.
"""
error.context("cleaning up old leftover image %s" % image)
if os.path.exists(image):
mtab = open('/etc/mtab', 'r')
mtab_contents = mtab.read()
mtab.close()
if image in mtab_contents:
utils.run('fuser -k %s' % image, ignore_status=True)
utils.run('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 get_answer_file_path(self, filename):
return os.path.join(self.mount, filename)
def copy_to(self, src):
logging.debug("Copying %s to disk image mount", 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)
logging.debug("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.
"""
@error.context_aware
def __init__(self, path, qemu_img_binary, tmpdir):
error.context("Creating unattended install floppy image %s" % path)
self.tmpdir = tmpdir
self.mount = tempfile.mkdtemp(prefix='floppy_', dir=self.tmpdir)
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)
utils.run(c_cmd)
f_cmd = 'mkfs.msdos -s 1 %s' % path
utils.run(f_cmd)
m_cmd = 'mount -o loop,rw %s %s' % (path, self.mount)
utils.run(m_cmd)
except error.CmdError, e:
cleanup(self.mount)
raise
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=self.tmpdir)
pwd = os.getcwd()
try:
m_cmd = 'mount -o loop,ro %s %s' % (virtio_floppy, virtio_mount)
utils.run(m_cmd)
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 IOError('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 ValueError('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. Important to note that it's possible to specify
drivers from a CDROM, so the floppy driver copy is optional.
Process:
1) Copy the virtio drivers on the virtio floppy to the install floppy,
if there is one available
"""
if os.path.isfile(virtio_floppy):
self._copy_virtio_drivers(virtio_floppy)
else:
logging.debug("No virtio floppy present, not needed for this OS anyway")
class CdromDisk(Disk):
"""
Represents a CDROM disk that we can master according to our needs.
"""
def __init__(self, path, tmpdir):
self.mount = tempfile.mkdtemp(prefix='cdrom_unattended_', dir=tmpdir)
self.path = path
clean_old_image(path)
if not os.path.isdir(os.path.dirname(path)):
os.makedirs(os.path.dirname(path))
@error.context_aware
def close(self):
error.context("Creating unattended install CD image %s" % self.path)
g_cmd = ('mkisofs -o %s -max-iso9660-filenames '
'-relaxed-filenames -D --input-charset iso8859-1 '
'%s' % (self.path, self.mount))
utils.run(g_cmd)
os.chmod(self.path, 0755)
cleanup(self.mount)
logging.debug("unattended install CD image %s successfuly created",
self.path)
class UnattendedInstallConfig(object):
"""
Creates a floppy disk image that will contain a config file for unattended
OS install. The parameters to the script are retrieved from environment
variables.
"""
def __init__(self, test, params):
"""
Sets class atributes from test parameters.
@param test: KVM test object.
@param params: Dictionary with test parameters.
"""
root_dir = test.bindir
images_dir = os.path.join(root_dir, 'images')
self.deps_dir = os.path.join(root_dir, 'deps')
self.unattended_dir = os.path.join(root_dir, 'unattended')
attributes = ['kernel_args', 'finish_program', 'cdrom_cd1',
'unattended_file', 'medium', 'url', 'kernel', 'initrd',
'nfs_server', 'nfs_dir', 'install_virtio', 'floppy',
'cdrom_unattended', 'boot_path', 'extra_params',
'qemu_img_binary', 'cdkey', 'finish_program']
for a in attributes:
setattr(self, a, params.get(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:
setattr(self, va, params.get(va, ''))
self.tmpdir = test.tmpdir
if getattr(self, 'unattended_file'):
self.unattended_file = os.path.join(root_dir, self.unattended_file)
if getattr(self, 'finish_program'):
self.finish_program = os.path.join(root_dir, self.finish_program)
if getattr(self, 'qemu_img_binary'):
if not os.path.isfile(getattr(self, 'qemu_img_binary')):
self.qemu_img_binary = os.path.join(root_dir,
self.qemu_img_binary)
if getattr(self, 'cdrom_cd1'):
self.cdrom_cd1 = os.path.join(root_dir, self.cdrom_cd1)
self.cdrom_cd1_mount = tempfile.mkdtemp(prefix='cdrom_cd1_',
dir=self.tmpdir)
if self.medium == 'nfs':
self.nfs_mount = tempfile.mkdtemp(prefix='nfs_',
dir=self.tmpdir)
if getattr(self, 'floppy'):
self.floppy = os.path.join(root_dir, self.floppy)
if not os.path.isdir(os.path.dirname(self.floppy)):
os.makedirs(os.path.dirname(self.floppy))
self.image_path = os.path.dirname(self.kernel)
def answer_kickstart(self, answer_path):
"""
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.
@return: Answer file contents
"""
contents = open(self.unattended_file).read()
dummy_cdkey_re = r'\bKVM_TEST_CDKEY\b'
if re.search(dummy_cdkey_re, contents):
if self.cdkey:
contents = re.sub(dummy_cdkey_re, self.cdkey, contents)
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 ValueError("Unexpected installation medium %s" % self.url)
contents = re.sub(dummy_medium_re, content, contents)
logging.debug("Unattended install contents:")
for line in contents.splitlines():
logging.debug(line)
utils.open_write_close(answer_path, contents)
def answer_windows_ini(self, answer_path):
parser = ConfigParser.ConfigParser()
parser.read(self.unattended_file)
# First, replacing the CDKEY
if self.cdkey:
parser.set('UserData', 'ProductKey', self.cdkey)
else:
logging.error("Param 'cdkey' required but not specified for "
"this unattended installation")
# Now, replacing the virtio network driver path, under double quotes
if self.install_virtio == 'yes':
parser.set('Unattended', 'OemPnPDriversPath',
'"%s"' % self.virtio_nework_path)
else:
parser.remove_option('Unattended', 'OemPnPDriversPath')
# Last, replace the virtio installer command
if self.install_virtio == 'yes':
driver = self.virtio_network_installer_path
else:
driver = 'dir'
dummy_re = 'KVM_TEST_VIRTIO_NETWORK_INSTALLER'
installer = parser.get('GuiRunOnce', 'Command0')
if dummy_re in installer:
installer = re.sub(dummy_re, driver, installer)
parser.set('GuiRunOnce', 'Command0', installer)
# Now, writing the in memory config state to the unattended file
fp = open(answer_path, 'w')
parser.write(fp)
# Let's read it so we can debug print the contents
fp = open(answer_path, 'r')
contents = fp.read()
logging.debug("Unattended install contents:")
for line in contents.splitlines():
logging.debug(line)
fp.close()
def answer_windows_xml(self, answer_path):
doc = xml.dom.minidom.parse(self.unattended_file)
if self.cdkey:
# First, replacing the CDKEY
product_key = doc.getElementsByTagName('ProductKey')[0]
key = product_key.getElementsByTagName('Key')[0]
key_text = key.childNodes[0]
assert key_text.nodeType == doc.TEXT_NODE
key_text.data = self.cdkey
else:
logging.error("Param 'cdkey' required but not specified for "
"this unattended installation")
# Now, replacing the virtio driver paths or removing the entire
# component PnpCustomizationsWinPE Element Node
if self.install_virtio == 'yes':
paths = doc.getElementsByTagName("Path")
values = [self.virtio_storage_path, self.virtio_network_path]
for path, value in zip(paths, values):
path_text = path.childNodes[0]
assert key_text.nodeType == doc.TEXT_NODE
path_text.data = value
else:
settings = doc.getElementsByTagName("settings")
for s in settings:
for c in s.getElementsByTagName("component"):
if (c.getAttribute('name') ==
"Microsoft-Windows-PnpCustomizationsWinPE"):
s.removeChild(c)
# Last but not least important, replacing the virtio installer command
command_lines = doc.getElementsByTagName("CommandLine")
for command_line in command_lines:
command_line_text = command_line.childNodes[0]
assert command_line_text.nodeType == doc.TEXT_NODE
dummy_re = 'KVM_TEST_VIRTIO_NETWORK_INSTALLER'
if (self.install_virtio == 'yes' and
hasattr(self, 'virtio_network_installer_path')):
driver = self.virtio_network_installer_path
else:
driver = 'dir'
if driver.endswith("msi"):
driver = 'msiexec /passive /package ' + driver
if dummy_re in command_line_text.data:
t = command_line_text.data
t = re.sub(dummy_re, driver, t)
command_line_text.data = t
contents = doc.toxml()
logging.debug("Unattended install contents:")
for line in contents.splitlines():
logging.debug(line)
fp = open(answer_path, 'w')
doc.writexml(fp)
def answer_suse_xml(self, answer_path):
# There's nothing to replace on SUSE files to date. Yay!
doc = xml.dom.minidom.parse(self.unattended_file)
contents = doc.toxml()
logging.debug("Unattended install contents:")
for line in contents.splitlines():
logging.debug(line)
fp = open(answer_path, 'w')
doc.writexml(fp)
def setup_boot_disk(self):
if self.unattended_file.endswith('.sif'):
dest_fname = 'winnt.sif'
setup_file = 'winnt.bat'
boot_disk = FloppyDisk(self.floppy, self.qemu_img_binary,
self.tmpdir)
answer_path = boot_disk.get_answer_file_path(dest_fname)
self.answer_windows_ini(answer_path)
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, self.tmpdir)
elif self.floppy:
boot_disk = FloppyDisk(self.floppy, self.qemu_img_binary,
self.tmpdir)
else:
raise ValueError("Neither cdrom_unattended nor floppy set "
"on the config file, please verify")
answer_path = boot_disk.get_answer_file_path(dest_fname)
self.answer_kickstart(answer_path)
elif self.unattended_file.endswith('.xml'):
if "autoyast" in self.extra_params:
# SUSE autoyast install
dest_fname = "autoinst.xml"
if self.cdrom_unattended:
boot_disk = CdromDisk(self.cdrom_unattended, self.tmpdir)
elif self.floppy:
boot_disk = FloppyDisk(self.floppy, self.qemu_img_binary,
self.tmpdir)
else:
raise ValueError("Neither cdrom_unattended nor floppy set "
"on the config file, please verify")
answer_path = boot_disk.get_answer_file_path(dest_fname)
self.answer_suse_xml(answer_path)
else:
# Windows unattended install
dest_fname = "autounattend.xml"
boot_disk = FloppyDisk(self.floppy, self.qemu_img_binary,
self.tmpdir)
answer_path = boot_disk.get_answer_file_path(dest_fname)
self.answer_windows_xml(answer_path)
if self.install_virtio == "yes":
boot_disk.setup_virtio_win2008(self.virtio_floppy)
boot_disk.copy_to(self.finish_program)
else:
raise ValueError('Unknown answer file type: %s' %
self.unattended_file)
boot_disk.close()
@error.context_aware
def setup_cdrom(self):
"""
Mount cdrom and copy vmlinuz and initrd.img.
"""
error.context("Copying vmlinuz and initrd.img from install cdrom %s" %
self.cdrom_cd1)
m_cmd = ('mount -t iso9660 -v -o loop,ro %s %s' %
(self.cdrom_cd1, self.cdrom_cd1_mount))
utils.run(m_cmd)
try:
if not os.path.isdir(self.image_path):
os.makedirs(self.image_path)
kernel_fetch_cmd = ("cp %s/%s/%s %s" %
(self.cdrom_cd1_mount, self.boot_path,
os.path.basename(self.kernel), self.kernel))
utils.run(kernel_fetch_cmd)
initrd_fetch_cmd = ("cp %s/%s/%s %s" %
(self.cdrom_cd1_mount, self.boot_path,
os.path.basename(self.initrd), self.initrd))
utils.run(initrd_fetch_cmd)
finally:
cleanup(self.cdrom_cd1_mount)
@error.context_aware
def setup_url(self):
"""
Download the vmlinuz and initrd.img from URL.
"""
error.context("downloading vmlinuz and initrd.img from %s" % self.url)
os.chdir(self.image_path)
kernel_fetch_cmd = "wget -q %s/%s/%s" % (self.url, self.boot_path,
os.path.basename(self.kernel))
initrd_fetch_cmd = "wget -q %s/%s/%s" % (self.url, self.boot_path,
os.path.basename(self.initrd))
if os.path.exists(self.kernel):
os.remove(self.kernel)
if os.path.exists(self.initrd):
os.remove(self.initrd)
utils.run(kernel_fetch_cmd)
utils.run(initrd_fetch_cmd)
def setup_nfs(self):
"""
Copy the vmlinuz and initrd.img from nfs.
"""
error.context("copying the vmlinuz and initrd.img from NFS share")
m_cmd = ("mount %s:%s %s -o ro" %
(self.nfs_server, self.nfs_dir, self.nfs_mount))
utils.run(m_cmd)
try:
kernel_fetch_cmd = ("cp %s/%s/%s %s" %
(self.nfs_mount, self.boot_path,
os.path.basename(self.kernel), self.image_path))
utils.run(kernel_fetch_cmd)
initrd_fetch_cmd = ("cp %s/%s/%s %s" %
(self.nfs_mount, self.boot_path,
os.path.basename(self.initrd), self.image_path))
utils.run(initrd_fetch_cmd)
finally:
cleanup(self.nfs_mount)
def setup(self):
"""
Configure the environment for unattended install.
Uses an appropriate strategy according to each install model.
"""
logging.info("Starting unattended install setup")
virt_utils.display_attributes(self)
if self.unattended_file and (self.floppy or self.cdrom_unattended):
self.setup_boot_disk()
if self.medium == "cdrom":
if self.kernel and self.initrd:
self.setup_cdrom()
elif self.medium == "url":
self.setup_url()
elif self.medium == "nfs":
self.setup_nfs()
else:
raise ValueError("Unexpected installation method %s" %
self.medium)
@error.context_aware
def run_unattended_install(test, params, env):
"""
Unattended install test:
1) Starts a VM with an appropriated setup to start an unattended OS install.
2) Wait until the install reports to the install watcher its end.
@param test: KVM test object.
@param params: Dictionary with the test parameters.
@param env: Dictionary with test environment.
"""
unattended_install_config = UnattendedInstallConfig(test, params)
unattended_install_config.setup()
vm = env.get_vm(params["main_vm"])
vm.create()
install_timeout = int(params.get("timeout", 3000))
post_install_delay = int(params.get("post_install_delay", 0))
port = vm.get_port(int(params.get("guest_port_unattended_install")))
migrate_background = params.get("migrate_background") == "yes"
if migrate_background:
mig_timeout = float(params.get("mig_timeout", "3600"))
mig_protocol = params.get("migration_protocol", "tcp")
logging.info("Waiting for installation to finish. Timeout set to %d s "
"(%d min)", install_timeout, install_timeout/60)
error.context("waiting for installation to finish")
start_time = time.time()
while (time.time() - start_time) < install_timeout:
try:
vm.verify_alive()
except virt_vm.VMDeadError, e:
if params.get("wait_no_ack", "no") == "yes":
break
else:
raise e
vm.verify_kernel_crash()
if params.get("wait_no_ack", "no") == "no":
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
client.connect((vm.get_address(), port))
if client.recv(1024) == "done":
break
except (socket.error, virt_vm.VMAddressError):
pass
if migrate_background:
# Drop the params which may break the migration
# Better method is to use dnsmasq to do the
# unattended installation
if vm.params.get("initrd"):
vm.params["initrd"] = None
if vm.params.get("kernel"):
vm.params["kernel"] = None
if vm.params.get("extra_params"):
vm.params["extra_params"] = re.sub("--append '.*'", "",
vm.params["extra_params"])
vm.migrate(timeout=mig_timeout, protocol=mig_protocol)
else:
time.sleep(1)
if params.get("wait_no_ack", "no") == "no":
client.close()
else:
raise error.TestFail("Timeout elapsed while waiting for install to "
"finish")
time_elapsed = time.time() - start_time
logging.info("Guest reported successful installation after %d s (%d min)",
time_elapsed, time_elapsed/60)
if params.get("shutdown_cleanly", "yes") == "yes":
shutdown_cleanly_timeout = int(params.get("shutdown_cleanly_timeout",
120))
logging.info("Wait for guest to shutdown cleanly")
if virt_utils.wait_for(vm.is_dead, shutdown_cleanly_timeout, 1, 1):
logging.info("Guest managed to shutdown cleanly")