diff --git a/glance/tests/functional/v2/test_legacy_update_cinder_store.py b/glance/tests/functional/v2/test_legacy_update_cinder_store.py new file mode 100644 index 0000000000..2d02922c6a --- /dev/null +++ b/glance/tests/functional/v2/test_legacy_update_cinder_store.py @@ -0,0 +1,254 @@ +# Copyright 2020 Red Hat, Inc. +# 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. + +from testtools import content as ttc +import time +from unittest import mock +import uuid + +from cinderclient.v3 import client as cinderclient +import glance_store +from glance_store._drivers import cinder +from oslo_config import cfg +from oslo_log import log as logging +from oslo_serialization import jsonutils + +from glance.common import wsgi +from glance.tests import functional + +LOG = logging.getLogger(__name__) + +CONF = cfg.CONF + + +class FakeObject(object): + def __init__(self, **kwargs): + for name, value in kwargs.items(): + setattr(self, name, value) + + +class TestLegacyUpdateCinderStore(functional.SynchronousAPIBase): + + def setUp(self): + super(TestLegacyUpdateCinderStore, self).setUp() + self.vol_id = uuid.uuid4() + self.cinder_store_mock = FakeObject( + client=mock.MagicMock(), volumes=FakeObject( + get=lambda v_id: FakeObject(volume_type='fast'), + detach=mock.MagicMock(), + create=lambda size_gb, name, metadata, volume_type: + FakeObject( + id=self.vol_id, manager=FakeObject( + get=lambda vol_id: FakeObject( + manager=FakeObject( + get=lambda vol_id: FakeObject( + status='in-use', + begin_detaching=mock.MagicMock(), + terminate_connection=mock.MagicMock())), + id=vol_id, + status='available', + size=1, + reserve=mock.MagicMock(), + initialize_connection=mock.MagicMock(), + encrypted=False, + unreserve=mock.MagicMock(), + delete=mock.MagicMock(), + attach=mock.MagicMock(), + update_all_metadata=mock.MagicMock(), + update_readonly_flag=mock.MagicMock()))))) + + def setup_stores(self): + pass + + def setup_single_store(self): + glance_store.register_opts(CONF) + self.config(show_multiple_locations=True) + self.config(show_image_direct_url=True) + self.config(default_store='cinder', group='glance_store') + self.config(stores=['http', 'swift', 'cinder'], group='glance_store') + self.config(cinder_volume_type='fast', group='glance_store') + self.config(cinder_store_user_name='fake_user', group='glance_store') + self.config(cinder_store_password='fake_pass', group='glance_store') + self.config(cinder_store_project_name='fake_project', + group='glance_store') + self.config(cinder_store_auth_address='http://auth_addr', + group='glance_store') + glance_store.create_stores(CONF) + + def unset_single_store(self): + glance_store.register_opts(CONF) + self.config(show_multiple_locations=True) + self.config(show_image_direct_url=True) + self.config(stores=[], group='glance_store') + self.config(cinder_volume_type='', group='glance_store') + self.config(cinder_store_user_name='', group='glance_store') + self.config(cinder_store_password='', group='glance_store') + self.config(cinder_store_project_name='', group='glance_store') + self.config(cinder_store_auth_address='', group='glance_store') + glance_store.create_stores(CONF) + + @mock.patch.object(cinderclient, 'Client') + def setup_multiple_stores(self, mock_client): + """Configures multiple backend stores. + + This configures the API with two cinder stores (store1 and + store2) as well as a os_glance_staging_store for + imports. + + """ + self.config(show_multiple_locations=True) + self.config(show_image_direct_url=True) + self.config(enabled_backends={'store1': 'cinder', 'store2': 'cinder'}) + glance_store.register_store_opts(CONF, + reserved_stores=wsgi.RESERVED_STORES) + self.config(default_backend='store1', + group='glance_store') + self.config(cinder_volume_type='fast', group='store1') + self.config(cinder_store_user_name='fake_user', group='store1') + self.config(cinder_store_password='fake_pass', group='store1') + self.config(cinder_store_project_name='fake_project', group='store1') + self.config(cinder_store_auth_address='http://auth_addr', + group='store1') + self.config(cinder_volume_type='reliable', group='store2') + self.config(cinder_store_user_name='fake_user', group='store2') + self.config(cinder_store_password='fake_pass', group='store2') + self.config(cinder_store_project_name='fake_project', group='store2') + self.config(cinder_store_auth_address='http://auth_addr', + group='store2') + self.config(filesystem_store_datadir=self._store_dir('staging'), + group='os_glance_staging_store') + glance_store.create_multi_stores(CONF, + reserved_stores=wsgi.RESERVED_STORES) + glance_store.verify_store() + + def _create_and_stage(self, data_iter=None): + resp = self.api_post('/v2/images', + json={'name': 'foo', + 'container_format': 'bare', + 'disk_format': 'raw'}) + image = jsonutils.loads(resp.text) + + if data_iter: + resp = self.api_put( + '/v2/images/%s/stage' % image['id'], + headers={'Content-Type': 'application/octet-stream'}, + body_file=data_iter) + else: + resp = self.api_put( + '/v2/images/%s/stage' % image['id'], + headers={'Content-Type': 'application/octet-stream'}, + data=b'IMAGEDATA') + self.assertEqual(204, resp.status_code) + + return image['id'] + + def _import_direct(self, image_id, stores): + """Do an import of image_id to the given stores.""" + body = {'method': {'name': 'glance-direct'}, + 'stores': stores, + 'all_stores': False} + + return self.api_post( + '/v2/images/%s/import' % image_id, + json=body) + + def _create_and_import(self, stores=[], data_iter=None): + """Create an image, stage data, and import into the given stores. + + :returns: image_id + """ + image_id = self._create_and_stage(data_iter=data_iter) + + resp = self._import_direct(image_id, stores) + self.assertEqual(202, resp.status_code) + + # Make sure it goes active + for i in range(0, 10): + image = self.api_get('/v2/images/%s' % image_id).json + if not image.get('os_glance_import_task'): + break + self.addDetail('Create-Import task id', + ttc.text_content(image['os_glance_import_task'])) + time.sleep(1) + + self.assertEqual('active', image['status']) + + return image_id + + @mock.patch.object(cinderclient, 'Client') + @mock.patch.object(cinder.Store, 'temporary_chown') + @mock.patch.object(cinder, 'connector') + @mock.patch.object(cinder, 'open') + def test_create_image(self, mock_open, mock_connector, + mock_chown, mocked_cc): + # setup multiple cinder stores + self.setup_multiple_stores() + self.start_server() + + mocked_cc.return_value = self.cinder_store_mock + # create an image + image_id = self._create_and_import(stores=['store1']) + image = self.api_get('/v2/images/%s' % image_id).json + # verify image is created with new location url + self.assertEqual('cinder://store1/%s' % self.vol_id, + image['locations'][0]['url']) + self.assertEqual('store1', image['locations'][0]['metadata']['store']) + mocked_cc.assert_called_once() + mock_open.assert_called_once() + mock_chown.assert_called_once() + mock_connector.get_connector_properties.assert_called_once() + + @mock.patch.object(cinderclient, 'Client') + @mock.patch.object(cinder.Store, 'temporary_chown') + @mock.patch.object(cinder, 'connector') + @mock.patch.object(cinder, 'open') + def test_migrate_image_after_upgrade(self, mock_open, mock_connector, + mock_chown, mocked_cc): + """Test to check if an image is successfully migrated when we + + upgrade from a single cinder store to multiple cinder stores. + """ + # setup single cinder store + self.setup_single_store() + self.start_server() + mocked_cc.return_value = self.cinder_store_mock + + # create image in single store + image_id = self._create_and_import(stores=['store1']) + image = self.api_get('/v2/images/%s' % image_id).json + # check the location url is in old format + self.assertEqual('cinder://%s' % self.vol_id, + image['locations'][0]['url']) + self.unset_single_store() + # setup multiple cinder stores + self.setup_multiple_stores() + cinder.keystone_sc = mock.MagicMock() + # get the image to run lazy loading + image = self.api_get('/v2/images/%s' % image_id).json + # verify the image is updated to new format + self.assertEqual('cinder://store1/%s' % self.vol_id, + image['locations'][0]['url']) + self.assertEqual('store1', image['locations'][0]['metadata']['store']) + image = self.api_get('/v2/images/%s' % image_id).json + # verify the image location url is consistent + self.assertEqual('cinder://store1/%s' % self.vol_id, + image['locations'][0]['url']) + mock_open.assert_called_once() + mock_chown.assert_called_once() + mock_connector.get_connector_properties.assert_called_once() + mocked_cc.assert_called() + # first call when creating volume and second call when migrating + # the image (setting up multiple stores) + self.assertEqual(2, mocked_cc.call_count) diff --git a/lower-constraints.txt b/lower-constraints.txt index fd35f7d75a..d64afb488f 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -54,6 +54,7 @@ msgpack==0.5.6 netaddr==0.7.19 netifaces==0.10.6 networkx==2.2 +os-brick==3.1.0 os-client-config==1.29.0 os-win==4.0.1 oslo.concurrency==3.26.0 @@ -65,6 +66,7 @@ oslo.log==3.36.0 oslo.messaging==5.29.0 oslo.middleware==3.31.0 oslo.policy==3.6.0 +oslo.privsep==1.32.0 oslo.reports==1.18.0 oslo.serialization==2.25.0 oslo.service==1.41.1 @@ -91,6 +93,7 @@ pyparsing==2.2.0 pyperclip==1.8.0 pysendfile==2.0.0 python-barbicanclient==4.6.0 +python-cinderclient==4.1.0 python-dateutil==2.7.0 python-editor==1.0.3 python-keystoneclient==3.8.0 @@ -116,7 +119,7 @@ stestr==2.0.0 stevedore==1.20.0 taskflow==4.0.0 Tempita==0.5.2 -tenacity==4.9.0 +tenacity==6.0.0 testrepository==0.0.18 testresources==2.0.0 testscenarios==0.4 diff --git a/test-requirements.txt b/test-requirements.txt index dfce3b9a89..948ae17ff8 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -30,3 +30,6 @@ psycopg2>=2.8.4 # LGPL/ZPL pysendfile>=2.0.0;sys_platform!='win32' # MIT xattr>=0.9.2;sys_platform!='win32' # MIT python-swiftclient>=3.2.0 # Apache-2.0 +python-cinderclient>=4.1.0 # Apache-2.0 +os-brick>=3.1.0 +oslo.privsep>=1.32.0