Adds the foundation for datastore capabilities

This allows for capabilities to be set in the mysql database capabilities
table and associated to datastores with the datastore_capabilities
table. This allows users to have datastore specific functionality that
is configurable and able to be served to end users. This commit is for
the groundwork for the capabilities API.

Change-Id: I7153d435bf8c00dc2a874a66c7a2b64e8fbafa09
Partially-Implements: blueprint capabilities
This commit is contained in:
Kaleb Pomeroy 2014-03-12 14:36:08 -05:00
parent 2f703fe36e
commit f4c9f56a99
8 changed files with 543 additions and 6 deletions

View File

@ -65,6 +65,16 @@ class NotFound(TroveError):
message = _("Resource %(uuid)s cannot be found")
class CapabilityNotFound(NotFound):
message = _("Capability '%(capability)s' cannot be found.")
class CapabilityDisabled(TroveError):
message = _("Capability '%(capability)s' is disabled.")
class FlavorNotFound(TroveError):
message = _("Resource %(uuid)s cannot be found")

View File

@ -32,7 +32,9 @@ db_api = get_db_api()
def persisted_models():
return {
'datastore': DBDatastore,
'capabilities': DBCapabilities,
'datastore_version': DBDatastoreVersion,
'capability_overrides': DBCapabilityOverrides,
}
@ -41,12 +43,284 @@ class DBDatastore(dbmodels.DatabaseModelBase):
_data_fields = ['id', 'name', 'default_version_id']
class DBCapabilities(dbmodels.DatabaseModelBase):
_data_fields = ['id', 'name', 'description', 'enabled']
class DBCapabilityOverrides(dbmodels.DatabaseModelBase):
_data_fields = ['id', 'capability_id', 'datastore_version_id', 'enabled']
class DBDatastoreVersion(dbmodels.DatabaseModelBase):
_data_fields = ['id', 'datastore_id', 'name', 'manager', 'image_id',
'packages', 'active']
class Capabilities(object):
def __init__(self, datastore_version_id=None):
self.capabilities = []
self.datastore_version_id = datastore_version_id
def __contains__(self, item):
return item in [capability.name for capability in self.capabilities]
def __len__(self):
return len(self.capabilities)
def __iter__(self):
for item in self.capabilities:
yield item
def __repr__(self):
return '<%s: %s>' % (type(self), self.capabilities)
def add(self, capability, enabled):
"""
Add a capability override to a datastore version.
"""
if self.datastore_version_id is not None:
DBCapabilityOverrides.create(
capability_id=capability.id,
datastore_version_id=self.datastore_version_id,
enabled=enabled)
self._load()
def _load(self):
"""
Bulk load and override default capabilities with configured
datastore version specific settings.
"""
capability_defaults = [Capability(c)
for c in DBCapabilities.find_all()]
capability_overrides = []
if self.datastore_version_id is not None:
# This should always happen but if there is any future case where
# we don't have a datastore version id number it won't stop
# defaults from rendering.
capability_overrides = [
CapabilityOverride(ce)
for ce in DBCapabilityOverrides.find_all(
datastore_version_id=self.datastore_version_id)
]
def override(cap):
# This logic is necessary to apply datastore version specific
# capability overrides when they are present in the database.
for capability_override in capability_overrides:
if cap.id == capability_override.capability_id:
# we have a mapped entity that indicates this datastore
# version has an override so we honor that.
return capability_override
# There were no overrides for this capability so we just hand it
# right back.
return cap
self.capabilities = map(override, capability_defaults)
LOG.debug('Capabilities for datastore %(ds_id)s: %(capabilities)s' %
{'ds_id': self.datastore_version_id,
'capabilities': self.capabilities})
@classmethod
def load(cls, datastore_version_id=None):
"""
Generates a Capabilities object by looking up all capabilities from
defaults and overrides and provides the one structure that should be
used as the interface to controlling capabilities per datastore.
:returns Capabilities:
"""
self = cls(datastore_version_id)
self._load()
return self
class BaseCapability(object):
def __init__(self, db_info):
self.db_info = db_info
def __repr__(self):
return ('<%(my_class)s: name: %(name)s, enabled: %(enabled)s>' %
{'my_class': type(self), 'name': self.name,
'enabled': self.enabled})
@property
def id(self):
"""
The capability's id
:returns str:
"""
return self.db_info.id
@property
def enabled(self):
"""
Is the capability/feature enabled?
:returns bool:
"""
return self.db_info.enabled
def enable(self):
"""
Enable the capability.
"""
self.db_info.enabled = True
self.db_info.save()
def disable(self):
"""
Disable the capability
"""
self.db_info.enabled = False
self.db_info.save()
def delete(self):
"""
Delete the capability from the database.
"""
self.db_info.delete()
class CapabilityOverride(BaseCapability):
"""
A capability override is simply an setting that applies to a
specific datastore version that overrides the default setting in the
base capability's entry for Trove.
"""
def __init__(self, db_info):
super(CapabilityOverride, self).__init__(db_info)
# This *may* be better solved with a join in the SQLAlchemy model but
# I was unable to get our query object to work properly for this.
parent_capability = Capability.load(db_info.capability_id)
if parent_capability:
self.parent_name = parent_capability.name
self.parent_description = parent_capability.description
else:
raise exception.CapabilityNotFound(
_("Somehow we got a datastore version capability without a "
"parent, that shouldn't happen. %s") % db_info.capability_id)
@property
def name(self):
"""
The name of the capability.
:returns str:
"""
return self.parent_name
@property
def description(self):
"""
The description of the capability.
:returns str:
"""
return self.parent_description
@property
def capability_id(self):
"""
Because capability overrides is an association table there are times
where having the capability id is necessary.
:returns str:
"""
return self.db_info.capability_id
@classmethod
def load(cls, capability_id):
"""
Generates a CapabilityOverride object from the capability_override id.
:returns CapabilityOverride:
"""
try:
return cls(DBCapabilityOverrides.find_by(
capability_id=capability_id))
except exception.ModelNotFoundError:
raise exception.CapabilityNotFound(
_("Capability Override not found for "
"capability %s") % capability_id)
@classmethod
def create(cls, capability, datastore_version_id, enabled):
"""
Create a new CapabilityOverride.
:param capability: The capability to be overridden for
this DS Version
:param datastore_version_id: The datastore version to apply the
override to.
:param enabled: Set enabled to True or False
:returns CapabilityOverride:
"""
return CapabilityOverride(
DBCapabilityOverrides.create(
capability_id=capability.id,
datastore_version_id=datastore_version_id,
enabled=enabled)
)
class Capability(BaseCapability):
@property
def name(self):
"""
The Capability name
:returns str:
"""
return self.db_info.name
@property
def description(self):
"""
The Capability description
:returns str:
"""
return self.db_info.description
@classmethod
def load(cls, capability_id_or_name):
"""
Generates a Capability object by looking up the capability first by
ID then by name.
:returns Capability:
"""
try:
return cls(DBCapabilities.find_by(id=capability_id_or_name))
except exception.ModelNotFoundError:
try:
return cls(DBCapabilities.find_by(name=capability_id_or_name))
except exception.ModelNotFoundError:
raise exception.CapabilityNotFound(
capability=capability_id_or_name)
@classmethod
def create(cls, name, description, enabled=False):
"""
Creates a new capability.
:returns Capability:
"""
return Capability(DBCapabilities.create(
name=name, description=description, enabled=enabled))
class Datastore(object):
def __init__(self, db_info):
@ -74,6 +348,9 @@ class Datastore(object):
def default_version_id(self):
return self.db_info.default_version_id
def delete(self):
self.db_info.delete()
class Datastores(object):
@ -96,6 +373,7 @@ class Datastores(object):
class DatastoreVersion(object):
def __init__(self, db_info):
self._capabilities = None
self.db_info = db_info
@classmethod
@ -147,6 +425,13 @@ class DatastoreVersion(object):
def manager(self):
return self.db_info.manager
@property
def capabilities(self):
if self._capabilities is None:
self._capabilities = Capabilities.load(self.db_info.id)
return self._capabilities
class DatastoreVersions(object):

