federation api: federation table and db layer

this commit introduces a new `Federation` table to
Magnum database, as well as the necessary DB layer
APIs to access and manage it.

this belongs to the first phase of the implementation
of the federation api. check [1] for more details.

[1] https://review.openstack.org/#/c/489609/

Change-Id: Ie8a68cd3198c8fc7930069fd2e55f1cad55b6c9b
Partially-Implements: blueprint federation-api
This commit is contained in:
Clenimar Filemon 2017-08-14 14:46:10 +02:00 committed by Clenimar Filemon
parent 2a60e56d41
commit 34754f36f0
7 changed files with 550 additions and 0 deletions

View File

@ -374,3 +374,11 @@ class TrusteeOrTrustToClusterFailed(MagnumException):
class CertificatesToClusterFailed(MagnumException):
message = _("Failed to create certificates for Cluster: %(cluster_uuid)s")
class FederationNotFound(ResourceNotFound):
message = _("Federation %(federation)s could not be found.")
class FederationAlreadyExists(Conflict):
message = _("A federation with UUID %(uuid)s already exists.")

View File

@ -444,3 +444,99 @@ class Connection(object):
:returns: Quota record.
"""
@abc.abstractmethod
def get_federation_by_id(self, context, federation_id):
"""Return a federation for a given federation id.
:param context: The security context
:param federation_id: The id of a federation
:returns: A federation
"""
@abc.abstractmethod
def get_federation_by_uuid(self, context, federation_uuid):
"""Return a federation for a given federation uuid.
:param context: The security context
:param federation_uuid: The uuid of a federation
:returns: A federation
"""
@abc.abstractmethod
def get_federation_by_name(self, context, federation_name):
"""Return a federation for a given federation name.
:param context: The security context
:param federation_name: The name of a federation
:returns: A federation
"""
@abc.abstractmethod
def get_federation_list(self, context, limit=None, marker=None,
sort_key=None, sort_dir=None, filters=None):
"""Get matching federations.
Return a list of the specified columns for all federations that
match the specified filters.
:param context: The security context
:param filters: Filters to apply. Defaults to None.
:param limit: Maximum number of federations to return.
:param marker: the last item of the previous page; we return the next
result set.
:param sort_key: Attribute by which results should be sorted.
:param sort_dir: direction in which results should be sorted.
(asc, desc)
:returns: A list of tuples of the specified columns.
"""
@abc.abstractmethod
def create_federation(self, values):
"""Create a new federation.
:param values: A dict containing several items used to identify
and track the federation.
For example:
::
{
'uuid': uuidutils.generate_uuid(),
'name': 'example',
'hostcluster_id': '91c8dd07-14a2-4fd8-b084-915fa53552fd',
'properties': 'dns-zone:example.com.'
}
:returns: A federation.
"""
@abc.abstractmethod
def destroy_federation(self, federation_id):
"""Destroy a federation.
This action *will not* destroy the host cluster nor the member
clusters.
:param federation_id: The id or uuid of a federation.
"""
@abc.abstractmethod
def update_federation(self, federation_id, values):
"""Update properties of a federation.
:param federation_id: The id or uuid of a federation.
:param values: A dict containing several items used to identify
and track the federation.
For example:
::
{
'uuid': uuidutils.generate_uuid(),
'name': 'example',
'hostcluster_id': '91c8dd07-14a2-4fd8-b084-915fa53552fd',
'properties': 'dns-zone:example.com.'
}
:returns: A federation.
:raises: FederationNotFound
"""

View File

@ -0,0 +1,47 @@
# 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.
""""add federation table
Revision ID: 9a1539f1cd2c
Revises: 041d9a0f1159
Create Date: 2017-08-07 11:47:29.865166
"""
# revision identifiers, used by Alembic.
revision = '9a1539f1cd2c'
down_revision = '041d9a0f1159'
from alembic import op
import sqlalchemy as sa
from magnum.db.sqlalchemy import models
def upgrade():
op.create_table(
'federation',
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('project_id', sa.String(length=255), nullable=True),
sa.Column('uuid', sa.String(length=36), nullable=True),
sa.Column('name', sa.String(length=255), nullable=True),
sa.Column('hostcluster_id', sa.String(length=255), nullable=True),
sa.Column('member_ids', models.JSONEncodedList(), nullable=True),
sa.Column('status', sa.String(length=20), nullable=True),
sa.Column('status_reason', sa.Text(), nullable=True),
sa.Column('properties', models.JSONEncodedList(), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('uuid', name='uniq_federation0uuid')
)

View File

@ -643,3 +643,112 @@ class Connection(api.Connection):
msg = (_('project_id %(project_id)s resource %(resource)s.') %
{'project_id': project_id, 'resource': resource})
raise exception.QuotaNotFound(msg=msg)
def _add_federation_filters(self, query, filters):
if filters is None:
filters = {}
possible_filters = ["name", "project_id", "hostcluster_id",
"member_ids", "properties"]
# TODO(clenimar): implement 'member_ids' filter as a contains query,
# so we return all the federations that have the given clusters,
# instead of all the federations that *only* have the exact given
# clusters.
filter_names = set(filters).intersection(possible_filters)
filter_dict = {filter_name: filters[filter_name]
for filter_name in filter_names}
query = query.filter_by(**filter_dict)
if 'status' in filters:
query = query.filter(
models.Federation.status.in_(filters['status']))
return query
def get_federation_by_id(self, context, federation_id):
query = model_query(models.Federation)
query = self._add_tenant_filters(context, query)
query = query.filter_by(id=federation_id)
try:
return query.one()
except NoResultFound:
raise exception.FederationNotFound(federation=federation_id)
def get_federation_by_uuid(self, context, federation_uuid):
query = model_query(models.Federation)
query = self._add_tenant_filters(context, query)
query = query.filter_by(uuid=federation_uuid)
try:
return query.one()
except NoResultFound:
raise exception.FederationNotFound(federation=federation_uuid)
def get_federation_by_name(self, context, federation_name):
query = model_query(models.Federation)
query = self._add_tenant_filters(context, query)
query = query.filter_by(name=federation_name)
try:
return query.one()
except MultipleResultsFound:
raise exception.Conflict('Multiple federations exist with same '
'name. Please use the federation uuid '
'instead.')
except NoResultFound:
raise exception.FederationNotFound(federation=federation_name)
def get_federation_list(self, context, limit=None, marker=None,
sort_key=None, sort_dir=None, filters=None):
query = model_query(models.Federation)
query = self._add_tenant_filters(context, query)
query = self._add_federation_filters(query, filters)
return _paginate_query(models.Federation, limit, marker,
sort_key, sort_dir, query)
def create_federation(self, values):
if not values.get('uuid'):
values['uuid'] = uuidutils.generate_uuid()
federation = models.Federation()
federation.update(values)
try:
federation.save()
except db_exc.DBDuplicateEntry:
raise exception.FederationAlreadyExists(uuid=values['uuid'])
return federation
def destroy_federation(self, federation_id):
session = get_session()
with session.begin():
query = model_query(models.Federation, session=session)
query = add_identity_filter(query, federation_id)
try:
query.one()
except NoResultFound:
raise exception.FederationNotFound(federation=federation_id)
query.delete()
def update_federation(self, federation_id, values):
if 'uuid' in values:
msg = _("Cannot overwrite UUID for an existing Federation.")
raise exception.InvalidParameterValue(err=msg)
return self._do_update_federation(federation_id, values)
def _do_update_federation(self, federation_id, values):
session = get_session()
with session.begin():
query = model_query(models.Federation, session=session)
query = add_identity_filter(query, federation_id)
try:
ref = query.with_lockmode('update').one()
except NoResultFound:
raise exception.FederationNotFound(federation=federation_id)
ref.update(values)
return ref

View File

@ -238,3 +238,21 @@ class Quota(Base):
project_id = Column(String(255))
resource = Column(String(255))
hard_limit = Column(Integer())
class Federation(Base):
"""Represents a Federation."""
__tablename__ = 'federation'
__table_args__ = (
schema.UniqueConstraint("uuid", name="uniq_federation0uuid"),
table_args()
)
id = Column(Integer, primary_key=True)
project_id = Column(String(255))
uuid = Column(String(36))
name = Column(String(255))
hostcluster_id = Column(String(255))
member_ids = Column(JSONEncodedList)
status = Column(String(20))
status_reason = Column(Text)
properties = Column(JSONEncodedDict)

View File

@ -0,0 +1,242 @@
# 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.
"""Tests for manipulating Federations via the DB API"""
from oslo_utils import uuidutils
import six
from magnum.common import context
from magnum.common import exception
from magnum.tests.unit.db import base
from magnum.tests.unit.db import utils
class DbFederationTestCase(base.DbTestCase):
def test_create_federation(self):
utils.create_test_federation()
def test_create_federation_already_exists(self):
utils.create_test_federation()
self.assertRaises(exception.FederationAlreadyExists,
utils.create_test_federation)
def test_get_federation_by_id(self):
federation = utils.create_test_federation()
res = self.dbapi.get_federation_by_id(self.context, federation.id)
self.assertEqual(federation.id, res.id)
self.assertEqual(federation.uuid, res.uuid)
def test_get_federation_by_name(self):
federation = utils.create_test_federation()
res = self.dbapi.get_federation_by_name(self.context, federation.name)
self.assertEqual(federation.name, res.name)
self.assertEqual(federation.uuid, res.uuid)
def test_get_federation_by_uuid(self):
federation = utils.create_test_federation()
res = self.dbapi.get_federation_by_uuid(self.context, federation.uuid)
self.assertEqual(federation.id, res.id)
self.assertEqual(federation.uuid, res.uuid)
def test_get_federation_that_does_not_exist(self):
self.assertRaises(exception.FederationNotFound,
self.dbapi.get_federation_by_id,
self.context, 999)
self.assertRaises(exception.FederationNotFound,
self.dbapi.get_federation_by_uuid,
self.context,
'12345678-9999-0000-aaaa-123456789012')
self.assertRaises(exception.FederationNotFound,
self.dbapi.get_federation_by_name,
self.context, 'not_found')
def test_get_federation_by_name_multiple_federation(self):
utils.create_test_federation(id=1, name='federation-1',
uuid=uuidutils.generate_uuid())
utils.create_test_federation(id=2, name='federation-1',
uuid=uuidutils.generate_uuid())
self.assertRaises(exception.Conflict,
self.dbapi.get_federation_by_name,
self.context, 'federation-1')
def test_get_federation_list(self):
uuids = []
for _ in range(5):
federation = utils.create_test_federation(
uuid=uuidutils.generate_uuid())
uuids.append(six.text_type(federation.uuid))
res = self.dbapi.get_federation_list(self.context, sort_key='uuid')
res_uuids = [r.uuid for r in res]
self.assertEqual(sorted(uuids), res_uuids)
def test_get_federation_list_sorted(self):
uuids = []
for _ in range(5):
federation = utils.create_test_federation(
uuid=uuidutils.generate_uuid())
uuids.append(six.text_type(federation.uuid))
res = self.dbapi.get_federation_list(self.context, sort_key='uuid')
res_uuids = [r.uuid for r in res]
self.assertEqual(sorted(uuids), res_uuids)
self.assertRaises(exception.InvalidParameterValue,
self.dbapi.get_federation_list,
self.context,
sort_key='foo')
def test_get_federation_list_with_filters(self):
fed1 = utils.create_test_federation(
id=1,
uuid=uuidutils.generate_uuid(),
name='fed1',
project_id='proj1',
hostcluster_id='master1',
member_ids=['member1', 'member2'],
properties={'dns-zone': 'fed1.com.'})
fed2 = utils.create_test_federation(
id=2,
uuid=uuidutils.generate_uuid(),
name='fed',
project_id='proj2',
hostcluster_id='master2',
member_ids=['member3', 'member4'],
properties={"dns-zone": "fed2.com."})
# NOTE(clenimar): we are specifying a project_id to the test
# resources above, which means that our current context
# (self.context) will not be able to see these resources.
# Create an admin context in order to test the queries:
ctx = context.make_admin_context(all_tenants=True)
# Filter by name:
res = self.dbapi.get_federation_list(ctx, filters={'name': 'fed1'})
self.assertEqual([fed1.id], [r.id for r in res])
res = self.dbapi.get_federation_list(ctx, filters={'name': 'foo'})
self.assertEqual([], [r.id for r in res])
# Filter by project_id
res = self.dbapi.get_federation_list(ctx,
filters={'project_id': 'proj1'})
self.assertEqual([fed1.id], [r.id for r in res])
res = self.dbapi.get_federation_list(ctx,
filters={'project_id': 'foo'})
self.assertEqual([], [r.id for r in res])
# Filter by hostcluster_id
res = self.dbapi.get_federation_list(ctx, filters={
'hostcluster_id': 'master1'})
self.assertEqual([fed1.id], [r.id for r in res])
res = self.dbapi.get_federation_list(ctx, filters={
'hostcluster_id': 'master2'})
self.assertEqual([fed2.id], [r.id for r in res])
res = self.dbapi.get_federation_list(ctx,
filters={'hostcluster_id': 'foo'})
self.assertEqual([], [r.id for r in res])
# Filter by member_ids (please note that it is currently implemented
# as an exact match. So it will only return federations whose member
# clusters are exactly those passed as a filter)
res = self.dbapi.get_federation_list(
ctx, filters={'member_ids': ['member1', 'member2']})
self.assertEqual([fed1.id], [r.id for r in res])
res = self.dbapi.get_federation_list(
ctx, filters={'member_ids': ['foo']})
self.assertEqual([], [r.id for r in res])
# Filter by properties
res = self.dbapi.get_federation_list(
ctx, filters={
'properties': {'dns-zone': 'fed2.com.'}
})
self.assertEqual([fed2.id], [r.id for r in res])
res = self.dbapi.get_federation_list(
ctx, filters={
'properties': {'dns-zone': 'foo.bar.'}
})
self.assertEqual([], [r.id for r in res])
def test_get_federation_list_by_admin_all_tenants(self):
uuids = []
for _ in range(5):
federation = utils.create_test_federation(
uuid=uuidutils.generate_uuid(),
project_id=uuidutils.generate_uuid())
uuids.append(six.text_type(federation['uuid']))
ctx = context.make_admin_context(all_tenants=True)
res = self.dbapi.get_federation_list(ctx)
res_uuids = [r.uuid for r in res]
self.assertEqual(len(res), 5)
self.assertEqual(sorted(uuids), sorted(res_uuids))
def test_destroy_federation(self):
federation = utils.create_test_federation()
self.assertIsNotNone(
self.dbapi.get_federation_by_id(self.context, federation.id))
self.dbapi.destroy_federation(federation.id)
self.assertRaises(exception.FederationNotFound,
self.dbapi.get_federation_by_id,
self.context, federation.id)
def test_destroy_federation_by_uuid(self):
federation = utils.create_test_federation(
uuid=uuidutils.generate_uuid())
self.assertIsNotNone(
self.dbapi.get_federation_by_uuid(self.context, federation.uuid))
self.dbapi.destroy_federation(federation.uuid)
self.assertRaises(exception.FederationNotFound,
self.dbapi.get_federation_by_uuid,
self.context, federation.uuid)
def test_destroy_federation_by_id_that_does_not_exist(self):
self.assertRaises(exception.FederationNotFound,
self.dbapi.destroy_federation,
'12345678-9999-0000-aaaa-123456789012')
def test_destroy_federation_by_uudid_that_does_not_exist(self):
self.assertRaises(exception.FederationNotFound,
self.dbapi.destroy_federation, '15')
def test_update_federation_members(self):
federation = utils.create_test_federation()
old_members = federation.member_ids
new_members = old_members + ['new-member-id']
self.assertNotEqual(old_members, new_members)
res = self.dbapi.update_federation(federation.id,
{'member_ids': new_members})
self.assertEqual(new_members, res.member_ids)
def test_update_federation_properties(self):
federation = utils.create_test_federation()
old_properties = federation.properties
new_properties = {
'dns-zone': 'new.domain.com.'
}
self.assertNotEqual(old_properties, new_properties)
res = self.dbapi.update_federation(federation.id,
{'properties': new_properties})
self.assertEqual(new_properties, res.properties)
def test_update_federation_not_found(self):
federation_uuid = uuidutils.generate_uuid()
self.assertRaises(exception.FederationNotFound,
self.dbapi.update_federation, federation_uuid,
{'member_ids': ['foo']})

View File

@ -236,3 +236,33 @@ def create_test_quotas(**kw):
del quotas['id']
dbapi = db_api.get_instance()
return dbapi.create_quota(quotas)
def get_test_federation(**kw):
return {
'id': kw.get('id', 42),
'uuid': kw.get('uuid', '60d6dbdc-9951-4cee-b020-55d3e15a749b'),
'name': kw.get('name', 'fake-name'),
'project_id': kw.get('project_id', 'fake_project'),
'hostcluster_id': kw.get('hostcluster_id', 'fake_master'),
'member_ids': kw.get('member_ids', ['fake_member1', 'fake_member2']),
'properties': kw.get('properties', {'dns-zone': 'example.com.'}),
'status': kw.get('status', 'CREATE_IN_PROGRESS'),
'status_reason': kw.get('status_reason', 'Completed successfully.'),
'created_at': kw.get('created_at'),
'updated_at': kw.get('updated_at')
}
def create_test_federation(**kw):
"""Create test federation entry in DB and return federation DB object.
:param kw: kwargs with overriding values for federation attributes.
:return: Test quotas DB object.
"""
federation = get_test_federation(**kw)
# Let DB generate ID if it isn't specified explicitly
if 'id' not in kw:
del federation['id']
dbapi = db_api.get_instance()
return dbapi.create_federation(federation)