Merge "Return uuid attribute for aggregates"

This commit is contained in:
Jenkins 2017-01-07 05:12:18 +00:00 committed by Gerrit Code Review
commit 9b918199d8
35 changed files with 397 additions and 20 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

@ -967,6 +967,12 @@ aggregate_remove_host:
in: body
required: true
type: object
aggregate_uuid:
description: |
The UUID of the host aggregate.
in: body
type: string
min_version: 2.41
aggregates:
description: |
The list of existing aggregates.

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,7 +19,7 @@
}
],
"status": "CURRENT",
"version": "2.40",
"version": "2.41",
"min_version": "2.1",
"updated": "2013-07-23T11:33:21Z"
}

View File

@ -22,7 +22,7 @@
}
],
"status": "CURRENT",
"version": "2.40",
"version": "2.41",
"min_version": "2.1",
"updated": "2013-07-23T11:33:21Z"
}

View File

@ -97,6 +97,7 @@ REST_API_VERSION_HISTORY = """REST API Version History:
provided for listing servers.
* 2.39 - Deprecates image-metadata proxy API
* 2.40 - Adds simple tenant usage pagination support.
* 2.41 - Return uuid attribute for aggregates.
"""
# The minimum and maximum versions of the API supported
@ -105,7 +106,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.40"
_MAX_API_VERSION = "2.41"
DEFAULT_API_VERSION = _MIN_API_VERSION
# Almost all proxy APIs which related to network, images and baremetal

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

@ -445,3 +445,11 @@ user documentation.
`CONF.api.max_limit` to encourage the adoption of this new microversion,
and circumvent the existing possibility DoS-like usage requests on systems
with thousands of instances.
2.41
----
The 'uuid' attribute of an aggregate is now returned from calls to the
`/os-aggregates` endpoint. This attribute is auto-generated upon creation of
an aggregate. The `os-aggregates` API resource endpoint remains an
administrator-only API.

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

@ -89,6 +89,9 @@ class _IntegratedTestBase(test.TestCase):
else:
self.api = self.api_fixture.api
if hasattr(self, 'microversion'):
self.api.microversion = self.microversion
self.useFixture(cast_as_call.CastAsCall(self.stubs))
self.addCleanup(nova.tests.unit.image.fake.FakeImageService_reset)

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.