Skip to content

Commit a263fa4

Browse files
committed
Allow unshelve to a specific host (Compute API part)
This patch introduce changes to the compute API that will allow PROJECT_ADMIN to unshelve an shelved offloaded server to a specific host. This patch also supports the ability to unpin the availability_zone of an instance that is bound to it. Implements: blueprint unshelve-to-host Change-Id: Ieb4766fdd88c469574fad823e05fe401537cdc30
1 parent bcb96f3 commit a263fa4

File tree

6 files changed

+743
-58
lines changed

6 files changed

+743
-58
lines changed

nova/api/openstack/compute/shelve.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,14 +95,15 @@ def _unshelve(self, req, id, body):
9595
context.can(shelve_policies.POLICY_ROOT % 'unshelve',
9696
target={'project_id': instance.project_id})
9797

98-
new_az = None
98+
unshelve_args = {}
99+
99100
unshelve_dict = body['unshelve']
100101
support_az = api_version_request.is_supported(req, '2.77')
101102
if support_az and unshelve_dict:
102-
new_az = unshelve_dict['availability_zone']
103+
unshelve_args['new_az'] = unshelve_dict['availability_zone']
103104

104105
try:
105-
self.compute_api.unshelve(context, instance, new_az=new_az)
106+
self.compute_api.unshelve(context, instance, **unshelve_args)
106107
except (exception.InstanceIsLocked,
107108
exception.UnshelveInstanceInvalidState,
108109
exception.MismatchVolumeAZException) as e:

nova/compute/api.py

Lines changed: 107 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,8 @@ def inner(self, context, instance, *args, **kwargs):
380380
class API:
381381
"""API for interacting with the compute manager."""
382382

383+
_sentinel = object()
384+
383385
def __init__(self, image_api=None, network_api=None, volume_api=None):
384386
self.image_api = image_api or glance.API()
385387
self.network_api = network_api or neutron.API()
@@ -4391,31 +4393,45 @@ def shelve_offload(self, context, instance, clean_shutdown=True):
43914393
context, instance=instance,
43924394
clean_shutdown=clean_shutdown, accel_uuids=accel_uuids)
43934395

4396+
def _check_offloaded(self, context, instance):
4397+
"""Check if the status of an instance is SHELVE_OFFLOADED,
4398+
if not raise an exception.
4399+
"""
4400+
if instance.vm_state != vm_states.SHELVED_OFFLOADED:
4401+
# NOTE(brinzhang): If the server status is 'SHELVED', it still
4402+
# belongs to a host, the availability_zone should not change.
4403+
# Unshelving a shelved offloaded server will go through the
4404+
# scheduler to find a new host.
4405+
raise exception.UnshelveInstanceInvalidState(
4406+
state=instance.vm_state, instance_uuid=instance.uuid)
4407+
4408+
def _ensure_host_in_az(self, context, host, availability_zone):
4409+
"""Ensure the host provided belongs to the availability zone,
4410+
if not raise an exception.
4411+
"""
4412+
if availability_zone is not None:
4413+
host_az = availability_zones.get_host_availability_zone(
4414+
context,
4415+
host
4416+
)
4417+
if host_az != availability_zone:
4418+
raise exception.UnshelveHostNotInAZ(
4419+
host=host, availability_zone=availability_zone)
4420+
43944421
def _validate_unshelve_az(self, context, instance, availability_zone):
43954422
"""Verify the specified availability_zone during unshelve.
43964423
4397-
Verifies that the server is shelved offloaded, the AZ exists and
4398-
if [cinder]/cross_az_attach=False, that any attached volumes are in
4399-
the same AZ.
4424+
Verifies the AZ exists and if [cinder]/cross_az_attach=False, that
4425+
any attached volumes are in the same AZ.
44004426
44014427
:param context: nova auth RequestContext for the unshelve action
44024428
:param instance: Instance object for the server being unshelved
44034429
:param availability_zone: The user-requested availability zone in
44044430
which to unshelve the server.
4405-
:raises: UnshelveInstanceInvalidState if the server is not shelved
4406-
offloaded
44074431
:raises: InvalidRequest if the requested AZ does not exist
44084432
:raises: MismatchVolumeAZException if [cinder]/cross_az_attach=False
44094433
and any attached volumes are not in the requested AZ
44104434
"""
4411-
if instance.vm_state != vm_states.SHELVED_OFFLOADED:
4412-
# NOTE(brinzhang): If the server status is 'SHELVED', it still
4413-
# belongs to a host, the availability_zone has not changed.
4414-
# Unshelving a shelved offloaded server will go through the
4415-
# scheduler to find a new host.
4416-
raise exception.UnshelveInstanceInvalidState(
4417-
state=instance.vm_state, instance_uuid=instance.uuid)
4418-
44194435
available_zones = availability_zones.get_availability_zones(
44204436
context, self.host_api, get_only_available=True)
44214437
if availability_zone not in available_zones:
@@ -4443,31 +4459,88 @@ def _validate_unshelve_az(self, context, instance, availability_zone):
44434459

