Merge "Adds the foundation for datastore capabilities"

This commit is contained in:
Jenkins 2014-06-05 10:46:45 +00:00 committed by Gerrit Code Review
commit 4e5693d9f7
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
self._datastore_name = None
@ -156,6 +434,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)