From 4072554608abd4828f281dcc0e20ce99ed6611b9 Mon Sep 17 00:00:00 2001
From: Sheel Rana <ranasheel2000@gmail.com>
Date: Wed, 30 Mar 2016 17:05:09 +0530
Subject: [PATCH] Support for volume service list

OSC does not support to list volume services.
This patch will provide support for adding volume service related
support.

Closes-bug:#1550999

Implements: bp cinder-command-support

Change-Id: I50ac14aeb96c4b8ddbf7b33e519feea0d126f752
---
 doc/source/command-objects/volume-service.rst |  31 ++++
 doc/source/commands.rst                       |   1 +
 openstackclient/tests/volume/v1/fakes.py      |  91 +++++++++++
 .../tests/volume/v1/test_service.py           | 141 ++++++++++++++++++
 openstackclient/tests/volume/v2/fakes.py      | 113 ++++++++++++--
 .../tests/volume/v2/test_service.py           | 141 ++++++++++++++++++
 openstackclient/volume/v1/service.py          |  70 +++++++++
 openstackclient/volume/v2/service.py          |  70 +++++++++
 .../volume_service-5e352a71dfbc828d.yaml      |   9 ++
 setup.cfg                                     |   4 +
 10 files changed, 660 insertions(+), 11 deletions(-)
 create mode 100644 doc/source/command-objects/volume-service.rst
 create mode 100644 openstackclient/tests/volume/v1/test_service.py
 create mode 100644 openstackclient/tests/volume/v2/test_service.py
 create mode 100644 openstackclient/volume/v1/service.py
 create mode 100644 openstackclient/volume/v2/service.py
 create mode 100644 releasenotes/notes/volume_service-5e352a71dfbc828d.yaml

diff --git a/doc/source/command-objects/volume-service.rst b/doc/source/command-objects/volume-service.rst
new file mode 100644
index 0000000000..aa9fa6d262
--- /dev/null
+++ b/doc/source/command-objects/volume-service.rst
@@ -0,0 +1,31 @@
+==============
+volume service
+==============
+
+Volume v1, v2
+
+volume service list
+-------------------
+
+List volume service
+
+.. program:: volume service list
+.. code:: bash
+
+    os volume service list
+        [--host <host>]
+        [--service <service>]
+        [--long]
+
+.. _volume-service-list:
+.. option:: --host <host>
+
+    List services on specified host (name only)
+
+.. option:: --service <service>
+
+    List only specified service (name only)
+
+.. option:: --long
+
+    List additional fields in output
diff --git a/doc/source/commands.rst b/doc/source/commands.rst
index c54cadb1b9..45b229abf8 100644
--- a/doc/source/commands.rst
+++ b/doc/source/commands.rst
@@ -127,6 +127,7 @@ referring to both Compute and Volume quotas.
 * ``user role``: (**Identity**) roles assigned to a user
 * ``volume``: (**Volume**) block volumes
 * ``volume type``: (**Volume**) deployment-specific types of volumes available
+* ``volume service``: (**Volume**) services to manage block storage operations
 
 
 Plugin Objects
diff --git a/openstackclient/tests/volume/v1/fakes.py b/openstackclient/tests/volume/v1/fakes.py
index 42673efaa7..d6c46439c6 100644
--- a/openstackclient/tests/volume/v1/fakes.py
+++ b/openstackclient/tests/volume/v1/fakes.py
@@ -129,6 +129,97 @@ QOS_WITH_ASSOCIATIONS = {
 }
 
 