44444460
@block_extended_resource_request
44454461
@check_instance_lock
4446-
@check_instance_state(vm_state=[vm_states.SHELVED,
4447-
vm_states.SHELVED_OFFLOADED])
4448-
def unshelve(self, context, instance, new_az=None):
4449-
"""Restore a shelved instance."""
4462+
@check_instance_state(
4463+
vm_state=[vm_states.SHELVED, vm_states.SHELVED_OFFLOADED])
4464+
def unshelve(
4465+
self, context, instance, new_az=_sentinel, host=None):
4466+
"""Restore a shelved instance.
4467+
4468+
:param context: the nova request context
4469+
:param instance: nova.objects.instance.Instance object
4470+
:param new_az: (optional) target AZ.
4471+
If None is provided then the current AZ restriction
4472+
will be removed from the instance.
4473+
If the parameter is not provided then the current
4474+
AZ restriction will not be changed.
4475+
:param host: (optional) a host to target
4476+
"""
4477+
# Unshelving a shelved offloaded server will go through the
4478+
# scheduler to pick a new host, so we update the
4479+
# RequestSpec.availability_zone here. Note that if scheduling
4480+
# fails the RequestSpec will remain updated, which is not great.
4481+
# Bug open to track this https://bugs.launchpad.net/nova/+bug/1978573
4482+
4483+
az_passed = new_az is not self._sentinel
4484+
44504485
request_spec = objects.RequestSpec.get_by_instance_uuid(
44514486
context, instance.uuid)
44524487

4453-
if new_az:
4488+
# We need to check a list of preconditions and validate inputs first
4489+
4490+
# Ensure instance is shelve offloaded
4491+
if az_passed or host:
4492+
self._check_offloaded(context, instance)
4493+
4494+
if az_passed and new_az:
4495+
# we have to ensure that new AZ is valid
44544496
self._validate_unshelve_az(context, instance, new_az)
4455-
LOG.debug("Replace the old AZ %(old_az)s in RequestSpec "
4456-
"with a new AZ %(new_az)s of the instance.",
4457-
{"old_az": request_spec.availability_zone,
4458-
"new_az": new_az}, instance=instance)
4459-
# Unshelving a shelved offloaded server will go through the
4460-
# scheduler to pick a new host, so we update the
4461-
# RequestSpec.availability_zone here. Note that if scheduling
4462-
# fails the RequestSpec will remain updated, which is not great,
4463-
# but if we want to change that we need to defer updating the
4464-
# RequestSpec until conductor which probably means RPC changes to
4465-
# pass the new_az variable to conductor. This is likely low
4466-
# priority since the RequestSpec.availability_zone on a shelved
4467-
# offloaded server does not mean much anyway and clearly the user
4468-
# is trying to put the server in the target AZ.
4469-
request_spec.availability_zone = new_az
4470-
request_spec.save()
4497+
# This will be the AZ of the instance after the unshelve. It can be
4498+
# None indicating that the instance is not pinned to any AZ after the
4499+
# unshelve
4500+
expected_az_after_unshelve = (
4501+
request_spec.availability_zone
4502+
if not az_passed else new_az
4503+
)
4504+
# host is requested, so we have to see if it exists and does not
4505+
# contradict with the AZ of the instance
4506+
if host:
4507+
# Ensure that the requested host exists otherwise raise
4508+
# a ComputeHostNotFound exception
4509+
objects.ComputeNode.get_first_node_by_host_for_old_compat(
4510+
context, host, use_slave=True)
4511+
# A specific host is requested so we need to make sure that it is
4512+
# not contradicts with the AZ of the instance
4513+
self._ensure_host_in_az(
4514+
context, host, expected_az_after_unshelve)
4515+
4516+
if new_az is None:
4517+
LOG.debug(
4518+
'Unpin instance from AZ "%(old_az)s".',
4519+
{'old_az': request_spec.availability_zone},
4520+
instance=instance
4521+
)
4522+
4523+
LOG.debug(
4524+
'Unshelving instance with old availability_zone "%(old_az)s" to '
4525+
'new availability_zone "%(new_az)s" and host "%(host)s".',
4526+
{
4527+
'old_az': request_spec.availability_zone,
4528+
'new_az': '%s' %
4529+
new_az if az_passed
4530+
else 'not provided',
4531+
'host': host,
4532+
},
4533+
instance=instance,
4534+
)
4535+
# OK every precondition checks out, we just need to tell the scheduler
4536+
# where to put the instance
4537+
# We have the expected AZ already calculated. So we just need to
4538+
# set it in the request_spec to drive the scheduling
4539+
request_spec.availability_zone = expected_az_after_unshelve
4540+
# if host is requested we also need to tell the scheduler that
4541+
if host:
4542+
request_spec.requested_destination = objects.Destination(host=host)
4543+
request_spec.save()
44714544

