diff --git a/cinderclient/tests/v1/fakes.py b/cinderclient/tests/v1/fakes.py index 73ad1a91b..c83f5adcd 100644 --- a/cinderclient/tests/v1/fakes.py +++ b/cinderclient/tests/v1/fakes.py @@ -558,3 +558,58 @@ class FakeHTTPClient(base_client.HTTPClient): def put_os_services_disable(self, body, **kw): return (200, {}, {'host': body['host'], 'binary': body['binary'], 'status': 'enabled'}) + + def get_os_availability_zone(self, **kw): + return (200, {}, { + "availabilityZoneInfo": [ + { + "zoneName": "zone-1", + "zoneState": {"available": True}, + "hosts": None, + }, + { + "zoneName": "zone-2", + "zoneState": {"available": False}, + "hosts": None, + }, + ] + }) + + def get_os_availability_zone_detail(self, **kw): + return (200, {}, { + "availabilityZoneInfo": [ + { + "zoneName": "zone-1", + "zoneState": {"available": True}, + "hosts": { + "fake_host-1": { + "cinder-volume": { + "active": True, + "available": True, + "updated_at": + datetime(2012, 12, 26, 14, 45, 25, 0) + } + } + } + }, + { + "zoneName": "internal", + "zoneState": {"available": True}, + "hosts": { + "fake_host-1": { + "cinder-sched": { + "active": True, + "available": True, + "updated_at": + datetime(2012, 12, 26, 14, 45, 24, 0) + } + } + } + }, + { + "zoneName": "zone-2", + "zoneState": {"available": False}, + "hosts": None, + }, + ] + }) diff --git a/cinderclient/tests/v1/test_availability_zone.py b/cinderclient/tests/v1/test_availability_zone.py new file mode 100644 index 000000000..a2e1fc8b3 --- /dev/null +++ b/cinderclient/tests/v1/test_availability_zone.py @@ -0,0 +1,87 @@ +# Copyright 2011-2013 OpenStack Foundation +# Copyright 2013 IBM Corp. +# All Rights Reserved. +# +# 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. + +import six + +from cinderclient.v1 import availability_zones +from cinderclient.v1 import shell +from cinderclient.tests import utils +from cinderclient.tests.v1 import fakes + + +cs = fakes.FakeClient() + + +class AvailabilityZoneTest(utils.TestCase): + + def _assertZone(self, zone, name, status): + self.assertEqual(zone.zoneName, name) + self.assertEqual(zone.zoneState, status) + + def test_list_availability_zone(self): + zones = cs.availability_zones.list(detailed=False) + cs.assert_called('GET', '/os-availability-zone') + + for zone in zones: + self.assertTrue(isinstance(zone, + availability_zones.AvailabilityZone)) + + self.assertEqual(2, len(zones)) + + l0 = [six.u('zone-1'), six.u('available')] + l1 = [six.u('zone-2'), six.u('not available')] + + z0 = shell._treeizeAvailabilityZone(zones[0]) + z1 = shell._treeizeAvailabilityZone(zones[1]) + + self.assertEqual((len(z0), len(z1)), (1, 1)) + + self._assertZone(z0[0], l0[0], l0[1]) + self._assertZone(z1[0], l1[0], l1[1]) + + def test_detail_availability_zone(self): + zones = cs.availability_zones.list(detailed=True) + cs.assert_called('GET', '/os-availability-zone/detail') + + for zone in zones: + self.assertTrue(isinstance(zone, + availability_zones.AvailabilityZone)) + + self.assertEqual(3, len(zones)) + + l0 = [six.u('zone-1'), six.u('available')] + l1 = [six.u('|- fake_host-1'), six.u('')] + l2 = [six.u('| |- cinder-volume'), + six.u('enabled :-) 2012-12-26 14:45:25')] + l3 = [six.u('internal'), six.u('available')] + l4 = [six.u('|- fake_host-1'), six.u('')] + l5 = [six.u('| |- cinder-sched'), + six.u('enabled :-) 2012-12-26 14:45:24')] + l6 = [six.u('zone-2'), six.u('not available')] + + z0 = shell._treeizeAvailabilityZone(zones[0]) + z1 = shell._treeizeAvailabilityZone(zones[1]) + z2 = shell._treeizeAvailabilityZone(zones[2]) + + self.assertEqual((len(z0), len(z1), len(z2)), (3, 3, 1)) + + self._assertZone(z0[0], l0[0], l0[1]) + self._assertZone(z0[1], l1[0], l1[1]) + self._assertZone(z0[2], l2[0], l2[1]) + self._assertZone(z1[0], l3[0], l3[1]) + self._assertZone(z1[1], l4[0], l4[1]) + self._assertZone(z1[2], l5[0], l5[1]) + self._assertZone(z2[0], l6[0], l6[1]) diff --git a/cinderclient/tests/v1/test_shell.py b/cinderclient/tests/v1/test_shell.py index a8c9425e3..71d78963e 100644 --- a/cinderclient/tests/v1/test_shell.py +++ b/cinderclient/tests/v1/test_shell.py @@ -105,6 +105,10 @@ class ShellTest(utils.TestCase): self.run_command('list --all-tenants=1') self.assert_called('GET', '/volumes/detail?all_tenants=1') + def test_list_availability_zone(self): + self.run_command('availability-zone-list') + self.assert_called('GET', '/os-availability-zone') + def test_show(self): self.run_command('show 1234') self.assert_called('GET', '/volumes/1234') diff --git a/cinderclient/tests/v2/fakes.py b/cinderclient/tests/v2/fakes.py index a1400821a..8f70e0926 100644 --- a/cinderclient/tests/v2/fakes.py +++ b/cinderclient/tests/v2/fakes.py @@ -565,3 +565,58 @@ class FakeHTTPClient(base_client.HTTPClient): def put_os_services_disable(self, body, **kw): return (200, {}, {'host': body['host'], 'binary': body['binary'], 'status': 'enabled'}) + + def get_os_availability_zone(self, **kw): + return (200, {}, { + "availabilityZoneInfo": [ + { + "zoneName": "zone-1", + "zoneState": {"available": True}, + "hosts": None, + }, + { + "zoneName": "zone-2", + "zoneState": {"available": False}, + "hosts": None, + }, + ] + }) + + def get_os_availability_zone_detail(self, **kw): + return (200, {}, { + "availabilityZoneInfo": [ + { + "zoneName": "zone-1", + "zoneState": {"available": True}, + "hosts": { + "fake_host-1": { + "cinder-volume": { + "active": True, + "available": True, + "updated_at": + datetime(2012, 12, 26, 14, 45, 25, 0) + } + } + } + }, + { + "zoneName": "internal", + "zoneState": {"available": True}, + "hosts": { + "fake_host-1": { + "cinder-sched": { + "active": True, + "available": True, + "updated_at": + datetime(2012, 12, 26, 14, 45, 24, 0) + } + } + } + }, + { + "zoneName": "zone-2", + "zoneState": {"available": False}, + "hosts": None, + }, + ] + }) diff --git a/cinderclient/tests/v2/test_availability_zone.py b/cinderclient/tests/v2/test_availability_zone.py new file mode 100644 index 000000000..a2e1fc8b3 --- /dev/null +++ b/cinderclient/tests/v2/test_availability_zone.py @@ -0,0 +1,87 @@ +# Copyright 2011-2013 OpenStack Foundation +# Copyright 2013 IBM Corp. +# All Rights Reserved. +# +# 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. + +import six + +from cinderclient.v1 import availability_zones +from cinderclient.v1 import shell +from cinderclient.tests import utils +from cinderclient.tests.v1 import fakes + + +cs = fakes.FakeClient() + + +class AvailabilityZoneTest(utils.TestCase): + + def _assertZone(self, zone, name, status): + self.assertEqual(zone.zoneName, name) + self.assertEqual(zone.zoneState, status) + + def test_list_availability_zone(self): + zones = cs.availability_zones.list(detailed=False) + cs.assert_called('GET', '/os-availability-zone') + + for zone in zones: + self.assertTrue(isinstance(zone, + availability_zones.AvailabilityZone)) + + self.assertEqual(2, len(zones)) + + l0 = [six.u('zone-1'), six.u('available')] + l1 = [six.u('zone-2'), six.u('not available')] + + z0 = shell._treeizeAvailabilityZone(zones[0]) + z1 = shell._treeizeAvailabilityZone(zones[1]) + + self.assertEqual((len(z0), len(z1)), (1, 1)) + + self._assertZone(z0[0], l0[0], l0[1]) + self._assertZone(z1[0], l1[0], l1[1]) + + def test_detail_availability_zone(self): + zones = cs.availability_zones.list(detailed=True) + cs.assert_called('GET', '/os-availability-zone/detail') + + for zone in zones: + self.assertTrue(isinstance(zone, + availability_zones.AvailabilityZone)) + + self.assertEqual(3, len(zones)) + + l0 = [six.u('zone-1'), six.u('available')] + l1 = [six.u('|- fake_host-1'), six.u('')] + l2 = [six.u('| |- cinder-volume'), + six.u('enabled :-) 2012-12-26 14:45:25')] + l3 = [six.u('internal'), six.u('available')] + l4 = [six.u('|- fake_host-1'), six.u('')] + l5 = [six.u('| |- cinder-sched'), + six.u('enabled :-) 2012-12-26 14:45:24')] + l6 = [six.u('zone-2'), six.u('not available')] + + z0 = shell._treeizeAvailabilityZone(zones[0]) + z1 = shell._treeizeAvailabilityZone(zones[1]) + z2 = shell._treeizeAvailabilityZone(zones[2]) + + self.assertEqual((len(z0), len(z1), len(z2)), (3, 3, 1)) + + self._assertZone(z0[0], l0[0], l0[1]) + self._assertZone(z0[1], l1[0], l1[1]) + self._assertZone(z0[2], l2[0], l2[1]) + self._assertZone(z1[0], l3[0], l3[1]) + self._assertZone(z1[1], l4[0], l4[1]) + self._assertZone(z1[2], l5[0], l5[1]) + self._assertZone(z2[0], l6[0], l6[1]) diff --git a/cinderclient/tests/v2/test_shell.py b/cinderclient/tests/v2/test_shell.py index eabf13476..2405192eb 100644 --- a/cinderclient/tests/v2/test_shell.py +++ b/cinderclient/tests/v2/test_shell.py @@ -83,6 +83,10 @@ class ShellTest(utils.TestCase): self.run_command('list --all-tenants=1') self.assert_called('GET', '/volumes/detail?all_tenants=1') + def test_list_availability_zone(self): + self.run_command('availability-zone-list') + self.assert_called('GET', '/os-availability-zone') + def test_show(self): self.run_command('show 1234') self.assert_called('GET', '/volumes/1234') diff --git a/cinderclient/v1/availability_zones.py b/cinderclient/v1/availability_zones.py new file mode 100644 index 000000000..85ea9d7bf --- /dev/null +++ b/cinderclient/v1/availability_zones.py @@ -0,0 +1,42 @@ +# Copyright 2011-2013 OpenStack Foundation +# Copyright 2013 IBM Corp. +# All Rights Reserved. +# +# 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. + +"""Availability Zone interface (v1 extension)""" + +from cinderclient import base + + +class AvailabilityZone(base.Resource): + NAME_ATTR = 'display_name' + + def __repr__(self): + return "" % self.zoneName + + +class AvailabilityZoneManager(base.ManagerWithFind): + """Manage :class:`AvailabilityZone` resources.""" + resource_class = AvailabilityZone + + def list(self, detailed=False): + """Get a list of all availability zones + + :rtype: list of :class:`AvailabilityZone` + """ + if detailed is True: + return self._list("/os-availability-zone/detail", + "availabilityZoneInfo") + else: + return self._list("/os-availability-zone", "availabilityZoneInfo") diff --git a/cinderclient/v1/client.py b/cinderclient/v1/client.py index 7dd84e128..1272c4e8b 100644 --- a/cinderclient/v1/client.py +++ b/cinderclient/v1/client.py @@ -14,6 +14,7 @@ # under the License. from cinderclient import client +from cinderclient.v1 import availability_zones from cinderclient.v1 import limits from cinderclient.v1 import quota_classes from cinderclient.v1 import quotas @@ -64,6 +65,8 @@ class Client(object): self.restores = volume_backups_restore.VolumeBackupRestoreManager(self) self.transfers = volume_transfers.VolumeTransferManager(self) self.services = services.ServiceManager(self) + self.availability_zones = \ + availability_zones.AvailabilityZoneManager(self) # Add in any extensions... if extensions: diff --git a/cinderclient/v1/shell.py b/cinderclient/v1/shell.py index ec62ba286..814422284 100644 --- a/cinderclient/v1/shell.py +++ b/cinderclient/v1/shell.py @@ -18,12 +18,14 @@ from __future__ import print_function import argparse +import copy import os import sys import time from cinderclient import exceptions from cinderclient import utils +from cinderclient.v1 import availability_zones def _poll_for_status(poll_fn, obj_id, action, final_ok_states, @@ -104,6 +106,11 @@ def _translate_volume_snapshot_keys(collection): _translate_keys(collection, convert) +def _translate_availability_zone_keys(collection): + convert = [('zoneName', 'name'), ('zoneState', 'status')] + _translate_keys(collection, convert) + + def _extract_metadata(args): metadata = {} for metadatum in args.metadata: @@ -859,3 +866,63 @@ def do_service_enable(cs, args): def do_service_disable(cs, args): """Disable the service.""" cs.services.disable(args.host, args.binary) + + +def _treeizeAvailabilityZone(zone): + """Build a tree view for availability zones.""" + AvailabilityZone = availability_zones.AvailabilityZone + + az = AvailabilityZone(zone.manager, + copy.deepcopy(zone._info), zone._loaded) + result = [] + + # Zone tree view item + az.zoneName = zone.zoneName + az.zoneState = ('available' + if zone.zoneState['available'] else 'not available') + az._info['zoneName'] = az.zoneName + az._info['zoneState'] = az.zoneState + result.append(az) + + if getattr(zone, "hosts", None) and zone.hosts is not None: + for (host, services) in zone.hosts.items(): + # Host tree view item + az = AvailabilityZone(zone.manager, + copy.deepcopy(zone._info), zone._loaded) + az.zoneName = '|- %s' % host + az.zoneState = '' + az._info['zoneName'] = az.zoneName + az._info['zoneState'] = az.zoneState + result.append(az) + + for (svc, state) in services.items(): + # Service tree view item + az = AvailabilityZone(zone.manager, + copy.deepcopy(zone._info), zone._loaded) + az.zoneName = '| |- %s' % svc + az.zoneState = '%s %s %s' % ( + 'enabled' if state['active'] else 'disabled', + ':-)' if state['available'] else 'XXX', + state['updated_at']) + az._info['zoneName'] = az.zoneName + az._info['zoneState'] = az.zoneState + result.append(az) + return result + + +@utils.service_type('volume') +def do_availability_zone_list(cs, _args): + """List all the availability zones.""" + try: + availability_zones = cs.availability_zones.list() + except exceptions.Forbidden as e: # policy doesn't allow probably + try: + availability_zones = cs.availability_zones.list(detailed=False) + except Exception: + raise e + + result = [] + for zone in availability_zones: + result += _treeizeAvailabilityZone(zone) + _translate_availability_zone_keys(result) + utils.print_list(result, ['Name', 'Status']) diff --git a/cinderclient/v2/availability_zones.py b/cinderclient/v2/availability_zones.py new file mode 100644 index 000000000..c8aef24cf --- /dev/null +++ b/cinderclient/v2/availability_zones.py @@ -0,0 +1,42 @@ +# Copyright 2011-2013 OpenStack Foundation +# Copyright 2013 IBM Corp. +# All Rights Reserved. +# +# 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. + +"""Availability Zone interface (v2 extension)""" + +from cinderclient import base + + +class AvailabilityZone(base.Resource): + NAME_ATTR = 'display_name' + + def __repr__(self): + return "" % self.zoneName + + +class AvailabilityZoneManager(base.ManagerWithFind): + """Manage :class:`AvailabilityZone` resources.""" + resource_class = AvailabilityZone + + def list(self, detailed=False): + """Get a list of all availability zones + + :rtype: list of :class:`AvailabilityZone` + """ + if detailed is True: + return self._list("/os-availability-zone/detail", + "availabilityZoneInfo") + else: + return self._list("/os-availability-zone", "availabilityZoneInfo") diff --git a/cinderclient/v2/client.py b/cinderclient/v2/client.py index 003ecae98..fea400f32 100644 --- a/cinderclient/v2/client.py +++ b/cinderclient/v2/client.py @@ -14,6 +14,7 @@ # under the License. from cinderclient import client +from cinderclient.v1 import availability_zones from cinderclient.v2 import limits from cinderclient.v2 import quota_classes from cinderclient.v2 import quotas @@ -62,6 +63,8 @@ class Client(object): self.restores = volume_backups_restore.VolumeBackupRestoreManager(self) self.transfers = volume_transfers.VolumeTransferManager(self) self.services = services.ServiceManager(self) + self.availability_zones = \ + availability_zones.AvailabilityZoneManager(self) # Add in any extensions... if extensions: diff --git a/cinderclient/v2/shell.py b/cinderclient/v2/shell.py index 5dc85d379..b49fded98 100644 --- a/cinderclient/v2/shell.py +++ b/cinderclient/v2/shell.py @@ -16,6 +16,7 @@ from __future__ import print_function import argparse +import copy import os import sys import time @@ -24,6 +25,7 @@ import six from cinderclient import exceptions from cinderclient import utils +from cinderclient.v2 import availability_zones def _poll_for_status(poll_fn, obj_id, action, final_ok_states, @@ -98,6 +100,11 @@ def _translate_volume_snapshot_keys(collection): _translate_keys(collection, convert) +def _translate_availability_zone_keys(collection): + convert = [('zoneName', 'name'), ('zoneState', 'status')] + _translate_keys(collection, convert) + + def _extract_metadata(args): metadata = {} for metadatum in args.metadata[0]: @@ -944,3 +951,63 @@ def do_service_enable(cs, args): def do_service_disable(cs, args): """Disable the service.""" cs.services.disable(args.host, args.binary) + + +def _treeizeAvailabilityZone(zone): + """Build a tree view for availability zones.""" + AvailabilityZone = availability_zones.AvailabilityZone + + az = AvailabilityZone(zone.manager, + copy.deepcopy(zone._info), zone._loaded) + result = [] + + # Zone tree view item + az.zoneName = zone.zoneName + az.zoneState = ('available' + if zone.zoneState['available'] else 'not available') + az._info['zoneName'] = az.zoneName + az._info['zoneState'] = az.zoneState + result.append(az) + + if getattr(zone, "hosts", None) and zone.hosts is not None: + for (host, services) in zone.hosts.items(): + # Host tree view item + az = AvailabilityZone(zone.manager, + copy.deepcopy(zone._info), zone._loaded) + az.zoneName = '|- %s' % host + az.zoneState = '' + az._info['zoneName'] = az.zoneName + az._info['zoneState'] = az.zoneState + result.append(az) + + for (svc, state) in services.items(): + # Service tree view item + az = AvailabilityZone(zone.manager, + copy.deepcopy(zone._info), zone._loaded) + az.zoneName = '| |- %s' % svc + az.zoneState = '%s %s %s' % ( + 'enabled' if state['active'] else 'disabled', + ':-)' if state['available'] else 'XXX', + state['updated_at']) + az._info['zoneName'] = az.zoneName + az._info['zoneState'] = az.zoneState + result.append(az) + return result + + +@utils.service_type('volume') +def do_availability_zone_list(cs, _args): + """List all the availability zones.""" + try: + availability_zones = cs.availability_zones.list() + except exceptions.Forbidden as e: # policy doesn't allow probably + try: + availability_zones = cs.availability_zones.list(detailed=False) + except Exception: + raise e + + result = [] + for zone in availability_zones: + result += _treeizeAvailabilityZone(zone) + _translate_availability_zone_keys(result) + utils.print_list(result, ['Name', 'Status'])