Browse Source
this commit introduces a new '/federations' endpoint to Magnum API, as well as its controllers, entities and conductor handlers. this corresponds to the first phase of the federation-api spec. please refer to [1] for more details. [1] https://review.openstack.org/#/c/489609/ Change-Id: I662ac2d6ddec07b50712109541486fd26c5d21de Partially-Implements: blueprint federation-apichanges/93/499193/13
23 changed files with 1593 additions and 2 deletions
@ -0,0 +1,454 @@
|
||||
# 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 uuid |
||||
|
||||
from oslo_log import log as logging |
||||
import pecan |
||||
import wsme |
||||
from wsme import types as wtypes |
||||
|
||||
from magnum.api import attr_validator |
||||
from magnum.api.controllers import base |
||||
from magnum.api.controllers import link |
||||
from magnum.api.controllers.v1 import collection |
||||
from magnum.api.controllers.v1 import types |
||||
from magnum.api import expose |
||||
from magnum.api import utils as api_utils |
||||
from magnum.api import validation |
||||
from magnum.common import exception |
||||
from magnum.common import name_generator |
||||
from magnum.common import policy |
||||
import magnum.conf |
||||
from magnum import objects |
||||
from magnum.objects import fields |
||||
|
||||
LOG = logging.getLogger(__name__) |
||||
CONF = magnum.conf.CONF |
||||
|
||||
|
||||
class FederationID(wtypes.Base): |
||||
"""API representation of a federation ID |
||||
|
||||
This class enforces type checking and value constraints, and converts |
||||
between the internal object model and the API representation of a |
||||
federation ID. |
||||
""" |
||||
uuid = types.uuid |
||||
|
||||
def __init__(self, uuid): |
||||
self.uuid = uuid |
||||
|
||||
|
||||
class Federation(base.APIBase): |
||||
"""API representation of a federation. |
||||
|
||||
This class enforces type checking and value constraints, and converts |
||||
between the internal object model and the API representation of a |
||||
Federation. |
||||
""" |
||||
|
||||
# Unique UUID for this federation. |
||||
uuid = types.uuid |
||||
|
||||
# Name of this federation, max length is limited to 242 because heat stack |
||||
# requires max length limit to 255, and Magnum amend a uuid length. |
||||
name = wtypes.StringType(min_length=1, max_length=242, |
||||
pattern='^[a-zA-Z][a-zA-Z0-9_.-]*$') |
||||
|
||||
# UUID of the hostcluster of the federation, i.e. the cluster that |
||||
# hosts the COE Federated API. |
||||
hostcluster_id = wsme.wsattr(wtypes.text) |
||||
|
||||
# List of UUIDs of all the member clusters of the federation. |
||||
member_ids = wsme.wsattr([wtypes.text]) |
||||
|
||||
# Status of the federation. |
||||
status = wtypes.Enum(str, *fields.FederationStatus.ALL) |
||||
|
||||
# Status reason of the federation. |
||||
status_reason = wtypes.text |
||||
|
||||
# Set of federation metadata (COE-specific in some cases). |
||||
properties = wtypes.DictType(str, str) |
||||
|
||||
# A list containing a self link and associated federations links |
||||
links = wsme.wsattr([link.Link], readonly=True) |
||||
|
||||
def __init__(self, **kwargs): |
||||
super(Federation, self).__init__() |
||||
self.fields = [] |
||||
for field in objects.Federation.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)) |
||||
|
||||
@staticmethod |
||||
def _convert_with_links(federation, url, expand=True): |
||||
if not expand: |
||||
federation.unset_fields_except(['uuid', 'name', 'hostcluster_id', |
||||
'member_ids', 'status', |
||||
'properties']) |
||||
|
||||
federation.links = [link.Link.make_link('self', url, 'federations', |
||||
federation.uuid), |
||||
link.Link.make_link('bookmark', url, 'federations', |
||||
federation.uuid, |
||||
bookmark=True)] |
||||
return federation |
||||
|
||||
@classmethod |
||||
def convert_with_links(cls, rpc_federation, expand=True): |
||||
federation = Federation(**rpc_federation.as_dict()) |
||||
return cls._convert_with_links(federation, pecan.request.host_url, |
||||
expand) |
||||
|
||||
@classmethod |
||||
def sample(cls, expand=True): |
||||
sample = cls(uuid='4221a353-8368-475f-b7de-3429d3f724b3', |
||||
name='example', |
||||
hostcluster_id='49dc23f5-ffc9-40c3-9d34-7be7f9e34d63', |
||||
member_ids=['49dc23f5-ffc9-40c3-9d34-7be7f9e34d63', |
||||
'f2439bcf-02a2-4278-9d8a-f07a2042230a', |
||||
'e549e0a5-3d3c-406f-bd7c-0e0182fb211c'], |
||||
properties={'dns-zone': 'example.com.'}, |
||||
status=fields.FederationStatus.CREATE_COMPLETE, |
||||
status_reason="CREATE completed successfully") |
||||
return cls._convert_with_links(sample, 'http://localhost:9511', expand) |
||||
|
||||
|
||||
class FederationPatchType(types.JsonPatchType): |
||||
_api_base = Federation |
||||
|
||||
@staticmethod |
||||
def internal_attrs(): |
||||
""""Returns a list of internal attributes. |
||||
|
||||
Internal attributes can't be added, replaced or removed. |
||||
""" |
||||
internal_attrs = [] |
||||
return types.JsonPatchType.internal_attrs() + internal_attrs |
||||
|
||||
|
||||
class FederationCollection(collection.Collection): |
||||
"""API representation of a collection of federations.""" |
||||
|
||||
# A list containing federation objects. |
||||
federations = [Federation] |
||||
|
||||
def __init__(self, **kwargs): |
||||
self._type = 'federations' |
||||
|
||||
@staticmethod |
||||
def convert_with_links(rpc_federation, limit, url=None, expand=False, |
||||
**kwargs): |
||||
collection = FederationCollection() |
||||
collection.federations = [Federation.convert_with_links(p, expand) |
||||
for p in rpc_federation] |
||||
collection.next = collection.get_next(limit, url=url, **kwargs) |
||||
return collection |
||||
|
||||
@classmethod |
||||
def sample(cls): |
||||
sample = cls() |
||||
sample.federations = [Federation.sample(expand=False)] |
||||
return sample |
||||
|
||||
|
||||
class FederationsController(base.Controller): |
||||
"""REST controller for federations.""" |
||||
|
||||
def __init__(self): |
||||
super(FederationsController, self).__init__() |
||||
|
||||
_custom_actions = { |
||||
'detail': ['GET'], |
||||
} |
||||
|
||||
def _generate_name_for_federation(self, context): |
||||
"""Generate a random name like: phi-17-federation.""" |
||||
name_gen = name_generator.NameGenerator() |
||||
name = name_gen.generate() |
||||
return name + '-federation' |
||||
|
||||
def _get_federation_collection(self, marker, limit, |
||||
sort_key, sort_dir, expand=False, |
||||
resource_url=None): |
||||
limit = api_utils.validate_limit(limit) |
||||
sort_dir = api_utils.validate_sort_dir(sort_dir) |
||||
|
||||
marker_obj = None |
||||
if marker: |
||||
marker_obj = objects.Federation.get_by_uuid(pecan.request.context, |
||||
marker) |
||||
|
||||
federations = objects.Federation.list(pecan.request.context, limit, |
||||
marker_obj, sort_key=sort_key, |
||||
sort_dir=sort_dir) |
||||
|
||||
return FederationCollection.convert_with_links(federations, limit, |
||||
url=resource_url, |
||||
expand=expand, |
||||
sort_key=sort_key, |
||||
sort_dir=sort_dir) |
||||
|
||||
@expose.expose(FederationCollection, types.uuid, int, wtypes.text, |
||||
wtypes.text) |
||||
def get_all(self, marker=None, limit=None, sort_key='id', |
||||
sort_dir='asc'): |
||||
"""Retrieve a list of federations. |
||||
|
||||
:param marker: pagination marker for large data sets. |
||||
:param limit: maximum number of resources to return in a single result. |
||||
:param sort_key: column to sort results by. Default: id. |
||||
:param sort_dir: direction to sort. "asc" or "desc". Default: asc. |
||||
""" |
||||
context = pecan.request.context |
||||
policy.enforce(context, 'federation:get_all', |
||||
action='federation:get_all') |
||||
return self._get_federation_collection(marker, limit, sort_key, |
||||
sort_dir) |
||||
|
||||
@expose.expose(FederationCollection, types.uuid, int, wtypes.text, |
||||
wtypes.text) |
||||
def detail(self, marker=None, limit=None, sort_key='id', |
||||
sort_dir='asc'): |
||||
"""Retrieve a list of federation with detail. |
||||
|
||||
:param marker: pagination marker for large data sets. |
||||
:param limit: maximum number of resources to return in a single result. |
||||
:param sort_key: column to sort results by. Default: id. |
||||
:param sort_dir: direction to sort. "asc" or "desc". Default: asc. |
||||
""" |
||||
context = pecan.request.context |
||||
policy.enforce(context, 'federation:detail', |
||||
action='federation:detail') |
||||
|
||||
# NOTE(lucasagomes): /detail should only work against collections |
||||
parent = pecan.request.path.split('/')[:-1][-1] |
||||
if parent != "federations": |
||||
raise exception.HTTPNotFound |
||||
|
||||
expand = True |
||||
resource_url = '/'.join(['federations', 'detail']) |
||||
return self._get_federation_collection(marker, limit, |
||||
sort_key, sort_dir, expand, |
||||
resource_url) |
||||
|
||||
@expose.expose(Federation, types.uuid_or_name) |
||||
def get_one(self, federation_ident): |
||||
"""Retrieve information about a given Federation. |
||||
|
||||
:param federation_ident: UUID or logical name of the Federation. |
||||
""" |
||||
context = pecan.request.context |
||||
federation = api_utils.get_resource('Federation', federation_ident) |
||||
policy.enforce(context, 'federation:get', federation.as_dict(), |
||||
action='federation:get') |
||||
|
||||
federation = Federation.convert_with_links(federation) |
||||
|
||||
return federation |
||||
|
||||
@expose.expose(FederationID, body=Federation, status_code=202) |
||||
def post(self, federation): |
||||
"""Create a new federation. |
||||
|
||||
:param federation: a federation within the request body. |
||||
""" |
||||
context = pecan.request.context |
||||
policy.enforce(context, 'federation:create', |
||||
action='federation:create') |
||||
|
||||
federation_dict = federation.as_dict() |
||||
|
||||
# Validate `hostcluster_id` |
||||
hostcluster_id = federation_dict.get('hostcluster_id') |
||||
attr_validator.validate_federation_hostcluster(hostcluster_id) |
||||
|
||||
# Validate `properties` dict. |
||||
properties_dict = federation_dict.get('properties') |
||||
attr_validator.validate_federation_properties(properties_dict) |
||||
|
||||
federation_dict['project_id'] = context.project_id |
||||
|
||||
# If no name is specified, generate a random human-readable name |
||||
name = (federation_dict.get('name') or |
||||
self._generate_name_for_federation(context)) |
||||
federation_dict['name'] = name |
||||
|
||||
new_federation = objects.Federation(context, **federation_dict) |
||||
new_federation.uuid = uuid.uuid4() |
||||
|
||||
# TODO(clenimar): remove hard-coded `create_timeout`. |
||||
pecan.request.rpcapi.federation_create_async(new_federation, |
||||
create_timeout=15) |
||||
|
||||
return FederationID(new_federation.uuid) |
||||
|
||||
@expose.expose(FederationID, types.uuid_or_name, types.boolean, |
||||
body=[FederationPatchType], status_code=202) |
||||
def patch(self, federation_ident, rollback=False, patch=None): |
||||
"""Update an existing Federation. |
||||
|
||||
Please note that the join/unjoin operation is performed by patching |
||||
`member_ids`. |
||||
|
||||
:param federation_ident: UUID or logical name of a federation. |
||||
:param rollback: whether to rollback federation on update failure. |
||||
:param patch: a json PATCH document to apply to this federation. |
||||
""" |
||||
federation = self._patch(federation_ident, patch) |
||||
pecan.request.rpcapi.federation_update_async(federation, rollback) |
||||
return FederationID(federation.uuid) |
||||
|
||||
def _patch(self, federation_ident, patch): |
||||
context = pecan.request.context |
||||
federation = api_utils.get_resource('Federation', federation_ident) |
||||
policy.enforce(context, 'federation:update', federation.as_dict(), |
||||
action='federation:update') |
||||
|
||||
# NOTE(clenimar): Magnum does not allow one to append items to existing |
||||
# fields through an `add` operation using HTTP PATCH (please check |
||||
# `magnum.api.utils.apply_jsonpatch`). In order to perform the join |
||||
# and unjoin operations, intercept the original JSON PATCH document |
||||
# and change the operation from either `add` or `remove` to `replace`. |
||||
patch_path = patch[0].get('path') |
||||
patch_value = patch[0].get('value') |
||||
patch_op = patch[0].get('op') |
||||
|
||||
if patch_path == '/member_ids': |
||||
if patch_op == 'add' and patch_value is not None: |
||||
patch = self._join_wrapper(federation_ident, patch) |
||||
elif patch_op == 'remove' and patch_value is not None: |
||||
patch = self._unjoin_wrapper(federation_ident, patch) |
||||
|
||||
try: |
||||
federation_dict = federation.as_dict() |
||||
new_federation = Federation( |
||||
**api_utils.apply_jsonpatch(federation_dict, patch)) |
||||
except api_utils.JSONPATCH_EXCEPTIONS as e: |
||||
raise exception.PatchError(patch=patch, reason=e) |
||||
|
||||
# Retrieve only what changed after the patch. |
||||
delta = self._update_changed_fields(federation, new_federation) |
||||
validation.validate_federation_properties(delta) |
||||
|
||||
return federation |
||||
|
||||
def _update_changed_fields(self, federation, new_federation): |
||||
"""Update only the patches that were modified and return the diff.""" |
||||
for field in objects.Federation.fields: |
||||
try: |
||||
patch_val = getattr(new_federation, field) |
||||
except AttributeError: |
||||
# Ignore fields that aren't exposed in the API |
||||
continue |
||||
if patch_val == wtypes.Unset: |
||||
patch_val = None |
||||
if federation[field] != patch_val: |
||||
federation[field] = patch_val |
||||
|
||||
return federation.obj_what_changed() |
||||
|
||||
def _join_wrapper(self, federation_ident, patch): |
||||
"""Intercept PATCH JSON documents for join operations. |
||||
|
||||
Take a PATCH JSON document with `add` operation:: |
||||
{ |
||||
'op': 'add', |
||||
'value': 'new_member_id', |
||||
'path': '/member_ids' |
||||
} |
||||
and transform it into a document with `replace` operation:: |
||||
{ |
||||
'op': 'replace', |
||||
'value': ['current_member_id1', ..., 'new_member_id'], |
||||
'path': '/member_ids' |
||||
} |
||||
""" |
||||
federation = api_utils.get_resource('Federation', federation_ident) |
||||
new_member_uuid = patch[0]['value'] |
||||
|
||||
# Check if the cluster exists |
||||
c = objects.Cluster.get_by_uuid(pecan.request.context, new_member_uuid) |
||||
|
||||
# Check if the cluster is already a member of the federation |
||||
if new_member_uuid not in federation.member_ids and c is not None: |
||||
# Retrieve all current members |
||||
members = federation.member_ids |
||||
# Add the new member |
||||
members.append(c.uuid) |
||||
else: |
||||
kw = {'uuid': new_member_uuid, 'federation_name': federation.name} |
||||
raise exception.MemberAlreadyExists(**kw) |
||||
|
||||
# Set `value` to the updated member list. Change `op` to `replace` |
||||
patch[0]['value'] = members |
||||
patch[0]['op'] = 'replace' |
||||
|
||||
return patch |
||||
|
||||
def _unjoin_wrapper(self, federation_ident, patch): |
||||
"""Intercept PATCH JSON documents for unjoin operations. |
||||
|
||||
Take a PATCH JSON document with `remove` operation:: |
||||
{ |
||||
'op': 'remove', |
||||
'value': 'former_member_id', |
||||
'path': '/member_ids' |
||||
} |
||||
and transform it into a document with `replace` operation:: |
||||
{ |
||||
'op': 'replace', |
||||
'value': ['current_member_id1', ..., 'current_member_idn'], |
||||
'path': '/member_ids' |
||||
} |
||||
""" |
||||
federation = api_utils.get_resource('Federation', federation_ident) |
||||
cluster_uuid = patch[0]['value'] |
||||
|
||||
# Check if the cluster exists |
||||
c = objects.Cluster.get_by_uuid(pecan.request.context, cluster_uuid) |
||||
|
||||
# Check if the cluster is a member cluster and if it exists |
||||
if cluster_uuid in federation.member_ids and c is not None: |
||||
# Retrieve all current members |
||||
members = federation.member_ids |
||||
# Unjoin the member |
||||
members.remove(cluster_uuid) |
||||
else: |
||||
raise exception.HTTPNotFound("Cluster %s is not a member of the " |
||||
"federation %s." % (cluster_uuid, |
||||
federation.name)) |
||||
|
||||
# Set `value` to the updated member list. Change `op` to `replace` |
||||
patch[0]['value'] = members |
||||
patch[0]['op'] = 'replace' |
||||
|
||||
return patch |
||||
|
||||
@expose.expose(None, types.uuid_or_name, status_code=204) |
||||
def delete(self, federation_ident): |
||||
"""Delete a federation. |
||||
|
||||
:param federation_ident: UUID of federation or logical name |
||||
of the federation. |
||||
""" |
||||
context = pecan.request.context |
||||
federation = api_utils.get_resource('Federation', federation_ident) |
||||
policy.enforce(context, 'federation:delete', federation.as_dict(), |
||||
action='federation:delete') |
||||
|
||||
pecan.request.rpcapi.federation_delete_async(federation.uuid) |
@ -0,0 +1,91 @@
|
||||
# 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 oslo_policy import policy |
||||
|
||||
from magnum.common.policies import base |
||||
|
||||
FEDERATION = 'federation:%s' |
||||
|
||||
rules = [ |
||||
policy.DocumentedRuleDefault( |
||||
name=FEDERATION % 'create', |
||||
check_str=base.RULE_DENY_CLUSTER_USER, |
||||
description='Create a new federation.', |
||||
operations=[ |
||||
{ |
||||
'path': '/v1/federations', |
||||
'method': 'POST' |
||||
} |
||||
] |
||||
), |
||||
policy.DocumentedRuleDefault( |
||||
name=FEDERATION % 'delete', |
||||
check_str=base.RULE_DENY_CLUSTER_USER, |
||||
description='Delete a federation.', |
||||
operations=[ |
||||
{ |
||||
'path': '/v1/federations/{federation_ident}', |
||||
'method': 'DELETE' |
||||
} |
||||
] |
||||
), |
||||
policy.DocumentedRuleDefault( |
||||
name=FEDERATION % 'detail', |
||||
check_str=base.RULE_DENY_CLUSTER_USER, |
||||
description='Retrieve a list of federations with detail.', |
||||
operations=[ |
||||
{ |
||||
'path': '/v1/federations', |
||||
'method': 'GET' |
||||
} |
||||
] |
||||
), |
||||
policy.DocumentedRuleDefault( |
||||
name=FEDERATION % 'get', |
||||
check_str=base.RULE_DENY_CLUSTER_USER, |
||||
description='Retrieve information about the given federation.', |
||||
operations=[ |
||||
{ |
||||
'path': '/v1/federations/{federation_ident}', |
||||
'method': 'GET' |
||||
} |
||||
] |
||||
), |
||||
policy.DocumentedRuleDefault( |
||||
name=FEDERATION % 'get_all', |
||||
check_str=base.RULE_DENY_CLUSTER_USER, |
||||
description='Retrieve a list of federations.', |
||||
operations=[ |
||||
{ |
||||
'path': '/v1/federations/', |
||||
'method': 'GET' |
||||
} |
||||
] |
||||
), |
||||
policy.DocumentedRuleDefault( |
||||
name=FEDERATION % 'update', |
||||
check_str=base.RULE_DENY_CLUSTER_USER, |
||||
description='Update an existing federation.', |
||||
operations=[ |
||||
{ |
||||
'path': '/v1/federations/{federation_ident}', |
||||
'method': 'PATCH' |
||||
} |
||||
] |
||||
) |
||||
] |
||||
|
||||
|
||||
def list_rules(): |
||||
return rules |
@ -0,0 +1,32 @@
|
||||
# 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 magnum.common import profiler |
||||
import magnum.conf |
||||
|
||||
CONF = magnum.conf.CONF |
||||
|
||||
|
||||
@profiler.trace_cls("rpc") |
||||
class Handler(object): |
||||
|
||||
def __init__(self): |
||||
super(Handler, self).__init__() |
||||
|
||||
def federation_create(self, context, federation, create_timeout): |
||||
raise NotImplementedError("This feature is not yet implemented.") |
||||
|
||||
def federation_update(self, context, federation, rollback=False): |
||||
raise NotImplementedError("This feature is not yet implemented.") |
||||
|
||||
def federation_delete(self, context, uuid): |
||||
raise NotImplementedError("This feature is not yet implemented.") |
@ -0,0 +1,215 @@
|
||||
# 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 oslo_utils import strutils |
||||
from oslo_utils import uuidutils |
||||
from oslo_versionedobjects import fields |
||||
|
||||
from magnum.common import exception |
||||
from magnum.db import api as dbapi |
||||
from magnum.objects import base |
||||
from magnum.objects import fields as m_fields |
||||
|
||||
|
||||
@base.MagnumObjectRegistry.register |
||||
class Federation(base.MagnumPersistentObject, base.MagnumObject, |
||||
base.MagnumObjectDictCompat): |
||||
"""Represents a Federation object. |
||||
|
||||
Version 1.0: Initial Version |
||||
""" |
||||
|
||||
VERSION = '1.0' |
||||
|
||||
dbapi = dbapi.get_instance() |
||||
|
||||
fields = { |
||||
'id': fields.IntegerField(), |
||||
'uuid': fields.UUIDField(nullable=True), |
||||
'name': fields.StringField(nullable=True), |
||||
'project_id': fields.StringField(nullable=True), |
||||
'hostcluster_id': fields.StringField(nullable=True), |
||||
'member_ids': fields.ListOfStringsField(nullable=True), |
||||
'status': m_fields.FederationStatusField(nullable=True), |
||||
'status_reason': fields.StringField(nullable=True), |
||||
'properties': fields.DictOfStringsField(nullable=True) |
||||
} |
||||
|
||||
@staticmethod |
||||
def _from_db_object(federation, db_federation): |
||||
"""Converts a database entity to a formal object.""" |
||||
for field in federation.fields: |
||||
federation[field] = db_federation[field] |
||||
|
||||
federation.obj_reset_changes() |
||||
return federation |
||||
|
||||
@staticmethod |
||||
def _from_db_object_list(db_objects, cls, context): |
||||
"""Converts a list of database entities to a list of formal objects.""" |
||||
return [Federation._from_db_object(cls(context), obj) |
||||
for obj in db_objects] |
||||
|
||||
@base.remotable_classmethod |
||||
def get(cls, context, federation_id): |
||||
"""Find a federation based on its id or uuid and return it. |
||||
|
||||
:param federation_id: the id *or* uuid of a federation. |
||||
:param context: Security context |
||||
:returns: a :class:`Federation` object. |
||||
""" |
||||
if strutils.is_int_like(federation_id): |
||||
return cls.get_by_id(context, federation_id) |
||||
elif uuidutils.is_uuid_like(federation_id): |
||||
return cls.get_by_uuid(context, federation_id) |
||||
else: |
||||
raise exception.InvalidIdentity(identity=federation_id) |
||||
|
||||
@base.remotable_classmethod |
||||
def get_by_id(cls, context, federation_id): |
||||
"""Find a federation based on its integer id and return it. |
||||
|
||||
:param federation_id: the id of a federation. |
||||
:param context: Security context |
||||
:returns: a :class:`Federation` object. |
||||
""" |
||||
db_federation = cls.dbapi.get_federation_by_id(context, federation_id) |
||||
federation = Federation._from_db_object(cls(context), db_federation) |
||||
return federation |
||||
|
||||
@base.remotable_classmethod |
||||
def get_by_uuid(cls, context, uuid): |
||||
"""Find a federation based on uuid and return it. |
||||
|
||||
:param uuid: the uuid of a federation. |
||||
:param context: Security context |
||||
:returns: a :class:`Federation` object. |
||||
""" |
||||
db_federation = cls.dbapi.get_federation_by_uuid(context, uuid) |
||||
federation = Federation._from_db_object(cls(context), db_federation) |
||||
return federation |
||||
|
||||
@base.remotable_classmethod |
||||
def get_count_all(cls, context, filters=None): |
||||
"""Get count of matching federation. |
||||
|
||||
:param context: The security context |
||||
:param filters: filter dict, can includes 'name', 'project_id', |
||||
'hostcluster_id', 'member_ids', 'status' (should be a |
||||
status list). |
||||
:returns: Count of matching federation. |
||||
""" |
||||
return cls.dbapi.get_federation_count_all(context, filters=filters) |
||||
|
||||
@base.remotable_classmethod |
||||
def get_by_name(cls, context, name): |
||||
"""Find a federation based on name and return a Federation object. |
||||
|
||||
:param name: the logical name of a federation. |
||||
:param context: Security context |
||||
:returns: a :class:`Federation` object. |
||||
""" |
||||
db_federation = cls.dbapi.get_federation_by_name(context, name) |
||||
federation = Federation._from_db_object(cls(context), db_federation) |
||||
return federation |
||||
|
||||
@base.remotable_classmethod |
||||
def list(cls, context, limit=None, marker=None, |
||||
sort_key=None, sort_dir=None, filters=None): |
||||
"""Return a list of Federation objects. |
||||
|
||||
:param context: Security context. |
||||
:param limit: maximum number of resources to return in a single result. |
||||
:param marker: pagination marker for large data sets. |
||||
:param sort_key: column to sort results by. |
||||
:param sort_dir: direction to sort. "asc" or "desc". |
||||
:param filters: filter dict, can includes 'name', 'project_id', |
||||
'hostcluster_id', 'member_ids', 'status' (should be a |
||||
status list). |
||||
:returns: a list of :class:`Federation` object. |
||||
|
||||
""" |
||||
db_federation = cls.dbapi.get_federation_list(context, limit=limit, |
||||
marker=marker, |
||||
sort_key=sort_key, |
||||
sort_dir=sort_dir, |
||||
filters=filters) |
||||
return Federation._from_db_object_list(db_federation, cls, context) |
||||
|
||||
@base.remotable |
||||
def create(self, context=None): |
||||
"""Create a Federation record in the DB. |
||||
|
||||
:param context: Security context. NOTE: This should only |
||||
be used internally by the indirection_api. |
||||
Unfortunately, RPC requires context as the first |
||||
argument, even though we don't use it. |
||||
A context should be set when instantiating the |
||||
object, e.g.: Federation(context) |
||||
|
||||
""" |
||||
values = self.obj_get_changes() |
||||
db_federation = self.dbapi.create_federation(values) |
||||
self._from_db_object(self, db_federation) |
||||
|
||||
@base.remotable |
||||
def destroy(self, context=None): |
||||
"""Delete the Federation from the DB. |
||||
|
||||
:param context: Security context. NOTE: This should only |
||||
be used internally by the indirection_api. |
||||
Unfortunately, RPC requires context as the first |
||||
argument, even though we don't use it. |
||||
A context should be set when instantiating the |
||||
object, e.g.: Federation(context) |
||||
""" |
||||
self.dbapi.destroy_federation(self.uuid) |
||||
self.obj_reset_changes() |
||||
|
||||
@base.remotable |
||||
def save(self, context=None): |
||||
"""Save updates to this Federation. |
||||
|
||||
Updates will be made column by column based on the result |
||||
of self.what_changed(). |
||||
|
||||
:param context: Security context. NOTE: This should only |
||||
be used internally by the indirection_api. |
||||
Unfortunately, RPC requires context as the first |
||||
argument, even though we don't use it. |
||||
A context should be set when instantiating the |
||||
object, e.g.: Federation(context) |
||||
""" |
||||
updates = self.obj_get_changes() |
||||
self.dbapi.update_federation(self.uuid, updates) |
||||
|
||||
self.obj_reset_changes() |
||||
|
||||
@base.remotable |
||||
def refresh(self, context=None): |
||||
"""Load updates for this Federation. |
||||
|
||||
Loads a Federation with the same uuid from the database and |
||||
checks for updated attributes. Updates are applied from |
||||
the loaded Federation column by column, if there are any updates. |
||||
|
||||
:param context: Security context. NOTE: This should only |
||||
be used internally by the indirection_api. |
||||
Unfortunately, RPC requires context as the first |
||||
argument, even though we don't use it. |
||||
A context should be set when instantiating the |
||||
object, e.g.: Federation(context) |
||||
""" |
||||
current = self.__class__.get_by_uuid(self._context, uuid=self.uuid) |
||||
for field in self.fields: |
||||
if self.obj_attr_is_set(field) and self[field] != current[field]: |
||||
self[field] = current[field] |
@ -0,0 +1,415 @@
|
||||
# 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 datetime |
||||
import mock |
||||
|
||||
from oslo_config import cfg |
||||
from oslo_utils import uuidutils |
||||
|
||||
from magnum.api.controllers.v1 import federation as api_federation |
||||
from magnum.conductor import api as rpcapi |
||||
import magnum.conf |
||||
from magnum import objects |
||||
from magnum.tests import base |
||||
from magnum.tests.unit.api import base as api_base |
||||
from magnum.tests.unit.api import utils as apiutils |
||||
from magnum.tests.unit.objects import utils as obj_utils |
||||
|
||||
CONF = magnum.conf.CONF |
||||
|
||||
|
||||
class TestFederationObject(base.TestCase): |
||||
def test_federation_init(self): |
||||
fed_dict = apiutils.federation_post_data() |
||||
fed_dict['uuid'] = uuidutils.generate_uuid() |
||||
federation = api_federation.Federation(**fed_dict) |
||||
self.assertEqual(fed_dict['uuid'], federation.uuid) |
||||
|
||||
|
||||
class TestListFederation(api_base.FunctionalTest): |
||||
def setUp(self): |
||||
super(TestListFederation, self).setUp() |
||||
|
||||
def test_empty(self): |
||||
response = self.get_json('/federations') |
||||
self.assertEqual(response['federations'], []) |
||||
|
||||
def test_one(self): |
||||
federation = obj_utils.create_test_federation( |
||||
self.context, uuid=uuidutils.generate_uuid()) |
||||
response = self.get_json('/federations') |
||||
self.assertEqual(federation.uuid, response['federations'][0]['uuid']) |
||||
|
||||
def test_get_one(self): |
||||
federation = obj_utils.create_test_federation( |
||||
self.context, uuid=uuidutils.generate_uuid()) |
||||
response = self.get_json('/federations/%s' % federation['uuid']) |
||||
self.assertTrue(response['uuid'], federation.uuid) |
||||
|
||||
def test_get_one_by_name(self): |
||||
federation = obj_utils.create_test_federation( |
||||
self.context, uuid=uuidutils.generate_uuid()) |
||||
response = self.get_json('/federations/%s' % federation['name']) |
||||
self.assertTrue(response['uuid'], federation.uuid) |
||||
|
||||
def test_get_one_by_name_not_found(self): |
||||
response = self.get_json('/federations/not_found', expect_errors=True) |
||||
self.assertEqual(404, response.status_int) |
||||
self.assertEqual('application/json', response.content_type) |
||||
self.assertTrue(response.json['errors']) |
||||
|
||||
def test_get_one_by_uuid(self): |
||||
temp_uuid = uuidutils.generate_uuid() |
||||
federation = obj_utils.create_test_federation(self.context, |
||||
uuid=temp_uuid) |
||||
response = self.get_json('/federations/%s' % temp_uuid) |
||||
self.assertTrue(response['uuid'], federation.uuid) |
||||
|
||||
def test_get_one_by_uuid_not_found(self): |
||||
temp_uuid = uuidutils.generate_uuid() |
||||
response = self.get_json('/federations/%s' % temp_uuid, |
||||
expect_errors=True) |
||||
self.assertEqual(404, response.status_int) |
||||
self.assertEqual('application/json', response.content_type) |
||||
self.assertTrue(response.json['errors']) |
||||
|
||||
def test_get_one_by_name_multiple_federation(self): |
||||
obj_utils.create_test_federation(self.context, name='test_federation', |
||||
uuid=uuidutils.generate_uuid()) |
||||
obj_utils.create_test_federation(self.context, name='test_federation', |
||||
uuid=uuidutils.generate_uuid()) |
||||
response = self.get_json('/federations/test_federation', |
||||
expect_errors=True) |
||||
self.assertEqual(409, response.status_int) |
||||
self.assertEqual('application/json', response.content_type) |
||||
self.assertTrue(response.json['errors']) |
||||
|
||||
def test_get_all_with_pagination_marker(self): |
||||
federation_list = [] |
||||
for id_ in range(4): |
||||
federation = obj_utils.create_test_federation( |
||||
self.context, id=id_, uuid=uuidutils.generate_uuid()) |
||||
federation_list.append(federation) |
||||
|
||||
response = self.get_json( |
||||
'/federations?limit=3&marker=%s' % federation_list[2].uuid) |
||||
self.assertEqual(1, len(response['federations'])) |
||||
self.assertEqual(federation_list[-1].uuid, |
||||
response['federations'][0]['uuid']) |
||||
|
||||
def test_detail(self): |
||||
federation = obj_utils.create_test_federation( |
||||
self.context, uuid=uuidutils.generate_uuid()) |
||||
response = self.get_json('/federations/detail') |
||||
self.assertEqual(federation.uuid, response['federations'][0]["uuid"]) |
||||
|
||||
def test_detail_with_pagination_marker(self): |
||||
federation_list = [] |
||||
for id_ in range(4): |
||||
federation = obj_utils.create_test_federation( |
||||
self.context, id=id_, uuid=uuidutils.generate_uuid()) |
||||
federation_list.append(federation) |
||||
|
||||
response = self.get_json( |
||||
'/federations/detail?limit=3&marker=%s' % federation_list[2].uuid) |
||||
self.assertEqual(1, len(response['federations'])) |
||||
self.assertEqual(federation_list[-1].uuid, |
||||
response['federations'][0]['uuid']) |
||||
|
||||
def test_detail_against_single(self): |
||||
federation = obj_utils.create_test_federation( |
||||
self.context, uuid=uuidutils.generate_uuid()) |
||||
response = self.get_json( |
||||
'/federations/%s/detail' % federation['uuid'], expect_errors=True) |
||||
self.assertEqual(404, response.status_int) |
||||
self.assertEqual('application/json', response.content_type) |
||||
self.assertTrue(response.json['errors']) |
||||
|
||||
def test_many(self): |
||||
federation_list = [] |
||||
for id_ in range(5): |
||||
temp_uuid = uuidutils.generate_uuid() |
||||
federation = obj_utils.create_test_federation( |
||||
self.context, id=id_, uuid=temp_uuid) |
||||
federation_list.append(federation.uuid) |
||||
|
||||
response = self.get_json('/federations') |
||||
self.assertEqual(len(federation_list), len(response['federations'])) |
||||
uuids = [f['uuid'] for f in response['federations']] |
||||
self.assertEqual(sorted(federation_list), sorted(uuids)) |
||||
|
||||
def test_links(self): |
||||
uuid = uuidutils.generate_uuid() |
||||
obj_utils.create_test_federation(self.context, id=1, uuid=uuid) |
||||
response = self.get_json('/federations/%s' % uuid) |
||||
self.assertIn('links', response.keys()) |
||||
self.assertEqual(2, len(response['links'])) |
||||
self.assertIn(uuid, response['links'][0]['href']) |
||||
for l in response['links']: |
||||
bookmark = l['rel'] == 'bookmark' |
||||
self.assertTrue(self.validate_link(l['href'], |
||||
bookmark=bookmark)) |
||||
|
||||
def test_collection_links(self): |
||||
for id_ in range(5): |
||||
obj_utils.create_test_federation(self.context, id=id_, |
||||
uuid=uuidutils.generate_uuid()) |
||||
response = self.get_json('/federations/?limit=3') |
||||
next_marker = response['federations'][-1]['uuid'] |
||||
self.assertIn(next_marker, response['next']) |
||||
|
||||
def test_collection_links_default_limit(self): |
||||
cfg.CONF.set_override('max_limit', 3, 'api') |
||||
for id_ in range(5): |
||||
obj_utils.create_test_federation(self.context, id=id_, |
||||
uuid=uuidutils.generate_uuid()) |
||||
response = self.get_json('/federations') |
||||
self.assertEqual(3, len(response['federations'])) |
||||
|
||||
next_marker = response['federations'][-1]['uuid'] |
||||
self.assertIn(next_marker, response['next']) |
||||
|
||||
|
||||
class TestPatch(api_base.FunctionalTest): |
||||
def setUp(self): |
||||
super(TestPatch, self).setUp() |
||||
p = mock.patch.object(rpcapi.API, 'federation_update_async') |
||||
self.mock_federation_update = p.start() |
||||
self.mock_federation_update.side_effect = \ |
||||
self._sim_rpc_federation_update |
||||
self.addCleanup(p.stop) |
||||
|
||||
def _sim_rpc_federation_update(self, federation, rollback=False): |
||||
federation.save() |
||||
return federation |
||||
|
||||
def test_member_join(self): |
||||
f = obj_utils.create_test_federation( |
||||
self.context, name='federation-example', |
||||
uuid=uuidutils.generate_uuid(), member_ids=[]) |
||||
new_member = obj_utils.create_test_cluster(self.context) |
||||
|
||||
response = self.patch_json( |
||||
'/federations/%s' % f.uuid, |
||||
[{'path': '/member_ids', 'value': new_member.uuid, 'op': 'add'}]) |
||||
self.assertEqual(202, response.status_int) |
||||
|
||||
# make sure it was added: |
||||
fed = self.get_json('/federations/%s' % f.uuid) |
||||
self.assertTrue(new_member.uuid in fed['member_ids']) |
||||
|
||||
def test_member_unjoin(self): |
||||
member = obj_utils.create_test_cluster(self.context) |
||||