From 27f3b1832a6bbed8e45ff2a1fd6839f5db44d0cd Mon Sep 17 00:00:00 2001 From: Zhenguo Niu Date: Tue, 18 Jul 2017 17:09:57 +0800 Subject: [PATCH] Add aggregate API This addes the aggregate APIs and docs Partially Implements: bp node-aggregate Change-Id: Iaa8d1de301f09360f7aa36e3a8fe5829e65b55c9 --- api-ref/source/v1/aggregates.inc | 182 ++++++++++++++++++ api-ref/source/v1/index.rst | 1 + api-ref/source/v1/parameters.yaml | 30 +++ .../aggregates/aggregate-create-post-req.json | 6 + .../aggregate-create-post-resp.json | 19 ++ .../aggregates/aggregate-get-resp.json | 19 ++ .../aggregates/aggregate-update-put-req.json | 12 ++ .../aggregates/aggregate-update-put-resp.json | 20 ++ .../aggregates/aggregates-list-resp.json | 42 ++++ mogan/api/controllers/v1/__init__.py | 13 ++ mogan/api/controllers/v1/aggregates.py | 180 +++++++++++++++++ mogan/api/controllers/v1/schemas/aggregate.py | 28 +++ mogan/common/policy.py | 15 ++ mogan/objects/aggregate.py | 2 +- 14 files changed, 568 insertions(+), 1 deletion(-) create mode 100644 api-ref/source/v1/aggregates.inc create mode 100644 api-ref/source/v1/samples/aggregates/aggregate-create-post-req.json create mode 100644 api-ref/source/v1/samples/aggregates/aggregate-create-post-resp.json create mode 100644 api-ref/source/v1/samples/aggregates/aggregate-get-resp.json create mode 100644 api-ref/source/v1/samples/aggregates/aggregate-update-put-req.json create mode 100644 api-ref/source/v1/samples/aggregates/aggregate-update-put-resp.json create mode 100644 api-ref/source/v1/samples/aggregates/aggregates-list-resp.json create mode 100644 mogan/api/controllers/v1/aggregates.py create mode 100644 mogan/api/controllers/v1/schemas/aggregate.py diff --git a/api-ref/source/v1/aggregates.inc b/api-ref/source/v1/aggregates.inc new file mode 100644 index 00000000..bd94f62b --- /dev/null +++ b/api-ref/source/v1/aggregates.inc @@ -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 `_. + +.. 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. diff --git a/api-ref/source/v1/index.rst b/api-ref/source/v1/index.rst index f066128e..119d61d0 100644 --- a/api-ref/source/v1/index.rst +++ b/api-ref/source/v1/index.rst @@ -14,3 +14,4 @@ Baremetal Compute API V1 (CURRENT) .. include:: flavors.inc .. include:: flavor_access.inc .. include:: availability_zones.inc +.. include:: aggregates.inc diff --git a/api-ref/source/v1/parameters.yaml b/api-ref/source/v1/parameters.yaml index d7be3c2a..79f7dd42 100644 --- a/api-ref/source/v1/parameters.yaml +++ b/api-ref/source/v1/parameters.yaml @@ -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, diff --git a/api-ref/source/v1/samples/aggregates/aggregate-create-post-req.json b/api-ref/source/v1/samples/aggregates/aggregate-create-post-req.json new file mode 100644 index 00000000..c86fb7a8 --- /dev/null +++ b/api-ref/source/v1/samples/aggregates/aggregate-create-post-req.json @@ -0,0 +1,6 @@ +{ + "name": "test_aggregate", + "metadata": { + "k1": "v1" + } +} diff --git a/api-ref/source/v1/samples/aggregates/aggregate-create-post-resp.json b/api-ref/source/v1/samples/aggregates/aggregate-create-post-resp.json new file mode 100644 index 00000000..7ca5675a --- /dev/null +++ b/api-ref/source/v1/samples/aggregates/aggregate-create-post-resp.json @@ -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 +} diff --git a/api-ref/source/v1/samples/aggregates/aggregate-get-resp.json b/api-ref/source/v1/samples/aggregates/aggregate-get-resp.json new file mode 100644 index 00000000..7ca5675a --- /dev/null +++ b/api-ref/source/v1/samples/aggregates/aggregate-get-resp.json @@ -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 +} diff --git a/api-ref/source/v1/samples/aggregates/aggregate-update-put-req.json b/api-ref/source/v1/samples/aggregates/aggregate-update-put-req.json new file mode 100644 index 00000000..743b84f4 --- /dev/null +++ b/api-ref/source/v1/samples/aggregates/aggregate-update-put-req.json @@ -0,0 +1,12 @@ +[ + { + "op": "replace", + "path": "/metadata/k1", + "value": "v2" + }, + { + "op": "add", + "path": "/metadata/k2", + "value": "v2" + } +] diff --git a/api-ref/source/v1/samples/aggregates/aggregate-update-put-resp.json b/api-ref/source/v1/samples/aggregates/aggregate-update-put-resp.json new file mode 100644 index 00000000..763e5f2b --- /dev/null +++ b/api-ref/source/v1/samples/aggregates/aggregate-update-put-resp.json @@ -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 +} diff --git a/api-ref/source/v1/samples/aggregates/aggregates-list-resp.json b/api-ref/source/v1/samples/aggregates/aggregates-list-resp.json new file mode 100644 index 00000000..c50bd5bf --- /dev/null +++ b/api-ref/source/v1/samples/aggregates/aggregates-list-resp.json @@ -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 + } + ] +} diff --git a/mogan/api/controllers/v1/__init__.py b/mogan/api/controllers/v1/__init__.py index 11b4068b..893028ef 100644 --- a/mogan/api/controllers/v1/__init__.py +++ b/mogan/api/controllers/v1/__init__.py @@ -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): diff --git a/mogan/api/controllers/v1/aggregates.py b/mogan/api/controllers/v1/aggregates.py new file mode 100644 index 00000000..57f0af49 --- /dev/null +++ b/mogan/api/controllers/v1/aggregates.py @@ -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() diff --git a/mogan/api/controllers/v1/schemas/aggregate.py b/mogan/api/controllers/v1/schemas/aggregate.py new file mode 100644 index 00000000..cb74788d --- /dev/null +++ b/mogan/api/controllers/v1/schemas/aggregate.py @@ -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, +} diff --git a/mogan/common/policy.py b/mogan/common/policy.py index 688436cb..67e8678e 100644 --- a/mogan/common/policy.py +++ b/mogan/common/policy.py @@ -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'), ] diff --git a/mogan/objects/aggregate.py b/mogan/objects/aggregate.py index 67f0fe34..2166a40f 100644 --- a/mogan/objects/aggregate.py +++ b/mogan/objects/aggregate.py @@ -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 []