Return uuid attribute for aggregates

Adds a Compute API microversion that triggers returning an aggregate's UUID
field. This field is necessary for scripts that must populate the placement API
with resource provider to aggregate relationships, which rely on UUIDs for
global identification.

APIImpact
blueprint: return-uuid-from-os-aggregates-api
Change-Id: I4112ccd508eb85403933fec8b52efd468e866772
Closes-bug: #1652642
This commit is contained in:
Jay Pipes 2016-12-26 17:58:44 -05:00 committed by Matt Riedemann
parent aba18ab045
commit 8b4fd32e7b
29 changed files with 376 additions and 17 deletions

View File

@ -34,6 +34,7 @@ Response
- metadata: aggregate_metadata
- name: aggregate_name
- updated_at: updated_consider_null
- uuid: aggregate_uuid
**Example List Aggregates: JSON response**
@ -79,6 +80,7 @@ Response
- id: aggregate_id_body
- name: aggregate_name
- updated_at: updated_consider_null
- uuid: aggregate_uuid
**Example Create Aggregate: JSON response**
@ -118,6 +120,7 @@ Response
- metadata: aggregate_metadata
- name: aggregate_name
- updated_at: updated_consider_null
- uuid: aggregate_uuid
**Example Show Aggregate Details: JSON response**
@ -168,6 +171,7 @@ Response
- metadata: aggregate_metadata
- name: aggregate_name
- updated_at: updated_consider_null
- uuid: aggregate_uuid
**Example Update Aggregate: JSON response**
@ -240,6 +244,7 @@ Response
- metadata: aggregate_metadata
- name: aggregate_name
- updated_at: updated_consider_null
- uuid: aggregate_uuid
**Example Add Host: JSON response**
@ -289,6 +294,7 @@ Response
- metadata: aggregate_metadata
- name: aggregate_name
- updated_at: updated_consider_null
- uuid: aggregate_uuid
**Example Remove Host: JSON response**
@ -338,6 +344,7 @@ Response
- metadata: aggregate_metadata
- name: aggregate_name
- updated_at: updated_consider_null
- uuid: aggregate_uuid
**Example Create Or Update Aggregate Metadata: JSON response**

View File

@ -0,0 +1,5 @@
{
"add_host": {
"host": "compute"
}
}

View File

@ -0,0 +1,9 @@
{
"set_metadata":
{
"metadata":
{
"key": "value"
}
}
}

View File

@ -0,0 +1,7 @@
{
"aggregate":
{
"name": "name",
"availability_zone": "nova"
}
}

View File

@ -0,0 +1,12 @@
{
"aggregate": {
"availability_zone": "nova",
"created_at": "2016-12-27T22:51:32.877711",
"deleted": false,
"deleted_at": null,
"id": 1,
"name": "name",
"updated_at": null,
"uuid": "86a0da0e-9f0c-4f51-a1e0-3c25edab3783"
}
}

View File

@ -0,0 +1,5 @@
{
"remove_host": {
"host": "compute"
}
}

View File

@ -0,0 +1,7 @@
{
"aggregate":
{
"name": "newname",
"availability_zone": "nova2"
}
}

View File

@ -0,0 +1,16 @@
{
"aggregate": {
"availability_zone": "nova2",
"created_at": "2016-12-27T23:47:32.897139",
"deleted": false,
"deleted_at": null,
"hosts": [],
"id": 1,
"metadata": {
"availability_zone": "nova2"
},
"name": "newname",
"updated_at": "2016-12-27T23:47:33.067180",
"uuid": "6f74e3f3-df28-48f3-98e1-ac941b1c5e43"
}
}

View File

@ -0,0 +1,18 @@
{
"aggregate": {
"availability_zone": "nova",
"created_at": "2016-12-27T23:47:30.594805",
"deleted": false,
"deleted_at": null,
"hosts": [
"compute"
],
"id": 1,
"metadata": {
"availability_zone": "nova"
},
"name": "name",
"updated_at": null,
"uuid": "d1842372-89c5-4fbd-ad5a-5d2e16c85456"
}
}

View File

@ -0,0 +1,16 @@
{
"aggregate": {
"availability_zone": "nova",
"created_at": "2016-12-27T23:47:30.563527",
"deleted": false,
"deleted_at": null,
"hosts": [],
"id": 1,
"metadata": {
"availability_zone": "nova"
},
"name": "name",
"updated_at": null,
"uuid": "fd0a5b12-7e8d-469d-bfd5-64a6823e7407"
}
}

View File

