]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/orchestrator: move InventoryDevice to python-common
authorSebastian Wagner <sebastian.wagner@suse.com>
Wed, 16 Oct 2019 11:07:31 +0000 (13:07 +0200)
committerSebastian Wagner <sebastian.wagner@suse.com>
Mon, 11 Nov 2019 09:57:05 +0000 (10:57 +0100)
Adapted:
* `mgr/dashboard`
* `mgr/ansible`
* `mgr/rook`
* `mgr/ssh`
* `mgr/orchestrator_cli`

Signed-off-by: Sebastian Wagner <sebastian.wagner@suse.com>
Co-authored--by: Kiefer Chang <kiefer.chang@suse.com>
20 files changed:
doc/mgr/orchestrator_modules.rst
qa/tasks/mgr/dashboard/test_orchestrator.py
src/pybind/mgr/ansible/output_wizards.py
src/pybind/mgr/ansible/requirements.txt
src/pybind/mgr/ansible/tests/test_output_wizards.py
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/inventory/inventory.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/inventory/inventory.model.ts
src/pybind/mgr/dashboard/requirements.txt
src/pybind/mgr/dashboard/tests/test_host.py
src/pybind/mgr/dashboard/tests/test_orchestrator.py
src/pybind/mgr/deepsea/module.py
src/pybind/mgr/orchestrator.py
src/pybind/mgr/orchestrator_cli/module.py
src/pybind/mgr/orchestrator_cli/requirements.txt
src/pybind/mgr/orchestrator_cli/test_orchestrator.py
src/pybind/mgr/orchestrator_cli/tox.ini
src/pybind/mgr/rook/module.py
src/pybind/mgr/ssh/module.py
src/pybind/mgr/test_orchestrator/module.py
src/python-common/ceph/tests/test_inventory.py [new file with mode: 0644]

index c68b6dd8ed3578a67a3cb0a83a4e733324c08a96..1d52a253dc422309e007a2eaaecc41329bff8082 100644 (file)
@@ -230,11 +230,19 @@ Inventory and status
 
 .. automethod:: Orchestrator.get_inventory
 .. autoclass:: InventoryFilter
-.. autoclass:: InventoryNode
 
-.. autoclass:: InventoryDevice
+.. py:currentmodule:: ceph.deployment.inventory
+
+.. autoclass:: Devices
+   :members:
+
+.. autoclass:: Device
    :members:
 
+.. py:currentmodule:: orchestrator
+
+
+
 .. automethod:: Orchestrator.describe_service
 .. autoclass:: ServiceDescription
 
index be541b8d1790ad16c0d8e96cdd135c911dcd7403..cfa4f9cba75ca29ee531d3b94f8790185f7ea910 100644 (file)
@@ -12,10 +12,7 @@ test_data = {
             'name': 'test-host0',
             'devices': [
                 {
-                    'type': 'hdd',
-                    'id': '/dev/sda',
-                    'size': 1024**4 * 4,
-                    'rotates': True
+                    'path': '/dev/sda',
                 }
             ]
         },
@@ -23,10 +20,7 @@ test_data = {
             'name': 'test-host1',
             'devices': [
                 {
-                    'type': 'hdd',
-                    'id': '/dev/sda',
-                    'size': 1024**4 * 4,
-                    'rotates': True
+                    'path': '/dev/sdb',
                 }
             ]
         }
@@ -94,8 +88,8 @@ class OrchestratorControllerTest(DashboardTestCase):
 
         if not data['devices']:
             return
-        test_devices = sorted(data['devices'], key=lambda d: d['id'])
-        resp_devices = sorted(resp_data['devices'], key=lambda d: d['id'])
+        test_devices = sorted(data['devices'], key=lambda d: d['path'])
+        resp_devices = sorted(resp_data['devices'], key=lambda d: d['path'])
 
         for test, resp in zip(test_devices, resp_devices):
             self._validate_device(test, resp)
