Add aggregate API

This addes the aggregate APIs and docs

Partially Implements: bp node-aggregate

Change-Id: Iaa8d1de301f09360f7aa36e3a8fe5829e65b55c9
This commit is contained in:
Zhenguo Niu 2017-07-18 17:09:57 +08:00
parent 9c76ae4467
commit 27f3b1832a
14 changed files with 568 additions and 1 deletions

View File

@ -0,0 +1,182 @@
.. -*- rst -*-
============
Aggregates
============
Creates and manages node aggregates. An aggregate assigns metadata to
groups of compute nodes. Aggregates are only visible to the cloud provider.
List Aggregates
===============
.. rest_method:: GET /aggregates
Lists all aggregates.
Normal response codes: 200
Error response codes: unauthorized(401), forbidden(403)
Response
--------
.. rest_parameters:: parameters.yaml
- aggregates: aggregates
- name: aggregate_name
- links: links
- metadata: aggregate_metadata
- uuid: aggregate_uuid
- created_at: created_at
- updated_at: updated_at
**Example List aggregates: JSON response**
.. literalinclude:: samples/aggregates/aggregates-list-resp.json
:language: javascript
Create Aggregate
================
.. rest_method:: POST /aggregates
Creates an aggregate.
Normal response codes: 201
Error response codes: badRequest(400), unauthorized(401), forbidden(403),
conflict(409)
Request
-------
.. rest_parameters:: parameters.yaml
- name: aggregate_name
- metadata: aggregate_metadata
**Example Create Aggregatei: JSON request**
.. literalinclude:: samples/aggregates/aggregate-create-post-req.json
:language: javascript
Response
--------
.. rest_parameters:: parameters.yaml
- name: aggregate_name
- links: links
- metadata: aggregate_metadata
- uuid: aggregate_uuid
- created_at: created_at
- updated_at: updated_at
**Example Create Aggregate: JSON response**
.. literalinclude:: samples/aggregates/aggregate-create-post-resp.json
:language: javascript
Update Aggregate
================
.. rest_method:: PATCH /aggregates/{aggregate_uuid}
Updates an aggregate.
Normal response codes: 200
Error response codes: badRequest(400), unauthorized(401), forbidden(403),
conflict(409)
Request
-------
The BODY of the PATCH request must be a JSON PATCH document, adhering to
`RFC 6902 <https://tools.ietf.org/html/rfc6902>`_.
.. rest_parameters:: parameters.yaml
- aggregate_uuid: aggregate_uuid_path
**Example Update Aggregate: JSON request**
.. literalinclude:: samples/aggregates/aggregate-update-put-req.json
:language: javascript
Response
--------
.. rest_parameters:: parameters.yaml
- name: aggregate_name
- links: links
- metadata: aggregate_metadata
- uuid: aggregate_uuid
- created_at: created_at
- updated_at: updated_at
**Example Update Aggregate: JSON response**
.. literalinclude:: samples/aggregates/aggregate-update-put-resp.json
:language: javascript
Show Aggregate Details
======================
.. rest_method:: GET /aggregates/{aggregate_uuid}
Shows details for an aggregate.
Normal response codes: 200
Error response codes: unauthorized(401), forbidden(403), itemNotFound(404)
Request
-------
.. rest_parameters:: parameters.yaml
- aggregate_uuid: aggregate_uuid_path
Response
--------
.. rest_parameters:: parameters.yaml
- name: aggregate_name
- links: links
- metadata: aggregate_metadata
- uuid: aggregate_uuid
- created_at: created_at
- updated_at: updated_at
**Example Show Aggregate Details**
.. literalinclude:: samples/aggregates/aggregate-get-resp.json
:language: javascript
Delete Aggregate
================
.. rest_method:: DELETE /aggregates/{aggregate_uuid}
Deletes an aggregate.
Normal response codes: 204
Error response codes: unauthorized(401), forbidden(403), itemNotFound(404)
Request
-------
.. rest_parameters:: parameters.yaml
- aggregate_uuid: aggregate_uuid_path
Response
--------
No body content is returned on a successful DELETE.

View File

