Merge "Replace glanceclient usage with openstacksdk"

This commit is contained in:
Zuul 2024-04-29 15:26:56 +00:00 committed by Gerrit Code Review
commit b6b8ee07ce
7 changed files with 96 additions and 133 deletions

View File

@ -229,7 +229,6 @@ default:
amqp=WARNING amqp=WARNING
amqplib=WARNING amqplib=WARNING
eventlet.wsgi.server=INFO eventlet.wsgi.server=INFO
glanceclient=WARNING
iso8601=WARNING iso8601=WARNING
keystoneauth.session=INFO keystoneauth.session=INFO
keystonemiddleware.auth_token=INFO keystonemiddleware.auth_token=INFO

View File

@ -21,8 +21,9 @@ import sys
import time import time
from urllib import parse as urlparse from urllib import parse as urlparse
from glanceclient import client from keystoneauth1 import exceptions as ks_exception
from glanceclient import exc as glance_exc import openstack
from openstack.connection import exceptions as openstack_exc
from oslo_log import log from oslo_log import log
from oslo_utils import uuidutils from oslo_utils import uuidutils
import tenacity import tenacity
@ -44,12 +45,11 @@ _GLANCE_SESSION = None
def _translate_image_exception(image_id, exc_value): def _translate_image_exception(image_id, exc_value):
if isinstance(exc_value, (glance_exc.Forbidden, if isinstance(exc_value, (openstack_exc.ForbiddenException)):
glance_exc.Unauthorized)):
return exception.ImageNotAuthorized(image_id=image_id) return exception.ImageNotAuthorized(image_id=image_id)
if isinstance(exc_value, glance_exc.NotFound): if isinstance(exc_value, openstack_exc.NotFoundException):
return exception.ImageNotFound(image_id=image_id) return exception.ImageNotFound(image_id=image_id)
if isinstance(exc_value, glance_exc.BadRequest): if isinstance(exc_value, openstack_exc.BadRequestException):
return exception.Invalid(exc_value) return exception.Invalid(exc_value)
return exc_value return exc_value
@ -70,8 +70,6 @@ def check_image_service(func):
if not _GLANCE_SESSION: if not _GLANCE_SESSION:
_GLANCE_SESSION = keystone.get_session('glance') _GLANCE_SESSION = keystone.get_session('glance')
# NOTE(pas-ha) glanceclient uses Adapter-based SessionClient,
# so we can pass session and auth separately, makes things easier
service_auth = keystone.get_auth('glance') service_auth = keystone.get_auth('glance')
self.endpoint = keystone.get_endpoint('glance', self.endpoint = keystone.get_endpoint('glance',
@ -85,10 +83,15 @@ def check_image_service(func):
if self.context.auth_token: if self.context.auth_token:
user_auth = keystone.get_service_auth(self.context, self.endpoint, user_auth = keystone.get_service_auth(self.context, self.endpoint,
service_auth) service_auth)
self.client = client.Client(2, session=_GLANCE_SESSION, sess = keystone.get_session('glance',
auth=user_auth or service_auth, auth=user_auth or service_auth)
endpoint_override=self.endpoint, conn = openstack.connection.Connection(
global_request_id=self.context.global_id) session=sess,
image_endpoint_override=self.endpoint,
image_api_version='2')
self.client = conn.global_request(self.context.global_id).image
return func(self, *args, **kwargs) return func(self, *args, **kwargs)
return wrapper return wrapper
@ -130,28 +133,22 @@ class GlanceImageService(object):
:raises: GlanceConnectionFailed :raises: GlanceConnectionFailed
""" """
retry_excs = (glance_exc.ServiceUnavailable,
glance_exc.InvalidEndpoint,
glance_exc.CommunicationError)
image_excs = (glance_exc.Forbidden,
glance_exc.Unauthorized,
glance_exc.NotFound,
glance_exc.BadRequest)
try: try:
return getattr(self.client.images, method)(*args, **kwargs) return getattr(self.client, method)(*args, **kwargs)
except retry_excs as e: except openstack_exc.SDKException:
exc_type, exc_value, exc_trace = sys.exc_info()
new_exc = _translate_image_exception(
args[0], exc_value)
if isinstance(new_exc, exception.IronicException):
# exception has been translated to a new one, raise it
raise type(new_exc)(new_exc).with_traceback(exc_trace)
except ks_exception.ClientException as e:
error_msg = ("Error contacting glance endpoint " error_msg = ("Error contacting glance endpoint "
"%(endpoint)s for '%(method)s'") "%(endpoint)s for '%(method)s'")
LOG.exception(error_msg, {'endpoint': self.endpoint, LOG.exception(error_msg, {'endpoint': self.endpoint,
'method': method}) 'method': method})
raise exception.GlanceConnectionFailed( raise exception.GlanceConnectionFailed(
endpoint=self.endpoint, reason=e) endpoint=self.endpoint, reason=e)
except image_excs:
exc_type, exc_value, exc_trace = sys.exc_info()
new_exc = _translate_image_exception(
args[0], exc_value)
raise type(new_exc)(new_exc).with_traceback(exc_trace)
@check_image_service @check_image_service
def show(self, image_href): def show(self, image_href):
@ -167,7 +164,7 @@ class GlanceImageService(object):
image_href) image_href)
image_id = service_utils.parse_image_id(image_href) image_id = service_utils.parse_image_id(image_href)
image = self.call('get', image_id) image = self.call('get_image', image_id)
if not service_utils.is_image_active(image): if not service_utils.is_image_active(image):
raise exception.ImageUnacceptable( raise exception.ImageUnacceptable(
@ -198,18 +195,20 @@ class GlanceImageService(object):
os.sendfile(data.fileno(), f.fileno(), 0, filesize) os.sendfile(data.fileno(), f.fileno(), 0, filesize)
return return
image_chunks = self.call('data', image_id) image_size = 0
# NOTE(dtantsur): when using Glance V2, image_chunks is a wrapper image_data = None
# around real data, so we have to check the wrapped data for None. if data:
if image_chunks.wrapped is None: image_chunks = self.call('download_image', image_id, stream=True)
raise exception.ImageDownloadFailed(
image_href=image_href, reason=_('image contains no data.'))
if data is None:
return image_chunks
else:
for chunk in image_chunks: for chunk in image_chunks:
data.write(chunk) data.write(chunk)
image_size += len(chunk)
else:
image_data = self.call('download_image', image_id).content
image_size = len(image_data)
if image_size == 0:
raise exception.ImageDownloadFailed(
image_href=image_href, reason=_('image contains no data.'))
return image_data
def _generate_temp_url(self, path, seconds, key, method, endpoint, def _generate_temp_url(self, path, seconds, key, method, endpoint,
image_id): image_id):
@ -400,7 +399,7 @@ class GlanceImageService(object):
Returns the direct url representing the backend storage location, Returns the direct url representing the backend storage location,
or None if this attribute is not shown by Glance. or None if this attribute is not shown by Glance.
""" """
image_meta = self.call('get', image_id) image_meta = self.call('get_image', image_id)
if not service_utils.is_image_available(self.context, image_meta): if not service_utils.is_image_available(self.context, image_meta):
raise exception.ImageNotFound(image_id=image_id) raise exception.ImageNotFound(image_id=image_id)

View File

@ -82,7 +82,6 @@ def update_opt_defaults():
'eventlet.wsgi.server=INFO', 'eventlet.wsgi.server=INFO',
'iso8601=WARNING', 'iso8601=WARNING',
'requests=WARNING', 'requests=WARNING',
'glanceclient=WARNING',
'urllib3.connectionpool=WARNING', 'urllib3.connectionpool=WARNING',
'keystonemiddleware.auth_token=INFO', 'keystonemiddleware.auth_token=INFO',
'keystoneauth.session=INFO', 'keystoneauth.session=INFO',

View File

@ -19,9 +19,10 @@ import importlib
import time import time
from unittest import mock from unittest import mock
from glanceclient import client as glance_client from keystoneauth1 import exceptions as ks_exception
from glanceclient import exc as glance_exc
from keystoneauth1 import loading as ks_loading from keystoneauth1 import loading as ks_loading
import openstack
from openstack.connection import exceptions as openstack_exc
from oslo_config import cfg from oslo_config import cfg
from oslo_utils import uuidutils from oslo_utils import uuidutils
import testtools import testtools
@ -150,7 +151,7 @@ class TestGlanceImageService(base.TestCase):
with mock.patch.object(self.service, 'call', autospec=True): with mock.patch.object(self.service, 'call', autospec=True):
self.service.call.return_value = image self.service.call.return_value = image
image_meta = self.service.show(image_id) image_meta = self.service.show(image_id)
self.service.call.assert_called_with('get', image_id) self.service.call.assert_called_with('get_image', image_id)
self.assertEqual(expected, image_meta) self.assertEqual(expected, image_meta)
def test_show_makes_datetimes(self): def test_show_makes_datetimes(self):
@ -159,7 +160,7 @@ class TestGlanceImageService(base.TestCase):
with mock.patch.object(self.service, 'call', autospec=True): with mock.patch.object(self.service, 'call', autospec=True):
self.service.call.return_value = image self.service.call.return_value = image
image_meta = self.service.show(image_id) image_meta = self.service.show(image_id)
self.service.call.assert_called_with('get', image_id) self.service.call.assert_called_with('get_image', image_id)
self.assertEqual(self.NOW_DATETIME, image_meta['created_at']) self.assertEqual(self.NOW_DATETIME, image_meta['created_at'])
self.assertEqual(self.NOW_DATETIME, image_meta['updated_at']) self.assertEqual(self.NOW_DATETIME, image_meta['updated_at'])
@ -185,10 +186,10 @@ class TestGlanceImageService(base.TestCase):
class MyGlanceStubClient(stubs.StubGlanceClient): class MyGlanceStubClient(stubs.StubGlanceClient):
"""A client that fails the first time, then succeeds.""" """A client that fails the first time, then succeeds."""
def get(self, image_id): def get_image(self, image_id):
if tries[0] == 0: if tries[0] == 0:
tries[0] = 1 tries[0] = 1
raise glance_exc.ServiceUnavailable('') raise ks_exception.ServiceUnavailable()
else: else:
return {} return {}
@ -216,11 +217,11 @@ class TestGlanceImageService(base.TestCase):
stub_service.download(image_id, writer) stub_service.download(image_id, writer)
def test_download_no_data(self): def test_download_no_data(self):
self.client.fake_wrapped = None self.client.image_data = b''
image_id = uuidutils.generate_uuid() image_id = uuidutils.generate_uuid()
image = self._make_datetime_fixture() image = self._make_datetime_fixture()
with mock.patch.object(self.client, 'get', return_value=image, with mock.patch.object(self.client, 'get_image', return_value=image,
autospec=True): autospec=True):
self.assertRaisesRegex(exception.ImageDownloadFailed, self.assertRaisesRegex(exception.ImageDownloadFailed,
'image contains no data', 'image contains no data',
@ -237,7 +238,7 @@ class TestGlanceImageService(base.TestCase):
s_tmpfname = '/whatever/source' s_tmpfname = '/whatever/source'
def get(self, image_id): def get_image(self, image_id):
return type('GlanceTestDirectUrlMeta', (object,), return type('GlanceTestDirectUrlMeta', (object,),
{'direct_url': 'file://%s' + self.s_tmpfname}) {'direct_url': 'file://%s' + self.s_tmpfname})
@ -275,25 +276,8 @@ class TestGlanceImageService(base.TestCase):
def test_client_forbidden_converts_to_imagenotauthed(self): def test_client_forbidden_converts_to_imagenotauthed(self):
class MyGlanceStubClient(stubs.StubGlanceClient): class MyGlanceStubClient(stubs.StubGlanceClient):
"""A client that raises a Forbidden exception.""" """A client that raises a Forbidden exception."""
def get(self, image_id): def get_image(self, image_id):
raise glance_exc.Forbidden(image_id) raise openstack_exc.ForbiddenException()
stub_client = MyGlanceStubClient()
stub_context = context.RequestContext(auth_token=True)
stub_context.user_id = 'fake'
stub_context.project_id = 'fake'
stub_service = image_service.GlanceImageService(stub_client,
stub_context)
image_id = uuidutils.generate_uuid()
writer = NullWriter()
self.assertRaises(exception.ImageNotAuthorized, stub_service.download,
image_id, writer)
def test_client_httpforbidden_converts_to_imagenotauthed(self):
class MyGlanceStubClient(stubs.StubGlanceClient):
"""A client that raises a HTTPForbidden exception."""
def get(self, image_id):
raise glance_exc.HTTPForbidden(image_id)
stub_client = MyGlanceStubClient() stub_client = MyGlanceStubClient()
stub_context = context.RequestContext(auth_token=True) stub_context = context.RequestContext(auth_token=True)
@ -309,25 +293,8 @@ class TestGlanceImageService(base.TestCase):
def test_client_notfound_converts_to_imagenotfound(self): def test_client_notfound_converts_to_imagenotfound(self):
class MyGlanceStubClient(stubs.StubGlanceClient): class MyGlanceStubClient(stubs.StubGlanceClient):
"""A client that raises a NotFound exception.""" """A client that raises a NotFound exception."""
def get(self, image_id): def get_image(self, image_id):
raise glance_exc.NotFound(image_id) raise openstack_exc.NotFoundException()
stub_client = MyGlanceStubClient()
stub_context = context.RequestContext(auth_token=True)
stub_context.user_id = 'fake'
stub_context.project_id = 'fake'
stub_service = image_service.GlanceImageService(stub_client,
stub_context)
image_id = uuidutils.generate_uuid()
writer = NullWriter()
self.assertRaises(exception.ImageNotFound, stub_service.download,
image_id, writer)
def test_client_httpnotfound_converts_to_imagenotfound(self):
class MyGlanceStubClient(stubs.StubGlanceClient):
"""A client that raises a HTTPNotFound exception."""
def get(self, image_id):
raise glance_exc.HTTPNotFound(image_id)
stub_client = MyGlanceStubClient() stub_client = MyGlanceStubClient()
stub_context = context.RequestContext(auth_token=True) stub_context = context.RequestContext(auth_token=True)
@ -348,7 +315,7 @@ class TestGlanceImageService(base.TestCase):
@mock.patch('ironic.common.keystone.get_adapter', autospec=True) @mock.patch('ironic.common.keystone.get_adapter', autospec=True)
@mock.patch('ironic.common.keystone.get_session', autospec=True, @mock.patch('ironic.common.keystone.get_session', autospec=True,
return_value=mock.sentinel.session) return_value=mock.sentinel.session)
@mock.patch.object(glance_client, 'Client', autospec=True) @mock.patch.object(openstack.connection, 'Connection', autospec=True)
class CheckImageServiceTestCase(base.TestCase): class CheckImageServiceTestCase(base.TestCase):
def setUp(self): def setUp(self):
super(CheckImageServiceTestCase, self).setUp() super(CheckImageServiceTestCase, self).setUp()
@ -385,13 +352,11 @@ class CheckImageServiceTestCase(base.TestCase):
self.assertEqual(0, mock_auth.call_count) self.assertEqual(0, mock_auth.call_count)
self.assertEqual(0, mock_sauth.call_count) self.assertEqual(0, mock_sauth.call_count)
def _assert_client_call(self, mock_gclient, url, user=False): def _assert_connnection_call(self, mock_gclient, url):
mock_gclient.assert_called_once_with( mock_gclient.assert_called_once_with(
2,
session=mock.sentinel.session, session=mock.sentinel.session,
global_request_id='global', image_endpoint_override=url,
auth=mock.sentinel.sauth if user else mock.sentinel.auth, image_api_version='2')
endpoint_override=url)
def test_check_image_service__config_auth(self, mock_gclient, mock_sess, def test_check_image_service__config_auth(self, mock_gclient, mock_sess,
mock_adapter, mock_sauth, mock_adapter, mock_sauth,
@ -406,9 +371,12 @@ class CheckImageServiceTestCase(base.TestCase):
wrapped_func = image_service.check_image_service(func) wrapped_func = image_service.check_image_service(func)
self.assertEqual(((), params), wrapped_func(self.service, **params)) self.assertEqual(((), params), wrapped_func(self.service, **params))
self._assert_client_call(mock_gclient, 'glance_url') self._assert_connnection_call(mock_gclient, 'glance_url')
mock_auth.assert_called_once_with('glance') mock_auth.assert_called_once_with('glance')
mock_sess.assert_called_once_with('glance') mock_sess.assert_has_calls([
mock.call('glance'),
mock.call('glance', auth=mock.sentinel.auth)
])
mock_adapter.assert_called_once_with('glance', mock_adapter.assert_called_once_with('glance',
session=mock.sentinel.session, session=mock.sentinel.session,
auth=mock.sentinel.auth) auth=mock.sentinel.auth)
@ -430,8 +398,11 @@ class CheckImageServiceTestCase(base.TestCase):
wrapped_func = image_service.check_image_service(func) wrapped_func = image_service.check_image_service(func)
self.assertEqual(((), params), wrapped_func(self.service, **params)) self.assertEqual(((), params), wrapped_func(self.service, **params))
self._assert_client_call(mock_gclient, 'glance_url', user=True) self._assert_connnection_call(mock_gclient, 'glance_url')
mock_sess.assert_called_once_with('glance') mock_sess.assert_has_calls([
mock.call('glance'),
mock.call('glance', auth=mock.sentinel.sauth)
])
mock_adapter.assert_called_once_with('glance', mock_adapter.assert_called_once_with('glance',
session=mock.sentinel.session, session=mock.sentinel.session,
auth=mock.sentinel.auth) auth=mock.sentinel.auth)
@ -455,26 +426,17 @@ class CheckImageServiceTestCase(base.TestCase):
wrapped_func = image_service.check_image_service(func) wrapped_func = image_service.check_image_service(func)
self.assertEqual(((), params), wrapped_func(self.service, **params)) self.assertEqual(((), params), wrapped_func(self.service, **params))
self.assertEqual('none', image_service.CONF.glance.auth_type) self.assertEqual('none', image_service.CONF.glance.auth_type)
self._assert_client_call(mock_gclient, 'foo') self._assert_connnection_call(mock_gclient, 'foo')
mock_sess.assert_called_once_with('glance') mock_sess.assert_has_calls([
mock.call('glance'),
mock.call('glance', auth=mock.sentinel.auth)
])
mock_adapter.assert_called_once_with('glance', mock_adapter.assert_called_once_with('glance',
session=mock.sentinel.session, session=mock.sentinel.session,
auth=mock.sentinel.auth) auth=mock.sentinel.auth)
self.assertEqual(0, mock_sauth.call_count) self.assertEqual(0, mock_sauth.call_count)
def _create_failing_glance_client(info):
class MyGlanceStubClient(stubs.StubGlanceClient):
"""A client that fails the first time, then succeeds."""
def get(self, image_id):
info['num_calls'] += 1
if info['num_calls'] == 1:
raise glance_exc.ServiceUnavailable('')
return {}
return MyGlanceStubClient()
class TestGlanceSwiftTempURL(base.TestCase): class TestGlanceSwiftTempURL(base.TestCase):
def setUp(self): def setUp(self):
super(TestGlanceSwiftTempURL, self).setUp() super(TestGlanceSwiftTempURL, self).setUp()

View File

@ -12,43 +12,43 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
from glanceclient import exc as glance_exc import io
from openstack.connection import exceptions as openstack_exc
NOW_GLANCE_FORMAT = "2010-10-11T10:30:22" NOW_GLANCE_FORMAT = "2010-10-11T10:30:22"
class _GlanceWrapper(object):
def __init__(self, wrapped):
self.wrapped = wrapped
def __iter__(self):
return iter(())
class StubGlanceClient(object): class StubGlanceClient(object):
fake_wrapped = object() image_data = b'this is an image'
def __init__(self, images=None): def __init__(self, images=None):
self._images = [] self._images = []
_images = images or [] _images = images or []
map(lambda image: self.create(**image), _images) map(lambda image: self.create(**image), _images)
# NOTE(bcwaldon): HACK to get client.images.* to work def get_image(self, image_id):
self.images = lambda: None
for fn in ('get', 'data'):
setattr(self.images, fn, getattr(self, fn))
def get(self, image_id):
for image in self._images: for image in self._images:
if image.id == str(image_id): if image.id == str(image_id):
return image return image
raise glance_exc.NotFound(image_id) raise openstack_exc.NotFoundException(image_id)
def data(self, image_id): def download_image(self, image_id, stream=False):
self.get(image_id) self.get_image(image_id)
return _GlanceWrapper(self.fake_wrapped) if stream:
return io.BytesIO(self.image_data)
else:
return FakeImageDownload(self.image_data)
class FakeImageDownload(object):
content = None
def __init__(self, content):
self.content = content
class FakeImage(dict): class FakeImage(dict):

View File

@ -0,0 +1,5 @@
---
upgrade:
- |
`python-glanceclient` is no longer a dependency, all OpenStack Glance
operations are now done using `openstacksdk`.

View File

@ -12,7 +12,6 @@ automaton>=1.9.0 # Apache-2.0
eventlet>=0.30.1 # MIT eventlet>=0.30.1 # MIT
WebOb>=1.7.1 # MIT WebOb>=1.7.1 # MIT
python-cinderclient!=4.0.0,>=3.3.0 # Apache-2.0 python-cinderclient!=4.0.0,>=3.3.0 # Apache-2.0
python-glanceclient>=2.8.0 # Apache-2.0
keystoneauth1>=4.2.0 # Apache-2.0 keystoneauth1>=4.2.0 # Apache-2.0
ironic-lib>=6.0.0 # Apache-2.0 ironic-lib>=6.0.0 # Apache-2.0
stevedore>=1.29.0 # Apache-2.0 stevedore>=1.29.0 # Apache-2.0