Merge "Refactor out claim_resources_on_destination into a utility" into stable/pike
This commit is contained in:
commit
ca5400ebcf
|
@ -71,7 +71,11 @@ class LiveMigrationTask(base.TaskBase):
|
|||
# skip_filters=True flag so the scheduler does the work of claiming
|
||||
# resources on the destination in Placement but still bypass the
|
||||
# scheduler filters, which honors the 'force' flag in the API.
|
||||
self._claim_resources_on_destination(source_node, dest_node)
|
||||
# This raises NoValidHost which will be handled in
|
||||
# ComputeTaskManager.
|
||||
scheduler_utils.claim_resources_on_destination(
|
||||
self.scheduler_client.reportclient, self.instance,
|
||||
source_node, dest_node)
|
||||
|
||||
# TODO(johngarbutt) need to move complexity out of compute manager
|
||||
# TODO(johngarbutt) disk_over_commit?
|
||||
|
@ -129,72 +133,6 @@ class LiveMigrationTask(base.TaskBase):
|
|||
'across cells.') % self.instance.uuid))
|
||||
return source_node, dest_node
|
||||
|
||||
def _claim_resources_on_destination(self, source_node, dest_node):
|
||||
"""Copies allocations from source node to dest node in Placement
|
||||
|
||||
:param source_node: source ComputeNode where the instance currently
|
||||
lives
|
||||
:param dest_node: destination ComputeNode where the instance is being
|
||||
forced to live migrate.
|
||||
"""
|
||||
reportclient = self.scheduler_client.reportclient
|
||||
# Get the current allocations for the source node and the instance.
|
||||
source_node_allocations = reportclient.get_allocations_for_instance(
|
||||
source_node.uuid, self.instance)
|
||||
if source_node_allocations:
|
||||
# Generate an allocation request for the destination node.
|
||||
alloc_request = {
|
||||
'allocations': [
|
||||
{
|
||||
'resource_provider': {
|
||||
'uuid': dest_node.uuid
|
||||
},
|
||||
'resources': source_node_allocations
|
||||
}
|
||||
]
|
||||
}
|
||||
# The claim_resources method will check for existing allocations
|
||||
# for the instance and effectively "double up" the allocations for
|
||||
# both the source and destination node. That's why when requesting
|
||||
# allocations for resources on the destination node before we live
|
||||
# migrate, we use the existing resource allocations from the
|
||||
# source node.
|
||||
if reportclient.claim_resources(
|
||||
self.instance.uuid, alloc_request,
|
||||
self.instance.project_id, self.instance.user_id):
|
||||
LOG.debug('Instance allocations successfully created on '
|
||||
'destination node %(dest)s: %(alloc_request)s',
|
||||
{'dest': dest_node.uuid,
|
||||
'alloc_request': alloc_request},
|
||||
instance=self.instance)
|
||||
else:
|
||||
# We have to fail even though the user requested that we force
|
||||
# the host. This is because we need Placement to have an
|
||||
# accurate reflection of what's allocated on all nodes so the
|
||||
# scheduler can make accurate decisions about which nodes have
|
||||
# capacity for building an instance. We also cannot rely on the
|
||||
# resource tracker in the compute service automatically healing
|
||||
# the allocations since that code is going away in Queens.
|
||||
reason = (_('Unable to migrate instance %(instance_uuid)s to '
|
||||
'host %(host)s. There is not enough capacity on '
|
||||
'the host for the instance.') %
|
||||
{'instance_uuid': self.instance.uuid,
|
||||
'host': self.destination})
|
||||
raise exception.MigrationPreCheckError(reason=reason)
|
||||
else:
|
||||
# This shouldn't happen, but it could be a case where there are
|
||||
# older (Ocata) computes still so the existing allocations are
|
||||
# getting overwritten by the update_available_resource periodic
|
||||
# task in the compute service.
|
||||
# TODO(mriedem): Make this an error when the auto-heal
|
||||
# compatibility code in the resource tracker is removed.
|
||||
LOG.warning('No instance allocations found for source node '
|
||||
'%(source)s in Placement. Not creating allocations '
|
||||
'for destination node %(dest)s and assuming the '
|
||||
'compute service will heal the allocations.',
|
||||
{'source': source_node.uuid, 'dest': dest_node.uuid},
|
||||
instance=self.instance)
|
||||
|
||||
def _check_destination_is_not_source(self):
|
||||
if self.destination == self.source:
|
||||
raise exception.UnableToMigrateToSelf(
|
||||
|
|
|
@ -218,6 +218,87 @@ def resources_from_request_spec(spec_obj):
|
|||
return resources
|
||||
|
||||
|
||||
# TODO(mriedem): Remove this when select_destinations() in the scheduler takes
|
||||
# some sort of skip_filters flag.
|
||||
def claim_resources_on_destination(
|
||||
reportclient, instance, source_node, dest_node):
|
||||
"""Copies allocations from source node to dest node in Placement
|
||||
|
||||
Normally the scheduler will allocate resources on a chosen destination
|
||||
node during a move operation like evacuate and live migration. However,
|
||||
because of the ability to force a host and bypass the scheduler, this
|
||||
method can be used to manually copy allocations from the source node to
|
||||
the forced destination node.
|
||||
|
||||
This is only appropriate when the instance flavor on the source node
|
||||
is the same on the destination node, i.e. don't use this for resize.
|
||||
|
||||
:param reportclient: An instance of the SchedulerReportClient.
|
||||
:param instance: The instance being moved.
|
||||
:param source_node: source ComputeNode where the instance currently
|
||||
lives
|
||||
:param dest_node: destination ComputeNode where the instance is being
|
||||
moved
|
||||
:raises NoValidHost: If the allocation claim on the destination
|
||||
node fails.
|
||||
"""
|
||||
# Get the current allocations for the source node and the instance.
|
||||
source_node_allocations = reportclient.get_allocations_for_instance(
|
||||
source_node.uuid, instance)
|
||||
if source_node_allocations:
|
||||
# Generate an allocation request for the destination node.
|
||||
alloc_request = {
|
||||
'allocations': [
|
||||
{
|
||||
'resource_provider': {
|
||||
'uuid': dest_node.uuid
|
||||
},
|
||||
'resources': source_node_allocations
|
||||
}
|
||||
]
|
||||
}
|
||||
# The claim_resources method will check for existing allocations
|
||||
# for the instance and effectively "double up" the allocations for
|
||||
# both the source and destination node. That's why when requesting
|
||||
# allocations for resources on the destination node before we move,
|
||||
# we use the existing resource allocations from the source node.
|
||||
if reportclient.claim_resources(
|
||||
instance.uuid, alloc_request,
|
||||
instance.project_id, instance.user_id):
|
||||
LOG.debug('Instance allocations successfully created on '
|
||||
'destination node %(dest)s: %(alloc_request)s',
|
||||
{'dest': dest_node.uuid,
|
||||
'alloc_request': alloc_request},
|
||||
instance=instance)
|
||||
else:
|
||||
# We have to fail even though the user requested that we force
|
||||
# the host. This is because we need Placement to have an
|
||||
# accurate reflection of what's allocated on all nodes so the
|
||||
# scheduler can make accurate decisions about which nodes have
|
||||
# capacity for building an instance. We also cannot rely on the
|
||||
# resource tracker in the compute service automatically healing
|
||||
# the allocations since that code is going away in Queens.
|
||||
reason = (_('Unable to move instance %(instance_uuid)s to '
|
||||
'host %(host)s. There is not enough capacity on '
|
||||
'the host for the instance.') %
|
||||
{'instance_uuid': instance.uuid,
|
||||
'host': dest_node.host})
|
||||
raise exception.NoValidHost(reason=reason)
|
||||
else:
|
||||
# This shouldn't happen, but it could be a case where there are
|
||||
# older (Ocata) computes still so the existing allocations are
|
||||
# getting overwritten by the update_available_resource periodic
|
||||
# task in the compute service.
|
||||
# TODO(mriedem): Make this an error when the auto-heal
|
||||
# compatibility code in the resource tracker is removed.
|
||||
LOG.warning('No instance allocations found for source node '
|
||||
'%(source)s in Placement. Not creating allocations '
|
||||
'for destination node %(dest)s and assuming the '
|
||||
'compute service will heal the allocations.',
|
||||
{'source': source_node.uuid, 'dest': dest_node.uuid},
|
||||
instance=instance)
|
||||
|
||||
|
||||
def set_vm_state_and_notify(context, instance_uuid, service, method, updates,
|
||||
ex, request_spec):
|
||||
"""changes VM state and notifies."""
|
||||
|
|
|
@ -66,7 +66,8 @@ class LiveMigrationTaskTestCase(test.NoDBTestCase):
|
|||
mock.patch.object(self.task, '_check_requested_destination',
|
||||
return_value=(mock.sentinel.source_node,
|
||||
mock.sentinel.dest_node)),
|
||||
mock.patch.object(self.task, '_claim_resources_on_destination'),
|
||||
mock.patch.object(scheduler_utils,
|
||||
'claim_resources_on_destination'),
|
||||
mock.patch.object(self.task.compute_rpcapi, 'live_migration'),
|
||||
) as (mock_check_up, mock_check_dest, mock_claim, mock_mig):
|
||||
mock_mig.return_value = "bob"
|
||||
|
@ -75,6 +76,7 @@ class LiveMigrationTaskTestCase(test.NoDBTestCase):
|
|||
mock_check_up.assert_called_once_with(self.instance_host)
|
||||
mock_check_dest.assert_called_once_with()
|
||||
mock_claim.assert_called_once_with(
|
||||
self.task.scheduler_client.reportclient, self.instance,
|
||||
mock.sentinel.source_node, mock.sentinel.dest_node)
|
||||
mock_mig.assert_called_once_with(
|
||||
self.context,
|
||||
|
@ -517,102 +519,6 @@ class LiveMigrationTaskTestCase(test.NoDBTestCase):
|
|||
self.assertRaises(exception.MigrationPreCheckError,
|
||||
self.task._call_livem_checks_on_host, {})
|
||||
|
||||
def test_claim_resources_on_destination_no_source_allocations(self):
|
||||
"""Tests the negative scenario where the instance does not have
|
||||
allocations in Placement on the source compute node so no claim is
|
||||
attempted on the destination compute node.
|
||||
"""
|
||||
source_node = objects.ComputeNode(uuid=uuids.source_node)
|
||||
dest_node = objects.ComputeNode(uuid=uuids.dest_node)
|
||||
|
||||
@mock.patch.object(self.task.scheduler_client.reportclient,
|
||||
'get_allocations_for_instance', return_value={})
|
||||
@mock.patch.object(self.task.scheduler_client.reportclient,
|
||||
'claim_resources',
|
||||
new_callable=mock.NonCallableMock)
|
||||
def test(mock_claim, mock_get_allocs):
|
||||
self.task._claim_resources_on_destination(source_node, dest_node)
|
||||
mock_get_allocs.assert_called_once_with(
|
||||
uuids.source_node, self.instance)
|
||||
|
||||
test()
|
||||
|
||||
def test_claim_resources_on_destination_claim_fails(self):
|
||||
"""Tests the negative scenario where the resource allocation claim
|
||||
on the destination compute node fails, resulting in an error.
|
||||
"""
|
||||
source_node = objects.ComputeNode(uuid=uuids.source_node)
|
||||
dest_node = objects.ComputeNode(uuid=uuids.dest_node)
|
||||
source_res_allocs = {
|
||||
'VCPU': self.instance.vcpus,
|
||||
'MEMORY_MB': self.instance.memory_mb,
|
||||
# This would really include ephemeral and swap too but we're lazy.
|
||||
'DISK_GB': self.instance.root_gb
|
||||
}
|
||||
dest_alloc_request = {
|
||||
'allocations': [
|
||||
{
|
||||
'resource_provider': {
|
||||
'uuid': uuids.dest_node
|
||||
},
|
||||
'resources': source_res_allocs
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@mock.patch.object(self.task.scheduler_client.reportclient,
|
||||
'get_allocations_for_instance',
|
||||
return_value=source_res_allocs)
|
||||
@mock.patch.object(self.task.scheduler_client.reportclient,
|
||||
'claim_resources', return_value=False)
|
||||
def test(mock_claim, mock_get_allocs):
|
||||
self.assertRaises(exception.MigrationPreCheckError,
|
||||
self.task._claim_resources_on_destination,
|
||||
source_node, dest_node)
|
||||
mock_get_allocs.assert_called_once_with(
|
||||
uuids.source_node, self.instance)
|
||||
mock_claim.assert_called_once_with(
|
||||
self.instance.uuid, dest_alloc_request,
|
||||
self.instance.project_id, self.instance.user_id)
|
||||
|
||||
test()
|
||||
|
||||
def test_claim_resources_on_destination(self):
|
||||
"""Happy path test where everything is successful."""
|
||||
source_node = objects.ComputeNode(uuid=uuids.source_node)
|
||||
dest_node = objects.ComputeNode(uuid=uuids.dest_node)
|
||||
source_res_allocs = {
|
||||
'VCPU': self.instance.vcpus,
|
||||
'MEMORY_MB': self.instance.memory_mb,
|
||||
# This would really include ephemeral and swap too but we're lazy.
|
||||
'DISK_GB': self.instance.root_gb
|
||||
}
|
||||
dest_alloc_request = {
|
||||
'allocations': [
|
||||
{
|
||||
'resource_provider': {
|
||||
'uuid': uuids.dest_node
|
||||
},
|
||||
'resources': source_res_allocs
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@mock.patch.object(self.task.scheduler_client.reportclient,
|
||||
'get_allocations_for_instance',
|
||||
return_value=source_res_allocs)
|
||||
@mock.patch.object(self.task.scheduler_client.reportclient,
|
||||
'claim_resources', return_value=True)
|
||||
def test(mock_claim, mock_get_allocs):
|
||||
self.task._claim_resources_on_destination(source_node, dest_node)
|
||||
mock_get_allocs.assert_called_once_with(
|
||||
uuids.source_node, self.instance)
|
||||
mock_claim.assert_called_once_with(
|
||||
self.instance.uuid, dest_alloc_request,
|
||||
self.instance.project_id, self.instance.user_id)
|
||||
|
||||
test()
|
||||
|
||||
@mock.patch.object(objects.InstanceMapping, 'get_by_instance_uuid',
|
||||
side_effect=exception.InstanceMappingNotFound(
|
||||
uuid=uuids.instance))
|
||||
|
|
|
@ -12,9 +12,14 @@
|
|||
|
||||
import mock
|
||||
|
||||
from nova import context as nova_context
|
||||
from nova import exception
|
||||
from nova import objects
|
||||
from nova.scheduler.client import report
|
||||
from nova.scheduler import utils
|
||||
from nova import test
|
||||
from nova.tests.unit import fake_instance
|
||||
from nova.tests import uuidsentinel as uuids
|
||||
|
||||
|
||||
class TestUtils(test.NoDBTestCase):
|
||||
|
@ -255,3 +260,113 @@ class TestUtils(test.NoDBTestCase):
|
|||
}
|
||||
utils.merge_resources(resources, new_resources, -1)
|
||||
self.assertEqual(merged, resources)
|
||||
|
||||
def test_claim_resources_on_destination_no_source_allocations(self):
|
||||
"""Tests the negative scenario where the instance does not have
|
||||
allocations in Placement on the source compute node so no claim is
|
||||
attempted on the destination compute node.
|
||||
"""
|
||||
reportclient = report.SchedulerReportClient()
|
||||
instance = fake_instance.fake_instance_obj(
|
||||
nova_context.get_admin_context())
|
||||
source_node = objects.ComputeNode(
|
||||
uuid=uuids.source_node, host=instance.host)
|
||||
dest_node = objects.ComputeNode(uuid=uuids.dest_node, host='dest-host')
|
||||
|
||||
@mock.patch.object(reportclient,
|
||||
'get_allocations_for_instance', return_value={})
|
||||
@mock.patch.object(reportclient,
|
||||
'claim_resources',
|
||||
new_callable=mock.NonCallableMock)
|
||||
def test(mock_claim, mock_get_allocs):
|
||||
utils.claim_resources_on_destination(
|
||||
reportclient, instance, source_node, dest_node)
|
||||
mock_get_allocs.assert_called_once_with(
|
||||
uuids.source_node, instance)
|
||||
|
||||
test()
|
||||
|
||||
def test_claim_resources_on_destination_claim_fails(self):
|
||||
"""Tests the negative scenario where the resource allocation claim
|
||||
on the destination compute node fails, resulting in an error.
|
||||
"""
|
||||
reportclient = report.SchedulerReportClient()
|
||||
instance = fake_instance.fake_instance_obj(
|
||||
nova_context.get_admin_context())
|
||||
source_node = objects.ComputeNode(
|
||||
uuid=uuids.source_node, host=instance.host)
|
||||
dest_node = objects.ComputeNode(uuid=uuids.dest_node, host='dest-host')
|
||||
source_res_allocs = {
|
||||
'VCPU': instance.vcpus,
|
||||
'MEMORY_MB': instance.memory_mb,
|
||||
# This would really include ephemeral and swap too but we're lazy.
|
||||
'DISK_GB': instance.root_gb
|
||||
}
|
||||
dest_alloc_request = {
|
||||
'allocations': [
|
||||
{
|
||||
'resource_provider': {
|
||||
'uuid': uuids.dest_node
|
||||
},
|
||||
'resources': source_res_allocs
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@mock.patch.object(reportclient,
|
||||
'get_allocations_for_instance',
|
||||
return_value=source_res_allocs)
|
||||
@mock.patch.object(reportclient,
|
||||
'claim_resources', return_value=False)
|
||||
def test(mock_claim, mock_get_allocs):
|
||||
self.assertRaises(exception.NoValidHost,
|
||||
utils.claim_resources_on_destination,
|
||||
reportclient, instance, source_node, dest_node)
|
||||
mock_get_allocs.assert_called_once_with(
|
||||
uuids.source_node, instance)
|
||||
mock_claim.assert_called_once_with(
|
||||
instance.uuid, dest_alloc_request,
|
||||
instance.project_id, instance.user_id)
|
||||
|
||||
test()
|
||||
|
||||
def test_claim_resources_on_destination(self):
|
||||
"""Happy path test where everything is successful."""
|
||||
reportclient = report.SchedulerReportClient()
|
||||
instance = fake_instance.fake_instance_obj(
|
||||
nova_context.get_admin_context())
|
||||
source_node = objects.ComputeNode(
|
||||
uuid=uuids.source_node, host=instance.host)
|
||||
dest_node = objects.ComputeNode(uuid=uuids.dest_node, host='dest-host')
|
||||
source_res_allocs = {
|
||||
'VCPU': instance.vcpus,
|
||||
'MEMORY_MB': instance.memory_mb,
|
||||
# This would really include ephemeral and swap too but we're lazy.
|
||||
'DISK_GB': instance.root_gb
|
||||
}
|
||||
dest_alloc_request = {
|
||||
'allocations': [
|
||||
{
|
||||
'resource_provider': {
|
||||
'uuid': uuids.dest_node
|
||||
},
|
||||
'resources': source_res_allocs
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@mock.patch.object(reportclient,
|
||||
'get_allocations_for_instance',
|
||||
return_value=source_res_allocs)
|
||||
@mock.patch.object(reportclient,
|
||||
'claim_resources', return_value=True)
|
||||
def test(mock_claim, mock_get_allocs):
|
||||
utils.claim_resources_on_destination(
|
||||
reportclient, instance, source_node, dest_node)
|
||||
mock_get_allocs.assert_called_once_with(
|
||||
uuids.source_node, instance)
|
||||
mock_claim.assert_called_once_with(
|
||||
instance.uuid, dest_alloc_request,
|
||||
instance.project_id, instance.user_id)
|
||||
|
||||
test()
|
||||
|
|
Loading…
Reference in New Issue