Remove deprecated image commands/API bindings

We deprecated the image proxy commands and APIs in Newton
due to the 2.36 microversion. We said after Ocata 15.0.0 we
would remove these, which we can do now in Pike.

Note that the list() method on the ImageManager has to be
moved to the GlanceManager since we still need to list images
for the --image-with option on the boot command. The _match_image
method in the shell has to be updated for a glance v2 response
where custom metadata properties are flat in the image body.

This needs to be released with a major version bump.

Change-Id: I2d9fd0243d42538bd1417a42357c17b09368d2a5
This commit is contained in:
Matt Riedemann 2017-02-22 07:19:07 -05:00 committed by Matt Riedemann
parent e7df023d31
commit 41f66d15aa
11 changed files with 44 additions and 448 deletions

View File

@ -79,10 +79,6 @@ class SimpleReadOnlyNovaClientTest(base.ClientTestBase):
def test_admin_hypervisor_list(self):
self.nova('hypervisor-list')
def test_admin_image_list(self):
out = self.nova('image-list', merge_stderr=True)
self.assertIn('Command image-list is deprecated', out)
@decorators.skip_because(bug="1157349")
def test_admin_interface_list(self):
self.nova('interface-list')

View File

@ -21,15 +21,6 @@ class TestImageMetaV239(base.ClientTestBase):
# fallback to 2.35 and emit a warning.
COMPUTE_API_VERSION = "2.39"
def test_command_deprecation(self):
output = self.nova('image-meta %s set test_key=test_value' %
self.image.id, merge_stderr=True)
self.assertIn('is deprecated', output)
output = self.nova('image-meta %s delete test_key' %
self.image.id, merge_stderr=True)
self.assertIn('is deprecated', output)
def test_limits(self):
"""Tests that 2.39 won't return 'maxImageMeta' resource limit and
the CLI output won't show it.

View File

@ -13,8 +13,6 @@
import random
import string
import novaclient
from novaclient import api_versions
from novaclient.tests.functional import base
from novaclient.tests.functional.v2.legacy import test_servers
from novaclient.v2 import shell
@ -25,18 +23,6 @@ class TestServersBootNovaClient(test_servers.TestServersBootNovaClient):
COMPUTE_API_VERSION = "2.latest"
def test_boot_server_using_image_with(self):
# --image-with relies on listing images via the compute image proxy
# API which does not work after 2.35 so we have to cap for this test.
try:
self.COMPUTE_API_VERSION = (
min(novaclient.API_MAX_VERSION,
api_versions.APIVersion('2.35')).get_string())
super(TestServersBootNovaClient,
self).test_boot_server_using_image_with()
finally:
self.COMPUTE_API_VERSION = '2.latest'
class TestServersListNovaClient(test_servers.TestServersListNovaClient):
"""Servers list functional tests."""

View File

@ -16,7 +16,7 @@ from novaclient.tests.unit.fixture_data import base
class V1(base.Fixture):
base_url = 'images'
base_url = 'v2/images'
def setUp(self):
super(V1, self).setUp()
@ -78,8 +78,3 @@ class V1(base.Fixture):
for u in (1, '1/metadata/test_key'):
self.requests_mock.delete(self.url(u), status_code=204,
headers=headers)
class V3(V1):
base_url = 'v1/images'

View File

@ -1103,7 +1103,7 @@ class FakeSessionClient(base_client.SessionClient):
#
# Images
#
def get_images_detail(self, **kw):
def get_images(self, **kw):
return (200, {}, {'images': [
{
"id": FAKE_IMAGE_UUID_SNAPSHOT,
@ -1131,9 +1131,7 @@ class FakeSessionClient(base_client.SessionClient):
"updated": "2010-10-10T12:00:00Z",
"created": "2010-08-10T12:00:00Z",
"status": "ACTIVE",
"metadata": {
"test_key": "test_value",
},
"test_key": "test_value",
"links": {},
},
{
@ -1149,41 +1147,20 @@ class FakeSessionClient(base_client.SessionClient):
]})
def get_images_555cae93_fb41_4145_9c52_f5b923538a26(self, **kw):
return (200, {}, {'image': self.get_images_detail()[2]['images'][0]})
return (200, {}, {'image': self.get_images()[2]['images'][0]})
def get_images_55bb23af_97a4_4068_bdf8_f10c62880ddf(self, **kw):
return (200, {}, {'image': self.get_images_detail()[2]['images'][1]})
return (200, {}, {'image': self.get_images()[2]['images'][1]})
def get_images_c99d7632_bd66_4be9_aed5_3dd14b223a76(self, **kw):
return (200, {}, {'image': self.get_images_detail()[2]['images'][2]})
return (200, {}, {'image': self.get_images()[2]['images'][2]})
def get_images_f27f479a_ddda_419a_9bbc_d6b56b210161(self, **kw):
return (200, {}, {'image': self.get_images_detail()[2]['images'][3]})
return (200, {}, {'image': self.get_images()[2]['images'][3]})
def get_images_3e861307_73a6_4d1f_8d68_f68b03223032(self):
raise exceptions.NotFound('404')
def post_images_c99d7632_bd66_4be9_aed5_3dd14b223a76_metadata(
self, body, **kw):
assert list(body) == ['metadata']
fakes.assert_has_keys(body['metadata'],
required=['test_key'])
get_image = self.get_images_c99d7632_bd66_4be9_aed5_3dd14b223a76
return (
200,
{},
{'metadata': get_image()[2]['image']['metadata']})
def delete_images_c99d7632_bd66_4be9_aed5_3dd14b223a76(self, **kw):
return (204, {}, None)
def delete_images_f27f479a_ddda_419a_9bbc_d6b56b210161(self, **kw):
return (204, {}, None)
def delete_images_c99d7632_bd66_4be9_aed5_3dd14b223a76_metadata_test_key(
self, **kw):
return (204, {}, None)
#
# Keypairs
#

View File

@ -11,12 +11,8 @@
# License for the specific language governing permissions and limitations
# under the License.
import warnings
import mock
from novaclient import api_versions
from novaclient import exceptions
from novaclient.tests.unit.fixture_data import client
from novaclient.tests.unit.fixture_data import images as data
from novaclient.tests.unit import utils
@ -29,95 +25,13 @@ class ImagesTest(utils.FixturedTestCase):
client_fixture_class = client.V1
data_fixture_class = data.V1
@mock.patch.object(warnings, 'warn')
def test_list_images(self, mock_warn):
il = self.cs.images.list()
@mock.patch('novaclient.base.Manager.alternate_service_type')
def test_list_images(self, mock_alternate_service_type):
il = self.cs.glance.list()
self.assert_request_id(il, fakes.FAKE_REQUEST_ID_LIST)
self.assert_called('GET', '/images/detail')
self.assert_called('GET', '/v2/images')
for i in il:
self.assertIsInstance(i, images.Image)
self.assertEqual(2, len(il))
self.assertEqual(1, mock_warn.call_count)
def test_list_images_undetailed(self):
il = self.cs.images.list(detailed=False)
self.assert_request_id(il, fakes.FAKE_REQUEST_ID_LIST)
self.assert_called('GET', '/images')
for i in il:
self.assertIsInstance(i, images.Image)
def test_list_images_with_marker_limit(self):
il = self.cs.images.list(marker=1234, limit=4)
self.assert_request_id(il, fakes.FAKE_REQUEST_ID_LIST)
self.assert_called('GET', '/images/detail?limit=4&marker=1234')
@mock.patch.object(warnings, 'warn')
def test_get_image_details(self, mock_warn):
i = self.cs.images.get(1)
self.assert_request_id(i, fakes.FAKE_REQUEST_ID_LIST)
self.assert_called('GET', '/images/1')
self.assertIsInstance(i, images.Image)
self.assertEqual(1, i.id)
self.assertEqual('CentOS 5.2', i.name)
self.assertEqual(1, mock_warn.call_count)
@mock.patch.object(warnings, 'warn')
def test_delete_image(self, mock_warn):
i = self.cs.images.delete(1)
self.assert_request_id(i, fakes.FAKE_REQUEST_ID_LIST)
self.assert_called('DELETE', '/images/1')
self.assertEqual(1, mock_warn.call_count)
@mock.patch.object(warnings, 'warn')
def test_delete_meta(self, mock_warn):
i = self.cs.images.delete_meta(1, {'test_key': 'test_value'})
self.assert_request_id(i, fakes.FAKE_REQUEST_ID_LIST)
self.assert_called('DELETE', '/images/1/metadata/test_key')
self.assertEqual(1, mock_warn.call_count)
@mock.patch.object(warnings, 'warn')
def test_set_meta(self, mock_warn):
i = self.cs.images.set_meta(1, {'test_key': 'test_value'})
self.assert_request_id(i, fakes.FAKE_REQUEST_ID_LIST)
self.assert_called('POST', '/images/1/metadata',
{"metadata": {'test_key': 'test_value'}})
self.assertEqual(1, mock_warn.call_count)
@mock.patch.object(warnings, 'warn')
def test_find(self, mock_warn):
i = self.cs.images.find(name="CentOS 5.2")
self.assert_request_id(i, fakes.FAKE_REQUEST_ID_LIST)
self.assertEqual(1, i.id)
self.assert_called('GET', '/images/1')
# This is two warnings because find calls findall which calls list
# which is the first warning, which finds one results and then calls
# get on that, which is the second warning.
self.assertEqual(2, mock_warn.call_count)
iml = self.cs.images.findall(status='SAVING')
self.assert_request_id(iml, fakes.FAKE_REQUEST_ID_LIST)
self.assertEqual(1, len(iml))
self.assertEqual('My Server Backup', iml[0].name)
def test_find_2_36(self):
"""Tests that using the find method fails after microversion 2.35.
"""
self.cs.api_version = api_versions.APIVersion('2.36')
self.assertRaises(exceptions.VersionNotFoundForAPIMethod,
self.cs.images.find, name="CentOS 5.2")
def test_delete_meta_2_39(self):
"""Tests that 'delete_meta' method fails after microversion 2.39.
"""
self.cs.api_version = api_versions.APIVersion('2.39')
self.assertRaises(exceptions.VersionNotFoundForAPIMethod,
self.cs.images.delete_meta, 1,
{'test_key': 'test_value'})
def test_set_meta_2_39(self):
"""Tests that 'set_meta' method fails after microversion 2.39.
"""
self.cs.api_version = api_versions.APIVersion('2.39')
self.assertRaises(exceptions.VersionNotFoundForAPIMethod,
self.cs.images.set_meta, 1,
{'test_key': 'test_value'})
mock_alternate_service_type.assert_called_once_with(
'image', allowed_types=('image',))

View File

@ -1199,35 +1199,6 @@ class ShellTest(utils.TestCase):
self.assert_called('POST', '/flavors/2/action',
{'removeTenantAccess': {'tenant': 'proj2'}})
def test_image_show(self):
_out, err = self.run_command('image-show %s' % FAKE_UUID_1)
self.assertIn('Command image-show is deprecated', err)
self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1)
def test_image_meta_set(self):
_out, err = self.run_command('image-meta %s set test_key=test_value' %
FAKE_UUID_1)
self.assertIn('Command image-meta is deprecated', err)
self.assert_called('POST', '/images/%s/metadata' % FAKE_UUID_1,
{'metadata': {'test_key': 'test_value'}})
def test_image_meta_del(self):
_out, err = self.run_command('image-meta %s delete test_key' %
FAKE_UUID_1)
self.assertIn('Command image-meta is deprecated', err)
self.assert_called('DELETE', '/images/%s/metadata/test_key' %
FAKE_UUID_1)
@mock.patch('sys.stdout', six.StringIO())
@mock.patch('sys.stderr', six.StringIO())
def test_image_meta_bad_action(self):
self.assertRaises(SystemExit, self.run_command,
'image-meta 1 BAD_ACTION test_key=test_value')
def test_image_list(self):
self.run_command('image-list')
self.assert_called('GET', '/images/detail')
def test_create_image(self):
self.run_command('image-create sample-server mysnapshot')
self.assert_called(
@ -1273,18 +1244,6 @@ class ShellTest(utils.TestCase):
exceptions.InstanceInDeletedState, self.run_command,
'image-create sample-server mysnapshot_deleted --poll')
def test_image_delete(self):
_out, err = self.run_command('image-delete %s' % FAKE_UUID_1)
self.assertIn('Command image-delete is deprecated', err)
self.assert_called('DELETE', '/images/%s' % FAKE_UUID_1)
def test_image_delete_multiple(self):
self.run_command('image-delete %s %s' % (FAKE_UUID_1, FAKE_UUID_2))
self.assert_called('GET', '/v2/images/' + FAKE_UUID_1, pos=0)
self.assert_called('DELETE', '/images/' + FAKE_UUID_1, pos=1)
self.assert_called('GET', '/v2/images/' + FAKE_UUID_2, pos=2)
self.assert_called('DELETE', '/images/' + FAKE_UUID_2, pos=3)
def test_list(self):
self.run_command('list')
self.assert_called('GET', '/servers/detail')
@ -3545,39 +3504,3 @@ class ShellNetworkUtilTest(utils.TestCase):
# the deprecated_network decorator will set cs.client.api_version
# after calling the wrapped function
self.assertEqual(cs.api_version, cs.api_version)
class ShellImageUtilTest(utils.TestCase):
def test_deprecated_image_newer(self):
@novaclient.v2.shell.deprecated_image
def tester(cs):
'foo'
self.assertEqual(api_versions.APIVersion('2.35'),
cs.api_version)
cs = mock.MagicMock()
cs.api_version = api_versions.APIVersion('2.9999')
tester(cs)
self.assertEqual('DEPRECATED: foo', tester.__doc__)
def test_deprecated_image_older(self):
@novaclient.v2.shell.deprecated_image
def tester(cs):
'foo'
# since we didn't need to adjust the api_version the mock won't
# have cs.client.api_version set on it
self.assertFalse(hasattr(cs, 'client'))
# we have to set the attribute back on cs so the decorator can
# set the value on it when we return from this wrapped function
setattr(cs, 'client', mock.MagicMock())
cs = mock.MagicMock()
cs.api_version = api_versions.APIVersion('2.1')
# we have to delete the cs.client attribute so hasattr won't return a
# false positive in the wrapped function
del cs.client
tester(cs)
self.assertEqual('DEPRECATED: foo', tester.__doc__)
# the deprecated_network decorator will set cs.client.api_version
# after calling the wrapped function
self.assertEqual(cs.api_version, cs.api_version)

View File

@ -153,7 +153,6 @@ class Client(object):
self.user_id = user_id
self.flavors = flavors.FlavorManager(self)
self.flavor_access = flavor_access.FlavorAccessManager(self)
self.images = images.ImageManager(self)
self.glance = images.GlanceManager(self)
self.limits = limits.LimitsManager(self)
self.servers = servers.ServerManager(self)

View File

@ -12,44 +12,25 @@
# License for the specific language governing permissions and limitations
# under the License.
"""
DEPRECATED: Image interface.
"""
import warnings
from oslo_utils import uuidutils
from six.moves.urllib import parse
from novaclient import api_versions
from novaclient import base
from novaclient import exceptions
from novaclient.i18n import _
class Image(base.Resource):
"""
DEPRECATED: An image is a collection of files used to create or rebuild a
server.
"""
HUMAN_ID = True
def __repr__(self):
return "<Image: %s>" % self.name
def delete(self):
"""
DEPRECATED: Delete this image.
:returns: An instance of novaclient.base.TupleWithMeta
"""
return self.manager.delete(self)
class GlanceManager(base.Manager):
"""Use glance directly from service catalog.
This is used to do name to id lookups for images. Do not use it
This is used to do name to id lookups for images and listing images for
the --image-with option to the 'boot' command. Do not use it
for anything else besides that. You have been warned.
"""
@ -85,110 +66,11 @@ class GlanceManager(base.Manager):
matches[0].append_request_ids(matches.request_ids)
return matches[0]
class ImageManager(base.ManagerWithFind):
"""
DEPRECATED: Manage :class:`Image` resources.
"""
resource_class = Image
@api_versions.wraps('2.0', '2.35')
def get(self, image):
def list(self):
"""
DEPRECATED: Get an image.
:param image: The ID of the image to get.
:rtype: :class:`Image`
"""
warnings.warn(
'The novaclient.v2.images module is deprecated and will be '
'removed after Nova 15.0.0 is released. Use python-glanceclient '
'or python-openstacksdk instead.', DeprecationWarning)
return self._get("/images/%s" % base.getid(image), "image")
def list(self, detailed=True, limit=None, marker=None):
"""
DEPRECATED: Get a list of all images.
Get a detailed list of all images.
:rtype: list of :class:`Image`
:param limit: maximum number of images to return.
:param marker: Begin returning images that appear later in the image
list than that represented by this image id (optional).
"""
# FIXME(mriedem): Should use the api_versions.wraps decorator but that
# breaks the ManagerWithFind.findall method which checks the argspec
# on this function looking for the 'detailed' arg, and it's getting
# tripped up if you use the wraps decorator. This is all deprecated for
# removal anyway so we probably don't care too much about this.
if self.api.api_version > api_versions.APIVersion('2.35'):
raise exceptions.VersionNotFoundForAPIMethod(
self.api.api_version, 'list')
warnings.warn(
'The novaclient.v2.images module is deprecated and will be '
'removed after Nova 15.0.0 is released. Use python-glanceclient '
'or python-openstacksdk instead.', DeprecationWarning)
params = {}
detail = ''
if detailed:
detail = '/detail'
if limit:
params['limit'] = int(limit)
if marker:
params['marker'] = str(marker)
params = sorted(params.items(), key=lambda x: x[0])
query = '?%s' % parse.urlencode(params) if params else ''
return self._list('/images%s%s' % (detail, query), 'images')
@api_versions.wraps('2.0', '2.35')
def delete(self, image):
"""
DEPRECATED: Delete an image.
It should go without saying that you can't delete an image
that you didn't create.
:param image: The :class:`Image` (or its ID) to delete.
:returns: An instance of novaclient.base.TupleWithMeta
"""
warnings.warn(
'The novaclient.v2.images module is deprecated and will be '
'removed after Nova 15.0.0 is released. Use python-glanceclient '
'or python-openstacksdk instead.', DeprecationWarning)
return self._delete("/images/%s" % base.getid(image))
@api_versions.wraps('2.0', '2.38')
def set_meta(self, image, metadata):
"""
DEPRECATED: Set an images metadata
:param image: The :class:`Image` to add metadata to
:param metadata: A dict of metadata to add to the image
"""
warnings.warn(
'The novaclient.v2.images module is deprecated and will be '
'removed after Nova 15.0.0 is released. Use python-glanceclient '
'or python-openstacksdk instead.', DeprecationWarning)
body = {'metadata': metadata}
return self._create("/images/%s/metadata" % base.getid(image),
body, "metadata")
@api_versions.wraps('2.0', '2.38')
def delete_meta(self, image, keys):
"""
DEPRECATED: Delete metadata from an image
:param image: The :class:`Image` to delete metadata
:param keys: A list of metadata keys to delete from the image
:returns: An instance of novaclient.base.TupleWithMeta
"""
warnings.warn(
'The novaclient.v2.images module is deprecated and will be '
'removed after Nova 15.0.0 is released. Use python-glanceclient '
'or python-openstacksdk instead.', DeprecationWarning)
result = base.TupleWithMeta((), None)
for k in keys:
ret = self._delete("/images/%s/metadata/%s" %
(base.getid(image), k))
result.append_request_ids(ret.request_ids)
return result
with self.alternate_service_type('image', allowed_types=('image',)):
return self._list('/v2/images', 'images')

View File

@ -72,10 +72,6 @@ msg_deprecate_net = ('WARNING: Command %s is deprecated and will be removed '
'after Nova 15.0.0 is released. Use python-neutronclient '
'or openstackclient instead.')
msg_deprecate_img = ('WARNING: Command %s is deprecated and will be removed '
'after Nova 15.0.0 is released. Use python-glanceclient '
'or openstackclient instead')
def deprecated_proxy(fn, msg_format):
@functools.wraps(fn)
@ -100,9 +96,6 @@ def deprecated_proxy(fn, msg_format):
deprecated_network = functools.partial(deprecated_proxy,
msg_format=msg_deprecate_net)
deprecated_image = functools.partial(deprecated_proxy,
msg_format=msg_deprecate_img)
def _key_value_pairing(text):
try:
@ -118,15 +111,21 @@ def _meta_parsing(metadata):
def _match_image(cs, wanted_properties):
image_list = cs.images.list()
image_list = cs.glance.list()
images_matched = []
match = set(wanted_properties)
for img in image_list:
try:
if match == match.intersection(set(img.metadata.items())):
images_matched.append(img)
except AttributeError:
pass
img_dict = {}
# exclude any unhashable entries
for key, value in img.to_dict().items():
try:
set([key, value])
except TypeError:
pass
else:
img_dict[key] = value
if match == match.intersection(set(img_dict.items())):
images_matched.append(img)
return images_matched
@ -1350,57 +1349,6 @@ def do_network_create(cs, args):
cs.networks.create(**kwargs)
@utils.arg(
'--limit',
dest="limit",
metavar="<limit>",
help=_('Number of images to return per request.'))
@deprecated_image
def do_image_list(cs, _args):
"""Print a list of available images to boot from."""
limit = _args.limit
image_list = cs.images.list(limit=limit)
def parse_server_name(image):
try:
return image.server['id']
except (AttributeError, KeyError):
return ''
fmts = {'Server': parse_server_name}
utils.print_list(image_list, ['ID', 'Name', 'Status', 'Server'],
fmts, sortby_index=1)
@utils.arg(
'image',
metavar='<image>',
help=_("Name or ID of image."))
@utils.arg(
'action',
metavar='<action>',
choices=['set', 'delete'],
help=_("Actions: 'set' or 'delete'."))
@utils.arg(
'metadata',
metavar='<key=value>',
nargs='+',
action='append',
default=[],
help=_('Metadata to add/update or delete (only key is necessary on '
'delete).'))
@deprecated_image
def do_image_meta(cs, args):
"""Set or delete metadata on an image."""
image = _find_image(cs, args.image)
metadata = _extract_metadata(args)
if args.action == 'set':
cs.images.set_meta(image, metadata)
elif args.action == 'delete':
cs.images.delete_meta(image, metadata.keys())
def _extract_metadata(args):
metadata = {}
for metadatum in args.metadata[0]:
@ -1449,34 +1397,6 @@ def _print_flavor(flavor):
utils.print_dict(info)
@utils.arg(
'image',
metavar='<image>',
help=_("Name or ID of image."))
@deprecated_image
def do_image_show(cs, args):
"""Show details about the given image."""
image = _find_image(cs, args.image)
_print_image(image)
@utils.arg(
'image', metavar='<image>', nargs='+',
help=_('Name or ID of image(s).'))
@deprecated_image
def do_image_delete(cs, args):
"""Delete specified image(s)."""
for image in args.image:
try:
# _find_image is using the GlanceManager which doesn't implement
# the delete() method so use the ImagesManager for that.
image = _find_image(cs, image)
cs.images.delete(image)
except Exception as e:
print(_("Delete for image %(image)s failed: %(e)s") %
{'image': image, 'e': e})
@utils.arg(
'--reservation-id',
dest='reservation_id',

View File

@ -0,0 +1,13 @@
---
prelude: >
Deprecated image commands and python API bindings have been removed.
upgrade:
- |
The following deprecated image commands have been removed::
* nova image-list
* nova image-show
* nova image-meta
* nova image-delete
Along with the related python API bindings in ``novaclient.v2.images``.