Add a new import method called glance-download that implements a glance to glance download in a multi-region cloud with a federated Keystone. This method will copy the image data and selected metadata to the target glance, checking that the downloaded size match the "size" image attribute in the source glance. Implements: blueprint glance-download-import Co-Authored-By: Victor Coutellier <victor.coutellier@gmail.com> Change-Id: Ic51c5fd87caf04d38aeaf758ad2d0e2f28098e4dchanges/18/840318/30
parent
653d52e90d
commit
480ea3825f
@ -0,0 +1,131 @@
|
||||
# Copyright 2018 Red Hat, Inc.
|
||||
# Copyright 2022 OVHCloud
|
||||
# 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 abc
|
||||
|
||||
import glance_store as store_api
|
||||
from glance_store import backend
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
import six
|
||||
from taskflow import task
|
||||
|
||||
from glance.common import exception
|
||||
from glance.i18n import _, _LE
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class BaseDownload(task.Task):
|
||||
|
||||
default_provides = 'file_uri'
|
||||
|
||||
def __init__(self, task_id, task_type, action_wrapper, stores,
|
||||
plugin_name):
|
||||
self.task_id = task_id
|
||||
self.task_type = task_type
|
||||
self.image_id = action_wrapper.image_id
|
||||
self.action_wrapper = action_wrapper
|
||||
self.stores = stores
|
||||
self._path = None
|
||||
self.plugin_name = plugin_name or 'Download'
|
||||
super(BaseDownload, self).__init__(
|
||||
name='%s-%s-%s' % (task_type, self.plugin_name, task_id))
|
||||
|
||||
# NOTE(abhishekk): Use reserved 'os_glance_staging_store' for
|
||||
# staging the data, the else part will be removed once old way
|
||||
# of configuring store is deprecated.
|
||||
if CONF.enabled_backends:
|
||||
self.store = store_api.get_store_from_store_identifier(
|
||||
'os_glance_staging_store')
|
||||
else:
|
||||
if CONF.node_staging_uri is None:
|
||||
msg = (_("%(task_id)s of %(task_type)s not configured "
|
||||
"properly. Missing node_staging_uri: %(work_dir)s") %
|
||||
{'task_id': self.task_id,
|
||||
'task_type': self.task_type,
|
||||
'work_dir': CONF.node_staging_uri})
|
||||
raise exception.BadTaskConfiguration(msg)
|
||||
|
||||
self.store = self._build_store()
|
||||
|
||||
def _build_store(self):
|
||||
# NOTE(flaper87): Due to the nice glance_store api (#sarcasm), we're
|
||||
# forced to build our own config object, register the required options
|
||||
# (and by required I mean *ALL* of them, even the ones we don't want),
|
||||
# and create our own store instance by calling a private function.
|
||||
# This is certainly unfortunate but it's the best we can do until the
|
||||
# glance_store refactor is done. A good thing is that glance_store is
|
||||
# under our team's management and it gates on Glance so changes to
|
||||
# this API will (should?) break task's tests.
|
||||
# TODO(abhishekk): After removal of backend module from glance_store
|
||||
# need to change this to use multi_backend module.
|
||||
conf = cfg.ConfigOpts()
|
||||
try:
|
||||
backend.register_opts(conf)
|
||||
except cfg.DuplicateOptError:
|
||||
pass
|
||||
|
||||
conf.set_override('filesystem_store_datadir',
|
||||
CONF.node_staging_uri[7:],
|
||||
group='glance_store')
|
||||
|
||||
# NOTE(flaper87): Do not even try to judge me for this... :(
|
||||
# With the glance_store refactor, this code will change, until
|
||||
# that happens, we don't have a better option and this is the
|
||||
# least worst one, IMHO.
|
||||
store = store_api.backend._load_store(conf, 'file')
|
||||
|
||||
if store is None:
|
||||
msg = (_("%(task_id)s of %(task_type)s not configured "
|
||||
"properly. Could not load the filesystem store") %
|
||||
{'task_id': self.task_id, 'task_type': self.task_type})
|
||||
raise exception.BadTaskConfiguration(msg)
|
||||
|
||||
store.configure()
|
||||
return store
|
||||
|
||||
def revert(self, result, **kwargs):
|
||||
LOG.error(_LE('Task: %(task_id)s failed to import image '
|
||||
'%(image_id)s to the filesystem.'),
|
||||
{'task_id': self.task_id,
|
||||
'image_id': self.image_id})
|
||||
# NOTE(abhishekk): Revert image state back to 'queued' as
|
||||
# something went wrong.
|
||||
# NOTE(danms): If we failed to stage the image, then none
|
||||
# of the _ImportToStore() tasks could have run, so we need
|
||||
# to move all stores out of "importing" and into "failed".
|
||||
with self.action_wrapper as action:
|
||||
action.set_image_attribute(status='queued')
|
||||
action.remove_importing_stores(self.stores)
|
||||
action.add_failed_stores(self.stores)
|
||||
|
||||
# NOTE(abhishekk): Deleting partial image data from staging area
|
||||
if self._path is not None:
|
||||
LOG.debug(('Deleting image %(image_id)s from staging '
|
||||
'area.'), {'image_id': self.image_id})
|
||||
try:
|
||||
if CONF.enabled_backends:
|
||||
store_api.delete(self._path, None)
|
||||
else:
|
||||
store_api.delete_from_backend(self._path)
|
||||
except Exception:
|
||||
LOG.exception(_LE("Error reverting web/glance download "
|
||||
"task: %(task_id)s"), {
|
||||
'task_id': self.task_id})
|
@ -0,0 +1,115 @@
|
||||
# Copyright 2022 OVHCloud
|
||||
# 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 urllib.request
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
from oslo_utils import encodeutils
|
||||
from oslo_utils import excutils
|
||||
from taskflow.patterns import linear_flow as lf
|
||||
|
||||
from glance.async_.flows._internal_plugins import base_download
|
||||
from glance.async_ import utils
|
||||
from glance.common import exception
|
||||
from glance.common import utils as common_utils
|
||||
from glance.i18n import _, _LI, _LE
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
class _DownloadGlanceImage(base_download.BaseDownload):
|
||||
|
||||
def __init__(self, context, task_id, task_type, action_wrapper, stores,
|
||||
glance_region, glance_image_id, glance_service_interface):
|
||||
self.context = context
|
||||
self.glance_region = glance_region
|
||||
self.glance_image_id = glance_image_id
|
||||
self.glance_service_interface = glance_service_interface
|
||||
super(_DownloadGlanceImage,
|
||||
self).__init__(task_id, task_type, action_wrapper, stores,
|
||||
'GlanceDownload')
|
||||
|
||||
def execute(self, image_size):
|
||||
"""Create temp file into store and return path to it
|
||||
|
||||
:param image_size: Glance Image Size retrieved from ImportMetadata task
|
||||
"""
|
||||
try:
|
||||
glance_endpoint = utils.get_glance_endpoint(
|
||||
self.context,
|
||||
self.glance_region,
|
||||
self.glance_service_interface)
|
||||
image_download_url = '%s/v2/images/%s/file' % (
|
||||
glance_endpoint, self.glance_image_id)
|
||||
if not common_utils.validate_import_uri(image_download_url):
|
||||
LOG.debug("Processed URI for glance-download does not pass "
|
||||
"filtering: %s", image_download_url)
|
||||
msg = (_("Processed URI for glance-download does not pass "
|
||||
"filtering: %s") % image_download_url)
|
||||
raise exception.ImportTaskError(msg)
|
||||
LOG.info(_LI("Downloading glance image %s"), image_download_url)
|
||||
token = self.context.auth_token
|
||||
request = urllib.request.Request(image_download_url,
|
||||
headers={'X-Auth-Token': token})
|
||||
data = urllib.request.urlopen(request)
|
||||
except Exception as e:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.error(
|
||||
_LE("Task %(task_id)s failed with exception %(error)s"), {
|
||||
"error": encodeutils.exception_to_unicode(e),
|
||||
"task_id": self.task_id
|
||||
})
|
||||
|
||||
self._path, bytes_written = self.store.add(self.image_id, data, 0)[0:2]
|
||||
if bytes_written != image_size:
|
||||
msg = (_("Task %(task_id)s failed because downloaded data "
|
||||
"size %(data_size)i is different from expected %("
|
||||
"expected)i") %
|
||||
{"task_id": self.task_id, "data_size": bytes_written,
|
||||
"expected": image_size})
|
||||
raise exception.ImportTaskError(msg)
|
||||
return self._path
|
||||
|
||||
|
||||
def get_flow(**kwargs):
|
||||
"""Return task flow for no-op.
|
||||
|
||||
:param context: request context
|
||||
:param task_id: Task ID.
|
||||
:param task_type: Type of the task.
|
||||
:param image_repo: Image repository used.
|
||||
:param image_id: Image ID
|
||||
:param source_region: Source region name
|
||||
"""
|
||||
context = kwargs.get('context')
|
||||
task_id = kwargs.get('task_id')
|
||||
task_type = kwargs.get('task_type')
|
||||
action_wrapper = kwargs.get('action_wrapper')
|
||||
stores = kwargs.get('backend', [None])
|
||||
# glance-download parameters
|
||||
import_req = kwargs.get('import_req')
|
||||
method = import_req.get('method')
|
||||
glance_region = method.get('glance_region')
|
||||
glance_image_id = method.get('glance_image_id')
|
||||
glance_service_interface = method.get('glance_service_interface')
|
||||
|
||||
return lf.Flow(task_type).add(
|
||||
_DownloadGlanceImage(context, task_id, task_type, action_wrapper,
|
||||
stores, glance_region, glance_image_id,
|
||||
glance_service_interface),
|
||||
)
|
@ -0,0 +1,207 @@
|
||||
# Copyright 2022 OVHCloud
|
||||
# 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 os
|
||||
from unittest import mock
|
||||
|
||||
from glance_store import backend
|
||||
from oslo_config import cfg
|
||||
from taskflow.types import failure
|
||||
|
||||
from glance.async_.flows import api_image_import
|
||||
import glance.common.exception
|
||||
from glance import domain
|
||||
import glance.tests.unit.utils as unit_test_utils
|
||||
import glance.tests.utils as test_utils
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
TENANT1 = '6838eb7b-6ded-434a-882c-b344c77fe8df'
|
||||
|
||||
|
||||
class TestBaseDownloadTask(test_utils.BaseTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestBaseDownloadTask, self).setUp()
|
||||
|
||||
self.config(node_staging_uri='/tmp/staging')
|
||||
self.image_repo = mock.MagicMock()
|
||||
self.image_id = mock.MagicMock()
|
||||
self.uri = mock.MagicMock()
|
||||
self.plugin_name = 'FakeBaseDownload'
|
||||
self.task_factory = domain.TaskFactory()
|
||||
|
||||
task_input = {
|
||||
"import_req": {
|
||||
'method': {
|
||||
'name': 'web_download',
|
||||
'uri': 'http://cloud.foo/image.qcow2'
|
||||
}
|
||||
}
|
||||
}
|
||||
task_ttl = CONF.task.task_time_to_live
|
||||
|
||||
self.task_type = 'import'
|
||||
request_id = 'fake_request_id'
|
||||
user_id = 'fake_user'
|
||||
self.task = self.task_factory.new_task(self.task_type, TENANT1,
|
||||
self.image_id, user_id,
|
||||
request_id,
|
||||
task_time_to_live=task_ttl,
|
||||
task_input=task_input)
|
||||
|
||||
self.task_id = self.task.task_id
|
||||
self.action_wrapper = api_image_import.ImportActionWrapper(
|
||||
self.image_repo, self.image_id, self.task_id)
|
||||
self.image_repo.get.return_value = mock.MagicMock(
|
||||
extra_properties={'os_glance_import_task': self.task_id})
|
||||
self.base_download_task = unit_test_utils.FakeBaseDownloadPlugin(
|
||||
self.task.task_id, self.task_type, self.action_wrapper,
|
||||
['foo'], self.plugin_name)
|
||||
self.base_download_task._path = "/path/to_downloaded_data"
|
||||
|
||||
def test_base_download_node_staging_uri_is_none(self):
|
||||
self.config(node_staging_uri=None)
|
||||
self.assertRaises(glance.common.exception.BadTaskConfiguration,
|
||||
unit_test_utils.FakeBaseDownloadPlugin,
|
||||
self.task.task_id, self.task_type, self.uri,
|
||||
self.action_wrapper, ['foo'])
|
||||
|
||||
@mock.patch.object(cfg.ConfigOpts, "set_override")
|
||||
def test_base_download_node_store_initialization_failed(
|
||||
self, mock_override):
|
||||
with mock.patch.object(backend, '_load_store') as mock_load_store:
|
||||
mock_load_store.return_value = None
|
||||
self.assertRaises(glance.common.exception.BadTaskConfiguration,
|
||||
unit_test_utils.FakeBaseDownloadPlugin,
|
||||
self.task.task_id, self.task_type, self.uri,
|
||||
self.action_wrapper, ['foo'])
|
||||
mock_override.assert_called()
|
||||
|
||||
def test_base_download_delete_staging_image_not_exist(self):
|
||||
staging_path = "file:///tmp/staging/temp-image"
|
||||
delete_from_fs_task = api_image_import._DeleteFromFS(
|
||||
self.task.task_id, self.task_type)
|
||||
with mock.patch.object(os.path, "exists") as mock_exists:
|
||||
mock_exists.return_value = False
|
||||
with mock.patch.object(os, "unlink") as mock_unlik:
|
||||
delete_from_fs_task.execute(staging_path)
|
||||
|
||||
self.assertEqual(1, mock_exists.call_count)
|
||||
self.assertEqual(0, mock_unlik.call_count)
|
||||
|
||||
@mock.patch.object(os.path, "exists")
|
||||
def test_base_download_delete_staging_image_failed(self, mock_exists):
|
||||
mock_exists.return_value = True
|
||||
staging_path = "file:///tmp/staging/temp-image"
|
||||
delete_from_fs_task = api_image_import._DeleteFromFS(
|
||||
self.task.task_id, self.task_type)
|
||||
with mock.patch.object(os, "unlink") as mock_unlink:
|
||||
try:
|
||||
delete_from_fs_task.execute(staging_path)
|
||||
except OSError:
|
||||
self.assertEqual(1, mock_unlink.call_count)
|
||||
|
||||
self.assertEqual(1, mock_exists.call_count)
|
||||
|
||||
@mock.patch.object(os.path, "exists")
|
||||
def test_base_download_delete_staging_image_succeed(self, mock_exists):
|
||||
mock_exists.return_value = True
|
||||
staging_path = "file:///tmp/staging/temp-image"
|
||||
delete_from_fs_task = api_image_import._DeleteFromFS(
|
||||
self.task.task_id, self.task_type)
|
||||
with mock.patch.object(os, "unlink") as mock_unlik:
|
||||
delete_from_fs_task.execute(staging_path)
|
||||
self.assertEqual(1, mock_exists.call_count)
|
||||
self.assertEqual(1, mock_unlik.call_count)
|
||||
|
||||
@mock.patch(
|
||||
"glance.async_.flows._internal_plugins.base_download.store_api")
|
||||
def test_base_download_revert_with_failure(self, mock_store_api):
|
||||
image = self.image_repo.get.return_value
|
||||
image.extra_properties['os_glance_importing_to_stores'] = 'foo'
|
||||
image.extra_properties['os_glance_failed_import'] = ''
|
||||
|
||||
self.base_download_task.execute = mock.MagicMock(
|
||||
side_effect=glance.common.exception.ImportTaskError)
|
||||
self.base_download_task.revert(None)
|
||||
mock_store_api.delete_from_backend.assert_called_once_with(
|
||||
"/path/to_downloaded_data")
|
||||
self.assertEqual(1, self.image_repo.save.call_count)
|
||||
self.assertEqual(
|
||||
'', image.extra_properties['os_glance_importing_to_stores'])
|
||||
self.assertEqual(
|
||||
'foo', image.extra_properties['os_glance_failed_import'])
|
||||
|
||||
@mock.patch(
|
||||
"glance.async_.flows._internal_plugins.base_download.store_api")
|
||||
def test_base_download_revert_without_failure_multi_store(self,
|
||||
mock_store_api):
|
||||
enabled_backends = {
|
||||
'fast': 'file',
|
||||
'cheap': 'file'
|
||||
}
|
||||
self.config(enabled_backends=enabled_backends)
|
||||
|
||||
self.base_download_task.revert("/path/to_downloaded_data")
|
||||
mock_store_api.delete.assert_called_once_with(
|
||||
"/path/to_downloaded_data", None)
|
||||
|
||||
@mock.patch(
|
||||
"glance.async_.flows._internal_plugins.base_download.store_api")
|
||||
def test_base_download_revert_with_failure_without_path(self,
|
||||
mock_store_api):
|
||||
image = self.image_repo.get.return_value
|
||||
image.status = 'importing'
|
||||
image.extra_properties['os_glance_importing_to_stores'] = 'foo'
|
||||
image.extra_properties['os_glance_failed_import'] = ''
|
||||
result = failure.Failure.from_exception(
|
||||
glance.common.exception.ImportTaskError())
|
||||
|
||||
self.base_download_task._path = None
|
||||
self.base_download_task.revert(result)
|
||||
mock_store_api.delete_from_backend.assert_not_called()
|
||||
|
||||
# NOTE(danms): Since we told revert that we were the problem,
|
||||
# we should have updated the image status and moved the stores
|
||||
# to the failed list.
|
||||
self.image_repo.save.assert_called_once_with(image, 'importing')
|
||||
self.assertEqual('queued', image.status)
|
||||
self.assertEqual(
|
||||
'', image.extra_properties['os_glance_importing_to_stores'])
|
||||
self.assertEqual(
|
||||
'foo', image.extra_properties['os_glance_failed_import'])
|
||||
|
||||
@mock.patch(
|
||||
"glance.async_.flows._internal_plugins.base_download.store_api")
|
||||
def test_base_download_revert_with_failure_with_path(self, mock_store_api):
|
||||
result = failure.Failure.from_exception(
|
||||
glance.common.exception.ImportTaskError())
|
||||
|
||||
self.base_download_task.revert(result)
|
||||
mock_store_api.delete_from_backend.assert_called_once_with(
|
||||
"/path/to_downloaded_data")
|
||||
|
||||
@mock.patch(
|
||||
"glance.async_.flows._internal_plugins.base_download.store_api")
|
||||
def test_base_download_delete_fails_on_revert(self, mock_store_api):
|
||||
result = failure.Failure.from_exception(
|
||||
glance.common.exception.ImportTaskError())
|
||||
mock_store_api.delete_from_backend.side_effect = Exception
|
||||
|
||||
# this will verify that revert does not break because of failure
|
||||
# while deleting data in staging area
|
||||
self.base_download_task.revert(result)
|
@ -0,0 +1,167 @@
|
||||
# Copyright 2022 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 unittest import mock
|
||||
import urllib.error
|
||||
|
||||
from glance_store._drivers import filesystem
|
||||
from oslo_config import cfg
|
||||
from oslo_utils.fixture import uuidsentinel
|
||||
|
||||
from glance.async_.flows._internal_plugins import glance_download
|
||||
from glance.async_.flows import api_image_import
|
||||
import glance.common.exception
|
||||
import glance.context
|
||||
from glance import domain
|
||||
import glance.tests.utils as test_utils
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
TENANT1 = '6838eb7b-6ded-434a-882c-b344c77fe8df'
|
||||
|
||||
|
||||
class TestGlanceDownloadTask(test_utils.BaseTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestGlanceDownloadTask, self).setUp()
|
||||
|
||||
self.config(node_staging_uri='/tmp/staging')
|
||||
self.image_repo = mock.MagicMock()
|
||||
self.image_id = mock.MagicMock()
|
||||
self.uri = mock.MagicMock()
|
||||
self.task_factory = domain.TaskFactory()
|
||||
self.context = glance.context.RequestContext(tenant=TENANT1,
|
||||
auth_token='token')
|
||||
task_input = {
|
||||
"import_req": {
|
||||
'method': {
|
||||
'name': 'glance-download',
|
||||
'glance_image_id': uuidsentinel.remote_image,
|
||||
'glance_region': 'RegionTwo',
|
||||
'glance_service_interface': 'public',
|
||||
}
|
||||
}
|
||||
}
|
||||
task_ttl = CONF.task.task_time_to_live
|
||||
|
||||
self.task_type = 'import'
|
||||
request_id = 'fake_request_id'
|
||||
user_id = 'fake_user'
|
||||
self.task = self.task_factory.new_task(self.task_type, TENANT1,
|
||||
self.image_id, user_id,
|
||||
request_id,
|
||||
task_time_to_live=task_ttl,
|
||||
task_input=task_input)
|
||||
|
||||
self.task_id = self.task.task_id
|
||||
self.action_wrapper = api_image_import.ImportActionWrapper(
|
||||
self.image_repo, self.image_id, self.task_id)
|
||||
self.image_repo.get.return_value = mock.MagicMock(
|
||||
extra_properties={'os_glance_import_task': self.task_id})
|
||||
|
||||
@mock.patch.object(filesystem.Store, 'add')
|
||||
@mock.patch('glance.async_.utils.get_glance_endpoint')
|
||||
def test_glance_download(self, mock_gge, mock_add):
|
||||
mock_gge.return_value = 'https://other.cloud.foo/image'
|
||||
glance_download_task = glance_download._DownloadGlanceImage(
|
||||
self.context, self.task.task_id, self.task_type,
|
||||
self.action_wrapper, ['foo'],
|
||||
'RegionTwo', uuidsentinel.remote_image, 'public')
|
||||
with mock.patch('urllib.request') as mock_request:
|
||||
mock_add.return_value = ["path", 12345]
|
||||
self.assertEqual(glance_download_task.execute(12345), "path")
|
||||
mock_add.assert_called_once_with(
|
||||
self.image_id,
|
||||
mock_request.urlopen.return_value, 0)
|
||||
mock_request.Request.assert_called_once_with(
|
||||
'https://other.cloud.foo/image/v2/images/%s/file' % (
|
||||
uuidsentinel.remote_image),
|
||||
headers={'X-Auth-Token': self.context.auth_token})
|
||||
mock_gge.assert_called_once_with(self.context, 'RegionTwo', 'public')
|
||||
|
||||
@mock.patch.object(filesystem.Store, 'add')
|
||||
@mock.patch('glance.async_.utils.get_glance_endpoint')
|
||||
def test_glance_download_failed(self, mock_gge, mock_add):
|
||||
mock_gge.return_value = 'https://other.cloud.foo/image'
|
||||
glance_download_task = glance_download._DownloadGlanceImage(
|
||||
self.context, self.task.task_id, self.task_type,
|
||||
self.action_wrapper, ['foo'],
|
||||
'RegionTwo', uuidsentinel.remote_image, 'public')
|
||||
with mock.patch('urllib.request') as mock_request:
|
||||
mock_request.urlopen.side_effect = urllib.error.HTTPError(
|
||||
'/file', 400, 'Test Fail', {}, None)
|
||||
self.assertRaises(urllib.error.HTTPError,
|
||||
glance_download_task.execute,
|
||||
12345)
|
||||
mock_add.assert_not_called()
|
||||
mock_request.Request.assert_called_once_with(
|
||||
'https://other.cloud.foo/image/v2/images/%s/file' % (
|
||||
uuidsentinel.remote_image),
|
||||
headers={'X-Auth-Token': self.context.auth_token})
|
||||
mock_gge.assert_called_once_with(self.context, 'RegionTwo', 'public')
|
||||
|
||||
@mock.patch('urllib.request')
|
||||
@mock.patch('glance.async_.utils.get_glance_endpoint')
|
||||
def test_glance_download_no_glance_endpoint(self, mock_gge, mock_request):
|
||||
mock_gge.side_effect = glance.common.exception.GlanceEndpointNotFound(
|
||||
region='RegionTwo',
|
||||
interface='public')
|
||||
glance_download_task = glance_download._DownloadGlanceImage(
|
||||
self.context, self.task.task_id, self.task_type,
|
||||
self.action_wrapper, ['foo'],
|
||||
'RegionTwo', uuidsentinel.remote_image, 'public')
|
||||
self.assertRaises(glance.common.exception.GlanceEndpointNotFound,
|
||||
glance_download_task.execute, 12345)
|
||||
mock_request.assert_not_called()
|
||||
|
||||
@mock.patch.object(filesystem.Store, 'add')
|
||||
@mock.patch('glance.async_.utils.get_glance_endpoint')
|
||||
def test_glance_download_size_mismatch(self, mock_gge, mock_add):
|
||||
mock_gge.return_value = 'https://other.cloud.foo/image'
|
||||
glance_download_task = glance_download._DownloadGlanceImage(
|
||||
self.context, self.task.task_id, self.task_type,
|
||||
self.action_wrapper, ['foo'],
|
||||
'RegionTwo', uuidsentinel.remote_image, 'public')
|
||||
with mock.patch('urllib.request') as mock_request:
|
||||
mock_add.return_value = ["path", 1]
|
||||
self.assertRaises(glance.common.exception.ImportTaskError,
|
||||
glance_download_task.execute, 12345)
|
||||
mock_add.assert_called_once_with(
|
||||
self.image_id,
|
||||
mock_request.urlopen.return_value, 0)
|
||||
mock_request.Request.assert_called_once_with(
|
||||
'https://other.cloud.foo/image/v2/images/%s/file' % (
|
||||
uuidsentinel.remote_image),
|
||||
headers={'X-Auth-Token': self.context.auth_token})
|
||||
mock_gge.assert_called_once_with(self.context, 'RegionTwo', 'public')
|
||||
|
||||
@mock.patch('urllib.request')
|
||||
@mock.patch('glance.common.utils.validate_import_uri')
|
||||
@mock.patch('glance.async_.utils.get_glance_endpoint')
|
||||
def test_glance_download_wrong_download_url(self, mock_gge, mock_validate,
|
||||
mock_request):
|
||||
mock_validate.return_value = False
|
||||
mock_gge.return_value = 'https://other.cloud.foo/image'
|
||||
glance_download_task = glance_download._DownloadGlanceImage(
|
||||
self.context, self.task.task_id, self.task_type,
|
||||
self.action_wrapper, ['foo'],
|
||||
'RegionTwo', uuidsentinel.remote_image, 'public')
|
||||
self.assertRaises(glance.common.exception.ImportTaskError,
|
||||
glance_download_task.execute, 12345)
|
||||
mock_request.assert_not_called()
|
||||
mock_validate.assert_called_once_with(
|
||||
'https://other.cloud.foo/image/v2/images/%s/file' % (
|
||||
uuidsentinel.remote_image))
|