+class FakeServiceClient(object):
+
+    def __init__(self, **kwargs):
+        self.services = mock.Mock()
+        self.services.resource_class = fakes.FakeResource(None, {})
+
+
+class TestService(utils.TestCommand):
+
+    def setUp(self):
+        super(TestService, self).setUp()
+
+        self.app.client_manager.volume = FakeServiceClient(
+            endpoint=fakes.AUTH_URL,
+            token=fakes.AUTH_TOKEN
+        )
+
+
+class FakeService(object):
+    """Fake one or more Services."""
+
+    @staticmethod
+    def create_one_service(attrs=None):
+        """Create a fake service.
+
+        :param Dictionary attrs:
+            A dictionary with all attributes of service
+        :retrun:
+            A FakeResource object with host, status, etc.
+        """
+        # Set default attribute
+        service_info = {
+            'host': 'host_test',
+            'binary': 'cinder_test',
+            'status': 'enabled',
+            'disabled_reason': 'LongHoliday-GoldenWeek',
+            'zone': 'fake_zone',
+            'updated_at': 'fake_date',
+            'state': 'fake_state',
+        }
+
+        # Overwrite default attributes if there are some attributes set
+        if attrs is None:
+            attrs = {}
+        service_info.update(attrs)
+
+        service = fakes.FakeResource(
+            None,
+            service_info,
+            loaded=True)
+
+        return service
+
+    @staticmethod
+    def create_services(attrs=None, count=2):
+        """Create multiple fake services.
+
+        :param Dictionary attrs:
+            A dictionary with all attributes of service
+        :param Integer count:
+            The number of services to be faked
+        :return:
+            A list of FakeResource objects
+        """
+        services = []
+        for n in range(0, count):
+            services.append(FakeService.create_one_service(attrs))
+
+        return services
+
+    @staticmethod
+    def get_services(services=None, count=2):
+        """Get an iterable MagicMock object with a list of faked services.
+
+        If services list is provided, then initialize the Mock object with the
+        list. Otherwise create one.
+
+        :param List services:
+            A list of FakeResource objects faking services
+        :param Integer count:
+            The number of services to be faked
+        :return
+            An iterable Mock object with side_effect set to a list of faked
+            services
+        """
+        if services is None:
+            services = FakeService.create_services(count)
+
+        return mock.MagicMock(side_effect=services)
+
+
 class FakeImagev1Client(object):
 
     def __init__(self, **kwargs):
diff --git a/openstackclient/tests/volume/v1/test_service.py b/openstackclient/tests/volume/v1/test_service.py
new file mode 100644
index 0000000000..7168434496
--- /dev/null
+++ b/openstackclient/tests/volume/v1/test_service.py
@@ -0,0 +1,141 @@
+#
+#   Licensed under the Apache License, Version 2.0 (the "License"); you may
+#   not use this file except in compliance with the License. You may obtain
+#   a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#   WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#   License for the specific language governing permissions and limitations
+#   under the License.
+#
+
+
+from openstackclient.tests.volume.v1 import fakes as service_fakes
+from openstackclient.volume.v1 import service
+
+
+class TestService(service_fakes.TestService):
+
+    def setUp(self):
+        super(TestService, self).setUp()
+
+        # Get a shortcut to the ServiceManager Mock
+        self.service_mock = self.app.client_manager.volume.services
+        self.service_mock.reset_mock()
+
+
+class TestServiceList(TestService):
+
+    # The service to be listed
+    services = service_fakes.FakeService.create_one_service()
+
+    def setUp(self):
+        super(TestServiceList, self).setUp()
+
+        self.service_mock.list.return_value = [self.services]
+
+        # Get the command object to test
+        self.cmd = service.ListService(self.app, None)
+
+    def test_service_list(self):
+        arglist = [
+            '--host', self.services.host,
+            '--service', self.services.binary,
+        ]
+        verifylist = [
+            ('host', self.services.host),
+            ('service', self.services.binary),
+        ]
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+        # In base command class Lister in cliff, abstract method take_action()
+        # returns a tuple containing the column names and an iterable
+        # containing the data to be listed.
+        columns, data = self.cmd.take_action(parsed_args)
+
+        expected_columns = [
+            'Binary',
+            'Host',
+            'Zone',
+            'Status',
+            'State',
+            'Updated At',
+        ]
+
+        # confirming if all expected columns are present in the result.
+        self.assertEqual(expected_columns, columns)
+
+        datalist = ((
+            self.services.binary,
+            self.services.host,
+            self.services.zone,
+            self.services.status,
+            self.services.state,
+            self.services.updated_at,
+        ), )
+
+        # confirming if all expected values are present in the result.
+        self.assertEqual(datalist, tuple(data))
+
+        # checking if proper call was made to list services
+        self.service_mock.list.assert_called_with(
+            self.services.host,
+            self.services.binary,
+        )
+
+        # checking if prohibited columns are present in output
+        self.assertNotIn("Disabled Reason", columns)
+        self.assertNotIn(self.services.disabled_reason,
+                         tuple(data))
+
+    def test_service_list_with_long_option(self):
+        arglist = [
+            '--host', self.services.host,
+            '--service', self.services.binary,
+            '--long'
+        ]
+        verifylist = [
+            ('host', self.services.host),
+            ('service', self.services.binary),
+            ('long', True)
+        ]
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+        # In base command class Lister in cliff, abstract method take_action()
+        # returns a tuple containing the column names and an iterable
+        # containing the data to be listed.
+        columns, data = self.cmd.take_action(parsed_args)
+
+        expected_columns = [
+            'Binary',
+            'Host',
+            'Zone',
+            'Status',
+            'State',
+            'Updated At',
+            'Disabled Reason'
+        ]
+
+        # confirming if all expected columns are present in the result.
+        self.assertEqual(expected_columns, columns)
+
+        datalist = ((
+            self.services.binary,
+            self.services.host,
+            self.services.zone,
+            self.services.status,
+            self.services.state,
+            self.services.updated_at,
+            self.services.disabled_reason,
+        ), )
+
+        # confirming if all expected values are present in the result.
+        self.assertEqual(datalist, tuple(data))
+
+        self.service_mock.list.assert_called_with(
+            self.services.host,
+            self.services.binary,
+        )
diff --git a/openstackclient/tests/volume/v2/fakes.py b/openstackclient/tests/volume/v2/fakes.py
index 97bbc59bce..120666a059 100644
--- a/openstackclient/tests/volume/v2/fakes.py
+++ b/openstackclient/tests/volume/v2/fakes.py
@@ -232,6 +232,97 @@ EXTENSION = {
 }
 
 
