Merge "federation api: federation table and db layer"

This commit is contained in:
Zuul 2018-02-07 21:34:49 +00:00 committed by Gerrit Code Review
commit 991d180b22
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)