index 6c157a0d17fe8fba97520848121a3b5ed3000dcb..a49b70d478a091954e9f6cd232b0ecba8456e9c5 100644 (file)
@@ -7,8 +7,8 @@ completion objects
 
 import json
 
-
-from orchestrator import InventoryDevice, InventoryNode
+from ceph.deployment import inventory
+from orchestrator import InventoryNode
 
 from .ansible_runner_svc import EVENT_DATA_URL
 
@@ -75,11 +75,7 @@ class ProcessInventory(OutputWizard):
                 host = event_data["host"]
 
                 devices = json.loads(event_data["res"]["stdout"])
-                devs = []
-                for storage_device in devices:
-                    dev = InventoryDevice.from_ceph_volume_inventory(storage_device)
-                    devs.append(dev)
-
+                devs = inventory.Devices.from_json(devices)
                 inventory_nodes.append(InventoryNode(host, devs))
 
 
@@ -140,7 +136,7 @@ class ProcessHostsList(OutputWizard):
             json_resp = json.loads(host_ls_json)
 
             for host in json_resp["data"]["hosts"]:
-                inventory_nodes.append(InventoryNode(host, []))
+                inventory_nodes.append(InventoryNode(host, inventory.Devices([])))
 
         except ValueError:
             self.log.exception("Malformed json response")
index 11914371262ef40012e0750057a5304c21aee6a8..c6ddb76f9f1e7f06a5f227d66b2801896eec2f62 100644 (file)
@@ -2,3 +2,4 @@ tox==2.9.1
 pytest
 mock
 requests-mock
+-e ../../../python-common
index 2a3a9017d3dab442d088c4b8d1d315dce8a344fb..3c3437659d4fa93f86740a5e541a264ab3a111bf 100644 (file)
@@ -199,9 +199,9 @@ class OutputWizardProcessInventory(unittest.TestCase):
         self.assertEqual(nodes_list[0].name, "192.168.121.144")
 
         # Devices
-        self.assertTrue(len(nodes_list[0].devices), 4)
+        self.assertTrue(len(nodes_list[0].devices.devices), 4)
 
         expected_device_ids = ["/dev/sdc", "/dev/sda", "/dev/sdb", "/dev/vda"]
-        device_ids = [dev.id for dev in nodes_list[0].devices]
+        device_ids = [dev.path for dev in nodes_list[0].devices.devices]
 
         self.assertEqual(expected_device_ids, device_ids)