+class FakeServiceClient(object):
+
+    def __init__(self, **kwargs):
+        self.services = mock.Mock()
+        self.services.resource_class = fakes.FakeResource(None, {})
+
+
+class TestService(utils.TestCommand):
+
+    def setUp(self):
+        super(TestService, self).setUp()
+
+        self.app.client_manager.volume = FakeServiceClient(
+            endpoint=fakes.AUTH_URL,
+            token=fakes.AUTH_TOKEN
+        )
+
+
+class FakeService(object):
+    """Fake one or more Services."""
+
+    @staticmethod
+    def create_one_service(attrs=None):
+        """Create a fake service.
+
+        :param Dictionary attrs:
+            A dictionary with all attributes of service
+        :retrun:
+            A FakeResource object with host, status, etc.
+        """
+        # Set default attribute
+        service_info = {
+            'host': 'host_test',
+            'binary': 'cinder_test',
+            'status': 'enabled',
+            'disabled_reason': 'LongHoliday-GoldenWeek',
+            'zone': 'fake_zone',
+            'updated_at': 'fake_date',
+            'state': 'fake_state',
+        }
+
+        # Overwrite default attributes if there are some attributes set
+        if attrs is None:
+            attrs = {}
+        service_info.update(attrs)
+
+        service = fakes.FakeResource(
+            None,
+            service_info,
+            loaded=True)
+
+        return service
+
+    @staticmethod
+    def create_services(attrs=None, count=2):
+        """Create multiple fake services.
+
+        :param Dictionary attrs:
+            A dictionary with all attributes of service
+        :param Integer count:
+            The number of services to be faked
+        :return:
+            A list of FakeResource objects
+        """
+        services = []
+        for n in range(0, count):
+            services.append(FakeService.create_one_service(attrs))
+
+        return services
+
+    @staticmethod
+    def get_services(services=None, count=2):
+        """Get an iterable MagicMock object with a list of faked services.
+
+        If services list is provided, then initialize the Mock object with the
+        list. Otherwise create one.
+
+        :param List services:
+            A list of FakeResource objects faking services
+        :param Integer count:
+            The number of services to be faked
+        :return
+            An iterable Mock object with side_effect set to a list of faked
+            services
+        """
+        if services is None:
+            services = FakeService.create_services(count)
+
+        return mock.MagicMock(side_effect=services)
+
+
 class FakeVolumeClient(object):
 
     def __init__(self, **kwargs):