@ -14,3 +14,4 @@ Baremetal Compute API V1 (CURRENT)
.. include:: flavors.inc
.. include:: flavor_access.inc
.. include:: availability_zones.inc
.. include:: aggregates.inc

View File

@ -15,6 +15,12 @@ address_path:
in: path
required: true
type: string
aggregate_uuid_path:
description: |
The UUID of the aggregate.
in: path
required: true
type: string
api_version:
in: path
required: true
@ -124,6 +130,30 @@ address:
in: body
required: true
type: string
aggregate_metadata:
description: |
Metadata key and value pairs associate with the aggregate.
in: body
required: true
type: object
aggregate_name:
description: |
The name of the node aggregate.
in: body
required: true
type: string
aggregate_uuid:
description: |
The UUID of the node aggregate.
in: body
required: true
type: string
aggregates:
description: |
The list of existing aggregates.
in: body
required: true
type: array
availability_zone:
description: |
The availability zone from which to launch the server. When you provision resources,

View File

@ -0,0 +1,6 @@
{
"name": "test_aggregate",
"metadata": {
"k1": "v1"
}
}

View File

@ -0,0 +1,19 @@
{
"name": "test_aggregate",
"uuid": "7de2859d-ec6d-42c7-bb86-9d630ba5ac94",
"links": [
{
"href": "http://10.3.150.17:6688/v1/aggregates/7de2859d-ec6d-42c7-bb86-9d630ba5ac94",
"rel": "self"
},
{
"href": "http://10.3.150.17:6688/aggregates/7de2859d-ec6d-42c7-bb86-9d630ba5ac94",
"rel": "bookmark"
}
],
"metadata": {
"k1": "v1"
},
"created_at": "2016-09-27T02:37:21.966342+00:00",
"updated_at": null
}

View File

@ -0,0 +1,19 @@
{
"name": "test_aggregate",
"uuid": "7de2859d-ec6d-42c7-bb86-9d630ba5ac94",
"links": [
{
"href": "http://10.3.150.17:6688/v1/aggregates/7de2859d-ec6d-42c7-bb86-9d630ba5ac94",
"rel": "self"
},
{
"href": "http://10.3.150.17:6688/aggregates/7de2859d-ec6d-42c7-bb86-9d630ba5ac94",
"rel": "bookmark"
}
],
"metadata": {
"k1": "v1"
},
"created_at": "2016-09-27T02:37:21.966342+00:00",
"updated_at": null
}

View File

@ -0,0 +1,12 @@
[
{
"op": "replace",
"path": "/metadata/k1",
"value": "v2"
},
{
"op": "add",
"path": "/metadata/k2",
"value": "v2"
}
]

View File

@ -0,0 +1,20 @@
{
"name": "test_aggregate",
"uuid": "7de2859d-ec6d-42c7-bb86-9d630ba5ac94",
"links": [
{
"href": "http://10.3.150.17:6688/v1/aggregates/7de2859d-ec6d-42c7-bb86-9d630ba5ac94",
"rel": "self"
},
{
"href": "http://10.3.150.17:6688/aggregates/7de2859d-ec6d-42c7-bb86-9d630ba5ac94",
"rel": "bookmark"
}
],
"metadata": {
"k1": "v2",
"k2": "v2"
},
"created_at": "2016-09-27T02:37:21.966342+00:00",
"updated_at": null
}

View File

@ -0,0 +1,42 @@
{
"aggregates": [
{
"name": "test_aggregate1",
"uuid": "7de2859d-ec6d-42c7-bb86-9d630ba5ac94",
"links": [
{
"href": "http://10.3.150.17:6688/v1/aggregates/7de2859d-ec6d-42c7-bb86-9d630ba5ac94",
"rel": "self"
},
{
"href": "http://10.3.150.17:6688/aggregates/7de2859d-ec6d-42c7-bb86-9d630ba5ac94",
"rel": "bookmark"
}
],
"metadata": {
"k1": "v1"
},
"created_at": "2016-09-27T02:37:21.966342+00:00",
"updated_at": null
},
{
"name": "test_aggregate2",
"uuid": "7de2859d-ec6d-42c7-bb86-9d630ba5ac94",
"links": [
{
"href": "http://10.3.150.17:6688/v1/aggregates/7de2859d-ec6d-42c7-bb86-9d630ba5ac94",
"rel": "self"
},
{
"href": "http://10.3.150.17:6688/aggregates/7de2859d-ec6d-42c7-bb86-9d630ba5ac94",
"rel": "bookmark"
}
],
"metadata": {
"k2": "v2"
},
"created_at": "2016-09-27T02:37:21.966342+00:00",
"updated_at": null
}
]
}

