federation api: api endpoints

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-api
This commit is contained in:
Clenimar Filemon 2017-08-17 15:25:04 +02:00 committed by Clenimar Filemon
parent 710192a63f
commit ec950be894
23 changed files with 1593 additions and 2 deletions

View File

@ -210,6 +210,33 @@ def validate_master_count(cluster, cluster_template):
"master_count must be 1 when master_lb_enabled is False"))
def validate_federation_hostcluster(cluster_uuid):
"""Validate Federation `hostcluster_id` parameter.
If the parameter was not specified raise an
`exceptions.InvalidParameterValue`. If the specified identifier does not
identify any Cluster, raise `exception.ClusterNotFound`
"""
if cluster_uuid is not None:
api_utils.get_resource('Cluster', cluster_uuid)
else:
raise exception.InvalidParameterValue(
"No hostcluster specified. "
"Please specify a hostcluster_id.")
def validate_federation_properties(properties):
"""Validate Federation `properties` parameter."""
if properties is None:
raise exception.InvalidParameterValue(
"Please specify a `properties` "
"dict for the federation.")
# Currently, we only support the property `dns-zone`.
if properties.get('dns-zone') is None:
raise exception.InvalidParameterValue("No DNS zone specified. "
"Please specify a `dns-zone`.")
# Dictionary that maintains a list of validation functions
validators = {'image_id': validate_image,
'flavor_id': validate_flavor,

View File

@ -29,6 +29,7 @@ from magnum.api.controllers.v1 import baymodel
from magnum.api.controllers.v1 import certificate
from magnum.api.controllers.v1 import cluster
from magnum.api.controllers.v1 import cluster_template
from magnum.api.controllers.v1 import federation
from magnum.api.controllers.v1 import magnum_services
from magnum.api.controllers.v1 import quota
from magnum.api.controllers.v1 import stats
@ -99,6 +100,9 @@ class V1(controllers_base.APIBase):
stats = [link.Link]
"""Links to the stats resource"""
# Links to the federations resources
federations = [link.Link]
@staticmethod
def convert():
v1 = V1()
@ -161,6 +165,13 @@ class V1(controllers_base.APIBase):
pecan.request.host_url,
'stats', '',
bookmark=True)]
v1.federations = [link.Link.make_link('self', pecan.request.host_url,
'federations', ''),
link.Link.make_link('bookmark',
pecan.request.host_url,
'federations', '',
bookmark=True)]
return v1
@ -175,6 +186,7 @@ class Controller(controllers_base.Controller):
certificates = certificate.CertificateController()
mservices = magnum_services.MagnumServiceController()
stats = stats.StatsController()
federations = federation.FederationsController()
@expose.expose(V1)
def get(self):

View File

@ -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)

View File

@ -30,6 +30,7 @@ from magnum import objects
CONF = magnum.conf.CONF
cluster_update_allowed_properties = set(['node_count'])
federation_update_allowed_properties = set(['member_ids', 'properties'])
def enforce_cluster_type_supported():
@ -200,6 +201,15 @@ def validate_cluster_properties(delta):
raise exception.InvalidParameterValue(err=err)
def validate_federation_properties(delta):
update_disallowed_properties = delta - federation_update_allowed_properties
if update_disallowed_properties:
err = (_("cannot change federation property(ies) %s.") %
", ".join(update_disallowed_properties))
raise exception.InvalidParameterValue(err=err)
class Validator(object):
@classmethod

View File