@@ -243,6 +334,8 @@ class FakeVolumeClient(object):
         self.backups.resource_class = fakes.FakeResource(None, {})
         self.volume_types = mock.Mock()
         self.volume_types.resource_class = fakes.FakeResource(None, {})
+        self.volume_type_access = mock.Mock()
+        self.volume_type_access.resource_class = fakes.FakeResource(None, {})
         self.restores = mock.Mock()
         self.restores.resource_class = fakes.FakeResource(None, {})
         self.qos_specs = mock.Mock()
@@ -279,7 +372,7 @@ class FakeVolume(object):
     """
 
     @staticmethod
-    def create_one_volume(attrs={}):
+    def create_one_volume(attrs=None):
         """Create a fake volume.
 
         :param Dictionary attrs:
@@ -287,6 +380,8 @@ class FakeVolume(object):
         :retrun:
             A FakeResource object with id, name, status, etc.
         """
+        attrs = attrs or {}
+
         # Set default attribute
         volume_info = {
             'id': 'volume-id' + uuid.uuid4().hex,
@@ -318,7 +413,7 @@ class FakeVolume(object):
         return volume
 
     @staticmethod
-    def create_volumes(attrs={}, count=2):
+    def create_volumes(attrs=None, count=2):
         """Create multiple fake volumes.
 
         :param Dictionary attrs:
@@ -359,16 +454,16 @@ class FakeAvailabilityZone(object):
     """Fake one or more volume availability zones (AZs)."""
 
     @staticmethod
-    def create_one_availability_zone(attrs={}, methods={}):
+    def create_one_availability_zone(attrs=None):
         """Create a fake AZ.
 
         :param Dictionary attrs:
             A dictionary with all attributes
-        :param Dictionary methods:
-            A dictionary with all methods
         :return:
             A FakeResource object with zoneName, zoneState, etc.
         """
