[Kimchi-devel] [PATCH v4 1/4] Host device passthrough: List all types of host devices

Zhou Zheng Sheng zhshzhou at linux.vnet.ibm.com
Tue May 27 06:15:50 UTC 2014


The URI /host/devices only presents scsi_host (particularly fc_host)
device information. To implement host PCI pass through, we should list all
types of host devices. This patch adds support for parsing various host
devices information, and listing them on /host/devices. So the user is free
to choose any listed PCI device to pass through to guest. Since the
patch changes the device information dictionary format, the existing code
consuming the device information is also changed accordingly.

To get all types of host device, access the following URL.

curl -k -u root -H "Content-Type: application/json" \
  -H  "Accept: application/json" \
  https://127.0.0.1:8001/host/devices

To get only fc_host devices, change the URL to
  "https://127.0.0.1:8001/host/devices?_cap=fc_host"

To get only pci device, change the URL to
  "https://127.0.0.1:8001/host/devices?_cap=pci"

v1:
  Parse the node device XML using xpath.

v2:
  Write a "dictize" function and parse the node device XML using dictize.

v3:
  Fix a naming mistake.

v4:
  It is observed that sometimes the parent devices is not listed by
  libvirt but the child device is listed. In previous version we catch
  this exception and ignore it. The root cause is unknown, and we failed
  to re-produce the problem. In v4 we do not catch it. It seems to be
  related to USB removable disk, and the problem is gone after we
  upgraded Linux kernel.

Signed-off-by: Zhou Zheng Sheng <zhshzhou at linux.vnet.ibm.com>
---
 docs/API.md                            |  11 +-
 src/kimchi/hostdev.py                  | 209 +++++++++++++++++++++++++++++++++
 src/kimchi/mockmodel.py                |   7 +-
 src/kimchi/model/host.py               |  15 +--
 src/kimchi/model/libvirtstoragepool.py |  18 +--
 src/kimchi/xmlutils.py                 |  26 +++-
 tests/test_rest.py                     |   6 +-
 tests/test_storagepool.py              |   7 +-
 8 files changed, 262 insertions(+), 37 deletions(-)
 create mode 100644 src/kimchi/hostdev.py

diff --git a/docs/API.md b/docs/API.md
index 9217a37..484a4c8 100644
--- a/docs/API.md
+++ b/docs/API.md
@@ -869,12 +869,11 @@ stats history
 * **GET**: Retrieve information of a single pci device.
            Currently only scsi_host devices are supported:
     * name: The name of the device.
-    * adapter_type: The capability type of the scsi_host device (fc_host).
-                    Empty if pci device is not scsi_host.
-    * wwnn: The HBA Word Wide Node Name.
-            Empty if pci device is not scsi_host.
-    * wwpn: The HBA Word Wide Port Name
-            Empty if pci device is not scsi_host.
+    * path: Path of device in sysfs.
+    * adapter: Host adapter information. Empty if pci device is not scsi_host.
+        * type: The capability type of the scsi_host device (fc_host, vport_ops).
+        * wwnn: The HBA Word Wide Node Name. Empty if pci device is not fc_host.
+        * wwpn: The HBA Word Wide Port Name. Empty if pci device is not fc_host.
 
 ### Collection: Host Packages Update
 
