Improve "Libvirt.create_snapshot_qemu" function
[yardstick.git] / yardstick / benchmark / contexts / standalone / model.py
index 4491660..4d43f26 100644 (file)
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from __future__ import absolute_import
 import os
 import re
 import time
-import glob
 import uuid
 import random
 import logging
-import itertools
 import errno
 
 from netaddr import IPNetwork
 import xml.etree.ElementTree as ET
 
 from yardstick import ssh
-from yardstick.common.constants import YARDSTICK_ROOT_PATH
+from yardstick.common import constants
+from yardstick.common import exceptions
 from yardstick.common.yaml_loader import yaml_load
 from yardstick.network_services.utils import PciAddress
-from yardstick.common.utils import write_file
+from yardstick.network_services.helpers.cpu import CpuSysCores
+
 
 LOG = logging.getLogger(__name__)
 
 VM_TEMPLATE = """
 <domain type="kvm">
- <name>{vm_name}</name>
 <name>{vm_name}</name>
   <uuid>{random_uuid}</uuid>
   <memory unit="MB">{memory}</memory>
   <currentMemory unit="MB">{memory}</currentMemory>
   <memoryBacking>
     <hugepages />
   </memoryBacking>
-  <vcpu placement="static">{vcpu}</vcpu>
+  <vcpu cpuset='{cpuset}'>{vcpu}</vcpu>
+ {cputune}
   <os>
     <type arch="x86_64" machine="pc-i440fx-utopic">hvm</type>
     <boot dev="hd" />
@@ -80,7 +80,13 @@ VM_TEMPLATE = """
       <source bridge="br-int" />
       <model type='virtio'/>
     </interface>
