7442 lines
355 KiB
Python
7442 lines
355 KiB
Python
#
|
|
# 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.
|
|
|
|
"""Unit tests for compute API."""
|
|
|
|
import contextlib
|
|
import datetime
|
|
|
|
import ddt
|
|
import fixtures
|
|
import iso8601
|
|
import mock
|
|
import os_traits as ot
|
|
from oslo_messaging import exceptions as oslo_exceptions
|
|
from oslo_serialization import jsonutils
|
|
from oslo_utils import fixture as utils_fixture
|
|
from oslo_utils.fixture import uuidsentinel as uuids
|
|
from oslo_utils import timeutils
|
|
from oslo_utils import uuidutils
|
|
import six
|
|
|
|
from nova.compute import api as compute_api
|
|
from nova.compute import flavors
|
|
from nova.compute import instance_actions
|
|
from nova.compute import power_state
|
|
from nova.compute import rpcapi as compute_rpcapi
|
|
from nova.compute import task_states
|
|
from nova.compute import utils as compute_utils
|
|
from nova.compute import vm_states
|
|
from nova import conductor
|
|
import nova.conf
|
|
from nova import context
|
|
from nova.db import api as db
|
|
from nova import exception
|
|
from nova.image import glance as image_api
|
|
from nova.network import constants
|
|
from nova.network import model
|
|
from nova.network import neutron as neutron_api
|
|
from nova import objects
|
|
from nova.objects import base as obj_base
|
|
from nova.objects import block_device as block_device_obj
|
|
from nova.objects import fields as fields_obj
|
|
from nova.objects import quotas as quotas_obj
|
|
from nova.objects import security_group as secgroup_obj
|
|
from nova.servicegroup import api as servicegroup_api
|
|
from nova import test
|
|
from nova.tests import fixtures as nova_fixtures
|
|
from nova.tests.unit import fake_block_device
|
|
from nova.tests.unit import fake_build_request
|
|
from nova.tests.unit import fake_instance
|
|
from nova.tests.unit import fake_request_spec
|
|
from nova.tests.unit import fake_volume
|
|
from nova.tests.unit.image import fake as fake_image
|
|
from nova.tests.unit import matchers
|
|
from nova.tests.unit.objects import test_flavor
|
|
from nova.tests.unit.objects import test_migration
|
|
from nova import utils
|
|
from nova.volume import cinder
|
|
|
|
|
|
CONF = nova.conf.CONF
|
|
|
|
FAKE_IMAGE_REF = 'fake-image-ref'
|
|
NODENAME = 'fakenode1'
|
|
SHELVED_IMAGE = 'fake-shelved-image'
|
|
SHELVED_IMAGE_NOT_FOUND = 'fake-shelved-image-notfound'
|
|
SHELVED_IMAGE_NOT_AUTHORIZED = 'fake-shelved-image-not-authorized'
|
|
SHELVED_IMAGE_EXCEPTION = 'fake-shelved-image-exception'
|
|
|
|
|
|
@ddt.ddt
|
|
class _ComputeAPIUnitTestMixIn(object):
|
|
def setUp(self):
|
|
super(_ComputeAPIUnitTestMixIn, self).setUp()
|
|
self.user_id = 'fake'
|
|
self.project_id = 'fake'
|
|
self.compute_api = compute_api.API()
|
|
self.context = context.RequestContext(self.user_id,
|
|
self.project_id)
|
|
|
|
def _get_vm_states(self, exclude_states=None):
|
|
vm_state = set([vm_states.ACTIVE, vm_states.BUILDING, vm_states.PAUSED,
|
|
vm_states.SUSPENDED, vm_states.RESCUED, vm_states.STOPPED,
|
|
vm_states.RESIZED, vm_states.SOFT_DELETED,
|
|
vm_states.DELETED, vm_states.ERROR, vm_states.SHELVED,
|
|
vm_states.SHELVED_OFFLOADED])
|
|
if not exclude_states:
|
|
exclude_states = set()
|
|
return vm_state - exclude_states
|
|
|
|
def _create_flavor(self, **updates):
|
|
flavor = {'id': 1,
|
|
'flavorid': 1,
|
|
'name': 'm1.tiny',
|
|
'memory_mb': 512,
|
|
'vcpus': 1,
|
|
'vcpu_weight': None,
|
|
'root_gb': 1,
|
|
'ephemeral_gb': 0,
|
|
'rxtx_factor': 1,
|
|
'swap': 0,
|
|
'deleted': 0,
|
|
'disabled': False,
|
|
'is_public': True,
|
|
'deleted_at': None,
|
|
'created_at': datetime.datetime(2012, 1, 19, 18,
|
|
49, 30, 877329),
|
|
'updated_at': None,
|
|
'description': None
|
|
}
|
|
if updates:
|
|
flavor.update(updates)
|
|
|
|
expected_attrs = None
|
|
if 'extra_specs' in updates and updates['extra_specs']:
|
|
expected_attrs = ['extra_specs']
|
|
|
|
return objects.Flavor._from_db_object(
|
|
self.context, objects.Flavor(extra_specs={}), flavor,
|
|
expected_attrs=expected_attrs)
|
|
|
|
def _create_instance_obj(self, params=None, flavor=None):
|
|
"""Create a test instance."""
|
|
if not params:
|
|
params = {}
|
|
|
|
if flavor is None:
|
|
flavor = self._create_flavor()
|
|
|
|
now = timeutils.utcnow()
|
|
|
|
instance = objects.Instance()
|
|
instance.metadata = {}
|
|
instance.metadata.update(params.pop('metadata', {}))
|
|
instance.system_metadata = params.pop('system_metadata', {})
|
|
instance._context = self.context
|
|
instance.id = 1
|
|
instance.uuid = uuidutils.generate_uuid()
|
|
instance.vm_state = vm_states.ACTIVE
|
|
instance.task_state = None
|
|
instance.image_ref = FAKE_IMAGE_REF
|
|
instance.reservation_id = 'r-fakeres'
|
|
instance.user_id = self.user_id
|
|
instance.project_id = self.project_id
|
|
instance.host = 'fake_host'
|
|
instance.node = NODENAME
|
|
instance.instance_type_id = flavor.id
|
|
instance.ami_launch_index = 0
|
|
instance.memory_mb = 0
|
|
instance.vcpus = 0
|
|
instance.root_gb = 0
|
|
instance.ephemeral_gb = 0
|
|
instance.architecture = fields_obj.Architecture.X86_64
|
|
instance.os_type = 'Linux'
|
|
instance.locked = False
|
|
instance.created_at = now
|
|
instance.updated_at = now
|
|
instance.launched_at = now
|
|
instance.disable_terminate = False
|
|
instance.info_cache = objects.InstanceInfoCache()
|
|
instance.flavor = flavor
|
|
instance.old_flavor = instance.new_flavor = None
|
|
instance.numa_topology = None
|
|
|
|
if params:
|
|
instance.update(params)
|
|
instance.obj_reset_changes()
|
|
return instance
|
|
|
|
def _create_keypair_obj(self, instance):
|
|
"""Create a test keypair."""
|
|
keypair = objects.KeyPair()
|
|
keypair.id = 1
|
|
keypair.name = 'fake_key'
|
|
keypair.user_id = instance.user_id
|
|
keypair.fingerprint = 'fake'
|
|
keypair.public_key = 'fake key'
|
|
keypair.type = 'ssh'
|
|
return keypair
|
|
|
|
def _obj_to_list_obj(self, list_obj, obj):
|
|
list_obj.objects = []
|
|
list_obj.objects.append(obj)
|
|
list_obj._context = self.context
|
|
list_obj.obj_reset_changes()
|
|
return list_obj
|
|
|
|
@mock.patch('nova.objects.Quotas.check_deltas')
|
|
@mock.patch('nova.conductor.conductor_api.ComputeTaskAPI.build_instances')
|
|
@mock.patch('nova.compute.api.API._record_action_start')
|
|
@mock.patch('nova.compute.api.API._check_requested_networks')
|
|
@mock.patch('nova.objects.Quotas.limit_check')
|
|
@mock.patch('nova.compute.api.API._get_image')
|
|
@mock.patch('nova.compute.api.API._provision_instances')
|
|
def test_create_with_networks_max_count_none(self, provision_instances,
|
|
get_image, check_limit,
|
|
check_requested_networks,
|
|
record_action_start,
|
|
build_instances,
|
|
check_deltas):
|
|
# Make sure max_count is checked for None, as Python3 doesn't allow
|
|
# comparison between NoneType and Integer, something that's allowed in
|
|
# Python 2.
|
|
provision_instances.return_value = []
|
|
get_image.return_value = (None, {})
|
|
check_requested_networks.return_value = 1
|
|
|
|
instance_type = self._create_flavor()
|
|
|
|
port = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
|
|
address = '10.0.0.1'
|
|
requested_networks = objects.NetworkRequestList(
|
|
objects=[objects.NetworkRequest(address=address,
|
|
port_id=port)])
|
|
|
|
with mock.patch.object(self.compute_api.network_api,
|
|
'create_resource_requests',
|
|
return_value=(None, [])):
|
|
self.compute_api.create(self.context, instance_type, 'image_id',
|
|
requested_networks=requested_networks,
|
|
max_count=None)
|
|
|
|
@mock.patch('nova.objects.Quotas.count_as_dict')
|
|
@mock.patch('nova.objects.Quotas.limit_check')
|
|
@mock.patch('nova.objects.Quotas.limit_check_project_and_user')
|
|
def test_create_quota_exceeded_messages(self, mock_limit_check_pu,
|
|
mock_limit_check, mock_count):
|
|
image_href = "image_href"
|
|
image_id = 0
|
|
instance_type = self._create_flavor()
|
|
|
|
quotas = {'instances': 1, 'cores': 1, 'ram': 1}
|
|
quota_exception = exception.OverQuota(quotas=quotas,
|
|
usages={'instances': 1, 'cores': 1, 'ram': 1},
|
|
overs=['instances'])
|
|
|
|
proj_count = {'instances': 1, 'cores': 1, 'ram': 1}
|
|
user_count = proj_count.copy()
|
|
mock_count.return_value = {'project': proj_count, 'user': user_count}
|
|
# instances/cores/ram quota
|
|
mock_limit_check_pu.side_effect = quota_exception
|
|
|
|
# We don't care about network validation in this test.
|
|
self.compute_api.network_api.validate_networks = (
|
|
mock.Mock(return_value=40))
|
|
with mock.patch.object(self.compute_api, '_get_image',
|
|
return_value=(image_id, {})) as mock_get_image:
|
|
for min_count, message in [(20, '20-40'), (40, '40')]:
|
|
try:
|
|
self.compute_api.create(self.context, instance_type,
|
|
"image_href", min_count=min_count,
|
|
max_count=40)
|
|
except exception.TooManyInstances as e:
|
|
self.assertEqual(message, e.kwargs['req'])
|
|
else:
|
|
self.fail("Exception not raised")
|
|
mock_get_image.assert_called_with(self.context, image_href)
|
|
self.assertEqual(2, mock_get_image.call_count)
|
|
self.assertEqual(2, mock_limit_check_pu.call_count)
|
|
|
|
@mock.patch('nova.objects.Quotas.limit_check')
|
|
def test_create_volume_backed_instance_with_trusted_certs(self,
|
|
check_limit):
|
|
# Creating an instance with no image_ref specified will result in
|
|
# creating a volume-backed instance
|
|
self.assertRaises(exception.CertificateValidationFailed,
|
|
self.compute_api.create, self.context,
|
|
instance_type=self._create_flavor(), image_href=None,
|
|
trusted_certs=['test-cert-1', 'test-cert-2'])
|
|
|
|
@mock.patch('nova.objects.Quotas.limit_check')
|
|
def test_create_volume_backed_instance_with_conf_trusted_certs(
|
|
self, check_limit):
|
|
self.flags(verify_glance_signatures=True, group='glance')
|
|
self.flags(enable_certificate_validation=True, group='glance')
|
|
self.flags(default_trusted_certificate_ids=['certs'], group='glance')
|
|
# Creating an instance with no image_ref specified will result in
|
|
# creating a volume-backed instance
|
|
self.assertRaises(exception.CertificateValidationFailed,
|
|
self.compute_api.create, self.context,
|
|
instance_type=self._create_flavor(),
|
|
image_href=None)
|
|
|
|
def _test_create_max_net_count(self, max_net_count, min_count, max_count):
|
|
with test.nested(
|
|
mock.patch.object(self.compute_api, '_get_image',
|
|
return_value=(None, {})),
|
|
mock.patch.object(self.compute_api, '_check_auto_disk_config'),
|
|
mock.patch.object(self.compute_api,
|
|
'_validate_and_build_base_options',
|
|
return_value=({}, max_net_count, None,
|
|
['default'], None))
|
|
) as (
|
|
get_image,
|
|
check_auto_disk_config,
|
|
validate_and_build_base_options
|
|
):
|
|
self.assertRaises(exception.PortLimitExceeded,
|
|
self.compute_api.create, self.context, 'fake_flavor',
|
|
'image_id', min_count=min_count, max_count=max_count)
|
|
|
|
def test_max_net_count_zero(self):
|
|
# Test when max_net_count is zero.
|
|
max_net_count = 0
|
|
min_count = 2
|
|
max_count = 3
|
|
self._test_create_max_net_count(max_net_count, min_count, max_count)
|
|
|
|
def test_max_net_count_less_than_min_count(self):
|
|
# Test when max_net_count is nonzero but less than min_count.
|
|
max_net_count = 1
|
|
min_count = 2
|
|
max_count = 3
|
|
self._test_create_max_net_count(max_net_count, min_count, max_count)
|
|
|
|
def test_specified_port_and_multiple_instances(self):
|
|
# Tests that if port is specified there is only one instance booting
|
|
# (i.e max_count == 1) as we can't share the same port across multiple
|
|
# instances.
|
|
port = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
|
|
address = '10.0.0.1'
|
|
min_count = 1
|
|
max_count = 2
|
|
requested_networks = objects.NetworkRequestList(
|
|
objects=[objects.NetworkRequest(address=address,
|
|
port_id=port)])
|
|
|
|
self.assertRaises(exception.MultiplePortsNotApplicable,
|
|
self.compute_api.create, self.context, 'fake_flavor', 'image_id',
|
|
min_count=min_count, max_count=max_count,
|
|
requested_networks=requested_networks)
|
|
|
|
def _test_specified_ip_and_multiple_instances_helper(self,
|
|
requested_networks):
|
|
# Tests that if ip is specified there is only one instance booting
|
|
# (i.e max_count == 1)
|
|
min_count = 1
|
|
max_count = 2
|
|
self.assertRaises(exception.InvalidFixedIpAndMaxCountRequest,
|
|
self.compute_api.create, self.context, "fake_flavor", 'image_id',
|
|
min_count=min_count, max_count=max_count,
|
|
requested_networks=requested_networks)
|
|
|
|
def test_specified_ip_and_multiple_instances(self):
|
|
network = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
|
|
address = '10.0.0.1'
|
|
requested_networks = objects.NetworkRequestList(
|
|
objects=[objects.NetworkRequest(network_id=network,
|
|
address=address)])
|
|
self._test_specified_ip_and_multiple_instances_helper(
|
|
requested_networks)
|
|
|
|
@mock.patch('nova.objects.BlockDeviceMapping.save')
|
|
@mock.patch.object(compute_rpcapi.ComputeAPI, 'reserve_block_device_name')
|
|
def test_create_volume_bdm_call_reserve_dev_name(self, mock_reserve,
|
|
mock_bdm_save):
|
|
bdm = objects.BlockDeviceMapping(
|
|
**fake_block_device.FakeDbBlockDeviceDict(
|
|
{
|
|
'id': 1,
|
|
'volume_id': 1,
|
|
'source_type': 'volume',
|
|
'destination_type': 'volume',
|
|
'device_name': 'vda',
|
|
'boot_index': 1,
|
|
}))
|
|
mock_reserve.return_value = bdm
|
|
instance = self._create_instance_obj()
|
|
volume = {'id': '1', 'multiattach': False}
|
|
result = self.compute_api._create_volume_bdm(self.context,
|
|
instance,
|
|
'vda',
|
|
volume,
|
|
None,
|
|
None)
|
|
self.assertTrue(mock_reserve.called)
|
|
self.assertEqual(result, bdm)
|
|
mock_bdm_save.assert_called_once_with()
|
|
|
|
@mock.patch.object(objects.BlockDeviceMapping, 'create')
|
|
def test_create_volume_bdm_local_creation(self, bdm_create):
|
|
instance = self._create_instance_obj()
|
|
volume_id = 'fake-vol-id'
|
|
bdm = objects.BlockDeviceMapping(
|
|
**fake_block_device.FakeDbBlockDeviceDict(
|
|
{
|
|
'instance_uuid': instance.uuid,
|
|
'volume_id': volume_id,
|
|
'source_type': 'volume',
|
|
'destination_type': 'volume',
|
|
'device_name': 'vda',
|
|
'boot_index': None,
|
|
'disk_bus': None,
|
|
'device_type': None
|
|
}))
|
|
result = self.compute_api._create_volume_bdm(self.context,
|
|
instance,
|
|
'/dev/vda',
|
|
{'id': volume_id},
|
|
None,
|
|
None,
|
|
is_local_creation=True)
|
|
self.assertEqual(result.instance_uuid, bdm.instance_uuid)
|
|
self.assertIsNone(result.device_name)
|
|
self.assertEqual(result.volume_id, bdm.volume_id)
|
|
self.assertTrue(bdm_create.called)
|
|
|
|
@mock.patch.object(compute_api.API, '_record_action_start')
|
|
@mock.patch.object(compute_rpcapi.ComputeAPI, 'reserve_block_device_name')
|
|
@mock.patch.object(objects.BlockDeviceMapping,
|
|
'get_by_volume_and_instance')
|
|
@mock.patch.object(compute_rpcapi.ComputeAPI, 'attach_volume')
|
|
def test_attach_volume_new_flow(self, mock_attach, mock_bdm,
|
|
mock_reserve, mock_record):
|
|
mock_bdm.side_effect = exception.VolumeBDMNotFound(
|
|
volume_id='fake-volume-id')
|
|
instance = self._create_instance_obj()
|
|
volume = fake_volume.fake_volume(1, 'test-vol', 'test-vol',
|
|
None, None, None, None, None)
|
|
|
|
fake_bdm = mock.MagicMock(spec=objects.BlockDeviceMapping)
|
|
mock_reserve.return_value = fake_bdm
|
|
|
|
mock_volume_api = mock.patch.object(self.compute_api, 'volume_api',
|
|
mock.MagicMock(spec=cinder.API))
|
|
|
|
with mock_volume_api as mock_v_api:
|
|
mock_v_api.get.return_value = volume
|
|
mock_v_api.attachment_create.return_value = \
|
|
{'id': uuids.attachment_id}
|
|
self.compute_api.attach_volume(
|
|
self.context, instance, volume['id'])
|
|
mock_v_api.check_availability_zone.assert_called_once_with(
|
|
self.context, volume, instance=instance)
|
|
mock_v_api.attachment_create.assert_called_once_with(self.context,
|
|
volume['id'],
|
|
instance.uuid)
|
|
mock_attach.assert_called_once_with(self.context,
|
|
instance, fake_bdm)
|
|
|
|
@mock.patch.object(compute_api.API, '_record_action_start')
|
|
@mock.patch.object(compute_rpcapi.ComputeAPI, 'reserve_block_device_name')
|
|
@mock.patch.object(objects.BlockDeviceMapping,
|
|
'get_by_volume_and_instance')
|
|
@mock.patch.object(compute_rpcapi.ComputeAPI, 'attach_volume')
|
|
def test_tagged_volume_attach_new_flow(self, mock_attach, mock_bdm,
|
|
mock_reserve, mock_record):
|
|
mock_bdm.side_effect = exception.VolumeBDMNotFound(
|
|
volume_id='fake-volume-id')
|
|
instance = self._create_instance_obj()
|
|
volume = fake_volume.fake_volume(1, 'test-vol', 'test-vol',
|
|
None, None, None, None, None)
|
|
|
|
fake_bdm = mock.MagicMock(spec=objects.BlockDeviceMapping)
|
|
mock_reserve.return_value = fake_bdm
|
|
|
|
mock_volume_api = mock.patch.object(self.compute_api, 'volume_api',
|
|
mock.MagicMock(spec=cinder.API))
|
|
|
|
with mock_volume_api as mock_v_api:
|
|
mock_v_api.get.return_value = volume
|
|
mock_v_api.attachment_create.return_value = \
|
|
{'id': uuids.attachment_id}
|
|
self.compute_api.attach_volume(
|
|
self.context, instance, volume['id'], tag='foo')
|
|
mock_reserve.assert_called_once_with(self.context, instance, None,
|
|
volume['id'],
|
|
device_type=None,
|
|
disk_bus=None, tag='foo',
|
|
multiattach=False)
|
|
mock_v_api.check_availability_zone.assert_called_once_with(
|
|
self.context, volume, instance=instance)
|
|
mock_v_api.attachment_create.assert_called_once_with(
|
|
self.context, volume['id'], instance.uuid)
|
|
mock_attach.assert_called_once_with(self.context,
|
|
instance, fake_bdm)
|
|
|
|
@mock.patch.object(compute_rpcapi.ComputeAPI, 'reserve_block_device_name')
|
|
@mock.patch.object(objects.BlockDeviceMapping,
|
|
'get_by_volume_and_instance')
|
|
@mock.patch.object(compute_rpcapi.ComputeAPI, 'attach_volume')
|
|
def test_attach_volume_attachment_create_fails(self, mock_attach, mock_bdm,
|
|
mock_reserve):
|
|
mock_bdm.side_effect = exception.VolumeBDMNotFound(
|
|
volume_id='fake-volume-id')
|
|
instance = self._create_instance_obj()
|
|
volume = fake_volume.fake_volume(1, 'test-vol', 'test-vol',
|
|
None, None, None, None, None)
|
|
|
|
fake_bdm = mock.MagicMock(spec=objects.BlockDeviceMapping)
|
|
mock_reserve.return_value = fake_bdm
|
|
|
|
mock_volume_api = mock.patch.object(self.compute_api, 'volume_api',
|
|
mock.MagicMock(spec=cinder.API))
|
|
|
|
with mock_volume_api as mock_v_api:
|
|
mock_v_api.get.return_value = volume
|
|
mock_v_api.attachment_create.side_effect = test.TestingException()
|
|
self.assertRaises(test.TestingException,
|
|
self.compute_api.attach_volume,
|
|
self.context, instance, volume['id'])
|
|
mock_v_api.check_availability_zone.assert_called_once_with(
|
|
self.context, volume, instance=instance)
|
|
mock_v_api.attachment_create.assert_called_once_with(
|
|
self.context, volume['id'], instance.uuid)
|
|
self.assertEqual(0, mock_attach.call_count)
|
|
fake_bdm.destroy.assert_called_once_with()
|
|
|
|
def test_suspend(self):
|
|
# Ensure instance can be suspended.
|
|
instance = self._create_instance_obj()
|
|
self.assertEqual(instance.vm_state, vm_states.ACTIVE)
|
|
self.assertIsNone(instance.task_state)
|
|
|
|
rpcapi = self.compute_api.compute_rpcapi
|
|
|
|
with test.nested(
|
|
mock.patch.object(instance, 'save'),
|
|
mock.patch.object(self.compute_api, '_record_action_start'),
|
|
mock.patch.object(rpcapi, 'suspend_instance')
|
|
) as (mock_inst_save, mock_record_action, mock_suspend_instance):
|
|
self.compute_api.suspend(self.context, instance)
|
|
self.assertEqual(vm_states.ACTIVE, instance.vm_state)
|
|
self.assertEqual(task_states.SUSPENDING, instance.task_state)
|
|
mock_inst_save.assert_called_once_with(expected_task_state=[None])
|
|
mock_record_action.assert_called_once_with(
|
|
self.context, instance, instance_actions.SUSPEND)
|
|
mock_suspend_instance.assert_called_once_with(self.context,
|
|
instance)
|
|
|
|
def _test_suspend_fails(self, vm_state):
|
|
params = dict(vm_state=vm_state)
|
|
instance = self._create_instance_obj(params=params)
|
|
self.assertIsNone(instance.task_state)
|
|
self.assertRaises(exception.InstanceInvalidState,
|
|
self.compute_api.suspend,
|
|
self.context, instance)
|
|
|
|
def test_suspend_fails_invalid_states(self):
|
|
invalid_vm_states = self._get_vm_states(set([vm_states.ACTIVE]))
|
|
for state in invalid_vm_states:
|
|
self._test_suspend_fails(state)
|
|
|
|
def test_resume(self):
|
|
# Ensure instance can be resumed (if suspended).
|
|
instance = self._create_instance_obj(
|
|
params=dict(vm_state=vm_states.SUSPENDED))
|
|
self.assertEqual(instance.vm_state, vm_states.SUSPENDED)
|
|
self.assertIsNone(instance.task_state)
|
|
|
|
rpcapi = self.compute_api.compute_rpcapi
|
|
|
|
with test.nested(
|
|
mock.patch.object(instance, 'save'),
|
|
mock.patch.object(self.compute_api, '_record_action_start'),
|
|
mock.patch.object(rpcapi, 'resume_instance')
|
|
) as (mock_inst_save, mock_record_action, mock_resume_instance):
|
|
self.compute_api.resume(self.context, instance)
|
|
self.assertEqual(vm_states.SUSPENDED, instance.vm_state)
|
|
self.assertEqual(task_states.RESUMING,
|
|
instance.task_state)
|
|
mock_inst_save.assert_called_once_with(expected_task_state=[None])
|
|
mock_record_action.assert_called_once_with(
|
|
self.context, instance, instance_actions.RESUME)
|
|
mock_resume_instance.assert_called_once_with(self.context,
|
|
instance)
|
|
|
|
def test_start(self):
|
|
params = dict(vm_state=vm_states.STOPPED)
|
|
instance = self._create_instance_obj(params=params)
|
|
|
|
rpcapi = self.compute_api.compute_rpcapi
|
|
|
|
with test.nested(
|
|
mock.patch.object(instance, 'save'),
|
|
mock.patch.object(self.compute_api, '_record_action_start'),
|
|
mock.patch.object(rpcapi, 'start_instance')
|
|
) as (mock_inst_save, mock_record_action, mock_start_instance):
|
|
self.compute_api.start(self.context, instance)
|
|
self.assertEqual(task_states.POWERING_ON,
|
|
instance.task_state)
|
|
mock_inst_save.assert_called_once_with(expected_task_state=[None])
|
|
mock_record_action.assert_called_once_with(
|
|
self.context, instance, instance_actions.START)
|
|
mock_start_instance.assert_called_once_with(self.context,
|
|
instance)
|
|
|
|
def test_start_invalid_state(self):
|
|
instance = self._create_instance_obj()
|
|
self.assertEqual(instance.vm_state, vm_states.ACTIVE)
|
|
self.assertRaises(exception.InstanceInvalidState,
|
|
self.compute_api.start,
|
|
self.context, instance)
|
|
|
|
def test_start_no_host(self):
|
|
params = dict(vm_state=vm_states.STOPPED, host='')
|
|
instance = self._create_instance_obj(params=params)
|
|
self.assertRaises(exception.InstanceNotReady,
|
|
self.compute_api.start,
|
|
self.context, instance)
|
|
|
|
def _test_stop(self, vm_state, force=False, clean_shutdown=True):
|
|
# Make sure 'progress' gets reset
|
|
params = dict(task_state=None, progress=99, vm_state=vm_state)
|
|
instance = self._create_instance_obj(params=params)
|
|
|
|
rpcapi = self.compute_api.compute_rpcapi
|
|
|
|
with test.nested(
|
|
mock.patch.object(instance, 'save'),
|
|
mock.patch.object(self.compute_api, '_record_action_start'),
|
|
mock.patch.object(rpcapi, 'stop_instance')
|
|
) as (mock_inst_save, mock_record_action, mock_stop_instance):
|
|
if force:
|
|
self.compute_api.force_stop(self.context, instance,
|
|
clean_shutdown=clean_shutdown)
|
|
else:
|
|
self.compute_api.stop(self.context, instance,
|
|
clean_shutdown=clean_shutdown)
|
|
self.assertEqual(task_states.POWERING_OFF,
|
|
instance.task_state)
|
|
self.assertEqual(0, instance.progress)
|
|
|
|
mock_inst_save.assert_called_once_with(expected_task_state=[None])
|
|
mock_record_action.assert_called_once_with(
|
|
self.context, instance, instance_actions.STOP)
|
|
mock_stop_instance.assert_called_once_with(
|
|
self.context, instance, do_cast=True,
|
|
clean_shutdown=clean_shutdown)
|
|
|
|
def test_stop(self):
|
|
self._test_stop(vm_states.ACTIVE)
|
|
|
|
def test_stop_stopped_instance_with_bypass(self):
|
|
self._test_stop(vm_states.STOPPED, force=True)
|
|
|
|
def test_stop_forced_shutdown(self):
|
|
self._test_stop(vm_states.ACTIVE, force=True)
|
|
|
|
def test_stop_without_clean_shutdown(self):
|
|
self._test_stop(vm_states.ACTIVE,
|
|
clean_shutdown=False)
|
|
|
|
def test_stop_forced_without_clean_shutdown(self):
|
|
self._test_stop(vm_states.ACTIVE, force=True,
|
|
clean_shutdown=False)
|
|
|
|
def _test_stop_invalid_state(self, vm_state):
|
|
params = dict(vm_state=vm_state)
|
|
instance = self._create_instance_obj(params=params)
|
|
self.assertRaises(exception.InstanceInvalidState,
|
|
self.compute_api.stop,
|
|
self.context, instance)
|
|
|
|
def test_stop_fails_invalid_states(self):
|
|
invalid_vm_states = self._get_vm_states(set([vm_states.ACTIVE,
|
|
vm_states.ERROR]))
|
|
for state in invalid_vm_states:
|
|
self._test_stop_invalid_state(state)
|
|
|
|
def test_stop_a_stopped_inst(self):
|
|
params = {'vm_state': vm_states.STOPPED}
|
|
instance = self._create_instance_obj(params=params)
|
|
|
|
self.assertRaises(exception.InstanceInvalidState,
|
|
self.compute_api.stop,
|
|
self.context, instance)
|
|
|
|
def test_stop_no_host(self):
|
|
params = {'host': ''}
|
|
instance = self._create_instance_obj(params=params)
|
|
self.assertRaises(exception.InstanceNotReady,
|
|
self.compute_api.stop,
|
|
self.context, instance)
|
|
|
|
@mock.patch('nova.compute.api.API._record_action_start')
|
|
@mock.patch('nova.compute.rpcapi.ComputeAPI.trigger_crash_dump')
|
|
def test_trigger_crash_dump(self,
|
|
trigger_crash_dump,
|
|
_record_action_start):
|
|
instance = self._create_instance_obj()
|
|
|
|
self.compute_api.trigger_crash_dump(self.context, instance)
|
|
|
|
_record_action_start.assert_called_once_with(self.context, instance,
|
|
instance_actions.TRIGGER_CRASH_DUMP)
|
|
|
|
trigger_crash_dump.assert_called_once_with(self.context, instance)
|
|
|
|
self.assertIsNone(instance.task_state)
|
|
|
|
def test_trigger_crash_dump_invalid_state(self):
|
|
params = dict(vm_state=vm_states.STOPPED)
|
|
instance = self._create_instance_obj(params)
|
|
self.assertRaises(exception.InstanceInvalidState,
|
|
self.compute_api.trigger_crash_dump,
|
|
self.context, instance)
|
|
|
|
def test_trigger_crash_dump_no_host(self):
|
|
params = dict(host='')
|
|
instance = self._create_instance_obj(params=params)
|
|
self.assertRaises(exception.InstanceNotReady,
|
|
self.compute_api.trigger_crash_dump,
|
|
self.context, instance)
|
|
|
|
def test_trigger_crash_dump_locked(self):
|
|
params = dict(locked=True)
|
|
instance = self._create_instance_obj(params=params)
|
|
self.assertRaises(exception.InstanceIsLocked,
|
|
self.compute_api.trigger_crash_dump,
|
|
self.context, instance)
|
|
|
|
def _test_reboot_type(self, vm_state, reboot_type, task_state=None):
|
|
# Ensure instance can be soft rebooted.
|
|
inst = self._create_instance_obj()
|
|
inst.vm_state = vm_state
|
|
inst.task_state = task_state
|
|
|
|
expected_task_state = [None]
|
|
if reboot_type == 'HARD':
|
|
expected_task_state = task_states.ALLOW_REBOOT
|
|
|
|
rpcapi = self.compute_api.compute_rpcapi
|
|
|
|
with test.nested(
|
|
mock.patch.object(self.context, 'elevated'),
|
|
mock.patch.object(inst, 'save'),
|
|
mock.patch.object(self.compute_api, '_record_action_start'),
|
|
mock.patch.object(rpcapi, 'reboot_instance')
|
|
) as (mock_elevated, mock_inst_save,
|
|
mock_record_action, mock_reboot_instance):
|
|
self.compute_api.reboot(self.context, inst, reboot_type)
|
|
|
|
mock_inst_save.assert_called_once_with(
|
|
expected_task_state=expected_task_state)
|
|
mock_record_action.assert_called_once_with(
|
|
self.context, inst, instance_actions.REBOOT)
|
|
mock_reboot_instance.assert_called_once_with(
|
|
self.context, instance=inst,
|
|
block_device_info=None, reboot_type=reboot_type)
|
|
|
|
def _test_reboot_type_fails(self, reboot_type, **updates):
|
|
inst = self._create_instance_obj()
|
|
inst.update(updates)
|
|
|
|
self.assertRaises(exception.InstanceInvalidState,
|
|
self.compute_api.reboot,
|
|
self.context, inst, reboot_type)
|
|
|
|
def test_reboot_hard_active(self):
|
|
self._test_reboot_type(vm_states.ACTIVE, 'HARD')
|
|
|
|
def test_reboot_hard_error(self):
|
|
self._test_reboot_type(vm_states.ERROR, 'HARD')
|
|
|
|
def test_reboot_hard_rebooting(self):
|
|
self._test_reboot_type(vm_states.ACTIVE, 'HARD',
|
|
task_state=task_states.REBOOTING)
|
|
|
|
def test_reboot_hard_reboot_started(self):
|
|
self._test_reboot_type(vm_states.ACTIVE, 'HARD',
|
|
task_state=task_states.REBOOT_STARTED)
|
|
|
|
def test_reboot_hard_reboot_pending(self):
|
|
self._test_reboot_type(vm_states.ACTIVE, 'HARD',
|
|
task_state=task_states.REBOOT_PENDING)
|
|
|
|
def test_reboot_hard_rescued(self):
|
|
self._test_reboot_type_fails('HARD', vm_state=vm_states.RESCUED)
|
|
|
|
def test_reboot_hard_resuming(self):
|
|
self._test_reboot_type(vm_states.ACTIVE,
|
|
'HARD', task_state=task_states.RESUMING)
|
|
|
|
def test_reboot_hard_unpausing(self):
|
|
self._test_reboot_type(vm_states.ACTIVE,
|
|
'HARD', task_state=task_states.UNPAUSING)
|
|
|
|
def test_reboot_hard_suspending(self):
|
|
self._test_reboot_type(vm_states.ACTIVE,
|
|
'HARD', task_state=task_states.SUSPENDING)
|
|
|
|
def test_reboot_hard_error_not_launched(self):
|
|
self._test_reboot_type_fails('HARD', vm_state=vm_states.ERROR,
|
|
launched_at=None)
|
|
|
|
def test_reboot_soft(self):
|
|
self._test_reboot_type(vm_states.ACTIVE, 'SOFT')
|
|
|
|
def test_reboot_soft_error(self):
|
|
self._test_reboot_type_fails('SOFT', vm_state=vm_states.ERROR)
|
|
|
|
def test_reboot_soft_paused(self):
|
|
self._test_reboot_type_fails('SOFT', vm_state=vm_states.PAUSED)
|
|
|
|
def test_reboot_soft_stopped(self):
|
|
self._test_reboot_type_fails('SOFT', vm_state=vm_states.STOPPED)
|
|
|
|
def test_reboot_soft_suspended(self):
|
|
self._test_reboot_type_fails('SOFT', vm_state=vm_states.SUSPENDED)
|
|
|
|
def test_reboot_soft_rebooting(self):
|
|
self._test_reboot_type_fails('SOFT', task_state=task_states.REBOOTING)
|
|
|
|
def test_reboot_soft_rebooting_hard(self):
|
|
self._test_reboot_type_fails('SOFT',
|
|
task_state=task_states.REBOOTING_HARD)
|
|
|
|
def test_reboot_soft_reboot_started(self):
|
|
self._test_reboot_type_fails('SOFT',
|
|
task_state=task_states.REBOOT_STARTED)
|
|
|
|
def test_reboot_soft_reboot_pending(self):
|
|
self._test_reboot_type_fails('SOFT',
|
|
task_state=task_states.REBOOT_PENDING)
|
|
|
|
def test_reboot_soft_rescued(self):
|
|
self._test_reboot_type_fails('SOFT', vm_state=vm_states.RESCUED)
|
|
|
|
def test_reboot_soft_error_not_launched(self):
|
|
self._test_reboot_type_fails('SOFT', vm_state=vm_states.ERROR,
|
|
launched_at=None)
|
|
|
|
def test_reboot_soft_resuming(self):
|
|
self._test_reboot_type_fails('SOFT', task_state=task_states.RESUMING)
|
|
|
|
def test_reboot_soft_pausing(self):
|
|
self._test_reboot_type_fails('SOFT', task_state=task_states.PAUSING)
|
|
|
|
def test_reboot_soft_unpausing(self):
|
|
self._test_reboot_type_fails('SOFT', task_state=task_states.UNPAUSING)
|
|
|
|
def test_reboot_soft_suspending(self):
|
|
self._test_reboot_type_fails('SOFT', task_state=task_states.SUSPENDING)
|
|
|
|
def _test_delete_resizing_part(self, inst, deltas):
|
|
old_flavor = inst.old_flavor
|
|
deltas['cores'] = -old_flavor.vcpus
|
|
deltas['ram'] = -old_flavor.memory_mb
|
|
|
|
def _set_delete_shelved_part(self, inst, mock_image_delete):
|
|
snapshot_id = inst.system_metadata.get('shelved_image_id')
|
|
if snapshot_id == SHELVED_IMAGE:
|
|
mock_image_delete.return_value = True
|
|
elif snapshot_id == SHELVED_IMAGE_NOT_FOUND:
|
|
mock_image_delete.side_effect = exception.ImageNotFound(
|
|
image_id=snapshot_id)
|
|
elif snapshot_id == SHELVED_IMAGE_NOT_AUTHORIZED:
|
|
mock_image_delete.side_effect = exception.ImageNotAuthorized(
|
|
image_id=snapshot_id)
|
|
elif snapshot_id == SHELVED_IMAGE_EXCEPTION:
|
|
mock_image_delete.side_effect = test.TestingException(
|
|
"Unexpected error")
|
|
|
|
return snapshot_id
|
|
|
|
@mock.patch.object(compute_utils,
|
|
'notify_about_instance_action')
|
|
@mock.patch.object(objects.Migration, 'get_by_instance_and_status')
|
|
@mock.patch.object(image_api.API, 'delete')
|
|
@mock.patch.object(objects.InstanceMapping, 'save')
|
|
@mock.patch.object(objects.InstanceMapping, 'get_by_instance_uuid')
|
|
@mock.patch.object(compute_utils,
|
|
'notify_about_instance_usage')
|
|
@mock.patch.object(db, 'instance_destroy')
|
|
@mock.patch.object(db, 'instance_system_metadata_get')
|
|
@mock.patch.object(neutron_api.API, 'deallocate_for_instance')
|
|
@mock.patch.object(db, 'instance_update_and_get_original')
|
|
@mock.patch.object(compute_api.API, '_record_action_start')
|
|
@mock.patch.object(servicegroup_api.API, 'service_is_up')
|
|
@mock.patch.object(objects.Service, 'get_by_compute_host')
|
|
@mock.patch.object(context.RequestContext, 'elevated')
|
|
@mock.patch.object(objects.BlockDeviceMappingList,
|
|
'get_by_instance_uuid', return_value=[])
|
|
@mock.patch.object(objects.Instance, 'save')
|
|
def _test_delete(self, delete_type, mock_save, mock_bdm_get, mock_elevated,
|
|
mock_get_cn, mock_up, mock_record, mock_inst_update,
|
|
mock_deallocate, mock_inst_meta, mock_inst_destroy,
|
|
mock_notify_legacy, mock_get_inst,
|
|
mock_save_im, mock_image_delete, mock_mig_get,
|
|
mock_notify, **attrs):
|
|
expected_save_calls = [mock.call()]
|
|
expected_record_calls = []
|
|
expected_elevated_calls = []
|
|
inst = self._create_instance_obj()
|
|
inst.update(attrs)
|
|
inst._context = self.context
|
|
deltas = {'instances': -1,
|
|
'cores': -inst.flavor.vcpus,
|
|
'ram': -inst.flavor.memory_mb}
|
|
delete_time = datetime.datetime(1955, 11, 5, 9, 30,
|
|
tzinfo=iso8601.UTC)
|
|
self.useFixture(utils_fixture.TimeFixture(delete_time))
|
|
task_state = (delete_type == 'soft_delete' and
|
|
task_states.SOFT_DELETING or task_states.DELETING)
|
|
updates = {'progress': 0, 'task_state': task_state}
|
|
if delete_type == 'soft_delete':
|
|
updates['deleted_at'] = delete_time
|
|
rpcapi = self.compute_api.compute_rpcapi
|
|
mock_confirm = self.useFixture(
|
|
fixtures.MockPatchObject(rpcapi, 'confirm_resize')).mock
|
|
|
|
def _reset_task_state(context, instance, migration, src_host,
|
|
cast=False):
|
|
inst.update({'task_state': None})
|
|
|
|
# After confirm resize action, instance task_state is reset to None
|
|
mock_confirm.side_effect = _reset_task_state
|
|
|
|
is_shelved = inst.vm_state in (vm_states.SHELVED,
|
|
vm_states.SHELVED_OFFLOADED)
|
|
if is_shelved:
|
|
snapshot_id = self._set_delete_shelved_part(inst,
|
|
mock_image_delete)
|
|
|
|
mock_terminate = self.useFixture(
|
|
fixtures.MockPatchObject(rpcapi, 'terminate_instance')).mock
|
|
mock_soft_delete = self.useFixture(
|
|
fixtures.MockPatchObject(rpcapi, 'soft_delete_instance')).mock
|
|
|
|
if inst.task_state == task_states.RESIZE_FINISH:
|
|
self._test_delete_resizing_part(inst, deltas)
|
|
|
|
# NOTE(comstud): This is getting messy. But what we are wanting
|
|
# to test is:
|
|
# * Check for downed host
|
|
# * If downed host:
|
|
# * Clean up instance, destroying it, sending notifications.
|
|
# (Tested in _test_downed_host_part())
|
|
# * If not downed host:
|
|
# * Record the action start.
|
|
# * Cast to compute_rpcapi.<method>
|
|
|
|
cast = True
|
|
is_downed_host = inst.host == 'down-host' or inst.host is None
|
|
if inst.vm_state == vm_states.RESIZED:
|
|
migration = objects.Migration._from_db_object(
|
|
self.context, objects.Migration(),
|
|
test_migration.fake_db_migration())
|
|
mock_elevated.return_value = self.context
|
|
expected_elevated_calls.append(mock.call())
|
|
mock_mig_get.return_value = migration
|
|
expected_record_calls.append(
|
|
mock.call(self.context, inst,
|
|
instance_actions.CONFIRM_RESIZE))
|
|
|
|
# After confirm resize action, instance task_state
|
|
# is reset to None, so is the expected value. But
|
|
# for soft delete, task_state will be again reset
|
|
# back to soft-deleting in the code to avoid status
|
|
# checking failure.
|
|
updates['task_state'] = None
|
|
if delete_type == 'soft_delete':
|
|
expected_save_calls.append(mock.call())
|
|
updates['task_state'] = 'soft-deleting'
|
|
|
|
if inst.host is not None:
|
|
mock_elevated.return_value = self.context
|
|
expected_elevated_calls.append(mock.call())
|
|
mock_get_cn.return_value = objects.Service()
|
|
mock_up.return_value = (inst.host != 'down-host')
|
|
|
|
if is_downed_host:
|
|
mock_elevated.return_value = self.context
|
|
expected_elevated_calls.append(mock.call())
|
|
expected_save_calls.append(mock.call())
|
|
state = ('soft' in delete_type and vm_states.SOFT_DELETED or
|
|
vm_states.DELETED)
|
|
updates.update({'vm_state': state,
|
|
'task_state': None,
|
|
'terminated_at': delete_time,
|
|
'deleted_at': delete_time,
|
|
'deleted': True})
|
|
fake_inst = fake_instance.fake_db_instance(**updates)
|
|
mock_inst_destroy.return_value = fake_inst
|
|
cell = objects.CellMapping(uuid=uuids.cell,
|
|
transport_url='fake://',
|
|
database_connection='fake://')
|
|
im = objects.InstanceMapping(cell_mapping=cell)
|
|
mock_get_inst.return_value = im
|
|
cast = False
|
|
|
|
if cast:
|
|
expected_record_calls.append(mock.call(self.context, inst,
|
|
instance_actions.DELETE))
|
|
|
|
# NOTE(takashin): If objects.Instance.destroy() is called,
|
|
# objects.Instance.uuid (inst.uuid) and host (inst.host) are changed.
|
|
# So preserve them before calling the method to test.
|
|
instance_uuid = inst.uuid
|
|
instance_host = inst.host
|
|
getattr(self.compute_api, delete_type)(self.context, inst)
|
|
for k, v in updates.items():
|
|
self.assertEqual(inst[k], v)
|
|
|
|
mock_save.assert_has_calls(expected_save_calls)
|
|
mock_bdm_get.assert_called_once_with(self.context, instance_uuid)
|
|
|
|
if expected_record_calls:
|
|
mock_record.assert_has_calls(expected_record_calls)
|
|
if expected_elevated_calls:
|
|
mock_elevated.assert_has_calls(expected_elevated_calls)
|
|
|
|
if inst.vm_state == vm_states.RESIZED:
|
|
mock_mig_get.assert_called_once_with(
|
|
self.context, instance_uuid, 'finished')
|
|
mock_confirm.assert_called_once_with(
|
|
self.context, inst, migration, migration['source_compute'],
|
|
cast=False)
|
|
if instance_host is not None:
|
|
mock_get_cn.assert_called_once_with(self.context,
|
|
instance_host)
|
|
mock_up.assert_called_once_with(
|
|
test.MatchType(objects.Service))
|
|
if is_downed_host:
|
|
if 'soft' in delete_type:
|
|
mock_notify_legacy.assert_has_calls([
|
|
mock.call(self.compute_api.notifier, self.context,
|
|
inst, 'delete.start'),
|
|
mock.call(self.compute_api.notifier, self.context,
|
|
inst, 'delete.end')])
|
|
else:
|
|
mock_notify_legacy.assert_has_calls([
|
|
mock.call(self.compute_api.notifier, self.context,
|
|
inst, '%s.start' % delete_type),
|
|
mock.call(self.compute_api.notifier, self.context,
|
|
inst, '%s.end' % delete_type)])
|
|
mock_deallocate.assert_called_once_with(self.context, inst)
|
|
mock_inst_destroy.assert_called_once_with(
|
|
self.context, instance_uuid, constraint=None,
|
|
hard_delete=False)
|
|
mock_get_inst.assert_called_with(self.context, instance_uuid)
|
|
self.assertEqual(2, mock_get_inst.call_count)
|
|
self.assertTrue(mock_get_inst.return_value.queued_for_delete)
|
|
mock_save_im.assert_called_once_with()
|
|
|
|
if cast:
|
|
if delete_type == 'soft_delete':
|
|
mock_soft_delete.assert_called_once_with(self.context, inst)
|
|
elif delete_type in ['delete', 'force_delete']:
|
|
mock_terminate.assert_called_once_with(
|
|
self.context, inst, [])
|
|
|
|
if is_shelved:
|
|
mock_image_delete.assert_called_once_with(self.context,
|
|
snapshot_id)
|
|
if not cast and delete_type == 'delete':
|
|
mock_notify.assert_has_calls([
|
|
mock.call(self.context, inst, host='fake-mini',
|
|
source='nova-api',
|
|
action=delete_type, phase='start'),
|
|
mock.call(self.context, inst, host='fake-mini',
|
|
source='nova-api',
|
|
action=delete_type, phase='end')])
|
|
|
|
def test_delete(self):
|
|
self._test_delete('delete')
|
|
|
|
def test_delete_if_not_launched(self):
|
|
self._test_delete('delete', launched_at=None)
|
|
|
|
def test_delete_in_resizing(self):
|
|
old_flavor = objects.Flavor(vcpus=1, memory_mb=512, extra_specs={})
|
|
self._test_delete('delete',
|
|
task_state=task_states.RESIZE_FINISH,
|
|
old_flavor=old_flavor)
|
|
|
|
def test_delete_in_resized(self):
|
|
self._test_delete('delete', vm_state=vm_states.RESIZED)
|
|
|
|
def test_delete_shelved(self):
|
|
fake_sys_meta = {'shelved_image_id': SHELVED_IMAGE}
|
|
self._test_delete('delete',
|
|
vm_state=vm_states.SHELVED,
|
|
system_metadata=fake_sys_meta)
|
|
|
|
def test_delete_shelved_offloaded(self):
|
|
fake_sys_meta = {'shelved_image_id': SHELVED_IMAGE}
|
|
self._test_delete('delete',
|
|
vm_state=vm_states.SHELVED_OFFLOADED,
|
|
system_metadata=fake_sys_meta)
|
|
|
|
def test_delete_shelved_image_not_found(self):
|
|
fake_sys_meta = {'shelved_image_id': SHELVED_IMAGE_NOT_FOUND}
|
|
self._test_delete('delete',
|
|
vm_state=vm_states.SHELVED_OFFLOADED,
|
|
system_metadata=fake_sys_meta)
|
|
|
|
def test_delete_shelved_image_not_authorized(self):
|
|
fake_sys_meta = {'shelved_image_id': SHELVED_IMAGE_NOT_AUTHORIZED}
|
|
self._test_delete('delete',
|
|
vm_state=vm_states.SHELVED_OFFLOADED,
|
|
system_metadata=fake_sys_meta)
|
|
|
|
def test_delete_shelved_exception(self):
|
|
fake_sys_meta = {'shelved_image_id': SHELVED_IMAGE_EXCEPTION}
|
|
self._test_delete('delete',
|
|
vm_state=vm_states.SHELVED,
|
|
system_metadata=fake_sys_meta)
|
|
|
|
def test_delete_with_down_host(self):
|
|
self._test_delete('delete', host='down-host')
|
|
|
|
def test_delete_soft_with_down_host(self):
|
|
self._test_delete('soft_delete', host='down-host')
|
|
|
|
def test_delete_soft_in_resized(self):
|
|
self._test_delete('soft_delete', vm_state=vm_states.RESIZED)
|
|
|
|
def test_delete_soft(self):
|
|
self._test_delete('soft_delete')
|
|
|
|
def test_delete_forced(self):
|
|
fake_sys_meta = {'shelved_image_id': SHELVED_IMAGE}
|
|
for vm_state in self._get_vm_states():
|
|
if vm_state in (vm_states.SHELVED, vm_states.SHELVED_OFFLOADED):
|
|
self._test_delete('force_delete',
|
|
vm_state=vm_state,
|
|
system_metadata=fake_sys_meta)
|
|
self._test_delete('force_delete', vm_state=vm_state)
|
|
|
|
@mock.patch('nova.compute.api.API._delete_while_booting',
|
|
return_value=False)
|
|
@mock.patch('nova.compute.api.API._lookup_instance')
|
|
@mock.patch('nova.objects.BlockDeviceMappingList.get_by_instance_uuid')
|
|
@mock.patch('nova.objects.Instance.save')
|
|
@mock.patch('nova.compute.utils.notify_about_instance_usage')
|
|
@mock.patch('nova.objects.Service.get_by_compute_host')
|
|
@mock.patch('nova.compute.api.API._local_delete')
|
|
def test_delete_error_state_with_no_host(
|
|
self, mock_local_delete, mock_service_get, _mock_notify,
|
|
_mock_save, mock_bdm_get, mock_lookup, _mock_del_booting):
|
|
# Instance in error state with no host should be a local delete
|
|
# for non API cells
|
|
inst = self._create_instance_obj(params=dict(vm_state=vm_states.ERROR,
|
|
host=None))
|
|
mock_lookup.return_value = None, inst
|
|
with mock.patch.object(self.compute_api.compute_rpcapi,
|
|
'terminate_instance') as mock_terminate:
|
|
self.compute_api.delete(self.context, inst)
|
|
mock_local_delete.assert_called_once_with(
|
|
self.context, inst, mock_bdm_get.return_value,
|
|
'delete', self.compute_api._do_delete)
|
|
mock_terminate.assert_not_called()
|
|
mock_service_get.assert_not_called()
|
|
|
|
@mock.patch('nova.compute.api.API._delete_while_booting',
|
|
return_value=False)
|
|
@mock.patch('nova.compute.api.API._lookup_instance')
|
|
@mock.patch('nova.objects.BlockDeviceMappingList.get_by_instance_uuid')
|
|
@mock.patch('nova.objects.Instance.save')
|
|
@mock.patch('nova.compute.utils.notify_about_instance_usage')
|
|
@mock.patch('nova.objects.Service.get_by_compute_host')
|
|
@mock.patch('nova.context.RequestContext.elevated')
|
|
@mock.patch('nova.servicegroup.api.API.service_is_up', return_value=True)
|
|
@mock.patch('nova.compute.api.API._record_action_start')
|
|
@mock.patch('nova.compute.api.API._local_delete')
|
|
def test_delete_error_state_with_host_set(
|
|
self, mock_local_delete, _mock_record, mock_service_up,
|
|
mock_elevated, mock_service_get, _mock_notify, _mock_save,
|
|
mock_bdm_get, mock_lookup, _mock_del_booting):
|
|
# Instance in error state with host set should be a non-local delete
|
|
# for non API cells if the service is up
|
|
inst = self._create_instance_obj(params=dict(vm_state=vm_states.ERROR,
|
|
host='fake-host'))
|
|
mock_lookup.return_value = inst
|
|
with mock.patch.object(self.compute_api.compute_rpcapi,
|
|
'terminate_instance') as mock_terminate:
|
|
self.compute_api.delete(self.context, inst)
|
|
mock_service_get.assert_called_once_with(
|
|
mock_elevated.return_value, 'fake-host')
|
|
mock_service_up.assert_called_once_with(
|
|
mock_service_get.return_value)
|
|
mock_terminate.assert_called_once_with(
|
|
self.context, inst, mock_bdm_get.return_value)
|
|
mock_local_delete.assert_not_called()
|
|
|
|
def test_delete_forced_when_task_state_is_not_none(self):
|
|
for vm_state in self._get_vm_states():
|
|
self._test_delete('force_delete', vm_state=vm_state,
|
|
task_state=task_states.RESIZE_MIGRATING)
|
|
|
|
@mock.patch.object(compute_utils, 'notify_about_instance_action')
|
|
@mock.patch.object(compute_utils, 'notify_about_instance_usage')
|
|
@mock.patch.object(db, 'instance_destroy')
|
|
@mock.patch.object(db, 'constraint')
|
|
@mock.patch.object(objects.BlockDeviceMappingList, 'get_by_instance_uuid')
|
|
@mock.patch.object(objects.Instance, 'save')
|
|
@mock.patch.object(objects.BuildRequest, 'get_by_instance_uuid')
|
|
def test_delete_fast_if_host_not_set(self, mock_br_get, mock_save,
|
|
mock_bdm_get, mock_cons,
|
|
mock_inst_destroy,
|
|
mock_notify_legacy, mock_notify):
|
|
self.useFixture(nova_fixtures.AllServicesCurrent())
|
|
inst = self._create_instance_obj()
|
|
inst.host = ''
|
|
updates = {'progress': 0, 'task_state': task_states.DELETING}
|
|
|
|
mock_lookup = self.useFixture(
|
|
fixtures.MockPatchObject(self.compute_api,
|
|
'_lookup_instance')).mock
|
|
|
|
mock_lookup.return_value = (None, inst)
|
|
mock_bdm_get.return_value = objects.BlockDeviceMappingList()
|
|
mock_br_get.side_effect = exception.BuildRequestNotFound(
|
|
uuid=inst.uuid)
|
|
|
|
mock_cons.return_value = 'constraint'
|
|
delete_time = datetime.datetime(1955, 11, 5, 9, 30,
|
|
tzinfo=iso8601.UTC)
|
|
updates['deleted_at'] = delete_time
|
|
updates['deleted'] = True
|
|
fake_inst = fake_instance.fake_db_instance(**updates)
|
|
mock_inst_destroy.return_value = fake_inst
|
|
|
|
instance_uuid = inst.uuid
|
|
self.compute_api.delete(self.context, inst)
|
|
|
|
for k, v in updates.items():
|
|
self.assertEqual(inst[k], v)
|
|
|
|
mock_lookup.assert_called_once_with(self.context, instance_uuid)
|
|
mock_bdm_get.assert_called_once_with(self.context, instance_uuid)
|
|
mock_br_get.assert_called_once_with(self.context, instance_uuid)
|
|
mock_save.assert_called_once_with()
|
|
|
|
mock_notify_legacy.assert_has_calls([
|
|
mock.call(self.compute_api.notifier, self.context,
|
|
inst, 'delete.start'),
|
|
mock.call(self.compute_api.notifier, self.context,
|
|
inst, 'delete.end')])
|
|
mock_notify.assert_has_calls([
|
|
mock.call(self.context, inst, host='fake-mini',
|
|
source='nova-api', action='delete', phase='start'),
|
|
mock.call(self.context, inst, host='fake-mini',
|
|
source='nova-api', action='delete', phase='end')])
|
|
|
|
mock_cons.assert_called_once_with(host=mock.ANY)
|
|
mock_inst_destroy.assert_called_once_with(
|
|
self.context, instance_uuid, constraint='constraint',
|
|
hard_delete=False)
|
|
|
|
def _fake_do_delete(context, instance, bdms,
|
|
rservations=None, local=False):
|
|
pass
|
|
|
|
@mock.patch.object(compute_utils, 'notify_about_instance_action')
|
|
@mock.patch.object(objects.BlockDeviceMapping, 'destroy')
|
|
@mock.patch.object(cinder.API, 'detach')
|
|
@mock.patch.object(compute_utils, 'notify_about_instance_usage')
|
|
@mock.patch.object(neutron_api.API, 'deallocate_for_instance')
|
|
@mock.patch.object(context.RequestContext, 'elevated')
|
|
@mock.patch.object(objects.Instance, 'destroy')
|
|
def test_local_delete_with_deleted_volume(
|
|
self, mock_inst_destroy, mock_elevated, mock_dealloc,
|
|
mock_notify_legacy, mock_detach, mock_bdm_destroy, mock_notify):
|
|
bdms = [objects.BlockDeviceMapping(
|
|
**fake_block_device.FakeDbBlockDeviceDict(
|
|
{'id': 42, 'volume_id': 'volume_id',
|
|
'source_type': 'volume', 'destination_type': 'volume',
|
|
'delete_on_termination': False}))]
|
|
|
|
inst = self._create_instance_obj()
|
|
inst._context = self.context
|
|
mock_elevated.return_value = self.context
|
|
mock_detach.side_effect = exception.VolumeNotFound('volume_id')
|
|
|
|
self.compute_api._local_delete(self.context, inst, bdms,
|
|
'delete',
|
|
self._fake_do_delete)
|
|
|
|
mock_notify_legacy.assert_has_calls([
|
|
mock.call(self.compute_api.notifier, self.context,
|
|
inst, 'delete.start'),
|
|
mock.call(self.compute_api.notifier, self.context,
|
|
inst, 'delete.end')])
|
|
mock_notify.assert_has_calls([
|
|
mock.call(self.context, inst, host='fake-mini', source='nova-api',
|
|
action='delete', phase='start'),
|
|
mock.call(self.context, inst, host='fake-mini', source='nova-api',
|
|
action='delete', phase='end')])
|
|
|
|
mock_elevated.assert_has_calls([mock.call(), mock.call()])
|
|
mock_detach.assert_called_once_with(mock.ANY, 'volume_id', inst.uuid)
|
|
mock_bdm_destroy.assert_called_once_with()
|
|
mock_inst_destroy.assert_called_once_with()
|
|
|
|
mock_dealloc.assert_called_once_with(self.context, inst)
|
|
|
|
@mock.patch.object(compute_utils, 'notify_about_instance_action')
|
|
@mock.patch.object(objects.BlockDeviceMapping, 'destroy')
|
|
@mock.patch.object(cinder.API, 'detach')
|
|
@mock.patch.object(compute_utils, 'notify_about_instance_usage')
|
|
@mock.patch.object(neutron_api.API, 'deallocate_for_instance')
|
|
@mock.patch.object(context.RequestContext, 'elevated')
|
|
@mock.patch.object(objects.Instance, 'destroy')
|
|
@mock.patch.object(compute_utils, 'delete_arqs_if_needed')
|
|
def test_local_delete_for_arqs(
|
|
self, mock_del_arqs, mock_inst_destroy, mock_elevated,
|
|
mock_dealloc, mock_notify_legacy, mock_detach,
|
|
mock_bdm_destroy, mock_notify):
|
|
inst = self._create_instance_obj()
|
|
inst._context = self.context
|
|
mock_elevated.return_value = self.context
|
|
bdms = []
|
|
self.compute_api._local_delete(self.context, inst, bdms,
|
|
'delete', self._fake_do_delete)
|
|
mock_del_arqs.assert_called_once_with(self.context, inst)
|
|
|
|
@mock.patch.object(objects.BlockDeviceMapping, 'destroy')
|
|
def test_local_cleanup_bdm_volumes_stashed_connector(self, mock_destroy):
|
|
"""Tests that we call volume_api.terminate_connection when we found
|
|
a stashed connector in the bdm.connection_info dict.
|
|
"""
|
|
inst = self._create_instance_obj()
|
|
# create two fake bdms, one is a volume and one isn't, both will be
|
|
# destroyed but we only cleanup the volume bdm in cinder
|
|
conn_info = {'connector': {'host': inst.host}}
|
|
vol_bdm = objects.BlockDeviceMapping(self.context, id=1,
|
|
instance_uuid=inst.uuid,
|
|
volume_id=uuids.volume_id,
|
|
source_type='volume',
|
|
destination_type='volume',
|
|
delete_on_termination=True,
|
|
connection_info=jsonutils.dumps(
|
|
conn_info
|
|
),
|
|
attachment_id=None,)
|
|
loc_bdm = objects.BlockDeviceMapping(self.context, id=2,
|
|
instance_uuid=inst.uuid,
|
|
volume_id=uuids.volume_id2,
|
|
source_type='blank',
|
|
destination_type='local')
|
|
bdms = objects.BlockDeviceMappingList(objects=[vol_bdm, loc_bdm])
|
|
|
|
@mock.patch.object(self.compute_api.volume_api, 'terminate_connection')
|
|
@mock.patch.object(self.compute_api.volume_api, 'detach')
|
|
@mock.patch.object(self.compute_api.volume_api, 'delete')
|
|
@mock.patch.object(self.context, 'elevated', return_value=self.context)
|
|
def do_test(self, mock_elevated, mock_delete,
|
|
mock_detach, mock_terminate):
|
|
self.compute_api._local_cleanup_bdm_volumes(
|
|
bdms, inst, self.context)
|
|
mock_terminate.assert_called_once_with(
|
|
self.context, uuids.volume_id, conn_info['connector'])
|
|
mock_detach.assert_called_once_with(
|
|
self.context, uuids.volume_id, inst.uuid)
|
|
mock_delete.assert_called_once_with(self.context, uuids.volume_id)
|
|
self.assertEqual(2, mock_destroy.call_count)
|
|
|
|
do_test(self)
|
|
|
|
@mock.patch.object(objects.BlockDeviceMapping, 'destroy')
|
|
def test_local_cleanup_bdm_volumes_with_attach_id(self, mock_destroy):
|
|
"""Tests that we call volume_api.attachment_delete when we have an
|
|
attachment_id in the bdm.
|
|
"""
|
|
instance = self._create_instance_obj()
|
|
conn_info = {'connector': {'host': instance.host}}
|
|
vol_bdm = objects.BlockDeviceMapping(
|
|
self.context,
|
|
id=1,
|
|
instance_uuid=instance.uuid,
|
|
volume_id=uuids.volume_id,
|
|
source_type='volume',
|
|
destination_type='volume',
|
|
delete_on_termination=True,
|
|
connection_info=jsonutils.dumps(conn_info),
|
|
attachment_id=uuids.attachment_id)
|
|
bdms = objects.BlockDeviceMappingList(objects=[vol_bdm])
|
|
|
|
@mock.patch.object(self.compute_api.volume_api, 'delete')
|
|
@mock.patch.object(self.compute_api.volume_api, 'attachment_delete')
|
|
@mock.patch.object(self.context, 'elevated', return_value=self.context)
|
|
def do_test(self, mock_elevated, mock_attach_delete, mock_delete):
|
|
self.compute_api._local_cleanup_bdm_volumes(
|
|
bdms, instance, self.context)
|
|
|
|
mock_attach_delete.assert_called_once_with(
|
|
self.context, vol_bdm.attachment_id)
|
|
mock_delete.assert_called_once_with(
|
|
self.context, vol_bdm.volume_id)
|
|
mock_destroy.assert_called_once_with()
|
|
do_test(self)
|
|
|
|
@mock.patch.object(objects.BlockDeviceMapping, 'destroy')
|
|
def test_local_cleanup_bdm_volumes_stashed_connector_host_none(
|
|
self, mock_destroy):
|
|
"""Tests that we call volume_api.terminate_connection when we found
|
|
a stashed connector in the bdm.connection_info dict.
|
|
|
|
This tests the case where:
|
|
|
|
1) the instance host is None
|
|
2) the instance vm_state is one where we expect host to be None
|
|
|
|
We allow a mismatch of the host in this situation if the instance is
|
|
in a state where we expect its host to have been set to None, such
|
|
as ERROR or SHELVED_OFFLOADED.
|
|
"""
|
|
params = dict(host=None, vm_state=vm_states.ERROR)
|
|
inst = self._create_instance_obj(params=params)
|
|
conn_info = {'connector': {'host': 'orig-host'}}
|
|
vol_bdm = objects.BlockDeviceMapping(self.context, id=1,
|
|
instance_uuid=inst.uuid,
|
|
volume_id=uuids.volume_id,
|
|
source_type='volume',
|
|
destination_type='volume',
|
|
delete_on_termination=True,
|
|
connection_info=jsonutils.dumps(
|
|
conn_info),
|
|
attachment_id=None)
|
|
bdms = objects.BlockDeviceMappingList(objects=[vol_bdm])
|
|
|
|
@mock.patch.object(self.compute_api.volume_api, 'terminate_connection')
|
|
@mock.patch.object(self.compute_api.volume_api, 'detach')
|
|
@mock.patch.object(self.compute_api.volume_api, 'delete')
|
|
@mock.patch.object(self.context, 'elevated', return_value=self.context)
|
|
def do_test(self, mock_elevated, mock_delete,
|
|
mock_detach, mock_terminate):
|
|
self.compute_api._local_cleanup_bdm_volumes(
|
|
bdms, inst, self.context)
|
|
mock_terminate.assert_called_once_with(
|
|
self.context, uuids.volume_id, conn_info['connector'])
|
|
mock_detach.assert_called_once_with(
|
|
self.context, uuids.volume_id, inst.uuid)
|
|
mock_delete.assert_called_once_with(self.context, uuids.volume_id)
|
|
mock_destroy.assert_called_once_with()
|
|
|
|
do_test(self)
|
|
|
|
def test_delete_disabled(self):
|
|
# If 'disable_terminate' is True, log output is executed only and
|
|
# just return immediately.
|
|
inst = self._create_instance_obj()
|
|
inst.disable_terminate = True
|
|
self.compute_api.delete(self.context, inst)
|
|
|
|
@mock.patch.object(objects.Instance, 'save',
|
|
side_effect=test.TestingException)
|
|
@mock.patch.object(objects.BlockDeviceMappingList, 'get_by_instance_uuid',
|
|
return_value=objects.BlockDeviceMappingList())
|
|
def test_delete_soft_rollback(self, mock_get, mock_save):
|
|
inst = self._create_instance_obj()
|
|
|
|
delete_time = datetime.datetime(1955, 11, 5)
|
|
self.useFixture(utils_fixture.TimeFixture(delete_time))
|
|
|
|
self.assertRaises(test.TestingException,
|
|
self.compute_api.soft_delete, self.context, inst)
|
|
mock_get.assert_called_once_with(self.context, inst.uuid)
|
|
mock_save.assert_called_once_with()
|
|
|
|
@mock.patch.object(objects.BuildRequest, 'get_by_instance_uuid')
|
|
def test_attempt_delete_of_buildrequest_success(self, mock_get_by_inst):
|
|
build_req_mock = mock.MagicMock()
|
|
mock_get_by_inst.return_value = build_req_mock
|
|
|
|
inst = self._create_instance_obj()
|
|
self.assertTrue(
|
|
self.compute_api._attempt_delete_of_buildrequest(self.context,
|
|
inst))
|
|
self.assertTrue(build_req_mock.destroy.called)
|
|
|
|
@mock.patch.object(objects.BuildRequest, 'get_by_instance_uuid')
|
|
def test_attempt_delete_of_buildrequest_not_found(self, mock_get_by_inst):
|
|
mock_get_by_inst.side_effect = exception.BuildRequestNotFound(
|
|
uuid='fake')
|
|
|
|
inst = self._create_instance_obj()
|
|
self.assertFalse(
|
|
self.compute_api._attempt_delete_of_buildrequest(self.context,
|
|
inst))
|
|
|
|
def test_attempt_delete_of_buildrequest_already_deleted(self):
|
|
inst = self._create_instance_obj()
|
|
build_req_mock = mock.MagicMock()
|
|
build_req_mock.destroy.side_effect = exception.BuildRequestNotFound(
|
|
uuid='fake')
|
|
with mock.patch.object(objects.BuildRequest, 'get_by_instance_uuid',
|
|
return_value=build_req_mock):
|
|
self.assertFalse(
|
|
self.compute_api._attempt_delete_of_buildrequest(self.context,
|
|
inst))
|
|
self.assertTrue(build_req_mock.destroy.called)
|
|
|
|
def test_delete_while_booting_buildreq_not_deleted(self):
|
|
self.useFixture(nova_fixtures.AllServicesCurrent())
|
|
inst = self._create_instance_obj()
|
|
with mock.patch.object(self.compute_api,
|
|
'_attempt_delete_of_buildrequest',
|
|
return_value=False):
|
|
self.assertFalse(
|
|
self.compute_api._delete_while_booting(self.context, inst))
|
|
|
|
def test_delete_while_booting_buildreq_deleted_instance_none(self):
|
|
self.useFixture(nova_fixtures.AllServicesCurrent())
|
|
inst = self._create_instance_obj()
|
|
|
|
@mock.patch.object(self.compute_api, '_attempt_delete_of_buildrequest',
|
|
return_value=True)
|
|
@mock.patch.object(self.compute_api, '_lookup_instance',
|
|
return_value=(None, None))
|
|
def test(mock_lookup, mock_attempt):
|
|
self.assertTrue(
|
|
self.compute_api._delete_while_booting(self.context,
|
|
inst))
|
|
|
|
test()
|
|
|
|
def test_delete_while_booting_buildreq_deleted_instance_not_found(self):
|
|
self.useFixture(nova_fixtures.AllServicesCurrent())
|
|
inst = self._create_instance_obj()
|
|
|
|
@mock.patch.object(self.compute_api, '_attempt_delete_of_buildrequest',
|
|
return_value=True)
|
|
@mock.patch.object(self.compute_api, '_lookup_instance',
|
|
side_effect=exception.InstanceNotFound(
|
|
instance_id='fake'))
|
|
def test(mock_lookup, mock_attempt):
|
|
self.assertTrue(
|
|
self.compute_api._delete_while_booting(self.context,
|
|
inst))
|
|
|
|
test()
|
|
|
|
@mock.patch('nova.compute.utils.notify_about_instance_delete')
|
|
@mock.patch('nova.objects.Instance.destroy')
|
|
def test_delete_instance_from_cell0(self, destroy_mock, notify_mock):
|
|
"""Tests the case that the instance does not have a host and was not
|
|
deleted while building, so conductor put it into cell0 so the API has
|
|
to delete the instance from cell0.
|
|
"""
|
|
instance = self._create_instance_obj({'host': None})
|
|
cell0 = objects.CellMapping(uuid=objects.CellMapping.CELL0_UUID)
|
|
|
|
with test.nested(
|
|
mock.patch.object(self.compute_api, '_delete_while_booting',
|
|
return_value=False),
|
|
mock.patch.object(self.compute_api, '_lookup_instance',
|
|
return_value=(cell0, instance)),
|
|
) as (
|
|
_delete_while_booting, _lookup_instance,
|
|
):
|
|
self.compute_api._delete(
|
|
self.context, instance, 'delete', mock.NonCallableMock())
|
|
_delete_while_booting.assert_called_once_with(
|
|
self.context, instance)
|
|
_lookup_instance.assert_called_once_with(
|
|
self.context, instance.uuid)
|
|
notify_mock.assert_called_once_with(
|
|
self.compute_api.notifier, self.context, instance)
|
|
destroy_mock.assert_called_once_with()
|
|
|
|
@mock.patch.object(context, 'target_cell')
|
|
@mock.patch.object(objects.InstanceMapping, 'get_by_instance_uuid',
|
|
side_effect=exception.InstanceMappingNotFound(
|
|
uuid='fake'))
|
|
def test_lookup_instance_mapping_none(self, mock_map_get,
|
|
mock_target_cell):
|
|
instance = self._create_instance_obj()
|
|
with mock.patch.object(objects.Instance, 'get_by_uuid',
|
|
return_value=instance) as mock_inst_get:
|
|
|
|
cell, ret_instance = self.compute_api._lookup_instance(
|
|
self.context, instance.uuid)
|
|
self.assertEqual((None, instance), (cell, ret_instance))
|
|
mock_inst_get.assert_called_once_with(self.context, instance.uuid)
|
|
self.assertFalse(mock_target_cell.called)
|
|
|
|
@mock.patch.object(context, 'target_cell')
|
|
@mock.patch.object(objects.InstanceMapping, 'get_by_instance_uuid',
|
|
return_value=objects.InstanceMapping(cell_mapping=None))
|
|
def test_lookup_instance_cell_mapping_none(self, mock_map_get,
|
|
mock_target_cell):
|
|
instance = self._create_instance_obj()
|
|
with mock.patch.object(objects.Instance, 'get_by_uuid',
|
|
return_value=instance) as mock_inst_get:
|
|
|
|
cell, ret_instance = self.compute_api._lookup_instance(
|
|
self.context, instance.uuid)
|
|
self.assertEqual((None, instance), (cell, ret_instance))
|
|
mock_inst_get.assert_called_once_with(self.context, instance.uuid)
|
|
self.assertFalse(mock_target_cell.called)
|
|
|
|
@mock.patch.object(context, 'target_cell')
|
|
def test_lookup_instance_cell_mapping(self, mock_target_cell):
|
|
instance = self._create_instance_obj()
|
|
mock_target_cell.return_value.__enter__.return_value = self.context
|
|
|
|
inst_map = objects.InstanceMapping(
|
|
cell_mapping=objects.CellMapping(database_connection='',
|
|
transport_url='none'))
|
|
|
|
@mock.patch.object(objects.InstanceMapping, 'get_by_instance_uuid',
|
|
return_value=inst_map)
|
|
@mock.patch.object(objects.Instance, 'get_by_uuid',
|
|
return_value=instance)
|
|
def test(mock_inst_get, mock_map_get):
|
|
cell, ret_instance = self.compute_api._lookup_instance(
|
|
self.context, instance.uuid)
|
|
expected_cell = inst_map.cell_mapping
|
|
self.assertEqual((expected_cell, instance),
|
|
(cell, ret_instance))
|
|
mock_inst_get.assert_called_once_with(self.context, instance.uuid)
|
|
mock_target_cell.assert_called_once_with(self.context,
|
|
inst_map.cell_mapping)
|
|
|
|
test()
|
|
|
|
@mock.patch.object(objects.Migration, 'save')
|
|
@mock.patch.object(objects.Migration, 'get_by_instance_and_status')
|
|
@mock.patch.object(context.RequestContext, 'elevated')
|
|
def _test_confirm_resize(self, mock_elevated, mock_get, mock_save,
|
|
mig_ref_passed=False):
|
|
params = dict(vm_state=vm_states.RESIZED)
|
|
fake_inst = self._create_instance_obj(params=params)
|
|
fake_mig = objects.Migration._from_db_object(
|
|
self.context, objects.Migration(),
|
|
test_migration.fake_db_migration())
|
|
|
|
mock_record = self.useFixture(
|
|
fixtures.MockPatchObject(self.compute_api,
|
|
'_record_action_start')).mock
|
|
mock_confirm = self.useFixture(
|
|
fixtures.MockPatchObject(self.compute_api.compute_rpcapi,
|
|
'confirm_resize')).mock
|
|
|
|
mock_elevated.return_value = self.context
|
|
if not mig_ref_passed:
|
|
mock_get.return_value = fake_mig
|
|
|
|
def _check_mig(expected_task_state=None):
|
|
self.assertEqual('confirming', fake_mig.status)
|
|
|
|
mock_save.side_effect = _check_mig
|
|
|
|
if mig_ref_passed:
|
|
self.compute_api.confirm_resize(self.context, fake_inst,
|
|
migration=fake_mig)
|
|
else:
|
|
self.compute_api.confirm_resize(self.context, fake_inst)
|
|
|
|
mock_elevated.assert_called_once_with()
|
|
mock_save.assert_called_once_with()
|
|
mock_record.assert_called_once_with(self.context, fake_inst,
|
|
'confirmResize')
|
|
mock_confirm.assert_called_once_with(self.context, fake_inst, fake_mig,
|
|
'compute-source')
|
|
|
|
if not mig_ref_passed:
|
|
mock_get.assert_called_once_with(self.context, fake_inst['uuid'],
|
|
'finished')
|
|
|
|
def test_confirm_resize(self):
|
|
self._test_confirm_resize()
|
|
|
|
def test_confirm_resize_with_migration_ref(self):
|
|
self._test_confirm_resize(mig_ref_passed=True)
|
|
|
|
@mock.patch('nova.virt.hardware.numa_get_constraints')
|
|
@mock.patch('nova.network.neutron.API.get_requested_resource_for_instance',
|
|
return_value=[])
|
|
@mock.patch('nova.availability_zones.get_host_availability_zone',
|
|
return_value='nova')
|
|
@mock.patch('nova.objects.Quotas.check_deltas')
|
|
@mock.patch('nova.objects.Migration.get_by_instance_and_status')
|
|
@mock.patch('nova.context.RequestContext.elevated')
|
|
@mock.patch('nova.objects.RequestSpec.get_by_instance_uuid')
|
|
def _test_revert_resize(
|
|
self, mock_get_reqspec, mock_elevated, mock_get_migration,
|
|
mock_check, mock_get_host_az, mock_get_requested_resources,
|
|
mock_get_numa, same_flavor):
|
|
params = dict(vm_state=vm_states.RESIZED)
|
|
fake_inst = self._create_instance_obj(params=params)
|
|
fake_inst.info_cache.network_info = model.NetworkInfo([
|
|
model.VIF(id=uuids.port1, profile={'allocation': uuids.rp})])
|
|
fake_mig = objects.Migration._from_db_object(
|
|
self.context, objects.Migration(),
|
|
test_migration.fake_db_migration())
|
|
fake_reqspec = objects.RequestSpec()
|
|
fake_reqspec.flavor = fake_inst.flavor
|
|
fake_numa_topology = objects.InstanceNUMATopology(cells=[
|
|
objects.InstanceNUMACell(
|
|
id=0, cpuset=set([0]), memory=512, pagesize=None,
|
|
cpu_pinning_raw=None, cpuset_reserved=None, cpu_policy=None,
|
|
cpu_thread_policy=None)])
|
|
|
|
if same_flavor:
|
|
fake_inst.old_flavor = fake_inst.flavor
|
|
else:
|
|
fake_inst.old_flavor = self._create_flavor(
|
|
id=200, flavorid='new-flavor-id', name='new_flavor',
|
|
disabled=False, extra_specs={'hw:numa_nodes': '1'})
|
|
|
|
mock_elevated.return_value = self.context
|
|
mock_get_migration.return_value = fake_mig
|
|
mock_get_reqspec.return_value = fake_reqspec
|
|
mock_get_numa.return_value = fake_numa_topology
|
|
|
|
def _check_reqspec():
|
|
if same_flavor:
|
|
assert_func = self.assertNotEqual
|
|
else:
|
|
assert_func = self.assertEqual
|
|
|
|
assert_func(fake_numa_topology, fake_reqspec.numa_topology)
|
|
assert_func(fake_inst.old_flavor, fake_reqspec.flavor)
|
|
|
|
def _check_state(expected_task_state=None):
|
|
self.assertEqual(task_states.RESIZE_REVERTING,
|
|
fake_inst.task_state)
|
|
|
|
def _check_mig():
|
|
self.assertEqual('reverting', fake_mig.status)
|
|
|
|
with test.nested(
|
|
mock.patch.object(fake_reqspec, 'save',
|
|
side_effect=_check_reqspec),
|
|
mock.patch.object(fake_inst, 'save', side_effect=_check_state),
|
|
mock.patch.object(fake_mig, 'save', side_effect=_check_mig),
|
|
mock.patch.object(self.compute_api, '_record_action_start'),
|
|
mock.patch.object(self.compute_api.compute_rpcapi, 'revert_resize')
|
|
) as (mock_reqspec_save, mock_inst_save, mock_mig_save,
|
|
mock_record_action, mock_revert_resize):
|
|
self.compute_api.revert_resize(self.context, fake_inst)
|
|
|
|
mock_elevated.assert_called_once_with()
|
|
mock_get_migration.assert_called_once_with(
|
|
self.context, fake_inst['uuid'], 'finished')
|
|
mock_inst_save.assert_called_once_with(expected_task_state=[None])
|
|
mock_mig_save.assert_called_once_with()
|
|
mock_get_reqspec.assert_called_once_with(
|
|
self.context, fake_inst.uuid)
|
|
if same_flavor:
|
|
# if we are not changing flavors through the revert, we
|
|
# shouldn't attempt to rebuild the NUMA topology since it won't
|
|
# have changed
|
|
mock_get_numa.assert_not_called()
|
|
else:
|
|
# not so if the flavor *has* changed though
|
|
mock_get_numa.assert_called_once_with(
|
|
fake_inst.old_flavor, mock.ANY)
|
|
mock_record_action.assert_called_once_with(self.context, fake_inst,
|
|
'revertResize')
|
|
mock_revert_resize.assert_called_once_with(
|
|
self.context, fake_inst, fake_mig, 'compute-dest',
|
|
mock_get_reqspec.return_value)
|
|
mock_get_requested_resources.assert_called_once_with(
|
|
self.context, fake_inst.uuid)
|
|
self.assertEqual(
|
|
[],
|
|
mock_get_reqspec.return_value.requested_resources)
|
|
|
|
def test_revert_resize(self):
|
|
self._test_revert_resize(same_flavor=False)
|
|
|
|
def test_revert_resize_same_flavor(self):
|
|
"""Test behavior when reverting a migration or a resize to the same
|
|
flavor.
|
|
"""
|
|
self._test_revert_resize(same_flavor=True)
|
|
|
|
@mock.patch('nova.network.neutron.API.get_requested_resource_for_instance')
|
|
@mock.patch('nova.availability_zones.get_host_availability_zone',
|
|
return_value='nova')
|
|
@mock.patch('nova.objects.Quotas.check_deltas')
|
|
@mock.patch('nova.objects.Migration.get_by_instance_and_status')
|
|
@mock.patch('nova.context.RequestContext.elevated')
|
|
@mock.patch('nova.objects.RequestSpec')
|
|
def test_revert_resize_concurrent_fail(
|
|
self, mock_reqspec, mock_elevated, mock_get_migration, mock_check,
|
|
mock_get_host_az, mock_get_requested_resources):
|
|
params = dict(vm_state=vm_states.RESIZED)
|
|
fake_inst = self._create_instance_obj(params=params)
|
|
fake_inst.info_cache.network_info = model.NetworkInfo([])
|
|
fake_inst.old_flavor = fake_inst.flavor
|
|
fake_mig = objects.Migration._from_db_object(
|
|
self.context, objects.Migration(),
|
|
test_migration.fake_db_migration())
|
|
|
|
mock_elevated.return_value = self.context
|
|
mock_get_migration.return_value = fake_mig
|
|
|
|
exc = exception.UnexpectedTaskStateError(
|
|
instance_uuid=fake_inst['uuid'],
|
|
actual={'task_state': task_states.RESIZE_REVERTING},
|
|
expected={'task_state': [None]})
|
|
|
|
with mock.patch.object(
|
|
fake_inst, 'save', side_effect=exc) as mock_inst_save:
|
|
self.assertRaises(exception.UnexpectedTaskStateError,
|
|
self.compute_api.revert_resize,
|
|
self.context,
|
|
fake_inst)
|
|
|
|
mock_elevated.assert_called_once_with()
|
|
mock_get_migration.assert_called_once_with(
|
|
self.context, fake_inst['uuid'], 'finished')
|
|
mock_inst_save.assert_called_once_with(expected_task_state=[None])
|
|
mock_get_requested_resources.assert_not_called()
|
|
|
|
@mock.patch('nova.compute.api.API.get_instance_host_status',
|
|
new=mock.Mock(return_value=fields_obj.HostStatus.UP))
|
|
@mock.patch('nova.virt.hardware.numa_get_constraints')
|
|
@mock.patch('nova.compute.api.API._allow_resize_to_same_host')
|
|
@mock.patch('nova.compute.utils.is_volume_backed_instance',
|
|
return_value=False)
|
|
@mock.patch('nova.compute.api.API._validate_flavor_image_nostatus')
|
|
@mock.patch('nova.objects.Migration')
|
|
@mock.patch.object(compute_api.API, '_record_action_start')
|
|
@mock.patch.object(quotas_obj.Quotas, 'limit_check_project_and_user')
|
|
@mock.patch.object(quotas_obj.Quotas, 'count_as_dict')
|
|
@mock.patch.object(objects.Instance, 'save')
|
|
@mock.patch.object(compute_utils, 'upsize_quota_delta')
|
|
@mock.patch.object(flavors, 'get_flavor_by_flavor_id')
|
|
@mock.patch.object(objects.RequestSpec, 'get_by_instance_uuid')
|
|
@mock.patch.object(objects.ComputeNodeList, 'get_all_by_host')
|
|
def _test_resize(self, mock_get_all_by_host,
|
|
mock_get_by_instance_uuid, mock_get_flavor, mock_upsize,
|
|
mock_inst_save, mock_count, mock_limit, mock_record,
|
|
mock_migration, mock_validate, mock_is_vol_backed,
|
|
mock_allow_resize_to_same_host,
|
|
mock_get_numa,
|
|
flavor_id_passed=True,
|
|
same_host=False, allow_same_host=False,
|
|
project_id=None,
|
|
same_flavor=False,
|
|
clean_shutdown=True,
|
|
host_name=None,
|
|
request_spec=True,
|
|
requested_destination=False,
|
|
allow_cross_cell_resize=False):
|
|
|
|
self.flags(allow_resize_to_same_host=allow_same_host)
|
|
mock_allow_resize_to_same_host.return_value = allow_same_host
|
|
|
|
params = {}
|
|
if project_id is not None:
|
|
# To test instance w/ different project id than context (admin)
|
|
params['project_id'] = project_id
|
|
fake_inst = self._create_instance_obj(params=params)
|
|
fake_numa_topology = objects.InstanceNUMATopology(cells=[
|
|
objects.InstanceNUMACell(
|
|
id=0, cpuset=set([0]), memory=512, pagesize=None,
|
|
cpu_pinning_raw=None, cpuset_reserved=None, cpu_policy=None,
|
|
cpu_thread_policy=None)])
|
|
|
|
mock_resize = self.useFixture(
|
|
fixtures.MockPatchObject(self.compute_api.compute_task_api,
|
|
'resize_instance')).mock
|
|
mock_get_numa.return_value = fake_numa_topology
|
|
|
|
if host_name:
|
|
mock_get_all_by_host.return_value = [objects.ComputeNode(
|
|
host=host_name, hypervisor_hostname='hypervisor_host')]
|
|
|
|
current_flavor = fake_inst.get_flavor()
|
|
if flavor_id_passed:
|
|
new_flavor = self._create_flavor(
|
|
id=200, flavorid='new-flavor-id', name='new_flavor',
|
|
disabled=False, extra_specs={'hw:numa_nodes': '1'})
|
|
if same_flavor:
|
|
new_flavor.id = current_flavor.id
|
|
mock_get_flavor.return_value = new_flavor
|
|
else:
|
|
new_flavor = current_flavor
|
|
|
|
if not (flavor_id_passed and same_flavor):
|
|
project_id, user_id = quotas_obj.ids_from_instance(self.context,
|
|
fake_inst)
|
|
if flavor_id_passed:
|
|
mock_upsize.return_value = {'cores': 0, 'ram': 0}
|
|
|
|
proj_count = {'instances': 1, 'cores': current_flavor.vcpus,
|
|
'ram': current_flavor.memory_mb}
|
|
user_count = proj_count.copy()
|
|
mock_count.return_value = {'project': proj_count,
|
|
'user': user_count}
|
|
|
|
def _check_state(expected_task_state=None):
|
|
self.assertEqual(task_states.RESIZE_PREP,
|
|
fake_inst.task_state)
|
|
self.assertEqual(fake_inst.progress, 0)
|
|
|
|
mock_inst_save.side_effect = _check_state
|
|
|
|
if allow_same_host:
|
|
filter_properties = {'ignore_hosts': []}
|
|
else:
|
|
filter_properties = {'ignore_hosts': [fake_inst['host']]}
|
|
|
|
if request_spec:
|
|
fake_spec = objects.RequestSpec()
|
|
if requested_destination:
|
|
cell1 = objects.CellMapping(uuid=uuids.cell1, name='cell1')
|
|
fake_spec.requested_destination = objects.Destination(
|
|
host='dummy_host', node='dummy_node', cell=cell1)
|
|
mock_get_by_instance_uuid.return_value = fake_spec
|
|
else:
|
|
mock_get_by_instance_uuid.side_effect = (
|
|
exception.RequestSpecNotFound(instance_uuid=fake_inst.id))
|
|
fake_spec = None
|
|
|
|
scheduler_hint = {'filter_properties': filter_properties}
|
|
|
|
mock_allow_cross_cell_resize = self.useFixture(
|
|
fixtures.MockPatchObject(
|
|
self.compute_api, '_allow_cross_cell_resize')).mock
|
|
mock_allow_cross_cell_resize.return_value = allow_cross_cell_resize
|
|
|
|
if flavor_id_passed:
|
|
self.compute_api.resize(self.context, fake_inst,
|
|
flavor_id='new-flavor-id',
|
|
clean_shutdown=clean_shutdown,
|
|
host_name=host_name)
|
|
else:
|
|
if request_spec:
|
|
self.compute_api.resize(self.context, fake_inst,
|
|
clean_shutdown=clean_shutdown,
|
|
host_name=host_name)
|
|
else:
|
|
self.assertRaises(exception.RequestSpecNotFound,
|
|
self.compute_api.resize,
|
|
self.context, fake_inst,
|
|
clean_shutdown=clean_shutdown,
|
|
host_name=host_name)
|
|
|
|
if request_spec:
|
|
if allow_same_host:
|
|
self.assertEqual([], fake_spec.ignore_hosts)
|
|
else:
|
|
self.assertEqual([fake_inst['host']], fake_spec.ignore_hosts)
|
|
|
|
if host_name is None:
|
|
self.assertIsNotNone(fake_spec.requested_destination)
|
|
else:
|
|
self.assertIn('host', fake_spec.requested_destination)
|
|
self.assertEqual(host_name,
|
|
fake_spec.requested_destination.host)
|
|
self.assertIn('node', fake_spec.requested_destination)
|
|
self.assertEqual('hypervisor_host',
|
|
fake_spec.requested_destination.node)
|
|
self.assertEqual(
|
|
allow_cross_cell_resize,
|
|
fake_spec.requested_destination.allow_cross_cell_move)
|
|
mock_allow_resize_to_same_host.assert_called_once()
|
|
|
|
if flavor_id_passed and not same_flavor:
|
|
mock_get_numa.assert_called_once_with(new_flavor, mock.ANY)
|
|
else:
|
|
mock_get_numa.assert_not_called()
|
|
|
|
if host_name:
|
|
mock_get_all_by_host.assert_called_once_with(
|
|
self.context, host_name, True)
|
|
|
|
if flavor_id_passed:
|
|
mock_get_flavor.assert_called_once_with('new-flavor-id',
|
|
read_deleted='no')
|
|
|
|
if not (flavor_id_passed and same_flavor):
|
|
if flavor_id_passed:
|
|
mock_upsize.assert_called_once_with(
|
|
test.MatchType(objects.Flavor),
|
|
test.MatchType(objects.Flavor))
|
|
image_meta = utils.get_image_from_system_metadata(
|
|
fake_inst.system_metadata)
|
|
if not same_flavor:
|
|
mock_validate.assert_called_once_with(
|
|
self.context, image_meta, new_flavor, root_bdm=None,
|
|
validate_pci=True)
|
|
# mock.ANY might be 'instances', 'cores', or 'ram'
|
|
# depending on how the deltas dict is iterated in check_deltas
|
|
mock_count.assert_called_once_with(
|
|
self.context, mock.ANY, project_id, user_id=user_id)
|
|
# The current and new flavor have the same cores/ram
|
|
req_cores = current_flavor.vcpus
|
|
req_ram = current_flavor.memory_mb
|
|
values = {'cores': req_cores, 'ram': req_ram}
|
|
mock_limit.assert_called_once_with(
|
|
self.context, user_values=values, project_values=values,
|
|
project_id=project_id, user_id=user_id)
|
|
|
|
mock_inst_save.assert_called_once_with(
|
|
expected_task_state=[None])
|
|
else:
|
|
# This is a migration
|
|
mock_validate.assert_not_called()
|
|
|
|
mock_migration.assert_not_called()
|
|
|
|
mock_get_by_instance_uuid.assert_called_once_with(self.context,
|
|
fake_inst.uuid)
|
|
|
|
if flavor_id_passed:
|
|
mock_record.assert_called_once_with(self.context, fake_inst,
|
|
'resize')
|
|
else:
|
|
if request_spec:
|
|
mock_record.assert_called_once_with(
|
|
self.context, fake_inst, 'migrate')
|
|
else:
|
|
mock_record.assert_not_called()
|
|
|
|
if request_spec:
|
|
mock_resize.assert_called_once_with(
|
|
self.context, fake_inst,
|
|
scheduler_hint=scheduler_hint,
|
|
flavor=test.MatchType(objects.Flavor),
|
|
clean_shutdown=clean_shutdown,
|
|
request_spec=fake_spec, do_cast=True)
|
|
else:
|
|
mock_resize.assert_not_called()
|
|
|
|
def _test_migrate(self, *args, **kwargs):
|
|
self._test_resize(*args, flavor_id_passed=False, **kwargs)
|
|
|
|
def test_resize(self):
|
|
self._test_resize()
|
|
|
|
def test_resize_same_host_and_allowed(self):
|
|
self._test_resize(same_host=True, allow_same_host=True)
|
|
|
|
def test_resize_same_host_and_not_allowed(self):
|
|
self._test_resize(same_host=True, allow_same_host=False)
|
|
|
|
def test_resize_different_project_id(self):
|
|
self._test_resize(project_id='different')
|
|
|
|
def test_resize_forced_shutdown(self):
|
|
self._test_resize(clean_shutdown=False)
|
|
|
|
def test_resize_allow_cross_cell_resize_true(self):
|
|
self._test_resize(allow_cross_cell_resize=True)
|
|
|
|
@mock.patch('nova.compute.api.API.get_instance_host_status',
|
|
new=mock.Mock(return_value=fields_obj.HostStatus.UP))
|
|
@mock.patch('nova.compute.flavors.get_flavor_by_flavor_id')
|
|
@mock.patch('nova.objects.Quotas.count_as_dict')
|
|
@mock.patch('nova.objects.Quotas.limit_check_project_and_user')
|
|
def test_resize_quota_check(self, mock_check, mock_count, mock_get):
|
|
self.flags(cores=1, group='quota')
|
|
self.flags(ram=2048, group='quota')
|
|
proj_count = {'instances': 1, 'cores': 1, 'ram': 1024}
|
|
user_count = proj_count.copy()
|
|
mock_count.return_value = {'project': proj_count,
|
|
'user': user_count}
|
|
|
|
cur_flavor = objects.Flavor(id=1, name='foo', vcpus=1, memory_mb=512,
|
|
root_gb=10, disabled=False, extra_specs={})
|
|
fake_inst = self._create_instance_obj()
|
|
fake_inst.flavor = cur_flavor
|
|
new_flavor = objects.Flavor(id=2, name='bar', vcpus=1, memory_mb=2048,
|
|
root_gb=10, disabled=False)
|
|
mock_get.return_value = new_flavor
|
|
mock_check.side_effect = exception.OverQuota(
|
|
overs=['ram'], quotas={'cores': 1, 'ram': 2048},
|
|
usages={'instances': 1, 'cores': 1, 'ram': 2048},
|
|
headroom={'ram': 2048})
|
|
|
|
self.assertRaises(exception.TooManyInstances, self.compute_api.resize,
|
|
self.context, fake_inst, flavor_id='new')
|
|
mock_check.assert_called_once_with(
|
|
self.context,
|
|
user_values={'cores': 1, 'ram': 2560},
|
|
project_values={'cores': 1, 'ram': 2560},
|
|
project_id=fake_inst.project_id, user_id=fake_inst.user_id)
|
|
|
|
def test_migrate(self):
|
|
self._test_migrate()
|
|
|
|
def test_migrate_same_host_and_allowed(self):
|
|
self._test_migrate(same_host=True, allow_same_host=True)
|
|
|
|
def test_migrate_same_host_and_not_allowed(self):
|
|
self._test_migrate(same_host=True, allow_same_host=False)
|
|
|
|
def test_migrate_different_project_id(self):
|
|
self._test_migrate(project_id='different')
|
|
|
|
def test_migrate_request_spec_not_found(self):
|
|
self._test_migrate(request_spec=False)
|
|
|
|
def test_migrate_with_requested_destination(self):
|
|
# RequestSpec has requested_destination
|
|
self._test_migrate(requested_destination=True)
|
|
|
|
def test_migrate_with_host_name(self):
|
|
self._test_migrate(host_name='target_host')
|
|
|
|
def test_migrate_with_host_name_allow_cross_cell_resize_true(self):
|
|
self._test_migrate(host_name='target_host',
|
|
allow_cross_cell_resize=True)
|
|
|
|
@mock.patch('nova.compute.api.API.get_instance_host_status',
|
|
new=mock.Mock(return_value=fields_obj.HostStatus.UP))
|
|
@mock.patch.object(objects.ComputeNodeList, 'get_all_by_host',
|
|
side_effect=exception.ComputeHostNotFound(
|
|
host='nonexistent_host'))
|
|
def test_migrate_nonexistent_host(self, mock_get_all_by_host):
|
|
fake_inst = self._create_instance_obj()
|
|
self.assertRaises(exception.ComputeHostNotFound,
|
|
self.compute_api.resize, self.context,
|
|
fake_inst, host_name='nonexistent_host')
|
|
|
|
@mock.patch('nova.compute.api.API.get_instance_host_status',
|
|
new=mock.Mock(return_value=fields_obj.HostStatus.UP))
|
|
@mock.patch.object(objects.Instance, 'save')
|
|
@mock.patch.object(compute_api.API, '_record_action_start')
|
|
@mock.patch.object(quotas_obj.Quotas, 'limit_check_project_and_user')
|
|
@mock.patch.object(quotas_obj.Quotas, 'count_as_dict')
|
|
@mock.patch.object(flavors, 'get_flavor_by_flavor_id')
|
|
def test_resize_invalid_flavor_fails(self, mock_get_flavor, mock_count,
|
|
mock_limit, mock_record, mock_save):
|
|
mock_resize = self.useFixture(fixtures.MockPatchObject(
|
|
self.compute_api.compute_task_api, 'resize_instance')).mock
|
|
|
|
fake_inst = self._create_instance_obj()
|
|
exc = exception.FlavorNotFound(flavor_id='flavor-id')
|
|
mock_get_flavor.side_effect = exc
|
|
|
|
self.assertRaises(exception.FlavorNotFound,
|
|
self.compute_api.resize, self.context,
|
|
fake_inst, flavor_id='flavor-id')
|
|
mock_get_flavor.assert_called_once_with('flavor-id', read_deleted='no')
|
|
# Should never reach these.
|
|
mock_count.assert_not_called()
|
|
mock_limit.assert_not_called()
|
|
mock_record.assert_not_called()
|
|
mock_resize.assert_not_called()
|
|
mock_save.assert_not_called()
|
|
|
|
@mock.patch('nova.compute.api.API.get_instance_host_status',
|
|
new=mock.Mock(return_value=fields_obj.HostStatus.UP))
|
|
@mock.patch.object(objects.Instance, 'save')
|
|
@mock.patch.object(compute_api.API, '_record_action_start')
|
|
@mock.patch.object(quotas_obj.Quotas, 'limit_check_project_and_user')
|
|
@mock.patch.object(quotas_obj.Quotas, 'count_as_dict')
|
|
@mock.patch.object(flavors, 'get_flavor_by_flavor_id')
|
|
def test_resize_disabled_flavor_fails(self, mock_get_flavor, mock_count,
|
|
mock_limit, mock_record, mock_save):
|
|
mock_resize = self.useFixture(fixtures.MockPatchObject(
|
|
self.compute_api.compute_task_api, 'resize_instance')).mock
|
|
|
|
fake_inst = self._create_instance_obj()
|
|
fake_flavor = self._create_flavor(id=200, flavorid='flavor-id',
|
|
name='foo', disabled=True)
|
|
mock_get_flavor.return_value = fake_flavor
|
|
|
|
self.assertRaises(exception.FlavorNotFound,
|
|
self.compute_api.resize, self.context,
|
|
fake_inst, flavor_id='flavor-id')
|
|
mock_get_flavor.assert_called_once_with('flavor-id', read_deleted='no')
|
|
# Should never reach these.
|
|
mock_count.assert_not_called()
|
|
mock_limit.assert_not_called()
|
|
mock_record.assert_not_called()
|
|
mock_resize.assert_not_called()
|
|
mock_save.assert_not_called()
|
|
|
|
@mock.patch('nova.compute.api.API.get_instance_host_status',
|
|
new=mock.Mock(return_value=fields_obj.HostStatus.UP))
|
|
@mock.patch.object(flavors, 'get_flavor_by_flavor_id')
|
|
def test_resize_to_zero_disk_flavor_fails(self, get_flavor_by_flavor_id):
|
|
fake_inst = self._create_instance_obj()
|
|
fake_flavor = self._create_flavor(id=200, flavorid='flavor-id',
|
|
name='foo', root_gb=0)
|
|
|
|
get_flavor_by_flavor_id.return_value = fake_flavor
|
|
|
|
with mock.patch.object(compute_utils, 'is_volume_backed_instance',
|
|
return_value=False):
|
|
self.assertRaises(exception.CannotResizeDisk,
|
|
self.compute_api.resize, self.context,
|
|
fake_inst, flavor_id='flavor-id')
|
|
|
|
@mock.patch('nova.compute.api.API.get_instance_host_status',
|
|
new=mock.Mock(return_value=fields_obj.HostStatus.UP))
|
|
@mock.patch('nova.compute.api.API._validate_flavor_image_nostatus')
|
|
@mock.patch.object(objects.RequestSpec, 'get_by_instance_uuid')
|
|
@mock.patch('nova.compute.api.API._record_action_start')
|
|
@mock.patch('nova.conductor.conductor_api.ComputeTaskAPI.resize_instance')
|
|
@mock.patch.object(flavors, 'get_flavor_by_flavor_id')
|
|
def test_resize_to_zero_disk_flavor_volume_backed(self,
|
|
get_flavor_by_flavor_id,
|
|
resize_instance_mock,
|
|
record_mock,
|
|
get_by_inst,
|
|
validate_mock):
|
|
params = dict(image_ref='')
|
|
fake_inst = self._create_instance_obj(params=params)
|
|
|
|
fake_flavor = self._create_flavor(id=200, flavorid='flavor-id',
|
|
name='foo', root_gb=0)
|
|
|
|
get_flavor_by_flavor_id.return_value = fake_flavor
|
|
|
|
@mock.patch.object(compute_utils, 'is_volume_backed_instance',
|
|
return_value=True)
|
|
@mock.patch.object(fake_inst, 'save')
|
|
def do_test(mock_save, mock_volume):
|
|
self.compute_api.resize(self.context, fake_inst,
|
|
flavor_id='flavor-id')
|
|
mock_volume.assert_called_once_with(self.context, fake_inst)
|
|
|
|
do_test()
|
|
|
|
@mock.patch('nova.compute.api.API.get_instance_host_status',
|
|
new=mock.Mock(return_value=fields_obj.HostStatus.UP))
|
|
@mock.patch.object(objects.Instance, 'save')
|
|
@mock.patch.object(compute_api.API, '_record_action_start')
|
|
@mock.patch.object(quotas_obj.Quotas,
|
|
'limit_check_project_and_user')
|
|
@mock.patch.object(quotas_obj.Quotas, 'count_as_dict')
|
|
@mock.patch.object(compute_utils, 'upsize_quota_delta')
|
|
@mock.patch.object(flavors, 'get_flavor_by_flavor_id')
|
|
def test_resize_quota_exceeds_fails(self, mock_get_flavor, mock_upsize,
|
|
mock_count, mock_limit, mock_record,
|
|
mock_save):
|
|
mock_resize = self.useFixture(fixtures.MockPatchObject(
|
|
self.compute_api.compute_task_api, 'resize_instance')).mock
|
|
|
|
fake_inst = self._create_instance_obj()
|
|
fake_flavor = self._create_flavor(id=200, flavorid='flavor-id',
|
|
name='foo', disabled=False)
|
|
mock_get_flavor.return_value = fake_flavor
|
|
deltas = dict(cores=0)
|
|
mock_upsize.return_value = deltas
|
|
quotas = {'cores': 0}
|
|
overs = ['cores']
|
|
over_quota_args = dict(quotas=quotas,
|
|
usages={'instances': 1, 'cores': 1, 'ram': 512},
|
|
overs=overs)
|
|
|
|
proj_count = {'instances': 1, 'cores': fake_inst.flavor.vcpus,
|
|
'ram': fake_inst.flavor.memory_mb}
|
|
user_count = proj_count.copy()
|
|
mock_count.return_value = {'project': proj_count, 'user': user_count}
|
|
|
|
req_cores = fake_inst.flavor.vcpus
|
|
req_ram = fake_inst.flavor.memory_mb
|
|
values = {'cores': req_cores, 'ram': req_ram}
|
|
mock_limit.side_effect = exception.OverQuota(**over_quota_args)
|
|
|
|
self.assertRaises(exception.TooManyInstances,
|
|
self.compute_api.resize, self.context,
|
|
fake_inst, flavor_id='flavor-id')
|
|
|
|
mock_save.assert_not_called()
|
|
mock_get_flavor.assert_called_once_with('flavor-id', read_deleted='no')
|
|
mock_upsize.assert_called_once_with(test.MatchType(objects.Flavor),
|
|
test.MatchType(objects.Flavor))
|
|
# mock.ANY might be 'instances', 'cores', or 'ram'
|
|
# depending on how the deltas dict is iterated in check_deltas
|
|
mock_count.assert_called_once_with(self.context, mock.ANY,
|
|
fake_inst.project_id,
|
|
user_id=fake_inst.user_id)
|
|
mock_limit.assert_called_once_with(self.context, user_values=values,
|
|
project_values=values,
|
|
project_id=fake_inst.project_id,
|
|
user_id=fake_inst.user_id)
|
|
# Should never reach these.
|
|
mock_record.assert_not_called()
|
|
mock_resize.assert_not_called()
|
|
|
|
@mock.patch('nova.compute.api.API.get_instance_host_status',
|
|
new=mock.Mock(return_value=fields_obj.HostStatus.UP))
|
|
@mock.patch.object(flavors, 'get_flavor_by_flavor_id')
|
|
@mock.patch.object(compute_utils, 'upsize_quota_delta')
|
|
@mock.patch.object(quotas_obj.Quotas, 'count_as_dict')
|
|
@mock.patch.object(quotas_obj.Quotas, 'limit_check_project_and_user')
|
|
def test_resize_quota_exceeds_fails_instance(self, mock_check, mock_count,
|
|
mock_upsize, mock_flavor):
|
|
fake_inst = self._create_instance_obj()
|
|
fake_flavor = self._create_flavor(id=200, flavorid='flavor-id',
|
|
name='foo', disabled=False)
|
|
mock_flavor.return_value = fake_flavor
|
|
deltas = dict(cores=1, ram=1)
|
|
mock_upsize.return_value = deltas
|
|
quotas = {'instances': 1, 'cores': -1, 'ram': -1}
|
|
overs = ['ram']
|
|
over_quota_args = dict(quotas=quotas,
|
|
usages={'instances': 1, 'cores': 1, 'ram': 512},
|
|
overs=overs)
|
|
mock_check.side_effect = exception.OverQuota(**over_quota_args)
|
|
|
|
with mock.patch.object(fake_inst, 'save') as mock_save:
|
|
self.assertRaises(exception.TooManyInstances,
|
|
self.compute_api.resize, self.context,
|
|
fake_inst, flavor_id='flavor-id')
|
|
self.assertFalse(mock_save.called)
|
|
|
|
@mock.patch('nova.compute.api.API.get_instance_host_status',
|
|
new=mock.Mock(return_value=fields_obj.HostStatus.UP))
|
|
@mock.patch.object(flavors, 'get_flavor_by_flavor_id')
|
|
@mock.patch.object(objects.Quotas, 'count_as_dict')
|
|
@mock.patch.object(objects.Quotas, 'limit_check_project_and_user')
|
|
def test_resize_instance_quota_exceeds_with_multiple_resources(
|
|
self, mock_check, mock_count, mock_get_flavor):
|
|
quotas = {'cores': 1, 'ram': 512}
|
|
overs = ['cores', 'ram']
|
|
over_quota_args = dict(quotas=quotas,
|
|
usages={'instances': 1, 'cores': 1, 'ram': 512},
|
|
overs=overs)
|
|
|
|
proj_count = {'instances': 1, 'cores': 1, 'ram': 512}
|
|
user_count = proj_count.copy()
|
|
mock_count.return_value = {'project': proj_count, 'user': user_count}
|
|
mock_check.side_effect = exception.OverQuota(**over_quota_args)
|
|
mock_get_flavor.return_value = self._create_flavor(id=333,
|
|
vcpus=3,
|
|
memory_mb=1536)
|
|
try:
|
|
self.compute_api.resize(self.context, self._create_instance_obj(),
|
|
'fake_flavor_id')
|
|
except exception.TooManyInstances as e:
|
|
self.assertEqual('cores, ram', e.kwargs['overs'])
|
|
self.assertEqual('2, 1024', e.kwargs['req'])
|
|
self.assertEqual('1, 512', e.kwargs['used'])
|
|
self.assertEqual('1, 512', e.kwargs['allowed'])
|
|
mock_get_flavor.assert_called_once_with('fake_flavor_id',
|
|
read_deleted="no")
|
|
else:
|
|
self.fail("Exception not raised")
|
|
|
|
@mock.patch.object(compute_api.API, '_record_action_start')
|
|
@mock.patch.object(objects.Instance, 'save')
|
|
def test_pause(self, mock_save, mock_record):
|
|
# Ensure instance can be paused.
|
|
instance = self._create_instance_obj()
|
|
self.assertEqual(instance.vm_state, vm_states.ACTIVE)
|
|
self.assertIsNone(instance.task_state)
|
|
|
|
rpcapi = self.compute_api.compute_rpcapi
|
|
|
|
mock_pause = self.useFixture(
|
|
fixtures.MockPatchObject(rpcapi, 'pause_instance')).mock
|
|
|
|
with mock.patch.object(rpcapi, 'pause_instance') as mock_pause:
|
|
self.compute_api.pause(self.context, instance)
|
|
|
|
self.assertEqual(vm_states.ACTIVE, instance.vm_state)
|
|
self.assertEqual(task_states.PAUSING,
|
|
instance.task_state)
|
|
mock_save.assert_called_once_with(expected_task_state=[None])
|
|
mock_record.assert_called_once_with(self.context, instance,
|
|
instance_actions.PAUSE)
|
|
mock_pause.assert_called_once_with(self.context, instance)
|
|
|
|
def _test_pause_fails(self, vm_state):
|
|
params = dict(vm_state=vm_state)
|
|
instance = self._create_instance_obj(params=params)
|
|
self.assertIsNone(instance.task_state)
|
|
self.assertRaises(exception.InstanceInvalidState,
|
|
self.compute_api.pause,
|
|
self.context, instance)
|
|
|
|
def test_pause_fails_invalid_states(self):
|
|
invalid_vm_states = self._get_vm_states(set([vm_states.ACTIVE]))
|
|
for state in invalid_vm_states:
|
|
self._test_pause_fails(state)
|
|
|
|
@mock.patch.object(compute_api.API, '_record_action_start')
|
|
@mock.patch.object(objects.Instance, 'save')
|
|
def test_unpause(self, mock_save, mock_record):
|
|
# Ensure instance can be unpaused.
|
|
params = dict(vm_state=vm_states.PAUSED)
|
|
instance = self._create_instance_obj(params=params)
|
|
self.assertEqual(instance.vm_state, vm_states.PAUSED)
|
|
self.assertIsNone(instance.task_state)
|
|
|
|
rpcapi = self.compute_api.compute_rpcapi
|
|
|
|
with mock.patch.object(rpcapi, 'unpause_instance') as mock_unpause:
|
|
self.compute_api.unpause(self.context, instance)
|
|
|
|
self.assertEqual(vm_states.PAUSED, instance.vm_state)
|
|
self.assertEqual(task_states.UNPAUSING, instance.task_state)
|
|
mock_save.assert_called_once_with(expected_task_state=[None])
|
|
mock_record.assert_called_once_with(self.context, instance,
|
|
instance_actions.UNPAUSE)
|
|
mock_unpause.assert_called_once_with(self.context, instance)
|
|
|
|
def test_get_diagnostics_none_host(self):
|
|
instance = self._create_instance_obj()
|
|
instance.host = None
|
|
self.assertRaises(exception.InstanceNotReady,
|
|
self.compute_api.get_diagnostics,
|
|
self.context, instance)
|
|
|
|
def test_get_instance_diagnostics_none_host(self):
|
|
instance = self._create_instance_obj()
|
|
instance.host = None
|
|
self.assertRaises(exception.InstanceNotReady,
|
|
self.compute_api.get_instance_diagnostics,
|
|
self.context, instance)
|
|
|
|
@mock.patch.object(objects.ComputeNodeList, 'get_all_by_host')
|
|
def test_live_migrate_active_vm_state(self, mock_nodelist):
|
|
instance = self._create_instance_obj()
|
|
self._live_migrate_instance(instance)
|
|
|
|
@mock.patch.object(objects.ComputeNodeList, 'get_all_by_host')
|
|
def test_live_migrate_paused_vm_state(self, mock_nodelist):
|
|
paused_state = dict(vm_state=vm_states.PAUSED)
|
|
instance = self._create_instance_obj(params=paused_state)
|
|
self._live_migrate_instance(instance)
|
|
|
|
@mock.patch.object(objects.ComputeNodeList, 'get_all_by_host')
|
|
@mock.patch.object(compute_utils, 'add_instance_fault_from_exc')
|
|
@mock.patch.object(objects.RequestSpec, 'get_by_instance_uuid')
|
|
@mock.patch.object(objects.InstanceAction, 'action_start')
|
|
@mock.patch.object(objects.Instance, 'save')
|
|
def test_live_migrate_messaging_timeout(self, _save, _action, get_spec,
|
|
add_instance_fault_from_exc,
|
|
mock_nodelist):
|
|
instance = self._create_instance_obj()
|
|
api = conductor.api.ComputeTaskAPI
|
|
|
|
with mock.patch.object(api, 'live_migrate_instance',
|
|
side_effect=oslo_exceptions.MessagingTimeout):
|
|
self.assertRaises(oslo_exceptions.MessagingTimeout,
|
|
self.compute_api.live_migrate,
|
|
self.context, instance,
|
|
host_name='fake_dest_host',
|
|
block_migration=True, disk_over_commit=True)
|
|
add_instance_fault_from_exc.assert_called_once_with(
|
|
self.context,
|
|
instance,
|
|
mock.ANY)
|
|
|
|
@mock.patch.object(objects.RequestSpec, 'get_by_instance_uuid')
|
|
@mock.patch.object(objects.InstanceAction, 'action_start')
|
|
@mock.patch.object(objects.ComputeNodeList, 'get_all_by_host',
|
|
side_effect=exception.ComputeHostNotFound(
|
|
host='fake_host'))
|
|
def test_live_migrate_computehost_notfound(self, mock_nodelist,
|
|
mock_action,
|
|
mock_get_spec):
|
|
instance = self._create_instance_obj()
|
|
self.assertRaises(exception.ComputeHostNotFound,
|
|
self.compute_api.live_migrate,
|
|
self.context, instance,
|
|
host_name='fake_host',
|
|
block_migration='auto',
|
|
disk_over_commit=False)
|
|
self.assertIsNone(instance.task_state)
|
|
|
|
@mock.patch.object(objects.RequestSpec, 'get_by_instance_uuid')
|
|
@mock.patch.object(objects.Instance, 'save')
|
|
@mock.patch.object(objects.InstanceAction, 'action_start')
|
|
def _live_migrate_instance(self, instance, _save, _action, get_spec):
|
|
api = conductor.api.ComputeTaskAPI
|
|
fake_spec = objects.RequestSpec()
|
|
get_spec.return_value = fake_spec
|
|
with mock.patch.object(api, 'live_migrate_instance') as task:
|
|
self.compute_api.live_migrate(self.context, instance,
|
|
block_migration=True,
|
|
disk_over_commit=True,
|
|
host_name='fake_dest_host')
|
|
self.assertEqual(task_states.MIGRATING, instance.task_state)
|
|
task.assert_called_once_with(self.context, instance,
|
|
'fake_dest_host',
|
|
block_migration=True,
|
|
disk_over_commit=True,
|
|
request_spec=fake_spec,
|
|
async_=False)
|
|
|
|
def _get_volumes_for_test_swap_volume(self):
|
|
volumes = {}
|
|
volumes[uuids.old_volume] = {
|
|
'id': uuids.old_volume,
|
|
'display_name': 'old_volume',
|
|
'attach_status': 'attached',
|
|
'size': 5,
|
|
'status': 'in-use',
|
|
'multiattach': False,
|
|
'attachments': {uuids.vol_instance: {'attachment_id': 'fakeid'}}}
|
|
volumes[uuids.new_volume] = {
|
|
'id': uuids.new_volume,
|
|
'display_name': 'new_volume',
|
|
'attach_status': 'detached',
|
|
'size': 5,
|
|
'status': 'available',
|
|
'multiattach': False}
|
|
|
|
return volumes
|
|
|
|
def _get_instance_for_test_swap_volume(self):
|
|
return fake_instance.fake_instance_obj(None, **{
|
|
'vm_state': vm_states.ACTIVE,
|
|
'launched_at': timeutils.utcnow(),
|
|
'locked': False,
|
|
'availability_zone': 'fake_az',
|
|
'uuid': uuids.vol_instance,
|
|
'task_state': None})
|
|
|
|
def _test_swap_volume_for_precheck_with_exception(
|
|
self, exc, instance_update=None, volume_update=None):
|
|
volumes = self._get_volumes_for_test_swap_volume()
|
|
instance = self._get_instance_for_test_swap_volume()
|
|
if instance_update:
|
|
instance.update(instance_update)
|
|
if volume_update:
|
|
volumes[volume_update['target']].update(volume_update['value'])
|
|
|
|
self.assertRaises(exc, self.compute_api.swap_volume, self.context,
|
|
instance, volumes[uuids.old_volume],
|
|
volumes[uuids.new_volume])
|
|
self.assertEqual('in-use', volumes[uuids.old_volume]['status'])
|
|
self.assertEqual('available', volumes[uuids.new_volume]['status'])
|
|
|
|
def test_swap_volume_with_invalid_server_state(self):
|
|
# Should fail if VM state is not valid
|
|
self._test_swap_volume_for_precheck_with_exception(
|
|
exception.InstanceInvalidState,
|
|
instance_update={'vm_state': vm_states.BUILDING})
|
|
self._test_swap_volume_for_precheck_with_exception(
|
|
exception.InstanceInvalidState,
|
|
instance_update={'vm_state': vm_states.STOPPED})
|
|
self._test_swap_volume_for_precheck_with_exception(
|
|
exception.InstanceInvalidState,
|
|
instance_update={'vm_state': vm_states.SUSPENDED})
|
|
|
|
def test_swap_volume_with_another_server_volume(self):
|
|
# Should fail if old volume's instance_uuid is not that of the instance
|
|
self._test_swap_volume_for_precheck_with_exception(
|
|
exception.InvalidVolume,
|
|
volume_update={
|
|
'target': uuids.old_volume,
|
|
'value': {
|
|
'attachments': {
|
|
uuids.vol_instance_2: {'attachment_id': 'fakeid'}}}})
|
|
|
|
def test_swap_volume_with_new_volume_attached(self):
|
|
# Should fail if new volume is attached
|
|
self._test_swap_volume_for_precheck_with_exception(
|
|
exception.InvalidVolume,
|
|
volume_update={'target': uuids.new_volume,
|
|
'value': {'attach_status': 'attached'}})
|
|
|
|
def test_swap_volume_with_smaller_new_volume(self):
|
|
# Should fail if new volume is smaller than the old volume
|
|
self._test_swap_volume_for_precheck_with_exception(
|
|
exception.InvalidVolume,
|
|
volume_update={'target': uuids.new_volume,
|
|
'value': {'size': 4}})
|
|
|
|
def test_swap_volume_with_swap_volume_error(self):
|
|
self._test_swap_volume(expected_exception=AttributeError)
|
|
|
|
def test_swap_volume_volume_api_usage(self):
|
|
self._test_swap_volume()
|
|
|
|
def test_swap_volume_volume_api_usage_new_attach_flow(self):
|
|
self._test_swap_volume(attachment_id=uuids.attachment_id)
|
|
|
|
def test_swap_volume_with_swap_volume_error_new_attach_flow(self):
|
|
self._test_swap_volume(expected_exception=AttributeError,
|
|
attachment_id=uuids.attachment_id)
|
|
|
|
def test_swap_volume_new_vol_already_attached_new_flow(self):
|
|
self._test_swap_volume(attachment_id=uuids.attachment_id,
|
|
volume_already_attached=True)
|
|
|
|
def _test_swap_volume(self, expected_exception=None, attachment_id=None,
|
|
volume_already_attached=False):
|
|
volumes = self._get_volumes_for_test_swap_volume()
|
|
instance = self._get_instance_for_test_swap_volume()
|
|
|
|
def fake_vol_api_begin_detaching(context, volume_id):
|
|
self.assertTrue(uuidutils.is_uuid_like(volume_id))
|
|
volumes[volume_id]['status'] = 'detaching'
|
|
|
|
def fake_vol_api_roll_detaching(context, volume_id):
|
|
self.assertTrue(uuidutils.is_uuid_like(volume_id))
|
|
if volumes[volume_id]['status'] == 'detaching':
|
|
volumes[volume_id]['status'] = 'in-use'
|
|
|
|
def fake_vol_api_reserve(context, volume_id):
|
|
self.assertTrue(uuidutils.is_uuid_like(volume_id))
|
|
self.assertEqual('available', volumes[volume_id]['status'])
|
|
volumes[volume_id]['status'] = 'attaching'
|
|
|
|
def fake_vol_api_unreserve(context, volume_id):
|
|
self.assertTrue(uuidutils.is_uuid_like(volume_id))
|
|
if volumes[volume_id]['status'] == 'attaching':
|
|
volumes[volume_id]['status'] = 'available'
|
|
|
|
def fake_vol_api_attachment_create(context, volume_id, instance_id):
|
|
self.assertTrue(uuidutils.is_uuid_like(volume_id))
|
|
self.assertEqual('available', volumes[volume_id]['status'])
|
|
volumes[volume_id]['status'] = 'reserved'
|
|
return {'id': uuids.attachment_id}
|
|
|
|
def fake_vol_api_attachment_delete(context, attachment_id):
|
|
self.assertTrue(uuidutils.is_uuid_like(attachment_id))
|
|
if volumes[uuids.new_volume]['status'] == 'reserved':
|
|
volumes[uuids.new_volume]['status'] = 'available'
|
|
|
|
def fake_volume_is_attached(context, instance, volume_id):
|
|
if volume_already_attached:
|
|
raise exception.InvalidVolume(reason='Volume already attached')
|
|
else:
|
|
pass
|
|
|
|
@mock.patch.object(compute_api.API, '_record_action_start')
|
|
@mock.patch.object(self.compute_api.compute_rpcapi, 'swap_volume',
|
|
return_value=True)
|
|
@mock.patch.object(self.compute_api.volume_api, 'unreserve_volume',
|
|
side_effect=fake_vol_api_unreserve)
|
|
@mock.patch.object(self.compute_api.volume_api, 'attachment_delete',
|
|
side_effect=fake_vol_api_attachment_delete)
|
|
@mock.patch.object(self.compute_api.volume_api, 'reserve_volume',
|
|
side_effect=fake_vol_api_reserve)
|
|
@mock.patch.object(self.compute_api,
|
|
'_check_volume_already_attached_to_instance',
|
|
side_effect=fake_volume_is_attached)
|
|
@mock.patch.object(self.compute_api.volume_api, 'attachment_create',
|
|
side_effect=fake_vol_api_attachment_create)
|
|
@mock.patch.object(self.compute_api.volume_api, 'roll_detaching',
|
|
side_effect=fake_vol_api_roll_detaching)
|
|
@mock.patch.object(objects.BlockDeviceMapping,
|
|
'get_by_volume_and_instance')
|
|
@mock.patch.object(self.compute_api.volume_api, 'begin_detaching',
|
|
side_effect=fake_vol_api_begin_detaching)
|
|
def _do_test(mock_begin_detaching, mock_get_by_volume_and_instance,
|
|
mock_roll_detaching, mock_attachment_create,
|
|
mock_check_volume_attached, mock_reserve_volume,
|
|
mock_attachment_delete, mock_unreserve_volume,
|
|
mock_swap_volume, mock_record):
|
|
bdm = objects.BlockDeviceMapping(
|
|
**fake_block_device.FakeDbBlockDeviceDict(
|
|
{'no_device': False, 'volume_id': '1', 'boot_index': 0,
|
|
'connection_info': 'inf', 'device_name': '/dev/vda',
|
|
'source_type': 'volume', 'destination_type': 'volume',
|
|
'tag': None, 'attachment_id': attachment_id},
|
|
anon=True))
|
|
mock_get_by_volume_and_instance.return_value = bdm
|
|
if expected_exception:
|
|
mock_swap_volume.side_effect = AttributeError()
|
|
self.assertRaises(expected_exception,
|
|
self.compute_api.swap_volume, self.context,
|
|
instance, volumes[uuids.old_volume],
|
|
volumes[uuids.new_volume])
|
|
self.assertEqual('in-use', volumes[uuids.old_volume]['status'])
|
|
self.assertEqual('available',
|
|
volumes[uuids.new_volume]['status'])
|
|
# Make assertions about what was called if there was or was not
|
|
# a Cinder 3.44 style attachment provided.
|
|
if attachment_id is None:
|
|
# Old style attachment, so unreserve was called and
|
|
# attachment_delete was not called.
|
|
mock_unreserve_volume.assert_called_once_with(
|
|
self.context, uuids.new_volume)
|
|
mock_attachment_delete.assert_not_called()
|
|
else:
|
|
# New style attachment, so unreserve was not called and
|
|
# attachment_delete was called.
|
|
mock_unreserve_volume.assert_not_called()
|
|
mock_attachment_delete.assert_called_once_with(
|
|
self.context, attachment_id)
|
|
|
|
# Assert the call to the rpcapi.
|
|
mock_swap_volume.assert_called_once_with(
|
|
self.context, instance=instance,
|
|
old_volume_id=uuids.old_volume,
|
|
new_volume_id=uuids.new_volume,
|
|
new_attachment_id=attachment_id)
|
|
mock_record.assert_called_once_with(
|
|
self.context, instance, instance_actions.SWAP_VOLUME)
|
|
elif volume_already_attached:
|
|
self.assertRaises(exception.InvalidVolume,
|
|
self.compute_api.swap_volume, self.context,
|
|
instance, volumes[uuids.old_volume],
|
|
volumes[uuids.new_volume])
|
|
self.assertEqual('in-use', volumes[uuids.old_volume]['status'])
|
|
self.assertEqual('available',
|
|
volumes[uuids.new_volume]['status'])
|
|
mock_check_volume_attached.assert_called_once_with(
|
|
self.context, instance, uuids.new_volume)
|
|
mock_roll_detaching.assert_called_once_with(self.context,
|
|
uuids.old_volume)
|
|
else:
|
|
self.compute_api.swap_volume(self.context, instance,
|
|
volumes[uuids.old_volume],
|
|
volumes[uuids.new_volume])
|
|
# Make assertions about what was called if there was or was not
|
|
# a Cinder 3.44 style attachment provided.
|
|
if attachment_id is None:
|
|
# Old style attachment, so reserve was called and
|
|
# attachment_create was not called.
|
|
mock_reserve_volume.assert_called_once_with(
|
|
self.context, uuids.new_volume)
|
|
mock_attachment_create.assert_not_called()
|
|
mock_check_volume_attached.assert_not_called()
|
|
else:
|
|
# New style attachment, so reserve was not called and
|
|
# attachment_create was called.
|
|
mock_reserve_volume.assert_not_called()
|
|
mock_check_volume_attached.assert_called_once_with(
|
|
self.context, instance, uuids.new_volume)
|
|
mock_attachment_create.assert_called_once_with(
|
|
self.context, uuids.new_volume, instance.uuid)
|
|
|
|
# Assert the call to the rpcapi.
|
|
mock_swap_volume.assert_called_once_with(
|
|
self.context, instance=instance,
|
|
old_volume_id=uuids.old_volume,
|
|
new_volume_id=uuids.new_volume,
|
|
new_attachment_id=attachment_id)
|
|
mock_record.assert_called_once_with(
|
|
self.context, instance, instance_actions.SWAP_VOLUME)
|
|
|
|
_do_test()
|
|
|
|
def test_count_attachments_for_swap_not_found_and_readonly(self):
|
|
"""Tests that attachment records that aren't found are considered
|
|
read/write by default. Also tests that read-only attachments are
|
|
not counted.
|
|
"""
|
|
ctxt = context.get_admin_context()
|
|
volume = {
|
|
'attachments': {
|
|
uuids.server1: {
|
|
'attachment_id': uuids.attachment1
|
|
},
|
|
uuids.server2: {
|
|
'attachment_id': uuids.attachment2
|
|
}
|
|
}
|
|
}
|
|
|
|
def fake_attachment_get(_context, attachment_id):
|
|
if attachment_id == uuids.attachment1:
|
|
raise exception.VolumeAttachmentNotFound(
|
|
attachment_id=attachment_id)
|
|
return {'attach_mode': 'ro'}
|
|
|
|
with mock.patch.object(self.compute_api.volume_api, 'attachment_get',
|
|
side_effect=fake_attachment_get) as mock_get:
|
|
self.assertEqual(
|
|
1, self.compute_api._count_attachments_for_swap(ctxt, volume))
|
|
mock_get.assert_has_calls([
|
|
mock.call(ctxt, uuids.attachment1),
|
|
mock.call(ctxt, uuids.attachment2)], any_order=True)
|
|
|
|
@mock.patch('nova.volume.cinder.API.attachment_get',
|
|
new_callable=mock.NonCallableMock) # asserts not called
|
|
def test_count_attachments_for_swap_no_query(self, mock_attachment_get):
|
|
"""Tests that if the volume has <2 attachments, we don't query
|
|
the attachments for their attach_mode value.
|
|
"""
|
|
volume = {}
|
|
self.assertEqual(
|
|
0, self.compute_api._count_attachments_for_swap(
|
|
mock.sentinel.context, volume))
|
|
volume = {
|
|
'attachments': {
|
|
uuids.server: {
|
|
'attachment_id': uuids.attach1
|
|
}
|
|
}
|
|
}
|
|
self.assertEqual(
|
|
1, self.compute_api._count_attachments_for_swap(
|
|
mock.sentinel.context, volume))
|
|
|
|
@mock.patch.object(compute_utils, 'is_volume_backed_instance')
|
|
@mock.patch.object(objects.Instance, 'save')
|
|
@mock.patch.object(image_api.API, 'create')
|
|
@mock.patch.object(utils, 'get_image_from_system_metadata')
|
|
@mock.patch.object(compute_api.API, '_record_action_start')
|
|
def _test_snapshot_and_backup(self, mock_record, mock_get_image,
|
|
mock_create, mock_save, mock_is_volume,
|
|
is_snapshot=True, with_base_ref=False,
|
|
min_ram=None, min_disk=None,
|
|
create_fails=False,
|
|
instance_vm_state=vm_states.ACTIVE):
|
|
params = dict(locked=True)
|
|
instance = self._create_instance_obj(params=params)
|
|
instance.vm_state = instance_vm_state
|
|
|
|
# Test non-inheritable properties, 'user_id' should also not be
|
|
# carried from sys_meta into image property...since it should be set
|
|
# explicitly by _create_image() in compute api.
|
|
fake_image_meta = {
|
|
'name': 'base-name',
|
|
'disk_format': 'fake',
|
|
'container_format': 'fake',
|
|
'properties': {
|
|
'user_id': 'meow',
|
|
'foo': 'bar',
|
|
'blah': 'bug?',
|
|
'cache_in_nova': 'dropped',
|
|
'bittorrent': 'dropped',
|
|
'img_signature_hash_method': 'dropped',
|
|
'img_signature': 'dropped',
|
|
'img_signature_key_type': 'dropped',
|
|
'img_signature_certificate_uuid': 'dropped'
|
|
},
|
|
}
|
|
image_type = is_snapshot and 'snapshot' or 'backup'
|
|
sent_meta = {
|
|
'visibility': 'private',
|
|
'name': 'fake-name',
|
|
'disk_format': 'fake',
|
|
'container_format': 'fake',
|
|
'properties': {
|
|
'user_id': self.context.user_id,
|
|
'instance_uuid': instance.uuid,
|
|
'image_type': image_type,
|
|
'foo': 'bar',
|
|
'blah': 'bug?',
|
|
'cow': 'moo',
|
|
'cat': 'meow',
|
|
},
|
|
|
|
}
|
|
if is_snapshot:
|
|
if min_ram is not None:
|
|
fake_image_meta['min_ram'] = min_ram
|
|
sent_meta['min_ram'] = min_ram
|
|
if min_disk is not None:
|
|
fake_image_meta['min_disk'] = min_disk
|
|
sent_meta['min_disk'] = min_disk
|
|
sent_meta.pop('disk_format', None)
|
|
sent_meta.pop('container_format', None)
|
|
else:
|
|
sent_meta['properties']['backup_type'] = 'fake-backup-type'
|
|
|
|
extra_props = dict(cow='moo', cat='meow')
|
|
|
|
if not is_snapshot:
|
|
mock_is_volume.return_value = False
|
|
|
|
mock_get_image.return_value = fake_image_meta
|
|
|
|
fake_image = dict(id='fake-image-id')
|
|
if create_fails:
|
|
mock_create.side_effect = test.TestingException()
|
|
else:
|
|
mock_create.return_value = fake_image
|
|
|
|
def check_state(expected_task_state=None):
|
|
expected_state = (is_snapshot and
|
|
task_states.IMAGE_SNAPSHOT_PENDING or
|
|
task_states.IMAGE_BACKUP)
|
|
self.assertEqual(expected_state, instance.task_state)
|
|
|
|
if not create_fails:
|
|
mock_save.side_effect = check_state
|
|
|
|
if is_snapshot:
|
|
with mock.patch.object(self.compute_api.compute_rpcapi,
|
|
'snapshot_instance') as mock_snapshot:
|
|
if create_fails:
|
|
self.assertRaises(test.TestingException,
|
|
self.compute_api.snapshot,
|
|
self.context, instance, 'fake-name',
|
|
extra_properties=extra_props)
|
|
else:
|
|
res = self.compute_api.snapshot(
|
|
self.context, instance, 'fake-name',
|
|
extra_properties=extra_props)
|
|
mock_record.assert_called_once_with(
|
|
self.context, instance, instance_actions.CREATE_IMAGE)
|
|
mock_snapshot.assert_called_once_with(
|
|
self.context, instance, fake_image['id'])
|
|
else:
|
|
with mock.patch.object(self.compute_api.compute_rpcapi,
|
|
'backup_instance') as mock_backup:
|
|
if create_fails:
|
|
self.assertRaises(test.TestingException,
|
|
self.compute_api.backup,
|
|
self.context, instance,
|
|
'fake-name', 'fake-backup-type',
|
|
'fake-rotation',
|
|
extra_properties=extra_props)
|
|
else:
|
|
res = self.compute_api.backup(
|
|
self.context, instance, 'fake-name',
|
|
'fake-backup-type', 'fake-rotation',
|
|
extra_properties=extra_props)
|
|
mock_record.assert_called_once_with(
|
|
self.context, instance, instance_actions.BACKUP)
|
|
mock_backup.assert_called_once_with(
|
|
self.context, instance, fake_image['id'],
|
|
'fake-backup-type', 'fake-rotation')
|
|
|
|
mock_create.assert_called_once_with(self.context, sent_meta)
|
|
mock_get_image.assert_called_once_with(instance.system_metadata)
|
|
|
|
if not is_snapshot:
|
|
mock_is_volume.assert_called_once_with(self.context, instance)
|
|
else:
|
|
mock_is_volume.assert_not_called()
|
|
|
|
if not create_fails:
|
|
self.assertEqual(fake_image, res)
|
|
mock_save.assert_called_once_with(expected_task_state=[None])
|
|
|
|
def test_snapshot(self):
|
|
self._test_snapshot_and_backup()
|
|
|
|
def test_snapshot_fails(self):
|
|
self._test_snapshot_and_backup(create_fails=True)
|
|
|
|
def test_snapshot_invalid_state(self):
|
|
instance = self._create_instance_obj()
|
|
instance.vm_state = vm_states.ACTIVE
|
|
instance.task_state = task_states.IMAGE_SNAPSHOT
|
|
self.assertRaises(exception.InstanceInvalidState,
|
|
self.compute_api.snapshot,
|
|
self.context, instance, 'fake-name')
|
|
instance.vm_state = vm_states.ACTIVE
|
|
instance.task_state = task_states.IMAGE_BACKUP
|
|
self.assertRaises(exception.InstanceInvalidState,
|
|
self.compute_api.snapshot,
|
|
self.context, instance, 'fake-name')
|
|
instance.vm_state = vm_states.BUILDING
|
|
instance.task_state = None
|
|
self.assertRaises(exception.InstanceInvalidState,
|
|
self.compute_api.snapshot,
|
|
self.context, instance, 'fake-name')
|
|
|
|
@mock.patch.object(objects.Instance, 'save')
|
|
@mock.patch.object(compute_utils, 'create_image')
|
|
@mock.patch.object(compute_rpcapi.ComputeAPI,
|
|
'snapshot_instance')
|
|
def test_vm_deleting_while_creating_snapshot(self,
|
|
snapshot_instance,
|
|
_create_image, save):
|
|
instance = self._create_instance_obj()
|
|
save.side_effect = exception.UnexpectedDeletingTaskStateError(
|
|
"Exception")
|
|
_create_image.return_value = dict(id='fake-image-id')
|
|
with mock.patch.object(self.compute_api.image_api,
|
|
'delete') as image_delete:
|
|
self.assertRaises(exception.InstanceInvalidState,
|
|
self.compute_api.snapshot,
|
|
self.context,
|
|
instance, 'fake-image')
|
|
image_delete.assert_called_once_with(self.context,
|
|
'fake-image-id')
|
|
|
|
@mock.patch.object(objects.Instance, 'save')
|
|
@mock.patch.object(compute_utils, 'create_image')
|
|
@mock.patch.object(compute_rpcapi.ComputeAPI,
|
|
'snapshot_instance')
|
|
def test_vm_deleted_while_creating_snapshot(self,
|
|
snapshot_instance,
|
|
_create_image, save):
|
|
instance = self._create_instance_obj()
|
|
save.side_effect = exception.InstanceNotFound(
|
|
"Exception")
|
|
_create_image.return_value = dict(id='fake-image-id')
|
|
with mock.patch.object(self.compute_api.image_api,
|
|
'delete') as image_delete:
|
|
self.assertRaises(exception.InstanceInvalidState,
|
|
self.compute_api.snapshot,
|
|
self.context,
|
|
instance, 'fake-image')
|
|
image_delete.assert_called_once_with(self.context,
|
|
'fake-image-id')
|
|
|
|
@mock.patch.object(objects.Instance, 'save')
|
|
@mock.patch.object(compute_utils, 'create_image')
|
|
@mock.patch.object(compute_rpcapi.ComputeAPI,
|
|
'snapshot_instance')
|
|
def test_vm_deleted_while_snapshot_and_snapshot_delete_failed(self,
|
|
snapshot_instance,
|
|
_create_image, save):
|
|
instance = self._create_instance_obj()
|
|
save.side_effect = exception.InstanceNotFound(instance_id='fake')
|
|
_create_image.return_value = dict(id='fake-image-id')
|
|
with mock.patch.object(self.compute_api.image_api,
|
|
'delete') as image_delete:
|
|
image_delete.side_effect = test.TestingException()
|
|
self.assertRaises(exception.InstanceInvalidState,
|
|
self.compute_api.snapshot,
|
|
self.context,
|
|
instance, 'fake-image')
|
|
image_delete.assert_called_once_with(self.context,
|
|
'fake-image-id')
|
|
|
|
def test_snapshot_with_base_image_ref(self):
|
|
self._test_snapshot_and_backup(with_base_ref=True)
|
|
|
|
def test_snapshot_min_ram(self):
|
|
self._test_snapshot_and_backup(min_ram=42)
|
|
|
|
def test_snapshot_min_disk(self):
|
|
self._test_snapshot_and_backup(min_disk=42)
|
|
|
|
def test_backup(self):
|
|
for state in [vm_states.ACTIVE, vm_states.STOPPED,
|
|
vm_states.PAUSED, vm_states.SUSPENDED]:
|
|
self._test_snapshot_and_backup(is_snapshot=False,
|
|
instance_vm_state=state)
|
|
|
|
def test_backup_fails(self):
|
|
self._test_snapshot_and_backup(is_snapshot=False, create_fails=True)
|
|
|
|
def test_backup_invalid_state(self):
|
|
instance = self._create_instance_obj()
|
|
instance.vm_state = vm_states.ACTIVE
|
|
instance.task_state = task_states.IMAGE_SNAPSHOT
|
|
self.assertRaises(exception.InstanceInvalidState,
|
|
self.compute_api.backup,
|
|
self.context, instance, 'fake-name',
|
|
'fake', 'fake')
|
|
instance.vm_state = vm_states.ACTIVE
|
|
instance.task_state = task_states.IMAGE_BACKUP
|
|
self.assertRaises(exception.InstanceInvalidState,
|
|
self.compute_api.backup,
|
|
self.context, instance, 'fake-name',
|
|
'fake', 'fake')
|
|
instance.vm_state = vm_states.BUILDING
|
|
instance.task_state = None
|
|
self.assertRaises(exception.InstanceInvalidState,
|
|
self.compute_api.backup,
|
|
self.context, instance, 'fake-name',
|
|
'fake', 'fake')
|
|
|
|
def test_backup_with_base_image_ref(self):
|
|
self._test_snapshot_and_backup(is_snapshot=False,
|
|
with_base_ref=True)
|
|
|
|
def test_backup_volume_backed_instance(self):
|
|
instance = self._create_instance_obj()
|
|
|
|
with mock.patch.object(compute_utils, 'is_volume_backed_instance',
|
|
return_value=True) as mock_is_volume_backed:
|
|
self.assertRaises(exception.InvalidRequest,
|
|
self.compute_api.backup, self.context,
|
|
instance, 'fake-name', 'weekly',
|
|
3, extra_properties={})
|
|
mock_is_volume_backed.assert_called_once_with(self.context,
|
|
instance)
|
|
|
|
def _test_snapshot_volume_backed(self, quiesce_required=False,
|
|
quiesce_fails=False,
|
|
quiesce_unsupported=False,
|
|
vm_state=vm_states.ACTIVE,
|
|
snapshot_fails=False, limits=None):
|
|
fake_sys_meta = {'image_min_ram': '11',
|
|
'image_min_disk': '22',
|
|
'image_container_format': 'ami',
|
|
'image_disk_format': 'ami',
|
|
'image_ram_disk': 'fake_ram_disk_id',
|
|
'image_bdm_v2': 'True',
|
|
'image_block_device_mapping': '[]',
|
|
'image_mappings': '[]',
|
|
'image_cache_in_nova': 'True'}
|
|
if quiesce_required:
|
|
fake_sys_meta['image_os_require_quiesce'] = 'yes'
|
|
params = dict(locked=True, vm_state=vm_state,
|
|
system_metadata=fake_sys_meta)
|
|
instance = self._create_instance_obj(params=params)
|
|
instance['root_device_name'] = 'vda'
|
|
|
|
instance_bdms = []
|
|
|
|
expect_meta = {
|
|
'name': 'test-snapshot',
|
|
'properties': {'root_device_name': 'vda',
|
|
'ram_disk': 'fake_ram_disk_id'},
|
|
'size': 0,
|
|
'min_disk': '22',
|
|
'visibility': 'private',
|
|
'min_ram': '11',
|
|
}
|
|
if quiesce_required:
|
|
expect_meta['properties']['os_require_quiesce'] = 'yes'
|
|
|
|
quiesced = [False, False]
|
|
quiesce_expected = not (quiesce_unsupported or quiesce_fails) \
|
|
and vm_state == vm_states.ACTIVE
|
|
|
|
@classmethod
|
|
def fake_bdm_list_get_by_instance_uuid(cls, context, instance_uuid):
|
|
return obj_base.obj_make_list(context, cls(),
|
|
objects.BlockDeviceMapping, instance_bdms)
|
|
|
|
def fake_image_create(_self, context, image_meta, data=None):
|
|
self.assertThat(image_meta, matchers.DictMatches(expect_meta))
|
|
|
|
def fake_volume_create_snapshot(self, context, volume_id, name,
|
|
description):
|
|
if snapshot_fails:
|
|
raise exception.OverQuota(overs="snapshots")
|
|
return {'id': '%s-snapshot' % volume_id}
|
|
|
|
def fake_quiesce_instance(context, instance):
|
|
if quiesce_unsupported:
|
|
raise exception.InstanceQuiesceNotSupported(
|
|
instance_id=instance['uuid'], reason='unsupported')
|
|
if quiesce_fails:
|
|
raise oslo_exceptions.MessagingTimeout('quiece timeout')
|
|
quiesced[0] = True
|
|
|
|
def fake_unquiesce_instance(context, instance, mapping=None):
|
|
quiesced[1] = True
|
|
|
|
def fake_get_absolute_limits(context):
|
|
if limits is not None:
|
|
return limits
|
|
return {"totalSnapshotsUsed": 0, "maxTotalSnapshots": 10}
|
|
|
|
self.stub_out('nova.objects.BlockDeviceMappingList'
|
|
'.get_by_instance_uuid',
|
|
fake_bdm_list_get_by_instance_uuid)
|
|
self.stub_out('nova.image.glance.API.create', fake_image_create)
|
|
self.stub_out('nova.volume.cinder.API.get',
|
|
lambda self, context, volume_id:
|
|
{'id': volume_id, 'display_description': ''})
|
|
self.stub_out('nova.volume.cinder.API.create_snapshot_force',
|
|
fake_volume_create_snapshot)
|
|
self.useFixture(fixtures.MockPatchObject(
|
|
self.compute_api.compute_rpcapi, 'quiesce_instance',
|
|
side_effect=fake_quiesce_instance))
|
|
self.useFixture(fixtures.MockPatchObject(
|
|
self.compute_api.compute_rpcapi, 'unquiesce_instance',
|
|
side_effect=fake_unquiesce_instance))
|
|
fake_image.stub_out_image_service(self)
|
|
|
|
with test.nested(
|
|
mock.patch.object(compute_api.API, '_record_action_start'),
|
|
mock.patch.object(compute_utils, 'EventReporter')) as (
|
|
mock_record, mock_event):
|
|
# No block devices defined
|
|
self.compute_api.snapshot_volume_backed(
|
|
self.context, instance, 'test-snapshot')
|
|
|
|
mock_record.assert_called_once_with(self.context,
|
|
instance,
|
|
instance_actions.CREATE_IMAGE)
|
|
mock_event.assert_called_once_with(self.context,
|
|
'api_snapshot_instance',
|
|
CONF.host,
|
|
instance.uuid,
|
|
graceful_exit=False)
|
|
|
|
bdm = fake_block_device.FakeDbBlockDeviceDict(
|
|
{'no_device': False, 'volume_id': '1', 'boot_index': 0,
|
|
'connection_info': 'inf', 'device_name': '/dev/vda',
|
|
'source_type': 'volume', 'destination_type': 'volume',
|
|
'tag': None})
|
|
instance_bdms.append(bdm)
|
|
|
|
expect_meta['properties']['bdm_v2'] = True
|
|
expect_meta['properties']['block_device_mapping'] = []
|
|
expect_meta['properties']['block_device_mapping'].append(
|
|
{'guest_format': None, 'boot_index': 0, 'no_device': None,
|
|
'image_id': None, 'volume_id': None, 'disk_bus': None,
|
|
'volume_size': None, 'source_type': 'snapshot',
|
|
'device_type': None, 'snapshot_id': '1-snapshot',
|
|
'device_name': '/dev/vda',
|
|
'destination_type': 'volume', 'delete_on_termination': False,
|
|
'tag': None, 'volume_type': None})
|
|
|
|
limits_patcher = mock.patch.object(
|
|
self.compute_api.volume_api, 'get_absolute_limits',
|
|
side_effect=fake_get_absolute_limits)
|
|
limits_patcher.start()
|
|
self.addCleanup(limits_patcher.stop)
|
|
|
|
with test.nested(
|
|
mock.patch.object(compute_api.API, '_record_action_start'),
|
|
mock.patch.object(compute_utils, 'EventReporter')) as (
|
|
mock_record, mock_event):
|
|
# All the db_only fields and the volume ones are removed
|
|
if snapshot_fails:
|
|
self.assertRaises(exception.OverQuota,
|
|
self.compute_api.snapshot_volume_backed,
|
|
self.context, instance, "test-snapshot")
|
|
else:
|
|
self.compute_api.snapshot_volume_backed(
|
|
self.context, instance, 'test-snapshot')
|
|
|
|
self.assertEqual(quiesce_expected, quiesced[0])
|
|
self.assertEqual(quiesce_expected, quiesced[1])
|
|
|
|
mock_record.assert_called_once_with(self.context,
|
|
instance,
|
|
instance_actions.CREATE_IMAGE)
|
|
mock_event.assert_called_once_with(self.context,
|
|
'api_snapshot_instance',
|
|
CONF.host,
|
|
instance.uuid,
|
|
graceful_exit=False)
|
|
|
|
instance.system_metadata['image_mappings'] = jsonutils.dumps(
|
|
[{'virtual': 'ami', 'device': 'vda'},
|
|
{'device': 'vda', 'virtual': 'ephemeral0'},
|
|
{'device': 'vdb', 'virtual': 'swap'},
|
|
{'device': 'vdc', 'virtual': 'ephemeral1'}])[:255]
|
|
instance.system_metadata['image_block_device_mapping'] = (
|
|
jsonutils.dumps(
|
|
[{'source_type': 'snapshot', 'destination_type': 'volume',
|
|
'guest_format': None, 'device_type': 'disk', 'boot_index': 1,
|
|
'disk_bus': 'ide', 'device_name': '/dev/vdf',
|
|
'delete_on_termination': True, 'snapshot_id': 'snapshot-2',
|
|
'volume_id': None, 'volume_size': 100, 'image_id': None,
|
|
'no_device': None, 'volume_type': None}])[:255])
|
|
|
|
bdm = fake_block_device.FakeDbBlockDeviceDict(
|
|
{'no_device': False, 'volume_id': None, 'boot_index': -1,
|
|
'connection_info': 'inf', 'device_name': '/dev/vdh',
|
|
'source_type': 'blank', 'destination_type': 'local',
|
|
'guest_format': 'swap', 'delete_on_termination': True,
|
|
'tag': None, 'volume_type': None})
|
|
instance_bdms.append(bdm)
|
|
# The non-volume image mapping will go at the front of the list
|
|
# because the volume BDMs are processed separately.
|
|
expect_meta['properties']['block_device_mapping'].insert(0,
|
|
{'guest_format': 'swap', 'boot_index': -1, 'no_device': False,
|
|
'image_id': None, 'volume_id': None, 'disk_bus': None,
|
|
'volume_size': None, 'source_type': 'blank',
|
|
'device_type': None, 'snapshot_id': None,
|
|
'device_name': '/dev/vdh',
|
|
'destination_type': 'local', 'delete_on_termination': True,
|
|
'tag': None, 'volume_type': None})
|
|
|
|
quiesced = [False, False]
|
|
|
|
with test.nested(
|
|
mock.patch.object(compute_api.API, '_record_action_start'),
|
|
mock.patch.object(compute_utils, 'EventReporter')) as (
|
|
mock_record, mock_event):
|
|
# Check that the mappings from the image properties are not
|
|
# included
|
|
if snapshot_fails:
|
|
self.assertRaises(exception.OverQuota,
|
|
self.compute_api.snapshot_volume_backed,
|
|
self.context, instance, "test-snapshot")
|
|
else:
|
|
self.compute_api.snapshot_volume_backed(
|
|
self.context, instance, 'test-snapshot')
|
|
|
|
self.assertEqual(quiesce_expected, quiesced[0])
|
|
self.assertEqual(quiesce_expected, quiesced[1])
|
|
|
|
mock_record.assert_called_once_with(self.context,
|
|
instance,
|
|
instance_actions.CREATE_IMAGE)
|
|
mock_event.assert_called_once_with(self.context,
|
|
'api_snapshot_instance',
|
|
CONF.host,
|
|
instance.uuid,
|
|
graceful_exit=False)
|
|
|
|
def test_snapshot_volume_backed(self):
|
|
self._test_snapshot_volume_backed(quiesce_required=False,
|
|
quiesce_unsupported=False)
|
|
|
|
def test_snapshot_volume_backed_with_quiesce_unsupported(self):
|
|
self._test_snapshot_volume_backed(quiesce_required=True,
|
|
quiesce_unsupported=False)
|
|
|
|
def test_snaphost_volume_backed_with_quiesce_failure(self):
|
|
self.assertRaises(oslo_exceptions.MessagingTimeout,
|
|
self._test_snapshot_volume_backed,
|
|
quiesce_required=True,
|
|
quiesce_fails=True)
|
|
|
|
def test_snapshot_volume_backed_with_quiesce_create_snap_fails(self):
|
|
self._test_snapshot_volume_backed(quiesce_required=True,
|
|
snapshot_fails=True)
|
|
|
|
def test_snapshot_volume_backed_unlimited_quota(self):
|
|
"""Tests that there is unlimited quota on volume snapshots so we
|
|
don't perform a quota check.
|
|
"""
|
|
limits = {'maxTotalSnapshots': -1, 'totalSnapshotsUsed': 0}
|
|
self._test_snapshot_volume_backed(limits=limits)
|
|
|
|
def test_snapshot_volume_backed_over_quota_before_snapshot(self):
|
|
"""Tests that the up-front check on quota fails before actually
|
|
attempting to snapshot any volumes.
|
|
"""
|
|
limits = {'maxTotalSnapshots': 1, 'totalSnapshotsUsed': 1}
|
|
self.assertRaises(exception.OverQuota,
|
|
self._test_snapshot_volume_backed,
|
|
limits=limits)
|
|
|
|
def test_snapshot_volume_backed_with_quiesce_skipped(self):
|
|
self._test_snapshot_volume_backed(quiesce_required=False,
|
|
quiesce_unsupported=True)
|
|
|
|
def test_snapshot_volume_backed_with_quiesce_exception(self):
|
|
self.assertRaises(exception.NovaException,
|
|
self._test_snapshot_volume_backed,
|
|
quiesce_required=True,
|
|
quiesce_unsupported=True)
|
|
|
|
def test_snapshot_volume_backed_with_suspended(self):
|
|
self._test_snapshot_volume_backed(quiesce_required=False,
|
|
quiesce_unsupported=True,
|
|
vm_state=vm_states.SUSPENDED)
|
|
|
|
def test_snapshot_volume_backed_with_pause(self):
|
|
self._test_snapshot_volume_backed(quiesce_required=False,
|
|
quiesce_unsupported=True,
|
|
vm_state=vm_states.PAUSED)
|
|
|
|
@mock.patch.object(context, 'set_target_cell')
|
|
@mock.patch.object(objects.BlockDeviceMapping, 'get_by_volume')
|
|
def test_get_bdm_by_volume_id(self, mock_get_by_volume,
|
|
mock_target_cell):
|
|
fake_cells = [mock.sentinel.cell0, mock.sentinel.cell1]
|
|
|
|
mock_get_by_volume.side_effect = [
|
|
exception.VolumeBDMNotFound(volume_id=mock.sentinel.volume_id),
|
|
mock.sentinel.bdm]
|
|
|
|
with mock.patch.object(compute_api, 'CELLS', fake_cells):
|
|
bdm = self.compute_api._get_bdm_by_volume_id(
|
|
self.context, mock.sentinel.volume_id,
|
|
mock.sentinel.expected_attrs)
|
|
|
|
self.assertEqual(mock.sentinel.bdm, bdm)
|
|
|
|
mock_target_cell.assert_has_calls([
|
|
mock.call(self.context, cell) for cell in fake_cells])
|
|
mock_get_by_volume.assert_has_calls(
|
|
[mock.call(self.context,
|
|
mock.sentinel.volume_id,
|
|
expected_attrs=mock.sentinel.expected_attrs)] * 2)
|
|
|
|
@mock.patch.object(context, 'set_target_cell')
|
|
@mock.patch.object(objects.BlockDeviceMapping, 'get_by_volume')
|
|
def test_get_missing_bdm_by_volume_id(self, mock_get_by_volume,
|
|
mock_target_cell):
|
|
fake_cells = [mock.sentinel.cell0, mock.sentinel.cell1]
|
|
|
|
mock_get_by_volume.side_effect = exception.VolumeBDMNotFound(
|
|
volume_id=mock.sentinel.volume_id)
|
|
|
|
with mock.patch.object(compute_api, 'CELLS', fake_cells):
|
|
self.assertRaises(
|
|
exception.VolumeBDMNotFound,
|
|
self.compute_api._get_bdm_by_volume_id,
|
|
self.context, mock.sentinel.volume_id,
|
|
mock.sentinel.expected_attrs)
|
|
|
|
@mock.patch.object(compute_api.API, '_get_bdm_by_volume_id')
|
|
def test_volume_snapshot_create(self, mock_get_bdm):
|
|
volume_id = '1'
|
|
create_info = {'id': 'eyedee'}
|
|
fake_bdm = fake_block_device.FakeDbBlockDeviceDict({
|
|
'id': 123,
|
|
'device_name': '/dev/sda2',
|
|
'source_type': 'volume',
|
|
'destination_type': 'volume',
|
|
'connection_info': "{'fake': 'connection_info'}",
|
|
'volume_id': 1,
|
|
'boot_index': -1})
|
|
fake_bdm['instance'] = fake_instance.fake_db_instance(
|
|
launched_at=timeutils.utcnow(),
|
|
vm_state=vm_states.ACTIVE)
|
|
fake_bdm['instance_uuid'] = fake_bdm['instance']['uuid']
|
|
fake_bdm = objects.BlockDeviceMapping._from_db_object(
|
|
self.context, objects.BlockDeviceMapping(),
|
|
fake_bdm, expected_attrs=['instance'])
|
|
|
|
mock_get_bdm.return_value = fake_bdm
|
|
|
|
with mock.patch.object(self.compute_api.compute_rpcapi,
|
|
'volume_snapshot_create') as mock_snapshot:
|
|
snapshot = self.compute_api.volume_snapshot_create(self.context,
|
|
volume_id, create_info)
|
|
|
|
expected_snapshot = {
|
|
'snapshot': {
|
|
'id': create_info['id'],
|
|
'volumeId': volume_id,
|
|
},
|
|
}
|
|
self.assertEqual(snapshot, expected_snapshot)
|
|
mock_get_bdm.assert_called_once_with(
|
|
self.context, volume_id, expected_attrs=['instance'])
|
|
mock_snapshot.assert_called_once_with(
|
|
self.context, fake_bdm['instance'], volume_id, create_info)
|
|
|
|
@mock.patch.object(
|
|
objects.BlockDeviceMapping, 'get_by_volume',
|
|
return_value=objects.BlockDeviceMapping(
|
|
instance=objects.Instance(
|
|
launched_at=timeutils.utcnow(), uuid=uuids.instance_uuid,
|
|
vm_state=vm_states.ACTIVE, task_state=task_states.SHELVING,
|
|
host='fake_host')))
|
|
def test_volume_snapshot_create_shelving(self, bdm_get_by_volume):
|
|
"""Tests a negative scenario where the instance task_state is not
|
|
accepted for creating a guest-assisted volume snapshot.
|
|
"""
|
|
self.assertRaises(exception.InstanceInvalidState,
|
|
self.compute_api.volume_snapshot_create,
|
|
self.context, mock.sentinel.volume_id,
|
|
mock.sentinel.create_info)
|
|
|
|
@mock.patch.object(
|
|
objects.BlockDeviceMapping, 'get_by_volume',
|
|
return_value=objects.BlockDeviceMapping(
|
|
instance=objects.Instance(
|
|
launched_at=timeutils.utcnow(), uuid=uuids.instance_uuid,
|
|
vm_state=vm_states.SHELVED_OFFLOADED, task_state=None,
|
|
host=None)))
|
|
def test_volume_snapshot_create_shelved_offloaded(self, bdm_get_by_volume):
|
|
"""Tests a negative scenario where the instance is shelved offloaded
|
|
so we don't have a host to cast to for the guest-assisted snapshot.
|
|
"""
|
|
self.assertRaises(exception.InstanceNotReady,
|
|
self.compute_api.volume_snapshot_create,
|
|
self.context, mock.sentinel.volume_id,
|
|
mock.sentinel.create_info)
|
|
|
|
@mock.patch.object(compute_api.API, '_get_bdm_by_volume_id')
|
|
def test_volume_snapshot_delete(self, mock_get_bdm):
|
|
volume_id = '1'
|
|
snapshot_id = '2'
|
|
fake_bdm = fake_block_device.FakeDbBlockDeviceDict({
|
|
'id': 123,
|
|
'device_name': '/dev/sda2',
|
|
'source_type': 'volume',
|
|
'destination_type': 'volume',
|
|
'connection_info': "{'fake': 'connection_info'}",
|
|
'volume_id': 1,
|
|
'boot_index': -1})
|
|
fake_bdm['instance'] = fake_instance.fake_db_instance(
|
|
launched_at=timeutils.utcnow(),
|
|
vm_state=vm_states.STOPPED)
|
|
fake_bdm['instance_uuid'] = fake_bdm['instance']['uuid']
|
|
fake_bdm = objects.BlockDeviceMapping._from_db_object(
|
|
self.context, objects.BlockDeviceMapping(),
|
|
fake_bdm, expected_attrs=['instance'])
|
|
|
|
mock_get_bdm.return_value = fake_bdm
|
|
|
|
with mock.patch.object(self.compute_api.compute_rpcapi,
|
|
'volume_snapshot_delete') as mock_snapshot:
|
|
self.compute_api.volume_snapshot_delete(self.context, volume_id,
|
|
snapshot_id, {})
|
|
|
|
mock_get_bdm.assert_called_once_with(self.context, volume_id,
|
|
expected_attrs=['instance'])
|
|
mock_snapshot.assert_called_once_with(
|
|
self.context, fake_bdm['instance'], volume_id, snapshot_id, {})
|
|
|
|
@mock.patch.object(
|
|
objects.BlockDeviceMapping, 'get_by_volume',
|
|
return_value=objects.BlockDeviceMapping(
|
|
instance=objects.Instance(
|
|
launched_at=timeutils.utcnow(), uuid=uuids.instance_uuid,
|
|
vm_state=vm_states.ACTIVE, task_state=task_states.SHELVING,
|
|
host='fake_host')))
|
|
def test_volume_snapshot_delete_shelving(self, bdm_get_by_volume):
|
|
"""Tests a negative scenario where the instance is shelving and the
|
|
task_state is set so we can't perform the guest-assisted snapshot.
|
|
"""
|
|
self.assertRaises(exception.InstanceInvalidState,
|
|
self.compute_api.volume_snapshot_delete,
|
|
self.context, mock.sentinel.volume_id,
|
|
mock.sentinel.snapshot_id, mock.sentinel.delete_info)
|
|
|
|
@mock.patch.object(
|
|
objects.BlockDeviceMapping, 'get_by_volume',
|
|
return_value=objects.BlockDeviceMapping(
|
|
instance=objects.Instance(
|
|
launched_at=timeutils.utcnow(), uuid=uuids.instance_uuid,
|
|
vm_state=vm_states.SHELVED_OFFLOADED, task_state=None,
|
|
host=None)))
|
|
def test_volume_snapshot_delete_shelved_offloaded(self, bdm_get_by_volume):
|
|
"""Tests a negative scenario where the instance is shelved offloaded
|
|
so there is no host to cast to for the guest-assisted snapshot delete.
|
|
"""
|
|
self.assertRaises(exception.InstanceNotReady,
|
|
self.compute_api.volume_snapshot_delete,
|
|
self.context, mock.sentinel.volume_id,
|
|
mock.sentinel.snapshot_id, mock.sentinel.delete_info)
|
|
|
|
def _test_boot_volume_bootable(self, is_bootable=False):
|
|
def get_vol_data(*args, **kwargs):
|
|
return {'bootable': is_bootable}
|
|
block_device_mapping = [{
|
|
'id': 1,
|
|
'device_name': 'vda',
|
|
'no_device': None,
|
|
'virtual_name': None,
|
|
'snapshot_id': None,
|
|
'volume_id': '1',
|
|
'delete_on_termination': False,
|
|
}]
|
|
|
|
expected_meta = {'min_disk': 0, 'min_ram': 0, 'properties': {},
|
|
'size': 0, 'status': 'active'}
|
|
|
|
with mock.patch.object(self.compute_api.volume_api, 'get',
|
|
side_effect=get_vol_data):
|
|
if not is_bootable:
|
|
self.assertRaises(exception.InvalidBDMVolumeNotBootable,
|
|
utils.get_bdm_image_metadata,
|
|
self.context, self.compute_api.image_api,
|
|
self.compute_api.volume_api,
|
|
block_device_mapping)
|
|
else:
|
|
meta = utils.get_bdm_image_metadata(
|
|
self.context, self.compute_api.image_api,
|
|
self.compute_api.volume_api, block_device_mapping)
|
|
self.assertEqual(expected_meta, meta)
|
|
|
|
def test_boot_volume_non_bootable(self):
|
|
self._test_boot_volume_bootable(False)
|
|
|
|
def test_boot_volume_bootable(self):
|
|
self._test_boot_volume_bootable(True)
|
|
|
|
def test_boot_volume_basic_property(self):
|
|
block_device_mapping = [{
|
|
'id': 1,
|
|
'device_name': 'vda',
|
|
'no_device': None,
|
|
'virtual_name': None,
|
|
'snapshot_id': None,
|
|
'volume_id': '1',
|
|
'delete_on_termination': False,
|
|
}]
|
|
fake_volume = {"volume_image_metadata":
|
|
{"min_ram": 256, "min_disk": 128, "foo": "bar"}}
|
|
with mock.patch.object(self.compute_api.volume_api, 'get',
|
|
return_value=fake_volume):
|
|
meta = utils.get_bdm_image_metadata(
|
|
self.context, self.compute_api.image_api,
|
|
self.compute_api.volume_api, block_device_mapping)
|
|
self.assertEqual(256, meta['min_ram'])
|
|
self.assertEqual(128, meta['min_disk'])
|
|
self.assertEqual('active', meta['status'])
|
|
self.assertEqual('bar', meta['properties']['foo'])
|
|
|
|
def test_boot_volume_snapshot_basic_property(self):
|
|
block_device_mapping = [{
|
|
'id': 1,
|
|
'device_name': 'vda',
|
|
'no_device': None,
|
|
'virtual_name': None,
|
|
'snapshot_id': '2',
|
|
'volume_id': None,
|
|
'delete_on_termination': False,
|
|
}]
|
|
fake_volume = {"volume_image_metadata":
|
|
{"min_ram": 256, "min_disk": 128, "foo": "bar"}}
|
|
fake_snapshot = {"volume_id": "1"}
|
|
with test.nested(
|
|
mock.patch.object(self.compute_api.volume_api, 'get',
|
|
return_value=fake_volume),
|
|
mock.patch.object(self.compute_api.volume_api, 'get_snapshot',
|
|
return_value=fake_snapshot)) as (
|
|
volume_get, volume_get_snapshot):
|
|
meta = utils.get_bdm_image_metadata(
|
|
self.context, self.compute_api.image_api,
|
|
self.compute_api.volume_api, block_device_mapping)
|
|
self.assertEqual(256, meta['min_ram'])
|
|
self.assertEqual(128, meta['min_disk'])
|
|
self.assertEqual('active', meta['status'])
|
|
self.assertEqual('bar', meta['properties']['foo'])
|
|
volume_get_snapshot.assert_called_once_with(self.context,
|
|
block_device_mapping[0]['snapshot_id'])
|
|
volume_get.assert_called_once_with(self.context,
|
|
fake_snapshot['volume_id'])
|
|
|
|
def _create_instance_with_disabled_disk_config(self, object=False):
|
|
sys_meta = {"image_auto_disk_config": "Disabled"}
|
|
params = {"system_metadata": sys_meta}
|
|
instance = self._create_instance_obj(params=params)
|
|
if object:
|
|
return instance
|
|
return obj_base.obj_to_primitive(instance)
|
|
|
|
def _setup_fake_image_with_disabled_disk_config(self):
|
|
self.fake_image = {
|
|
'id': 1,
|
|
'name': 'fake_name',
|
|
'status': 'active',
|
|
'properties': {"auto_disk_config": "Disabled"},
|
|
}
|
|
|
|
fake_image.stub_out_image_service(self)
|
|
self.stub_out('nova.tests.unit.image.fake._FakeImageService.show',
|
|
lambda obj, context, image_id, **kwargs: self.fake_image)
|
|
return self.fake_image['id']
|
|
|
|
def _setup_fake_image_with_invalid_arch(self):
|
|
self.fake_image = {
|
|
'id': 2,
|
|
'name': 'fake_name',
|
|
'status': 'active',
|
|
'properties': {"hw_architecture": "arm64"},
|
|
}
|
|
|
|
fake_image.stub_out_image_service(self)
|
|
self.stub_out('nova.tests.unit.image.fake._FakeImageService.show',
|
|
lambda obj, context, image_id, **kwargs: self.fake_image)
|
|
return self.fake_image['id']
|
|
|
|
@mock.patch('nova.compute.api.API.get_instance_host_status',
|
|
new=mock.Mock(return_value=fields_obj.HostStatus.UP))
|
|
def test_resize_with_disabled_auto_disk_config_fails(self):
|
|
fake_inst = self._create_instance_with_disabled_disk_config(
|
|
object=True)
|
|
|
|
self.assertRaises(exception.AutoDiskConfigDisabledByImage,
|
|
self.compute_api.resize,
|
|
self.context, fake_inst,
|
|
auto_disk_config=True)
|
|
|
|
def test_create_with_disabled_auto_disk_config_fails(self):
|
|
image_id = self._setup_fake_image_with_disabled_disk_config()
|
|
|
|
self.assertRaises(exception.AutoDiskConfigDisabledByImage,
|
|
self.compute_api.create, self.context,
|
|
"fake_flavor", image_id, auto_disk_config=True)
|
|
|
|
def test_rebuild_with_disabled_auto_disk_config_fails(self):
|
|
fake_inst = self._create_instance_with_disabled_disk_config(
|
|
object=True)
|
|
image_id = self._setup_fake_image_with_disabled_disk_config()
|
|
self.assertRaises(exception.AutoDiskConfigDisabledByImage,
|
|
self.compute_api.rebuild,
|
|
self.context,
|
|
fake_inst,
|
|
image_id,
|
|
"new password",
|
|
auto_disk_config=True)
|
|
|
|
def test_rebuild_with_invalid_image_arch(self):
|
|
instance = fake_instance.fake_instance_obj(
|
|
self.context, vm_state=vm_states.ACTIVE, cell_name='fake-cell',
|
|
launched_at=timeutils.utcnow(),
|
|
system_metadata={}, image_ref='foo',
|
|
expected_attrs=['system_metadata'])
|
|
image_id = self._setup_fake_image_with_invalid_arch()
|
|
self.assertRaises(exception.InvalidArchitectureName,
|
|
self.compute_api.rebuild,
|
|
self.context,
|
|
instance,
|
|
image_id,
|
|
"new password")
|
|
self.assertIsNone(instance.task_state)
|
|
|
|
@mock.patch.object(objects.RequestSpec, 'get_by_instance_uuid')
|
|
@mock.patch.object(objects.Instance, 'save')
|
|
@mock.patch.object(objects.Instance, 'get_flavor')
|
|
@mock.patch.object(objects.BlockDeviceMappingList, 'get_by_instance_uuid')
|
|
@mock.patch.object(compute_api.API, '_get_image')
|
|
@mock.patch.object(compute_api.API, '_check_auto_disk_config')
|
|
@mock.patch.object(compute_api.API, '_checks_for_create_and_rebuild')
|
|
@mock.patch.object(compute_api.API, '_record_action_start')
|
|
def test_rebuild(self, _record_action_start,
|
|
_checks_for_create_and_rebuild, _check_auto_disk_config,
|
|
_get_image, bdm_get_by_instance_uuid, get_flavor, instance_save,
|
|
req_spec_get_by_inst_uuid):
|
|
orig_system_metadata = {}
|
|
instance = fake_instance.fake_instance_obj(self.context,
|
|
vm_state=vm_states.ACTIVE, cell_name='fake-cell',
|
|
launched_at=timeutils.utcnow(),
|
|
system_metadata=orig_system_metadata,
|
|
image_ref='foo',
|
|
expected_attrs=['system_metadata'])
|
|
get_flavor.return_value = test_flavor.fake_flavor
|
|
flavor = instance.get_flavor()
|
|
image_href = 'foo'
|
|
image = {
|
|
"min_ram": 10, "min_disk": 1,
|
|
"properties": {
|
|
'architecture': fields_obj.Architecture.X86_64}}
|
|
admin_pass = ''
|
|
files_to_inject = []
|
|
bdms = objects.BlockDeviceMappingList()
|
|
|
|
_get_image.return_value = (None, image)
|
|
bdm_get_by_instance_uuid.return_value = bdms
|
|
|
|
fake_spec = objects.RequestSpec()
|
|
req_spec_get_by_inst_uuid.return_value = fake_spec
|
|
|
|
with mock.patch.object(self.compute_api.compute_task_api,
|
|
'rebuild_instance') as rebuild_instance:
|
|
self.compute_api.rebuild(self.context, instance, image_href,
|
|
admin_pass, files_to_inject)
|
|
|
|
rebuild_instance.assert_called_once_with(self.context,
|
|
instance=instance, new_pass=admin_pass,
|
|
injected_files=files_to_inject, image_ref=image_href,
|
|
orig_image_ref=image_href,
|
|
orig_sys_metadata=orig_system_metadata, bdms=bdms,
|
|
preserve_ephemeral=False, host=instance.host,
|
|
request_spec=fake_spec)
|
|
|
|
_check_auto_disk_config.assert_called_once_with(
|
|
image=image, auto_disk_config=None)
|
|
_checks_for_create_and_rebuild.assert_called_once_with(self.context,
|
|
None, image, flavor, {}, [], None)
|
|
self.assertNotEqual(orig_system_metadata, instance.system_metadata)
|
|
bdm_get_by_instance_uuid.assert_called_once_with(
|
|
self.context, instance.uuid)
|
|
|
|
@mock.patch.object(objects.RequestSpec, 'save')
|
|
@mock.patch.object(objects.RequestSpec, 'get_by_instance_uuid')
|
|
@mock.patch.object(objects.Instance, 'save')
|
|
@mock.patch.object(objects.Instance, 'get_flavor')
|
|
@mock.patch.object(objects.BlockDeviceMappingList, 'get_by_instance_uuid')
|
|
@mock.patch.object(compute_api.API, '_get_image')
|
|
@mock.patch.object(compute_api.API, '_check_auto_disk_config')
|
|
@mock.patch.object(compute_api.API, '_checks_for_create_and_rebuild')
|
|
@mock.patch.object(compute_api.API, '_record_action_start')
|
|
def test_rebuild_change_image(self, _record_action_start,
|
|
_checks_for_create_and_rebuild, _check_auto_disk_config,
|
|
_get_image, bdm_get_by_instance_uuid, get_flavor, instance_save,
|
|
req_spec_get_by_inst_uuid, req_spec_save):
|
|
orig_system_metadata = {}
|
|
get_flavor.return_value = test_flavor.fake_flavor
|
|
orig_image_href = 'orig_image'
|
|
orig_image = {
|
|
"min_ram": 10, "min_disk": 1,
|
|
"properties": {'architecture': fields_obj.Architecture.X86_64,
|
|
'vm_mode': 'hvm'}}
|
|
new_image_href = 'new_image'
|
|
new_image = {
|
|
"min_ram": 10, "min_disk": 1,
|
|
"properties": {'architecture': fields_obj.Architecture.X86_64,
|
|
'vm_mode': 'xen'}}
|
|
admin_pass = ''
|
|
files_to_inject = []
|
|
bdms = objects.BlockDeviceMappingList()
|
|
|
|
instance = fake_instance.fake_instance_obj(self.context,
|
|
vm_state=vm_states.ACTIVE, cell_name='fake-cell',
|
|
launched_at=timeutils.utcnow(),
|
|
system_metadata=orig_system_metadata,
|
|
expected_attrs=['system_metadata'],
|
|
image_ref=orig_image_href,
|
|
node='node',
|
|
vm_mode=fields_obj.VMMode.HVM)
|
|
flavor = instance.get_flavor()
|
|
|
|
def get_image(context, image_href):
|
|
if image_href == new_image_href:
|
|
return (None, new_image)
|
|
if image_href == orig_image_href:
|
|
return (None, orig_image)
|
|
_get_image.side_effect = get_image
|
|
bdm_get_by_instance_uuid.return_value = bdms
|
|
|
|
fake_spec = objects.RequestSpec(id=1)
|
|
req_spec_get_by_inst_uuid.return_value = fake_spec
|
|
|
|
with mock.patch.object(self.compute_api.compute_task_api,
|
|
'rebuild_instance') as rebuild_instance:
|
|
self.compute_api.rebuild(self.context, instance, new_image_href,
|
|
admin_pass, files_to_inject)
|
|
|
|
rebuild_instance.assert_called_once_with(self.context,
|
|
instance=instance, new_pass=admin_pass,
|
|
injected_files=files_to_inject, image_ref=new_image_href,
|
|
orig_image_ref=orig_image_href,
|
|
orig_sys_metadata=orig_system_metadata, bdms=bdms,
|
|
preserve_ephemeral=False, host=None,
|
|
request_spec=fake_spec)
|
|
# assert the request spec was modified so the scheduler picks
|
|
# the existing instance host/node
|
|
req_spec_save.assert_called_once_with()
|
|
self.assertIn('_nova_check_type', fake_spec.scheduler_hints)
|
|
self.assertEqual('rebuild',
|
|
fake_spec.scheduler_hints['_nova_check_type'][0])
|
|
|
|
_check_auto_disk_config.assert_called_once_with(
|
|
image=new_image, auto_disk_config=None)
|
|
_checks_for_create_and_rebuild.assert_called_once_with(self.context,
|
|
None, new_image, flavor, {}, [], None)
|
|
self.assertEqual(fields_obj.VMMode.XEN, instance.vm_mode)
|
|
|
|
@mock.patch.object(objects.KeyPair, 'get_by_name')
|
|
@mock.patch.object(objects.RequestSpec, 'get_by_instance_uuid')
|
|
@mock.patch.object(objects.Instance, 'save')
|
|
@mock.patch.object(objects.Instance, 'get_flavor')
|
|
@mock.patch.object(objects.BlockDeviceMappingList, 'get_by_instance_uuid')
|
|
@mock.patch.object(compute_api.API, '_get_image')
|
|
@mock.patch.object(compute_api.API, '_check_auto_disk_config')
|
|
@mock.patch.object(compute_api.API, '_checks_for_create_and_rebuild')
|
|
@mock.patch.object(compute_api.API, '_record_action_start')
|
|
def test_rebuild_change_keypair(self, _record_action_start,
|
|
_checks_for_create_and_rebuild, _check_auto_disk_config,
|
|
_get_image, bdm_get_by_instance_uuid, get_flavor, instance_save,
|
|
req_spec_get_by_inst_uuid, mock_get_keypair):
|
|
orig_system_metadata = {}
|
|
orig_key_name = 'orig_key_name'
|
|
orig_key_data = 'orig_key_data_XXX'
|
|
instance = fake_instance.fake_instance_obj(self.context,
|
|
vm_state=vm_states.ACTIVE, cell_name='fake-cell',
|
|
launched_at=timeutils.utcnow(),
|
|
system_metadata=orig_system_metadata,
|
|
image_ref='foo',
|
|
expected_attrs=['system_metadata'],
|
|
key_name=orig_key_name,
|
|
key_data=orig_key_data)
|
|
get_flavor.return_value = test_flavor.fake_flavor
|
|
flavor = instance.get_flavor()
|
|
image_href = 'foo'
|
|
image = {
|
|
"min_ram": 10, "min_disk": 1,
|
|
"properties": {'architecture': fields_obj.Architecture.X86_64,
|
|
'vm_mode': 'hvm'}}
|
|
admin_pass = ''
|
|
files_to_inject = []
|
|
bdms = objects.BlockDeviceMappingList()
|
|
|
|
_get_image.return_value = (None, image)
|
|
bdm_get_by_instance_uuid.return_value = bdms
|
|
|
|
fake_spec = objects.RequestSpec()
|
|
req_spec_get_by_inst_uuid.return_value = fake_spec
|
|
|
|
keypair = self._create_keypair_obj(instance)
|
|
mock_get_keypair.return_value = keypair
|
|
with mock.patch.object(self.compute_api.compute_task_api,
|
|
'rebuild_instance') as rebuild_instance:
|
|
self.compute_api.rebuild(self.context, instance, image_href,
|
|
admin_pass, files_to_inject, key_name=keypair.name)
|
|
|
|
rebuild_instance.assert_called_once_with(self.context,
|
|
instance=instance, new_pass=admin_pass,
|
|
injected_files=files_to_inject, image_ref=image_href,
|
|
orig_image_ref=image_href,
|
|
orig_sys_metadata=orig_system_metadata, bdms=bdms,
|
|
preserve_ephemeral=False, host=instance.host,
|
|
request_spec=fake_spec)
|
|
|
|
_check_auto_disk_config.assert_called_once_with(
|
|
image=image, auto_disk_config=None)
|
|
_checks_for_create_and_rebuild.assert_called_once_with(self.context,
|
|
None, image, flavor, {}, [], None)
|
|
self.assertNotEqual(orig_key_name, instance.key_name)
|
|
self.assertNotEqual(orig_key_data, instance.key_data)
|
|
|
|
@mock.patch.object(objects.RequestSpec, 'get_by_instance_uuid')
|
|
@mock.patch.object(objects.Instance, 'save')
|
|
@mock.patch.object(objects.Instance, 'get_flavor')
|
|
@mock.patch.object(objects.BlockDeviceMappingList, 'get_by_instance_uuid')
|
|
@mock.patch.object(compute_api.API, '_get_image')
|
|
@mock.patch.object(compute_api.API, '_check_auto_disk_config')
|
|
@mock.patch.object(compute_api.API, '_checks_for_create_and_rebuild')
|
|
@mock.patch.object(compute_api.API, '_record_action_start')
|
|
def test_rebuild_change_trusted_certs(self, _record_action_start,
|
|
_checks_for_create_and_rebuild, _check_auto_disk_config,
|
|
_get_image, bdm_get_by_instance_uuid, get_flavor, instance_save,
|
|
req_spec_get_by_inst_uuid):
|
|
orig_system_metadata = {}
|
|
orig_trusted_certs = ['orig-trusted-cert-1', 'orig-trusted-cert-2']
|
|
new_trusted_certs = ['new-trusted-cert-1', 'new-trusted-cert-2']
|
|
instance = fake_instance.fake_instance_obj(
|
|
self.context, vm_state=vm_states.ACTIVE, cell_name='fake-cell',
|
|
launched_at=timeutils.utcnow(),
|
|
system_metadata=orig_system_metadata, image_ref='foo',
|
|
expected_attrs=['system_metadata'],
|
|
trusted_certs=orig_trusted_certs)
|
|
get_flavor.return_value = test_flavor.fake_flavor
|
|
flavor = instance.get_flavor()
|
|
image_href = 'foo'
|
|
image = {
|
|
"min_ram": 10, "min_disk": 1,
|
|
"properties": {'architecture': fields_obj.Architecture.X86_64,
|
|
'vm_mode': 'hvm'}}
|
|
admin_pass = ''
|
|
files_to_inject = []
|
|
bdms = objects.BlockDeviceMappingList()
|
|
|
|
_get_image.return_value = (None, image)
|
|
bdm_get_by_instance_uuid.return_value = bdms
|
|
|
|
fake_spec = objects.RequestSpec()
|
|
req_spec_get_by_inst_uuid.return_value = fake_spec
|
|
|
|
with mock.patch.object(self.compute_api.compute_task_api,
|
|
'rebuild_instance') as rebuild_instance:
|
|
self.compute_api.rebuild(self.context, instance, image_href,
|
|
admin_pass, files_to_inject,
|
|
trusted_certs=new_trusted_certs)
|
|
|
|
rebuild_instance.assert_called_once_with(
|
|
self.context, instance=instance, new_pass=admin_pass,
|
|
injected_files=files_to_inject, image_ref=image_href,
|
|
orig_image_ref=image_href,
|
|
orig_sys_metadata=orig_system_metadata, bdms=bdms,
|
|
preserve_ephemeral=False, host=instance.host,
|
|
request_spec=fake_spec)
|
|
|
|
_check_auto_disk_config.assert_called_once_with(
|
|
image=image, auto_disk_config=None)
|
|
_checks_for_create_and_rebuild.assert_called_once_with(
|
|
self.context, None, image, flavor, {}, [], None)
|
|
self.assertEqual(new_trusted_certs, instance.trusted_certs.ids)
|
|
|
|
@mock.patch.object(objects.RequestSpec, 'get_by_instance_uuid')
|
|
@mock.patch.object(objects.Instance, 'save')
|
|
@mock.patch.object(objects.Instance, 'get_flavor')
|
|
@mock.patch.object(objects.BlockDeviceMappingList, 'get_by_instance_uuid')
|
|
@mock.patch.object(compute_api.API, '_get_image')
|
|
@mock.patch.object(compute_api.API, '_check_auto_disk_config')
|
|
@mock.patch.object(compute_api.API, '_checks_for_create_and_rebuild')
|
|
@mock.patch.object(compute_api.API, '_record_action_start')
|
|
def test_rebuild_unset_trusted_certs(self, _record_action_start,
|
|
_checks_for_create_and_rebuild,
|
|
_check_auto_disk_config,
|
|
_get_image, bdm_get_by_instance_uuid,
|
|
get_flavor, instance_save,
|
|
req_spec_get_by_inst_uuid):
|
|
"""Tests the scenario that the server was created with some trusted
|
|
certs and then rebuilt without trusted_image_certificates=None
|
|
explicitly to unset the trusted certs on the server.
|
|
"""
|
|
orig_system_metadata = {}
|
|
orig_trusted_certs = ['orig-trusted-cert-1', 'orig-trusted-cert-2']
|
|
new_trusted_certs = None
|
|
instance = fake_instance.fake_instance_obj(
|
|
self.context, vm_state=vm_states.ACTIVE, cell_name='fake-cell',
|
|
launched_at=timeutils.utcnow(),
|
|
system_metadata=orig_system_metadata, image_ref='foo',
|
|
expected_attrs=['system_metadata'],
|
|
trusted_certs=orig_trusted_certs)
|
|
get_flavor.return_value = test_flavor.fake_flavor
|
|
flavor = instance.get_flavor()
|
|
image_href = 'foo'
|
|
image = {
|
|
"min_ram": 10, "min_disk": 1,
|
|
"properties": {'architecture': fields_obj.Architecture.X86_64,
|
|
'vm_mode': 'hvm'}}
|
|
admin_pass = ''
|
|
files_to_inject = []
|
|
bdms = objects.BlockDeviceMappingList()
|
|
|
|
_get_image.return_value = (None, image)
|
|
bdm_get_by_instance_uuid.return_value = bdms
|
|
|
|
fake_spec = objects.RequestSpec()
|
|
req_spec_get_by_inst_uuid.return_value = fake_spec
|
|
|
|
with mock.patch.object(self.compute_api.compute_task_api,
|
|
'rebuild_instance') as rebuild_instance:
|
|
self.compute_api.rebuild(self.context, instance, image_href,
|
|
admin_pass, files_to_inject,
|
|
trusted_certs=new_trusted_certs)
|
|
|
|
rebuild_instance.assert_called_once_with(
|
|
self.context, instance=instance, new_pass=admin_pass,
|
|
injected_files=files_to_inject, image_ref=image_href,
|
|
orig_image_ref=image_href,
|
|
orig_sys_metadata=orig_system_metadata, bdms=bdms,
|
|
preserve_ephemeral=False, host=instance.host,
|
|
request_spec=fake_spec)
|
|
|
|
_check_auto_disk_config.assert_called_once_with(
|
|
image=image, auto_disk_config=None)
|
|
_checks_for_create_and_rebuild.assert_called_once_with(
|
|
self.context, None, image, flavor, {}, [], None)
|
|
self.assertIsNone(instance.trusted_certs)
|
|
|
|
@mock.patch.object(compute_utils, 'is_volume_backed_instance',
|
|
return_value=True)
|
|
@mock.patch.object(objects.Instance, 'get_flavor')
|
|
@mock.patch.object(objects.BlockDeviceMappingList, 'get_by_instance_uuid')
|
|
@mock.patch.object(compute_api.API, '_get_image')
|
|
@mock.patch.object(compute_api.API, '_check_auto_disk_config')
|
|
@mock.patch.object(compute_api.API, '_record_action_start')
|
|
def test_rebuild_volume_backed_instance_with_trusted_certs(
|
|
self, _record_action_start, _check_auto_disk_config, _get_image,
|
|
bdm_get_by_instance_uuid, get_flavor, instance_is_volume_backed):
|
|
orig_system_metadata = {}
|
|
new_trusted_certs = ['new-trusted-cert-1', 'new-trusted-cert-2']
|
|
instance = fake_instance.fake_instance_obj(
|
|
self.context, vm_state=vm_states.ACTIVE, cell_name='fake-cell',
|
|
launched_at=timeutils.utcnow(),
|
|
system_metadata=orig_system_metadata, image_ref=None,
|
|
expected_attrs=['system_metadata'], trusted_certs=None)
|
|
get_flavor.return_value = test_flavor.fake_flavor
|
|
image_href = 'foo'
|
|
image = {
|
|
"min_ram": 10, "min_disk": 1,
|
|
"properties": {'architecture': fields_obj.Architecture.X86_64,
|
|
'vm_mode': 'hvm'}}
|
|
admin_pass = ''
|
|
files_to_inject = []
|
|
bdms = objects.BlockDeviceMappingList()
|
|
|
|
_get_image.return_value = (None, image)
|
|
bdm_get_by_instance_uuid.return_value = bdms
|
|
|
|
self.assertRaises(exception.CertificateValidationFailed,
|
|
self.compute_api.rebuild, self.context, instance,
|
|
image_href, admin_pass, files_to_inject,
|
|
trusted_certs=new_trusted_certs)
|
|
|
|
_check_auto_disk_config.assert_called_once_with(
|
|
image=image, auto_disk_config=None)
|
|
|
|
@mock.patch('nova.objects.Quotas.limit_check')
|
|
def test_check_metadata_properties_quota_with_empty_dict(self,
|
|
limit_check):
|
|
metadata = {}
|
|
self.compute_api._check_metadata_properties_quota(self.context,
|
|
metadata)
|
|
self.assertEqual(0, limit_check.call_count)
|
|
|
|
@mock.patch('nova.objects.Quotas.limit_check')
|
|
def test_check_injected_file_quota_with_empty_list(self,
|
|
limit_check):
|
|
injected_files = []
|
|
self.compute_api._check_injected_file_quota(self.context,
|
|
injected_files)
|
|
self.assertEqual(0, limit_check.call_count)
|
|
|
|
def _test_check_injected_file_quota_onset_file_limit_exceeded(self,
|
|
side_effect):
|
|
injected_files = [
|
|
{
|
|
"path": "/etc/banner.txt",
|
|
"contents": "foo"
|
|
}
|
|
]
|
|
with mock.patch('nova.objects.Quotas.limit_check',
|
|
side_effect=side_effect):
|
|
self.compute_api._check_injected_file_quota(
|
|
self.context, injected_files)
|
|
|
|
def test_check_injected_file_quota_onset_file_limit_exceeded(self):
|
|
# This is the first call to limit_check.
|
|
side_effect = exception.OverQuota(overs='injected_files')
|
|
self.assertRaises(exception.OnsetFileLimitExceeded,
|
|
self._test_check_injected_file_quota_onset_file_limit_exceeded,
|
|
side_effect)
|
|
|
|
def test_check_injected_file_quota_onset_file_path_limit(self):
|
|
# This is the second call to limit_check.
|
|
side_effect = (mock.DEFAULT,
|
|
exception.OverQuota(overs='injected_file_path_bytes',
|
|
quotas={'injected_file_path_bytes': 255}))
|
|
self.assertRaises(exception.OnsetFilePathLimitExceeded,
|
|
self._test_check_injected_file_quota_onset_file_limit_exceeded,
|
|
side_effect)
|
|
|
|
def test_check_injected_file_quota_onset_file_content_limit(self):
|
|
# This is the second call to limit_check but with different overs.
|
|
side_effect = (mock.DEFAULT,
|
|
exception.OverQuota(overs='injected_file_content_bytes',
|
|
quotas={'injected_file_content_bytes': 10240}))
|
|
self.assertRaises(exception.OnsetFileContentLimitExceeded,
|
|
self._test_check_injected_file_quota_onset_file_limit_exceeded,
|
|
side_effect)
|
|
|
|
@mock.patch('nova.objects.Quotas.count_as_dict')
|
|
@mock.patch('nova.objects.Quotas.limit_check_project_and_user')
|
|
@mock.patch('nova.objects.Instance.save')
|
|
@mock.patch('nova.objects.InstanceAction.action_start')
|
|
@mock.patch('nova.compute.api.API._update_queued_for_deletion')
|
|
def test_restore_by_admin(self, update_qfd, action_start, instance_save,
|
|
quota_check, quota_count):
|
|
admin_context = context.RequestContext('admin_user',
|
|
'admin_project',
|
|
True)
|
|
proj_count = {'instances': 1, 'cores': 1, 'ram': 512}
|
|
user_count = proj_count.copy()
|
|
quota_count.return_value = {'project': proj_count, 'user': user_count}
|
|
instance = self._create_instance_obj()
|
|
instance.vm_state = vm_states.SOFT_DELETED
|
|
instance.task_state = None
|
|
instance.save()
|
|
with mock.patch.object(self.compute_api, 'compute_rpcapi') as rpc:
|
|
self.compute_api.restore(admin_context, instance)
|
|
rpc.restore_instance.assert_called_once_with(admin_context,
|
|
instance)
|
|
self.assertEqual(instance.task_state, task_states.RESTORING)
|
|
# mock.ANY might be 'instances', 'cores', or 'ram' depending on how the
|
|
# deltas dict is iterated in check_deltas
|
|
quota_count.assert_called_once_with(admin_context, mock.ANY,
|
|
instance.project_id,
|
|
user_id=instance.user_id)
|
|
quota_check.assert_called_once_with(
|
|
admin_context,
|
|
user_values={'instances': 2,
|
|
'cores': 1 + instance.flavor.vcpus,
|
|
'ram': 512 + instance.flavor.memory_mb},
|
|
project_values={'instances': 2,
|
|
'cores': 1 + instance.flavor.vcpus,
|
|
'ram': 512 + instance.flavor.memory_mb},
|
|
project_id=instance.project_id, user_id=instance.user_id)
|
|
update_qfd.assert_called_once_with(admin_context, instance, False)
|
|
|
|
@mock.patch('nova.objects.Quotas.count_as_dict')
|
|
@mock.patch('nova.objects.Quotas.limit_check_project_and_user')
|
|
@mock.patch('nova.objects.Instance.save')
|
|
@mock.patch('nova.objects.InstanceAction.action_start')
|
|
@mock.patch('nova.compute.api.API._update_queued_for_deletion')
|
|
def test_restore_by_instance_owner(self, update_qfd, action_start,
|
|
instance_save,
|
|
quota_check, quota_count):
|
|
proj_count = {'instances': 1, 'cores': 1, 'ram': 512}
|
|
user_count = proj_count.copy()
|
|
quota_count.return_value = {'project': proj_count, 'user': user_count}
|
|
instance = self._create_instance_obj()
|
|
instance.vm_state = vm_states.SOFT_DELETED
|
|
instance.task_state = None
|
|
instance.save()
|
|
with mock.patch.object(self.compute_api, 'compute_rpcapi') as rpc:
|
|
self.compute_api.restore(self.context, instance)
|
|
rpc.restore_instance.assert_called_once_with(self.context,
|
|
instance)
|
|
self.assertEqual(instance.project_id, self.context.project_id)
|
|
self.assertEqual(instance.task_state, task_states.RESTORING)
|
|
# mock.ANY might be 'instances', 'cores', or 'ram' depending on how the
|
|
# deltas dict is iterated in check_deltas
|
|
quota_count.assert_called_once_with(self.context, mock.ANY,
|
|
instance.project_id,
|
|
user_id=instance.user_id)
|
|
quota_check.assert_called_once_with(
|
|
self.context,
|
|
user_values={'instances': 2,
|
|
'cores': 1 + instance.flavor.vcpus,
|
|
'ram': 512 + instance.flavor.memory_mb},
|
|
project_values={'instances': 2,
|
|
'cores': 1 + instance.flavor.vcpus,
|
|
'ram': 512 + instance.flavor.memory_mb},
|
|
project_id=instance.project_id, user_id=instance.user_id)
|
|
update_qfd.assert_called_once_with(self.context, instance, False)
|
|
|
|
@mock.patch.object(objects.InstanceAction, 'action_start')
|
|
def test_external_instance_event(self, mock_action_start):
|
|
|
|
instances = [
|
|
objects.Instance(uuid=uuids.instance_1, host='host1',
|
|
migration_context=None),
|
|
objects.Instance(uuid=uuids.instance_2, host='host1',
|
|
migration_context=None),
|
|
objects.Instance(uuid=uuids.instance_3, host='host2',
|
|
migration_context=None),
|
|
objects.Instance(uuid=uuids.instance_4, host='host2',
|
|
migration_context=None),
|
|
objects.Instance(uuid=uuids.instance_5, host='host2',
|
|
migration_context=None, task_state=None,
|
|
vm_state=vm_states.STOPPED,
|
|
power_state=power_state.SHUTDOWN),
|
|
objects.Instance(uuid=uuids.instance_6, host='host2',
|
|
migration_context=None, task_state=None,
|
|
vm_state=vm_states.ACTIVE,
|
|
power_state=power_state.RUNNING)
|
|
]
|
|
# Create a single cell context and associate it with all instances
|
|
mapping = objects.InstanceMapping.get_by_instance_uuid(
|
|
self.context, instances[0].uuid)
|
|
with context.target_cell(self.context, mapping.cell_mapping) as cc:
|
|
cell_context = cc
|
|
for instance in instances:
|
|
instance._context = cell_context
|
|
|
|
volume_id = uuidutils.generate_uuid()
|
|
events = [
|
|
objects.InstanceExternalEvent(
|
|
instance_uuid=uuids.instance_1,
|
|
name='network-changed'),
|
|
objects.InstanceExternalEvent(
|
|
instance_uuid=uuids.instance_2,
|
|
name='network-changed'),
|
|
objects.InstanceExternalEvent(
|
|
instance_uuid=uuids.instance_3,
|
|
name='network-changed'),
|
|
objects.InstanceExternalEvent(
|
|
instance_uuid=uuids.instance_4,
|
|
name='volume-extended',
|
|
tag=volume_id),
|
|
objects.InstanceExternalEvent(
|
|
instance_uuid=uuids.instance_5,
|
|
name='power-update',
|
|
tag="POWER_ON"),
|
|
objects.InstanceExternalEvent(
|
|
instance_uuid=uuids.instance_6,
|
|
name='power-update',
|
|
tag="POWER_OFF"),
|
|
]
|
|
self.compute_api.compute_rpcapi = mock.MagicMock()
|
|
self.compute_api.external_instance_event(self.context,
|
|
instances, events)
|
|
method = self.compute_api.compute_rpcapi.external_instance_event
|
|
method.assert_any_call(cell_context, instances[0:2], events[0:2],
|
|
host='host1')
|
|
method.assert_any_call(cell_context, instances[2:], events[2:],
|
|
host='host2')
|
|
calls = [mock.call(self.context, uuids.instance_4,
|
|
instance_actions.EXTEND_VOLUME, want_result=False),
|
|
mock.call(self.context, uuids.instance_5,
|
|
instance_actions.START, want_result=False),
|
|
mock.call(self.context, uuids.instance_6,
|
|
instance_actions.STOP, want_result=False)]
|
|
mock_action_start.assert_has_calls(calls)
|
|
self.assertEqual(2, method.call_count)
|
|
|
|
def test_external_instance_event_power_update_invalid_tag(self):
|
|
instance1 = objects.Instance(self.context)
|
|
instance1.uuid = uuids.instance1
|
|
instance1.id = 1
|
|
instance1.vm_state = vm_states.ACTIVE
|
|
instance1.task_state = None
|
|
instance1.power_state = power_state.RUNNING
|
|
instance1.host = 'host1'
|
|
instance1.migration_context = None
|
|
instance2 = objects.Instance(self.context)
|
|
instance2.uuid = uuids.instance2
|
|
instance2.id = 2
|
|
instance2.vm_state = vm_states.STOPPED
|
|
instance2.task_state = None
|
|
instance2.power_state = power_state.SHUTDOWN
|
|
instance2.host = 'host2'
|
|
instance2.migration_context = None
|
|
instances = [instance1, instance2]
|
|
events = [
|
|
objects.InstanceExternalEvent(
|
|
instance_uuid=instance1.uuid,
|
|
name='power-update',
|
|
tag="VACATION"),
|
|
objects.InstanceExternalEvent(
|
|
instance_uuid=instance2.uuid,
|
|
name='power-update',
|
|
tag="POWER_ON")
|
|
]
|
|
with test.nested(
|
|
mock.patch.object(self.compute_api.compute_rpcapi,
|
|
'external_instance_event'),
|
|
mock.patch.object(objects.InstanceAction, 'action_start'),
|
|
mock.patch.object(compute_api, 'LOG')
|
|
) as (
|
|
mock_ex, mock_action_start, mock_log
|
|
):
|
|
self.compute_api.external_instance_event(self.context,
|
|
instances, events)
|
|
self.assertEqual(2, mock_ex.call_count)
|
|
# event VACATION requested on instance1 is ignored because
|
|
# its an invalid event tag.
|
|
mock_ex.assert_has_calls(
|
|
[mock.call(self.context, [instance2],
|
|
[events[1]], host=u'host2'),
|
|
mock.call(self.context, [instance1], [], host=u'host1')],
|
|
any_order=True)
|
|
mock_action_start.assert_called_once_with(
|
|
self.context, instance2.uuid, instance_actions.START,
|
|
want_result=False)
|
|
self.assertEqual(1, mock_log.warning.call_count)
|
|
self.assertIn(
|
|
'Invalid power state', mock_log.warning.call_args[0][0])
|
|
|
|
def test_external_instance_event_evacuating_instance(self):
|
|
# Since we're patching the db's migration_get(), use a dict here so
|
|
# that we can validate the id is making its way correctly to the db api
|
|
migrations = {}
|
|
migrations[42] = {'id': 42, 'source_compute': 'host1',
|
|
'dest_compute': 'host2', 'source_node': None,
|
|
'dest_node': None, 'dest_host': None,
|
|
'old_instance_type_id': None,
|
|
'new_instance_type_id': None,
|
|
'uuid': uuids.migration,
|
|
'instance_uuid': uuids.instance_2, 'status': None,
|
|
'migration_type': 'evacuation', 'memory_total': None,
|
|
'memory_processed': None, 'memory_remaining': None,
|
|
'disk_total': None, 'disk_processed': None,
|
|
'disk_remaining': None, 'deleted': False,
|
|
'hidden': False, 'created_at': None,
|
|
'updated_at': None, 'deleted_at': None,
|
|
'cross_cell_move': False, 'user_id': None,
|
|
'project_id': None}
|
|
|
|
def migration_get(context, id):
|
|
return migrations[id]
|
|
|
|
instances = [
|
|
objects.Instance(uuid=uuids.instance_1, host='host1',
|
|
migration_context=None),
|
|
objects.Instance(uuid=uuids.instance_2, host='host1',
|
|
migration_context=objects.MigrationContext(
|
|
migration_id=42)),
|
|
objects.Instance(uuid=uuids.instance_3, host='host2',
|
|
migration_context=None)
|
|
]
|
|
|
|
# Create a single cell context and associate it with all instances
|
|
mapping = objects.InstanceMapping.get_by_instance_uuid(
|
|
self.context, instances[0].uuid)
|
|
with context.target_cell(self.context, mapping.cell_mapping) as cc:
|
|
cell_context = cc
|
|
for instance in instances:
|
|
instance._context = cell_context
|
|
|
|
events = [
|
|
objects.InstanceExternalEvent(
|
|
instance_uuid=uuids.instance_1,
|
|
name='network-changed'),
|
|
objects.InstanceExternalEvent(
|
|
instance_uuid=uuids.instance_2,
|
|
name='network-changed'),
|
|
objects.InstanceExternalEvent(
|
|
instance_uuid=uuids.instance_3,
|
|
name='network-changed'),
|
|
]
|
|
|
|
with mock.patch('nova.db.sqlalchemy.api.migration_get', migration_get):
|
|
self.compute_api.compute_rpcapi = mock.MagicMock()
|
|
self.compute_api.external_instance_event(self.context,
|
|
instances, events)
|
|
method = self.compute_api.compute_rpcapi.external_instance_event
|
|
method.assert_any_call(cell_context, instances[0:2], events[0:2],
|
|
host='host1')
|
|
method.assert_any_call(cell_context, instances[1:], events[1:],
|
|
host='host2')
|
|
self.assertEqual(2, method.call_count)
|
|
|
|
@mock.patch('nova.objects.Migration.get_by_id')
|
|
@mock.patch('nova.objects.HostMapping.get_by_host')
|
|
@mock.patch('nova.context.set_target_cell')
|
|
@mock.patch('nova.context.get_admin_context')
|
|
def test_external_instance_event_cross_cell_move(
|
|
self, get_admin_context, set_target_cell, get_hm_by_host,
|
|
get_mig_by_id):
|
|
"""Tests a scenario where an external server event comes for an
|
|
instance undergoing a cross-cell migration so the event is routed
|
|
to both the source host in the source cell and dest host in dest cell
|
|
using the properly targeted request contexts.
|
|
"""
|
|
migration = objects.Migration(
|
|
id=1, source_compute='host1', dest_compute='host2',
|
|
cross_cell_move=True)
|
|
migration_context = objects.MigrationContext(
|
|
instance_uuid=uuids.instance, migration_id=migration.id,
|
|
migration_type='resize', cross_cell_move=True)
|
|
instance = objects.Instance(
|
|
self.context, uuid=uuids.instance, host=migration.source_compute,
|
|
migration_context=migration_context)
|
|
get_mig_by_id.return_value = migration
|
|
source_cell_mapping = objects.CellMapping(name='source-cell')
|
|
dest_cell_mapping = objects.CellMapping(name='dest-cell')
|
|
|
|
# Wrap _get_relevant_hosts and sort the result for predictable asserts.
|
|
original_get_relevant_hosts = self.compute_api._get_relevant_hosts
|
|
|
|
def wrap_get_relevant_hosts(_self, *a, **kw):
|
|
hosts, cross_cell_move = original_get_relevant_hosts(*a, **kw)
|
|
return sorted(hosts), cross_cell_move
|
|
self.stub_out('nova.compute.api.API._get_relevant_hosts',
|
|
wrap_get_relevant_hosts)
|
|
|
|
def fake_hm_get_by_host(ctxt, host):
|
|
if host == migration.source_compute:
|
|
return objects.HostMapping(
|
|
host=host, cell_mapping=source_cell_mapping)
|
|
if host == migration.dest_compute:
|
|
return objects.HostMapping(
|
|
host=host, cell_mapping=dest_cell_mapping)
|
|
raise Exception('Unexpected host: %s' % host)
|
|
|
|
get_hm_by_host.side_effect = fake_hm_get_by_host
|
|
# get_admin_context should be called twice in order (source and dest)
|
|
get_admin_context.side_effect = [
|
|
mock.sentinel.source_context, mock.sentinel.dest_context]
|
|
|
|
event = objects.InstanceExternalEvent(
|
|
instance_uuid=instance.uuid, name='network-vif-plugged')
|
|
events = [event]
|
|
|
|
with mock.patch.object(self.compute_api.compute_rpcapi,
|
|
'external_instance_event') as rpc_mock:
|
|
self.compute_api.external_instance_event(
|
|
self.context, [instance], events)
|
|
|
|
# We should have gotten the migration because of the migration_context.
|
|
get_mig_by_id.assert_called_once_with(self.context, migration.id)
|
|
# We should have gotten two host mappings (for source and dest).
|
|
self.assertEqual(2, get_hm_by_host.call_count)
|
|
get_hm_by_host.assert_has_calls([
|
|
mock.call(self.context, migration.source_compute),
|
|
mock.call(self.context, migration.dest_compute)])
|
|
self.assertEqual(2, get_admin_context.call_count)
|
|
# We should have targeted a context to both cells.
|
|
self.assertEqual(2, set_target_cell.call_count)
|
|
set_target_cell.assert_has_calls([
|
|
mock.call(mock.sentinel.source_context, source_cell_mapping),
|
|
mock.call(mock.sentinel.dest_context, dest_cell_mapping)])
|
|
# We should have RPC cast to both hosts in different cells.
|
|
self.assertEqual(2, rpc_mock.call_count)
|
|
rpc_mock.assert_has_calls([
|
|
mock.call(mock.sentinel.source_context, [instance], events,
|
|
host=migration.source_compute),
|
|
mock.call(mock.sentinel.dest_context, [instance], events,
|
|
host=migration.dest_compute)],
|
|
# The rpc calls are based on iterating over a dict which is not
|
|
# ordered so we have to just assert the calls in any order.
|
|
any_order=True)
|
|
|
|
def test_volume_ops_invalid_task_state(self):
|
|
instance = self._create_instance_obj()
|
|
self.assertEqual(instance.vm_state, vm_states.ACTIVE)
|
|
instance.task_state = 'Any'
|
|
volume_id = uuidutils.generate_uuid()
|
|
self.assertRaises(exception.InstanceInvalidState,
|
|
self.compute_api.attach_volume,
|
|
self.context, instance, volume_id)
|
|
|
|
self.assertRaises(exception.InstanceInvalidState,
|
|
self.compute_api.detach_volume,
|
|
self.context, instance, volume_id)
|
|
|
|
new_volume_id = uuidutils.generate_uuid()
|
|
self.assertRaises(exception.InstanceInvalidState,
|
|
self.compute_api.swap_volume,
|
|
self.context, instance,
|
|
volume_id, new_volume_id)
|
|
|
|
@mock.patch.object(cinder.API, 'get',
|
|
side_effect=exception.CinderConnectionFailed(reason='error'))
|
|
def test_get_bdm_image_metadata_with_cinder_down(self, mock_get):
|
|
bdms = [objects.BlockDeviceMapping(
|
|
**fake_block_device.FakeDbBlockDeviceDict(
|
|
{
|
|
'id': 1,
|
|
'volume_id': 1,
|
|
'source_type': 'volume',
|
|
'destination_type': 'volume',
|
|
'device_name': 'vda',
|
|
}))]
|
|
self.assertRaises(exception.CinderConnectionFailed,
|
|
utils.get_bdm_image_metadata,
|
|
self.context,
|
|
self.compute_api.image_api,
|
|
self.compute_api.volume_api,
|
|
bdms, legacy_bdm=True)
|
|
|
|
def test_get_volumes_for_bdms_errors(self):
|
|
"""Simple test to make sure _get_volumes_for_bdms raises up errors."""
|
|
# Use a mix of pre-existing and source_type=image volumes to test the
|
|
# filtering logic in the method.
|
|
bdms = objects.BlockDeviceMappingList(objects=[
|
|
objects.BlockDeviceMapping(source_type='image', volume_id=None),
|
|
objects.BlockDeviceMapping(source_type='volume',
|
|
volume_id=uuids.volume_id)])
|
|
for exc in (
|
|
exception.VolumeNotFound(volume_id=uuids.volume_id),
|
|
exception.CinderConnectionFailed(reason='gremlins'),
|
|
exception.Forbidden()
|
|
):
|
|
with mock.patch.object(self.compute_api.volume_api, 'get',
|
|
side_effect=exc) as mock_vol_get:
|
|
self.assertRaises(type(exc),
|
|
self.compute_api._get_volumes_for_bdms,
|
|
self.context, bdms)
|
|
mock_vol_get.assert_called_once_with(self.context, uuids.volume_id)
|
|
|
|
@ddt.data(True, False)
|
|
def test_validate_vol_az_for_create_multiple_vols_diff_az(self, cross_az):
|
|
"""Tests cross_az_attach=True|False scenarios where the volumes are
|
|
in different zones.
|
|
"""
|
|
self.flags(cross_az_attach=cross_az, group='cinder')
|
|
volumes = [{'availability_zone': str(x)} for x in range(2)]
|
|
if cross_az:
|
|
# Since cross_az_attach=True (the default) we do not care that the
|
|
# volumes are in different zones.
|
|
self.assertIsNone(self.compute_api._validate_vol_az_for_create(
|
|
None, volumes))
|
|
else:
|
|
# In this case the volumes cannot be in different zones.
|
|
ex = self.assertRaises(
|
|
exception.MismatchVolumeAZException,
|
|
self.compute_api._validate_vol_az_for_create, None, volumes)
|
|
self.assertIn('Volumes are in different availability zones: 0,1',
|
|
six.text_type(ex))
|
|
|
|
def test_validate_vol_az_for_create_vol_az_matches_default_cpu_az(self):
|
|
"""Tests the scenario that the instance is not being created in a
|
|
specific zone and the volume's zone matches
|
|
CONF.default_availabilty_zone so None is returned indicating the
|
|
RequestSpec.availability_zone does not need to be updated.
|
|
"""
|
|
self.flags(cross_az_attach=False, group='cinder')
|
|
volumes = [{'availability_zone': CONF.default_availability_zone}]
|
|
self.assertIsNone(self.compute_api._validate_vol_az_for_create(
|
|
None, volumes))
|
|
|
|
@mock.patch.object(cinder.API, 'get_snapshot',
|
|
side_effect=exception.CinderConnectionFailed(reason='error'))
|
|
def test_validate_bdm_with_cinder_down(self, mock_get_snapshot):
|
|
instance = self._create_instance_obj()
|
|
instance_type = self._create_flavor()
|
|
bdms = [objects.BlockDeviceMapping(
|
|
**fake_block_device.FakeDbBlockDeviceDict(
|
|
{
|
|
'id': 1,
|
|
'snapshot_id': 1,
|
|
'source_type': 'volume',
|
|
'destination_type': 'volume',
|
|
'device_name': 'vda',
|
|
'boot_index': 0,
|
|
}))]
|
|
image_cache = volumes = {}
|
|
self.assertRaises(exception.CinderConnectionFailed,
|
|
self.compute_api._validate_bdm,
|
|
self.context,
|
|
instance, instance_type, bdms, image_cache, volumes)
|
|
|
|
@mock.patch.object(cinder.API, 'attachment_create',
|
|
side_effect=exception.InvalidInput(reason='error'))
|
|
def test_validate_bdm_with_error_volume_new_flow(self, mock_attach_create):
|
|
# Tests that an InvalidInput exception raised from
|
|
# volume_api.attachment_create due to the volume status not being
|
|
# 'available' results in _validate_bdm re-raising InvalidVolume.
|
|
instance = self._create_instance_obj()
|
|
del instance.id
|
|
instance_type = self._create_flavor()
|
|
volume_id = 'e856840e-9f5b-4894-8bde-58c6e29ac1e8'
|
|
volume_info = {'status': 'error',
|
|
'attach_status': 'detached',
|
|
'id': volume_id, 'multiattach': False}
|
|
bdms = [objects.BlockDeviceMapping(
|
|
**fake_block_device.FakeDbBlockDeviceDict(
|
|
{
|
|
'boot_index': 0,
|
|
'volume_id': volume_id,
|
|
'source_type': 'volume',
|
|
'destination_type': 'volume',
|
|
'device_name': 'vda',
|
|
}))]
|
|
|
|
self.assertRaises(exception.InvalidVolume,
|
|
self.compute_api._validate_bdm,
|
|
self.context,
|
|
instance, instance_type, bdms, {},
|
|
{volume_id: volume_info})
|
|
|
|
mock_attach_create.assert_called_once_with(
|
|
self.context, volume_id, instance.uuid)
|
|
|
|
def test_validate_bdm_missing_boot_index(self):
|
|
"""Tests that _validate_bdm will fail if there is no boot_index=0 entry
|
|
"""
|
|
bdms = objects.BlockDeviceMappingList(objects=[
|
|
objects.BlockDeviceMapping(
|
|
boot_index=None, image_id=uuids.image_id,
|
|
source_type='image', destination_type='volume')])
|
|
image_cache = volumes = {}
|
|
self.assertRaises(exception.InvalidBDMBootSequence,
|
|
self.compute_api._validate_bdm,
|
|
self.context, objects.Instance(), objects.Flavor(),
|
|
bdms, image_cache, volumes)
|
|
|
|
def test_validate_bdm_with_volume_type_name_is_specified(self):
|
|
"""Test _check_requested_volume_type method is used.
|
|
"""
|
|
instance = self._create_instance_obj()
|
|
instance_type = self._create_flavor()
|
|
|
|
volume_type = 'fake_lvm_1'
|
|
volume_types = [{'id': 'fake_volume_type_id_1', 'name': 'fake_lvm_1'},
|
|
{'id': 'fake_volume_type_id_2', 'name': 'fake_lvm_2'}]
|
|
|
|
bdm1 = objects.BlockDeviceMapping(
|
|
**fake_block_device.AnonFakeDbBlockDeviceDict(
|
|
{
|
|
'uuid': uuids.image_id,
|
|
'source_type': 'image',
|
|
'destination_type': 'volume',
|
|
'device_name': 'vda',
|
|
'boot_index': 0,
|
|
'volume_size': 3,
|
|
'volume_type': 'fake_lvm_1'
|
|
}))
|
|
bdm2 = objects.BlockDeviceMapping(
|
|
**fake_block_device.AnonFakeDbBlockDeviceDict(
|
|
{
|
|
'uuid': uuids.image_id,
|
|
'source_type': 'snapshot',
|
|
'destination_type': 'volume',
|
|
'device_name': 'vdb',
|
|
'boot_index': 1,
|
|
'volume_size': 3,
|
|
'volume_type': 'fake_lvm_1'
|
|
}))
|
|
bdms = [bdm1, bdm2]
|
|
|
|
with test.nested(
|
|
mock.patch.object(cinder.API, 'get_all_volume_types',
|
|
return_value=volume_types),
|
|
mock.patch.object(compute_api.API,
|
|
'_check_requested_volume_type')) as (
|
|
get_all_vol_types, vol_type_requested):
|
|
|
|
image_cache = volumes = {}
|
|
self.compute_api._validate_bdm(self.context, instance,
|
|
instance_type, bdms, image_cache,
|
|
volumes)
|
|
|
|
get_all_vol_types.assert_called_once_with(self.context)
|
|
|
|
vol_type_requested.assert_any_call(bdms[0], volume_type,
|
|
volume_types)
|
|
vol_type_requested.assert_any_call(bdms[1], volume_type,
|
|
volume_types)
|
|
|
|
@mock.patch('nova.compute.api.API._get_image')
|
|
def test_validate_bdm_missing_volume_size(self, mock_get_image):
|
|
"""Tests that _validate_bdm fail if there volume_size not provided
|
|
"""
|
|
instance = self._create_instance_obj()
|
|
# first we test the case of instance.image_ref == bdm.image_id
|
|
bdms = objects.BlockDeviceMappingList(objects=[
|
|
objects.BlockDeviceMapping(
|
|
boot_index=0, image_id=instance.image_ref,
|
|
source_type='image', destination_type='volume',
|
|
volume_type=None, snapshot_id=None, volume_id=None,
|
|
volume_size=None)])
|
|
image_cache = volumes = {}
|
|
self.assertRaises(exception.InvalidBDM,
|
|
self.compute_api._validate_bdm,
|
|
self.context, instance, objects.Flavor(),
|
|
bdms, image_cache, volumes)
|
|
self.assertEqual(0, mock_get_image.call_count)
|
|
# then we test the case of instance.image_ref != bdm.image_id
|
|
image_id = uuids.image_id
|
|
bdms = objects.BlockDeviceMappingList(objects=[
|
|
objects.BlockDeviceMapping(
|
|
boot_index=0, image_id=image_id,
|
|
source_type='image', destination_type='volume',
|
|
volume_type=None, snapshot_id=None, volume_id=None,
|
|
volume_size=None)])
|
|
self.assertRaises(exception.InvalidBDM,
|
|
self.compute_api._validate_bdm,
|
|
self.context, instance, objects.Flavor(),
|
|
bdms, image_cache, volumes)
|
|
mock_get_image.assert_called_once_with(self.context, image_id)
|
|
|
|
def test_the_specified_volume_type_id_assignment_to_name(self):
|
|
"""Test _check_requested_volume_type method is called, if the user
|
|
is using the volume type ID, assign volume_type to volume type name.
|
|
"""
|
|
volume_type = 'fake_volume_type_id_1'
|
|
volume_types = [{'id': 'fake_volume_type_id_1', 'name': 'fake_lvm_1'},
|
|
{'id': 'fake_volume_type_id_2', 'name': 'fake_lvm_2'}]
|
|
|
|
bdms = [objects.BlockDeviceMapping(
|
|
**fake_block_device.AnonFakeDbBlockDeviceDict(
|
|
{
|
|
'uuid': uuids.image_id,
|
|
'source_type': 'image',
|
|
'destination_type': 'volume',
|
|
'device_name': 'vda',
|
|
'boot_index': 0,
|
|
'volume_size': 3,
|
|
'volume_type': 'fake_volume_type_id_1'
|
|
}))]
|
|
|
|
self.compute_api._check_requested_volume_type(bdms[0],
|
|
volume_type,
|
|
volume_types)
|
|
self.assertEqual(bdms[0].volume_type, volume_types[0]['name'])
|
|
|
|
def _test_provision_instances_with_cinder_error(self,
|
|
expected_exception):
|
|
@mock.patch('nova.compute.utils.check_num_instances_quota')
|
|
@mock.patch.object(objects.Instance, 'create')
|
|
@mock.patch.object(objects.RequestSpec, 'from_components')
|
|
def do_test(
|
|
mock_req_spec_from_components, _mock_create,
|
|
mock_check_num_inst_quota):
|
|
req_spec_mock = mock.MagicMock()
|
|
|
|
mock_check_num_inst_quota.return_value = 1
|
|
mock_req_spec_from_components.return_value = req_spec_mock
|
|
|
|
ctxt = context.RequestContext('fake-user', 'fake-project')
|
|
flavor = self._create_flavor()
|
|
min_count = max_count = 1
|
|
boot_meta = {
|
|
'id': 'fake-image-id',
|
|
'properties': {'mappings': []},
|
|
'status': 'fake-status',
|
|
'location': 'far-away'}
|
|
base_options = {'image_ref': 'fake-ref',
|
|
'display_name': 'fake-name',
|
|
'project_id': 'fake-project',
|
|
'availability_zone': None,
|
|
'metadata': {},
|
|
'access_ip_v4': None,
|
|
'access_ip_v6': None,
|
|
'config_drive': None,
|
|
'key_name': None,
|
|
'reservation_id': None,
|
|
'kernel_id': None,
|
|
'ramdisk_id': None,
|
|
'root_device_name': None,
|
|
'user_data': None,
|
|
'numa_topology': None,
|
|
'pci_requests': None,
|
|
'port_resource_requests': None}
|
|
security_groups = {}
|
|
block_device_mapping = objects.BlockDeviceMappingList(
|
|
objects=[objects.BlockDeviceMapping(
|
|
**fake_block_device.FakeDbBlockDeviceDict(
|
|
{
|
|
'id': 1,
|
|
'volume_id': 1,
|
|
'source_type': 'volume',
|
|
'destination_type': 'volume',
|
|
'device_name': 'vda',
|
|
'boot_index': 0,
|
|
}))])
|
|
shutdown_terminate = True
|
|
instance_group = None
|
|
check_server_group_quota = False
|
|
filter_properties = {'scheduler_hints': None,
|
|
'instance_type': flavor}
|
|
trusted_certs = None
|
|
|
|
self.assertRaises(expected_exception,
|
|
self.compute_api._provision_instances, ctxt,
|
|
flavor, min_count, max_count, base_options,
|
|
boot_meta, security_groups, block_device_mapping,
|
|
shutdown_terminate, instance_group,
|
|
check_server_group_quota, filter_properties,
|
|
None, objects.TagList(), trusted_certs, False)
|
|
|
|
do_test()
|
|
|
|
@mock.patch.object(cinder.API, 'get',
|
|
side_effect=exception.CinderConnectionFailed(reason='error'))
|
|
def test_provision_instances_with_cinder_down(self, mock_get):
|
|
self._test_provision_instances_with_cinder_error(
|
|
expected_exception=exception.CinderConnectionFailed)
|
|
|
|
@mock.patch.object(cinder.API, 'get', new=mock.Mock(
|
|
return_value={'id': '1', 'multiattach': False}))
|
|
@mock.patch.object(cinder.API, 'check_availability_zone', new=mock.Mock())
|
|
@mock.patch.object(cinder.API, 'attachment_create', new=mock.Mock(
|
|
side_effect=exception.InvalidInput(reason='error')))
|
|
def test_provision_instances_with_error_volume(self):
|
|
self._test_provision_instances_with_cinder_error(
|
|
expected_exception=exception.InvalidVolume)
|
|
|
|
@mock.patch('nova.objects.RequestSpec.from_components')
|
|
@mock.patch('nova.objects.BuildRequest')
|
|
@mock.patch('nova.objects.Instance')
|
|
@mock.patch('nova.objects.InstanceMapping.create')
|
|
def test_provision_instances_with_keypair(self, mock_im, mock_instance,
|
|
mock_br, mock_rs):
|
|
fake_keypair = objects.KeyPair(name='test')
|
|
inst_type = self._create_flavor()
|
|
|
|
@mock.patch.object(self.compute_api, '_get_volumes_for_bdms')
|
|
@mock.patch.object(self.compute_api,
|
|
'_create_reqspec_buildreq_instmapping',
|
|
new=mock.MagicMock())
|
|
@mock.patch('nova.compute.utils.check_num_instances_quota')
|
|
@mock.patch('nova.network.security_group_api')
|
|
@mock.patch.object(self.compute_api,
|
|
'create_db_entry_for_new_instance')
|
|
@mock.patch.object(self.compute_api,
|
|
'_bdm_validate_set_size_and_instance')
|
|
def do_test(mock_bdm_v, mock_cdb, mock_sg, mock_cniq, mock_get_vols):
|
|
mock_cniq.return_value = 1
|
|
self.compute_api._provision_instances(self.context,
|
|
inst_type,
|
|
1, 1, mock.MagicMock(),
|
|
{}, None,
|
|
None, None, None, {}, None,
|
|
fake_keypair,
|
|
objects.TagList(), None,
|
|
False)
|
|
self.assertEqual(
|
|
'test',
|
|
mock_instance.return_value.keypairs.objects[0].name)
|
|
self.compute_api._provision_instances(self.context,
|
|
inst_type,
|
|
1, 1, mock.MagicMock(),
|
|
{}, None,
|
|
None, None, None, {}, None,
|
|
None, objects.TagList(),
|
|
None, False)
|
|
self.assertEqual(
|
|
0,
|
|
len(mock_instance.return_value.keypairs.objects))
|
|
|
|
do_test()
|
|
|
|
@mock.patch('nova.accelerator.cyborg.get_device_profile_request_groups')
|
|
@mock.patch('nova.objects.RequestSpec.from_components')
|
|
@mock.patch('nova.objects.BuildRequest')
|
|
@mock.patch('nova.objects.Instance')
|
|
@mock.patch('nova.objects.InstanceMapping.create')
|
|
def _test_provision_instances_with_accels(self,
|
|
instance_type, dp_request_groups, prev_request_groups,
|
|
mock_im, mock_instance, mock_br, mock_rs, mock_get_dp):
|
|
|
|
@mock.patch.object(self.compute_api, '_get_volumes_for_bdms')
|
|
@mock.patch.object(self.compute_api,
|
|
'_create_reqspec_buildreq_instmapping',
|
|
new=mock.MagicMock())
|
|
@mock.patch('nova.compute.utils.check_num_instances_quota')
|
|
@mock.patch('nova.network.security_group_api')
|
|
@mock.patch.object(self.compute_api,
|
|
'create_db_entry_for_new_instance')
|
|
@mock.patch.object(self.compute_api,
|
|
'_bdm_validate_set_size_and_instance')
|
|
def do_test(mock_bdm_v, mock_cdb, mock_sg, mock_cniq, mock_get_vols):
|
|
mock_cniq.return_value = 1
|
|
self.compute_api._provision_instances(self.context,
|
|
instance_type,
|
|
1, 1, mock.MagicMock(),
|
|
{}, None,
|
|
None, None, None, {}, None,
|
|
None,
|
|
objects.TagList(), None,
|
|
False)
|
|
|
|
mock_get_dp.return_value = dp_request_groups
|
|
fake_rs = fake_request_spec.fake_spec_obj()
|
|
fake_rs.requested_resources = prev_request_groups
|
|
mock_rs.return_value = fake_rs
|
|
do_test()
|
|
return mock_get_dp, fake_rs
|
|
|
|
def test_provision_instances_with_accels_ok(self):
|
|
# If extra_specs has accel spec, device profile's request_groups
|
|
# should be obtained, and added to reqspec's requested_resources.
|
|
dp_name = 'mydp'
|
|
extra_specs = {'extra_specs': {'accel:device_profile': dp_name}}
|
|
instance_type = self._create_flavor(**extra_specs)
|
|
|
|
prev_groups = [objects.RequestGroup(requester_id='prev0'),
|
|
objects.RequestGroup(requester_id='prev1')]
|
|
dp_groups = [objects.RequestGroup(requester_id='deviceprofile2'),
|
|
objects.RequestGroup(requester_id='deviceprofile3')]
|
|
|
|
mock_get_dp, fake_rs = self._test_provision_instances_with_accels(
|
|
instance_type, dp_groups, prev_groups)
|
|
mock_get_dp.assert_called_once_with(self.context, dp_name)
|
|
self.assertEqual(prev_groups + dp_groups, fake_rs.requested_resources)
|
|
|
|
def test_provision_instances_with_accels_no_dp(self):
|
|
# If extra specs has no accel spec, no attempt should be made to
|
|
# get device profile's request_groups, and reqspec.requested_resources
|
|
# should be left unchanged.
|
|
instance_type = self._create_flavor()
|
|
prev_groups = [objects.RequestGroup(requester_id='prev0'),
|
|
objects.RequestGroup(requester_id='prev1')]
|
|
mock_get_dp, fake_rs = self._test_provision_instances_with_accels(
|
|
instance_type, [], prev_groups)
|
|
mock_get_dp.assert_not_called()
|
|
self.assertEqual(prev_groups, fake_rs.requested_resources)
|
|
|
|
def test_provision_instances_creates_build_request(self):
|
|
@mock.patch.object(self.compute_api, '_get_volumes_for_bdms')
|
|
@mock.patch.object(self.compute_api,
|
|
'_create_reqspec_buildreq_instmapping')
|
|
@mock.patch.object(objects.Instance, 'create')
|
|
@mock.patch('nova.compute.utils.check_num_instances_quota')
|
|
@mock.patch.object(objects.RequestSpec, 'from_components')
|
|
def do_test(mock_req_spec_from_components, mock_check_num_inst_quota,
|
|
mock_inst_create, mock_create_rs_br_im, mock_get_volumes):
|
|
|
|
min_count = 1
|
|
max_count = 2
|
|
mock_check_num_inst_quota.return_value = 2
|
|
|
|
ctxt = context.RequestContext('fake-user', 'fake-project')
|
|
flavor = self._create_flavor()
|
|
boot_meta = {
|
|
'id': 'fake-image-id',
|
|
'properties': {'mappings': []},
|
|
'status': 'fake-status',
|
|
'location': 'far-away'}
|
|
base_options = {'image_ref': 'fake-ref',
|
|
'display_name': 'fake-name',
|
|
'project_id': 'fake-project',
|
|
'availability_zone': None,
|
|
'metadata': {},
|
|
'access_ip_v4': None,
|
|
'access_ip_v6': None,
|
|
'config_drive': None,
|
|
'key_name': None,
|
|
'reservation_id': None,
|
|
'kernel_id': None,
|
|
'ramdisk_id': None,
|
|
'root_device_name': None,
|
|
'user_data': None,
|
|
'numa_topology': None,
|
|
'pci_requests': None,
|
|
'port_resource_requests': None}
|
|
security_groups = {}
|
|
block_device_mappings = objects.BlockDeviceMappingList(
|
|
objects=[objects.BlockDeviceMapping(
|
|
**fake_block_device.FakeDbBlockDeviceDict(
|
|
{
|
|
'id': 1,
|
|
'volume_id': 1,
|
|
'source_type': 'volume',
|
|
'destination_type': 'volume',
|
|
'device_name': 'vda',
|
|
'boot_index': 0,
|
|
}))])
|
|
instance_tags = objects.TagList(objects=[objects.Tag(tag='tag')])
|
|
shutdown_terminate = True
|
|
trusted_certs = None
|
|
instance_group = None
|
|
check_server_group_quota = False
|
|
filter_properties = {'scheduler_hints': None,
|
|
'instance_type': flavor}
|
|
|
|
with mock.patch.object(
|
|
self.compute_api,
|
|
'_bdm_validate_set_size_and_instance',
|
|
return_value=block_device_mappings) as validate_bdm:
|
|
instances_to_build = self.compute_api._provision_instances(
|
|
ctxt, flavor,
|
|
min_count, max_count, base_options, boot_meta,
|
|
security_groups, block_device_mappings,
|
|
shutdown_terminate, instance_group,
|
|
check_server_group_quota, filter_properties, None,
|
|
instance_tags, trusted_certs, False)
|
|
validate_bdm.assert_has_calls([mock.call(
|
|
ctxt, test.MatchType(objects.Instance), flavor,
|
|
block_device_mappings, {}, mock_get_volumes.return_value,
|
|
False)] * max_count)
|
|
|
|
for rs, br, im in instances_to_build:
|
|
self.assertIsInstance(br.instance, objects.Instance)
|
|
self.assertTrue(uuidutils.is_uuid_like(br.instance.uuid))
|
|
self.assertEqual(base_options['project_id'],
|
|
br.instance.project_id)
|
|
self.assertEqual(1, br.block_device_mappings[0].id)
|
|
self.assertEqual(br.instance.uuid, br.tags[0].resource_id)
|
|
mock_create_rs_br_im.assert_any_call(ctxt, rs, br, im)
|
|
|
|
do_test()
|
|
|
|
def test_provision_instances_creates_instance_mapping(self):
|
|
@mock.patch.object(self.compute_api, '_get_volumes_for_bdms')
|
|
@mock.patch.object(self.compute_api,
|
|
'_create_reqspec_buildreq_instmapping',
|
|
new=mock.MagicMock())
|
|
@mock.patch('nova.compute.utils.check_num_instances_quota')
|
|
@mock.patch.object(objects.Instance, 'create', new=mock.MagicMock())
|
|
@mock.patch.object(self.compute_api, '_validate_bdm',
|
|
new=mock.MagicMock())
|
|
@mock.patch.object(objects.RequestSpec, 'from_components')
|
|
@mock.patch('nova.objects.InstanceMapping')
|
|
def do_test(mock_inst_mapping, mock_rs, mock_check_num_inst_quota,
|
|
mock_get_vols):
|
|
inst_mapping_mock = mock.MagicMock()
|
|
|
|
mock_check_num_inst_quota.return_value = 1
|
|
mock_inst_mapping.return_value = inst_mapping_mock
|
|
|
|
ctxt = context.RequestContext('fake-user', 'fake-project')
|
|
flavor = self._create_flavor()
|
|
min_count = max_count = 1
|
|
boot_meta = {
|
|
'id': 'fake-image-id',
|
|
'properties': {'mappings': []},
|
|
'status': 'fake-status',
|
|
'location': 'far-away'}
|
|
base_options = {'image_ref': 'fake-ref',
|
|
'display_name': 'fake-name',
|
|
'project_id': 'fake-project',
|
|
'availability_zone': None,
|
|
'metadata': {},
|
|
'access_ip_v4': None,
|
|
'access_ip_v6': None,
|
|
'config_drive': None,
|
|
'key_name': None,
|
|
'reservation_id': None,
|
|
'kernel_id': None,
|
|
'ramdisk_id': None,
|
|
'root_device_name': None,
|
|
'user_data': None,
|
|
'numa_topology': None,
|
|
'pci_requests': None,
|
|
'port_resource_requests': None}
|
|
security_groups = {}
|
|
block_device_mapping = objects.BlockDeviceMappingList(
|
|
objects=[objects.BlockDeviceMapping(
|
|
**fake_block_device.FakeDbBlockDeviceDict(
|
|
{
|
|
'id': 1,
|
|
'volume_id': 1,
|
|
'source_type': 'volume',
|
|
'destination_type': 'volume',
|
|
'device_name': 'vda',
|
|
'boot_index': 0,
|
|
}))])
|
|
shutdown_terminate = True
|
|
instance_group = None
|
|
check_server_group_quota = False
|
|
filter_properties = {'scheduler_hints': None,
|
|
'instance_type': flavor}
|
|
trusted_certs = None
|
|
|
|
instances_to_build = (
|
|
self.compute_api._provision_instances(ctxt, flavor,
|
|
min_count, max_count, base_options, boot_meta,
|
|
security_groups, block_device_mapping, shutdown_terminate,
|
|
instance_group, check_server_group_quota,
|
|
filter_properties, None, objects.TagList(), trusted_certs,
|
|
False))
|
|
rs, br, im = instances_to_build[0]
|
|
self.assertTrue(uuidutils.is_uuid_like(br.instance.uuid))
|
|
self.assertEqual(br.instance_uuid, im.instance_uuid)
|
|
|
|
self.assertEqual(br.instance.uuid,
|
|
inst_mapping_mock.instance_uuid)
|
|
self.assertIsNone(inst_mapping_mock.cell_mapping)
|
|
self.assertEqual(ctxt.project_id, inst_mapping_mock.project_id)
|
|
# Verify that the instance mapping created has user_id populated.
|
|
self.assertEqual(ctxt.user_id, inst_mapping_mock.user_id)
|
|
do_test()
|
|
|
|
@mock.patch.object(cinder.API, 'get',
|
|
return_value={'id': '1', 'multiattach': False})
|
|
@mock.patch.object(cinder.API, 'check_availability_zone',)
|
|
@mock.patch.object(cinder.API, 'attachment_create',
|
|
side_effect=[{'id': uuids.attachment_id},
|
|
exception.InvalidInput(reason='error')])
|
|
@mock.patch.object(objects.BlockDeviceMapping, 'save')
|
|
def test_provision_instances_cleans_up_when_volume_invalid_new_flow(self,
|
|
_mock_bdm, _mock_cinder_attach_create,
|
|
_mock_cinder_check_availability_zone, _mock_cinder_get):
|
|
@mock.patch.object(self.compute_api,
|
|
'_create_reqspec_buildreq_instmapping')
|
|
@mock.patch('nova.compute.utils.check_num_instances_quota')
|
|
@mock.patch.object(objects, 'Instance')
|
|
@mock.patch.object(objects.RequestSpec, 'from_components')
|
|
@mock.patch.object(objects, 'BuildRequest')
|
|
@mock.patch.object(objects, 'InstanceMapping')
|
|
def do_test(mock_inst_mapping, mock_build_req,
|
|
mock_req_spec_from_components, mock_inst,
|
|
mock_check_num_inst_quota, mock_create_rs_br_im):
|
|
|
|
min_count = 1
|
|
max_count = 2
|
|
mock_check_num_inst_quota.return_value = 2
|
|
req_spec_mock = mock.MagicMock()
|
|
mock_req_spec_from_components.return_value = req_spec_mock
|
|
inst_mocks = [mock.MagicMock() for i in range(max_count)]
|
|
for inst_mock in inst_mocks:
|
|
inst_mock.project_id = 'fake-project'
|
|
mock_inst.side_effect = inst_mocks
|
|
build_req_mocks = [mock.MagicMock() for i in range(max_count)]
|
|
mock_build_req.side_effect = build_req_mocks
|
|
inst_map_mocks = [mock.MagicMock() for i in range(max_count)]
|
|
mock_inst_mapping.side_effect = inst_map_mocks
|
|
|
|
ctxt = context.RequestContext('fake-user', 'fake-project')
|
|
flavor = self._create_flavor(extra_specs={})
|
|
boot_meta = {
|
|
'id': 'fake-image-id',
|
|
'properties': {'mappings': []},
|
|
'status': 'fake-status',
|
|
'location': 'far-away'}
|
|
base_options = {'image_ref': 'fake-ref',
|
|
'display_name': 'fake-name',
|
|
'project_id': 'fake-project',
|
|
'availability_zone': None,
|
|
'metadata': {},
|
|
'access_ip_v4': None,
|
|
'access_ip_v6': None,
|
|
'config_drive': None,
|
|
'key_name': None,
|
|
'reservation_id': None,
|
|
'kernel_id': None,
|
|
'ramdisk_id': None,
|
|
'root_device_name': None,
|
|
'user_data': None,
|
|
'numa_topology': None,
|
|
'pci_requests': None,
|
|
'port_resource_requests': None}
|
|
security_groups = {}
|
|
block_device_mapping = objects.BlockDeviceMappingList(
|
|
objects=[objects.BlockDeviceMapping(
|
|
**fake_block_device.FakeDbBlockDeviceDict(
|
|
{
|
|
'id': 1,
|
|
'volume_id': 1,
|
|
'source_type': 'volume',
|
|
'destination_type': 'volume',
|
|
'device_name': 'vda',
|
|
'boot_index': 0,
|
|
}))])
|
|
shutdown_terminate = True
|
|
instance_group = None
|
|
check_server_group_quota = False
|
|
filter_properties = {'scheduler_hints': None,
|
|
'instance_type': flavor}
|
|
tags = objects.TagList()
|
|
trusted_certs = None
|
|
self.assertRaises(exception.InvalidVolume,
|
|
self.compute_api._provision_instances, ctxt,
|
|
flavor, min_count, max_count, base_options,
|
|
boot_meta, security_groups, block_device_mapping,
|
|
shutdown_terminate, instance_group,
|
|
check_server_group_quota, filter_properties,
|
|
None, tags, trusted_certs, False)
|
|
# First instance, build_req, mapping is created and destroyed
|
|
mock_create_rs_br_im.assert_called_once_with(ctxt, req_spec_mock,
|
|
build_req_mocks[0],
|
|
inst_map_mocks[0])
|
|
self.assertTrue(build_req_mocks[0].destroy.called)
|
|
self.assertTrue(inst_map_mocks[0].destroy.called)
|
|
# Second instance, build_req, mapping is not created nor destroyed
|
|
self.assertFalse(inst_mocks[1].create.called)
|
|
self.assertFalse(inst_mocks[1].destroy.called)
|
|
self.assertFalse(build_req_mocks[1].destroy.called)
|
|
self.assertFalse(inst_map_mocks[1].destroy.called)
|
|
|
|
do_test()
|
|
|
|
def test_provision_instances_creates_reqspec_with_secgroups(self):
|
|
@mock.patch.object(self.compute_api,
|
|
'_create_reqspec_buildreq_instmapping',
|
|
new=mock.MagicMock())
|
|
@mock.patch('nova.compute.utils.check_num_instances_quota')
|
|
@mock.patch('nova.network.security_group_api'
|
|
'.populate_security_groups')
|
|
@mock.patch.object(compute_api, 'objects')
|
|
@mock.patch.object(self.compute_api,
|
|
'create_db_entry_for_new_instance',
|
|
new=mock.MagicMock())
|
|
@mock.patch.object(self.compute_api,
|
|
'_bdm_validate_set_size_and_instance',
|
|
new=mock.MagicMock())
|
|
def test(mock_objects, mock_secgroup, mock_cniq):
|
|
ctxt = context.RequestContext('fake-user', 'fake-project')
|
|
mock_cniq.return_value = 1
|
|
inst_type = self._create_flavor()
|
|
self.compute_api._provision_instances(ctxt, inst_type, None, None,
|
|
mock.MagicMock(), None, None,
|
|
[], None, None, None, None,
|
|
None, objects.TagList(),
|
|
None, False)
|
|
secgroups = mock_secgroup.return_value
|
|
mock_objects.RequestSpec.from_components.assert_called_once_with(
|
|
mock.ANY, mock.ANY, mock.ANY, mock.ANY, mock.ANY, mock.ANY,
|
|
mock.ANY, mock.ANY, mock.ANY,
|
|
security_groups=secgroups, port_resource_requests=mock.ANY)
|
|
test()
|
|
|
|
def _test_rescue(self, vm_state=vm_states.ACTIVE, rescue_password=None,
|
|
rescue_image=None, clean_shutdown=True):
|
|
instance = self._create_instance_obj(params={'vm_state': vm_state})
|
|
bdms = []
|
|
with test.nested(
|
|
mock.patch.object(objects.BlockDeviceMappingList,
|
|
'get_by_instance_uuid', return_value=bdms),
|
|
mock.patch.object(compute_utils, 'is_volume_backed_instance',
|
|
return_value=False),
|
|
mock.patch.object(instance, 'save'),
|
|
mock.patch.object(self.compute_api, '_record_action_start'),
|
|
mock.patch.object(self.compute_api.compute_rpcapi,
|
|
'rescue_instance')
|
|
) as (
|
|
bdm_get_by_instance_uuid, volume_backed_inst, instance_save,
|
|
record_action_start, rpcapi_rescue_instance
|
|
):
|
|
self.compute_api.rescue(self.context, instance,
|
|
rescue_password=rescue_password,
|
|
rescue_image_ref=rescue_image,
|
|
clean_shutdown=clean_shutdown)
|
|
# assert field values set on the instance object
|
|
self.assertEqual(task_states.RESCUING, instance.task_state)
|
|
# assert our mock calls
|
|
bdm_get_by_instance_uuid.assert_called_once_with(
|
|
self.context, instance.uuid)
|
|
volume_backed_inst.assert_called_once_with(
|
|
self.context, instance, bdms)
|
|
instance_save.assert_called_once_with(expected_task_state=[None])
|
|
record_action_start.assert_called_once_with(
|
|
self.context, instance, instance_actions.RESCUE)
|
|
rpcapi_rescue_instance.assert_called_once_with(
|
|
self.context, instance=instance,
|
|
rescue_password=rescue_password,
|
|
rescue_image_ref=rescue_image,
|
|
clean_shutdown=clean_shutdown)
|
|
|
|
def test_rescue_active(self):
|
|
self._test_rescue()
|
|
|
|
def test_rescue_stopped(self):
|
|
self._test_rescue(vm_state=vm_states.STOPPED)
|
|
|
|
def test_rescue_error(self):
|
|
self._test_rescue(vm_state=vm_states.ERROR)
|
|
|
|
def test_rescue_with_password(self):
|
|
self._test_rescue(rescue_password='fake-password')
|
|
|
|
def test_rescue_with_image(self):
|
|
self._test_rescue(rescue_image='fake-image')
|
|
|
|
def test_rescue_forced_shutdown(self):
|
|
self._test_rescue(clean_shutdown=False)
|
|
|
|
def test_unrescue(self):
|
|
instance = self._create_instance_obj(
|
|
params={'vm_state': vm_states.RESCUED})
|
|
with test.nested(
|
|
mock.patch.object(instance, 'save'),
|
|
mock.patch.object(self.compute_api, '_record_action_start'),
|
|
mock.patch.object(self.compute_api.compute_rpcapi,
|
|
'unrescue_instance')
|
|
) as (
|
|
instance_save, record_action_start, rpcapi_unrescue_instance
|
|
):
|
|
self.compute_api.unrescue(self.context, instance)
|
|
# assert field values set on the instance object
|
|
self.assertEqual(task_states.UNRESCUING, instance.task_state)
|
|
# assert our mock calls
|
|
instance_save.assert_called_once_with(expected_task_state=[None])
|
|
record_action_start.assert_called_once_with(
|
|
self.context, instance, instance_actions.UNRESCUE)
|
|
rpcapi_unrescue_instance.assert_called_once_with(
|
|
self.context, instance=instance)
|
|
|
|
@mock.patch('nova.objects.compute_node.ComputeNode'
|
|
'.get_by_host_and_nodename')
|
|
@mock.patch('nova.compute.utils.is_volume_backed_instance',
|
|
return_value=True)
|
|
@mock.patch('nova.objects.block_device.BlockDeviceMappingList'
|
|
'.get_by_instance_uuid')
|
|
def test_rescue_bfv_with_required_trait(self, mock_get_bdms,
|
|
mock_is_volume_backed,
|
|
mock_get_cn):
|
|
instance = self._create_instance_obj()
|
|
bdms = objects.BlockDeviceMappingList(objects=[
|
|
objects.BlockDeviceMapping(
|
|
boot_index=0, image_id=uuids.image_id, source_type='image',
|
|
destination_type='volume', volume_type=None,
|
|
snapshot_id=None, volume_id=uuids.volume_id,
|
|
volume_size=None)])
|
|
with test.nested(
|
|
mock.patch.object(self.compute_api.placementclient,
|
|
'get_provider_traits'),
|
|
mock.patch.object(self.compute_api.volume_api, 'get'),
|
|
mock.patch.object(self.compute_api.volume_api, 'check_attached'),
|
|
mock.patch.object(instance, 'save'),
|
|
mock.patch.object(self.compute_api, '_record_action_start'),
|
|
mock.patch.object(self.compute_api.compute_rpcapi,
|
|
'rescue_instance')
|
|
) as (
|
|
mock_get_traits, mock_get_volume, mock_check_attached,
|
|
mock_instance_save, mock_record_start, mock_rpcapi_rescue
|
|
):
|
|
# Mock out the returned compute node, bdms and volume
|
|
mock_get_cn.return_value = mock.Mock(uuid=uuids.cn)
|
|
mock_get_bdms.return_value = bdms
|
|
mock_get_volume.return_value = mock.sentinel.volume
|
|
|
|
# Ensure the required trait is returned, allowing BFV rescue
|
|
mock_trait_info = mock.Mock(traits=[ot.COMPUTE_RESCUE_BFV])
|
|
mock_get_traits.return_value = mock_trait_info
|
|
|
|
# Try to rescue the instance
|
|
self.compute_api.rescue(self.context, instance,
|
|
rescue_image_ref=uuids.rescue_image_id,
|
|
allow_bfv_rescue=True)
|
|
|
|
# Assert all of the calls made in the compute API
|
|
mock_get_bdms.assert_called_once_with(self.context, instance.uuid)
|
|
mock_get_volume.assert_called_once_with(
|
|
self.context, uuids.volume_id)
|
|
mock_check_attached.assert_called_once_with(
|
|
self.context, mock.sentinel.volume)
|
|
mock_is_volume_backed.assert_called_once_with(
|
|
self.context, instance, bdms)
|
|
mock_get_cn.assert_called_once_with(
|
|
self.context, instance.host, instance.node)
|
|
mock_get_traits.assert_called_once_with(self.context, uuids.cn)
|
|
mock_instance_save.assert_called_once_with(
|
|
expected_task_state=[None])
|
|
mock_record_start.assert_called_once_with(
|
|
self.context, instance, instance_actions.RESCUE)
|
|
mock_rpcapi_rescue.assert_called_once_with(
|
|
self.context, instance=instance, rescue_password=None,
|
|
rescue_image_ref=uuids.rescue_image_id, clean_shutdown=True)
|
|
|
|
# Assert that the instance task state as set in the compute API
|
|
self.assertEqual(task_states.RESCUING, instance.task_state)
|
|
|
|
@mock.patch('nova.objects.compute_node.ComputeNode'
|
|
'.get_by_host_and_nodename')
|
|
@mock.patch('nova.compute.utils.is_volume_backed_instance',
|
|
return_value=True)
|
|
@mock.patch('nova.objects.block_device.BlockDeviceMappingList'
|
|
'.get_by_instance_uuid')
|
|
def test_rescue_bfv_without_required_trait(self, mock_get_bdms,
|
|
mock_is_volume_backed,
|
|
mock_get_cn):
|
|
instance = self._create_instance_obj()
|
|
bdms = objects.BlockDeviceMappingList(objects=[
|
|
objects.BlockDeviceMapping(
|
|
boot_index=0, image_id=uuids.image_id, source_type='image',
|
|
destination_type='volume', volume_type=None,
|
|
snapshot_id=None, volume_id=uuids.volume_id,
|
|
volume_size=None)])
|
|
with test.nested(
|
|
mock.patch.object(self.compute_api.placementclient,
|
|
'get_provider_traits'),
|
|
mock.patch.object(self.compute_api.volume_api, 'get'),
|
|
mock.patch.object(self.compute_api.volume_api, 'check_attached'),
|
|
) as (
|
|
mock_get_traits, mock_get_volume, mock_check_attached
|
|
):
|
|
# Mock out the returned compute node, bdms and volume
|
|
mock_get_bdms.return_value = bdms
|
|
mock_get_volume.return_value = mock.sentinel.volume
|
|
mock_get_cn.return_value = mock.Mock(uuid=uuids.cn)
|
|
|
|
# Ensure the required trait is not returned, denying BFV rescue
|
|
mock_trait_info = mock.Mock(traits=[])
|
|
mock_get_traits.return_value = mock_trait_info
|
|
|
|
# Assert that any attempt to rescue a bfv instance on a compute
|
|
# node that does not report the COMPUTE_RESCUE_BFV trait fails and
|
|
# raises InstanceNotRescuable
|
|
self.assertRaises(exception.InstanceNotRescuable,
|
|
self.compute_api.rescue, self.context, instance,
|
|
rescue_image_ref=None, allow_bfv_rescue=True)
|
|
|
|
# Assert the calls made in the compute API prior to the failure
|
|
mock_get_bdms.assert_called_once_with(self.context, instance.uuid)
|
|
mock_get_volume.assert_called_once_with(
|
|
self.context, uuids.volume_id)
|
|
mock_check_attached.assert_called_once_with(
|
|
self.context, mock.sentinel.volume)
|
|
mock_is_volume_backed.assert_called_once_with(
|
|
self.context, instance, bdms)
|
|
mock_get_cn.assert_called_once_with(
|
|
self.context, instance.host, instance.node)
|
|
mock_get_traits.assert_called_once_with(
|
|
self.context, uuids.cn)
|
|
|
|
@mock.patch('nova.compute.utils.is_volume_backed_instance',
|
|
return_value=True)
|
|
@mock.patch('nova.objects.block_device.BlockDeviceMappingList'
|
|
'.get_by_instance_uuid')
|
|
def test_rescue_bfv_without_allow_flag(self, mock_get_bdms,
|
|
mock_is_volume_backed):
|
|
instance = self._create_instance_obj()
|
|
bdms = objects.BlockDeviceMappingList(objects=[
|
|
objects.BlockDeviceMapping(
|
|
boot_index=0, image_id=uuids.image_id, source_type='image',
|
|
destination_type='volume', volume_type=None,
|
|
snapshot_id=None, volume_id=uuids.volume_id,
|
|
volume_size=None)])
|
|
with test.nested(
|
|
mock.patch.object(self.compute_api.volume_api, 'get'),
|
|
mock.patch.object(self.compute_api.volume_api, 'check_attached'),
|
|
) as (
|
|
mock_get_volume, mock_check_attached
|
|
):
|
|
# Mock out the returned bdms and volume
|
|
mock_get_bdms.return_value = bdms
|
|
mock_get_volume.return_value = mock.sentinel.volume
|
|
|
|
# Assert that any attempt to rescue a bfv instance with
|
|
# allow_bfv_rescue=False fails and raises InstanceNotRescuable
|
|
self.assertRaises(exception.InstanceNotRescuable,
|
|
self.compute_api.rescue, self.context, instance,
|
|
rescue_image_ref=None, allow_bfv_rescue=False)
|
|
|
|
# Assert the calls made in the compute API prior to the failure
|
|
mock_get_bdms.assert_called_once_with(self.context, instance.uuid)
|
|
mock_get_volume.assert_called_once_with(
|
|
self.context, uuids.volume_id)
|
|
mock_check_attached.assert_called_once_with(
|
|
self.context, mock.sentinel.volume)
|
|
mock_is_volume_backed.assert_called_once_with(
|
|
self.context, instance, bdms)
|
|
|
|
def test_set_admin_password_invalid_state(self):
|
|
# Tests that InstanceInvalidState is raised when not ACTIVE.
|
|
instance = self._create_instance_obj({'vm_state': vm_states.STOPPED})
|
|
self.assertRaises(exception.InstanceInvalidState,
|
|
self.compute_api.set_admin_password,
|
|
self.context, instance)
|
|
|
|
def test_set_admin_password(self):
|
|
# Ensure instance can have its admin password set.
|
|
instance = self._create_instance_obj()
|
|
|
|
@mock.patch.object(objects.Instance, 'save')
|
|
@mock.patch.object(self.compute_api, '_record_action_start')
|
|
@mock.patch.object(self.compute_api.compute_rpcapi,
|
|
'set_admin_password')
|
|
def do_test(compute_rpcapi_mock, record_mock, instance_save_mock):
|
|
# call the API
|
|
self.compute_api.set_admin_password(self.context, instance, 'pass')
|
|
# make our assertions
|
|
instance_save_mock.assert_called_once_with(
|
|
expected_task_state=[None])
|
|
record_mock.assert_called_once_with(
|
|
self.context, instance, instance_actions.CHANGE_PASSWORD)
|
|
compute_rpcapi_mock.assert_called_once_with(
|
|
self.context, instance=instance, new_pass='pass')
|
|
|
|
do_test()
|
|
|
|
def _test_attach_interface_invalid_state(self, state):
|
|
instance = self._create_instance_obj(
|
|
params={'vm_state': state})
|
|
self.assertRaises(exception.InstanceInvalidState,
|
|
self.compute_api.attach_interface,
|
|
self.context, instance, '', '', '', [])
|
|
|
|
def test_attach_interface_invalid_state(self):
|
|
for state in [vm_states.BUILDING, vm_states.DELETED,
|
|
vm_states.ERROR, vm_states.RESCUED,
|
|
vm_states.RESIZED, vm_states.SOFT_DELETED,
|
|
vm_states.SUSPENDED, vm_states.SHELVED,
|
|
vm_states.SHELVED_OFFLOADED]:
|
|
self._test_attach_interface_invalid_state(state)
|
|
|
|
def _test_detach_interface_invalid_state(self, state):
|
|
instance = self._create_instance_obj(
|
|
params={'vm_state': state})
|
|
self.assertRaises(exception.InstanceInvalidState,
|
|
self.compute_api.detach_interface,
|
|
self.context, instance, '', '', '', [])
|
|
|
|
def test_detach_interface_invalid_state(self):
|
|
for state in [vm_states.BUILDING, vm_states.DELETED,
|
|
vm_states.ERROR, vm_states.RESCUED,
|
|
vm_states.RESIZED, vm_states.SOFT_DELETED,
|
|
vm_states.SUSPENDED, vm_states.SHELVED,
|
|
vm_states.SHELVED_OFFLOADED]:
|
|
self._test_detach_interface_invalid_state(state)
|
|
|
|
def _test_check_and_transform_bdm(self, block_device_mapping):
|
|
instance_type = self._create_flavor()
|
|
base_options = {'uuid': uuids.bdm_instance,
|
|
'image_ref': 'fake_image_ref',
|
|
'metadata': {}}
|
|
image_meta = {'status': 'active',
|
|
'name': 'image_name',
|
|
'deleted': False,
|
|
'container_format': 'bare',
|
|
'id': 'image_id'}
|
|
legacy_bdm = False
|
|
block_device_mapping = block_device_mapping
|
|
self.assertRaises(exception.InvalidRequest,
|
|
self.compute_api._check_and_transform_bdm,
|
|
self.context, base_options, instance_type,
|
|
image_meta, 1, 1, block_device_mapping, legacy_bdm)
|
|
|
|
def test_check_and_transform_bdm_source_volume(self):
|
|
block_device_mapping = [{'boot_index': 0,
|
|
'device_name': None,
|
|
'image_id': 'image_id',
|
|
'source_type': 'image'},
|
|
{'device_name': '/dev/vda',
|
|
'source_type': 'volume',
|
|
'destination_type': 'volume',
|
|
'device_type': None,
|
|
'volume_id': 'volume_id'}]
|
|
self._test_check_and_transform_bdm(block_device_mapping)
|
|
|
|
def test_check_and_transform_bdm_source_snapshot(self):
|
|
block_device_mapping = [{'boot_index': 0,
|
|
'device_name': None,
|
|
'image_id': 'image_id',
|
|
'source_type': 'image'},
|
|
{'device_name': '/dev/vda',
|
|
'source_type': 'snapshot',
|
|
'destination_type': 'volume',
|
|
'device_type': None,
|
|
'volume_id': 'volume_id'}]
|
|
self._test_check_and_transform_bdm(block_device_mapping)
|
|
|
|
def test_bdm_validate_set_size_and_instance(self):
|
|
swap_size = 42
|
|
ephemeral_size = 24
|
|
instance = self._create_instance_obj()
|
|
instance_type = self._create_flavor(swap=swap_size,
|
|
ephemeral_gb=ephemeral_size)
|
|
block_device_mapping = [
|
|
{'device_name': '/dev/sda1',
|
|
'source_type': 'snapshot', 'destination_type': 'volume',
|
|
'snapshot_id': '00000000-aaaa-bbbb-cccc-000000000000',
|
|
'delete_on_termination': False,
|
|
'boot_index': 0},
|
|
{'device_name': '/dev/sdb2',
|
|
'source_type': 'blank', 'destination_type': 'local',
|
|
'guest_format': 'swap', 'delete_on_termination': False},
|
|
{'device_name': '/dev/sdb3',
|
|
'source_type': 'blank', 'destination_type': 'local',
|
|
'guest_format': 'ext3', 'delete_on_termination': False}]
|
|
|
|
block_device_mapping = (
|
|
block_device_obj.block_device_make_list_from_dicts(
|
|
self.context,
|
|
map(fake_block_device.AnonFakeDbBlockDeviceDict,
|
|
block_device_mapping)))
|
|
|
|
with mock.patch.object(self.compute_api, '_validate_bdm'):
|
|
image_cache = volumes = {}
|
|
bdms = self.compute_api._bdm_validate_set_size_and_instance(
|
|
self.context, instance, instance_type, block_device_mapping,
|
|
image_cache, volumes)
|
|
|
|
expected = [{'device_name': '/dev/sda1',
|
|
'source_type': 'snapshot', 'destination_type': 'volume',
|
|
'snapshot_id': '00000000-aaaa-bbbb-cccc-000000000000',
|
|
'delete_on_termination': False,
|
|
'boot_index': 0},
|
|
{'device_name': '/dev/sdb2',
|
|
'source_type': 'blank', 'destination_type': 'local',
|
|
'guest_format': 'swap', 'delete_on_termination': False},
|
|
{'device_name': '/dev/sdb3',
|
|
'source_type': 'blank', 'destination_type': 'local',
|
|
'delete_on_termination': False}]
|
|
# Check that the bdm matches what was asked for and that instance_uuid
|
|
# and volume_size are set properly.
|
|
for exp, bdm in zip(expected, bdms):
|
|
self.assertEqual(exp['device_name'], bdm.device_name)
|
|
self.assertEqual(exp['destination_type'], bdm.destination_type)
|
|
self.assertEqual(exp['source_type'], bdm.source_type)
|
|
self.assertEqual(exp['delete_on_termination'],
|
|
bdm.delete_on_termination)
|
|
self.assertEqual(instance.uuid, bdm.instance_uuid)
|
|
self.assertEqual(swap_size, bdms[1].volume_size)
|
|
self.assertEqual(ephemeral_size, bdms[2].volume_size)
|
|
|
|
@mock.patch.object(objects.BuildRequestList, 'get_by_filters')
|
|
@mock.patch('nova.compute.instance_list.get_instance_objects_sorted')
|
|
@mock.patch.object(objects.CellMapping, 'get_by_uuid')
|
|
def test_tenant_to_project_conversion(self, mock_cell_map_get, mock_get,
|
|
mock_buildreq_get):
|
|
mock_cell_map_get.side_effect = exception.CellMappingNotFound(
|
|
uuid='fake')
|
|
mock_get.return_value = objects.InstanceList(objects=[]), list()
|
|
api = compute_api.API()
|
|
api.get_all(self.context, search_opts={'tenant_id': 'foo'})
|
|
filters = mock_get.call_args_list[0][0][1]
|
|
self.assertEqual({'project_id': 'foo'}, filters)
|
|
|
|
def test_populate_instance_names_host_name(self):
|
|
params = dict(display_name="vm1")
|
|
instance = self._create_instance_obj(params=params)
|
|
self.compute_api._populate_instance_names(instance, 1, 0)
|
|
self.assertEqual('vm1', instance.hostname)
|
|
|
|
def test_populate_instance_names_host_name_is_empty(self):
|
|
params = dict(display_name=u'\u865a\u62df\u673a\u662f\u4e2d\u6587')
|
|
instance = self._create_instance_obj(params=params)
|
|
self.compute_api._populate_instance_names(instance, 1, 0)
|
|
self.assertEqual('Server-%s' % instance.uuid, instance.hostname)
|
|
|
|
def test_populate_instance_names_host_name_multi(self):
|
|
params = dict(display_name="vm")
|
|
instance = self._create_instance_obj(params=params)
|
|
self.compute_api._populate_instance_names(instance, 2, 1)
|
|
self.assertEqual('vm-2', instance.hostname)
|
|
|
|
def test_populate_instance_names_host_name_is_empty_multi(self):
|
|
params = dict(display_name=u'\u865a\u62df\u673a\u662f\u4e2d\u6587')
|
|
instance = self._create_instance_obj(params=params)
|
|
self.compute_api._populate_instance_names(instance, 2, 1)
|
|
self.assertEqual('Server-%s' % instance.uuid, instance.hostname)
|
|
|
|
def test_host_statuses(self):
|
|
five_min_ago = timeutils.utcnow() - datetime.timedelta(minutes=5)
|
|
instances = [
|
|
objects.Instance(uuid=uuids.instance_1, host='host1', services=
|
|
self._obj_to_list_obj(objects.ServiceList(
|
|
self.context), objects.Service(id=0, host='host1',
|
|
disabled=True, forced_down=True,
|
|
binary='nova-compute'))),
|
|
objects.Instance(uuid=uuids.instance_2, host='host2', services=
|
|
self._obj_to_list_obj(objects.ServiceList(
|
|
self.context), objects.Service(id=0, host='host2',
|
|
disabled=True, forced_down=False,
|
|
binary='nova-compute'))),
|
|
objects.Instance(uuid=uuids.instance_3, host='host3', services=
|
|
self._obj_to_list_obj(objects.ServiceList(
|
|
self.context), objects.Service(id=0, host='host3',
|
|
disabled=False, last_seen_up=five_min_ago,
|
|
forced_down=False, binary='nova-compute'))),
|
|
objects.Instance(uuid=uuids.instance_4, host='host4', services=
|
|
self._obj_to_list_obj(objects.ServiceList(
|
|
self.context), objects.Service(id=0, host='host4',
|
|
disabled=False, last_seen_up=timeutils.utcnow(),
|
|
forced_down=False, binary='nova-compute'))),
|
|
objects.Instance(uuid=uuids.instance_5, host='host5', services=
|
|
objects.ServiceList()),
|
|
objects.Instance(uuid=uuids.instance_6, host=None, services=
|
|
self._obj_to_list_obj(objects.ServiceList(
|
|
self.context), objects.Service(id=0, host='host6',
|
|
disabled=True, forced_down=False,
|
|
binary='nova-compute'))),
|
|
objects.Instance(uuid=uuids.instance_7, host='host2', services=
|
|
self._obj_to_list_obj(objects.ServiceList(
|
|
self.context), objects.Service(id=0, host='host2',
|
|
disabled=True, forced_down=False,
|
|
binary='nova-compute')))
|
|
]
|
|
|
|
host_statuses = self.compute_api.get_instances_host_statuses(
|
|
instances)
|
|
expect_statuses = {uuids.instance_1: fields_obj.HostStatus.DOWN,
|
|
uuids.instance_2: fields_obj.HostStatus.MAINTENANCE,
|
|
uuids.instance_3: fields_obj.HostStatus.UNKNOWN,
|
|
uuids.instance_4: fields_obj.HostStatus.UP,
|
|
uuids.instance_5: fields_obj.HostStatus.NONE,
|
|
uuids.instance_6: fields_obj.HostStatus.NONE,
|
|
uuids.instance_7: fields_obj.HostStatus.MAINTENANCE}
|
|
for instance in instances:
|
|
self.assertEqual(expect_statuses[instance.uuid],
|
|
host_statuses[instance.uuid])
|
|
|
|
@mock.patch.object(objects.Migration, 'get_by_id_and_instance')
|
|
@mock.patch.object(objects.InstanceAction, 'action_start')
|
|
def test_live_migrate_force_complete_succeeded(
|
|
self, action_start, get_by_id_and_instance):
|
|
|
|
rpcapi = self.compute_api.compute_rpcapi
|
|
|
|
instance = self._create_instance_obj()
|
|
instance.task_state = task_states.MIGRATING
|
|
|
|
migration = objects.Migration()
|
|
migration.id = 0
|
|
migration.status = 'running'
|
|
get_by_id_and_instance.return_value = migration
|
|
|
|
with mock.patch.object(
|
|
rpcapi, 'live_migration_force_complete') as lm_force_complete:
|
|
self.compute_api.live_migrate_force_complete(
|
|
self.context, instance, migration)
|
|
|
|
lm_force_complete.assert_called_once_with(self.context,
|
|
instance,
|
|
migration)
|
|
action_start.assert_called_once_with(
|
|
self.context, instance.uuid, 'live_migration_force_complete',
|
|
want_result=False)
|
|
|
|
@mock.patch.object(objects.Migration, 'get_by_id_and_instance')
|
|
def test_live_migrate_force_complete_invalid_migration_state(
|
|
self, get_by_id_and_instance):
|
|
instance = self._create_instance_obj()
|
|
instance.task_state = task_states.MIGRATING
|
|
|
|
migration = objects.Migration()
|
|
migration.id = 0
|
|
migration.status = 'error'
|
|
get_by_id_and_instance.return_value = migration
|
|
|
|
self.assertRaises(exception.InvalidMigrationState,
|
|
self.compute_api.live_migrate_force_complete,
|
|
self.context, instance, migration.id)
|
|
|
|
def test_live_migrate_force_complete_invalid_vm_state(self):
|
|
instance = self._create_instance_obj()
|
|
instance.task_state = None
|
|
|
|
self.assertRaises(exception.InstanceInvalidState,
|
|
self.compute_api.live_migrate_force_complete,
|
|
self.context, instance, '1')
|
|
|
|
def _get_migration(self, migration_id, status, migration_type):
|
|
migration = objects.Migration()
|
|
migration.id = migration_id
|
|
migration.status = status
|
|
migration.migration_type = migration_type
|
|
return migration
|
|
|
|
@mock.patch('nova.compute.api.API._record_action_start')
|
|
@mock.patch.object(compute_rpcapi.ComputeAPI, 'live_migration_abort')
|
|
@mock.patch.object(objects.Migration, 'get_by_id_and_instance')
|
|
def test_live_migrate_abort_succeeded(self,
|
|
mock_get_migration,
|
|
mock_lm_abort,
|
|
mock_rec_action):
|
|
instance = self._create_instance_obj()
|
|
instance.task_state = task_states.MIGRATING
|
|
migration = self._get_migration(21, 'running', 'live-migration')
|
|
mock_get_migration.return_value = migration
|
|
|
|
self.compute_api.live_migrate_abort(self.context,
|
|
instance,
|
|
migration.id)
|
|
mock_rec_action.assert_called_once_with(self.context,
|
|
instance,
|
|
instance_actions.LIVE_MIGRATION_CANCEL)
|
|
mock_lm_abort.assert_called_once_with(self.context, instance,
|
|
migration.id)
|
|
|
|
@mock.patch('nova.compute.api.API._record_action_start')
|
|
@mock.patch.object(compute_rpcapi.ComputeAPI, 'live_migration_abort')
|
|
@mock.patch.object(objects.Migration, 'get_by_id_and_instance')
|
|
def test_live_migrate_abort_in_queue_succeeded(self,
|
|
mock_get_migration,
|
|
mock_lm_abort,
|
|
mock_rec_action):
|
|
instance = self._create_instance_obj()
|
|
instance.task_state = task_states.MIGRATING
|
|
for migration_status in ('queued', 'preparing'):
|
|
migration = self._get_migration(
|
|
21, migration_status, 'live-migration')
|
|
mock_get_migration.return_value = migration
|
|
self.compute_api.live_migrate_abort(self.context,
|
|
instance,
|
|
migration.id,
|
|
support_abort_in_queue=True)
|
|
mock_rec_action.assert_called_once_with(
|
|
self.context, instance, instance_actions.LIVE_MIGRATION_CANCEL)
|
|
mock_lm_abort.assert_called_once_with(self.context, instance,
|
|
migration.id)
|
|
mock_get_migration.reset_mock()
|
|
mock_rec_action.reset_mock()
|
|
mock_lm_abort.reset_mock()
|
|
|
|
@mock.patch.object(objects.Migration, 'get_by_id_and_instance')
|
|
def test_live_migration_abort_in_queue_old_microversion_fails(
|
|
self, mock_get_migration):
|
|
instance = self._create_instance_obj()
|
|
instance.task_state = task_states.MIGRATING
|
|
migration = self._get_migration(21, 'queued', 'live-migration')
|
|
mock_get_migration.return_value = migration
|
|
self.assertRaises(exception.InvalidMigrationState,
|
|
self.compute_api.live_migrate_abort, self.context,
|
|
instance, migration.id,
|
|
support_abort_in_queue=False)
|
|
|
|
@mock.patch.object(objects.Migration, 'get_by_id_and_instance')
|
|
def test_live_migration_abort_wrong_migration_status(self,
|
|
mock_get_migration):
|
|
instance = self._create_instance_obj()
|
|
instance.task_state = task_states.MIGRATING
|
|
migration = self._get_migration(21, 'completed', 'live-migration')
|
|
mock_get_migration.return_value = migration
|
|
|
|
self.assertRaises(exception.InvalidMigrationState,
|
|
self.compute_api.live_migrate_abort,
|
|
self.context,
|
|
instance,
|
|
migration.id)
|
|
|
|
def test_check_requested_networks_no_requested_networks(self):
|
|
# When there are no requested_networks we call validate_networks on
|
|
# the network API and return the results.
|
|
with mock.patch.object(self.compute_api.network_api,
|
|
'validate_networks', return_value=3):
|
|
count = self.compute_api._check_requested_networks(
|
|
self.context, None, 5)
|
|
self.assertEqual(3, count)
|
|
|
|
def test_check_requested_networks_no_allocate(self):
|
|
# When requested_networks is the single 'none' case for no allocation,
|
|
# we don't validate networks and return the count passed in.
|
|
requested_networks = (
|
|
objects.NetworkRequestList(
|
|
objects=[objects.NetworkRequest(network_id='none')]))
|
|
with mock.patch.object(self.compute_api.network_api,
|
|
'validate_networks') as validate:
|
|
count = self.compute_api._check_requested_networks(
|
|
self.context, requested_networks, 5)
|
|
self.assertEqual(5, count)
|
|
self.assertFalse(validate.called)
|
|
|
|
def test_check_requested_networks_auto_allocate(self):
|
|
# When requested_networks is the single 'auto' case for allocation,
|
|
# we validate networks and return the results.
|
|
requested_networks = (
|
|
objects.NetworkRequestList(
|
|
objects=[objects.NetworkRequest(network_id='auto')]))
|
|
with mock.patch.object(self.compute_api.network_api,
|
|
'validate_networks', return_value=4):
|
|
count = self.compute_api._check_requested_networks(
|
|
self.context, requested_networks, 5)
|
|
self.assertEqual(4, count)
|
|
|
|
@mock.patch.object(objects.InstanceMapping, 'save')
|
|
@mock.patch.object(objects.InstanceMapping, 'get_by_instance_uuid')
|
|
def test_update_queued_for_deletion(self, mock_get, mock_save):
|
|
uuid = uuids.inst
|
|
inst = objects.Instance(uuid=uuid)
|
|
im = objects.InstanceMapping(instance_uuid=uuid)
|
|
mock_get.return_value = im
|
|
self.compute_api._update_queued_for_deletion(self.context, inst, True)
|
|
self.assertTrue(im.queued_for_delete)
|
|
mock_get.assert_called_once_with(self.context, inst.uuid)
|
|
mock_save.assert_called_once_with()
|
|
|
|
@mock.patch.object(objects.InstanceMappingList,
|
|
'get_not_deleted_by_cell_and_project')
|
|
def test_generate_minimal_construct_for_down_cells(self, mock_get_ims):
|
|
im1 = objects.InstanceMapping(instance_uuid=uuids.inst1, cell_id=1,
|
|
project_id='fake', created_at=None, queued_for_delete=False)
|
|
mock_get_ims.return_value = [im1]
|
|
down_cell_uuids = [uuids.cell1, uuids.cell2, uuids.cell3]
|
|
result = self.compute_api._generate_minimal_construct_for_down_cells(
|
|
self.context, down_cell_uuids, [self.context.project_id], None)
|
|
for inst in result:
|
|
self.assertEqual(inst.uuid, im1.instance_uuid)
|
|
self.assertIn('created_at', inst)
|
|
# minimal construct doesn't contain the usual keys
|
|
self.assertNotIn('display_name', inst)
|
|
self.assertEqual(3, mock_get_ims.call_count)
|
|
|
|
@mock.patch.object(objects.InstanceMappingList,
|
|
'get_not_deleted_by_cell_and_project')
|
|
def test_generate_minimal_construct_for_down_cells_limited(self,
|
|
mock_get_ims):
|
|
im1 = objects.InstanceMapping(instance_uuid=uuids.inst1, cell_id=1,
|
|
project_id='fake', created_at=None, queued_for_delete=False)
|
|
# If this gets called a third time, it'll explode, thus asserting
|
|
# that we break out of the loop once the limit is reached
|
|
mock_get_ims.side_effect = [[im1, im1], [im1]]
|
|
down_cell_uuids = [uuids.cell1, uuids.cell2, uuids.cell3]
|
|
result = self.compute_api._generate_minimal_construct_for_down_cells(
|
|
self.context, down_cell_uuids, [self.context.project_id], 3)
|
|
for inst in result:
|
|
self.assertEqual(inst.uuid, im1.instance_uuid)
|
|
self.assertIn('created_at', inst)
|
|
# minimal construct doesn't contain the usual keys
|
|
self.assertNotIn('display_name', inst)
|
|
# Two instances at limit 3 from first cell, one at limit 1 from the
|
|
# second, no third call.
|
|
self.assertEqual(2, mock_get_ims.call_count)
|
|
mock_get_ims.assert_has_calls([
|
|
mock.call(self.context, uuids.cell1, [self.context.project_id],
|
|
limit=3),
|
|
mock.call(self.context, uuids.cell2, [self.context.project_id],
|
|
limit=1),
|
|
])
|
|
|
|
@mock.patch.object(objects.BuildRequestList, 'get_by_filters')
|
|
@mock.patch.object(objects.InstanceMappingList,
|
|
'get_not_deleted_by_cell_and_project')
|
|
def test_get_all_without_cell_down_support(self, mock_get_ims,
|
|
mock_buildreq_get):
|
|
mock_buildreq_get.return_value = objects.BuildRequestList()
|
|
im1 = objects.InstanceMapping(instance_uuid=uuids.inst1, cell_id=1,
|
|
project_id='fake', created_at=None, queued_for_delete=False)
|
|
mock_get_ims.return_value = [im1]
|
|
cell_instances = self._list_of_instances(2)
|
|
with mock.patch('nova.compute.instance_list.'
|
|
'get_instance_objects_sorted') as mock_inst_get:
|
|
mock_inst_get.return_value = objects.InstanceList(
|
|
self.context, objects=cell_instances), [uuids.cell1]
|
|
insts = self.compute_api.get_all(self.context,
|
|
cell_down_support=False)
|
|
fields = ['metadata', 'info_cache', 'security_groups']
|
|
mock_inst_get.assert_called_once_with(self.context, {}, None, None,
|
|
fields, None, None, cell_down_support=False)
|
|
for i, instance in enumerate(cell_instances):
|
|
self.assertEqual(instance, insts[i])
|
|
mock_get_ims.assert_not_called()
|
|
|
|
@mock.patch.object(objects.BuildRequestList, 'get_by_filters')
|
|
@mock.patch.object(objects.InstanceMappingList,
|
|
'get_not_deleted_by_cell_and_project')
|
|
def test_get_all_with_cell_down_support(self, mock_get_ims,
|
|
mock_buildreq_get):
|
|
mock_buildreq_get.return_value = objects.BuildRequestList()
|
|
im = objects.InstanceMapping(context=self.context,
|
|
instance_uuid=uuids.inst1, cell_id=1,
|
|
project_id='fake', created_at=None, queued_for_delete=False)
|
|
mock_get_ims.return_value = [im]
|
|
cell_instances = self._list_of_instances(2)
|
|
full_instances = objects.InstanceList(self.context,
|
|
objects=cell_instances)
|
|
inst = objects.Instance(context=self.context, uuid=im.instance_uuid,
|
|
project_id=im.project_id, created_at=im.created_at)
|
|
partial_instances = objects.InstanceList(self.context, objects=[inst])
|
|
with mock.patch('nova.compute.instance_list.'
|
|
'get_instance_objects_sorted') as mock_inst_get:
|
|
mock_inst_get.return_value = objects.InstanceList(
|
|
self.context, objects=cell_instances), [uuids.cell1]
|
|
insts = self.compute_api.get_all(self.context, limit=3,
|
|
cell_down_support=True)
|
|
fields = ['metadata', 'info_cache', 'security_groups']
|
|
mock_inst_get.assert_called_once_with(self.context, {},
|
|
3, None, fields, None, None,
|
|
cell_down_support=True)
|
|
for i, instance in enumerate(partial_instances + full_instances):
|
|
self.assertTrue(obj_base.obj_equal_prims(instance, insts[i]))
|
|
# With an original limit of 3, and 0 build requests but 2 instances
|
|
# from "up" cells, we should only get at most 1 instance mapping
|
|
# to fill the limit.
|
|
mock_get_ims.assert_called_once_with(self.context, uuids.cell1,
|
|
self.context.project_id,
|
|
limit=1)
|
|
|
|
@mock.patch.object(objects.BuildRequestList, 'get_by_filters')
|
|
@mock.patch.object(objects.InstanceMappingList,
|
|
'get_not_deleted_by_cell_and_project')
|
|
def test_get_all_with_cell_down_support_all_tenants(self, mock_get_ims,
|
|
mock_buildreq_get):
|
|
mock_buildreq_get.return_value = objects.BuildRequestList()
|
|
im = objects.InstanceMapping(context=self.context,
|
|
instance_uuid=uuids.inst1, cell_id=1,
|
|
project_id='fake', created_at=None, queued_for_delete=False)
|
|
mock_get_ims.return_value = [im]
|
|
inst = objects.Instance(context=self.context, uuid=im.instance_uuid,
|
|
project_id=im.project_id, created_at=im.created_at)
|
|
partial_instances = objects.InstanceList(self.context, objects=[inst])
|
|
with mock.patch('nova.compute.instance_list.'
|
|
'get_instance_objects_sorted') as mock_inst_get:
|
|
mock_inst_get.return_value = objects.InstanceList(
|
|
partial_instances), [uuids.cell1]
|
|
insts = self.compute_api.get_all(self.context, limit=3,
|
|
cell_down_support=True, all_tenants=True)
|
|
for i, instance in enumerate(partial_instances):
|
|
self.assertTrue(obj_base.obj_equal_prims(instance, insts[i]))
|
|
# get_not_deleted_by_cell_and_project is called with None
|
|
# project_id because of the all_tenants case.
|
|
mock_get_ims.assert_called_once_with(self.context, uuids.cell1,
|
|
None,
|
|
limit=3)
|
|
|
|
@mock.patch('nova.compute.api.API._save_user_id_in_instance_mapping',
|
|
new=mock.MagicMock())
|
|
@mock.patch.object(objects.Instance, 'get_by_uuid')
|
|
def test_get_instance_from_cell_success(self, mock_get_inst):
|
|
cell_mapping = objects.CellMapping(uuid=uuids.cell1,
|
|
name='1', id=1)
|
|
im = objects.InstanceMapping(instance_uuid=uuids.inst,
|
|
cell_mapping=cell_mapping)
|
|
mock_get_inst.return_value = objects.Instance(uuid=uuids.inst)
|
|
result = self.compute_api._get_instance_from_cell(self.context,
|
|
im, [], True)
|
|
self.assertEqual(uuids.inst, result.uuid)
|
|
mock_get_inst.assert_called_once()
|
|
|
|
@mock.patch.object(objects.Instance, 'get_by_uuid')
|
|
def test_get_instance_from_cell_failure(self, mock_get_inst):
|
|
# Make sure InstanceNotFound is bubbled up and not treated like
|
|
# other errors
|
|
mock_get_inst.side_effect = exception.InstanceNotFound(
|
|
instance_id=uuids.inst)
|
|
cell_mapping = objects.CellMapping(uuid=uuids.cell1,
|
|
name='1', id=1)
|
|
im = objects.InstanceMapping(instance_uuid=uuids.inst,
|
|
cell_mapping=cell_mapping)
|
|
exp = self.assertRaises(exception.InstanceNotFound,
|
|
self.compute_api._get_instance_from_cell, self.context,
|
|
im, [], False)
|
|
self.assertIn('could not be found', six.text_type(exp))
|
|
|
|
@mock.patch('nova.compute.api.API._save_user_id_in_instance_mapping')
|
|
@mock.patch.object(objects.RequestSpec, 'get_by_instance_uuid')
|
|
@mock.patch('nova.context.scatter_gather_cells')
|
|
def test_get_instance_with_cell_down_support(self, mock_sg, mock_rs,
|
|
mock_save_uid):
|
|
cell_mapping = objects.CellMapping(uuid=uuids.cell1,
|
|
name='1', id=1)
|
|
im1 = objects.InstanceMapping(instance_uuid=uuids.inst1,
|
|
cell_mapping=cell_mapping,
|
|
queued_for_delete=True)
|
|
im2 = objects.InstanceMapping(instance_uuid=uuids.inst2,
|
|
cell_mapping=cell_mapping,
|
|
queued_for_delete=False,
|
|
project_id='fake',
|
|
created_at=None)
|
|
mock_sg.return_value = {
|
|
uuids.cell1: context.did_not_respond_sentinel
|
|
}
|
|
|
|
# No cell down support, error means we return 500
|
|
exp = self.assertRaises(exception.NovaException,
|
|
self.compute_api._get_instance_from_cell, self.context,
|
|
im1, [], False)
|
|
self.assertIn('info is not available', six.text_type(exp))
|
|
|
|
# Have cell down support, error + queued_for_delete = NotFound
|
|
exp = self.assertRaises(exception.InstanceNotFound,
|
|
self.compute_api._get_instance_from_cell, self.context,
|
|
im1, [], True)
|
|
self.assertIn('could not be found', six.text_type(exp))
|
|
|
|
# Have cell down support, error + archived reqspec = NotFound
|
|
mock_rs.side_effect = exception.RequestSpecNotFound(
|
|
instance_uuid=uuids.inst2)
|
|
exp = self.assertRaises(exception.InstanceNotFound,
|
|
self.compute_api._get_instance_from_cell, self.context,
|
|
im2, [], True)
|
|
self.assertIn('could not be found', six.text_type(exp))
|
|
|
|
# Have cell down support, error + reqspec + not queued_for_delete
|
|
# means we return a minimal instance
|
|
req_spec = objects.RequestSpec(instance_uuid=uuids.inst2,
|
|
user_id='fake',
|
|
flavor=objects.Flavor(name='fake1'),
|
|
image=objects.ImageMeta(id=uuids.image,
|
|
name='fake1'),
|
|
availability_zone='nova')
|
|
mock_rs.return_value = req_spec
|
|
mock_rs.side_effect = None
|
|
result = self.compute_api._get_instance_from_cell(self.context,
|
|
im2, [], True)
|
|
self.assertIn('user_id', result)
|
|
self.assertNotIn('display_name', result)
|
|
self.assertEqual(uuids.inst2, result.uuid)
|
|
self.assertEqual('nova', result.availability_zone)
|
|
self.assertEqual(uuids.image, result.image_ref)
|
|
# Verify that user_id is populated during a compute_api.get().
|
|
mock_save_uid.assert_called_once_with(im2, result)
|
|
|
|
# Same as above, but boot-from-volume where image is not None but the
|
|
# id of the image is not set.
|
|
req_spec.image = objects.ImageMeta(name='fake1')
|
|
result = self.compute_api._get_instance_from_cell(self.context,
|
|
im2, [], True)
|
|
self.assertIsNone(result.image_ref)
|
|
|
|
# Same as above, but boot-from-volume where image is None
|
|
req_spec.image = None
|
|
result = self.compute_api._get_instance_from_cell(self.context,
|
|
im2, [], True)
|
|
self.assertIsNone(result.image_ref)
|
|
|
|
@mock.patch.object(objects.InstanceMapping, 'get_by_instance_uuid',
|
|
side_effect=exception.InstanceMappingNotFound(uuid='fake'))
|
|
@mock.patch.object(objects.BuildRequest, 'get_by_instance_uuid')
|
|
@mock.patch.object(objects.Instance, 'get_by_uuid')
|
|
def test_get_instance_no_mapping(self, mock_get_inst, mock_get_build_req,
|
|
mock_get_inst_map):
|
|
|
|
self.useFixture(nova_fixtures.AllServicesCurrent())
|
|
# No Mapping means NotFound
|
|
self.assertRaises(exception.InstanceNotFound,
|
|
self.compute_api.get, self.context,
|
|
uuids.inst_uuid)
|
|
|
|
@mock.patch.object(objects.InstanceMapping, 'get_by_instance_uuid')
|
|
@mock.patch.object(objects.BuildRequest, 'get_by_instance_uuid')
|
|
@mock.patch.object(objects.Instance, 'get_by_uuid')
|
|
def test_get_instance_not_in_cell(self, mock_get_inst, mock_get_build_req,
|
|
mock_get_inst_map):
|
|
build_req_obj = fake_build_request.fake_req_obj(self.context)
|
|
mock_get_inst_map.return_value = objects.InstanceMapping(
|
|
cell_mapping=None)
|
|
mock_get_build_req.return_value = build_req_obj
|
|
instance = build_req_obj.instance
|
|
mock_get_inst.return_value = instance
|
|
|
|
inst_from_build_req = self.compute_api.get(self.context, instance.uuid)
|
|
mock_get_inst_map.assert_called_once_with(self.context,
|
|
instance.uuid)
|
|
mock_get_build_req.assert_called_once_with(self.context,
|
|
instance.uuid)
|
|
self.assertEqual(instance, inst_from_build_req)
|
|
|
|
@mock.patch('nova.compute.api.API._save_user_id_in_instance_mapping',
|
|
new=mock.MagicMock())
|
|
@mock.patch.object(objects.InstanceMapping, 'get_by_instance_uuid')
|
|
@mock.patch.object(objects.BuildRequest, 'get_by_instance_uuid')
|
|
@mock.patch.object(objects.Instance, 'get_by_uuid')
|
|
def test_get_instance_not_in_cell_buildreq_deleted_inst_in_cell(
|
|
self, mock_get_inst, mock_get_build_req, mock_get_inst_map):
|
|
# This test checks the following scenario:
|
|
# The instance is not mapped to a cell, so it should be retrieved from
|
|
# a BuildRequest object. However the BuildRequest does not exist
|
|
# because the instance was put in a cell and mapped while while
|
|
# attempting to get the BuildRequest. So pull the instance from the
|
|
# cell.
|
|
self.useFixture(nova_fixtures.AllServicesCurrent())
|
|
build_req_obj = fake_build_request.fake_req_obj(self.context)
|
|
instance = build_req_obj.instance
|
|
inst_map = objects.InstanceMapping(cell_mapping=objects.CellMapping(
|
|
uuid=uuids.cell), instance_uuid=instance.uuid)
|
|
|
|
mock_get_inst_map.side_effect = [
|
|
objects.InstanceMapping(cell_mapping=None), inst_map]
|
|
mock_get_build_req.side_effect = exception.BuildRequestNotFound(
|
|
uuid=instance.uuid)
|
|
mock_get_inst.return_value = instance
|
|
|
|
inst_from_get = self.compute_api.get(self.context, instance.uuid)
|
|
|
|
inst_map_calls = [mock.call(self.context, instance.uuid),
|
|
mock.call(self.context, instance.uuid)]
|
|
mock_get_inst_map.assert_has_calls(inst_map_calls)
|
|
self.assertEqual(2, mock_get_inst_map.call_count)
|
|
mock_get_build_req.assert_called_once_with(self.context,
|
|
instance.uuid)
|
|
|
|
mock_get_inst.assert_called_once_with(self.context, instance.uuid,
|
|
expected_attrs=[
|
|
'metadata',
|
|
'system_metadata',
|
|
'security_groups',
|
|
'info_cache'])
|
|
self.assertEqual(instance, inst_from_get)
|
|
|
|
@mock.patch.object(context, 'target_cell')
|
|
@mock.patch.object(objects.InstanceMapping, 'get_by_instance_uuid')
|
|
@mock.patch.object(objects.BuildRequest, 'get_by_instance_uuid')
|
|
@mock.patch.object(objects.Instance, 'get_by_uuid')
|
|
def test_get_instance_not_in_cell_buildreq_deleted_inst_still_not_in_cell(
|
|
self, mock_get_inst, mock_get_build_req, mock_get_inst_map,
|
|
mock_target_cell):
|
|
# This test checks the following scenario:
|
|
# The instance is not mapped to a cell, so it should be retrieved from
|
|
# a BuildRequest object. However the BuildRequest does not exist which
|
|
# means it should now be possible to find the instance in a cell db.
|
|
# But the instance is not mapped which means the cellsv2 migration has
|
|
# not occurred in this scenario, so the instance is pulled from the
|
|
# configured Nova db.
|
|
|
|
# TODO(alaski): The tested case will eventually be an error condition.
|
|
# But until we force cellsv2 migrations we need this to work.
|
|
self.useFixture(nova_fixtures.AllServicesCurrent())
|
|
build_req_obj = fake_build_request.fake_req_obj(self.context)
|
|
instance = build_req_obj.instance
|
|
|
|
mock_get_inst_map.side_effect = [
|
|
objects.InstanceMapping(cell_mapping=None),
|
|
objects.InstanceMapping(cell_mapping=None)]
|
|
mock_get_build_req.side_effect = exception.BuildRequestNotFound(
|
|
uuid=instance.uuid)
|
|
mock_get_inst.return_value = instance
|
|
|
|
self.assertRaises(exception.InstanceNotFound,
|
|
self.compute_api.get,
|
|
self.context, instance.uuid)
|
|
|
|
@mock.patch('nova.objects.InstanceMapping.save')
|
|
def test_save_user_id_in_instance_mapping(self, im_save):
|
|
# Verify user_id is populated if it not set
|
|
im = objects.InstanceMapping()
|
|
i = objects.Instance(user_id='fake')
|
|
self.compute_api._save_user_id_in_instance_mapping(im, i)
|
|
self.assertEqual(im.user_id, i.user_id)
|
|
im_save.assert_called_once_with()
|
|
# Verify user_id is not saved if it is already set
|
|
im_save.reset_mock()
|
|
im.user_id = 'fake-other'
|
|
self.compute_api._save_user_id_in_instance_mapping(im, i)
|
|
self.assertNotEqual(im.user_id, i.user_id)
|
|
im_save.assert_not_called()
|
|
# Verify user_id is not saved if it is None
|
|
im_save.reset_mock()
|
|
im = objects.InstanceMapping()
|
|
i = objects.Instance(user_id=None)
|
|
self.compute_api._save_user_id_in_instance_mapping(im, i)
|
|
self.assertNotIn('user_id', im)
|
|
im_save.assert_not_called()
|
|
|
|
@mock.patch('nova.compute.api.API._save_user_id_in_instance_mapping')
|
|
@mock.patch.object(objects.InstanceMapping, 'get_by_instance_uuid')
|
|
@mock.patch.object(objects.BuildRequest, 'get_by_instance_uuid')
|
|
@mock.patch.object(objects.Instance, 'get_by_uuid')
|
|
def test_get_instance_in_cell(self, mock_get_inst, mock_get_build_req,
|
|
mock_get_inst_map, mock_save_uid):
|
|
self.useFixture(nova_fixtures.AllServicesCurrent())
|
|
# This just checks that the instance is looked up normally and not
|
|
# synthesized from a BuildRequest object. Verification of pulling the
|
|
# instance from the proper cell will be added when that capability is.
|
|
instance = self._create_instance_obj()
|
|
build_req_obj = fake_build_request.fake_req_obj(self.context)
|
|
inst_map = objects.InstanceMapping(cell_mapping=objects.CellMapping(
|
|
uuid=uuids.cell), instance_uuid=instance.uuid)
|
|
mock_get_inst_map.return_value = inst_map
|
|
mock_get_build_req.return_value = build_req_obj
|
|
mock_get_inst.return_value = instance
|
|
|
|
returned_inst = self.compute_api.get(self.context, instance.uuid)
|
|
mock_get_build_req.assert_not_called()
|
|
mock_get_inst_map.assert_called_once_with(self.context,
|
|
instance.uuid)
|
|
# Verify that user_id is populated during a compute_api.get().
|
|
mock_save_uid.assert_called_once_with(inst_map, instance)
|
|
self.assertEqual(instance, returned_inst)
|
|
mock_get_inst.assert_called_once_with(self.context, instance.uuid,
|
|
expected_attrs=[
|
|
'metadata',
|
|
'system_metadata',
|
|
'security_groups',
|
|
'info_cache'])
|
|
|
|
def _list_of_instances(self, length=1):
|
|
instances = []
|
|
for i in range(length):
|
|
instances.append(
|
|
fake_instance.fake_instance_obj(self.context, objects.Instance,
|
|
uuid=uuidutils.generate_uuid())
|
|
)
|
|
return instances
|
|
|
|
@mock.patch.object(objects.BuildRequestList, 'get_by_filters')
|
|
@mock.patch.object(objects.CellMapping, 'get_by_uuid',
|
|
side_effect=exception.CellMappingNotFound(uuid='fake'))
|
|
def test_get_all_includes_build_requests(self, mock_cell_mapping_get,
|
|
mock_buildreq_get):
|
|
|
|
build_req_instances = self._list_of_instances(2)
|
|
build_reqs = [objects.BuildRequest(self.context, instance=instance)
|
|
for instance in build_req_instances]
|
|
mock_buildreq_get.return_value = objects.BuildRequestList(self.context,
|
|
objects=build_reqs)
|
|
|
|
cell_instances = self._list_of_instances(2)
|
|
|
|
with mock.patch('nova.compute.instance_list.'
|
|
'get_instance_objects_sorted') as mock_inst_get:
|
|
mock_inst_get.return_value = objects.InstanceList(
|
|
self.context, objects=cell_instances), list()
|
|
|
|
instances = self.compute_api.get_all(
|
|
self.context, search_opts={'foo': 'bar'},
|
|
limit=None, marker='fake-marker', sort_keys=['baz'],
|
|
sort_dirs=['desc'])
|
|
|
|
mock_buildreq_get.assert_called_once_with(
|
|
self.context, {'foo': 'bar'}, limit=None, marker='fake-marker',
|
|
sort_keys=['baz'], sort_dirs=['desc'])
|
|
fields = ['metadata', 'info_cache', 'security_groups']
|
|
mock_inst_get.assert_called_once_with(
|
|
self.context, {'foo': 'bar'}, None, None,
|
|
fields, ['baz'], ['desc'], cell_down_support=False)
|
|
for i, instance in enumerate(build_req_instances + cell_instances):
|
|
self.assertEqual(instance, instances[i])
|
|
|
|
@mock.patch.object(objects.BuildRequestList, 'get_by_filters')
|
|
@mock.patch.object(objects.CellMapping, 'get_by_uuid',
|
|
side_effect=exception.CellMappingNotFound(uuid='fake'))
|
|
def test_get_all_includes_build_requests_filter_dupes(self,
|
|
mock_cell_mapping_get, mock_buildreq_get):
|
|
|
|
build_req_instances = self._list_of_instances(2)
|
|
build_reqs = [objects.BuildRequest(self.context, instance=instance)
|
|
for instance in build_req_instances]
|
|
mock_buildreq_get.return_value = objects.BuildRequestList(self.context,
|
|
objects=build_reqs)
|
|
|
|
cell_instances = self._list_of_instances(2)
|
|
|
|
with mock.patch('nova.compute.instance_list.'
|
|
'get_instance_objects_sorted') as mock_inst_get:
|
|
# Insert one of the build_req_instances here so it shows up twice
|
|
mock_inst_get.return_value = objects.InstanceList(self.context,
|
|
objects=build_req_instances[:1] + cell_instances), list()
|
|
|
|
instances = self.compute_api.get_all(
|
|
self.context, search_opts={'foo': 'bar'},
|
|
limit=None, marker='fake-marker', sort_keys=['baz'],
|
|
sort_dirs=['desc'])
|
|
|
|
mock_buildreq_get.assert_called_once_with(
|
|
self.context, {'foo': 'bar'}, limit=None, marker='fake-marker',
|
|
sort_keys=['baz'], sort_dirs=['desc'])
|
|
fields = ['metadata', 'info_cache', 'security_groups']
|
|
mock_inst_get.assert_called_once_with(
|
|
self.context, {'foo': 'bar'}, None, None,
|
|
fields, ['baz'], ['desc'], cell_down_support=False)
|
|
for i, instance in enumerate(build_req_instances + cell_instances):
|
|
self.assertEqual(instance, instances[i])
|
|
|
|
@mock.patch.object(objects.BuildRequestList, 'get_by_filters')
|
|
@mock.patch.object(objects.CellMapping, 'get_by_uuid',
|
|
side_effect=exception.CellMappingNotFound(uuid='fake'))
|
|
def test_get_all_build_requests_decrement_limit(self,
|
|
mock_cell_mapping_get,
|
|
mock_buildreq_get):
|
|
|
|
build_req_instances = self._list_of_instances(2)
|
|
build_reqs = [objects.BuildRequest(self.context, instance=instance)
|
|
for instance in build_req_instances]
|
|
mock_buildreq_get.return_value = objects.BuildRequestList(self.context,
|
|
objects=build_reqs)
|
|
|
|
cell_instances = self._list_of_instances(2)
|
|
|
|
with mock.patch('nova.compute.instance_list.'
|
|
'get_instance_objects_sorted') as mock_inst_get:
|
|
mock_inst_get.return_value = objects.InstanceList(
|
|
self.context, objects=cell_instances), list()
|
|
|
|
instances = self.compute_api.get_all(
|
|
self.context, search_opts={'foo': 'bar'},
|
|
limit=10, marker='fake-marker', sort_keys=['baz'],
|
|
sort_dirs=['desc'])
|
|
|
|
mock_buildreq_get.assert_called_once_with(
|
|
self.context, {'foo': 'bar'}, limit=10, marker='fake-marker',
|
|
sort_keys=['baz'], sort_dirs=['desc'])
|
|
fields = ['metadata', 'info_cache', 'security_groups']
|
|
mock_inst_get.assert_called_once_with(
|
|
self.context, {'foo': 'bar'}, 8, None,
|
|
fields, ['baz'], ['desc'], cell_down_support=False)
|
|
for i, instance in enumerate(build_req_instances + cell_instances):
|
|
self.assertEqual(instance, instances[i])
|
|
|
|
@mock.patch.object(context, 'target_cell')
|
|
@mock.patch.object(objects.BuildRequestList, 'get_by_filters')
|
|
@mock.patch.object(objects.CellMapping, 'get_by_uuid')
|
|
@mock.patch.object(objects.CellMappingList, 'get_all')
|
|
def test_get_all_includes_build_request_cell0(self, mock_cm_get_all,
|
|
mock_cell_mapping_get,
|
|
mock_buildreq_get, mock_target_cell):
|
|
|
|
build_req_instances = self._list_of_instances(2)
|
|
build_reqs = [objects.BuildRequest(self.context, instance=instance)
|
|
for instance in build_req_instances]
|
|
mock_buildreq_get.return_value = objects.BuildRequestList(self.context,
|
|
objects=build_reqs)
|
|
|
|
cell_instances = self._list_of_instances(2)
|
|
|
|
with mock.patch('nova.compute.instance_list.'
|
|
'get_instance_objects_sorted') as mock_inst_get:
|
|
mock_inst_get.return_value = objects.InstanceList(
|
|
self.context,
|
|
objects=cell_instances), []
|
|
|
|
instances = self.compute_api.get_all(
|
|
self.context, search_opts={'foo': 'bar'},
|
|
limit=10, marker='fake-marker', sort_keys=['baz'],
|
|
sort_dirs=['desc'])
|
|
|
|
for cm in mock_cm_get_all.return_value:
|
|
mock_target_cell.assert_any_call(self.context, cm)
|
|
|
|
fields = ['metadata', 'info_cache', 'security_groups']
|
|
mock_inst_get.assert_called_once_with(
|
|
mock.ANY, {'foo': 'bar'},
|
|
8, None,
|
|
fields, ['baz'], ['desc'], cell_down_support=False)
|
|
for i, instance in enumerate(build_req_instances +
|
|
cell_instances):
|
|
self.assertEqual(instance, instances[i])
|
|
|
|
@mock.patch.object(objects.BuildRequest, 'get_by_instance_uuid')
|
|
@mock.patch.object(objects.InstanceMapping, 'get_by_instance_uuid')
|
|
def test_update_existing_instance_not_in_cell(self, mock_instmap_get,
|
|
mock_buildreq_get):
|
|
mock_instmap_get.side_effect = exception.InstanceMappingNotFound(
|
|
uuid='fake')
|
|
self.useFixture(nova_fixtures.AllServicesCurrent())
|
|
|
|
instance = self._create_instance_obj()
|
|
# Just making sure that the instance has been created
|
|
self.assertIsNotNone(instance.id)
|
|
updates = {'display_name': 'foo_updated'}
|
|
with mock.patch.object(instance, 'save') as mock_inst_save:
|
|
returned_instance = self.compute_api.update_instance(
|
|
self.context, instance, updates)
|
|
mock_buildreq_get.assert_not_called()
|
|
self.assertEqual('foo_updated', returned_instance.display_name)
|
|
mock_inst_save.assert_called_once_with()
|
|
|
|
@mock.patch.object(objects.BuildRequest, 'get_by_instance_uuid')
|
|
@mock.patch.object(objects.InstanceMapping, 'get_by_instance_uuid')
|
|
def test_update_existing_instance_in_cell(self, mock_instmap_get,
|
|
mock_buildreq_get):
|
|
inst_map = objects.InstanceMapping(cell_mapping=objects.CellMapping())
|
|
mock_instmap_get.return_value = inst_map
|
|
self.useFixture(nova_fixtures.AllServicesCurrent())
|
|
|
|
instance = self._create_instance_obj()
|
|
# Just making sure that the instance has been created
|
|
self.assertIsNotNone(instance.id)
|
|
updates = {'display_name': 'foo_updated'}
|
|
with mock.patch.object(instance, 'save') as mock_inst_save:
|
|
returned_instance = self.compute_api.update_instance(
|
|
self.context, instance, updates)
|
|
mock_buildreq_get.assert_not_called()
|
|
self.assertEqual('foo_updated', returned_instance.display_name)
|
|
mock_inst_save.assert_called_once_with()
|
|
|
|
@mock.patch.object(objects.BuildRequest, 'get_by_instance_uuid')
|
|
def test_update_future_instance_with_buildreq(self, mock_buildreq_get):
|
|
|
|
# This test checks that a new instance which is not yet peristed in
|
|
# DB can be found by looking up the BuildRequest object so we can
|
|
# update it.
|
|
|
|
build_req_obj = fake_build_request.fake_req_obj(self.context)
|
|
mock_buildreq_get.return_value = build_req_obj
|
|
self.useFixture(nova_fixtures.AllServicesCurrent())
|
|
|
|
instance = self._create_instance_obj()
|
|
# Fake the fact that the instance is not yet persisted in DB
|
|
del instance.id
|
|
|
|
updates = {'display_name': 'foo_updated'}
|
|
with mock.patch.object(build_req_obj, 'save') as mock_buildreq_save:
|
|
returned_instance = self.compute_api.update_instance(
|
|
self.context, instance, updates)
|
|
|
|
mock_buildreq_get.assert_called_once_with(self.context, instance.uuid)
|
|
self.assertEqual(build_req_obj.instance, returned_instance)
|
|
mock_buildreq_save.assert_called_once_with()
|
|
self.assertEqual('foo_updated', returned_instance.display_name)
|
|
|
|
@mock.patch.object(context, 'target_cell')
|
|
@mock.patch.object(objects.Instance, 'get_by_uuid')
|
|
@mock.patch.object(objects.InstanceMapping, 'get_by_instance_uuid')
|
|
@mock.patch.object(objects.BuildRequest, 'get_by_instance_uuid')
|
|
def test_update_instance_in_cell_in_transition_state(self,
|
|
mock_buildreq_get,
|
|
mock_instmap_get,
|
|
mock_inst_get,
|
|
mock_target_cell):
|
|
|
|
# This test is for covering the following case:
|
|
# - when we lookup the instance initially, that one is not yet mapped
|
|
# to a cell and consequently we retrieve it from the BuildRequest
|
|
# - when we update the instance, that one could have been mapped
|
|
# meanwhile and the BuildRequest was deleted
|
|
# - if the instance is mapped, lookup the cell DB to find the instance
|
|
|
|
self.useFixture(nova_fixtures.AllServicesCurrent())
|
|
|
|
instance = self._create_instance_obj()
|
|
# Fake the fact that the instance is not yet persisted in DB
|
|
del instance.id
|
|
|
|
mock_buildreq_get.side_effect = exception.BuildRequestNotFound(
|
|
uuid=instance.uuid)
|
|
inst_map = objects.InstanceMapping(cell_mapping=objects.CellMapping())
|
|
mock_instmap_get.return_value = inst_map
|
|
mock_inst_get.return_value = instance
|
|
|
|
updates = {'display_name': 'foo_updated'}
|
|
with mock.patch.object(instance, 'save') as mock_inst_save:
|
|
returned_instance = self.compute_api.update_instance(
|
|
self.context, instance, updates)
|
|
|
|
mock_buildreq_get.assert_called_once_with(self.context, instance.uuid)
|
|
mock_target_cell.assert_called_once_with(self.context,
|
|
inst_map.cell_mapping)
|
|
mock_inst_save.assert_called_once_with()
|
|
self.assertEqual('foo_updated', returned_instance.display_name)
|
|
|
|
@mock.patch.object(objects.InstanceMapping, 'get_by_instance_uuid')
|
|
@mock.patch.object(objects.BuildRequest, 'get_by_instance_uuid')
|
|
def test_update_instance_not_in_cell_in_transition_state(self,
|
|
mock_buildreq_get,
|
|
mock_instmap_get):
|
|
|
|
# This test is for covering the following case:
|
|
# - when we lookup the instance initially, that one is not yet mapped
|
|
# to a cell and consequently we retrieve it from the BuildRequest
|
|
# - when we update the instance, that one could have been mapped
|
|
# meanwhile and the BuildRequest was deleted
|
|
# - if the instance is not mapped, lookup the API DB to find whether
|
|
# the instance was deleted
|
|
|
|
self.useFixture(nova_fixtures.AllServicesCurrent())
|
|
|
|
instance = self._create_instance_obj()
|
|
# Fake the fact that the instance is not yet persisted in DB
|
|
del instance.id
|
|
|
|
mock_buildreq_get.side_effect = exception.BuildRequestNotFound(
|
|
uuid=instance.uuid)
|
|
mock_instmap_get.side_effect = exception.InstanceMappingNotFound(
|
|
uuid='fake')
|
|
|
|
updates = {'display_name': 'foo_updated'}
|
|
with mock.patch.object(instance, 'save') as mock_inst_save:
|
|
self.assertRaises(exception.InstanceNotFound,
|
|
self.compute_api.update_instance,
|
|
self.context, instance, updates)
|
|
|
|
mock_buildreq_get.assert_called_once_with(self.context, instance.uuid)
|
|
mock_inst_save.assert_not_called()
|
|
|
|
def test_populate_instance_for_create_neutron_secgroups(self):
|
|
"""Tests that a list of security groups passed in do not actually get
|
|
stored on with the instance when using neutron.
|
|
"""
|
|
flavor = self._create_flavor()
|
|
params = {'display_name': 'fake-instance'}
|
|
instance = self._create_instance_obj(params, flavor)
|
|
security_groups = objects.SecurityGroupList()
|
|
security_groups.objects = [
|
|
secgroup_obj.SecurityGroup(uuid=uuids.secgroup_id)
|
|
]
|
|
instance = self.compute_api._populate_instance_for_create(
|
|
self.context, instance, {}, 0, security_groups, flavor, 1,
|
|
False)
|
|
self.assertEqual(0, len(instance.security_groups))
|
|
|
|
def test_retrieve_trusted_certs_object(self):
|
|
ids = ['0b5d2c72-12cc-4ba6-a8d7-3ff5cc1d8cb8',
|
|
'674736e3-f25c-405c-8362-bbf991e0ce0a']
|
|
|
|
retrieved_certs = self.compute_api._retrieve_trusted_certs_object(
|
|
self.context, ids)
|
|
self.assertEqual(ids, retrieved_certs.ids)
|
|
|
|
def test_retrieve_trusted_certs_object_conf(self):
|
|
ids = ['conf-trusted-cert-1', 'conf-trusted-cert-2']
|
|
|
|
self.flags(verify_glance_signatures=True, group='glance')
|
|
self.flags(enable_certificate_validation=True, group='glance')
|
|
self.flags(default_trusted_certificate_ids='conf-trusted-cert-1, '
|
|
'conf-trusted-cert-2',
|
|
group='glance')
|
|
retrieved_certs = self.compute_api._retrieve_trusted_certs_object(
|
|
self.context, None)
|
|
self.assertEqual(ids, retrieved_certs.ids)
|
|
|
|
def test_retrieve_trusted_certs_object_none(self):
|
|
self.flags(enable_certificate_validation=False, group='glance')
|
|
self.assertIsNone(
|
|
self.compute_api._retrieve_trusted_certs_object(self.context,
|
|
None))
|
|
|
|
def test_retrieve_trusted_certs_object_empty(self):
|
|
self.flags(enable_certificate_validation=False, group='glance')
|
|
self.assertIsNone(self.compute_api._retrieve_trusted_certs_object(
|
|
self.context, []))
|
|
|
|
@mock.patch('nova.objects.HostMapping.get_by_host')
|
|
@mock.patch('nova.objects.ComputeNode.get_by_host_and_nodename')
|
|
@mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
|
|
'get_provider_by_name')
|
|
def test__validate_host_or_node_with_host(
|
|
self, mock_get_provider_by_name, mock_get_host_node, mock_get_hm):
|
|
host = 'fake-host'
|
|
node = None
|
|
|
|
self.compute_api._validate_host_or_node(self.context, host, node)
|
|
mock_get_hm.assert_called_once_with(self.context, 'fake-host')
|
|
mock_get_host_node.assert_not_called()
|
|
mock_get_provider_by_name.assert_not_called()
|
|
|
|
@mock.patch('nova.objects.HostMapping.get_by_host')
|
|
@mock.patch('nova.objects.ComputeNode.get_by_host_and_nodename')
|
|
@mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
|
|
'get_provider_by_name')
|
|
def test__validate_host_or_node_with_invalid_host(
|
|
self, mock_get_provider_by_name, mock_get_host_node, mock_get_hm):
|
|
host = 'fake-host'
|
|
node = None
|
|
|
|
mock_get_hm.side_effect = exception.HostMappingNotFound(name=host)
|
|
self.assertRaises(exception.ComputeHostNotFound,
|
|
self.compute_api._validate_host_or_node,
|
|
self.context, host, node)
|
|
mock_get_hm.assert_called_once_with(self.context, 'fake-host')
|
|
mock_get_host_node.assert_not_called()
|
|
mock_get_provider_by_name.assert_not_called()
|
|
|
|
@mock.patch('nova.context.target_cell')
|
|
@mock.patch('nova.objects.HostMapping.get_by_host')
|
|
@mock.patch('nova.objects.ComputeNode.get_by_host_and_nodename')
|
|
@mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
|
|
'get_provider_by_name')
|
|
def test__validate_host_or_node_with_host_and_node(
|
|
self, mock_get_provider_by_name, mock_get_host_node, mock_get_hm,
|
|
mock_target_cell):
|
|
host = 'fake-host'
|
|
node = 'fake-host'
|
|
|
|
self.compute_api._validate_host_or_node(self.context, host, node)
|
|
mock_get_host_node.assert_called_once_with(
|
|
mock_target_cell.return_value.__enter__.return_value,
|
|
'fake-host', 'fake-host')
|
|
mock_get_hm.assert_called_once_with(self.context, 'fake-host')
|
|
mock_get_provider_by_name.assert_not_called()
|
|
|
|
@mock.patch('nova.context.target_cell')
|
|
@mock.patch('nova.objects.HostMapping.get_by_host')
|
|
@mock.patch('nova.objects.ComputeNode.get_by_host_and_nodename')
|
|
@mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
|
|
'get_provider_by_name')
|
|
def test__validate_host_or_node_with_invalid_host_and_node(
|
|
self, mock_get_provider_by_name, mock_get_host_node, mock_get_hm,
|
|
mock_target_cell):
|
|
host = 'fake-host'
|
|
node = 'fake-host'
|
|
|
|
mock_get_host_node.side_effect = (
|
|
exception.ComputeHostNotFound(host=host))
|
|
self.assertRaises(exception.ComputeHostNotFound,
|
|
self.compute_api._validate_host_or_node,
|
|
self.context, host, node)
|
|
mock_get_host_node.assert_called_once_with(
|
|
mock_target_cell.return_value.__enter__.return_value,
|
|
'fake-host', 'fake-host')
|
|
mock_get_hm.assert_called_once_with(self.context, 'fake-host')
|
|
mock_get_provider_by_name.assert_not_called()
|
|
|
|
@mock.patch('nova.objects.HostMapping.get_by_host')
|
|
@mock.patch('nova.objects.ComputeNode.get_by_host_and_nodename')
|
|
@mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
|
|
'get_provider_by_name')
|
|
def test__validate_host_or_node_with_node(
|
|
self, mock_get_provider_by_name, mock_get_host_node, mock_get_hm):
|
|
host = None
|
|
node = 'fake-host'
|
|
|
|
self.compute_api._validate_host_or_node(self.context, host, node)
|
|
mock_get_provider_by_name.assert_called_once_with(
|
|
self.context, 'fake-host')
|
|
mock_get_host_node.assert_not_called()
|
|
mock_get_hm.assert_not_called()
|
|
|
|
@mock.patch('nova.objects.HostMapping.get_by_host')
|
|
@mock.patch('nova.objects.ComputeNode.get_by_host_and_nodename')
|
|
@mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
|
|
'get_provider_by_name')
|
|
def test__validate_host_or_node_with_invalid_node(
|
|
self, mock_get_provider_by_name, mock_get_host_node, mock_get_hm):
|
|
host = None
|
|
node = 'fake-host'
|
|
|
|
mock_get_provider_by_name.side_effect = (
|
|
exception.ResourceProviderNotFound(name_or_uuid=node))
|
|
self.assertRaises(exception.ComputeHostNotFound,
|
|
self.compute_api._validate_host_or_node,
|
|
self.context, host, node)
|
|
mock_get_provider_by_name.assert_called_once_with(
|
|
self.context, 'fake-host')
|
|
mock_get_host_node.assert_not_called()
|
|
mock_get_hm.assert_not_called()
|
|
|
|
@mock.patch('nova.objects.HostMapping.get_by_host')
|
|
@mock.patch('nova.objects.ComputeNode.get_by_host_and_nodename')
|
|
@mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
|
|
'get_provider_by_name')
|
|
def test__validate_host_or_node_with_rp_500_exception(
|
|
self, mock_get_provider_by_name, mock_get_host_node, mock_get_hm):
|
|
host = None
|
|
node = 'fake-host'
|
|
|
|
mock_get_provider_by_name.side_effect = (
|
|
exception.PlacementAPIConnectFailure())
|
|
self.assertRaises(exception.PlacementAPIConnectFailure,
|
|
self.compute_api._validate_host_or_node,
|
|
self.context, host, node)
|
|
mock_get_provider_by_name.assert_called_once_with(
|
|
self.context, 'fake-host')
|
|
mock_get_host_node.assert_not_called()
|
|
mock_get_hm.assert_not_called()
|
|
|
|
|
|
class ComputeAPIUnitTestCase(_ComputeAPIUnitTestMixIn, test.NoDBTestCase):
|
|
def setUp(self):
|
|
super(ComputeAPIUnitTestCase, self).setUp()
|
|
self.compute_api = compute_api.API()
|
|
|
|
def test_resize_same_flavor_fails(self):
|
|
self.assertRaises(exception.CannotResizeToSameFlavor,
|
|
self._test_resize, same_flavor=True)
|
|
|
|
def test_find_service_in_cell_error_case(self):
|
|
self.assertRaises(exception.NovaException,
|
|
compute_api._find_service_in_cell, self.context)
|
|
|
|
@mock.patch('nova.objects.Service.get_by_id')
|
|
def test_find_service_in_cell_targets(self, mock_get_service):
|
|
mock_get_service.side_effect = [exception.NotFound(),
|
|
mock.sentinel.service]
|
|
compute_api.CELLS = [mock.sentinel.cell0, mock.sentinel.cell1]
|
|
|
|
@contextlib.contextmanager
|
|
def fake_target(context, cell):
|
|
yield 'context-for-%s' % cell
|
|
|
|
with mock.patch('nova.context.target_cell') as mock_target:
|
|
mock_target.side_effect = fake_target
|
|
s = compute_api._find_service_in_cell(self.context, service_id=123)
|
|
self.assertEqual(mock.sentinel.service, s)
|
|
cells = [call[0][0]
|
|
for call in mock_get_service.call_args_list]
|
|
self.assertEqual(['context-for-%s' % c for c in compute_api.CELLS],
|
|
cells)
|
|
|
|
def test__validate_numa_rebuild_non_numa(self):
|
|
"""Assert that a rebuild of an instance without a NUMA
|
|
topology passes validation.
|
|
"""
|
|
flavor = objects.Flavor(
|
|
id=42, vcpus=1, memory_mb=512, root_gb=1, extra_specs={})
|
|
instance = self._create_instance_obj(flavor=flavor)
|
|
# we use a dict instead of image metadata object as
|
|
# _validate_numa_rebuild constructs the object internally
|
|
image = {
|
|
'id': uuids.image_id, 'status': 'foo',
|
|
'properties': {}}
|
|
self.compute_api._validate_numa_rebuild(instance, image, flavor)
|
|
|
|
def test__validate_numa_rebuild_no_conflict(self):
|
|
"""Assert that a rebuild of an instance without a change
|
|
in NUMA topology passes validation.
|
|
"""
|
|
flavor = objects.Flavor(
|
|
id=42, vcpus=1, memory_mb=512, root_gb=1,
|
|
extra_specs={"hw:numa_nodes": 1})
|
|
instance = self._create_instance_obj(flavor=flavor)
|
|
# we use a dict instead of image metadata object as
|
|
# _validate_numa_rebuild constructs the object internally
|
|
image = {
|
|
'id': uuids.image_id, 'status': 'foo',
|
|
'properties': {}}
|
|
# The flavor creates a NUMA topology but the default image and the
|
|
# rebuild image do not have any image properties so there will
|
|
# be no conflict.
|
|
self.compute_api._validate_numa_rebuild(instance, image, flavor)
|
|
|
|
def test__validate_numa_rebuild_add_numa_toplogy(self):
|
|
"""Assert that a rebuild of an instance with a new image
|
|
that requests a NUMA topology when the original instance did not
|
|
have a NUMA topology is invalid.
|
|
"""
|
|
|
|
flavor = objects.Flavor(
|
|
id=42, vcpus=1, memory_mb=512, root_gb=1,
|
|
extra_specs={})
|
|
# _create_instance_obj results in the instance.image_meta being None.
|
|
instance = self._create_instance_obj(flavor=flavor)
|
|
# we use a dict instead of image metadata object as
|
|
# _validate_numa_rebuild constructs the object internally
|
|
image = {
|
|
'id': uuids.image_id, 'status': 'foo',
|
|
'properties': {"hw_numa_nodes": 1}}
|
|
# The flavor and default image have no NUMA topology defined. The image
|
|
# used to rebuild requests a NUMA topology which is not allowed as it
|
|
# would alter the NUMA constrains.
|
|
self.assertRaises(
|
|
exception.ImageNUMATopologyRebuildConflict,
|
|
self.compute_api._validate_numa_rebuild, instance, image, flavor)
|
|
|
|
def test__validate_numa_rebuild_remove_numa_toplogy(self):
|
|
"""Assert that a rebuild of an instance with a new image
|
|
that does not request a NUMA topology when the original image did
|
|
is invalid if it would alter the instances topology as a result.
|
|
"""
|
|
|
|
flavor = objects.Flavor(
|
|
id=42, vcpus=1, memory_mb=512, root_gb=1,
|
|
extra_specs={})
|
|
# _create_instance_obj results in the instance.image_meta being None.
|
|
instance = self._create_instance_obj(flavor=flavor)
|
|
# we use a dict instead of image metadata object as
|
|
# _validate_numa_rebuild constructs the object internally
|
|
old_image = {
|
|
'id': uuidutils.generate_uuid(), 'status': 'foo',
|
|
'properties': {"hw_numa_nodes": 1}}
|
|
old_image_meta = objects.ImageMeta.from_dict(old_image)
|
|
image = {
|
|
'id': uuidutils.generate_uuid(), 'status': 'foo',
|
|
'properties': {}}
|
|
with mock.patch(
|
|
'nova.objects.instance.Instance.image_meta',
|
|
new_callable=mock.PropertyMock(return_value=old_image_meta)):
|
|
# The old image has a NUMA topology defined but the new image
|
|
# used to rebuild does not. This would alter the NUMA constrains
|
|
# and therefor should raise.
|
|
self.assertRaises(
|
|
exception.ImageNUMATopologyRebuildConflict,
|
|
self.compute_api._validate_numa_rebuild, instance,
|
|
image, flavor)
|
|
|
|
def test__validate_numa_rebuild_alter_numa_toplogy(self):
|
|
"""Assert that a rebuild of an instance with a new image
|
|
that requests a different NUMA topology than the original image
|
|
is invalid.
|
|
"""
|
|
|
|
# NOTE(sean-k-mooney): we need to use 2 vcpus here or we will fail
|
|
# with a different exception ImageNUMATopologyAsymmetric when we
|
|
# construct the NUMA constrains as the rebuild image would result
|
|
# in an invalid topology.
|
|
flavor = objects.Flavor(
|
|
id=42, vcpus=2, memory_mb=512, root_gb=1,
|
|
extra_specs={})
|
|
# _create_instance_obj results in the instance.image_meta being None.
|
|
instance = self._create_instance_obj(flavor=flavor)
|
|
# we use a dict instead of image metadata object as
|
|
# _validate_numa_rebuild constructs the object internally
|
|
old_image = {
|
|
'id': uuidutils.generate_uuid(), 'status': 'foo',
|
|
'properties': {"hw_numa_nodes": 1}}
|
|
old_image_meta = objects.ImageMeta.from_dict(old_image)
|
|
image = {
|
|
'id': uuidutils.generate_uuid(), 'status': 'foo',
|
|
'properties': {"hw_numa_nodes": 2}}
|
|
with mock.patch(
|
|
'nova.objects.instance.Instance.image_meta',
|
|
new_callable=mock.PropertyMock(return_value=old_image_meta)):
|
|
# the original image requested 1 NUMA node and the image used
|
|
# for rebuild requests 2 so assert an error is raised.
|
|
self.assertRaises(
|
|
exception.ImageNUMATopologyRebuildConflict,
|
|
self.compute_api._validate_numa_rebuild, instance,
|
|
image, flavor)
|
|
|
|
@mock.patch('nova.pci.request.get_pci_requests_from_flavor')
|
|
def test_pmu_image_and_flavor_conflict(self, mock_request):
|
|
"""Tests that calling _validate_flavor_image_nostatus()
|
|
with an image that conflicts with the flavor raises but no
|
|
exception is raised if there is no conflict.
|
|
"""
|
|
image = {'id': uuids.image_id, 'status': 'foo',
|
|
'properties': {'hw_pmu': False}}
|
|
flavor = objects.Flavor(
|
|
vcpus=1, memory_mb=512, root_gb=1, extra_specs={'hw:pmu': "true"})
|
|
self.assertRaises(
|
|
exception.ImagePMUConflict,
|
|
self.compute_api._validate_flavor_image_nostatus,
|
|
self.context, image, flavor, None)
|
|
|
|
@mock.patch('nova.pci.request.get_pci_requests_from_flavor')
|
|
def test_pmu_image_and_flavor_same_value(self, mock_request):
|
|
# assert that if both the image and flavor are set to the same value
|
|
# no exception is raised and the function returns nothing.
|
|
flavor = objects.Flavor(
|
|
vcpus=1, memory_mb=512, root_gb=1, extra_specs={'hw:pmu': "true"})
|
|
|
|
image = {'id': uuids.image_id, 'status': 'foo',
|
|
'properties': {'hw_pmu': True}}
|
|
self.assertIsNone(self.compute_api._validate_flavor_image_nostatus(
|
|
self.context, image, flavor, None))
|
|
|
|
@mock.patch('nova.pci.request.get_pci_requests_from_flavor')
|
|
def test_pmu_image_only(self, mock_request):
|
|
# assert that if only the image metadata is set then it is valid
|
|
flavor = objects.Flavor(
|
|
vcpus=1, memory_mb=512, root_gb=1, extra_specs={})
|
|
|
|
# ensure string to bool conversion works for image metadata
|
|
# property by using "yes".
|
|
image = {'id': uuids.image_id, 'status': 'foo',
|
|
'properties': {'hw_pmu': "yes"}}
|
|
self.assertIsNone(self.compute_api._validate_flavor_image_nostatus(
|
|
self.context, image, flavor, None))
|
|
|
|
@mock.patch('nova.pci.request.get_pci_requests_from_flavor')
|
|
def test_pmu_flavor_only(self, mock_request):
|
|
# assert that if only the flavor extra_spec is set then it is valid
|
|
# and test the string to bool conversion of "on" works.
|
|
flavor = objects.Flavor(
|
|
vcpus=1, memory_mb=512, root_gb=1, extra_specs={'hw:pmu': "on"})
|
|
|
|
image = {'id': uuids.image_id, 'status': 'foo', 'properties': {}}
|
|
self.assertIsNone(self.compute_api._validate_flavor_image_nostatus(
|
|
self.context, image, flavor, None))
|
|
|
|
@mock.patch('nova.pci.request.get_pci_requests_from_flavor')
|
|
def test_pci_validated(self, mock_request):
|
|
"""Tests that calling _validate_flavor_image_nostatus() with
|
|
validate_pci=True results in get_pci_requests_from_flavor() being
|
|
called.
|
|
"""
|
|
image = {'id': uuids.image_id, 'status': 'foo'}
|
|
flavor = self._create_flavor()
|
|
self.compute_api._validate_flavor_image_nostatus(
|
|
self.context, image, flavor, root_bdm=None, validate_pci=True)
|
|
mock_request.assert_called_once_with(flavor)
|
|
|
|
def test_validate_and_build_base_options_translate_neutron_secgroup(self):
|
|
"""Tests that _check_requested_secgroups will return a uuid for a
|
|
requested Neutron security group and that will be returned from
|
|
_validate_and_build_base_options
|
|
"""
|
|
instance_type = objects.Flavor(**test_flavor.fake_flavor)
|
|
boot_meta = metadata = {}
|
|
kernel_id = ramdisk_id = key_name = key_data = user_data = \
|
|
access_ip_v4 = access_ip_v6 = config_drive = \
|
|
auto_disk_config = reservation_id = None
|
|
# This tests that 'default' is unchanged, but 'fake-security-group'
|
|
# will be translated to a UUID for Neutron.
|
|
requested_secgroups = ['default', 'fake-security-group']
|
|
# This will short-circuit _check_requested_networks
|
|
requested_networks = objects.NetworkRequestList(objects=[
|
|
objects.NetworkRequest(network_id='none')])
|
|
max_count = 1
|
|
supports_port_resource_request = False
|
|
with mock.patch(
|
|
'nova.network.security_group_api.validate_name',
|
|
return_value=uuids.secgroup_uuid) as scget:
|
|
base_options, max_network_count, key_pair, security_groups, \
|
|
network_metadata = (
|
|
self.compute_api._validate_and_build_base_options(
|
|
self.context, instance_type, boot_meta, uuids.image_href,
|
|
mock.sentinel.image_id, kernel_id, ramdisk_id,
|
|
'fake-display-name', 'fake-description', key_name,
|
|
key_data, requested_secgroups, 'fake-az', user_data,
|
|
metadata, access_ip_v4, access_ip_v6, requested_networks,
|
|
config_drive, auto_disk_config, reservation_id, max_count,
|
|
supports_port_resource_request
|
|
)
|
|
)
|
|
# Assert the neutron security group API get method was called once
|
|
# and only for the non-default security group name.
|
|
scget.assert_called_once_with(self.context, 'fake-security-group')
|
|
# Assert we translated the non-default secgroup name to uuid.
|
|
self.assertCountEqual(['default', uuids.secgroup_uuid],
|
|
security_groups)
|
|
|
|
@mock.patch('nova.compute.api.API._record_action_start')
|
|
@mock.patch.object(compute_rpcapi.ComputeAPI, 'attach_interface')
|
|
def test_tagged_interface_attach(self, mock_attach, mock_record):
|
|
instance = self._create_instance_obj()
|
|
self.compute_api.attach_interface(self.context, instance, None, None,
|
|
None, tag='foo')
|
|
mock_attach.assert_called_with(self.context, instance=instance,
|
|
network_id=None, port_id=None,
|
|
requested_ip=None, tag='foo')
|
|
mock_record.assert_called_once_with(
|
|
self.context, instance, instance_actions.ATTACH_INTERFACE)
|
|
|
|
@mock.patch('nova.compute.api.API._record_action_start')
|
|
def test_attach_interface_qos_aware_port(self, mock_record):
|
|
instance = self._create_instance_obj()
|
|
with mock.patch.object(
|
|
self.compute_api.network_api, 'show_port',
|
|
return_value={'port': {
|
|
constants.RESOURCE_REQUEST: {
|
|
'resources': {'CUSTOM_RESOURCE_CLASS': 42}
|
|
}}}) as mock_show_port:
|
|
self.assertRaises(
|
|
exception.AttachInterfaceWithQoSPolicyNotSupported,
|
|
self.compute_api.attach_interface,
|
|
self.context, instance,
|
|
'foo_net_id', 'foo_port_id', None
|
|
)
|
|
mock_show_port.assert_called_once_with(self.context, 'foo_port_id')
|
|
|
|
@mock.patch('nova.compute.api.API._record_action_start')
|
|
@mock.patch.object(compute_rpcapi.ComputeAPI, 'detach_interface')
|
|
def test_detach_interface(self, mock_detach, mock_record):
|
|
instance = self._create_instance_obj()
|
|
self.compute_api.detach_interface(self.context, instance, None)
|
|
mock_detach.assert_called_with(self.context, instance=instance,
|
|
port_id=None)
|
|
mock_record.assert_called_once_with(
|
|
self.context, instance, instance_actions.DETACH_INTERFACE)
|
|
|
|
def test_check_attach_and_reserve_volume_multiattach_old_version(self):
|
|
"""Tests that _check_attach_and_reserve_volume fails if trying
|
|
to use a multiattach volume with a microversion<2.60.
|
|
"""
|
|
instance = self._create_instance_obj()
|
|
volume = {'id': uuids.volumeid, 'multiattach': True}
|
|
bdm = objects.BlockDeviceMapping(volume_id=uuids.volumeid,
|
|
instance_uuid=instance.uuid)
|
|
self.assertRaises(exception.MultiattachNotSupportedOldMicroversion,
|
|
self.compute_api._check_attach_and_reserve_volume,
|
|
self.context, volume, instance, bdm,
|
|
supports_multiattach=False)
|
|
|
|
@mock.patch('nova.volume.cinder.API.get',
|
|
return_value={'id': uuids.volumeid, 'multiattach': True})
|
|
def test_attach_volume_shelved_offloaded_fails(
|
|
self, volume_get):
|
|
"""Tests that trying to attach a multiattach volume to a shelved
|
|
offloaded instance fails because it's not supported.
|
|
"""
|
|
instance = self._create_instance_obj(
|
|
params={'vm_state': vm_states.SHELVED_OFFLOADED})
|
|
with mock.patch.object(
|
|
self.compute_api, '_check_volume_already_attached_to_instance',
|
|
return_value=None):
|
|
self.assertRaises(exception.MultiattachToShelvedNotSupported,
|
|
self.compute_api.attach_volume,
|
|
self.context, instance, uuids.volumeid)
|
|
|
|
def test_validate_bdm_check_volume_type_raise_not_found(self):
|
|
"""Tests that _validate_bdm will fail if the requested volume type
|
|
name or id does not match the volume types in Cinder.
|
|
"""
|
|
volume_types = [{'id': 'fake_volume_type_id_1', 'name': 'fake_lvm_1'},
|
|
{'id': 'fake_volume_type_id_2', 'name': 'fake_lvm_2'}]
|
|
bdm = objects.BlockDeviceMapping(
|
|
**fake_block_device.FakeDbBlockDeviceDict(
|
|
{
|
|
'uuid': uuids.image_id,
|
|
'source_type': 'image',
|
|
'destination_type': 'volume',
|
|
'device_name': 'vda',
|
|
'boot_index': 0,
|
|
'volume_size': 3,
|
|
'volume_type': 'lvm-1'}))
|
|
self.assertRaises(exception.VolumeTypeNotFound,
|
|
self.compute_api._check_requested_volume_type,
|
|
bdm, 'lvm-1', volume_types)
|
|
|
|
@mock.patch.object(neutron_api.API, 'has_substr_port_filtering_extension')
|
|
@mock.patch.object(neutron_api.API, 'list_ports')
|
|
@mock.patch.object(objects.BuildRequestList, 'get_by_filters',
|
|
new_callable=mock.NonCallableMock)
|
|
def test_get_all_ip_filter(self, mock_buildreq_get, mock_list_port,
|
|
mock_check_ext):
|
|
mock_check_ext.return_value = True
|
|
cell_instances = self._list_of_instances(2)
|
|
mock_list_port.return_value = {
|
|
'ports': [{'device_id': 'fake_device_id'}]}
|
|
with mock.patch('nova.compute.instance_list.'
|
|
'get_instance_objects_sorted') as mock_inst_get:
|
|
mock_inst_get.return_value = objects.InstanceList(
|
|
self.context, objects=cell_instances), list()
|
|
|
|
self.compute_api.get_all(
|
|
self.context, search_opts={'ip': 'fake'},
|
|
limit=None, marker=None, sort_keys=['baz'],
|
|
sort_dirs=['desc'])
|
|
|
|
mock_list_port.assert_called_once_with(
|
|
self.context, fixed_ips='ip_address_substr=fake',
|
|
fields=['device_id'])
|
|
fields = ['metadata', 'info_cache', 'security_groups']
|
|
mock_inst_get.assert_called_once_with(
|
|
self.context, {'ip': 'fake', 'uuid': ['fake_device_id']},
|
|
None, None, fields, ['baz'], ['desc'], cell_down_support=False)
|
|
|
|
@mock.patch.object(neutron_api.API, 'has_substr_port_filtering_extension')
|
|
@mock.patch.object(neutron_api.API, 'list_ports')
|
|
@mock.patch.object(objects.BuildRequestList, 'get_by_filters',
|
|
new_callable=mock.NonCallableMock)
|
|
def test_get_all_ip6_filter(self, mock_buildreq_get, mock_list_port,
|
|
mock_check_ext):
|
|
mock_check_ext.return_value = True
|
|
cell_instances = self._list_of_instances(2)
|
|
mock_list_port.return_value = {
|
|
'ports': [{'device_id': 'fake_device_id'}]}
|
|
with mock.patch('nova.compute.instance_list.'
|
|
'get_instance_objects_sorted') as mock_inst_get:
|
|
mock_inst_get.return_value = objects.InstanceList(
|
|
self.context, objects=cell_instances), list()
|
|
|
|
self.compute_api.get_all(
|
|
self.context, search_opts={'ip6': 'fake'},
|
|
limit=None, marker=None, sort_keys=['baz'],
|
|
sort_dirs=['desc'])
|
|
|
|
mock_list_port.assert_called_once_with(
|
|
self.context, fixed_ips='ip_address_substr=fake',
|
|
fields=['device_id'])
|
|
fields = ['metadata', 'info_cache', 'security_groups']
|
|
mock_inst_get.assert_called_once_with(
|
|
self.context, {'ip6': 'fake', 'uuid': ['fake_device_id']},
|
|
None, None, fields, ['baz'], ['desc'], cell_down_support=False)
|
|
|
|
@mock.patch.object(neutron_api.API, 'has_substr_port_filtering_extension')
|
|
@mock.patch.object(neutron_api.API, 'list_ports')
|
|
@mock.patch.object(objects.BuildRequestList, 'get_by_filters',
|
|
new_callable=mock.NonCallableMock)
|
|
def test_get_all_ip_and_ip6_filter(self, mock_buildreq_get, mock_list_port,
|
|
mock_check_ext):
|
|
mock_check_ext.return_value = True
|
|
cell_instances = self._list_of_instances(2)
|
|
mock_list_port.return_value = {
|
|
'ports': [{'device_id': 'fake_device_id'}]}
|
|
with mock.patch('nova.compute.instance_list.'
|
|
'get_instance_objects_sorted') as mock_inst_get:
|
|
mock_inst_get.return_value = objects.InstanceList(
|
|
self.context, objects=cell_instances), list()
|
|
|
|
self.compute_api.get_all(
|
|
self.context, search_opts={'ip': 'fake1', 'ip6': 'fake2'},
|
|
limit=None, marker=None, sort_keys=['baz'],
|
|
sort_dirs=['desc'])
|
|
|
|
mock_list_port.assert_has_calls([
|
|
mock.call(
|
|
self.context, fixed_ips='ip_address_substr=fake1',
|
|
fields=['device_id']),
|
|
mock.call(
|
|
self.context, fixed_ips='ip_address_substr=fake2',
|
|
fields=['device_id'])
|
|
])
|
|
fields = ['metadata', 'info_cache', 'security_groups']
|
|
mock_inst_get.assert_called_once_with(
|
|
self.context, {'ip': 'fake1', 'ip6': 'fake2',
|
|
'uuid': ['fake_device_id', 'fake_device_id']},
|
|
None, None, fields, ['baz'], ['desc'], cell_down_support=False)
|
|
|
|
@mock.patch.object(neutron_api.API, 'has_substr_port_filtering_extension')
|
|
@mock.patch.object(neutron_api.API, 'list_ports')
|
|
def test_get_all_ip6_filter_exc(self, mock_list_port, mock_check_ext):
|
|
mock_check_ext.return_value = True
|
|
mock_list_port.side_effect = exception.InternalError('fake')
|
|
|
|
instances = self.compute_api.get_all(
|
|
self.context, search_opts={'ip6': 'fake'},
|
|
limit=None, marker='fake-marker', sort_keys=['baz'],
|
|
sort_dirs=['desc'])
|
|
mock_list_port.assert_called_once_with(
|
|
self.context, fixed_ips='ip_address_substr=fake',
|
|
fields=['device_id'])
|
|
self.assertEqual([], instances.objects)
|
|
|
|
@mock.patch.object(compute_utils, 'notify_about_instance_action')
|
|
@mock.patch('nova.compute.api.API._delete_while_booting',
|
|
return_value=False)
|
|
@mock.patch('nova.compute.api.API._lookup_instance')
|
|
@mock.patch.object(objects.BlockDeviceMappingList, 'get_by_instance_uuid')
|
|
@mock.patch('nova.context.RequestContext.elevated')
|
|
@mock.patch.object(objects.Instance, 'save')
|
|
@mock.patch.object(compute_utils, 'notify_about_instance_usage')
|
|
@mock.patch.object(objects.BlockDeviceMapping, 'destroy')
|
|
@mock.patch.object(objects.Instance, 'destroy')
|
|
def _test_delete_volume_backed_instance(
|
|
self, vm_state, mock_instance_destroy, bdm_destroy,
|
|
notify_about_instance_usage, mock_save, mock_elevated,
|
|
bdm_get_by_instance_uuid, mock_lookup, _mock_del_booting,
|
|
notify_about_instance_action):
|
|
volume_id = uuidutils.generate_uuid()
|
|
conn_info = {'connector': {'host': 'orig-host'}}
|
|
bdms = [objects.BlockDeviceMapping(
|
|
**fake_block_device.FakeDbBlockDeviceDict(
|
|
{'id': 42, 'volume_id': volume_id,
|
|
'source_type': 'volume', 'destination_type': 'volume',
|
|
'delete_on_termination': False,
|
|
'connection_info': jsonutils.dumps(conn_info)}))]
|
|
|
|
bdm_get_by_instance_uuid.return_value = bdms
|
|
mock_elevated.return_value = self.context
|
|
|
|
params = {'host': None, 'vm_state': vm_state}
|
|
inst = self._create_instance_obj(params=params)
|
|
mock_lookup.return_value = None, inst
|
|
connector = conn_info['connector']
|
|
|
|
with mock.patch.object(self.compute_api.network_api,
|
|
'deallocate_for_instance') as mock_deallocate, \
|
|
mock.patch.object(self.compute_api.volume_api,
|
|
'terminate_connection') as mock_terminate_conn, \
|
|
mock.patch.object(self.compute_api.volume_api,
|
|
'detach') as mock_detach:
|
|
self.compute_api.delete(self.context, inst)
|
|
|
|
mock_deallocate.assert_called_once_with(self.context, inst)
|
|
mock_detach.assert_called_once_with(self.context, volume_id,
|
|
inst.uuid)
|
|
mock_terminate_conn.assert_called_once_with(self.context,
|
|
volume_id, connector)
|
|
bdm_destroy.assert_called_once_with()
|
|
|
|
def test_delete_volume_backed_instance_in_error(self):
|
|
self._test_delete_volume_backed_instance(vm_states.ERROR)
|
|
|
|
def test_delete_volume_backed_instance_in_shelved_offloaded(self):
|
|
self._test_delete_volume_backed_instance(vm_states.SHELVED_OFFLOADED)
|
|
|
|
def test_compute_api_host(self):
|
|
self.assertTrue(hasattr(self.compute_api, 'host'))
|
|
self.assertEqual(CONF.host, self.compute_api.host)
|
|
|
|
@mock.patch('nova.scheduler.client.report.SchedulerReportClient')
|
|
def test_placement_client_init(self, mock_report_client):
|
|
"""Tests to make sure that the construction of the placement client
|
|
only happens once per API class instance.
|
|
"""
|
|
self.assertIsNone(self.compute_api._placementclient)
|
|
# Access the property twice to make sure SchedulerReportClient is
|
|
# only loaded once.
|
|
for x in range(2):
|
|
self.compute_api.placementclient
|
|
mock_report_client.assert_called_once_with()
|
|
|
|
def test_validate_host_for_cold_migrate_same_host_fails(self):
|
|
"""Asserts CannotMigrateToSameHost is raised when trying to cold
|
|
migrate to the same host.
|
|
"""
|
|
instance = fake_instance.fake_instance_obj(self.context)
|
|
self.assertRaises(exception.CannotMigrateToSameHost,
|
|
self.compute_api._validate_host_for_cold_migrate,
|
|
self.context, instance, instance.host,
|
|
allow_cross_cell_resize=False)
|
|
|
|
@mock.patch('nova.objects.ComputeNode.'
|
|
'get_first_node_by_host_for_old_compat')
|
|
def test_validate_host_for_cold_migrate_diff_host_no_cross_cell(
|
|
self, mock_cn_get):
|
|
"""Tests the scenario where allow_cross_cell_resize=False and the
|
|
host is found in the same cell as the instance.
|
|
"""
|
|
instance = fake_instance.fake_instance_obj(self.context)
|
|
node = self.compute_api._validate_host_for_cold_migrate(
|
|
self.context, instance, uuids.host, allow_cross_cell_resize=False)
|
|
self.assertIs(node, mock_cn_get.return_value)
|
|
mock_cn_get.assert_called_once_with(
|
|
self.context, uuids.host, use_slave=True)
|
|
|
|
@mock.patch('nova.objects.HostMapping.get_by_host',
|
|
side_effect=exception.HostMappingNotFound(name=uuids.host))
|
|
def test_validate_host_for_cold_migrate_cross_cell_host_mapping_not_found(
|
|
self, mock_hm_get):
|
|
"""Tests the scenario where allow_cross_cell_resize=True but the
|
|
HostMapping for the given host could not be found.
|
|
"""
|
|
instance = fake_instance.fake_instance_obj(self.context)
|
|
self.assertRaises(exception.ComputeHostNotFound,
|
|
self.compute_api._validate_host_for_cold_migrate,
|
|
self.context, instance, uuids.host,
|
|
allow_cross_cell_resize=True)
|
|
mock_hm_get.assert_called_once_with(self.context, uuids.host)
|
|
|
|
@mock.patch('nova.objects.HostMapping.get_by_host',
|
|
return_value=objects.HostMapping(
|
|
cell_mapping=objects.CellMapping(uuid=uuids.cell2)))
|
|
@mock.patch('nova.context.target_cell')
|
|
@mock.patch('nova.objects.ComputeNode.'
|
|
'get_first_node_by_host_for_old_compat')
|
|
def test_validate_host_for_cold_migrate_cross_cell(
|
|
self, mock_cn_get, mock_target_cell, mock_hm_get):
|
|
"""Tests the scenario where allow_cross_cell_resize=True and the
|
|
ComputeNode is pulled from the target cell defined by the HostMapping.
|
|
"""
|
|
instance = fake_instance.fake_instance_obj(self.context)
|
|
node = self.compute_api._validate_host_for_cold_migrate(
|
|
self.context, instance, uuids.host,
|
|
allow_cross_cell_resize=True)
|
|
self.assertIs(node, mock_cn_get.return_value)
|
|
mock_hm_get.assert_called_once_with(self.context, uuids.host)
|
|
# get_first_node_by_host_for_old_compat is called with a temporarily
|
|
# cell-targeted context
|
|
mock_cn_get.assert_called_once_with(
|
|
mock_target_cell.return_value.__enter__.return_value,
|
|
uuids.host, use_slave=True)
|
|
|
|
def _test_get_migrations_sorted_filter_duplicates(self, migrations,
|
|
expected):
|
|
"""Tests the cross-cell scenario where there are multiple migrations
|
|
with the same UUID from different cells and only one should be
|
|
returned.
|
|
"""
|
|
sort_keys = ['created_at', 'id']
|
|
sort_dirs = ['desc', 'desc']
|
|
filters = {'migration_type': 'resize'}
|
|
limit = 1000
|
|
marker = None
|
|
with mock.patch(
|
|
'nova.compute.migration_list.get_migration_objects_sorted',
|
|
return_value=objects.MigrationList(
|
|
objects=migrations)) as getter:
|
|
sorted_migrations = self.compute_api.get_migrations_sorted(
|
|
self.context, filters, sort_dirs=sort_dirs,
|
|
sort_keys=sort_keys, limit=limit, marker=marker)
|
|
self.assertEqual(1, len(sorted_migrations))
|
|
getter.assert_called_once_with(
|
|
self.context, filters, limit, marker, sort_keys, sort_dirs)
|
|
self.assertIs(expected, sorted_migrations[0])
|
|
|
|
def test_get_migrations_sorted_filter_duplicates(self):
|
|
"""Tests filtering duplicated Migration records where both have
|
|
created_at and updated_at set.
|
|
"""
|
|
t1 = timeutils.utcnow()
|
|
source_cell_migration = objects.Migration(
|
|
uuid=uuids.migration, created_at=t1, updated_at=t1)
|
|
t2 = t1 + datetime.timedelta(seconds=1)
|
|
target_cell_migration = objects.Migration(
|
|
uuid=uuids.migration, created_at=t2, updated_at=t2)
|
|
self._test_get_migrations_sorted_filter_duplicates(
|
|
[source_cell_migration, target_cell_migration],
|
|
target_cell_migration)
|
|
# Run it again in reverse.
|
|
self._test_get_migrations_sorted_filter_duplicates(
|
|
[target_cell_migration, source_cell_migration],
|
|
target_cell_migration)
|
|
|
|
def test_get_migrations_sorted_filter_duplicates_using_created_at(self):
|
|
"""Tests the cross-cell scenario where there are multiple migrations
|
|
with the same UUID from different cells and only one should be
|
|
returned. In this test the first Migration object to be processed has
|
|
not been updated yet but is created after the second record to process.
|
|
"""
|
|
t1 = timeutils.utcnow()
|
|
older = objects.Migration(
|
|
uuid=uuids.migration, created_at=t1, updated_at=t1)
|
|
t2 = t1 + datetime.timedelta(seconds=1)
|
|
newer = objects.Migration(
|
|
uuid=uuids.migration, created_at=t2, updated_at=None)
|
|
self._test_get_migrations_sorted_filter_duplicates(
|
|
[newer, older], newer)
|
|
# Test with just created_at.
|
|
older.updated_at = None
|
|
self._test_get_migrations_sorted_filter_duplicates(
|
|
[newer, older], newer)
|
|
# Run it again in reverse.
|
|
self._test_get_migrations_sorted_filter_duplicates(
|
|
[older, newer], newer)
|
|
|
|
@mock.patch('nova.objects.Migration.get_by_instance_and_status')
|
|
def test_confirm_resize_cross_cell_move_true(self, mock_migration_get):
|
|
"""Tests confirm_resize where Migration.cross_cell_move is True"""
|
|
instance = fake_instance.fake_instance_obj(
|
|
self.context, vm_state=vm_states.RESIZED, task_state=None,
|
|
launched_at=timeutils.utcnow())
|
|
migration = objects.Migration(cross_cell_move=True)
|
|
mock_migration_get.return_value = migration
|
|
with test.nested(
|
|
mock.patch.object(self.context, 'elevated',
|
|
return_value=self.context),
|
|
mock.patch.object(migration, 'save'),
|
|
mock.patch.object(self.compute_api, '_record_action_start'),
|
|
mock.patch.object(self.compute_api.compute_task_api,
|
|
'confirm_snapshot_based_resize'),
|
|
) as (
|
|
mock_elevated, mock_migration_save, mock_record_action,
|
|
mock_conductor_confirm
|
|
):
|
|
self.compute_api.confirm_resize(self.context, instance)
|
|
mock_elevated.assert_called_once_with()
|
|
mock_migration_save.assert_called_once_with()
|
|
self.assertEqual('confirming', migration.status)
|
|
mock_record_action.assert_called_once_with(
|
|
self.context, instance, instance_actions.CONFIRM_RESIZE)
|
|
mock_conductor_confirm.assert_called_once_with(
|
|
self.context, instance, migration)
|
|
|
|
@mock.patch('nova.objects.service.get_minimum_version_all_cells')
|
|
def test_allow_cross_cell_resize_default_false(self, mock_get_min_ver):
|
|
"""Based on the default policy this asserts nobody is allowed to
|
|
perform cross-cell resize.
|
|
"""
|
|
instance = objects.Instance(
|
|
project_id='fake-project', user_id='fake-user')
|
|
self.assertFalse(self.compute_api._allow_cross_cell_resize(
|
|
self.context, instance))
|
|
# We did not need to check the minimum nova-compute version since the
|
|
# policy check failed.
|
|
mock_get_min_ver.assert_not_called()
|
|
|
|
@mock.patch('nova.objects.service.get_minimum_version_all_cells',
|
|
return_value=compute_api.MIN_COMPUTE_CROSS_CELL_RESIZE - 1)
|
|
def test_allow_cross_cell_resize_false_old_version(self, mock_get_min_ver):
|
|
"""Policy allows cross-cell resize but minimum nova-compute service
|
|
version is not new enough.
|
|
"""
|
|
instance = objects.Instance(
|
|
project_id='fake-project', user_id='fake-user')
|
|
with mock.patch.object(self.context, 'can', return_value=True) as can:
|
|
self.assertFalse(self.compute_api._allow_cross_cell_resize(
|
|
self.context, instance))
|
|
can.assert_called_once()
|
|
mock_get_min_ver.assert_called_once_with(
|
|
self.context, ['nova-compute'])
|
|
|
|
@mock.patch('nova.objects.service.get_minimum_version_all_cells',
|
|
return_value=compute_api.MIN_COMPUTE_CROSS_CELL_RESIZE)
|
|
def test_allow_cross_cell_resize_true(self, mock_get_min_ver):
|
|
"""Policy allows cross-cell resize and minimum nova-compute service
|
|
version is new enough.
|
|
"""
|
|
instance = objects.Instance(
|
|
project_id='fake-project', user_id='fake-user')
|
|
with mock.patch.object(self.context, 'can', return_value=True) as can:
|
|
self.assertTrue(self.compute_api._allow_cross_cell_resize(
|
|
self.context, instance))
|
|
can.assert_called_once()
|
|
mock_get_min_ver.assert_called_once_with(
|
|
self.context, ['nova-compute'])
|
|
|
|
def _test_block_accelerators(self, instance, args_info):
|
|
@compute_api.block_accelerators
|
|
def myfunc(self, context, instance, *args, **kwargs):
|
|
args_info['args'] = (context, instance, *args)
|
|
args_info['kwargs'] = dict(**kwargs)
|
|
|
|
args = ('arg1', 'arg2')
|
|
kwargs = {'arg3': 'dummy3', 'arg4': 'dummy4'}
|
|
|
|
myfunc(mock.ANY, self.context, instance, *args, **kwargs)
|
|
|
|
expected_args = (self.context, instance, *args)
|
|
return expected_args, kwargs
|
|
|
|
def test_block_accelerators_no_device_profile(self):
|
|
instance = self._create_instance_obj()
|
|
args_info = {}
|
|
|
|
expected_args, kwargs = self._test_block_accelerators(
|
|
instance, args_info)
|
|
self.assertEqual(expected_args, args_info['args'])
|
|
self.assertEqual(kwargs, args_info['kwargs'])
|
|
|
|
def test_block_accelerators_with_device_profile(self):
|
|
extra_specs = {'accel:device_profile': 'mydp'}
|
|
flavor = self._create_flavor(extra_specs=extra_specs)
|
|
instance = self._create_instance_obj(flavor=flavor)
|
|
args_info = {}
|
|
|
|
self.assertRaisesRegex(exception.ForbiddenWithAccelerators,
|
|
'Forbidden with instances that have accelerators.',
|
|
self._test_block_accelerators, instance, args_info)
|
|
# myfunc was not called
|
|
self.assertEqual({}, args_info)
|
|
|
|
|
|
class DiffDictTestCase(test.NoDBTestCase):
|
|
"""Unit tests for _diff_dict()."""
|
|
|
|
def test_no_change(self):
|
|
old = dict(a=1, b=2, c=3)
|
|
new = dict(a=1, b=2, c=3)
|
|
diff = compute_api._diff_dict(old, new)
|
|
|
|
self.assertEqual(diff, {})
|
|
|
|
def test_new_key(self):
|
|
old = dict(a=1, b=2, c=3)
|
|
new = dict(a=1, b=2, c=3, d=4)
|
|
diff = compute_api._diff_dict(old, new)
|
|
|
|
self.assertEqual(diff, dict(d=['+', 4]))
|
|
|
|
def test_changed_key(self):
|
|
old = dict(a=1, b=2, c=3)
|
|
new = dict(a=1, b=4, c=3)
|
|
diff = compute_api._diff_dict(old, new)
|
|
|
|
self.assertEqual(diff, dict(b=['+', 4]))
|
|
|
|
def test_removed_key(self):
|
|
old = dict(a=1, b=2, c=3)
|
|
new = dict(a=1, c=3)
|
|
diff = compute_api._diff_dict(old, new)
|
|
|
|
self.assertEqual(diff, dict(b=['-']))
|