@ -0,0 +1,20 @@
{
"aggregates": [
{
"availability_zone": "nova",
"created_at": "2016-12-27T23:47:32.911515",
"deleted": false,
"deleted_at": null,
"hosts": [
"compute"
],
"id": 1,
"metadata": {
"availability_zone": "nova"
},
"name": "name",
"updated_at": null,
"uuid": "6ba28ba7-f29b-45cc-a30b-6e3a40c2fb14"
}
]
}

View File

@ -0,0 +1,17 @@
{
"aggregate": {
"availability_zone": "nova",
"created_at": "2016-12-27T23:59:18.623100",
"deleted": false,
"deleted_at": null,
"hosts": [],
"id": 1,
"metadata": {
"availability_zone": "nova",
"key": "value"
},
"name": "name",
"updated_at": "2016-12-27T23:59:18.723348",
"uuid": "26002bdb-62cc-41bd-813a-0ad22db32625"
}
}

View File

@ -0,0 +1,16 @@
{
"aggregate": {
"availability_zone": "nova",
"created_at": "2016-12-27T23:47:30.594805",
"deleted": false,
"deleted_at": null,
"hosts": [],
"id": 1,
"metadata": {
"availability_zone": "nova"
},
"name": "name",
"updated_at": null,
"uuid": "d1842372-89c5-4fbd-ad5a-5d2e16c85456"
}
}

View File