View File

@ -32,6 +32,10 @@ def map(engine, models):
Table('datastores', meta, autoload=True))
orm.mapper(models['datastore_version'],
Table('datastore_versions', meta, autoload=True))
orm.mapper(models['capabilities'],
Table('capabilities', meta, autoload=True))
orm.mapper(models['capability_overrides'],
Table('capability_overrides', meta, autoload=True))
orm.mapper(models['service_statuses'],
Table('service_statuses', meta, autoload=True))
orm.mapper(models['dns_records'],

View File

@ -0,0 +1,61 @@
# Copyright (c) 2014 Rackspace Hosting
#
# 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 sqlalchemy import ForeignKey
from sqlalchemy.schema import Column
from sqlalchemy.schema import MetaData
from sqlalchemy.schema import UniqueConstraint
from trove.db.sqlalchemy.migrate_repo.schema import create_tables
from trove.db.sqlalchemy.migrate_repo.schema import drop_tables
from trove.db.sqlalchemy.migrate_repo.schema import String
from trove.db.sqlalchemy.migrate_repo.schema import Table
from trove.db.sqlalchemy.migrate_repo.schema import Boolean
meta = MetaData()
capabilities = Table(
'capabilities',
meta,
Column('id', String(36), primary_key=True, nullable=False),
Column('name', String(255), unique=True),
Column('description', String(255), nullable=False),
Column('enabled', Boolean())
)
capability_overrides = Table(
'capability_overrides',
meta,
Column('id', String(36), primary_key=True, nullable=False),
Column('datastore_version_id', String(36),
ForeignKey('datastore_versions.id')),
Column('capability_id', String(36), ForeignKey('capabilities.id')),
Column('enabled', Boolean()),
UniqueConstraint('datastore_version_id', 'capability_id',
name='idx_datastore_capabilities_enabled')
)
def upgrade(migrate_engine):
meta.bind = migrate_engine
Table('datastores', meta, autoload=True)
Table('datastore_versions', meta, autoload=True)
create_tables([capabilities, capability_overrides])
def downgrade(migrate_engine):
meta.bind = migrate_engine
drop_tables([capabilities, capability_overrides])

View File

@ -0,0 +1,73 @@
# Copyright (c) 2014 Rackspace Hosting
#
# 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 testtools
from trove.datastore import models as datastore_models
from trove.datastore.models import Datastore
from trove.datastore.models import Capability
from trove.datastore.models import DatastoreVersion
from trove.datastore.models import DBCapabilityOverrides
from trove.tests.unittests.util import util
import uuid
class TestDatastoreBase(testtools.TestCase):
def setUp(self):
# Basic setup and mock/fake structures for testing only
super(TestDatastoreBase, self).setUp()
util.init_db()
self.rand_id = str(uuid.uuid4())
self.ds_name = "my-test-datastore" + self.rand_id
self.ds_version = "my-test-version" + self.rand_id
self.capability_name = "root_on_create" + self.rand_id
self.capability_desc = "Enables root on create"
self.capability_enabled = True
datastore_models.update_datastore(self.ds_name, False)
self.datastore = Datastore.load(self.ds_name)
datastore_models.update_datastore_version(
self.ds_name, self.ds_version, "mysql", "", "", True)
self.datastore_version = DatastoreVersion.load(self.datastore,
self.ds_version)
self.test_id = self.datastore_version.id
self.cap1 = Capability.create(self.capability_name,
self.capability_desc, True)
self.cap2 = Capability.create("require_volume" + self.rand_id,
"Require external volume", True)
self.cap3 = Capability.create("test_capability" + self.rand_id,
"Test capability", False)
def tearDown(self):
super(TestDatastoreBase, self).tearDown()
capabilities_overridden = DBCapabilityOverrides.find_all(
datastore_version_id=self.datastore_version.id).all()
for ce in capabilities_overridden:
ce.delete()
self.cap1.delete()
self.cap2.delete()
self.cap3.delete()
Datastore.load(self.ds_name).delete()
def capability_name_filter(self, capabilities):
new_capabilities = []
for capability in capabilities:
if self.rand_id in capability.name:
new_capabilities.append(capability)
return new_capabilities

View File

@ -0,0 +1,52 @@
# Copyright (c) 2014 Rackspace Hosting
#
# 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 trove.tests.unittests.datastore.base import TestDatastoreBase
from trove.datastore.models import CapabilityOverride
from trove.datastore.models import Capability
from trove.common.exception import CapabilityNotFound
class TestCapabilities(TestDatastoreBase):
def setUp(self):
super(TestCapabilities, self).setUp()
def tearDown(self):
super(TestCapabilities, self).tearDown()
def test_capability(self):
cap = Capability.load(self.capability_name)
self.assertEqual(cap.name, self.capability_name)
self.assertEqual(cap.description, self.capability_desc)
self.assertEqual(cap.enabled, self.capability_enabled)
def test_ds_capability_create_disabled(self):
self.ds_cap = CapabilityOverride.create(
self.cap1, self.datastore_version.id, enabled=False)
self.assertFalse(self.ds_cap.enabled)
self.ds_cap.delete()
def test_capability_enabled(self):
self.assertTrue(Capability.load(self.capability_name).enabled)
def test_capability_disabled(self):
capability = Capability.load(self.capability_name)
capability.disable()
self.assertFalse(capability.enabled)
self.assertFalse(Capability.load(self.capability_name).enabled)
def test_load_nonexistent_capability(self):
self.assertRaises(CapabilityNotFound, Capability.load, "non-existent")

View File

@ -13,16 +13,19 @@
# License for the specific language governing permissions and limitations
# under the License.
from testtools import TestCase
from trove.datastore import models as datastore_models
from trove.common.exception import DatastoreDefaultDatastoreNotFound
from trove.datastore.models import Datastore
from trove.tests.unittests.datastore.base import TestDatastoreBase
from trove.common import exception
class TestDatastore(TestCase):
def setUp(self):
super(TestDatastore, self).setUp()
class TestDatastore(TestDatastoreBase):
def test_create_failure_with_datastore_default_notfound(self):
self.assertRaises(
DatastoreDefaultDatastoreNotFound,
exception.DatastoreDefaultDatastoreNotFound,
datastore_models.get_datastore_version)
def test_load_datastore(self):
datastore = Datastore.load(self.ds_name)
self.assertEqual(datastore.name, self.ds_name)

View File

@ -0,0 +1,49 @@
# Copyright (c) 2014 Rackspace Hosting
#
# 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 trove.datastore.models import DatastoreVersion
from trove.tests.unittests.datastore.base import TestDatastoreBase
class TestDatastoreVersions(TestDatastoreBase):
def test_load_datastore_version(self):
datastore_version = DatastoreVersion.load(self.datastore,
self.ds_version)
self.assertEqual(datastore_version.name, self.ds_version)
def test_datastore_verison_capabilities(self):
self.datastore_version.capabilities.add(self.cap1, enabled=False)
test_filtered_capabilities = self.capability_name_filter(
self.datastore_version.capabilities)
self.assertEqual(len(test_filtered_capabilities), 3,
'Capabilities the test thinks it has are: %s, '
'Filtered capabilities: %s' %
(self.datastore_version.capabilities,
test_filtered_capabilities))
# Test a fresh reloading of the datastore
self.datastore_version = DatastoreVersion.load(self.datastore,
self.ds_version)
test_filtered_capabilities = self.capability_name_filter(
self.datastore_version.capabilities)
self.assertEqual(len(test_filtered_capabilities), 3,
'Capabilities the test thinks it has are: %s, '
'Filtered capabilities: %s' %
(self.datastore_version.capabilities,
test_filtered_capabilities))
self.assertIn(self.cap2.name, self.datastore_version.capabilities)
self.assertNotIn("non-existent", self.datastore_version.capabilities)
self.assertIn(self.cap1.name, self.datastore_version.capabilities)