-   </devices>
+    <serial type='pty'>
+      <target port='0'/>
+    </serial>
+    <console type='pty'>
+      <target type='serial' port='0'/>
+    </console>
+  </devices>
 </domain>
 """
 WAIT_FOR_BOOT = 30
@@ -95,33 +101,68 @@ class Libvirt(object):
         cmd_template = "virsh list --name | grep -i %s"
         status = connection.execute(cmd_template % vm_name)[0]
         if status == 0:
-            LOG.info("VM '%s' is already present.. destroying" % vm_name)
+            LOG.info("VM '%s' is already present... destroying", vm_name)
             connection.execute("virsh destroy %s" % vm_name)
 
     @staticmethod
     def virsh_create_vm(connection, cfg):
-        err = connection.execute("virsh create %s" % cfg)[0]
-        LOG.info("VM create status: %s" % (err))
+        LOG.info('VM create, XML config: %s', cfg)
+        status, _, error = connection.execute('virsh create %s' % cfg)
+        if status:
+            raise exceptions.LibvirtCreateError(error=error)
 
     @staticmethod
     def virsh_destroy_vm(vm_name, connection):
-        connection.execute("virsh destroy %s" % vm_name)
+        LOG.info('VM destroy, VM name: %s', vm_name)
+        status, _, error = connection.execute('virsh destroy %s' % vm_name)
+        if status:
+            LOG.warning('Error destroying VM %s. Error: %s', vm_name, error)
 
     @staticmethod
-    def add_interface_address(interface, pci_address):
+    def _add_interface_address(interface, pci_address):
+        """Add a PCI 'address' XML node
+
+        <address type='pci' domain='0x0000' bus='0x00' slot='0x08'
+         function='0x0'/>
+
+        Refence: https://software.intel.com/en-us/articles/
+                 configure-sr-iov-network-virtual-functions-in-linux-kvm
+        """
         vm_pci = ET.SubElement(interface, 'address')
         vm_pci.set('type', 'pci')
-        vm_pci.set('domain', '0x%s' % pci_address.domain)
-        vm_pci.set('bus', '0x%s' % pci_address.bus)
-        vm_pci.set('slot', '0x%s' % pci_address.slot)
-        vm_pci.set('function', '0x%s' % pci_address.function)
+        vm_pci.set('domain', '0x{}'.format(pci_address.domain))
+        vm_pci.set('bus', '0x{}'.format(pci_address.bus))
+        vm_pci.set('slot', '0x{}'.format(pci_address.slot))
+        vm_pci.set('function', '0x{}'.format(pci_address.function))
         return vm_pci
 
     @classmethod
-    def add_ovs_interface(cls, vpath, port_num, vpci, vports_mac, xml):
-        vhost_path = '{0}/var/run/openvswitch/dpdkvhostuser{1}'
-        root = ET.parse(xml)
-        pci_address = PciAddress.parse_address(vpci.strip(), multi_line=True)
+    def add_ovs_interface(cls, vpath, port_num, vpci, vports_mac, xml_str):
+        """Add a DPDK OVS 'interface' XML node in 'devices' node
+
+        <devices>
+            <interface type='vhostuser'>
+                <mac address='00:00:00:00:00:01'/>
+                <source type='unix' path='/usr/local/var/run/openvswitch/
+                 dpdkvhostuser0' mode='client'/>
+                <model type='virtio'/>
+                <driver queues='4'>
+                    <host mrg_rxbuf='off'/>
+                </driver>
+                <address type='pci' domain='0x0000' bus='0x00' slot='0x03'
+                 function='0x0'/>
+            </interface>
+            ...
+        </devices>
+
+        Reference: http://docs.openvswitch.org/en/latest/topics/dpdk/
+                   vhost-user/
+        """
+
+        vhost_path = ('{0}/var/run/openvswitch/dpdkvhostuser{1}'.
+                      format(vpath, port_num))
+        root = ET.fromstring(xml_str)
+        pci_address = PciAddress(vpci.strip())
         device = root.find('devices')
 
         interface = ET.SubElement(device, 'interface')
@@ -131,7 +172,7 @@ class Libvirt(object):
 
         source = ET.SubElement(interface, 'source')
         source.set('type', 'unix')
-        source.set('path', vhost_path.format(vpath, port_num))
+        source.set('path', vhost_path)
         source.set('mode', 'client')
 
         model = ET.SubElement(interface, 'model')
@@ -143,14 +184,35 @@ class Libvirt(object):
         host = ET.SubElement(driver, 'host')
         host.set('mrg_rxbuf', 'off')
 
-        cls.add_interface_address(interface, pci_address)
+        cls._add_interface_address(interface, pci_address)
 
-        root.write(xml)
+        return ET.tostring(root)
 
     @classmethod
-    def add_sriov_interfaces(cls, vm_pci, vf_pci, vfmac, xml):
-        root = ET.parse(xml)
-        pci_address = PciAddress.parse_address(vf_pci.strip(), multi_line=True)
+    def add_sriov_interfaces(cls, vm_pci, vf_pci, vf_mac, xml_str):
+        """Add a SR-IOV 'interface' XML node in 'devices' node
+
+        <devices>
+           <interface type='hostdev' managed='yes'>
+             <source>
+               <address type='pci' domain='0x0000' bus='0x00' slot='0x03'
+                function='0x0'/>
+             </source>
+             <mac address='52:54:00:6d:90:02'>
+             <address type='pci' domain='0x0000' bus='0x02' slot='0x04'
+              function='0x1'/>
+           </interface>
+           ...
+         </devices>
+
+        Reference: https://access.redhat.com/documentation/en-us/
+            red_hat_enterprise_linux/6/html/
+            virtualization_host_configuration_and_guest_installation_guide/
+            sect-virtualization_host_configuration_and_guest_installation_guide
+            -sr_iov-how_sr_iov_libvirt_works
+        """
+
+        root = ET.fromstring(xml_str)
         device = root.find('devices')
 
         interface = ET.SubElement(device, 'interface')
@@ -158,33 +220,56 @@ class Libvirt(object):
         interface.set('type', 'hostdev')
 
         mac = ET.SubElement(interface, 'mac')
-        mac.set('address', vfmac)
-        source = ET.SubElement(interface, 'source')
+        mac.set('address', vf_mac)
 
-        addr = ET.SubElement(source, "address")
-        addr.set('domain', "0x0")
-        addr.set('bus', "{0}".format(pci_address.bus))
-        addr.set('function', "{0}".format(pci_address.function))
-        addr.set('slot', "0x{0}".format(pci_address.slot))
-        addr.set('type', "pci")
+        source = ET.SubElement(interface, 'source')
+        pci_address = PciAddress(vf_pci.strip())
+        cls._add_interface_address(source, pci_address)
 
-        pci_vm_address = PciAddress.parse_address(vm_pci.strip(), multi_line=True)
-        cls.add_interface_address(interface, pci_vm_address)
+        pci_vm_address = PciAddress(vm_pci.strip())
+        cls._add_interface_address(interface, pci_vm_address)
 
-        root.write(xml)
+        return ET.tostring(root)
 
     @staticmethod
-    def create_snapshot_qemu(connection, index, vm_image):
-        # build snapshot image
-        image = "/var/lib/libvirt/images/%s.qcow2" % index
-        connection.execute("rm %s" % image)
-        qemu_template = "qemu-img create -f qcow2 -o backing_file=%s %s"
-        connection.execute(qemu_template % (vm_image, image))
-
-        return image
+    def create_snapshot_qemu(connection, index, base_image):
+        """Create the snapshot image for a VM using a base image
+
+        :param connection: SSH connection to the remote host
+        :param index: index of the VM to be spawn
+        :param base_image: path of the VM base image in the remote host
+        :return: snapshot image path
+        """
+        vm_image = '/var/lib/libvirt/images/%s.qcow2' % index
+        connection.execute('rm -- "%s"' % vm_image)
+        status, _, _ = connection.execute('test -r %s' % base_image)
+        if status:
+            if not os.access(base_image, os.R_OK):
+                raise exceptions.LibvirtQemuImageBaseImageNotPresent(
+                    vm_image=vm_image, base_image=base_image)
+            # NOTE(ralonsoh): done in two steps to avoid root permission
+            # issues.
+            LOG.info('Copy %s from execution host to remote host', base_image)
+            file_name = os.path.basename(os.path.normpath(base_image))
+            connection.put_file(base_image, '/tmp/%s' % file_name)
+            status, _, error = connection.execute(
+                'mv -- "/tmp/%s" "%s"' % (file_name, base_image))
+            if status:
+                raise exceptions.LibvirtQemuImageCreateError(
+                    vm_image=vm_image, base_image=base_image, error=error)
+
+        LOG.info('Convert image %s to %s', base_image, vm_image)
+        qemu_cmd = ('qemu-img create -f qcow2 -o backing_file=%s %s' %
+                    (base_image, vm_image))
+        status, _, error = connection.execute(qemu_cmd)
+        if status:
+            raise exceptions.LibvirtQemuImageCreateError(
+                vm_image=vm_image, base_image=base_image, error=error)
+        return vm_image
 
     @classmethod
-    def build_vm_xml(cls, connection, flavor, cfg, vm_name, index):
+    def build_vm_xml(cls, connection, flavor, vm_name, index):
+        """Build the XML from the configuration parameters"""
         memory = flavor.get('ram', '4096')
         extra_spec = flavor.get('extra_specs', {})
         cpu = extra_spec.get('hw:cpu_cores', '2')
@@ -192,7 +277,10 @@ class Libvirt(object):
         threads = extra_spec.get('hw:cpu_threads', '2')
         vcpu = int(cpu) * int(threads)
         numa_cpus = '0-%s' % (vcpu - 1)
+        hw_socket = flavor.get('hw_socket', '0')
+        cpuset = Libvirt.pin_vcpu_for_perf(connection, hw_socket)
 
+        cputune = extra_spec.get('cputune', '')
         mac = StandaloneContextHelper.get_mac_address(0x00)
         image = cls.create_snapshot_qemu(connection, index,
                                          flavor.get("images", None))
@@ -203,36 +291,9 @@ class Libvirt(object):
             memory=memory, vcpu=vcpu, cpu=cpu,
             numa_cpus=numa_cpus,
             socket=socket, threads=threads,
-            vm_image=image)
-
-        write_file(cfg, vm_xml)
-
-        return [vcpu, mac]
+            vm_image=image, cpuset=cpuset, cputune=cputune)
 
-    @staticmethod
-    def split_cpu_list(cpu_list):
-        if not cpu_list:
-            return []
-
-        ranges = cpu_list.split(',')
-        bounds = ([int(b) for b in r.split('-')] for r in ranges)
-        range_objects = \
-            (range(bound[0], bound[1] + 1 if len(bound) == 2
-             else bound[0] + 1) for bound in bounds)
-
-        return sorted(itertools.chain.from_iterable(range_objects))
-
-    @classmethod
-    def get_numa_nodes(cls):
-        nodes_sysfs = glob.iglob("/sys/devices/system/node/node*")
-        nodes = {}
-        for node_sysfs in nodes_sysfs:
-            num = os.path.basename(node_sysfs).replace("node", "")
-            with open(os.path.join(node_sysfs, "cpulist")) as cpulist_file:
-                cpulist = cpulist_file.read().strip()
-            nodes[num] = cls.split_cpu_list(cpulist)
-        LOG.info("nodes: {0}".format(nodes))
-        return nodes
+        return vm_xml, mac
 
     @staticmethod
     def update_interrupts_hugepages_perf(connection):
@@ -240,14 +301,24 @@ class Libvirt(object):
         connection.execute("echo never > /sys/kernel/mm/transparent_hugepage/enabled")
 
     @classmethod
-    def pin_vcpu_for_perf(cls, connection, vm_name, cpu):
-        nodes = cls.get_numa_nodes()
-        num_nodes = len(nodes)
-        vcpi_pin_template = "virsh vcpupin {0} {1} {2}"
-        for i in range(0, int(cpu)):
-            core = nodes[str(num_nodes - 1)][i % len(nodes[str(num_nodes - 1)])]
-            connection.execute(vcpi_pin_template.format(vm_name, i, core))
-        cls.update_interrupts_hugepages_perf(connection)
+    def pin_vcpu_for_perf(cls, connection, socket='0'):
+        threads = ""
+        sys_obj = CpuSysCores(connection)
+        soc_cpu = sys_obj.get_core_socket()
+        sys_cpu = int(soc_cpu["cores_per_socket"])
+        socket = str(socket)
+        cores = "%s-%s" % (soc_cpu[socket][0], soc_cpu[socket][sys_cpu - 1])
+        if int(soc_cpu["thread_per_core"]) > 1:
+            threads = "%s-%s" % (soc_cpu[socket][sys_cpu], soc_cpu[socket][-1])
+        cpuset = "%s,%s" % (cores, threads)
+        return cpuset
+
+    @classmethod
+    def write_file(cls, file_name, xml_str):
+        """Dump a XML string to a file"""
+        root = ET.fromstring(xml_str)
+        et = ET.ElementTree(element=root)
+        et.write(file_name, encoding='utf-8', method='xml')
 
 
 class StandaloneContextHelper(object):
@@ -258,7 +329,8 @@ class StandaloneContextHelper(object):
         super(StandaloneContextHelper, self).__init__()
 
     @staticmethod
-    def install_req_libs(connection, extra_pkgs=[]):
+    def install_req_libs(connection, extra_pkgs=None):
+        extra_pkgs = extra_pkgs or []
         pkgs = ["qemu-kvm", "libvirt-bin", "bridge-utils", "numactl", "fping"]
         pkgs.extend(extra_pkgs)
         cmd_template = "dpkg-query -W --showformat='${Status}\\n' \"%s\"|grep 'ok installed'"
@@ -266,9 +338,6 @@ class StandaloneContextHelper(object):
             if connection.execute(cmd_template % pkg)[0]:
                 connection.execute("apt-get update")
                 connection.execute("apt-get -y install %s" % pkg)
-        else:
-            # all installed
-            return
 
     @staticmethod
     def get_kernel_module(connection, pci, driver):
@@ -278,7 +347,7 @@ class StandaloneContextHelper(object):
         return driver
 
     @classmethod
-    def get_nic_details(cls, connection, networks, dpdk_nic_bind):
+    def get_nic_details(cls, connection, networks, dpdk_devbind):
         for key, ports in networks.items():
             if key == "mgmt":
                 continue
@@ -288,11 +357,11 @@ class StandaloneContextHelper(object):
             driver = cls.get_kernel_module(connection, phy_ports, phy_driver)
 
             # Make sure that ports are bound to kernel drivers e.g. i40e/ixgbe
-            bind_cmd = "{dpdk_nic_bind} --force -b {driver} {port}"
+            bind_cmd = "{dpdk_devbind} --force -b {driver} {port}"
             lshw_cmd = "lshw -c network -businfo | grep '{port}'"
             link_show_cmd = "ip -s link show {interface}"
 
-            cmd = bind_cmd.format(dpdk_nic_bind=dpdk_nic_bind,
+            cmd = bind_cmd.format(dpdk_devbind=dpdk_devbind,
                                   driver=driver, port=ports['phy_port'])
             connection.execute(cmd)
 
@@ -305,7 +374,7 @@ class StandaloneContextHelper(object):
                 'interface': str(interface),
                 'driver': driver
             })
-        LOG.info("{0}".format(networks))
+        LOG.info(networks)
 
         return networks
 
@@ -342,7 +411,8 @@ class StandaloneContextHelper(object):
         except IOError as io_error:
             if io_error.errno != errno.ENOENT:
                 raise
-            self.file_path = os.path.join(YARDSTICK_ROOT_PATH, file_path)
+            self.file_path = os.path.join(constants.YARDSTICK_ROOT_PATH,
+                                          file_path)
             cfg = self.read_config_file()
 
         nodes.extend([node for node in cfg["nodes"] if str(node["role"]) != nfvi_role])
@@ -374,7 +444,7 @@ class StandaloneContextHelper(object):
         while not mgmtip and times:
             connection.execute("fping -c 1 -g %s > /dev/null 2>&1" % cidr)
             out = connection.execute("ip neighbor | grep '%s'" % mac)[1]
-            LOG.info("fping -c 1 -g %s > /dev/null 2>&1" % cidr)
+            LOG.info("fping -c 1 -g %s > /dev/null 2>&1", cidr)
             if out.strip():
                 mgmtip = str(out.split(" ")[0]).strip()
                 client = ssh.SSH.from_node(node, overrides={"ip": mgmtip})
@@ -474,7 +544,7 @@ class OvsDeploy(object):
         StandaloneContextHelper.install_req_libs(self.connection, pkgs)
 
     def ovs_deploy(self):
-        ovs_deploy = os.path.join(YARDSTICK_ROOT_PATH,
+        ovs_deploy = os.path.join(constants.YARDSTICK_ROOT_PATH,
                                   "yardstick/resources/scripts/install/",
                                   self.OVS_DEPLOY_SCRIPT)
         if os.path.isfile(ovs_deploy):
@@ -490,4 +560,6 @@ class OvsDeploy(object):
 
             cmd = "sudo -E %s --ovs='%s' --dpdk='%s' -p='%s'" % (remote_ovs_deploy,
                                                                  ovs, dpdk, http_proxy)
-            self.connection.execute(cmd)
+            exit_status, _, stderr = self.connection.execute(cmd)
+            if exit_status:
+                raise exceptions.OVSDeployError(stderr=stderr)