@ -28,6 +28,7 @@ from magnum.common import short_id
from magnum.conductor.handlers import ca_conductor
from magnum.conductor.handlers import cluster_conductor
from magnum.conductor.handlers import conductor_listener
from magnum.conductor.handlers import federation_conductor
from magnum.conductor.handlers import indirection_api
import magnum.conf
from magnum import version
@ -51,6 +52,7 @@ def main():
cluster_conductor.Handler(),
conductor_listener.Handler(),
ca_conductor.Handler(),
federation_conductor.Handler(),
]
server = rpc_service.Service.create(CONF.conductor.topic,

View File

@ -382,3 +382,8 @@ class FederationNotFound(ResourceNotFound):
class FederationAlreadyExists(Conflict):
message = _("A federation with UUID %(uuid)s already exists.")
class MemberAlreadyExists(Conflict):
message = _("A cluster with UUID %(uuid)s is already a member of the"
"federation %(federation_name)s.")

View File

@ -20,6 +20,7 @@ from magnum.common.policies import baymodel
from magnum.common.policies import certificate
from magnum.common.policies import cluster
from magnum.common.policies import cluster_template
from magnum.common.policies import federation
from magnum.common.policies import magnum_service
from magnum.common.policies import quota
from magnum.common.policies import stats
@ -33,6 +34,7 @@ def list_rules():
certificate.list_rules(),
cluster.list_rules(),
cluster_template.list_rules(),
federation.list_rules(),
magnum_service.list_rules(),
quota.list_rules(),
stats.list_rules()

View File

@ -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

View File

@ -51,6 +51,29 @@ class API(rpc_service.API):
def cluster_update_async(self, cluster, rollback=False):
self._cast('cluster_update', cluster=cluster, rollback=rollback)
# Federation Operations
def federation_create(self, federation, create_timeout):
return self._call('federation_create', federation=federation,
create_timeout=create_timeout)
def federation_create_async(self, federation, create_timeout):
self._cast('federation_create', federation=federation,
create_timeout=create_timeout)
def federation_delete(self, uuid):
return self._call('federation_delete', uuid=uuid)
def federation_delete_async(self, uuid):
self._cast('federation_delete', uuid=uuid)
def federation_update(self, federation):
return self._call('federation_update', federation=federation)
def federation_update_async(self, federation, rollback=False):
self._cast('federation_update', federation=federation,
rollback=rollback)
# CA operations
def sign_certificate(self, cluster, certificate):

View File

@ -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.")

View File

@ -173,6 +173,21 @@ class Driver(object):
raise NotImplementedError("Subclasses must implement "
"'delete_cluster'.")
@abc.abstractmethod
def create_federation(self, context, federation):
raise NotImplementedError("Subclasses must implement "
"'create_federation'.")
@abc.abstractmethod
def update_federation(self, context, federation):
raise NotImplementedError("Subclasses must implement "
"'update_federation'.")
@abc.abstractmethod
def delete_federation(self, context, federation):
raise NotImplementedError("Subclasses must implement "
"'delete_federation'.")
def get_monitor(self, context, cluster):
"""return the monitor with container data for this driver."""

View File

@ -74,6 +74,15 @@ class HeatDriver(driver.Driver):
raise NotImplementedError("Must implement 'get_template_definition'")
def create_federation(self, context, federation):
return NotImplementedError("Must implement 'create_federation'")
def update_federation(self, context, federation):
return NotImplementedError("Must implement 'update_federation'")
def delete_federation(self, context, federation):
return NotImplementedError("Must implement 'delete_federation'")
def update_cluster_status(self, context, cluster):
if cluster.stack_id is None:
# NOTE(mgoddard): During cluster creation it is possible to poll

View File

@ -15,6 +15,7 @@
from magnum.objects import certificate
from magnum.objects import cluster
from magnum.objects import cluster_template
from magnum.objects import federation
from magnum.objects import magnum_service
from magnum.objects import quota
from magnum.objects import stats
@ -28,10 +29,13 @@ Quota = quota.Quota
X509KeyPair = x509keypair.X509KeyPair
Certificate = certificate.Certificate
Stats = stats.Stats
Federation = federation.Federation
__all__ = (Cluster,
ClusterTemplate,
MagnumService,
X509KeyPair,
Certificate,
Stats,
Quota)
Quota,
Federation
)

View File

@ -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]

View File

