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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.