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:
parent
710192a63f
commit
ec950be894
@ -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,
|
||||
|
@ -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):
|
||||
|
454
magnum/api/controllers/v1/federation.py
Normal file
454
magnum/api/controllers/v1/federation.py
Normal 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)
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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.")
|
||||
|
@ -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()
|
||||
|
91
magnum/common/policies/federation.py
Normal file
91
magnum/common/policies/federation.py
Normal 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
|
@ -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):
|
||||
|
32
magnum/conductor/handlers/federation_conductor.py
Normal file
32
magnum/conductor/handlers/federation_conductor.py
Normal 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.")
|
@ -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."""
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
)
|
||||
|
215
magnum/objects/federation.py
Normal file
215
magnum/objects/federation.py
Normal 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]
|
@ -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()
|
||||
|
@ -86,7 +86,11 @@ 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'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):
|
||||
file_name = self.get_path(paste_file)
|
||||
|
415
magnum/tests/unit/api/controllers/v1/test_federation.py
Normal file
415
magnum/tests/unit/api/controllers/v1/test_federation.py
Normal 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'])
|
@ -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)
|
||||
|
@ -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)
|
161
magnum/tests/unit/objects/test_federation.py
Normal file
161
magnum/tests/unit/objects/test_federation.py
Normal 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)
|
@ -363,6 +363,7 @@ object_data = {
|
||||
'MagnumService': '1.0-2d397ec59b0046bd5ec35cd3e06efeca',
|
||||
'Stats': '1.0-73a1cd6e3c0294c932a66547faba216c',
|
||||
'Quota': '1.0-94e100aebfa88f7d8428e007f2049c18',
|
||||
'Federation': '1.0-166da281432b083f0e4b851336e12e20'
|
||||
}
|
||||
|
||||
|
||||
|
@ -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:
|
||||
|
11
releasenotes/notes/add-federation-api-cf55d04f96772b0f.yaml
Normal file
11
releasenotes/notes/add-federation-api-cf55d04f96772b0f.yaml
Normal 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
|
Loading…
Reference in New Issue
Block a user