@ -49,6 +49,28 @@ class ClusterStatus(fields.Enum):
super(ClusterStatus, self).__init__(valid_values=ClusterStatus.ALL)
class FederationStatus(fields.Enum):
CREATE_IN_PROGRESS = 'CREATE_IN_PROGRESS'
CREATE_FAILED = 'CREATE_FAILED'
CREATE_COMPLETE = 'CREATE_COMPLETE'
UPDATE_IN_PROGRESS = 'UPDATE_IN_PROGRESS'
UPDATE_FAILED = 'UPDATE_FAILED'
UPDATE_COMPLETE = 'UPDATE_COMPLETE'
DELETE_IN_PROGRESS = 'DELETE_IN_PROGRESS'
DELETE_FAILED = 'DELETE_FAILED'
DELETE_COMPLETE = 'DELETE_COMPLETE'
ALL = (CREATE_IN_PROGRESS, CREATE_FAILED, CREATE_COMPLETE,
UPDATE_IN_PROGRESS, UPDATE_FAILED, UPDATE_COMPLETE,
DELETE_IN_PROGRESS, DELETE_FAILED, DELETE_COMPLETE)
STATUS_FAILED = (CREATE_FAILED, UPDATE_FAILED, DELETE_FAILED)
def __init__(self):
super(FederationStatus, self).__init__(
valid_values=FederationStatus.ALL)
class ContainerStatus(fields.Enum):
ALL = (
ERROR, RUNNING, STOPPED, PAUSED, UNKNOWN,
@ -146,3 +168,7 @@ class ClusterTypeField(fields.BaseEnumField):
class ServerTypeField(fields.BaseEnumField):
AUTO_TYPE = ServerType()
class FederationStatusField(fields.BaseEnumField):
AUTO_TYPE = FederationStatus()

View File

@ -86,6 +86,10 @@ class TestRootController(api_base.FunctionalTest):
u'mservices': [{u'href': u'http://localhost/v1/mservices/',
u'rel': u'self'},
{u'href': u'http://localhost/mservices/',
u'rel': u'bookmark'}],
u'federations': [{u'href': u'http://localhost/v1/federations/',
u'rel': u'self'},
{u'href': u'http://localhost/federations/',
u'rel': u'bookmark'}]}
def make_app(self, paste_file):

View File