44724545
instance.task_state = task_states.UNSHELVING
44734546
instance.save(expected_task_state=[None])

nova/conductor/manager.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1022,6 +1022,12 @@ def safe_image_show(ctx, image_id):
10221022
scheduler_utils.populate_filter_properties(
10231023
filter_properties, selection)
10241024
(host, node) = (selection.service_host, selection.nodename)
1025+
LOG.debug(
1026+
"Scheduler selected host: %s, node:%s",
1027+
host,
1028+
node,
1029+
instance=instance
1030+
)
10251031
instance.availability_zone = (
10261032
availability_zones.get_host_availability_zone(
10271033
context, host))

nova/exception.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1676,9 +1676,15 @@ class MismatchVolumeAZException(Invalid):
16761676

16771677

16781678
class UnshelveInstanceInvalidState(InstanceInvalidState):
1679-
msg_fmt = _('Specifying an availability zone when unshelving server '
1680-
'%(instance_uuid)s with status "%(state)s" is not supported. '
1681-
'The server status must be SHELVED_OFFLOADED.')
1679+
msg_fmt = _('Specifying an availability zone or a host when unshelving '
1680+
'server "%(instance_uuid)s" with status "%(state)s" is not '
1681+
'supported. The server status must be SHELVED_OFFLOADED.')
1682+
code = 409
1683+
1684+
1685+
class UnshelveHostNotInAZ(Invalid):
1686+
msg_fmt = _('Host "%(host)s" is not in the availability zone '
1687+
'"%(availability_zone)s".')
16821688
code = 409
16831689

16841690

nova/tests/unit/api/openstack/compute/test_shelve.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,9 @@ def test_unshelve_with_az_pre_2_77_failed(self, mock_get_instance):
140140
'unshelve') as mock_unshelve:
141141
self.controller._unshelve(self.req, fakes.FAKE_UUID, body=body)
142142
mock_unshelve.assert_called_once_with(
143-
self.req.environ['nova.context'], instance, new_az=None)
143+
self.req.environ['nova.context'],
144+
instance,
145+
)
144146

145147
@mock.patch('nova.compute.api.API.unshelve')
146148
@mock.patch('nova.api.openstack.common.get_instance')
@@ -158,7 +160,9 @@ def test_unshelve_with_none_pre_2_77_success(
158160
APIVersionRequest('2.76'))
159161
self.controller._unshelve(self.req, fakes.FAKE_UUID, body=body)
160162
mock_unshelve.assert_called_once_with(
161-
self.req.environ['nova.context'], instance, new_az=None)
163+
self.req.environ['nova.context'],
164+
instance,
165+
)
162166

163167
@mock.patch('nova.compute.api.API.unshelve')
164168
@mock.patch('nova.api.openstack.common.get_instance')

0 commit comments

Comments
 (0)