From: Lucas Meneghel Rodrigues <lmr@redhat.com>
To: autotest@test.kernel.org
Cc: kvm@vger.kernel.org
Subject: [PATCH 03/11] Moving unattended_install test from kvm test to common virt location
Date: Tue, 11 Oct 2011 18:07:09 -0300 [thread overview]
Message-ID: <1318367237-26081-4-git-send-email-lmr@redhat.com> (raw)
In-Reply-To: <1318367237-26081-1-git-send-email-lmr@redhat.com>
Signed-off-by: Lucas Meneghel Rodrigues <lmr@redhat.com>
---
client/tests/kvm/tests/unattended_install.py | 632 --------------------------
client/virt/tests/unattended_install.py | 632 ++++++++++++++++++++++++++
2 files changed, 632 insertions(+), 632 deletions(-)
delete mode 100644 client/tests/kvm/tests/unattended_install.py
create mode 100644 client/virt/tests/unattended_install.py
diff --git a/client/tests/kvm/tests/unattended_install.py b/client/tests/kvm/tests/unattended_install.py
deleted file mode 100644
index 41a5e60..0000000
--- a/client/tests/kvm/tests/unattended_install.py
+++ /dev/null
@@ -1,632 +0,0 @@
-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:
- logging.error("Error during floppy initialization: %s" % 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
- self.deps_dir = os.path.join(test.virtdir, 'deps')
- self.unattended_dir = os.path.join(test.virtdir, '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(test.virtdir, self.unattended_file)
-
- if getattr(self, 'finish_program'):
- self.finish_program = os.path.join(test.virtdir, 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))
- 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:
- 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")
diff --git a/client/virt/tests/unattended_install.py b/client/virt/tests/unattended_install.py
new file mode 100644
index 0000000..efa3c10
--- /dev/null
+++ b/client/virt/tests/unattended_install.py
@@ -0,0 +1,632 @@
+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:
+ logging.error("Error during floppy initialization: %s" % 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
+ self.deps_dir = os.path.join(test.virtdir, 'deps')
+ self.unattended_dir = os.path.join(test.virtdir, '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(test.virtdir, self.unattended_file)
+
+ if getattr(self, 'finish_program'):
+ self.finish_program = os.path.join(test.virtdir, 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 in ["cdrom", "kernel_initrd"]:
+ 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 in ["cdrom", "kernel_initrd"]:
+ 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))
+ 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:
+ 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")
--
1.7.6.4
next prev parent reply other threads:[~2011-10-11 21:07 UTC|newest]
Thread overview: 16+ messages / expand[flat|nested] mbox.gz Atom feed top
2011-10-11 21:07 [PATCH 00/11] [RFC] Libvirt test v2 Lucas Meneghel Rodrigues
2011-10-11 21:07 ` [PATCH 02/11] virt: Introducing virt_test.virt_test class Lucas Meneghel Rodrigues
2011-10-11 21:07 ` Lucas Meneghel Rodrigues [this message]
2011-10-11 21:07 ` [PATCH 04/11] Moving get_started code to client.virt.virt_utils Lucas Meneghel Rodrigues
2011-10-11 21:07 ` [PATCH 05/11] virt: Introducing libvirt VM class Lucas Meneghel Rodrigues
2011-10-12 6:51 ` [Autotest] " Amos Kong
2011-10-12 8:14 ` Daniel P. Berrange
2011-10-13 17:26 ` Lucas Meneghel Rodrigues
2011-10-11 21:07 ` [PATCH 06/11] virt: Introducing libvirt monitor Lucas Meneghel Rodrigues
2011-10-12 7:48 ` [Autotest] " Amos Kong
2011-10-13 17:12 ` Lucas Meneghel Rodrigues
2011-10-11 21:07 ` [PATCH 07/11] virt.virt_env_process: Add libvirt vm handling Lucas Meneghel Rodrigues
2011-10-11 21:07 ` [PATCH 08/11] client.tests: Introducing libvirt test Lucas Meneghel Rodrigues
2011-10-11 21:07 ` [PATCH 09/11] Virt: builtin HTTP server for unattended installs Lucas Meneghel Rodrigues
2011-10-11 21:07 ` [PATCH 10/11] Virt: support XEN via libvirt and auto url installer Lucas Meneghel Rodrigues
2011-10-11 21:07 ` [PATCH 11/11] Virt: add support for XEN via libvirt installs and auto url Lucas Meneghel Rodrigues
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=1318367237-26081-4-git-send-email-lmr@redhat.com \
--to=lmr@redhat.com \
--cc=autotest@test.kernel.org \
--cc=kvm@vger.kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).