From f7e080c8bfeae354b4ca17e41663f8b84c4e7e1a Mon Sep 17 00:00:00 2001
From: Szymon Borkowski <szymon.borkowski@intel.com>
Date: Mon, 6 Jun 2016 15:16:11 +0200
Subject: [PATCH] Add PCI devices collector to inspector

Adds a new collector, which gathers list of PCI devices.
Each entry is a dictionary containing 2 keys:
- vendor-id
- product-id
Such information can then be used by the inspector to distinguish
appropriate PCI devices.

Change-Id: Id7521d66410e7d408d7eada692b6123e769ce084
Partial-Bug: #1580893
---
 ironic_python_agent/inspector.py              | 50 +++++++++++++++++
 .../tests/unit/test_inspector.py              | 55 +++++++++++++++++++
 ...add-pci-devices-info-3f86934a505d1b31.yaml |  9 +++
 setup.cfg                                     |  1 +
 4 files changed, 115 insertions(+)
 create mode 100644 releasenotes/notes/add-pci-devices-info-3f86934a505d1b31.yaml

diff --git a/ironic_python_agent/inspector.py b/ironic_python_agent/inspector.py
index 715baf189..3c59f9e3c 100644
--- a/ironic_python_agent/inspector.py
+++ b/ironic_python_agent/inspector.py
@@ -16,6 +16,7 @@
 import base64
 import io
 import json
+import os
 import tarfile
 import time
 
@@ -381,3 +382,52 @@ def collect_extra_hardware(data, failures):
     except ValueError as exc:
         msg = 'JSON returned from hardware-detect cannot be decoded: %s'
         failures.add(msg, exc)
+
+
+def collect_pci_devices_info(data, failures):
+    """Collect a list of PCI devices.
+
+    Each PCI device entry in list is a dictionary containing vendor_id and
+    product_id keys, which will be then used by the ironic inspector to
+    distinguish various PCI devices.
+
+    The data is gathered from /sys/bus/pci/devices directory.
+
+    :param data: mutable data that we'll send to inspector
+    :param failures: AccumulatedFailures object
+    """
+    pci_devices_path = '/sys/bus/pci/devices'
+    pci_devices_info = []
+    try:
+        subdirs = os.listdir(pci_devices_path)
+    except OSError as exc:
+        msg = 'Failed to get list of PCI devices: %s'
+        failures.add(msg, exc)
+        return
+    for subdir in subdirs:
+        if not os.path.isdir(os.path.join(pci_devices_path, subdir)):
+            continue
+        try:
+            # note(sborkows): ids located in files inside PCI devices
+            # directory are stored in hex format (0x1234 for example) and
+            # we only need that part after 'x' delimiter
+            with open(os.path.join(pci_devices_path, subdir,
+                                   'vendor')) as vendor_file:
+                vendor = vendor_file.read().strip().split('x')[1]
+            with open(os.path.join(pci_devices_path, subdir,
+                                   'device')) as vendor_device:
+                device = vendor_device.read().strip().split('x')[1]
+        except IOError as exc:
+            LOG.warning('Failed to gather vendor id or product id '
+                        'from PCI device %s: %s', subdir, exc)
+            continue
+        except IndexError as exc:
+            LOG.warning('Wrong format of vendor id or product id in PCI '
+                        'device %s: %s', subdir, exc)
+            continue
+        LOG.debug(
+            'Found a PCI device with vendor id %s and product id %s',
+            vendor, device)
+        pci_devices_info.append({'vendor_id': vendor,
+                                 'product_id': device})
+    data['pci_devices'] = pci_devices_info
diff --git a/ironic_python_agent/tests/unit/test_inspector.py b/ironic_python_agent/tests/unit/test_inspector.py
index cb75d7913..dfeb3a678 100644
--- a/ironic_python_agent/tests/unit/test_inspector.py
+++ b/ironic_python_agent/tests/unit/test_inspector.py
@@ -17,6 +17,7 @@ import base64
 import collections
 import copy
 import io
+import os
 import tarfile
 import time
 
@@ -461,6 +462,60 @@ class TestCollectExtraHardware(test_base.BaseTestCase):
         mock_execute.assert_called_once_with('hardware-detect')
 
 