View File

@ -25,6 +25,7 @@ from wsme import types as wtypes
from mogan.api.controllers import base
from mogan.api.controllers import link
from mogan.api.controllers.v1 import aggregates
from mogan.api.controllers.v1 import availability_zone
from mogan.api.controllers.v1 import flavors
from mogan.api.controllers.v1 import keypairs
@ -50,6 +51,9 @@ class V1(base.APIBase):
keypairs = [link.Link]
"""Links to the keypairs resource"""
aggregates = [link.Link]
"""Links to the aggregates resource"""
@staticmethod
def convert():
v1 = V1()
@ -84,6 +88,14 @@ class V1(base.APIBase):
'keypairs', '',
bookmark=True)
]
v1.aggregates = [link.Link.make_link('self',
pecan.request.public_url,
'aggregates', ''),
link.Link.make_link('bookmark',
pecan.request.public_url,
'aggregates', '',
bookmark=True)
]
return v1
@ -94,6 +106,7 @@ class Controller(rest.RestController):
servers = servers.ServerController()
availability_zones = availability_zone.AvailabilityZoneController()
keypairs = keypairs.KeyPairController()
aggregates = aggregates.AggregateController()
@expose.expose(V1)
def get(self):

View File

@ -0,0 +1,180 @@
# Copyright 2017 Huawei Technologies Co.,LTD.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import pecan
from pecan import rest
from six.moves import http_client
import wsme
from wsme import types as wtypes
from mogan.api.controllers import base
from mogan.api.controllers import link
from mogan.api.controllers.v1.schemas import aggregate as agg_schema
from mogan.api.controllers.v1 import types
from mogan.api.controllers.v1 import utils as api_utils
from mogan.api import expose
from mogan.api import validation
from mogan.common import exception
from mogan.common import policy
from mogan import objects
class Aggregate(base.APIBase):
"""API representation of an aggregate.
This class enforces type checking and value constraints, and converts
between the internal object model and the API representation of
an aggregate.
"""
uuid = types.uuid
"""The UUID of the aggregate"""
name = wtypes.text
"""The name of the aggregate"""
metadata = {wtypes.text: types.jsontype}
"""The meta data of the aggregate"""
links = wsme.wsattr([link.Link], readonly=True)
"""A list containing a self link"""
def __init__(self, **kwargs):
self.fields = []
for field in objects.Aggregate.fields:
# Skip fields we do not expose.
if not hasattr(self, field):
continue
self.fields.append(field)
setattr(self, field, kwargs.get(field, wtypes.Unset))
@classmethod
def convert_with_links(cls, db_aggregate):
aggregate = Aggregate(**db_aggregate.as_dict())
url = pecan.request.public_url
aggregate.links = [link.Link.make_link('self', url,
'aggregates',
aggregate.uuid),
link.Link.make_link('bookmark', url,
'aggregates',
aggregate.uuid,
bookmark=True)
]
return aggregate
class AggregatePatchType(types.JsonPatchType):
_api_base = Aggregate
class AggregateCollection(base.APIBase):
"""API representation of a collection of aggregates."""
aggregates = [Aggregate]
"""A list containing Aggregate objects"""
@staticmethod
def convert_with_links(aggregates, url=None, **kwargs):
collection = AggregateCollection()
collection.aggregates = [Aggregate.convert_with_links(aggregate)
for aggregate in aggregates]
return collection
class AggregateController(rest.RestController):
"""REST controller for Aggregates."""
@policy.authorize_wsgi("mogan:aggregate", "get_all")
@expose.expose(AggregateCollection)
def get_all(self):
"""Retrieve a list of aggregates."""
aggregates = objects.AggregateList.get_all(pecan.request.context)
return AggregateCollection.convert_with_links(aggregates)
@policy.authorize_wsgi("mogan:aggregate", "get_one")
@expose.expose(Aggregate, types.uuid)
def get_one(self, aggregate_uuid):
"""Retrieve information about the given aggregate.
:param aggregate_uuid: UUID of an aggregate.
"""
db_aggregate = objects.Aggregate.get(pecan.request.context,
aggregate_uuid)
return Aggregate.convert_with_links(db_aggregate)
@policy.authorize_wsgi("mogan:aggregate", "create")
@expose.expose(Aggregate, body=types.jsontype,
status_code=http_client.CREATED)
def post(self, aggregate):
"""Create an new aggregate.
:param aggregate: an aggregate within the request body.
"""
validation.check_schema(aggregate, agg_schema.create_aggregate)
new_aggregate = objects.Aggregate(pecan.request.context, **aggregate)
new_aggregate.create()
# Set the HTTP Location Header
pecan.response.location = link.build_url('aggregates',
new_aggregate.uuid)
return Aggregate.convert_with_links(new_aggregate)
@policy.authorize_wsgi("mogan:aggregate", "update")
@wsme.validate(types.uuid, [AggregatePatchType])
@expose.expose(Aggregate, types.uuid, body=[AggregatePatchType])
def patch(self, aggregate_uuid, patch):
"""Update an aggregate.
:param aggregate_uuid: the uuid of the aggregate to be updated.
:param aggregate: a json PATCH document to apply to this aggregate.
"""
db_aggregate = objects.Aggregate.get(pecan.request.context,
aggregate_uuid)
try:
aggregate = Aggregate(
**api_utils.apply_jsonpatch(db_aggregate.as_dict(), patch))
except api_utils.JSONPATCH_EXCEPTIONS as e:
raise exception.PatchError(patch=patch, reason=e)
# Update only the fields that have changed
for field in objects.Aggregate.fields:
try:
patch_val = getattr(aggregate, field)
except AttributeError:
# Ignore fields that aren't exposed in the API
continue
if patch_val == wtypes.Unset:
patch_val = None
if db_aggregate[field] != patch_val:
db_aggregate[field] = patch_val
db_aggregate.save()
return Aggregate.convert_with_links(db_aggregate)
@policy.authorize_wsgi("mogan:aggregate", "delete")
@expose.expose(None, types.uuid, status_code=http_client.NO_CONTENT)
def delete(self, aggregate_uuid):
"""Delete an aggregate.
:param aggregate_uuid: UUID of an aggregate.
"""
db_aggregate = objects.Aggregate.get(pecan.request.context,
aggregate_uuid)
db_aggregate.destroy()

