From a8ec2ac49475c60c8e72a0fc3db6df778918bb49 Mon Sep 17 00:00:00 2001
From: Richard Theis <rtheis@us.ibm.com>
Date: Thu, 14 Jan 2016 08:22:09 -0600
Subject: [PATCH] Support listing volume availability zones

Update the "os availability zone list" command to support listing
volume availability zones along with the currently listed compute
availability zones. This adds a --compute and --volume option to
the command in order to select the availability zones to list. By
default, all availability zones are listed.

If the Block Storage API does not support listing availability
zones then an warning message will be issued.

Change-Id: I8159509a41bd1fb1b4e77fdbb512cf64a5ac11a9
Closes-Bug: #1532945
---
 .../command-objects/availability-zone.rst     | 12 ++-
 doc/source/commands.rst                       |  2 +-
 openstackclient/common/availability_zone.py   | 75 +++++++++++---
 .../tests/common/test_availability_zone.py    | 99 +++++++++++++++++--
 openstackclient/tests/volume/v2/fakes.py      | 54 ++++++++++
 .../notes/bug-1532945-1a5485b8d0ebddb8.yaml   |  8 ++
 6 files changed, 224 insertions(+), 26 deletions(-)
 create mode 100644 releasenotes/notes/bug-1532945-1a5485b8d0ebddb8.yaml

diff --git a/doc/source/command-objects/availability-zone.rst b/doc/source/command-objects/availability-zone.rst
index 3743523088..8c021529c8 100644
--- a/doc/source/command-objects/availability-zone.rst
+++ b/doc/source/command-objects/availability-zone.rst
@@ -2,7 +2,7 @@
 availability zone
 =================
 
-Compute v2
+Compute v2, Block Storage v2
 
 availability zone list
 ----------------------
@@ -13,8 +13,18 @@ List availability zones and their status
 .. code:: bash
 
     os availability zone list
+        [--compute]
+        [--volume]
         [--long]
 
+.. option:: --compute
+
+    List compute availability zones
+
+.. option:: --volume
+
+    List volume availability zones
+
 .. option:: --long
 
     List additional fields in output
diff --git a/doc/source/commands.rst b/doc/source/commands.rst
index 621a0a9050..5427b27ebb 100644
--- a/doc/source/commands.rst
+++ b/doc/source/commands.rst
@@ -70,7 +70,7 @@ the API resources will be merged, as in the ``quota`` object that has options
 referring to both Compute and Volume quotas.
 
 * ``access token``: (**Identity**) long-lived OAuth-based token
-* ``availability zone``: (**Compute**) a logical partition of hosts or block storage services
+* ``availability zone``: (**Compute**, **Volume**) a logical partition of hosts or block storage services
 * ``aggregate``: (**Compute**) a grouping of servers
 * ``backup``: (**Volume**) a volume copy
 * ``catalog``: (**Identity**) service catalog
diff --git a/openstackclient/common/availability_zone.py b/openstackclient/common/availability_zone.py
index 0fe6c73acb..e72732e722 100644
--- a/openstackclient/common/availability_zone.py
+++ b/openstackclient/common/availability_zone.py
@@ -11,7 +11,7 @@
 #   under the License.
 #
 
-"""Compute v2 Availability Zone action implementations"""
+"""Availability Zone action implementations"""
 
 import copy
 import logging
@@ -24,15 +24,19 @@ from openstackclient.common import utils
 from openstackclient.i18n import _  # noqa
 
 
-def _xform_availability_zone(az, include_extra):
-    result = []
-    zone_info = {}
+def _xform_common_availability_zone(az, zone_info):
     if hasattr(az, 'zoneState'):
         zone_info['zone_status'] = ('available' if az.zoneState['available']
                                     else 'not available')
     if hasattr(az, 'zoneName'):
         zone_info['zone_name'] = az.zoneName
 
+
+def _xform_compute_availability_zone(az, include_extra):
+    result = []
+    zone_info = {}
+    _xform_common_availability_zone(az, zone_info)
+
     if not include_extra:
         result.append(zone_info)
         return result
@@ -58,6 +62,14 @@ def _xform_availability_zone(az, include_extra):
     return result
 
 
+def _xform_volume_availability_zone(az):
+    result = []
+    zone_info = {}
+    _xform_common_availability_zone(az, zone_info)
+    result.append(zone_info)
+    return result
+
+
 class ListAvailabilityZone(lister.Lister):
     """List availability zones and their status"""
 
@@ -65,6 +77,16 @@ class ListAvailabilityZone(lister.Lister):
 
     def get_parser(self, prog_name):
         parser = super(ListAvailabilityZone, self).get_parser(prog_name)
+        parser.add_argument(
+            '--compute',
+            action='store_true',
+            default=False,
+            help='List compute availability zones')
+        parser.add_argument(
+            '--volume',
+            action='store_true',
+            default=False,
+            help='List volume availability zones')
         parser.add_argument(
             '--long',
             action='store_true',
@@ -73,15 +95,7 @@ class ListAvailabilityZone(lister.Lister):
         )
         return parser
 
