From 51dd510636c817d2f7b5cb0b3c967fb604de87bb Mon Sep 17 00:00:00 2001 From: junboli Date: Mon, 8 Jan 2018 11:02:11 +0800 Subject: [PATCH] Fix NFS/CIFS share creation failure issue When the image count is over 25, there might not get manila-service-image, because current manila shares creation is using novaclient to get image info, but novaclient can only get 25 images due to pagination of glance server, So this change is to switch to use glanceclient instead of novaclient to get image info, because glanceclient can iter all image info, while novaclient is rarely maintained with stuff of image API. Change-Id: Id905d47600bda9923cebae617749c8286552ec94 Closes-Bug: #1741425 --- manila/image/__init__.py | 33 +++++ manila/image/glance.py | 71 ++++++++++ manila/share/drivers/service_instance.py | 4 +- manila/tests/image/__init__.py | 0 manila/tests/image/test_image.py | 125 ++++++++++++++++++ .../share/drivers/test_service_instance.py | 8 +- ...-to-use-glanceclient-dde019b0b141caf8.yaml | 5 + requirements.txt | 1 + 8 files changed, 242 insertions(+), 5 deletions(-) create mode 100644 manila/image/__init__.py create mode 100644 manila/image/glance.py create mode 100644 manila/tests/image/__init__.py create mode 100644 manila/tests/image/test_image.py create mode 100644 releasenotes/notes/switch-to-use-glanceclient-dde019b0b141caf8.yaml diff --git a/manila/image/__init__.py b/manila/image/__init__.py new file mode 100644 index 0000000000..7e34439193 --- /dev/null +++ b/manila/image/__init__.py @@ -0,0 +1,33 @@ +# 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 oslo_config.cfg +import oslo_utils.importutils + + +_glance_opts = [ + oslo_config.cfg.StrOpt('image_api_class', + default='manila.image.glance.API', + help='The full class name of the ' + 'Glance API class to use.'), +] + +oslo_config.cfg.CONF.register_opts(_glance_opts) + + +def API(): + importutils = oslo_utils.importutils + glance_api_class = oslo_config.cfg.CONF.image_api_class + cls = importutils.import_class(glance_api_class) + return cls() diff --git a/manila/image/glance.py b/manila/image/glance.py new file mode 100644 index 0000000000..db905d6c1d --- /dev/null +++ b/manila/image/glance.py @@ -0,0 +1,71 @@ +# 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. + +""" +Handles all requests to Glance. +""" + +from glanceclient import client as glance_client +from glanceclient import exc as glance_exception +from keystoneauth1 import loading as ks_loading +from oslo_config import cfg + +from manila.common import client_auth +from manila.common.config import core_opts +from manila.db import base + +GLANCE_GROUP = 'glance' +AUTH_OBJ = None + + +glance_opts = [ + cfg.StrOpt('api_microversion', + default='2', + help='Version of Glance API to be used.'), + cfg.StrOpt('region_name', + default='RegionOne', + help='Region name for connecting to glance.'), + ] + +CONF = cfg.CONF +CONF.register_opts(core_opts) +CONF.register_opts(glance_opts, GLANCE_GROUP) +ks_loading.register_session_conf_options(CONF, GLANCE_GROUP) +ks_loading.register_auth_conf_options(CONF, GLANCE_GROUP) + + +def list_opts(): + return client_auth.AuthClientLoader.list_opts(GLANCE_GROUP) + + +def glanceclient(context): + global AUTH_OBJ + if not AUTH_OBJ: + AUTH_OBJ = client_auth.AuthClientLoader( + client_class=glance_client.Client, + exception_module=glance_exception, + cfg_group=GLANCE_GROUP) + return AUTH_OBJ.get_client(context, + version=CONF[GLANCE_GROUP].api_microversion, + region_name=CONF[GLANCE_GROUP].region_name) + + +class API(base.Base): + """API for interacting with glanceclient.""" + + def image_list(self, context): + client = glanceclient(context) + if hasattr(client, 'images'): + return client.images.list() + return client.glance.list() diff --git a/manila/share/drivers/service_instance.py b/manila/share/drivers/service_instance.py index 1cc751bd84..20a588b769 100644 --- a/manila/share/drivers/service_instance.py +++ b/manila/share/drivers/service_instance.py @@ -33,6 +33,7 @@ from manila import compute from manila import context from manila import exception from manila.i18n import _ +from manila import image from manila.network.linux import ip_lib from manila.network.neutron import api as neutron from manila import utils @@ -207,6 +208,7 @@ class ServiceInstanceManager(object): self.admin_context = context.get_admin_context() self._execute = utils.execute + self.image_api = image.API() self.compute_api = compute.API() self.path_to_private_key = self.get_config_option( @@ -482,7 +484,7 @@ class ServiceInstanceManager(object): def _get_service_image(self, context): """Returns ID of service image for service vm creating.""" service_image_name = self.get_config_option("service_image_name") - images = [image.id for image in self.compute_api.image_list(context) + images = [image.id for image in self.image_api.image_list(context) if image.name == service_image_name and image.status == 'active'] if len(images) == 1: diff --git a/manila/tests/image/__init__.py b/manila/tests/image/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/manila/tests/image/test_image.py b/manila/tests/image/test_image.py new file mode 100644 index 0000000000..8fdb2b9fa1 --- /dev/null +++ b/manila/tests/image/test_image.py @@ -0,0 +1,125 @@ +# 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 mock + +from manila import context +from manila.image import glance +from manila import test +from manila.tests import utils as test_utils + + +class FakeGlanceClient(object): + + class Image(object): + + def list(self, *args, **kwargs): + return [{'id': 'id1'}, {'id': 'id2'}] + + def __getattr__(self, item): + return None + + def __init__(self): + self.image = self.Image() + + +def get_fake_auth_obj(): + return type('FakeAuthObj', (object, ), {'get_client': mock.Mock()}) + + +class GlanceClientTestCase(test.TestCase): + + @mock.patch('manila.image.glance.AUTH_OBJ', None) + def test_no_auth_obj(self): + mock_client_loader = self.mock_object( + glance.client_auth, 'AuthClientLoader') + fake_context = 'fake_context' + data = { + 'glance': { + 'api_microversion': 'foo_api_microversion', + 'region_name': 'foo_region_name' + } + } + + with test_utils.create_temp_config_with_opts(data): + glance.glanceclient(fake_context) + + mock_client_loader.return_value.get_client.assert_called_once_with( + fake_context, + version=data['glance']['api_microversion'], + region_name=data['glance']['region_name'] + ) + + @mock.patch('manila.image.glance.AUTH_OBJ', get_fake_auth_obj()) + def test_with_auth_obj(self): + fake_context = 'fake_context' + data = { + 'glance': { + 'api_microversion': 'foo_api_microversion', + 'region_name': 'foo_region_name' + } + } + + with test_utils.create_temp_config_with_opts(data): + glance.glanceclient(fake_context) + + glance.AUTH_OBJ.get_client.assert_called_once_with( + fake_context, + version=data['glance']['api_microversion'], + region_name=data['glance']['region_name'] + ) + + +class GlanceApiTestCase(test.TestCase): + def setUp(self): + super(GlanceApiTestCase, self).setUp() + + self.api = glance.API() + self.glanceclient = FakeGlanceClient() + self.ctx = context.get_admin_context() + self.mock_object(glance, 'glanceclient', + mock.Mock(return_value=self.glanceclient)) + + def test_image_list_glanceclient_has_no_proxy(self): + image_list = ['fake', 'image', 'list'] + + class FakeGlanceClient(object): + def list(self): + return image_list + + self.glanceclient.glance = FakeGlanceClient() + + result = self.api.image_list(self.ctx) + + self.assertEqual(image_list, result) + + def test_image_list_glanceclient_has_proxy(self): + image_list1 = ['fake', 'image', 'list1'] + image_list2 = ['fake', 'image', 'list2'] + + class FakeImagesClient(object): + def list(self): + return image_list1 + + class FakeGlanceClient(object): + def list(self): + return image_list2 + + self.glanceclient.images = FakeImagesClient() + self.glanceclient.glance = FakeGlanceClient() + + result = self.api.image_list(self.ctx) + + self.assertEqual(image_list1, result) diff --git a/manila/tests/share/drivers/test_service_instance.py b/manila/tests/share/drivers/test_service_instance.py index 4af36ebd28..70b5b0004c 100644 --- a/manila/tests/share/drivers/test_service_instance.py +++ b/manila/tests/share/drivers/test_service_instance.py @@ -678,7 +678,7 @@ class ServiceInstanceManagerTestCase(test.TestCase): fake_image3 = fake_compute.FakeImage( name='another-image', status='active') - self.mock_object(self._manager.compute_api, 'image_list', + self.mock_object(self._manager.image_api, 'image_list', mock.Mock(return_value=[fake_image1, fake_image2, fake_image3])) @@ -687,7 +687,7 @@ class ServiceInstanceManagerTestCase(test.TestCase): self.assertEqual(fake_image1.id, result) def test_get_service_image_not_found(self): - self.mock_object(self._manager.compute_api, 'image_list', + self.mock_object(self._manager.image_api, 'image_list', mock.Mock(return_value=[])) self.assertRaises( exception.ServiceInstanceException, @@ -696,7 +696,7 @@ class ServiceInstanceManagerTestCase(test.TestCase): fake_error_image = fake_compute.FakeImage( name='service_image_name', status='error') - self.mock_object(self._manager.compute_api, 'image_list', + self.mock_object(self._manager.image_api, 'image_list', mock.Mock(return_value=[fake_error_image])) self.assertRaises( exception.ServiceInstanceException, @@ -707,7 +707,7 @@ class ServiceInstanceManagerTestCase(test.TestCase): name=fake_get_config_option('service_image_name'), status='active') fake_images = [fake_image, fake_image] - self.mock_object(self._manager.compute_api, 'image_list', + self.mock_object(self._manager.image_api, 'image_list', mock.Mock(return_value=fake_images)) self.assertRaises( exception.ServiceInstanceException, diff --git a/releasenotes/notes/switch-to-use-glanceclient-dde019b0b141caf8.yaml b/releasenotes/notes/switch-to-use-glanceclient-dde019b0b141caf8.yaml new file mode 100644 index 0000000000..76d15170f4 --- /dev/null +++ b/releasenotes/notes/switch-to-use-glanceclient-dde019b0b141caf8.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + Switch to use glance client to retrive image list, novaclient is rarely + maintained with glance API. diff --git a/requirements.txt b/requirements.txt index fbd226494b..f5898cb277 100644 --- a/requirements.txt +++ b/requirements.txt @@ -42,4 +42,5 @@ stevedore>=1.20.0 # Apache-2.0 tooz>=1.58.0 # Apache-2.0 python-cinderclient>=3.3.0 # Apache-2.0 python-novaclient>=9.1.0 # Apache-2.0 +python-glanceclient>=2.8.0 # Apache-2.0 WebOb>=1.7.1 # MIT