index 6a4809d151fae163b5182228424e1f8263dfbf93..9bbcac8328c9acbe4b236e78e3e176b19e12a10b 100644 (file)
@@ -44,23 +44,23 @@ export class InventoryComponent implements OnChanges, OnInit {
     this.columns = [
       {
         name: this.i18n('Device path'),
-        prop: 'id',
+        prop: 'path',
         flexGrow: 1
       },
       {
         name: this.i18n('Type'),
-        prop: 'type',
+        prop: 'human_readable_type',
         flexGrow: 1
       },
       {
         name: this.i18n('Size'),
-        prop: 'size',
+        prop: 'sys_api.size',
         flexGrow: 1,
         pipe: this.dimlessBinary
       },
       {
         name: this.i18n('Rotates'),
-        prop: 'rotates',
+        prop: 'sys_api.rotational',
         flexGrow: 1
       },
       {
@@ -70,7 +70,7 @@ export class InventoryComponent implements OnChanges, OnInit {
       },
       {
         name: this.i18n('Model'),
-        prop: 'model',
+        prop: 'sys_api.model',
         flexGrow: 1
       },
       {
@@ -128,7 +128,7 @@ export class InventoryComponent implements OnChanges, OnInit {
         data.forEach((node: InventoryNode) => {
           node.devices.forEach((device: Device) => {
             device.hostname = node.name;
-            device.uid = `${node.name}-${device.id}`;
+            device.uid = `${node.name}-${device.device_id}`;
             devices.push(device);
           });
         });
index 2757703775f1ed8c2980112df3b4971569ef3041..b7cd2ea9a0590dd9185dffe3d67c24c323719671 100644 (file)
@@ -1,16 +1,22 @@
+export class SysAPI {
+  vendor: string;
+  model: string;
+  size: number;
+  rotational: string;
+  human_readable_size: string;
+}
+
 export class Device {
   hostname: string;
   uid: string;
   osd_ids: number[];
 
-  blank: boolean;
-  type: string;
-  id: string;
-  size: number;
-  rotates: boolean;
+  path: string;
+  sys_api: SysAPI;
   available: boolean;
-  dev_id: string;
-  extended: any;
+  rejected_reasons: string[];
+  device_id: string;
+  human_readable_type: string;
 }
 
 export class InventoryNode {
index 8661c59998f66b44da128b89f15a89d4acfabed4..eca83046077e6756d101546ccc63f4d2d0ac15a2 100644 (file)
@@ -8,3 +8,4 @@ python3-saml
 requests
 Routes
 six
+../../../python-common
index 23bd75390a60ae00c3ea5ec677561f2f7760ac00..2e6bbc624a8c8011ed14aefe46b9ccbd023f9095 100644 (file)
@@ -80,7 +80,7 @@ class TestHosts(unittest.TestCase):
         fake_client = mock.Mock()
         fake_client.available.return_value = True
         fake_client.hosts.list.return_value = [
-            InventoryNode('node1', []), InventoryNode('node2', [])]
+            InventoryNode('node1'), InventoryNode('node2')]
         instance.return_value = fake_client
 
         hosts = get_hosts()
index 332f4b71e72641b814d83ad3021d9a1d728d52d5..8fa191a51dbca29914a6f4acd21ef360fe105303 100644 (file)
@@ -4,6 +4,8 @@ try:
 except ImportError:
     from unittest import mock
 
+from ceph.deployment.inventory import Devices
+
 from orchestrator import InventoryNode, ServiceDescription
 
 from . import ControllerTestCase
index 734a457d8587e50e81b354c9edd8d545faee1dab..114f6aefff94ed9e7b4e9f52592a37b63995e395 100644 (file)
@@ -14,6 +14,7 @@ import requests
 
 from threading import Event, Thread, Lock
 
+from ceph.deployment import inventory
 from mgr_module import MgrModule
 import orchestrator
 
@@ -154,7 +155,7 @@ class DeepSeaOrchestrator(MgrModule, orchestrator.Orchestrator):
                         # nodes, the cache will never be populated, and you'll always have
                         # the full round trip to DeepSea.
                         self.inventory_cache[node_name] = orchestrator.OutdatableData(node_devs)
-                    devs = orchestrator.InventoryDevice.from_ceph_volume_inventory_list(node_devs)
+                    devs = inventory.Devices.from_json(node_devs)
                     result.append(orchestrator.InventoryNode(node_name, devs))
             else:
                 self.log.error(event_data['return'])
index dc9e57f2fe9ed7a686b0b80279d99932469626fa..fe2dc230265324cc906077b1c4d3757494395fbe 100644 (file)
@@ -14,6 +14,8 @@ import random
 import datetime
 import copy
 
+from ceph.deployment import inventory
+
 from mgr_module import MgrModule, PersistentStoreDict
 from mgr_util import format_bytes
 
@@ -836,107 +838,6 @@ class InventoryFilter(object):
         self.nodes = nodes  # Optional: get info about certain named nodes only
 
 
-class InventoryDevice(object):
-    """
-    When fetching inventory, block devices are reported in this format.
-
-    Note on device identifiers: the format of this is up to the orchestrator,
-    but the same identifier must also work when passed into StatefulServiceSpec.
-    The identifier should be something meaningful like a device WWID or
-    stable device node path -- not something made up by the orchestrator.
-
-    "Extended" is for reporting any special configuration that may have
-    already been done out of band on the block device.  For example, if
-    the device has already been configured for encryption, report that
-    here so that it can be indicated to the user.  The set of
-    extended properties may differ between orchestrators.  An orchestrator
-    is permitted to support no extended properties (only normal block
-    devices)
-    """
-    def __init__(self, blank=False, type=None, id=None, size=None,
-                 rotates=False, available=False, dev_id=None, extended=None,
-                 metadata_space_free=None):
-        # type: (bool, str, str, int, bool, bool, str, dict, bool) -> None
-
-        self.blank = blank
-
-        #: 'ssd', 'hdd', 'nvme'
-        self.type = type
-
-        #: unique within a node (or globally if you like).
-        self.id = id
-
-        #: byte integer.
-        self.size = size
-
-        #: indicates if it is a spinning disk
-        self.rotates = rotates
-
-        #: can be used to create a new OSD?
-        self.available = available
-
-        #: vendor/model
-        self.dev_id = dev_id
-
-        #: arbitrary JSON-serializable object
-        self.extended = extended if extended is not None else extended
-
-        # If this drive is not empty, but is suitable for appending
-        # additional journals, wals, or bluestore dbs, then report
-        # how much space is available.
-        self.metadata_space_free = metadata_space_free
-
-    def to_json(self):
-        return dict(type=self.type, blank=self.blank, id=self.id,
-                    size=self.size, rotates=self.rotates,
-                    available=self.available, dev_id=self.dev_id,
-                    extended=self.extended)
-
-    @classmethod
-    @handle_type_error
-    def from_json(cls, data):
-        return cls(**data)
-
-    @classmethod
-    def from_ceph_volume_inventory(cls, data):
-        # TODO: change InventoryDevice itself to mirror c-v inventory closely!
-
-        dev = InventoryDevice()
-        dev.id = data["path"]
-        dev.type = 'hdd' if data["sys_api"]["rotational"] == "1" else 'ssd/nvme'
-        dev.size = data["sys_api"]["size"]
-        dev.rotates = data["sys_api"]["rotational"] == "1"
-        dev.available = data["available"]
-        dev.dev_id = "%s/%s" % (data["sys_api"]["vendor"],
-                                data["sys_api"]["model"])
-        dev.extended = data
-        return dev
-
-    @classmethod
-    def from_ceph_volume_inventory_list(cls, datas):
-        return [cls.from_ceph_volume_inventory(d) for d in datas]
-
-    def pretty_print(self, only_header=False):
-        """Print a human friendly line with the information of the device
-
-        :param only_header: Print only the name of the device attributes
-
-        Ex::
-
-            Device Path           Type       Size    Rotates  Available Model
-            /dev/sdc            hdd   50.00 GB       True       True ATA/QEMU
-
-        """
-        row_format = "  {0:<15} {1:>10} {2:>10} {3:>10} {4:>10} {5:<15}\n"
-        if only_header:
-            return row_format.format("Device Path", "Type", "Size", "Rotates",
-                                     "Available", "Model")
-        else:
-            return row_format.format(str(self.id), self.type if self.type is not None else "",
-                                     format_bytes(self.size if self.size is not None else 0, 5,
-                                                  colored=False),
-                                     str(self.rotates), str(self.available),
-                                     self.dev_id if self.dev_id is not None else "")
 
 
 class InventoryNode(object):
@@ -944,22 +845,24 @@ class InventoryNode(object):
     When fetching inventory, all Devices are groups inside of an
     InventoryNode.
     """
-    def __init__(self, name, devices):
-        # type: (str, List[InventoryDevice]) -> None
-        assert isinstance(devices, list)
+    def __init__(self, name, devices=None):
+        # type: (str, inventory.Devices) -> None
+        if devices is None:
+            devices = inventory.Devices([])
+        assert isinstance(devices, inventory.Devices)
+
         self.name = name  # unique within cluster.  For example a hostname.
         self.devices = devices
 
     def to_json(self):
-        return {'name': self.name, 'devices': [d.to_json() for d in self.devices]}
+        return {'name': self.name, 'devices': self.devices.to_json()}
 
     @classmethod
     def from_json(cls, data):
         try:
             _data = copy.deepcopy(data)
             name = _data.pop('name')
-            devices = [InventoryDevice.from_json(device)
-                       for device in _data.pop('devices')]
+            devices = inventory.Devices.from_json(_data.pop('devices'))
             if _data:
                 error_msg = 'Unknown key(s) in Inventory: {}'.format(','.join(_data.keys()))
                 raise OrchestratorValidationError(error_msg)
@@ -967,10 +870,13 @@ class InventoryNode(object):
         except KeyError as e:
             error_msg = '{} is required for {}'.format(e, cls.__name__)
             raise OrchestratorValidationError(error_msg)
+        except TypeError as e:
+            raise OrchestratorValidationError('Failed to read inventory: {}'.format(e))
+
 
     @classmethod
     def from_nested_items(cls, hosts):
-        devs = InventoryDevice.from_ceph_volume_inventory_list
+        devs = inventory.Devices.from_json
         return [cls(item[0], devs(item[1].data)) for item in hosts]
 
 
index 6f9f3c30008b7c9e33bafbff9d950bd2580a37a0..174e23028839e6cf86fca73bce045f0bcbdc609b 100644 (file)
@@ -2,8 +2,11 @@ import errno
 import json
 from functools import wraps
 
+from ceph.deployment.inventory import Device
 from prettytable import PrettyTable
 
+from mgr_util import format_bytes
+
 try:
     from typing import List, Set, Optional
 except ImportError:
@@ -215,22 +218,27 @@ class OrchestratorCli(orchestrator.OrchestratorClientMixin, MgrModule):
             data = [n.to_json() for n in completion.result]
             return HandleCommandResult(stdout=json.dumps(data))
         else:
-            # Return a human readable version
-            result = ""
-
-            for inventory_node in completion.result:
-                result += "Host {0}:\n".format(inventory_node.name)
-
-                if inventory_node.devices:
-                    result += inventory_node.devices[0].pretty_print(only_header=True)
-                else:
-                    result += "No storage devices found"
-
-                for d in inventory_node.devices:
-                    result += d.pretty_print()
-                result += "\n"
-
-            return HandleCommandResult(stdout=result)
+            out = []
+
+            for host in completion.result: # type: orchestrator.InventoryNode
+                out.append('Host {}:'.format(host.name))
+                table = PrettyTable(
+                    ['Path', 'Type', 'Size', 'Available', 'Ceph Device ID', 'Reject Reasons'],
+                    border=False)
+                table._align['Path'] = 'l'
+                for d in host.devices.devices:  # type: Device
+                    table.add_row(
+                        (
+                            d.path,
+                            d.human_readable_type,
+                            format_bytes(d.sys_api.get('size', 0), 5, colored=False),
+                            d.available,
+                            d.device_id,
+                            ', '.join(d.rejected_reasons)
+                        )
+                    )
+                out.append(table.get_string())
+            return HandleCommandResult(stdout='\n'.join(out))
 
     @_read_cli('orchestrator service ls',
                "name=host,type=CephString,req=false "
index 62843e4929ea1a5ece739bbc1f8d80dd9617c49d..2585d05fc1b8892124126b88585fca05db8e9f2a 100644 (file)
@@ -1,2 +1,5 @@
 tox==2.9.1
--e ../../../python-common
\ No newline at end of file
+../../../python-common
+pytest
+mock
+requests-mock
index adc911b45732ca7514603b14acd530ec9ca9efc3..de50acb0722ebb290f14235a490a503a2ef0bc68 100644 (file)
@@ -3,25 +3,22 @@ import json
 
 import pytest
 
+from ceph.deployment import inventory
 from orchestrator import ReadCompletion, raise_if_exception, RGWSpec
-from orchestrator import InventoryNode, InventoryDevice, ServiceDescription
+from orchestrator import InventoryNode, ServiceDescription
 from orchestrator import OrchestratorValidationError
 
 
-def _test_resource(data, resource_class, extra):
-    # create the instance with normal way
-    rsc = resource_class(**data)
-    if hasattr(rsc, 'pretty_print'):
-        assert rsc.pretty_print()
-
+def _test_resource(data, resource_class, extra=None):
     # ensure we can deserialize and serialize
     rsc = resource_class.from_json(data)
     rsc.to_json()
 
-    # if there is an unexpected data provided
-    data.update(extra)
-    with pytest.raises(OrchestratorValidationError):
-        resource_class.from_json(data)
+    if extra:
+        # if there is an unexpected data provided
+        data.update(extra)
+        with pytest.raises(OrchestratorValidationError):
+            resource_class.from_json(data)
 
 
 def test_inventory():
@@ -29,16 +26,20 @@ def test_inventory():
         'name': 'host0',
         'devices': [
             {
-                'type': 'hdd',
-                'id': '/dev/sda',
-                'size': 1024,
-                'rotates': True
+                'sys_api': {
+                    'rotational': '1',
+                    'size': 1024,
+                },
+                'path': '/dev/sda',
+                'available': False,
+                'rejected_reasons': [],
+                'lvs': []
             }
         ]
     }
     _test_resource(json_data, InventoryNode, {'abc': False})
     for devices in json_data['devices']:
-        _test_resource(devices, InventoryDevice, {'abc': False})
+        _test_resource(devices, inventory.Device)
 
     json_data = [{}, {'name': 'host0'}, {'devices': []}]
     for data in json_data:
index fb3c90e712a08c3b8da278709e5e0d44c8d73ecf..cbec12983750060eb778aa889856fb352c76f6f8 100644 (file)
@@ -5,10 +5,7 @@ toxworkdir = {env:CEPH_BUILD_DIR}/orchestrator_cli
 minversion = 2.5
 
 [testenv]
-deps =
-    pytest
-    mock
-    requests-mock
+deps = -rrequirements.txt
 setenv=
     UNITTEST = true
     py27: PYTHONPATH = {toxinidir}/../../../../build/lib/cython_modules/lib.2
index 2d196ecac22554c35a83edc83088b71ca58700b2..63bb25cffdb6a2f9bcd30d9999b1dbce8f72fe14 100644 (file)
@@ -3,6 +3,8 @@ import functools
 import os
 import uuid
 
+from ceph.deployment import inventory
+
 try:
     from typing import List, Dict
     from ceph.deployment.drive_group import DriveGroupSpec
@@ -312,32 +314,24 @@ class RookOrchestrator(MgrModule, orchestrator.Orchestrator):
         for node_name, node_devs in devs.items():
             devs = []
             for d in node_devs:
-                dev = orchestrator.InventoryDevice()
-
-                # XXX CAUTION!  https://github.com/rook/rook/issues/1716
-                # Passing this through for the sake of completeness but it
-                # is not trustworthy!
-                dev.blank = d['empty']
-                dev.type = 'hdd' if d['rotational'] else 'ssd'
-                dev.id = d['name']
-                dev.size = d['size']
-
-                if d['filesystem'] == "" and not d['rotational']:
-                    # Empty or partitioned SSD
-                    partitioned_space = sum(
-                        [p['size'] for p in d['Partitions']])
-                    dev.metadata_space_free = max(0, d[
-                        'size'] - partitioned_space)
-
+                dev = inventory.Device(
+                    path=d['name'],
+                    sys_api=dict(
+                        rotational='1' if d['rotational'] else '0',
+                        size=d['size']
+                    ),
+                    available=d['empty'],
+                    rejected_reasons=[] if d['empty'] else ['not empty'],
+                )
                 devs.append(dev)
 
-            result.append(orchestrator.InventoryNode(node_name, devs))
+            result.append(orchestrator.InventoryNode(node_name, inventory.Devices(devs)))
 
         return result
 
     @deferred_read
     def get_hosts(self):
-        return [orchestrator.InventoryNode(n, []) for n in self.rook_cluster.get_node_names()]
+        return [orchestrator.InventoryNode(n, inventory.Devices([])) for n in self.rook_cluster.get_node_names()]
 
     @deferred_read
     def describe_service(self, service_type=None, service_id=None, node_name=None, refresh=False):
index 9601580462c54100bf0bf749ae39b8d3840f8b58..bc4e890003471ed82ed9ebf8fb49c7105d4c6f48 100644 (file)
@@ -10,6 +10,7 @@ import random
 import tempfile
 import multiprocessing.pool
 
+from ceph.deployment import inventory
 from mgr_module import MgrModule
 import orchestrator
 
@@ -464,7 +465,7 @@ class SSHOrchestrator(MgrModule, orchestrator.OrchestratorClientMixin):
         TODO:
           - InventoryNode probably needs to be able to report labels
         """
-        nodes = [orchestrator.InventoryNode(host_name, []) for host_name in self.inventory_cache]
+        nodes = [orchestrator.InventoryNode(host_name, inventory.Devices([])) for host_name in self.inventory_cache]
         return orchestrator.TrivialReadCompletion(nodes)
 
     def _refresh_host_services(self, host):
@@ -639,7 +640,7 @@ class SSHOrchestrator(MgrModule, orchestrator.OrchestratorClientMixin):
             else:
                 self.log.debug("reading cached inventory for '{}'".format(host))
 
-            devices = orchestrator.InventoryDevice.from_ceph_volume_inventory_list(host_info.data)
+            devices = inventory.Devices.from_json(host_info.data)
             return orchestrator.InventoryNode(host, devices)
 
         results = []
index cb097c5f2816ad71ff102c0e93c6ad2e4e14e38b..9ad65968dff9ee19c2ea430963f694b34348cab4 100644 (file)
@@ -9,6 +9,7 @@ from subprocess import check_output, CalledProcessError
 
 import six
 
+from ceph.deployment import inventory
 from mgr_module import CLICommand, HandleCommandResult
 from mgr_module import MgrModule, PersistentStoreDict
 
@@ -201,10 +202,7 @@ class TestOrchestrator(MgrModule, orchestrator.Orchestrator):
 
         for out in c_v_out.splitlines():
             self.log.error(out)
-            devs = []
-            for device in json.loads(out):
-                dev = orchestrator.InventoryDevice.from_ceph_volume_inventory(device)
-                devs.append(dev)
+            devs = inventory.Devices.from_json(json.loads(out))
             return [orchestrator.InventoryNode('localhost', devs)]
         self.log.error('c-v failed: ' + str(c_v_out))
         raise Exception('c-v failed')
@@ -289,7 +287,7 @@ class TestOrchestrator(MgrModule, orchestrator.Orchestrator):
     def get_hosts(self):
         if self._inventory:
             return self._inventory
-        return [orchestrator.InventoryNode('localhost', [])]
+        return [orchestrator.InventoryNode('localhost', inventory.Devices([]))]
 
     @deferred_write("add_host")
     def add_host(self, host):
diff --git a/src/python-common/ceph/tests/test_inventory.py b/src/python-common/ceph/tests/test_inventory.py
new file mode 100644 (file)
index 0000000..647564b
--- /dev/null
@@ -0,0 +1,163 @@
+import json
+
+from ceph.deployment.inventory import Devices
+
+
+def test_from_json():
+    data = json.loads("""
+    [
+  {
+    "available": false,
+    "rejected_reasons": [
+      "locked"
+    ],
+    "sys_api": {
+      "scheduler_mode": "",
+      "rotational": "0",
+      "vendor": "",
+      "human_readable_size": "50.00 GB",
+      "sectors": 0,
+      "sas_device_handle": "",
+      "partitions": {},
+      "rev": "",
+      "sas_address": "",
+      "locked": 1,
+      "sectorsize": "512",
+      "removable": "0",
+      "path": "/dev/dm-0",
+      "support_discard": "",
+      "model": "",
+      "ro": "0",
+      "nr_requests": "128",
+      "size": 53687091200
+    },
+    "lvs": [],
+    "path": "/dev/dm-0"
+  },
+  {
+    "available": false,
+    "rejected_reasons": [
+      "locked"
+    ],
+    "sys_api": {
+      "scheduler_mode": "",
+      "rotational": "0",
+      "vendor": "",
+      "human_readable_size": "31.47 GB",
+      "sectors": 0,
+      "sas_device_handle": "",
+      "partitions": {},
+      "rev": "",
+      "sas_address": "",
+      "locked": 1,
+      "sectorsize": "512",
+      "removable": "0",
+      "path": "/dev/dm-1",
+      "support_discard": "",
+      "model": "",
+      "ro": "0",
+      "nr_requests": "128",
+      "size": 33789313024
+    },
+    "lvs": [],
+    "path": "/dev/dm-1"
+  },
+  {
+    "available": false,
+    "rejected_reasons": [
+      "locked"
+    ],
+    "sys_api": {
+      "scheduler_mode": "",
+      "rotational": "0",
+      "vendor": "",
+      "human_readable_size": "394.27 GB",
+      "sectors": 0,
+      "sas_device_handle": "",
+      "partitions": {},
+      "rev": "",
+      "sas_address": "",
+      "locked": 1,
+      "sectorsize": "512",
+      "removable": "0",
+      "path": "/dev/dm-2",
+      "support_discard": "",
+      "model": "",
+      "ro": "0",
+      "nr_requests": "128",
+      "size": 423347879936
+    },
+    "lvs": [],
+    "path": "/dev/dm-2"
+  },
+  {
+    "available": false,
+    "rejected_reasons": [
+      "locked"
+    ],
+    "sys_api": {
+      "scheduler_mode": "cfq",
+      "rotational": "0",
+      "vendor": "ATA",
+      "human_readable_size": "476.94 GB",
+      "sectors": 0,
+      "sas_device_handle": "",
+      "partitions": {
+        "sda2": {
+          "start": "411648",
+          "holders": [],
+          "sectorsize": 512,
+          "sectors": "2097152",
+          "size": "1024.00 MB"
+        },
+        "sda3": {
+          "start": "2508800",
+          "holders": [
+            "dm-1",
+            "dm-2",
+            "dm-0"
+          ],
+          "sectorsize": 512,
+          "sectors": "997705728",
+          "size": "475.74 GB"
+        },
+        "sda1": {
+          "start": "2048",
+          "holders": [],
+          "sectorsize": 512,
+          "sectors": "409600",
+          "size": "200.00 MB"
+        }
+      },
+      "rev": "0000",
+      "sas_address": "",
+      "locked": 1,
+      "sectorsize": "512",
+      "removable": "0",
+      "path": "/dev/sda",
+      "support_discard": "",
+      "model": "SanDisk SD8SN8U5",
+      "ro": "0",
+      "nr_requests": "128",
+      "size": 512110190592
+    },
+    "lvs": [
+      {
+        "comment": "not used by ceph",
+        "name": "swap"
+      },
+      {
+        "comment": "not used by ceph",
+        "name": "home"
+      },
+      {
+        "comment": "not used by ceph",
+        "name": "root"
+      }
+    ],
+    "path": "/dev/sda"
+  }
+]""".strip())
+    ds = Devices.from_json(data)
+    assert len(ds.devices) == 4
+    assert Devices.from_json(ds.to_json()) == ds