-    @utils.log_method(log)
-    def take_action(self, parsed_args):
-
-        if parsed_args.long:
-            columns = ('Zone Name', 'Zone Status',
-                       'Host Name', 'Service Name', 'Service Status')
-        else:
-            columns = ('Zone Name', 'Zone Status')
-
+    def get_compute_availability_zones(self, parsed_args):
         compute_client = self.app.client_manager.compute
         try:
             data = compute_client.availability_zones.list()
@@ -94,7 +108,40 @@ class ListAvailabilityZone(lister.Lister):
         # Argh, the availability zones are not iterable...
         result = []
         for zone in data:
-            result += _xform_availability_zone(zone, parsed_args.long)
+            result += _xform_compute_availability_zone(zone, parsed_args.long)
+        return result
+
+    def get_volume_availability_zones(self, parsed_args):
+        volume_client = self.app.client_manager.volume
+        try:
+            data = volume_client.availability_zones.list()
+        except Exception:
+            message = "Availability zones list not supported by " \
+                      "Block Storage API"
+            self.log.warning(message)
+
+        result = []
+        for zone in data:
+            result += _xform_volume_availability_zone(zone)
+        return result
+
+    @utils.log_method(log)
+    def take_action(self, parsed_args):
+
+        if parsed_args.long:
+            columns = ('Zone Name', 'Zone Status',
+                       'Host Name', 'Service Name', 'Service Status')
+        else:
+            columns = ('Zone Name', 'Zone Status')
+
+        # Show everything by default.
+        show_all = (not parsed_args.compute and not parsed_args.volume)
+
+        result = []
+        if parsed_args.compute or show_all:
+            result += self.get_compute_availability_zones(parsed_args)
+        if parsed_args.volume or show_all:
+            result += self.get_volume_availability_zones(parsed_args)
 
         return (columns,
                 (utils.get_dict_properties(
diff --git a/openstackclient/tests/common/test_availability_zone.py b/openstackclient/tests/common/test_availability_zone.py
index 35089d0623..232b56c99c 100644
--- a/openstackclient/tests/common/test_availability_zone.py
+++ b/openstackclient/tests/common/test_availability_zone.py
@@ -17,6 +17,7 @@ from openstackclient.common import availability_zone
 from openstackclient.tests.compute.v2 import fakes as compute_fakes
 from openstackclient.tests import fakes
 from openstackclient.tests import utils
+from openstackclient.tests.volume.v2 import fakes as volume_fakes
 
 
 def _build_compute_az_datalist(compute_az, long_datalist=False):
@@ -39,6 +40,22 @@ def _build_compute_az_datalist(compute_az, long_datalist=False):
     return (datalist,)
 
 
+def _build_volume_az_datalist(volume_az, long_datalist=False):
+    datalist = ()
+    if not long_datalist:
+        datalist = (
+            volume_az.zoneName,
+            'available',
+        )
+    else:
+        datalist = (
+            volume_az.zoneName,
+            'available',
+            '', '', '',
+        )
+    return (datalist,)
+
+
 class TestAvailabilityZone(utils.TestCommand):
 
     def setUp(self):
@@ -53,16 +70,37 @@ class TestAvailabilityZone(utils.TestCommand):
         self.compute_azs_mock = compute_client.availability_zones
         self.compute_azs_mock.reset_mock()
 
+        volume_client = volume_fakes.FakeVolumeClient(
+            endpoint=fakes.AUTH_URL,
+            token=fakes.AUTH_TOKEN,
+        )
+        self.app.client_manager.volume = volume_client
+
+        self.volume_azs_mock = volume_client.availability_zones
+        self.volume_azs_mock.reset_mock()
+
 
 class TestAvailabilityZoneList(TestAvailabilityZone):
 
     compute_azs = \
         compute_fakes.FakeAvailabilityZone.create_availability_zones()
+    volume_azs = \
+        volume_fakes.FakeAvailabilityZone.create_availability_zones(count=1)
+
+    short_columnslist = ('Zone Name', 'Zone Status')
+    long_columnslist = (
+        'Zone Name',
+        'Zone Status',
+        'Host Name',
+        'Service Name',
+        'Service Status',
+    )
 
     def setUp(self):
         super(TestAvailabilityZoneList, self).setUp()
 
         self.compute_azs_mock.list.return_value = self.compute_azs
+        self.volume_azs_mock.list.return_value = self.volume_azs
 
         # Get the command object to test
         self.cmd = availability_zone.ListAvailabilityZone(self.app, None)
@@ -76,12 +114,14 @@ class TestAvailabilityZoneList(TestAvailabilityZone):
         columns, data = self.cmd.take_action(parsed_args)
 
         self.compute_azs_mock.list.assert_called_with()
+        self.volume_azs_mock.list.assert_called_with()
 
-        columnslist = ('Zone Name', 'Zone Status')
-        self.assertEqual(columnslist, columns)
+        self.assertEqual(self.short_columnslist, columns)
         datalist = ()
         for compute_az in self.compute_azs:
             datalist += _build_compute_az_datalist(compute_az)
+        for volume_az in self.volume_azs:
+            datalist += _build_volume_az_datalist(volume_az)
         self.assertEqual(datalist, tuple(data))
 
     def test_availability_zone_list_long(self):
@@ -97,17 +137,56 @@ class TestAvailabilityZoneList(TestAvailabilityZone):
         columns, data = self.cmd.take_action(parsed_args)
 
         self.compute_azs_mock.list.assert_called_with()
+        self.volume_azs_mock.list.assert_called_with()
 
-        columnslist = (
-            'Zone Name',
-            'Zone Status',
-            'Host Name',
-            'Service Name',
-            'Service Status',
-        )
-        self.assertEqual(columnslist, columns)
+        self.assertEqual(self.long_columnslist, columns)
         datalist = ()
         for compute_az in self.compute_azs:
             datalist += _build_compute_az_datalist(compute_az,
                                                    long_datalist=True)
+        for volume_az in self.volume_azs:
+            datalist += _build_volume_az_datalist(volume_az,
+                                                  long_datalist=True)
+        self.assertEqual(datalist, tuple(data))
+
+    def test_availability_zone_list_compute(self):
+        arglist = [
+            '--compute',
+        ]
+        verifylist = [
+            ('compute', True),
+        ]
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+        # DisplayCommandBase.take_action() returns two tuples
+        columns, data = self.cmd.take_action(parsed_args)
+
+        self.compute_azs_mock.list.assert_called_with()
+        self.volume_azs_mock.list.assert_not_called()
+
+        self.assertEqual(self.short_columnslist, columns)
+        datalist = ()
+        for compute_az in self.compute_azs:
+            datalist += _build_compute_az_datalist(compute_az)
+        self.assertEqual(datalist, tuple(data))
+
+    def test_availability_zone_list_volume(self):
+        arglist = [
+            '--volume',
+        ]
+        verifylist = [
+            ('volume', True),
+        ]
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+        # DisplayCommandBase.take_action() returns two tuples
+        columns, data = self.cmd.take_action(parsed_args)
+
+        self.compute_azs_mock.list.assert_not_called()
+        self.volume_azs_mock.list.assert_called_with()
+
+        self.assertEqual(self.short_columnslist, columns)
+        datalist = ()
+        for volume_az in self.volume_azs:
+            datalist += _build_volume_az_datalist(volume_az)
         self.assertEqual(datalist, tuple(data))
diff --git a/openstackclient/tests/volume/v2/fakes.py b/openstackclient/tests/volume/v2/fakes.py
index 60cec33558..2e58e58df9 100644
--- a/openstackclient/tests/volume/v2/fakes.py
+++ b/openstackclient/tests/volume/v2/fakes.py
@@ -202,6 +202,8 @@ class FakeVolumeClient(object):
         self.restores.resource_class = fakes.FakeResource(None, {})
         self.qos_specs = mock.Mock()
         self.qos_specs.resource_class = fakes.FakeResource(None, {})
+        self.availability_zones = mock.Mock()
+        self.availability_zones.resource_class = fakes.FakeResource(None, {})
         self.auth_token = kwargs['token']
         self.management_url = kwargs['endpoint']
 
@@ -304,3 +306,55 @@ class FakeVolume(object):
             volumes = FakeVolume.create_volumes(count)
 
         return mock.MagicMock(side_effect=volumes)
+
+
+class FakeAvailabilityZone(object):
+    """Fake one or more volume availability zones (AZs)."""
+
+    @staticmethod
+    def create_one_availability_zone(attrs={}, methods={}):
+        """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.
+        """
+        # Set default attributes.
+        availability_zone = {
+            'zoneName': uuid.uuid4().hex,
+            'zoneState': {'available': True},
+        }
+
+        # Overwrite default attributes.
+        availability_zone.update(attrs)
+
+        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):
+        """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:
+            A list of FakeResource objects faking the AZs
+        """
+        availability_zones = []
+        for i in range(0, count):
+            availability_zone = \
+                FakeAvailabilityZone.create_one_availability_zone(
+                    attrs, methods)
+            availability_zones.append(availability_zone)
+
+        return availability_zones
diff --git a/releasenotes/notes/bug-1532945-1a5485b8d0ebddb8.yaml b/releasenotes/notes/bug-1532945-1a5485b8d0ebddb8.yaml
new file mode 100644
index 0000000000..bee8102bcb
--- /dev/null
+++ b/releasenotes/notes/bug-1532945-1a5485b8d0ebddb8.yaml
@@ -0,0 +1,8 @@
+---
+features:
+  - |
+    Add volume support to `os availability zone list`
+    [Bug `1532945 <https://bugs.launchpad.net/bugs/1532945>`_]
+
+    * New `--compute` option to only list compute availability zones.
+    * New `--volume` option to only list volume availability zones.