View File

@ -0,0 +1,28 @@
# Copyright 2017 Huawei Technologies Co.,LTD.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from mogan.api.validation import parameter_types
create_aggregate = {
"type": "object",
"properties": {
'name': parameter_types.name,
'metadata': parameter_types.metadata,
},
'required': ['name'],
'additionalProperties': False,
}

View File

@ -144,6 +144,21 @@ server_policies = [
policy.RuleDefault('mogan:server:detach_interface',
'rule:default',
description='Detach a network interface'),
policy.RuleDefault('mogan:aggregate:create',
'rule:admin_api',
description='Create aggregate records'),
policy.RuleDefault('mogan:aggregate:update',
'rule:admin_api',
description='Update aggregate records'),
policy.RuleDefault('mogan:aggregate:delete',
'rule:admin_api',
description='Delete aggregate records'),
policy.RuleDefault('mogan:aggregate:get_all',
'rule:admin_api',
description='Retrieve all aggregate records'),
policy.RuleDefault('mogan:aggregate:get_one',
'rule:admin_api',
description='Show aggregate details'),
]

View File

@ -21,7 +21,7 @@ from mogan.objects import base
from mogan.objects import fields as object_fields
def _get_nodes_from_cache(context, aggregate_id):
def _get_nodes_from_cache(aggregate_id):
return []