From 23f13437dd64496fcbc138bbaa9b0ac615a3cf23 Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Fri, 22 May 2015 13:34:51 -0700 Subject: [PATCH] Deprecate volume/volume-type/volume-snapshot CRUD CLIs/APIs This deprecates all of the volume CLIs/APIs that go directly to the cinder API via the volume service type. This will emit a warning each time a deprecated CLI/API is used and also updates the help docs for the deprecated CLIs and docstrings for APIs. The plan is to do a release once this is merged so people start seeing it and then we'll actually remove the deprecated CLIs/APIs in the first python-novaclient release after the Nova server 2016.1 'M' release. DocImpact: The volume, volume-type and volume-snapshot CRUD CLIs/APIs are deprecated and will be removed after the Nova 2016.1 release. Use python-cinderclient or openstackclient for CLIs instead. Use python-cinderclient or python-openstacksdk for APIs instead. Related-Bug: #1454369 Change-Id: I4e92f924739de9b664e08117984bfb60359f2212 --- .../tests/functional/test_readonly_nova.py | 6 ++- novaclient/tests/unit/v2/test_shell.py | 33 ++++++++------- novaclient/tests/unit/v2/test_volumes.py | 24 ++++++++--- novaclient/v2/shell.py | 41 ++++++++++++++----- novaclient/v2/volume_snapshots.py | 34 +++++++++++---- novaclient/v2/volume_types.py | 32 +++++++++++---- novaclient/v2/volumes.py | 33 +++++++++++---- 7 files changed, 149 insertions(+), 54 deletions(-) diff --git a/novaclient/tests/functional/test_readonly_nova.py b/novaclient/tests/functional/test_readonly_nova.py index c67c3a378..67454e3cd 100644 --- a/novaclient/tests/functional/test_readonly_nova.py +++ b/novaclient/tests/functional/test_readonly_nova.py @@ -134,10 +134,12 @@ class SimpleReadOnlyNovaClientTest(base.ClientTestBase): self.nova('volume-list') def test_admin_volume_snapshot_list(self): - self.nova('volume-snapshot-list') + out = self.nova('volume-snapshot-list', merge_stderr=True) + self.assertIn('Command volume-snapshot-list is deprecated', out) def test_admin_volume_type_list(self): - self.nova('volume-type-list') + out = self.nova('volume-type-list', merge_stderr=True) + self.assertIn('Command volume-type-list is deprecated', out) def test_admin_help(self): self.nova('help') diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 91f0e27e4..d6ddc2e14 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -74,12 +74,13 @@ class ShellTest(utils.TestCase): lambda *_: fakes.FakeClient)) @mock.patch('sys.stdout', new_callable=six.StringIO) - def run_command(self, cmd, mock_stdout): + @mock.patch('sys.stderr', new_callable=six.StringIO) + def run_command(self, cmd, mock_stderr, mock_stdout): if isinstance(cmd, list): self.shell.main(cmd) else: self.shell.main(cmd.split()) - return mock_stdout.getvalue() + return mock_stdout.getvalue(), mock_stderr.getvalue() def assert_called(self, method, url, body=None, **kwargs): return self.shell.cs.assert_called(method, url, body, **kwargs) @@ -803,7 +804,7 @@ class ShellTest(utils.TestCase): ) def test_create_image_show(self): - output = self.run_command( + output, _ = self.run_command( 'image-create sample-server mysnapshot --show') self.assert_called_anytime( 'POST', '/servers/1234/action', @@ -908,7 +909,7 @@ class ShellTest(utils.TestCase): mock.ANY, mock.ANY, mock.ANY, sortby_index=1) def test_list_fields(self): - output = self.run_command( + output, _ = self.run_command( 'list --fields ' 'host,security_groups,OS-EXT-MOD:some_thing') self.assert_called('GET', '/servers/detail') @@ -926,7 +927,7 @@ class ShellTest(utils.TestCase): {'reboot': {'type': 'HARD'}}) def test_rebuild(self): - output = self.run_command('rebuild sample-server 1') + output, _ = self.run_command('rebuild sample-server 1') self.assert_called('GET', '/servers?name=sample-server', pos=-6) self.assert_called('GET', '/servers/1234', pos=-5) self.assert_called('GET', '/images/1', pos=-4) @@ -937,8 +938,8 @@ class ShellTest(utils.TestCase): self.assertIn('adminPass', output) def test_rebuild_password(self): - output = self.run_command('rebuild sample-server 1' - ' --rebuild-password asdf') + output, _ = self.run_command('rebuild sample-server 1' + ' --rebuild-password asdf') self.assert_called('GET', '/servers?name=sample-server', pos=-6) self.assert_called('GET', '/servers/1234', pos=-5) self.assert_called('GET', '/images/1', pos=-4) @@ -1091,7 +1092,7 @@ class ShellTest(utils.TestCase): self.run_command, 'show xxx') def test_show_unavailable_image_and_flavor(self): - output = self.run_command('show 9013') + output, _ = self.run_command('show 9013') self.assert_called('GET', '/servers/9013', pos=-8) self.assert_called('GET', '/flavors/80645cf4-6ad3-410a-bbc8-6f3e1e291f51', @@ -1860,7 +1861,7 @@ class ShellTest(utils.TestCase): self.assert_called('GET', '/os-networks') def test_network_list_fields(self): - output = self.run_command( + output, _ = self.run_command( 'network-list --fields ' 'vlan,project_id') self.assert_called('GET', '/os-networks') @@ -2037,7 +2038,7 @@ class ShellTest(utils.TestCase): self.run_command('limits --tenant 1234') self.assert_called('GET', '/limits?tenant_id=1234') - stdout = self.run_command('limits --tenant 1234') + stdout, _ = self.run_command('limits --tenant 1234') self.assertIn('Verb', stdout) self.assertIn('Name', stdout) @@ -2190,11 +2191,13 @@ class ShellTest(utils.TestCase): self.assert_called('DELETE', '/servers/1234/os-interface/port_id') def test_volume_list(self): - self.run_command('volume-list') + _, err = self.run_command('volume-list') + self.assertIn('Command volume-list is deprecated', err) self.assert_called('GET', '/volumes/detail') def test_volume_show(self): - self.run_command('volume-show Work') + _, err = self.run_command('volume-show Work') + self.assertIn('Command volume-show is deprecated', err) self.assert_called('GET', '/volumes?display_name=Work', pos=-2) self.assert_called( 'GET', @@ -2203,7 +2206,8 @@ class ShellTest(utils.TestCase): ) def test_volume_create(self): - self.run_command('volume-create 2 --display-name Work') + _, err = self.run_command('volume-create 2 --display-name Work') + self.assertIn('Command volume-create is deprecated', err) self.assert_called('POST', '/volumes', {'volume': {'display_name': 'Work', @@ -2215,7 +2219,8 @@ class ShellTest(utils.TestCase): 'size': 2}}) def test_volume_delete(self): - self.run_command('volume-delete Work') + _, err = self.run_command('volume-delete Work') + self.assertIn('Command volume-delete is deprecated', err) self.assert_called('DELETE', '/volumes/15e59938-07d5-11e1-90e3-e3dffe0c5983') diff --git a/novaclient/tests/unit/v2/test_volumes.py b/novaclient/tests/unit/v2/test_volumes.py index f3da71d33..2e5f3d47a 100644 --- a/novaclient/tests/unit/v2/test_volumes.py +++ b/novaclient/tests/unit/v2/test_volumes.py @@ -13,6 +13,10 @@ # License for the specific language governing permissions and limitations # under the License. +import warnings + +import mock + from novaclient.tests.unit import utils from novaclient.tests.unit.v2 import fakes from novaclient.v2 import volumes @@ -23,26 +27,33 @@ cs = fakes.FakeClient() class VolumesTest(utils.TestCase): - def test_list_servers(self): + @mock.patch.object(warnings, 'warn') + def test_list_volumes(self, mock_warn): vl = cs.volumes.list() cs.assert_called('GET', '/volumes/detail') for v in vl: self.assertIsInstance(v, volumes.Volume) + self.assertEqual(1, mock_warn.call_count) - def test_list_volumes_undetailed(self): + @mock.patch.object(warnings, 'warn') + def test_list_volumes_undetailed(self, mock_warn): vl = cs.volumes.list(detailed=False) cs.assert_called('GET', '/volumes') for v in vl: self.assertIsInstance(v, volumes.Volume) + self.assertEqual(1, mock_warn.call_count) - def test_get_volume_details(self): + @mock.patch.object(warnings, 'warn') + def test_get_volume_details(self, mock_warn): vol_id = '15e59938-07d5-11e1-90e3-e3dffe0c5983' v = cs.volumes.get(vol_id) cs.assert_called('GET', '/volumes/%s' % vol_id) self.assertIsInstance(v, volumes.Volume) self.assertEqual(v.id, vol_id) + self.assertEqual(1, mock_warn.call_count) - def test_create_volume(self): + @mock.patch.object(warnings, 'warn') + def test_create_volume(self, mock_warn): v = cs.volumes.create( size=2, display_name="My volume", @@ -50,8 +61,10 @@ class VolumesTest(utils.TestCase): ) cs.assert_called('POST', '/volumes') self.assertIsInstance(v, volumes.Volume) + self.assertEqual(1, mock_warn.call_count) - def test_delete_volume(self): + @mock.patch.object(warnings, 'warn') + def test_delete_volume(self, mock_warn): vol_id = '15e59938-07d5-11e1-90e3-e3dffe0c5983' v = cs.volumes.get(vol_id) v.delete() @@ -60,6 +73,7 @@ class VolumesTest(utils.TestCase): cs.assert_called('DELETE', '/volumes/%s' % vol_id) cs.volumes.delete(v) cs.assert_called('DELETE', '/volumes/%s' % vol_id) + self.assertEqual(4, mock_warn.call_count) def test_create_server_volume(self): v = cs.volumes.create_server_volume( diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index f304988be..d7112b751 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -61,6 +61,14 @@ CLIENT_BDM2_KEYS = { } +# NOTE(mriedem): Remove this along with the deprecated commands in the first +# python-novaclient release AFTER the nova server 2016.1 'M' release. +def emit_volume_deprecation_warning(command_name): + print('WARNING: Command %s is deprecated and will be removed after Nova ' + '2016.1 is released. Use python-cinderclient or openstackclient ' + 'instead.' % command_name, file=sys.stderr) + + def _key_value_pairing(text): try: (k, v) = text.split('=', 1) @@ -1986,7 +1994,8 @@ def _translate_availability_zone_keys(collection): const=1, help=argparse.SUPPRESS) def do_volume_list(cs, args): - """List all the volumes.""" + """DEPRECATED: List all the volumes.""" + emit_volume_deprecation_warning('volume-list') search_opts = {'all_tenants': args.all_tenants} volumes = cs.volumes.list(search_opts=search_opts) _translate_volume_keys(volumes) @@ -2004,7 +2013,8 @@ def do_volume_list(cs, args): metavar='', help=_('Name or ID of the volume.')) def do_volume_show(cs, args): - """Show details about a volume.""" + """DEPRECATED: Show details about a volume.""" + emit_volume_deprecation_warning('volume-show') volume = _find_volume(cs, args.volume) _print_volume(volume) @@ -2056,7 +2066,8 @@ def do_volume_show(cs, args): help=_('Optional Availability Zone for volume. (Default=None)'), default=None) def do_volume_create(cs, args): - """Add a new volume.""" + """DEPRECATED: Add a new volume.""" + emit_volume_deprecation_warning('volume-create') volume = cs.volumes.create(args.size, args.snapshot_id, args.display_name, @@ -2072,7 +2083,8 @@ def do_volume_create(cs, args): metavar='', nargs='+', help=_('Name or ID of the volume(s) to delete.')) def do_volume_delete(cs, args): - """Remove volume(s).""" + """DEPRECATED: Remove volume(s).""" + emit_volume_deprecation_warning('volume-delete') for volume in args.volume: try: _find_volume(cs, volume).delete() @@ -2138,7 +2150,8 @@ def do_volume_detach(cs, args): def do_volume_snapshot_list(cs, _args): - """List all the snapshots.""" + """DEPRECATED: List all the snapshots.""" + emit_volume_deprecation_warning('volume-snapshot-list') snapshots = cs.volume_snapshots.list() _translate_volume_snapshot_keys(snapshots) utils.print_list(snapshots, ['ID', 'Volume ID', 'Status', 'Display Name', @@ -2150,7 +2163,8 @@ def do_volume_snapshot_list(cs, _args): metavar='', help=_('Name or ID of the snapshot.')) def do_volume_snapshot_show(cs, args): - """Show details about a snapshot.""" + """DEPRECATED: Show details about a snapshot.""" + emit_volume_deprecation_warning('volume-snapshot-show') snapshot = _find_volume_snapshot(cs, args.snapshot) _print_volume_snapshot(snapshot) @@ -2182,7 +2196,8 @@ def do_volume_snapshot_show(cs, args): '--display_description', help=argparse.SUPPRESS) def do_volume_snapshot_create(cs, args): - """Add a new snapshot.""" + """DEPRECATED: Add a new snapshot.""" + emit_volume_deprecation_warning('volume-snapshot-create') snapshot = cs.volume_snapshots.create(args.volume_id, args.force, args.display_name, @@ -2195,7 +2210,8 @@ def do_volume_snapshot_create(cs, args): metavar='', help=_('Name or ID of the snapshot to delete.')) def do_volume_snapshot_delete(cs, args): - """Remove a snapshot.""" + """DEPRECATED: Remove a snapshot.""" + emit_volume_deprecation_warning('volume-snapshot-delete') snapshot = _find_volume_snapshot(cs, args.snapshot) snapshot.delete() @@ -2205,7 +2221,8 @@ def _print_volume_type_list(vtypes): def do_volume_type_list(cs, args): - """Print a list of available 'volume types'.""" + """DEPRECATED: Print a list of available 'volume types'.""" + emit_volume_deprecation_warning('volume-type-list') vtypes = cs.volume_types.list() _print_volume_type_list(vtypes) @@ -2215,7 +2232,8 @@ def do_volume_type_list(cs, args): metavar='', help=_("Name of the new volume type")) def do_volume_type_create(cs, args): - """Create a new volume type.""" + """DEPRECATED: Create a new volume type.""" + emit_volume_deprecation_warning('volume-type-create') vtype = cs.volume_types.create(args.name) _print_volume_type_list([vtype]) @@ -2225,7 +2243,8 @@ def do_volume_type_create(cs, args): metavar='', help=_("Unique ID of the volume type to delete")) def do_volume_type_delete(cs, args): - """Delete a specific volume type.""" + """DEPRECATED: Delete a specific volume type.""" + emit_volume_deprecation_warning('volume-type-delete') cs.volume_types.delete(args.id) diff --git a/novaclient/v2/volume_snapshots.py b/novaclient/v2/volume_snapshots.py index 6ff6deb9e..1bc154d32 100644 --- a/novaclient/v2/volume_snapshots.py +++ b/novaclient/v2/volume_snapshots.py @@ -14,15 +14,17 @@ # under the License. """ -Volume snapshot interface (1.1 extension). +DEPRECATED: Volume snapshot interface (1.1 extension). """ +import warnings + from novaclient import base class Snapshot(base.Resource): """ - A Snapshot is a point-in-time snapshot of an openstack volume. + DEPRECATED: A Snapshot is a point-in-time snapshot of an openstack volume. """ NAME_ATTR = 'display_name' @@ -31,14 +33,14 @@ class Snapshot(base.Resource): def delete(self): """ - Delete this snapshot. + DEPRECATED: Delete this snapshot. """ self.manager.delete(self) class SnapshotManager(base.ManagerWithFind): """ - Manage :class:`Snapshot` resources. + DEPRECATED: Manage :class:`Snapshot` resources. """ resource_class = Snapshot @@ -46,7 +48,7 @@ class SnapshotManager(base.ManagerWithFind): display_description=None): """ - Create a snapshot of the given volume. + DEPRECATED: Create a snapshot of the given volume. :param volume_id: The ID of the volume to snapshot. :param force: If force is True, create a snapshot even if the volume is @@ -55,6 +57,10 @@ class SnapshotManager(base.ManagerWithFind): :param display_description: Description of the snapshot :rtype: :class:`Snapshot` """ + warnings.warn('The novaclient.v2.volume_snapshots module is ' + 'deprecated and will be removed after Nova 2016.1 is ' + 'released. Use python-cinderclient or ' + 'python-openstacksdk instead.', DeprecationWarning) with self.alternate_service_type('volume'): body = {'snapshot': {'volume_id': volume_id, 'force': force, @@ -64,20 +70,28 @@ class SnapshotManager(base.ManagerWithFind): def get(self, snapshot_id): """ - Get a snapshot. + DEPRECATED: Get a snapshot. :param snapshot_id: The ID of the snapshot to get. :rtype: :class:`Snapshot` """ + warnings.warn('The novaclient.v2.volume_snapshots module is ' + 'deprecated and will be removed after Nova 2016.1 is ' + 'released. Use python-cinderclient or ' + 'python-openstacksdk instead.', DeprecationWarning) with self.alternate_service_type('volume'): return self._get("/snapshots/%s" % snapshot_id, "snapshot") def list(self, detailed=True): """ - Get a list of all snapshots. + DEPRECATED: Get a list of all snapshots. :rtype: list of :class:`Snapshot` """ + warnings.warn('The novaclient.v2.volume_snapshots module is ' + 'deprecated and will be removed after Nova 2016.1 is ' + 'released. Use python-cinderclient or ' + 'python-openstacksdk instead.', DeprecationWarning) with self.alternate_service_type('volume'): if detailed is True: return self._list("/snapshots/detail", "snapshots") @@ -86,9 +100,13 @@ class SnapshotManager(base.ManagerWithFind): def delete(self, snapshot): """ - Delete a snapshot. + DEPRECATED: Delete a snapshot. :param snapshot: The :class:`Snapshot` to delete. """ + warnings.warn('The novaclient.v2.volume_snapshots module is ' + 'deprecated and will be removed after Nova 2016.1 is ' + 'released. Use python-cinderclient or ' + 'python-openstacksdk instead.', DeprecationWarning) with self.alternate_service_type('volume'): self._delete("/snapshots/%s" % base.getid(snapshot)) diff --git a/novaclient/v2/volume_types.py b/novaclient/v2/volume_types.py index e36225b45..fd9268775 100644 --- a/novaclient/v2/volume_types.py +++ b/novaclient/v2/volume_types.py @@ -15,15 +15,17 @@ """ -Volume Type interface. +DEPRECATED: Volume Type interface. """ +import warnings + from novaclient import base class VolumeType(base.Resource): """ - A Volume Type is the type of volume to be created + DEPRECATED: A Volume Type is the type of volume to be created """ def __repr__(self): return "" % self.name @@ -31,46 +33,62 @@ class VolumeType(base.Resource): class VolumeTypeManager(base.ManagerWithFind): """ - Manage :class:`VolumeType` resources. + DEPRECATED: Manage :class:`VolumeType` resources. """ resource_class = VolumeType def list(self): """ - Get a list of all volume types. + DEPRECATED: Get a list of all volume types. :rtype: list of :class:`VolumeType`. """ + warnings.warn('The novaclient.v2.volume_types module is deprecated ' + 'and will be removed after Nova 2016.1 is released. Use ' + 'python-cinderclient or python-openstacksdk instead.', + DeprecationWarning) with self.alternate_service_type('volume'): return self._list("/types", "volume_types") def get(self, volume_type): """ - Get a specific volume type. + DEPRECATED: Get a specific volume type. :param volume_type: The ID of the :class:`VolumeType` to get. :rtype: :class:`VolumeType` """ + warnings.warn('The novaclient.v2.volume_types module is deprecated ' + 'and will be removed after Nova 2016.1 is released. Use ' + 'python-cinderclient or python-openstacksdk instead.', + DeprecationWarning) with self.alternate_service_type('volume'): return self._get("/types/%s" % base.getid(volume_type), "volume_type") def delete(self, volume_type): """ - Delete a specific volume_type. + DEPRECATED: Delete a specific volume_type. :param volume_type: The ID of the :class:`VolumeType` to get. """ + warnings.warn('The novaclient.v2.volume_types module is deprecated ' + 'and will be removed after Nova 2016.1 is released. Use ' + 'python-cinderclient or python-openstacksdk instead.', + DeprecationWarning) with self.alternate_service_type('volume'): self._delete("/types/%s" % base.getid(volume_type)) def create(self, name): """ - Create a volume type. + DEPRECATED: Create a volume type. :param name: Descriptive name of the volume type :rtype: :class:`VolumeType` """ + warnings.warn('The novaclient.v2.volume_types module is deprecated ' + 'and will be removed after Nova 2016.1 is released. Use ' + 'python-cinderclient or python-openstacksdk instead.', + DeprecationWarning) with self.alternate_service_type('volume'): body = { "volume_type": { diff --git a/novaclient/v2/volumes.py b/novaclient/v2/volumes.py index 182698ff5..b5f2ae687 100644 --- a/novaclient/v2/volumes.py +++ b/novaclient/v2/volumes.py @@ -17,6 +17,8 @@ Volume interface (1.1 extension). """ +import warnings + import six from six.moves.urllib import parse @@ -25,7 +27,8 @@ from novaclient import base class Volume(base.Resource): """ - A volume is an extra block level storage to the OpenStack instances. + DEPRECATED: A volume is an extra block level storage to the OpenStack + instances. """ NAME_ATTR = 'display_name' @@ -34,14 +37,14 @@ class Volume(base.Resource): def delete(self): """ - Delete this volume. + DEPRECATED: Delete this volume. """ self.manager.delete(self) class VolumeManager(base.ManagerWithFind): """ - Manage :class:`Volume` resources. + DEPRECATED: Manage :class:`Volume` resources. """ resource_class = Volume @@ -49,7 +52,7 @@ class VolumeManager(base.ManagerWithFind): display_description=None, volume_type=None, availability_zone=None, imageRef=None): """ - Create a volume. + DEPRECATED: Create a volume. :param size: Size of volume in GB :param snapshot_id: ID of the snapshot @@ -60,6 +63,10 @@ class VolumeManager(base.ManagerWithFind): :rtype: :class:`Volume` :param imageRef: reference to an image stored in glance """ + warnings.warn('The novaclient.v2.volumes.VolumeManager.create() ' + 'method is deprecated and will be removed after Nova ' + '2016.1 is released. Use python-cinderclient or ' + 'python-openstacksdk instead.', DeprecationWarning) # NOTE(melwitt): Ensure we use the volume endpoint for this call with self.alternate_service_type('volume'): body = {'volume': {'size': size, @@ -73,20 +80,28 @@ class VolumeManager(base.ManagerWithFind): def get(self, volume_id): """ - Get a volume. + DEPRECATED: Get a volume. :param volume_id: The ID of the volume to get. :rtype: :class:`Volume` """ + warnings.warn('The novaclient.v2.volumes.VolumeManager.get() ' + 'method is deprecated and will be removed after Nova ' + '2016.1 is released. Use python-cinderclient or ' + 'python-openstacksdk instead.', DeprecationWarning) with self.alternate_service_type('volume'): return self._get("/volumes/%s" % volume_id, "volume") def list(self, detailed=True, search_opts=None): """ - Get a list of all volumes. + DEPRECATED: Get a list of all volumes. :rtype: list of :class:`Volume` """ + warnings.warn('The novaclient.v2.volumes.VolumeManager.list() ' + 'method is deprecated and will be removed after Nova ' + '2016.1 is released. Use python-cinderclient or ' + 'python-openstacksdk instead.', DeprecationWarning) with self.alternate_service_type('volume'): search_opts = search_opts or {} @@ -105,10 +120,14 @@ class VolumeManager(base.ManagerWithFind): def delete(self, volume): """ - Delete a volume. + DEPRECATED: Delete a volume. :param volume: The :class:`Volume` to delete. """ + warnings.warn('The novaclient.v2.volumes.VolumeManager.delete() ' + 'method is deprecated and will be removed after Nova ' + '2016.1 is released. Use python-cinderclient or ' + 'python-openstacksdk instead.', DeprecationWarning) with self.alternate_service_type('volume'): self._delete("/volumes/%s" % base.getid(volume))