@ -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)
federation = obj_utils.create_test_federation(
self.context, name='federation-example',
uuid=uuidutils.generate_uuid(), member_ids=[member.uuid])
response = self.patch_json(
'/federations/%s' % federation.uuid,
[{'path': '/member_ids', 'value': member.uuid, 'op': 'remove'}])
self.assertEqual(202, response.status_int)
# make sure it was deleted:
fed = self.get_json('/federations/%s' % federation.uuid)
self.assertFalse(member.uuid in fed['member_ids'])
def test_join_non_existent_cluster(self):
foo_uuid = uuidutils.generate_uuid()
f = obj_utils.create_test_federation(
self.context, name='federation-example',
uuid=uuidutils.generate_uuid(), member_ids=[])
response = self.patch_json(
'/federations/%s' % f.uuid,
[{'path': '/member_ids', 'value': foo_uuid, 'op': 'add'}],
expect_errors=True)
self.assertEqual(404, response.status_int)
def test_unjoin_non_existent_cluster(self):
foo_uuid = uuidutils.generate_uuid()
f = obj_utils.create_test_federation(
self.context, name='federation-example',
uuid=uuidutils.generate_uuid(), member_ids=[])
response = self.patch_json(
'/federations/%s' % f.uuid,
[{'path': '/member_ids', 'value': foo_uuid, 'op': 'remove'}],
expect_errors=True)
self.assertEqual(404, response.status_int)
def test_join_cluster_already_member(self):
cluster = obj_utils.create_test_cluster(self.context)
f = obj_utils.create_test_federation(
self.context, name='federation-example',
uuid=uuidutils.generate_uuid(), member_ids=[cluster.uuid])
response = self.patch_json(
'/federations/%s' % f.uuid,
[{'path': '/member_ids', 'value': cluster.uuid, 'op': 'add'}],
expect_errors=True)
self.assertEqual(409, response.status_int)
def test_unjoin_non_member_cluster(self):
cluster = obj_utils.create_test_cluster(self.context)
f = obj_utils.create_test_federation(
self.context, name='federation-example',
uuid=uuidutils.generate_uuid(), member_ids=[])
response = self.patch_json(
'/federations/%s' % f.uuid,
[{'path': '/member_ids', 'value': cluster.uuid, 'op': 'remove'}],
expect_errors=True)
self.assertEqual(404, response.status_int)
class TestPost(api_base.FunctionalTest):
def setUp(self):
super(TestPost, self).setUp()
p = mock.patch.object(rpcapi.API, 'federation_create_async')
self.mock_fed_create = p.start()
self.mock_fed_create.side_effect = self._simulate_federation_create
self.addCleanup(p.stop)
self.hostcluster = obj_utils.create_test_cluster(self.context)
def _simulate_federation_create(self, federation, create_timeout):
federation.create()
return federation
@mock.patch('oslo_utils.timeutils.utcnow')
def test_create_federation(self, mock_utcnow):
bdict = apiutils.federation_post_data(
uuid=uuidutils.generate_uuid(),
hostcluster_id=self.hostcluster.uuid)
test_time = datetime.datetime(2000, 1, 1, 0, 0)
mock_utcnow.return_value = test_time
response = self.post_json('/federations', bdict)
self.assertEqual('application/json', response.content_type)
self.assertEqual(202, response.status_int)
self.assertTrue(uuidutils.is_uuid_like(response.json['uuid']))
def test_create_federation_no_hostcluster_id(self):
bdict = apiutils.federation_post_data(uuid=uuidutils.generate_uuid())
del bdict['hostcluster_id']
response = self.post_json('/federations', bdict, expect_errors=True)
self.assertEqual(400, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['errors'])
def test_create_federation_hostcluster_does_not_exist(self):
bdict = apiutils.federation_post_data(
uuid=uuidutils.generate_uuid(),
hostcluster_id=uuidutils.generate_uuid())
response = self.post_json('/federations', bdict, expect_errors=True)
self.assertEqual(404, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['errors'])
def test_create_federation_no_dns_zone_name(self):
bdict = apiutils.federation_post_data(
uuid=uuidutils.generate_uuid(),
hostcluster_id=self.hostcluster.uuid)
del bdict['properties']
response = self.post_json('/federations', bdict, expect_errors=True)
self.assertEqual(400, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['errors'])
def test_create_federation_generate_uuid(self):
bdict = apiutils.federation_post_data(
hostcluster_id=self.hostcluster.uuid)
del bdict['uuid']
response = self.post_json('/federations', bdict)
self.assertEqual(202, response.status_int)
def test_create_federation_with_invalid_name(self):
invalid_names = [
'x' * 243, '123456', '123456test_federation',
'-test_federation', '.test_federation', '_test_federation', ''
]
for value in invalid_names:
bdict = apiutils.federation_post_data(
uuid=uuidutils.generate_uuid(), name=value,
hostcluster_id=self.hostcluster.uuid)
response = self.post_json('/federations', bdict,
expect_errors=True)
self.assertEqual('application/json', response.content_type)
self.assertEqual(400, response.status_int)
self.assertTrue(response.json['errors'])
def test_create_federation_with_valid_name(self):
valid_names = [
'test_federation123456', 'test-federation', 'test.federation',
'testfederation.', 'testfederation-', 'testfederation_',
'test.-_federation', 'Testfederation'
]
for value in valid_names:
bdict = apiutils.federation_post_data(
name=value, hostcluster_id=self.hostcluster.uuid)
bdict['uuid'] = uuidutils.generate_uuid()
response = self.post_json('/federations', bdict)
self.assertEqual(202, response.status_int)
def test_create_federation_without_name(self):
bdict = apiutils.federation_post_data(
uuid=uuidutils.generate_uuid(),
hostcluster_id=self.hostcluster.uuid)
del bdict['name']
response = self.post_json('/federations', bdict)
self.assertEqual(202, response.status_int)
class TestDelete(api_base.FunctionalTest):
def setUp(self):
super(TestDelete, self).setUp()
self.federation = obj_utils.create_test_federation(
self.context, name='federation-example',
uuid=uuidutils.generate_uuid())
p = mock.patch.object(rpcapi.API, 'federation_delete_async')
self.mock_federation_delete = p.start()
self.mock_federation_delete.side_effect = \
self._simulate_federation_delete
self.addCleanup(p.stop)
def _simulate_federation_delete(self, federation_uuid):
federation = objects.Federation.get_by_uuid(self.context,
federation_uuid)
federation.destroy()
def test_delete_federation(self):
self.delete('/federations/%s' % self.federation.uuid)
response = self.get_json('/federations/%s' % self.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_delete_federation_not_found(self):
delete = self.delete('/federations/%s' % uuidutils.generate_uuid(),
expect_errors=True)
self.assertEqual(404, delete.status_int)
self.assertEqual('application/json', delete.content_type)
self.assertTrue(delete.json['errors'])
def test_delete_federation_with_name(self):
delete = self.delete('/federations/%s' % self.federation.name)
self.assertEqual(204, delete.status_int)
def test_delete_federation_with_name_not_found(self):
delete = self.delete('/federations/%s' % 'foo',
expect_errors=True)
self.assertEqual(404, delete.status_int)
self.assertEqual('application/json', delete.content_type)
self.assertTrue(delete.json['errors'])

View File

@ -20,6 +20,7 @@ from magnum.api.controllers.v1 import bay as bay_controller
from magnum.api.controllers.v1 import baymodel as baymodel_controller
from magnum.api.controllers.v1 import cluster as cluster_controller
from magnum.api.controllers.v1 import cluster_template as cluster_tmp_ctrl
from magnum.api.controllers.v1 import federation as federation_controller
from magnum.tests.unit.db import utils
@ -86,3 +87,9 @@ def mservice_get_data(**kw):
'created_at': kw.get('created_at', faketime),
'updated_at': kw.get('updated_at', faketime),
}
def federation_post_data(**kw):
federation = utils.get_test_federation(**kw)
internal = federation_controller.FederationPatchType.internal_attrs()
return remove_internal(federation, internal)

