diff --git a/doc/source/command-objects/availability-zone.rst b/doc/source/command-objects/availability-zone.rst index 374352308..8c021529c 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 621a0a905..5427b27eb 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 0fe6c73ac..e72732e72 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 35089d062..232b56c99 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 60cec3355..2e58e58df 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 000000000..bee8102bc --- /dev/null +++ b/releasenotes/notes/bug-1532945-1a5485b8d0ebddb8.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Add volume support to `os availability zone list` + [Bug `1532945 `_] + + * New `--compute` option to only list compute availability zones. + * New `--volume` option to only list volume availability zones.