335 lines
16 KiB
Python
335 lines
16 KiB
Python
# Copyright 2013 OpenStack Foundation
|
|
# 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 random
|
|
import time
|
|
|
|
import mock
|
|
from os_xenapi.client import exception as xenapi_exception
|
|
from os_xenapi.client import host_glance
|
|
from os_xenapi.client import XenAPI
|
|
|
|
from nova.compute import utils as compute_utils
|
|
from nova import context
|
|
from nova import exception
|
|
from nova.image import glance as common_glance
|
|
from nova.tests.unit.virt.xenapi import stubs
|
|
from nova import utils
|
|
from nova.virt.xenapi import driver as xenapi_conn
|
|
from nova.virt.xenapi import fake
|
|
from nova.virt.xenapi.image import glance
|
|
from nova.virt.xenapi import vm_utils
|
|
|
|
|
|
class TestGlanceStore(stubs.XenAPITestBaseNoDB):
|
|
def setUp(self):
|
|
super(TestGlanceStore, self).setUp()
|
|
self.store = glance.GlanceStore()
|
|
|
|
self.flags(api_servers=['http://localhost:9292'], group='glance')
|
|
self.flags(connection_url='http://localhost',
|
|
connection_password='test_pass',
|
|
group='xenserver')
|
|
|
|
self.context = context.RequestContext(
|
|
'user', 'project', auth_token='foobar')
|
|
|
|
fake.reset()
|
|
stubs.stubout_session(self, fake.SessionBase)
|
|
driver = xenapi_conn.XenAPIDriver(False)
|
|
self.session = driver._session
|
|
|
|
self.stub_out('nova.virt.xenapi.vm_utils.get_sr_path',
|
|
lambda *a, **kw: '/fake/sr/path')
|
|
|
|
self.instance = {'uuid': 'blah',
|
|
'system_metadata': [],
|
|
'auto_disk_config': True,
|
|
'os_type': 'default',
|
|
'xenapi_use_agent': 'true'}
|
|
|
|
def _get_params(self):
|
|
return {'image_id': 'fake_image_uuid',
|
|
'endpoint': 'http://localhost:9292',
|
|
'sr_path': '/fake/sr/path',
|
|
'api_version': 2,
|
|
'extra_headers': {'X-Auth-Token': 'foobar',
|
|
'X-Roles': '',
|
|
'X-Tenant-Id': 'project',
|
|
'X-User-Id': 'user',
|
|
'X-Identity-Status': 'Confirmed'}}
|
|
|
|
def _get_download_params(self):
|
|
params = self._get_params()
|
|
params['uuid_stack'] = ['uuid1']
|
|
return params
|
|
|
|
@mock.patch.object(vm_utils, '_make_uuid_stack', return_value=['uuid1'])
|
|
def test_download_image(self, mock_make_uuid_stack):
|
|
params = self._get_download_params()
|
|
with mock.patch.object(self.session, 'call_plugin_serialized'
|
|
) as mock_call_plugin:
|
|
self.store.download_image(self.context, self.session,
|
|
self.instance, 'fake_image_uuid')
|
|
|
|
mock_call_plugin.assert_called_once_with('glance.py',
|
|
'download_vhd2',
|
|
**params)
|
|
mock_make_uuid_stack.assert_called_once_with()
|
|
|
|
@mock.patch.object(vm_utils, '_make_uuid_stack', return_value=['uuid1'])
|
|
@mock.patch.object(random, 'shuffle')
|
|
@mock.patch.object(time, 'sleep')
|
|
@mock.patch.object(compute_utils, 'add_instance_fault_from_exc')
|
|
def test_download_image_retry(self, mock_fault, mock_sleep,
|
|
mock_shuffle, mock_make_uuid_stack):
|
|
params = self._get_download_params()
|
|
self.flags(num_retries=2, group='glance')
|
|
|
|
params.pop("endpoint")
|
|
calls = [mock.call('glance.py', 'download_vhd2',
|
|
endpoint='http://10.0.1.1:9292',
|
|
**params),
|
|
mock.call('glance.py', 'download_vhd2',
|
|
endpoint='http://10.0.0.1:9293',
|
|
**params)]
|
|
|
|
glance_api_servers = ['http://10.0.1.1:9292',
|
|
'http://10.0.0.1:9293']
|
|
self.flags(api_servers=glance_api_servers, group='glance')
|
|
|
|
with (mock.patch.object(self.session, 'call_plugin_serialized')
|
|
) as mock_call_plugin_serialized:
|
|
error_details = ["", "", "RetryableError", ""]
|
|
error = self.session.XenAPI.Failure(details=error_details)
|
|
mock_call_plugin_serialized.side_effect = [error, "success"]
|
|
|
|
self.store.download_image(self.context, self.session,
|
|
self.instance, 'fake_image_uuid')
|
|
|
|
mock_call_plugin_serialized.assert_has_calls(calls)
|
|
|
|
self.assertEqual(1, mock_fault.call_count)
|
|
|
|
def _get_upload_params(self, auto_disk_config=True,
|
|
expected_os_type='default'):
|
|
params = {}
|
|
params['vdi_uuids'] = ['fake_vdi_uuid']
|
|
params['properties'] = {'auto_disk_config': auto_disk_config,
|
|
'os_type': expected_os_type}
|
|
return params
|
|
|
|
@mock.patch.object(utils, 'get_auto_disk_config_from_instance')
|
|
@mock.patch.object(common_glance, 'generate_identity_headers')
|
|
@mock.patch.object(vm_utils, 'get_sr_path')
|
|
@mock.patch.object(host_glance, 'upload_vhd')
|
|
def test_upload_image(self, mock_upload, mock_sr_path, mock_extra_header,
|
|
mock_disk_config):
|
|
params = self._get_upload_params()
|
|
mock_upload.return_value = 'fake_upload'
|
|
mock_sr_path.return_value = 'fake_sr_path'
|
|
mock_extra_header.return_value = 'fake_extra_header'
|
|
mock_disk_config.return_value = 'true'
|
|
self.store.upload_image(self.context, self.session, self.instance,
|
|
'fake_image_uuid', ['fake_vdi_uuid'])
|
|
|
|
mock_sr_path.assert_called_once_with(self.session)
|
|
mock_extra_header.assert_called_once_with(self.context)
|
|
mock_upload.assert_called_once_with(
|
|
self.session, 3, mock.ANY, mock.ANY, 'fake_image_uuid',
|
|
'fake_sr_path', 'fake_extra_header', **params)
|
|
|
|
@mock.patch.object(utils, 'get_auto_disk_config_from_instance')
|
|
@mock.patch.object(common_glance, 'generate_identity_headers')
|
|
@mock.patch.object(vm_utils, 'get_sr_path')
|
|
@mock.patch.object(host_glance, 'upload_vhd')
|
|
def test_upload_image_None_os_type(self, mock_upload, mock_sr_path,
|
|
mock_extra_header, mock_disk_config):
|
|
self.instance['os_type'] = None
|
|
mock_sr_path.return_value = 'fake_sr_path'
|
|
mock_extra_header.return_value = 'fake_extra_header'
|
|
mock_upload.return_value = 'fake_upload'
|
|
mock_disk_config.return_value = 'true'
|
|
params = self._get_upload_params(True, 'linux')
|
|
self.store.upload_image(self.context, self.session, self.instance,
|
|
'fake_image_uuid', ['fake_vdi_uuid'])
|
|
|
|
mock_sr_path.assert_called_once_with(self.session)
|
|
mock_extra_header.assert_called_once_with(self.context)
|
|
mock_upload.assert_called_once_with(
|
|
self.session, 3, mock.ANY, mock.ANY, 'fake_image_uuid',
|
|
'fake_sr_path', 'fake_extra_header', **params)
|
|
mock_disk_config.assert_called_once_with(self.instance)
|
|
|
|
@mock.patch.object(utils, 'get_auto_disk_config_from_instance')
|
|
@mock.patch.object(common_glance, 'generate_identity_headers')
|
|
@mock.patch.object(vm_utils, 'get_sr_path')
|
|
@mock.patch.object(host_glance, 'upload_vhd')
|
|
def test_upload_image_no_os_type(self, mock_upload, mock_sr_path,
|
|
mock_extra_header, mock_disk_config):
|
|
mock_sr_path.return_value = 'fake_sr_path'
|
|
mock_extra_header.return_value = 'fake_extra_header'
|
|
mock_upload.return_value = 'fake_upload'
|
|
del self.instance['os_type']
|
|
params = self._get_upload_params(True, 'linux')
|
|
self.store.upload_image(self.context, self.session, self.instance,
|
|
'fake_image_uuid', ['fake_vdi_uuid'])
|
|
|
|
mock_sr_path.assert_called_once_with(self.session)
|
|
mock_extra_header.assert_called_once_with(self.context)
|
|
mock_upload.assert_called_once_with(
|
|
self.session, 3, mock.ANY, mock.ANY, 'fake_image_uuid',
|
|
'fake_sr_path', 'fake_extra_header', **params)
|
|
mock_disk_config.assert_called_once_with(self.instance)
|
|
|
|
@mock.patch.object(common_glance, 'generate_identity_headers')
|
|
@mock.patch.object(vm_utils, 'get_sr_path')
|
|
@mock.patch.object(host_glance, 'upload_vhd')
|
|
def test_upload_image_auto_config_disk_disabled(
|
|
self, mock_upload, mock_sr_path, mock_extra_header):
|
|
mock_sr_path.return_value = 'fake_sr_path'
|
|
mock_extra_header.return_value = 'fake_extra_header'
|
|
mock_upload.return_value = 'fake_upload'
|
|
sys_meta = [{"key": "image_auto_disk_config", "value": "Disabled"}]
|
|
self.instance["system_metadata"] = sys_meta
|
|
params = self._get_upload_params("disabled")
|
|
self.store.upload_image(self.context, self.session, self.instance,
|
|
'fake_image_uuid', ['fake_vdi_uuid'])
|
|
|
|
mock_sr_path.assert_called_once_with(self.session)
|
|
mock_extra_header.assert_called_once_with(self.context)
|
|
mock_upload.assert_called_once_with(
|
|
self.session, 3, mock.ANY, mock.ANY, 'fake_image_uuid',
|
|
'fake_sr_path', 'fake_extra_header', **params)
|
|
|
|
@mock.patch.object(common_glance, 'generate_identity_headers')
|
|
@mock.patch.object(vm_utils, 'get_sr_path')
|
|
@mock.patch.object(host_glance, 'upload_vhd')
|
|
def test_upload_image_raises_exception(self, mock_upload, mock_sr_path,
|
|
mock_extra_header):
|
|
|
|
mock_sr_path.return_value = 'fake_sr_path'
|
|
mock_extra_header.return_value = 'fake_extra_header'
|
|
mock_upload.side_effect = RuntimeError
|
|
params = self._get_upload_params()
|
|
self.assertRaises(RuntimeError, self.store.upload_image,
|
|
self.context, self.session, self.instance,
|
|
'fake_image_uuid', ['fake_vdi_uuid'])
|
|
|
|
mock_sr_path.assert_called_once_with(self.session)
|
|
mock_extra_header.assert_called_once_with(self.context)
|
|
mock_upload.assert_called_once_with(
|
|
self.session, 3, mock.ANY, mock.ANY, 'fake_image_uuid',
|
|
'fake_sr_path', 'fake_extra_header', **params)
|
|
|
|
@mock.patch.object(time, 'sleep')
|
|
@mock.patch.object(compute_utils, 'add_instance_fault_from_exc')
|
|
def test_upload_image_retries_then_raises_exception(self,
|
|
mock_add_inst,
|
|
mock_time_sleep):
|
|
self.flags(num_retries=2, group='glance')
|
|
params = self._get_params()
|
|
params.update(self._get_upload_params())
|
|
|
|
error_details = ["", "", "RetryableError", ""]
|
|
error = XenAPI.Failure(details=error_details)
|
|
|
|
with mock.patch.object(self.session, 'call_plugin_serialized',
|
|
side_effect=error) as mock_call_plugin:
|
|
self.assertRaises(exception.CouldNotUploadImage,
|
|
self.store.upload_image,
|
|
self.context, self.session, self.instance,
|
|
'fake_image_uuid', ['fake_vdi_uuid'])
|
|
|
|
time_sleep_args = [mock.call(0.5), mock.call(1)]
|
|
call_plugin_args = [
|
|
mock.call('glance.py', 'upload_vhd2', **params),
|
|
mock.call('glance.py', 'upload_vhd2', **params),
|
|
mock.call('glance.py', 'upload_vhd2', **params)]
|
|
add_inst_args = [
|
|
mock.call(self.context, self.instance, error,
|
|
(XenAPI.Failure, error, mock.ANY)),
|
|
mock.call(self.context, self.instance, error,
|
|
(XenAPI.Failure, error, mock.ANY)),
|
|
mock.call(self.context, self.instance, error,
|
|
(XenAPI.Failure, error, mock.ANY))]
|
|
mock_time_sleep.assert_has_calls(time_sleep_args)
|
|
mock_call_plugin.assert_has_calls(call_plugin_args)
|
|
mock_add_inst.assert_has_calls(add_inst_args)
|
|
|
|
@mock.patch.object(time, 'sleep')
|
|
@mock.patch.object(compute_utils, 'add_instance_fault_from_exc')
|
|
def test_upload_image_retries_on_signal_exception(self,
|
|
mock_add_inst,
|
|
mock_time_sleep):
|
|
self.flags(num_retries=2, group='glance')
|
|
params = self._get_params()
|
|
params.update(self._get_upload_params())
|
|
|
|
error_details = ["", "task signaled", "", ""]
|
|
error = XenAPI.Failure(details=error_details)
|
|
|
|
# Note(johngarbutt) XenServer 6.1 and later has this error
|
|
error_details_v61 = ["", "signal: SIGTERM", "", ""]
|
|
error_v61 = self.session.XenAPI.Failure(details=error_details_v61)
|
|
|
|
with mock.patch.object(self.session, 'call_plugin_serialized',
|
|
side_effect=[error, error_v61, None]
|
|
) as mock_call_plugin:
|
|
self.store.upload_image(self.context, self.session, self.instance,
|
|
'fake_image_uuid', ['fake_vdi_uuid'])
|
|
|
|
time_sleep_args = [mock.call(0.5), mock.call(1)]
|
|
call_plugin_args = [
|
|
mock.call('glance.py', 'upload_vhd2', **params),
|
|
mock.call('glance.py', 'upload_vhd2', **params),
|
|
mock.call('glance.py', 'upload_vhd2', **params)]
|
|
add_inst_args = [
|
|
mock.call(self.context, self.instance, error,
|
|
(XenAPI.Failure, error, mock.ANY)),
|
|
mock.call(self.context, self.instance, error_v61,
|
|
(XenAPI.Failure, error_v61, mock.ANY))]
|
|
mock_time_sleep.assert_has_calls(time_sleep_args)
|
|
mock_call_plugin.assert_has_calls(call_plugin_args)
|
|
mock_add_inst.assert_has_calls(add_inst_args)
|
|
|
|
@mock.patch.object(utils, 'get_auto_disk_config_from_instance')
|
|
@mock.patch.object(common_glance, 'generate_identity_headers')
|
|
@mock.patch.object(vm_utils, 'get_sr_path')
|
|
@mock.patch.object(host_glance, 'upload_vhd')
|
|
def test_upload_image_raises_exception_image_not_found(self,
|
|
mock_upload,
|
|
mock_sr_path,
|
|
mock_extra_header,
|
|
mock_disk_config):
|
|
params = self._get_upload_params()
|
|
mock_upload.return_value = 'fake_upload'
|
|
mock_sr_path.return_value = 'fake_sr_path'
|
|
mock_extra_header.return_value = 'fake_extra_header'
|
|
mock_disk_config.return_value = 'true'
|
|
image_id = 'fake_image_id'
|
|
mock_upload.side_effect = xenapi_exception.PluginImageNotFound(
|
|
image_id=image_id
|
|
)
|
|
self.assertRaises(exception.ImageNotFound, self.store.upload_image,
|
|
self.context, self.session, self.instance,
|
|
'fake_image_uuid', ['fake_vdi_uuid'])
|
|
|
|
mock_sr_path.assert_called_once_with(self.session)
|
|
mock_extra_header.assert_called_once_with(self.context)
|
|
mock_upload.assert_called_once_with(
|
|
self.session, 3, mock.ANY, mock.ANY, 'fake_image_uuid',
|
|
'fake_sr_path', 'fake_extra_header', **params)
|