+        attrs = attrs or {}
+
         # Set default attributes.
         availability_zone = {
             'zoneName': uuid.uuid4().hex,
@@ -380,18 +475,15 @@ class FakeAvailabilityZone(object):
 
         availability_zone = fakes.FakeResource(
             info=copy.deepcopy(availability_zone),
-            methods=methods,
             loaded=True)
         return availability_zone
 
     @staticmethod
-    def create_availability_zones(attrs={}, methods={}, count=2):
+    def create_availability_zones(attrs=None, count=2):
         """Create multiple fake AZs.
 
         :param Dictionary attrs:
             A dictionary with all attributes
-        :param Dictionary methods:
-            A dictionary with all methods
         :param int count:
             The number of AZs to fake
         :return:
@@ -400,8 +492,7 @@ class FakeAvailabilityZone(object):
         availability_zones = []
         for i in range(0, count):
             availability_zone = \
-                FakeAvailabilityZone.create_one_availability_zone(
-                    attrs, methods)
+                FakeAvailabilityZone.create_one_availability_zone(attrs)
             availability_zones.append(availability_zone)
 
         return availability_zones
diff --git a/openstackclient/tests/volume/v2/test_service.py b/openstackclient/tests/volume/v2/test_service.py
new file mode 100644
index 0000000000..ba2e1b3217
--- /dev/null
+++ b/openstackclient/tests/volume/v2/test_service.py
@@ -0,0 +1,141 @@
+#
+#   Licensed under the Apache License, Version 2.0 (the "License"); you may
+#   not use this file except in compliance with the License. You may obtain
+#   a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#   WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#   License for the specific language governing permissions and limitations
+#   under the License.
+#
+
+
+from openstackclient.tests.volume.v2 import fakes as service_fakes
+from openstackclient.volume.v2 import service
+
+
+class TestService(service_fakes.TestService):
+
+    def setUp(self):
+        super(TestService, self).setUp()
+
+        # Get a shortcut to the ServiceManager Mock
+        self.service_mock = self.app.client_manager.volume.services
+        self.service_mock.reset_mock()
+
+
+class TestServiceList(TestService):
+
+    # The service to be listed
+    services = service_fakes.FakeService.create_one_service()
+
+    def setUp(self):
+        super(TestServiceList, self).setUp()
+
+        self.service_mock.list.return_value = [self.services]
+
+        # Get the command object to test
+        self.cmd = service.ListService(self.app, None)
+
+    def test_service_list(self):
+        arglist = [
+            '--host', self.services.host,
+            '--service', self.services.binary,
+        ]
+        verifylist = [
+            ('host', self.services.host),
+            ('service', self.services.binary),
+        ]
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+        # In base command class Lister in cliff, abstract method take_action()
+        # returns a tuple containing the column names and an iterable
+        # containing the data to be listed.
+        columns, data = self.cmd.take_action(parsed_args)
+
+        expected_columns = [
+            'Binary',
+            'Host',
+            'Zone',
+            'Status',
+            'State',
+            'Updated At',
+        ]
+
+        # confirming if all expected columns are present in the result.
+        self.assertEqual(expected_columns, columns)
+
+        datalist = ((
+            self.services.binary,
+            self.services.host,
+            self.services.zone,
+            self.services.status,
+            self.services.state,
+            self.services.updated_at,
+        ), )
+
+        # confirming if all expected values are present in the result.
+        self.assertEqual(datalist, tuple(data))
+
+        # checking if proper call was made to list services
+        self.service_mock.list.assert_called_with(
+            self.services.host,
+            self.services.binary,
+        )
+
+        # checking if prohibited columns are present in output
+        self.assertNotIn("Disabled Reason", columns)
+        self.assertNotIn(self.services.disabled_reason,
+                         tuple(data))
+
+    def test_service_list_with_long_option(self):
+        arglist = [
+            '--host', self.services.host,
+            '--service', self.services.binary,
+            '--long'
+        ]
+        verifylist = [
+            ('host', self.services.host),
+            ('service', self.services.binary),
+            ('long', True)
+        ]
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+        # In base command class Lister in cliff, abstract method take_action()
+        # returns a tuple containing the column names and an iterable
+        # containing the data to be listed.
+        columns, data = self.cmd.take_action(parsed_args)
+
+        expected_columns = [
+            'Binary',
+            'Host',
+            'Zone',
+            'Status',
+            'State',
+            'Updated At',
+            'Disabled Reason'
+        ]
+
+        # confirming if all expected columns are present in the result.
+        self.assertEqual(expected_columns, columns)
+
+        datalist = ((
+            self.services.binary,
+            self.services.host,
+            self.services.zone,
+            self.services.status,
+            self.services.state,
+            self.services.updated_at,
+            self.services.disabled_reason,
+        ), )
+
+        # confirming if all expected values are present in the result.
+        self.assertEqual(datalist, tuple(data))
+
+        self.service_mock.list.assert_called_with(
+            self.services.host,
+            self.services.binary,
+        )
diff --git a/openstackclient/volume/v1/service.py b/openstackclient/volume/v1/service.py
new file mode 100644
index 0000000000..f26be13e0c
--- /dev/null
+++ b/openstackclient/volume/v1/service.py
@@ -0,0 +1,70 @@
+#
+#   Licensed under the Apache License, Version 2.0 (the "License"); you may
+#   not use this file except in compliance with the License. You may obtain
+#   a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#   WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#   License for the specific language governing permissions and limitations
+#   under the License.
+#
+
+"""Service action implementations"""
+
+from openstackclient.common import command
+from openstackclient.common import utils
+
+
+class ListService(command.Lister):
+    """List service command"""
+
+    def get_parser(self, prog_name):
+        parser = super(ListService, self).get_parser(prog_name)
+        parser.add_argument(
+            "--host",
+            metavar="<host>",
+            help="List services on specified host (name only)")
+        parser.add_argument(
+            "--service",
+            metavar="<service>",
+            help="List only specified service (name only)")
+        parser.add_argument(
+            "--long",
+            action="store_true",
+            default=False,
+            help="List additional fields in output"
+        )
+        return parser
+
+    def take_action(self, parsed_args):
+        service_client = self.app.client_manager.volume
+
+        if parsed_args.long:
+            columns = [
+                "Binary",
+                "Host",
+                "Zone",
+                "Status",
+                "State",
+                "Updated At",
+                "Disabled Reason"
+            ]
+        else:
+            columns = [
+                "Binary",
+                "Host",
+                "Zone",
+                "Status",
+                "State",
+                "Updated At"
+            ]
+
+        data = service_client.services.list(parsed_args.host,
+                                            parsed_args.service)
+        return (columns,
+                (utils.get_item_properties(
+                    s, columns,
+                ) for s in data))
diff --git a/openstackclient/volume/v2/service.py b/openstackclient/volume/v2/service.py
new file mode 100644
index 0000000000..f26be13e0c
--- /dev/null
+++ b/openstackclient/volume/v2/service.py
@@ -0,0 +1,70 @@
+#
+#   Licensed under the Apache License, Version 2.0 (the "License"); you may
+#   not use this file except in compliance with the License. You may obtain
+#   a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#   WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#   License for the specific language governing permissions and limitations
+#   under the License.
+#
+
+"""Service action implementations"""
+
+from openstackclient.common import command
+from openstackclient.common import utils
+
+
+class ListService(command.Lister):
+    """List service command"""
+
+    def get_parser(self, prog_name):
+        parser = super(ListService, self).get_parser(prog_name)
+        parser.add_argument(
+            "--host",
+            metavar="<host>",
+            help="List services on specified host (name only)")
+        parser.add_argument(
+            "--service",
+            metavar="<service>",
+            help="List only specified service (name only)")
+        parser.add_argument(
+            "--long",
+            action="store_true",
+            default=False,
+            help="List additional fields in output"
+        )
+        return parser
+
+    def take_action(self, parsed_args):
+        service_client = self.app.client_manager.volume
+
+        if parsed_args.long:
+            columns = [
+                "Binary",
+                "Host",
+                "Zone",
+                "Status",
+                "State",
+                "Updated At",
+                "Disabled Reason"
+            ]
+        else:
+            columns = [
+                "Binary",
+                "Host",
+                "Zone",
+                "Status",
+                "State",
+                "Updated At"
+            ]
+
+        data = service_client.services.list(parsed_args.host,
+                                            parsed_args.service)
+        return (columns,
+                (utils.get_item_properties(
+                    s, columns,
+                ) for s in data))
diff --git a/releasenotes/notes/volume_service-5e352a71dfbc828d.yaml b/releasenotes/notes/volume_service-5e352a71dfbc828d.yaml
new file mode 100644
index 0000000000..1cfa81cfae
--- /dev/null
+++ b/releasenotes/notes/volume_service-5e352a71dfbc828d.yaml
@@ -0,0 +1,9 @@
+---
+features:
+  - |
+    Adds support for volume service list.
+
+    An user can list available volume services by using
+    ``volume service list``
+
+    [Bug 1550999 'https://bugs.launchpad.net/python-openstackclient/+bug/1550999'_]
diff --git a/setup.cfg b/setup.cfg
index 7689713c33..0407c3cf7e 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -421,6 +421,8 @@ openstack.volume.v1 =
     volume_qos_show = openstackclient.volume.v1.qos_specs:ShowQos
     volume_qos_unset = openstackclient.volume.v1.qos_specs:UnsetQos
 
+    volume_service_list = openstackclient.volume.v1.service:ListService
+
 openstack.volume.v2 =
     backup_create = openstackclient.volume.v2.backup:CreateBackup
     backup_delete = openstackclient.volume.v2.backup:DeleteBackup
@@ -458,6 +460,8 @@ openstack.volume.v2 =
     volume_qos_show = openstackclient.volume.v2.qos_specs:ShowQos
     volume_qos_unset = openstackclient.volume.v2.qos_specs:UnsetQos
 
+    volume_service_list = openstackclient.volume.v2.service:ListService
+
 [build_sphinx]
 source-dir = doc/source
 build-dir = doc/build