View File

@ -0,0 +1,38 @@
# 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.conductor.handlers import federation_conductor
from magnum import objects
from magnum.tests.unit.db import base as db_base
from magnum.tests.unit.db import utils
class TestHandler(db_base.DbTestCase):
def setUp(self):
super(TestHandler, self).setUp()
self.handler = federation_conductor.Handler()
federation_dict = utils.get_test_federation()
self.federation = objects.Federation(self.context, **federation_dict)
self.federation.create()
def test_create_federation(self):
self.assertRaises(NotImplementedError, self.handler.federation_create,
self.context, self.federation, create_timeout=15)
def test_update_federation(self):
self.assertRaises(NotImplementedError, self.handler.federation_update,
self.context, self.federation, rollback=False)
def test_delete_federation(self):
self.assertRaises(NotImplementedError, self.handler.federation_delete,
self.context, self.federation.uuid)

View File

@ -0,0 +1,161 @@
# 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 mock
from oslo_utils import uuidutils
from testtools.matchers import HasLength
from magnum.common import exception
from magnum import objects
from magnum.tests.unit.db import base
from magnum.tests.unit.db import utils
class TestFederationObject(base.DbTestCase):
def setUp(self):
super(TestFederationObject, self).setUp()
self.fake_federation = utils.get_test_federation(
uuid=uuidutils.generate_uuid(),
hostcluster_id=uuidutils.generate_uuid(),
member_ids=[]
)
def test_get_by_id(self):
federation_id = self.fake_federation['id']
with mock.patch.object(self.dbapi, 'get_federation_by_id',
autospec=True) as mock_get_federation:
mock_get_federation.return_value = self.fake_federation
federation = objects.Federation.get(self.context, federation_id)
mock_get_federation.assert_called_once_with(self.context,
federation_id)
self.assertEqual(self.context, federation._context)
def test_get_by_uuid(self):
federation_uuid = self.fake_federation['uuid']
with mock.patch.object(self.dbapi, 'get_federation_by_uuid',
autospec=True) as mock_get_federation:
mock_get_federation.return_value = self.fake_federation
federation = objects.Federation.get(self.context, federation_uuid)
mock_get_federation.assert_called_once_with(self.context,
federation_uuid)
self.assertEqual(self.context, federation._context)
def test_get_by_name(self):
name = self.fake_federation['name']
with mock.patch.object(self.dbapi, 'get_federation_by_name',
autospec=True) as mock_get_federation:
mock_get_federation.return_value = self.fake_federation
federation = objects.Federation.get_by_name(self.context, name)
mock_get_federation.assert_called_once_with(self.context, name)
self.assertEqual(self.context, federation._context)
def test_get_bad_id_and_uuid(self):
self.assertRaises(exception.InvalidIdentity,
objects.Federation.get, self.context, 'not-a-uuid')
def test_list(self):
with mock.patch.object(self.dbapi, 'get_federation_list',
autospec=True) as mock_get_list:
mock_get_list.return_value = [self.fake_federation]
federations = objects.Federation.list(self.context)
self.assertEqual(1, mock_get_list.call_count)
self.assertThat(federations, HasLength(1))
self.assertIsInstance(federations[0], objects.Federation)
self.assertEqual(self.context, federations[0]._context)
def test_list_all(self):
with mock.patch.object(self.dbapi, 'get_federation_list',
autospec=True) as mock_get_list:
mock_get_list.return_value = [self.fake_federation]
self.context.all_tenants = True
federations = objects.Federation.list(self.context)
mock_get_list.assert_called_once_with(
self.context, limit=None, marker=None, filters=None,
sort_dir=None, sort_key=None)
self.assertEqual(1, mock_get_list.call_count)
self.assertThat(federations, HasLength(1))
self.assertIsInstance(federations[0], objects.Federation)
self.assertEqual(self.context, federations[0]._context)
def test_list_with_filters(self):
with mock.patch.object(self.dbapi, 'get_federation_list',
autospec=True) as mock_get_list:
mock_get_list.return_value = [self.fake_federation]
filters = {'name': 'federation1'}
federations = objects.Federation.list(self.context,
filters=filters)
mock_get_list.assert_called_once_with(self.context, sort_key=None,
sort_dir=None,
filters=filters, limit=None,
marker=None)
self.assertEqual(1, mock_get_list.call_count)
self.assertThat(federations, HasLength(1))
self.assertIsInstance(federations[0], objects.Federation)
self.assertEqual(self.context, federations[0]._context)
def test_create(self):
with mock.patch.object(self.dbapi, 'create_federation',
autospec=True) as mock_create_federation:
mock_create_federation.return_value = self.fake_federation
federation = objects.Federation(self.context,
**self.fake_federation)
federation.create()
mock_create_federation.assert_called_once_with(
self.fake_federation)
self.assertEqual(self.context, federation._context)
def test_destroy(self):
uuid = self.fake_federation['uuid']
with mock.patch.object(self.dbapi, 'get_federation_by_uuid',
autospec=True) as mock_get_federation:
mock_get_federation.return_value = self.fake_federation
with mock.patch.object(self.dbapi, 'destroy_federation',
autospec=True) as mock_destroy_federation:
federation = objects.Federation.get_by_uuid(self.context, uuid)
federation.destroy()
mock_get_federation.assert_called_once_with(self.context, uuid)
mock_destroy_federation.assert_called_once_with(uuid)
self.assertEqual(self.context, federation._context)
def test_save(self):
uuid = self.fake_federation['uuid']
with mock.patch.object(self.dbapi, 'get_federation_by_uuid',
autospec=True) as mock_get_federation:
mock_get_federation.return_value = self.fake_federation
with mock.patch.object(self.dbapi, 'update_federation',
autospec=True) as mock_update_federation:
federation = objects.Federation.get_by_uuid(self.context, uuid)
federation.member_ids = ['new-member']
federation.save()
mock_get_federation.assert_called_once_with(self.context, uuid)
mock_update_federation.assert_called_once_with(
uuid, {'member_ids': ['new-member']})
self.assertEqual(self.context, federation._context)
def test_refresh(self):
uuid = self.fake_federation['uuid']
new_uuid = uuidutils.generate_uuid()
returns = [dict(self.fake_federation, uuid=uuid),
dict(self.fake_federation, uuid=new_uuid)]
expected = [mock.call(self.context, uuid),
mock.call(self.context, uuid)]
with mock.patch.object(self.dbapi, 'get_federation_by_uuid',
side_effect=returns,
autospec=True) as mock_get_federation:
federation = objects.Federation.get_by_uuid(self.context, uuid)
self.assertEqual(uuid, federation.uuid)
federation.refresh()
self.assertEqual(new_uuid, federation.uuid)
self.assertEqual(expected, mock_get_federation.call_args_list)
self.assertEqual(self.context, federation._context)