@ -19,6 +19,7 @@ import datetime
from webob import exc
from nova.api.openstack import api_version_request
from nova.api.openstack import common
from nova.api.openstack.compute.schemas import aggregates
from nova.api.openstack import extensions
@ -47,7 +48,7 @@ class AggregateController(wsgi.Controller):
context = _get_context(req)
context.can(aggr_policies.POLICY_ROOT % 'index')
aggregates = self.api.get_aggregate_list(context)
return {'aggregates': [self._marshall_aggregate(a)['aggregate']
return {'aggregates': [self._marshall_aggregate(req, a)['aggregate']
for a in aggregates]}
# NOTE(gmann): Returns 200 for backwards compatibility but should be 201
@ -77,7 +78,7 @@ class AggregateController(wsgi.Controller):
except exception.InvalidAggregateAction as e:
raise exc.HTTPBadRequest(explanation=e.format_message())
agg = self._marshall_aggregate(aggregate)
agg = self._marshall_aggregate(req, aggregate)
# To maintain the same API result as before the changes for returning
# nova objects were made.
@ -95,7 +96,7 @@ class AggregateController(wsgi.Controller):
aggregate = self.api.get_aggregate(context, id)
except exception.AggregateNotFound as e:
raise exc.HTTPNotFound(explanation=e.format_message())
return self._marshall_aggregate(aggregate)
return self._marshall_aggregate(req, aggregate)
@extensions.expected_errors((400, 404, 409))
@validation.schema(aggregates.update_v20, '2.0', '2.0')
@ -117,7 +118,7 @@ class AggregateController(wsgi.Controller):
except exception.InvalidAggregateAction as e:
raise exc.HTTPBadRequest(explanation=e.format_message())
return self._marshall_aggregate(aggregate)
return self._marshall_aggregate(req, aggregate)
# NOTE(gmann): Returns 200 for backwards compatibility but should be 204
# as this operation complete the deletion of aggregate resource and return
@ -154,7 +155,7 @@ class AggregateController(wsgi.Controller):
except (exception.AggregateHostExists,
exception.InvalidAggregateAction) as e:
raise exc.HTTPConflict(explanation=e.format_message())
return self._marshall_aggregate(aggregate)
return self._marshall_aggregate(req, aggregate)
# NOTE(gmann): Returns 200 for backwards compatibility but should be 202
# for representing async API as this API just accepts the request and
@ -179,7 +180,7 @@ class AggregateController(wsgi.Controller):
msg = _('Cannot remove host %(host)s in aggregate %(id)s') % {
'host': host, 'id': id}
raise exc.HTTPConflict(explanation=msg)
return self._marshall_aggregate(aggregate)
return self._marshall_aggregate(req, aggregate)
@extensions.expected_errors((400, 404))
@wsgi.action('set_metadata')
@ -198,18 +199,19 @@ class AggregateController(wsgi.Controller):
except exception.InvalidAggregateAction as e:
raise exc.HTTPBadRequest(explanation=e.format_message())
return self._marshall_aggregate(aggregate)
return self._marshall_aggregate(req, aggregate)
def _marshall_aggregate(self, aggregate):
def _marshall_aggregate(self, req, aggregate):
_aggregate = {}
for key, value in self._build_aggregate_items(aggregate):
for key, value in self._build_aggregate_items(req, aggregate):
# NOTE(danms): The original API specified non-TZ-aware timestamps
if isinstance(value, datetime.datetime):
value = value.replace(tzinfo=None)
_aggregate[key] = value
return {"aggregate": _aggregate}
def _build_aggregate_items(self, aggregate):
def _build_aggregate_items(self, req, aggregate):
show_uuid = api_version_request.is_supported(req, min_version="2.41")
keys = aggregate.obj_fields
# NOTE(rlrossit): Within the compute API, metadata will always be
# set on the aggregate object (at a minimum to {}). Because of this,
@ -217,11 +219,9 @@ class AggregateController(wsgi.Controller):
# case it is only ['availability_zone']) without worrying about
# lazy-loading an unset variable
for key in keys:
# NOTE(danms): Skip the uuid field because we have no microversion
# to expose it
if ((aggregate.obj_attr_is_set(key)
or key in aggregate.obj_extra_fields) and
key != 'uuid'):
(show_uuid or key != 'uuid')):
yield key, getattr(aggregate, key)

View File

@ -0,0 +1,5 @@
{
"add_host": {
"host": "%(host_name)s"
}
}

View File

@ -0,0 +1,9 @@
{
"set_metadata":
{
"metadata":
{
"key": "value"
}
}
}

View File

@ -0,0 +1,7 @@
{
"aggregate":
{
"name": "name",
"availability_zone": "nova"
}
}

View File

@ -0,0 +1,12 @@
{
"aggregate": {
"availability_zone": "nova",
"created_at": "%(strtime)s",
"deleted": false,
"deleted_at": null,
"id": %(aggregate_id)s,
"name": "name",
"updated_at": null,
"uuid": "%(uuid)s"
}
}

View File

@ -0,0 +1,5 @@
{
"remove_host": {
"host": "%(host_name)s"
}
}

View File

@ -0,0 +1,7 @@
{
"aggregate":
{
"name": "newname",
"availability_zone": "nova2"
}
}

View File

@ -0,0 +1,16 @@
{
"aggregate": {
"availability_zone": "nova2",
"created_at": "%(strtime)s",
"deleted": false,
"deleted_at": null,
"hosts": [],
"id": 1,
"metadata": {
"availability_zone": "nova2"
},
"name": "newname",
"updated_at": "%(strtime)s",
"uuid": "%(uuid)s"
}
}

View File

@ -0,0 +1,18 @@
{
"aggregate": {
"availability_zone": "nova",
"created_at": "%(strtime)s",
"deleted": false,
"deleted_at": null,
"hosts": [
"%(compute_host)s"
],
"id": 1,
"metadata": {
"availability_zone": "nova"
},
"name": "name",
"updated_at": null,
"uuid": "%(uuid)s"
}
}

View File

@ -0,0 +1,16 @@
{
"aggregate": {
"availability_zone": "nova",
"created_at": "%(strtime)s",
"deleted": false,
"deleted_at": null,
"hosts": [],
"id": 1,
"metadata": {
"availability_zone": "nova"
},
"name": "name",
"updated_at": null,
"uuid": "%(uuid)s"
}
}

View File

@ -0,0 +1,20 @@
{
"aggregates": [
{
"availability_zone": "nova",
"created_at": "%(strtime)s",
"deleted": false,
"deleted_at": null,
"hosts": [
"%(compute_host)s"
],
"id": 1,
"metadata": {
"availability_zone": "nova"
},
"name": "name",
"updated_at": null,
"uuid": "%(uuid)s"
}
]
}

View File

@ -0,0 +1,17 @@
{
"aggregate": {
"availability_zone": "nova",
"created_at": "%(strtime)s",
"deleted": false,
"deleted_at": null,
"hosts": [],
"id": 1,
"metadata": {
"availability_zone": "nova",
"key": "value"
},
"name": "name",
"updated_at": %(strtime)s,
"uuid": "%(uuid)s"
}
}

View File

@ -0,0 +1,16 @@
{
"aggregate": {
"availability_zone": "nova",
"created_at": "%(strtime)s",
"deleted": false,
"deleted_at": null,
"hosts": [],
"id": 1,
"metadata": {
"availability_zone": "nova"
},
"name": "name",
"updated_at": null,
"uuid": "%(uuid)s"
}
}

View File

@ -13,12 +13,18 @@
# License for the specific language governing permissions and limitations
# under the License.
from oslo_serialization import jsonutils
from nova.tests.functional.api_sample_tests import api_sample_base
class AggregatesSampleJsonTest(api_sample_base.ApiSampleTestBaseV21):
ADMIN_API = True
sample_dir = "os-aggregates"
# extra_subs is a noop in the base v2.1 test class; it's used to sub in
# additional details for response verification of actions performed on an
# existing aggregate.
extra_subs = {}
def _test_aggregate_create(self):
subs = {
@ -37,6 +43,7 @@ class AggregatesSampleJsonTest(api_sample_base.ApiSampleTestBaseV21):
}
response = self._do_post('os-aggregates/%s/action' % aggregate_id,
'aggregate-add-host-post-req', subs)
subs.update(self.extra_subs)
self._verify_response('aggregates-add-host-post-resp', subs,
response, 200)
@ -49,14 +56,15 @@ class AggregatesSampleJsonTest(api_sample_base.ApiSampleTestBaseV21):
def test_aggregate_get(self):
agg_id = self._test_aggregate_create()
response = self._do_get('os-aggregates/%s' % agg_id)
self._verify_response('aggregates-get-resp', {}, response, 200)
self._verify_response('aggregates-get-resp', self.extra_subs,
response, 200)
def test_add_metadata(self):
agg_id = self._test_aggregate_create()
response = self._do_post('os-aggregates/%s/action' % agg_id,
'aggregate-metadata-post-req',
{'action': 'set_metadata'})
self._verify_response('aggregates-metadata-post-resp', {},
self._verify_response('aggregates-metadata-post-resp', self.extra_subs,
response, 200)
def test_add_host(self):
@ -70,6 +78,7 @@ class AggregatesSampleJsonTest(api_sample_base.ApiSampleTestBaseV21):
}
response = self._do_post('os-aggregates/1/action',
'aggregate-remove-host-post-req', subs)
subs.update(self.extra_subs)
self._verify_response('aggregates-remove-host-post-resp',
subs, response, 200)
@ -78,4 +87,33 @@ class AggregatesSampleJsonTest(api_sample_base.ApiSampleTestBaseV21):
response = self._do_put('os-aggregates/%s' % aggregate_id,
'aggregate-update-post-req', {})
self._verify_response('aggregate-update-post-resp',
{}, response, 200)
self.extra_subs, response, 200)
class AggregatesV2_41_SampleJsonTest(AggregatesSampleJsonTest):
microversion = '2.41'
scenarios = [
(
"v2_41", {
'api_major_version': 'v2.1',
},
)
]
def _test_aggregate_create(self):
subs = {
"aggregate_id": '(?P<id>\d+)',
}
response = self._do_post('os-aggregates', 'aggregate-post-req', subs)
# This feels like cheating since we're getting the uuid from the
# response before we even validate that it exists in the response based
# on the sample, but we'll fail with a KeyError if it doesn't which is
# maybe good enough. Alternatively we have to mock out the DB API
# to return a fake aggregate with a hard-coded uuid that matches the
# API sample which isn't fun either.
subs['uuid'] = jsonutils.loads(response.content)['aggregate']['uuid']
# save off the uuid for subs validation on other actions performed
# on this aggregate
self.extra_subs['uuid'] = subs['uuid']
return self._verify_response('aggregate-post-resp',
subs, response, 200)