diff --git a/src/kimchi/hostdev.py b/src/kimchi/hostdev.py
new file mode 100644
index 0000000..d4c142d
--- /dev/null
+++ b/src/kimchi/hostdev.py
@@ -0,0 +1,209 @@
+#
+# Kimchi
+#
+# Copyright IBM Corp, 2014
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA
+
+from kimchi.model.libvirtconnection import LibvirtConnection
+from kimchi.utils import kimchi_log
+from kimchi.xmlutils import dictize
+
+
+def _get_all_host_dev_infos():
+    libvirt_conn = LibvirtConnection('qemu:///system').get()
+    node_devs = libvirt_conn.listAllDevices()
+    return [get_dev_info(node_dev) for node_dev in node_devs]
+
+
+def _get_dev_info_tree(dev_infos):
+    devs = dict([(dev_info['name'], dev_info) for dev_info in dev_infos])
+    root = None
+    for dev_info in dev_infos:
+        if dev_info['parent'] is None:
+            root = dev_info
+            continue
+        parent = devs[dev_info['parent']]
+
+        try:
+            children = parent['children']
+        except KeyError:
+            parent['children'] = [dev_info]
+        else:
+            children.append(dev_info)
+    return root
+
+
+def get_dev_info(node_dev):
+    ''' Parse the node device XML string into dict according to
+    http://libvirt.org/formatnode.html. '''
+
+    def shift_subdict(d, toshift):
+        subdict = d.pop(toshift)
+        d.update(subdict)
+        return d
+
+    xmlstr = node_dev.XMLDesc()
+    info = dictize(xmlstr)['device']
+    dev_type = info['capability'].pop('type')
+    info['device_type'] = dev_type
+    shift_subdict(info, 'capability')
+    info['parent'] = node_dev.parent()
+
+    get_dev_type_info = {
+        'net': _get_net_dev_info,
+        'pci': _get_pci_dev_info,
+        'scsi': _get_scsi_dev_info,
+        'scsi_generic': _get_scsi_generic_dev_info,
+        'scsi_host': _get_scsi_host_dev_info,
+        'scsi_target': _get_scsi_target_dev_info,
+        'storage': _get_storage_dev_info,
+        'system': _get_system_dev_info,
+        'usb': _get_usb_dev_info,
+        'usb_device': _get_usb_device_dev_info,
+    }
+    try:
+        get_detail_info = get_dev_type_info[dev_type]
+    except KeyError:
+        kimchi_log.error("Unknown device type: %s", dev_type)
+        return info
+
+    return get_detail_info(info)
+
+
+def _get_net_dev_info(info):
+    cap = info.pop('capability')
+    links = {"80203": "IEEE 802.3", "80211": "IEEE 802.11"}
+    link_raw = cap['type']
+    info['link_type'] = links.get(link_raw, link_raw)
+
+    return info
+
+
+def _get_pci_dev_info(info):
+    for k in ('vendor', 'product'):
+        info[k]['description'] = info[k].pop('pyval')
+    try:
+        info['iommuGroup'] = info['iommuGroup']['number']
+    except KeyError:
+        # No IOMMU group support.
+        pass
+    return info
+
+
+def _get_scsi_dev_info(info):
+    return info
+
+
+def _get_scsi_generic_dev_info(info):
+    # scsi_generic is not documented in libvirt official website. Try to
+    # parse scsi_generic according to the following libvirt path series.
+    # https://www.redhat.com/archives/libvir-list/2013-June/msg00014.html
+    return info
+
+
+def _get_scsi_host_dev_info(info):
+    try:
+        cap_info = info.pop('capability')
+    except KeyError:
+        # kimchi.model.libvirtstoragepool.ScsiPoolDef assumes
+        # info['adapter']['type'] always exists.
+        info['adapter'] = {'type': ''}
+        return info
+    info['adapter'] = cap_info
+    return info
+
+
+def _get_scsi_target_dev_info(info):
+    # scsi_target is not documented in libvirt official website. Try to
+    # parse scsi_target according to the libvirt commit db19834a0a.
+    return info
+
+
+def _get_storage_dev_info(info):
+    try:
+        cap_info = info.pop('capability')
+    except KeyError:
+        return info
+
+    if cap_info['type'] == 'removable':
+        cap_info['available'] = bool(cap_info.pop('media_available'))
+        if cap_info['available']:
+            cap_info.update({'size': cap_info.pop('media_size'),
+                             'label': cap_info.pop('media_label')})
+    info['media'] = cap_info
+    return info
+
+
+def _get_system_dev_info(info):
+    return info
+
+
+def _get_usb_dev_info(info):
+    return info
+
+
+def _get_usb_device_dev_info(info):
+    for k in ('vendor', 'product'):
+        try:
+            info[k]['description'] = info[k].pop('pyval')
+        except KeyError:
+            # Some USB devices don't provide vendor/product description.
+            pass
+    return info
+
+
+# For test and debug
+def _print_host_dev_tree():
+    dev_infos = _get_all_host_dev_infos()
+    root = _get_dev_info_tree(dev_infos)
+    if root is None:
+        print "No device found"
+        return
+    print '-----------------'
+    print '\n'.join(_format_dev_node(root))
+
+
+def _format_dev_node(node):
+    from pprint import pformat
+
+    try:
+        children = node['children']
+        del node['children']
+    except KeyError:
+        children = []
+
+    lines = []
+    lines.extend([' ~' + line for line in pformat(node).split('\n')])
+
+    count = len(children)
+    for i, child in enumerate(children):
+        if count == 1:
+            lines.append('   \-----------------')
+        else:
+            lines.append('   +-----------------')
+        clines = _format_dev_node(child)
+        if i == count - 1:
+            p = '    '
+        else:
+            p = '   |'
+        lines.extend([p + cline for cline in clines])
+    lines.append('')
+
+    return lines
+
+
+if __name__ == '__main__':
+    _print_host_dev_tree()
diff --git a/src/kimchi/mockmodel.py b/src/kimchi/mockmodel.py
index 05720f4..7420e7e 100644
--- a/src/kimchi/mockmodel.py
+++ b/src/kimchi/mockmodel.py
@@ -493,9 +493,10 @@ class MockModel(object):
     def device_lookup(self, nodedev_name):
         return {
             'name': nodedev_name,
-            'adapter_type': 'fc_host',
-            'wwnn': uuid.uuid4().hex[:16],
-            'wwpn': uuid.uuid4().hex[:16]}
+            'adapter': {
+                'type': 'fc_host',
+                'wwnn': uuid.uuid4().hex[:16],
+                'wwpn': uuid.uuid4().hex[:16]}}
 
     def isopool_lookup(self, name):
         return {'state': 'active',
diff --git a/src/kimchi/model/host.py b/src/kimchi/model/host.py
index e9ac487..f4bd613 100644
--- a/src/kimchi/model/host.py
+++ b/src/kimchi/model/host.py
@@ -28,6 +28,7 @@ import psutil
 from cherrypy.process.plugins import BackgroundTask
 
 from kimchi import disks
+from kimchi import hostdev
 from kimchi import netinfo
 from kimchi import xmlutils
 from kimchi.basemodel import Singleton
@@ -285,20 +286,10 @@ class DeviceModel(object):
     def lookup(self, nodedev_name):
         conn = self.conn.get()
         try:
-            dev_xml = conn.nodeDeviceLookupByName(nodedev_name).XMLDesc(0)
+            dev = conn.nodeDeviceLookupByName(nodedev_name)
         except:
             raise NotFoundError('KCHHOST0003E', {'name': nodedev_name})
-        cap_type = xmlutils.xpath_get_text(
-            dev_xml, '/device/capability/capability/@type')
-        wwnn = xmlutils.xpath_get_text(
-            dev_xml, '/device/capability/capability/wwnn')
-        wwpn = xmlutils.xpath_get_text(
-            dev_xml, '/device/capability/capability/wwpn')
-        return {
-            'name': nodedev_name,
-            'adapter_type': cap_type[0] if len(cap_type) >= 1 else '',
-            'wwnn': wwnn[0] if len(wwnn) == 1 else '',
-            'wwpn': wwpn[0] if len(wwpn) == 1 else ''}
+        return hostdev.get_dev_info(dev)
 
 
 class PackagesUpdateModel(object):
diff --git a/src/kimchi/model/libvirtstoragepool.py b/src/kimchi/model/libvirtstoragepool.py
index 47b239b..b15bf1a 100644
--- a/src/kimchi/model/libvirtstoragepool.py
+++ b/src/kimchi/model/libvirtstoragepool.py
@@ -180,34 +180,34 @@ class ScsiPoolDef(StoragePoolDef):
         self.poolArgs['source']['name'] = tmp_name.replace('scsi_', '')
         # fc_host adapters type are only available in libvirt >= 1.0.5
         if not self.poolArgs['fc_host_support']:
-            self.poolArgs['source']['adapter_type'] = 'scsi_host'
+            self.poolArgs['source']['adapter']['type'] = 'scsi_host'
             msg = "Libvirt version <= 1.0.5. Setting SCSI host name as '%s'; "\
                   "setting SCSI adapter type as 'scsi_host'; "\
                   "ignoring wwnn and wwpn." % tmp_name
             kimchi_log.info(msg)
         # Path for Fibre Channel scsi hosts
         self.poolArgs['path'] = '/dev/disk/by-path'
-        if not self.poolArgs['source']['adapter_type']:
-            self.poolArgs['source']['adapter_type'] = 'scsi_host'
+        if not self.poolArgs['source']['adapter']['type']:
+            self.poolArgs['source']['adapter']['type'] = 'scsi_host'
 
     @property
     def xml(self):
         # Required parameters
         # name:
-        # source[adapter_type]:
+        # source[adapter][type]:
         # source[name]:
-        # source[wwnn]:
-        # source[wwpn]:
+        # source[adapter][wwnn]:
+        # source[adapter][wwpn]:
         # path:
 
         xml = """
         <pool type='scsi'>
           <name>{name}</name>
           <source>
-            <adapter type='{source[adapter_type]}'\
+            <adapter type='{source[adapter][type]}'\
                      name='{source[name]}'\
-                     wwnn='{source[wwnn]}'\
-                     wwpn='{source[wwpn]}'/>
+                     wwnn='{source[adapter][wwnn]}'\
+                     wwpn='{source[adapter][wwpn]}'/>
           </source>
           <target>
             <path>{path}</path>
diff --git a/src/kimchi/xmlutils.py b/src/kimchi/xmlutils.py
index 76f0696..56517f2 100644
--- a/src/kimchi/xmlutils.py
+++ b/src/kimchi/xmlutils.py
@@ -18,6 +18,7 @@
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA
 
 import libxml2
+from lxml import objectify
 
 
 from xml.etree import ElementTree
@@ -26,7 +27,7 @@ from xml.etree import ElementTree
 def xpath_get_text(xml, expr):
     doc = libxml2.parseDoc(xml)
     res = doc.xpathEval(expr)
-    ret = [None if x.children == None else x.children.content for x in res]
+    ret = [None if x.children is None else x.children.content for x in res]
 
     doc.freeDoc()
     return ret
@@ -37,3 +38,26 @@ def xml_item_update(xml, xpath, value):
     item = root.find(xpath)
     item.text = value
     return ElementTree.tostring(root, encoding="utf-8")
+
+
+def dictize(xmlstr):
+    root = objectify.fromstring(xmlstr)
+    return {root.tag: _dictize(root)}
+
+
+def _dictize(e):
+    d = {}
+    if e.text is not None:
+        if not e.attrib and e.countchildren() == 0:
+            return e.pyval
+        d['pyval'] = e.pyval
+    d.update(e.attrib)
+    for child in e.iterchildren():
+        if child.tag in d:
+            continue
+        if len(child) > 1:
+            d[child.tag] = [
+                _dictize(same_tag_child) for same_tag_child in child]
+        else:
+            d[child.tag] = _dictize(child)
+    return d
diff --git a/tests/test_rest.py b/tests/test_rest.py
index 7ed94cb..cb1ae9a 100644
--- a/tests/test_rest.py
+++ b/tests/test_rest.py
@@ -158,9 +158,9 @@ class RestTests(unittest.TestCase):
         nodedev = json.loads(self.request('/host/devices/scsi_host4').read())
         # Mockmodel generates random wwpn and wwnn
         self.assertEquals('scsi_host4', nodedev['name'])
-        self.assertEquals('fc_host', nodedev['adapter_type'])
-        self.assertEquals(16, len(nodedev['wwpn']))
-        self.assertEquals(16, len(nodedev['wwnn']))
+        self.assertEquals('fc_host', nodedev['adapter']['type'])
+        self.assertEquals(16, len(nodedev['adapter']['wwpn']))
+        self.assertEquals(16, len(nodedev['adapter']['wwnn']))
 
     def test_get_vms(self):
         vms = json.loads(self.request('/vms').read())
diff --git a/tests/test_storagepool.py b/tests/test_storagepool.py
index 22b4943..3e3ad83 100644
--- a/tests/test_storagepool.py
+++ b/tests/test_storagepool.py
@@ -145,9 +145,10 @@ class storagepoolTests(unittest.TestCase):
                  'path': '/dev/disk/by-path',
                  'source': {
                      'name': 'scsi_host3',
-                     'adapter_type': 'fc_host',
-                     'wwpn': '0123456789abcdef',
-                     'wwnn': 'abcdef0123456789'}},
+                     'adapter': {
+                         'type': 'fc_host',
+                         'wwpn': '0123456789abcdef',
+                         'wwnn': 'abcdef0123456789'}}},
              'xml':
              """
              <pool type='scsi'>
-- 
1.9.0




More information about the Kimchi-devel mailing list