View File

@ -363,6 +363,7 @@ object_data = {
'MagnumService': '1.0-2d397ec59b0046bd5ec35cd3e06efeca',
'Stats': '1.0-73a1cd6e3c0294c932a66547faba216c',
'Quota': '1.0-94e100aebfa88f7d8428e007f2049c18',
'Federation': '1.0-166da281432b083f0e4b851336e12e20'
}

View File

@ -157,6 +157,33 @@ def get_test_magnum_service_object(context, **kw):
return magnum_service
def get_test_federation(context, **kw):
"""Return a Federation object with appropriate attributes.
NOTE: The object leaves the attributes marked as changed, such
that a create() could be used to commit it to the DB.
"""
db_federation = db_utils.get_test_federation(**kw)
# Let DB generate ID if it isn't specified explicitly
if 'id' not in kw:
del db_federation['id']
federation = objects.Federation(context)
for key in db_federation:
setattr(federation, key, db_federation[key])
return federation
def create_test_federation(context, **kw):
"""Create and return a test Federation object.
Create a Federation in the DB and return a Federation object with
appropriate attributes.
"""
federation = get_test_federation(context, **kw)
federation.create()
return federation
def datetime_or_none(dt):
"""Validate a datetime or None value."""
if dt is None:

View File

@ -0,0 +1,11 @@
---
features:
- |
This release introduces 'federations' endpoint
to Magnum API, which allows an admin to create
and manage federations of clusters through Magnum.
As the feature is still under development,
the endpoints are not bound to any driver yet.
For more details, please refer to bp/federation-api [1].
[1] https://review.openstack.org/#/q/topic:bp/federation-api