View File

@ -18,6 +18,7 @@
import mock
from webob import exc
from nova.api.openstack import api_version_request
from nova.api.openstack.compute import aggregates as aggregates_v21
from nova.compute import api as compute_api
from nova import context
@ -743,11 +744,23 @@ class AggregateTestCaseV21(test.NoDBTestCase):
'metadata': {'foo': 'bar', 'availability_zone': 'nova'},
'hosts': ['host1', 'host2']}
agg_obj = _make_agg_obj(agg)
marshalled_agg = self.controller._marshall_aggregate(agg_obj)
# _marshall_aggregate() puts all fields and obj_extra_fields in the
# top-level dict, so we need to put availability_zone at the top also
agg['availability_zone'] = 'nova'
avr_v240 = api_version_request.APIVersionRequest("2.40")
avr_v241 = api_version_request.APIVersionRequest("2.41")
req = mock.MagicMock(api_version_request=avr_v241)
marshalled_agg = self.controller._marshall_aggregate(req, agg_obj)
self.assertEqual(agg, marshalled_agg['aggregate'])
req = mock.MagicMock(api_version_request=avr_v240)
marshalled_agg = self.controller._marshall_aggregate(req, agg_obj)
# UUID isn't in microversion 2.40 and before
del agg['uuid']
self.assertEqual(agg, marshalled_agg['aggregate'])

View File

@ -0,0 +1,5 @@
---
features:
- A new 2.41 microversion was added to the Compute API. Users specifying this
microversion will now see the 'uuid' attribute of aggregates when calling
the `os-aggregates` REST API endpoint.