Add unpin AZ functionality
This allows a user to remove the availability-zone pinning from a server, as well as re-pin a server to the AZ it is currently in. Related to blueprint unpin-az Generated-By: Claude Opus 4.6 Change-Id: I4278f6c43814ca620662b44cc4950467324211e2 Signed-off-by: Dan Smith <dansmith@redhat.com>
This commit is contained in:
1 parent
a968a73a8f
commit
4bb7256f60
13 files changed
+401
-5
No files matched your search
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"server": {
|
||||
"accessIPv4": "1.2.3.4",
|
||||
"accessIPv6": "80fe::",
|
||||
"OS-DCF:diskConfig": "AUTO",
|
||||
"hostname": "new-server-hostname.example.com",
|
||||
"pinned_availability_zone": null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
{
|
||||
"server": {
|
||||
"accessIPv4": "1.2.3.4",
|
||||
"accessIPv6": "80fe::",
|
||||
"addresses": {
|
||||
"private": [
|
||||
{
|
||||
"addr": "192.168.1.30",
|
||||
"OS-EXT-IPS-MAC:mac_addr": "00:0c:29:0d:11:74",
|
||||
"OS-EXT-IPS:type": "fixed",
|
||||
"version": 4
|
||||
}
|
||||
]
|
||||
},
|
||||
"created": "2013-09-03T04:01:32Z",
|
||||
"description": null,
|
||||
"locked": false,
|
||||
"locked_reason": null,
|
||||
"flavor": {
|
||||
"disk": 1,
|
||||
"ephemeral": 0,
|
||||
"extra_specs": {},
|
||||
"original_name": "m1.tiny",
|
||||
"ram": 512,
|
||||
"swap": 0,
|
||||
"vcpus": 1
|
||||
},
|
||||
"hostId": "92154fab69d5883ba2c8622b7e65f745dd33257221c07af363c51b29",
|
||||
"id": "0e44cc9c-e052-415d-afbf-469b0d384170",
|
||||
"image": {
|
||||
"id": "70a599e0-31e7-49b7-b260-868f441e862b",
|
||||
"links": [
|
||||
{
|
||||
"href": "http://openstack.example.com/6f70656e737461636b20342065766572/images/70a599e0-31e7-49b7-b260-868f441e862b",
|
||||
"rel": "bookmark"
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"architecture": "x86_64",
|
||||
"auto_disk_config": "True"
|
||||
}
|
||||
},
|
||||
"key_name": null,
|
||||
"links": [
|
||||
{
|
||||
"href": "http://openstack.example.com/v2/6f70656e737461636b20342065766572/servers/0e44cc9c-e052-415d-afbf-469b0d384170",
|
||||
"rel": "self"
|
||||
},
|
||||
{
|
||||
"href": "http://openstack.example.com/6f70656e737461636b20342065766572/servers/0e44cc9c-e052-415d-afbf-469b0d384170",
|
||||
"rel": "bookmark"
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"My Server Name": "Apache1"
|
||||
},
|
||||
"name": "new-server-test",
|
||||
"config_drive": "",
|
||||
"OS-DCF:diskConfig": "AUTO",
|
||||
"OS-EXT-AZ:availability_zone": "nova",
|
||||
"OS-EXT-SRV-ATTR:hostname": "new-server-hostname.example.com",
|
||||
"OS-EXT-STS:power_state": 1,
|
||||
"OS-EXT-STS:task_state": null,
|
||||
"OS-EXT-STS:vm_state": "active",
|
||||
"os-extended-volumes:volumes_attached": [],
|
||||
"OS-SRV-USG:launched_at": "2013-09-23T13:37:00.880302",
|
||||
"OS-SRV-USG:terminated_at": null,
|
||||
"pinned_availability_zone": null,
|
||||
"progress": 0,
|
||||
"scheduler_hints": {},
|
||||
"security_groups": [
|
||||
{
|
||||
"name": "default"
|
||||
}
|
||||
],
|
||||
"server_groups": [],
|
||||
"status": "ACTIVE",
|
||||
"tags": [],
|
||||
"tenant_id": "6f70656e737461636b20342065766572",
|
||||
"trusted_image_certificates": null,
|
||||
"updated": "2013-09-03T04:01:33Z",
|
||||
"user_id": "fake"
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@
|
||||
}
|
||||
],
|
||||
"status": "CURRENT",
|
||||
"version": "2.103",
|
||||
"version": "2.104",
|
||||
"min_version": "2.1",
|
||||
"updated": "2013-07-23T11:33:21Z"
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
}
|
||||
],
|
||||
"status": "CURRENT",
|
||||
"version": "2.103",
|
||||
"version": "2.104",
|
||||
"min_version": "2.1",
|
||||
"updated": "2013-07-23T11:33:21Z"
|
||||
}
|
||||
|
||||
@@ -286,6 +286,9 @@ REST_API_VERSION_HISTORY = """REST API Version History:
|
||||
filters from various flavors APIs and restrict additional query
|
||||
string parameters for all APIs.
|
||||
* 2.103 - Remove the ``/os-volumes_boot`` API
|
||||
* 2.104 - Add support for updating ``pinned_availability_zone`` via
|
||||
``PUT /servers/{server_id}`` to unpin or re-pin a server's
|
||||
availability zone.
|
||||
"""
|
||||
|
||||
# The minimum and maximum versions of the API supported
|
||||
@@ -294,7 +297,7 @@ REST_API_VERSION_HISTORY = """REST API Version History:
|
||||
# Note(cyeoh): This only applies for the v2.1 API once microversions
|
||||
# support is fully merged. It does not affect the V2 API.
|
||||
_MIN_API_VERSION = '2.1'
|
||||
_MAX_API_VERSION = '2.103'
|
||||
_MAX_API_VERSION = '2.104'
|
||||
DEFAULT_API_VERSION = _MIN_API_VERSION
|
||||
|
||||
# Almost all proxy APIs which are related to network, images and baremetal
|
||||
|
||||
@@ -1335,3 +1335,25 @@ microversion 2.75.
|
||||
The ``/os-volumes_boot`` API is an old alias for the ``/servers`` API and was
|
||||
undocumented and untested. It has now been removed and will return HTTP 404 for
|
||||
all requests.
|
||||
|
||||
.. _microversion 2.104:
|
||||
|
||||
2.104
|
||||
-----
|
||||
|
||||
Add support for updating the ``pinned_availability_zone`` field via
|
||||
``PUT /servers/{server_id}``. Setting ``pinned_availability_zone`` to ``null``
|
||||
will unpin the server from its availability zone, allowing it to be scheduled
|
||||
to any availability zone on the next move operation. Setting
|
||||
``pinned_availability_zone`` to the server's current
|
||||
``OS-EXT-AZ:availability_zone`` value will re-pin the server to that zone.
|
||||
|
||||
The following transitions will return an HTTP 409 (Conflict) error:
|
||||
|
||||
- Attempting to re-pin (set to a non-``null`` value) to an availability zone
|
||||
that does not match the server's current location
|
||||
- Attempting to change from one non-``null`` availability zone to a different
|
||||
non-``null`` value (unpin first, then re-pin)
|
||||
|
||||
Setting ``pinned_availability_zone`` to its current value is idempotent and
|
||||
returns 200.
|
||||
@@ -401,6 +401,10 @@ update_v294 = copy.deepcopy(update_v290)
|
||||
update_v294['properties']['server'][
|
||||
'properties']['hostname'] = parameter_types.fqdn
|
||||
|
||||
update_v2104 = copy.deepcopy(update_v294)
|
||||
update_v2104['properties']['server'][
|
||||
'properties']['pinned_availability_zone'] = parameter_types.name_or_none
|
||||
|
||||
rebuild = {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
|
||||
@@ -31,6 +31,7 @@ from nova.api.openstack.compute.schemas import servers as schema
|
||||
from nova.api.openstack.compute.views import servers as views_servers
|
||||
from nova.api.openstack import wsgi
|
||||
from nova.api import validation
|
||||
from nova import availability_zones as avail_zone
|
||||
from nova import block_device
|
||||
from nova.compute import api as compute
|
||||
from nova.compute import flavors
|
||||
@@ -952,12 +953,13 @@ class ServersController(wsgi.Controller):
|
||||
else:
|
||||
self.compute_api.delete(context, instance)
|
||||
|
||||
@wsgi.expected_errors(404)
|
||||
@wsgi.expected_errors((404, 409))
|
||||
@validation.schema(schema.update_v20, '2.0', '2.0')
|
||||
@validation.schema(schema.update, '2.1', '2.18')
|
||||
@validation.schema(schema.update_v219, '2.19', '2.89')
|
||||
@validation.schema(schema.update_v290, '2.90', '2.93')
|
||||
@validation.schema(schema.update_v294, '2.94')
|
||||
@validation.schema(schema.update_v294, '2.94', '2.103')
|
||||
@validation.schema(schema.update_v2104, '2.104')
|
||||
@validation.response_body_schema(schema.update_response, '2.0', '2.8')
|
||||
@validation.response_body_schema(schema.update_response_v29, '2.9', '2.18')
|
||||
@validation.response_body_schema(schema.update_response_v219, '2.19', '2.25') # noqa: E501
|
||||
@@ -993,6 +995,42 @@ class ServersController(wsgi.Controller):
|
||||
if 'hostname' in server:
|
||||
update_dict['hostname'] = server['hostname']
|
||||
|
||||
if (api_version_request.is_supported(req, '2.104') and
|
||||
'pinned_availability_zone' in server):
|
||||
requested_az = server['pinned_availability_zone']
|
||||
request_spec = objects.RequestSpec.get_by_instance_uuid(
|
||||
ctxt, instance.uuid)
|
||||
current_pinned_az = request_spec.availability_zone
|
||||
|
||||
if requested_az is None:
|
||||
if current_pinned_az is not None:
|
||||
request_spec.availability_zone = None
|
||||
request_spec.save()
|
||||
else:
|
||||
instance_az = (
|
||||
avail_zone.get_instance_availability_zone(
|
||||
ctxt, instance))
|
||||
if current_pinned_az is not None:
|
||||
if requested_az != current_pinned_az:
|
||||
raise exc.HTTPConflict(
|
||||
explanation=_(
|
||||
"Cannot change pinned availability "
|
||||
"zone directly. Unpin first by "
|
||||
"setting pinned_availability_zone "
|
||||
"to null."))
|
||||
elif requested_az != instance_az:
|
||||
raise exc.HTTPConflict(
|
||||
explanation=_(
|
||||
"Server can only be pinned to its "
|
||||
"current availability zone "
|
||||
"'%(current_az)s', but "
|
||||
"'%(requested_az)s' was requested."
|
||||
) % {'current_az': instance_az,
|
||||
'requested_az': requested_az})
|
||||
else:
|
||||
request_spec.availability_zone = requested_az
|
||||
request_spec.save()
|
||||
|
||||
helpers.translate_attributes(helpers.UPDATE, server, update_dict)
|
||||
|
||||
try:
|
||||
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"server": {
|
||||
"accessIPv4": "%(access_ip_v4)s",
|
||||
"accessIPv6": "%(access_ip_v6)s",
|
||||
"OS-DCF:diskConfig": "AUTO",
|
||||
"hostname": "new-server-hostname.example.com",
|
||||
"pinned_availability_zone": null
|
||||
}
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
{
|
||||
"server": {
|
||||
"accessIPv4": "%(access_ip_v4)s",
|
||||
"accessIPv6": "%(access_ip_v6)s",
|
||||
"addresses": {
|
||||
"private": [
|
||||
{
|
||||
"addr": "%(ip)s",
|
||||
"OS-EXT-IPS-MAC:mac_addr": "00:0c:29:0d:11:74",
|
||||
"OS-EXT-IPS:type": "fixed",
|
||||
"version": 4
|
||||
}
|
||||
]
|
||||
},
|
||||
"created": "%(isotime)s",
|
||||
"description": null,
|
||||
"locked": false,
|
||||
"locked_reason": null,
|
||||
"flavor": {
|
||||
"disk": 1,
|
||||
"ephemeral": 0,
|
||||
"extra_specs": {},
|
||||
"original_name": "m1.tiny",
|
||||
"ram": 512,
|
||||
"swap": 0,
|
||||
"vcpus": 1
|
||||
},
|
||||
"hostId": "%(hostid)s",
|
||||
"id": "%(id)s",
|
||||
"image": {
|
||||
"id": "%(uuid)s",
|
||||
"links": [
|
||||
{
|
||||
"href": "%(compute_endpoint)s/images/%(uuid)s",
|
||||
"rel": "bookmark"
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"architecture": "x86_64",
|
||||
"auto_disk_config": "True"
|
||||
}
|
||||
},
|
||||
"key_name": null,
|
||||
"links": [
|
||||
{
|
||||
"href": "%(versioned_compute_endpoint)s/servers/%(uuid)s",
|
||||
"rel": "self"
|
||||
},
|
||||
{
|
||||
"href": "%(compute_endpoint)s/servers/%(uuid)s",
|
||||
"rel": "bookmark"
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"My Server Name": "Apache1"
|
||||
},
|
||||
"name": "new-server-test",
|
||||
"config_drive": "",
|
||||
"OS-DCF:diskConfig": "AUTO",
|
||||
"OS-EXT-AZ:availability_zone": "nova",
|
||||
"OS-EXT-SRV-ATTR:hostname": "new-server-hostname.example.com",
|
||||
"OS-EXT-STS:power_state": 1,
|
||||
"OS-EXT-STS:task_state": null,
|
||||
"OS-EXT-STS:vm_state": "active",
|
||||
"os-extended-volumes:volumes_attached": [],
|
||||
"OS-SRV-USG:launched_at": "%(strtime)s",
|
||||
"OS-SRV-USG:terminated_at": null,
|
||||
"pinned_availability_zone": null,
|
||||
"progress": 0,
|
||||
"scheduler_hints": {},
|
||||
"security_groups": [
|
||||
{
|
||||
"name": "default"
|
||||
}
|
||||
],
|
||||
"server_groups": [],
|
||||
"status": "ACTIVE",
|
||||
"tags": [],
|
||||
"tenant_id": "6f70656e737461636b20342065766572",
|
||||
"trusted_image_certificates": null,
|
||||
"updated": "%(isotime)s",
|
||||
"user_id": "fake"
|
||||
}
|
||||
}
|
||||
@@ -868,6 +868,22 @@ class ServersUpdateSampleJson294Test(ServersUpdateSampleJsonTest):
|
||||
self._verify_response('server-action-rebuild-resp', subs, resp, 202)
|
||||
|
||||
|
||||
class ServersUpdateSampleJson2104Test(ServersUpdateSampleJsonTest):
|
||||
microversion = '2.104'
|
||||
scenarios = [('v2_104', {'api_major_version': 'v2.1'})]
|
||||
ADMIN_API = False
|
||||
|
||||
def test_update_server(self):
|
||||
uuid = self._post_server()
|
||||
subs = {}
|
||||
subs['hostid'] = '[a-f0-9]+'
|
||||
subs['access_ip_v4'] = '1.2.3.4'
|
||||
subs['access_ip_v6'] = '80fe::'
|
||||
response = self._do_put('servers/%s' % uuid,
|
||||
'server-update-req', subs)
|
||||
self._verify_response('server-update-resp', subs, response, 200)
|
||||
|
||||
|
||||
class ServerSortKeysJsonTests(ServersSampleBase):
|
||||
sample_dir = 'servers-sort'
|
||||
|
||||
|
||||
@@ -4259,6 +4259,124 @@ class ServersControllerUpdateTestV290(_ServersControllerUpdateTest):
|
||||
self.assertIn('hostname', str(ex))
|
||||
|
||||
|
||||
class ServersControllerUpdateTestV2104(_ServersControllerUpdateTest):
|
||||
|
||||
microversion = '2.104'
|
||||
|
||||
def _setup_reqspec_mock(self, availability_zone):
|
||||
mock_reqspec = mock.MagicMock()
|
||||
mock_reqspec.availability_zone = availability_zone
|
||||
objects.RequestSpec.get_by_instance_uuid.return_value = mock_reqspec
|
||||
return mock_reqspec
|
||||
|
||||
@mock.patch('nova.availability_zones.get_instance_availability_zone',
|
||||
return_value='az1')
|
||||
def test_update_server_unpin_az(self, mock_get_az):
|
||||
mock_reqspec = self._setup_reqspec_mock('az1')
|
||||
|
||||
body = {'server': {'pinned_availability_zone': None}}
|
||||
req = self._get_request(body)
|
||||
res_dict = self.controller.update(req, FAKE_UUID, body=body)
|
||||
|
||||
self.assertEqual(FAKE_UUID, res_dict['server']['id'])
|
||||
self.assertIsNone(mock_reqspec.availability_zone)
|
||||
mock_reqspec.save.assert_called_once()
|
||||
|
||||
@mock.patch('nova.availability_zones.get_instance_availability_zone',
|
||||
return_value='az1')
|
||||
def test_update_server_repin_az(self, mock_get_az):
|
||||
mock_reqspec = self._setup_reqspec_mock(None)
|
||||
|
||||
body = {'server': {'pinned_availability_zone': 'az1'}}
|
||||
req = self._get_request(body)
|
||||
res_dict = self.controller.update(req, FAKE_UUID, body=body)
|
||||
|
||||
self.assertEqual(FAKE_UUID, res_dict['server']['id'])
|
||||
self.assertEqual('az1', mock_reqspec.availability_zone)
|
||||
mock_reqspec.save.assert_called_once()
|
||||
|
||||
@mock.patch('nova.availability_zones.get_instance_availability_zone',
|
||||
return_value='az1')
|
||||
def test_update_server_unpin_az_already_unpinned(self, mock_get_az):
|
||||
mock_reqspec = self._setup_reqspec_mock(None)
|
||||
|
||||
body = {'server': {'pinned_availability_zone': None}}
|
||||
req = self._get_request(body)
|
||||
res_dict = self.controller.update(req, FAKE_UUID, body=body)
|
||||
|
||||
self.assertEqual(FAKE_UUID, res_dict['server']['id'])
|
||||
mock_reqspec.save.assert_not_called()
|
||||
|
||||
@mock.patch('nova.availability_zones.get_instance_availability_zone',
|
||||
return_value='az1')
|
||||
def test_update_server_repin_az_already_pinned(self, mock_get_az):
|
||||
mock_reqspec = self._setup_reqspec_mock('az1')
|
||||
|
||||
body = {'server': {'pinned_availability_zone': 'az1'}}
|
||||
req = self._get_request(body)
|
||||
self.controller.update(req, FAKE_UUID, body=body)
|
||||
mock_reqspec.save.assert_not_called()
|
||||
|
||||
@mock.patch('nova.availability_zones.get_instance_availability_zone',
|
||||
return_value='az1')
|
||||
def test_update_server_repin_az_wrong_az(self, mock_get_az):
|
||||
self._setup_reqspec_mock(None)
|
||||
|
||||
body = {'server': {'pinned_availability_zone': 'az2'}}
|
||||
req = self._get_request(body)
|
||||
self.assertRaises(
|
||||
webob.exc.HTTPConflict,
|
||||
self.controller.update,
|
||||
req, FAKE_UUID, body=body)
|
||||
|
||||
@mock.patch('nova.availability_zones.get_instance_availability_zone',
|
||||
return_value='az1')
|
||||
def test_update_server_repin_az_change_pinned(self, mock_get_az):
|
||||
self._setup_reqspec_mock('az1')
|
||||
|
||||
body = {'server': {'pinned_availability_zone': 'az2'}}
|
||||
req = self._get_request(body)
|
||||
self.assertRaises(
|
||||
webob.exc.HTTPConflict,
|
||||
self.controller.update,
|
||||
req, FAKE_UUID, body=body)
|
||||
|
||||
def test_update_server_pinned_az_old_version(self):
|
||||
body = {'server': {'pinned_availability_zone': None}}
|
||||
req = self._get_request(body)
|
||||
req.api_version_request = api_version_request.APIVersionRequest(
|
||||
'2.103')
|
||||
self.assertRaises(
|
||||
exception.ValidationError,
|
||||
self.controller.update,
|
||||
req, FAKE_UUID, body=body)
|
||||
|
||||
def test_update_server_pinned_az_empty_string(self):
|
||||
body = {'server': {'pinned_availability_zone': ''}}
|
||||
req = self._get_request(body)
|
||||
self.assertRaises(
|
||||
exception.ValidationError,
|
||||
self.controller.update,
|
||||
req, FAKE_UUID, body=body)
|
||||
|
||||
@mock.patch('nova.availability_zones.get_instance_availability_zone',
|
||||
return_value='az1')
|
||||
def test_update_server_pinned_az_with_name(self, mock_get_az):
|
||||
mock_reqspec = self._setup_reqspec_mock('az1')
|
||||
|
||||
body = {'server': {
|
||||
'name': 'new-name',
|
||||
'pinned_availability_zone': None,
|
||||
}}
|
||||
req = self._get_request(body)
|
||||
res_dict = self.controller.update(req, FAKE_UUID, body=body)
|
||||
|
||||
self.assertEqual(FAKE_UUID, res_dict['server']['id'])
|
||||
self.assertEqual('new-name', res_dict['server']['name'])
|
||||
self.assertIsNone(mock_reqspec.availability_zone)
|
||||
mock_reqspec.save.assert_called_once()
|
||||
|
||||
|
||||
class ServersControllerTriggerCrashDumpTest(ControllerTest):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
A new microversion has been added to allow unpinning and re-pinning the
|
||||
availability zone for an instance. By doing a ``PUT /servers/{server_id}``
|
||||
containing a value for ``pinned_availability_zone``, a user can remove the
|
||||
AZ or add it back. For re-pinning, only the current AZ of the instance is
|
||||
allowed to be set (i.e. equal to the ``OS-EXT-AZ:availability_zone``)
|
||||
value.
|
||||
Reference in new issue
Block a user