+@mock.patch.object(os, 'listdir', autospec=True)
+class TestCollectPciDevicesInfo(test_base.BaseTestCase):
+    def setUp(self):
+        super(TestCollectPciDevicesInfo, self).setUp()
+        self.data = {}
+        self.failures = utils.AccumulatedFailures()
+
+    @mock.patch.object(os.path, 'isdir', autospec=True)
+    def test_success(self, mock_isdir, mock_listdir):
+        subdirs = ['foo', 'bar']
+        mock_listdir.return_value = subdirs
+        mock_isdir.return_value = True
+        reads = ['0x1234', '0x5678', '0x9876', '0x5432']
+        expected_pci_devices = [{'vendor_id': '1234', 'product_id': '5678'},
+                                {'vendor_id': '9876', 'product_id': '5432'}]
+
+        mock_open = mock.mock_open()
+        with mock.patch('six.moves.builtins.open', mock_open):
+            mock_read = mock_open.return_value.read
+            mock_read.side_effect = reads
+            inspector.collect_pci_devices_info(self.data, self.failures)
+
+        self.assertEqual(2 * len(subdirs), mock_open.call_count)
+        self.assertListEqual(expected_pci_devices, self.data['pci_devices'])
+
+    def test_wrong_path(self, mock_listdir):
+        mock_listdir.side_effect = OSError()
+
+        inspector.collect_pci_devices_info(self.data, self.failures)
+
+        self.assertFalse('pci_devices' in self.data)
+        self.assertEqual(1, len(self.failures._failures))
+
+    @mock.patch.object(os.path, 'isdir', autospec=True)
+    def test_bad_pci_device_info(self, mock_isdir, mock_listdir):
+        subdirs = ['foo', 'bar', 'baz']
+        mock_listdir.return_value = subdirs
+        mock_isdir.return_value = True
+        reads = ['0x1234', '0x5678', '0x9876', IOError, IndexError,
+                 '0x5432']
+        expected_pci_devices = [{'vendor_id': '1234', 'product_id': '5678'}]
+
+        mock_open = mock.mock_open()
+        with mock.patch('six.moves.builtins.open', mock_open):
+            mock_read = mock_open.return_value.read
+            mock_read.side_effect = reads
+            inspector.collect_pci_devices_info(self.data, self.failures)
+
+        # note(sborkows): due to throwing IOError, the corresponding mock_open
+        # will not be called, so there are 5 mock_open calls in total
+        self.assertEqual(5, mock_open.call_count)
+        self.assertListEqual(expected_pci_devices, self.data['pci_devices'])
+
+
 @mock.patch.object(utils, 'get_agent_params', lambda: {'BOOTIF': '01-cdef'})
 @mock.patch.object(hardware, 'dispatch_to_managers', autospec=True)
 class TestWaitForDhcp(test_base.BaseTestCase):
diff --git a/releasenotes/notes/add-pci-devices-info-3f86934a505d1b31.yaml b/releasenotes/notes/add-pci-devices-info-3f86934a505d1b31.yaml
new file mode 100644
index 000000000..0a5097e9f
--- /dev/null
+++ b/releasenotes/notes/add-pci-devices-info-3f86934a505d1b31.yaml
@@ -0,0 +1,9 @@
+---
+features:
+  - Adds new PCI devices collector named "pci_devices"
+    to inspector module.
+    Data is gathered from /sys/bus/pci/devices directory
+    and is stored under "pci_devices" key as a dictionary
+    containing "vendor_id" and "product_id" keys, which
+    then will be used by the inspector to distinguish
+    various PCI devices.
diff --git a/setup.cfg b/setup.cfg
index 2d6ec623b..14a893e61 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -33,6 +33,7 @@ ironic_python_agent.inspector.collectors =
     default = ironic_python_agent.inspector:collect_default
     logs = ironic_python_agent.inspector:collect_logs
     extra-hardware = ironic_python_agent.inspector:collect_extra_hardware
+    pci-devices = ironic_python_agent.inspector:collect_pci_devices_info
 
 [pbr]
 autodoc_index_modules = True