Enable Redis database cluster support

The Launch Cluster dialog only supports datastore clusters that
Trove can deploy.  Redis clusters can now be deployed by Trove.
The Launch Cluster dialog requires the following changes for Redis.

- Customized the Launch Cluster dialog fields for Redis
- Added a new Redis specific cluster details panel.
- Modified the Trove api to specify the volume parameter in
cluster_create only if the value is greater than 0.
- Added Redis to the db_capability helper.
- Modified the Trove test data and added unit tests for the
customized fields depending on the datastore selected.

Change-Id: I902b09d9f8a60da6276acb810e2ce616484ea51c
Co-Authored-By: Duk Loi <duk@tesora.com>
Implements: blueprint trove-enable-redis-clustering-support
This commit is contained in:
Matt Van Dijk 2016-01-20 10:52:03 -05:00
parent f94afa4653
commit d09319b786
7 changed files with 234 additions and 44 deletions

View File

@ -58,17 +58,12 @@ def cluster_delete(request, cluster_id):
def cluster_create(request, name, volume, flavor, num_instances,
datastore, datastore_version,
nics=None, root_password=None):
# TODO(dklyle): adding to support trove without volume
# support for now until API supports checking for volume support
if volume > 0:
volume_params = {'size': volume}
else:
volume_params = None
instances = []
for i in range(num_instances):
instance = {}
instance["flavorRef"] = flavor
instance["volume"] = volume_params
if volume > 0:
instance["volume"] = {'size': volume}
if nics:
instance["nics"] = [{"net-id": nics}]
instances.append(instance)

View File

@ -41,7 +41,7 @@ class LaunchForm(forms.SelfHandlingForm):
'class': 'switchable',
'data-slug': 'datastore'
}))
mongodb_flavor = forms.ChoiceField(
flavor = forms.ChoiceField(
label=_("Flavor"),
help_text=_("Size of instance to launch."),
required=False,
@ -96,22 +96,23 @@ class LaunchForm(forms.SelfHandlingForm):
'class': 'switched',
'data-switch-on': 'datastore',
}))
num_instances_per_shards = forms.IntegerField(
label=_("Instances Per Shard"),
num_instances = forms.IntegerField(
label=_("Number of Instances"),
initial=3,
required=False,
help_text=_("Number of instances per shard. (Read only)"),
help_text=_("Number of instances in the cluster."),
widget=forms.TextInput(attrs={
'readonly': 'readonly',
'class': 'switched',
'data-switch-on': 'datastore',
}))
# (name of field variable, label)
mongodb_fields = [
('mongodb_flavor', _('Flavor')),
default_fields = [
('flavor', _('Flavor')),
('num_instances', _('Number of Instances'))
]
mongodb_fields = default_fields + [
('num_shards', _('Number of Shards')),
('num_instances_per_shards', _('Instances Per Shard'))
]
vertica_fields = [
('num_instances_vertica', ('Number of Instances')),
@ -134,18 +135,25 @@ class LaunchForm(forms.SelfHandlingForm):
if datastore_field_value:
datastore = datastore_field_value.split(',')[0]
if db_capability.is_mongodb_datastore(datastore):
if self.data.get("num_shards", 0) < 1:
msg = _("The number of shards must be greater than 1.")
self._errors["num_shards"] = self.error_class([msg])
elif db_capability.is_vertica_datastore(datastore):
if db_capability.is_vertica_datastore(datastore):
if not self.data.get("vertica_flavor", None):
msg = _("The flavor must be specified.")
self._errors["vertica_flavor"] = self.error_class([msg])
if not self.data.get("root_password", None):
msg = _("Password for root user must be specified.")
self._errors["root_password"] = self.error_class([msg])
else:
if not self.data.get("flavor", None):
msg = _("The flavor must be specified.")
self._errors["flavor"] = self.error_class([msg])
if int(self.data.get("num_instances", 0)) < 1:
msg = _("The number of instances must be greater than 1.")
self._errors["num_instances"] = self.error_class([msg])
if db_capability.is_mongodb_datastore(datastore):
if int(self.data.get("num_shards", 0)) < 1:
msg = _("The number of shards must be greater than 1.")
self._errors["num_shards"] = self.error_class([msg])
return self.cleaned_data
@ -166,7 +174,8 @@ class LaunchForm(forms.SelfHandlingForm):
valid_flavor = []
for ds in self.datastores(request):
# TODO(michayu): until capabilities lands
if db_capability.is_mongodb_datastore(ds.name):
if (db_capability.is_mongodb_datastore(ds.name) or
db_capability.is_redis_datastore(ds.name)):
versions = self.datastore_versions(request, ds.name)
for version in versions:
if version.name == "inactive":
@ -174,7 +183,7 @@ class LaunchForm(forms.SelfHandlingForm):
valid_flavor = self.datastore_flavors(request, ds.name,
versions[0].name)
if valid_flavor:
self.fields['mongodb_flavor'].choices = sorted(
self.fields['flavor'].choices = sorted(
[(f.id, "%s" % f.name) for f in valid_flavor])
if db_capability.is_vertica_datastore(ds.name):
@ -224,8 +233,7 @@ class LaunchForm(forms.SelfHandlingForm):
datastores = []
for ds in self.datastores(request):
# TODO(michayu): until capabilities lands
if (db_capability.is_vertica_datastore(ds.name)
or db_capability.is_mongodb_datastore(ds.name)):
if db_capability.is_cluster_capable_datastore(ds.name):
datastores.append(ds)
return datastores
@ -269,6 +277,8 @@ class LaunchForm(forms.SelfHandlingForm):
fields = self.mongodb_fields
elif db_capability.is_vertica_datastore(datastore):
fields = self.vertica_fields
else:
fields = self.default_fields
for field in fields:
attr_key = 'data-datastore-' + selection_text
@ -282,8 +292,8 @@ class LaunchForm(forms.SelfHandlingForm):
datastore = data['datastore'].split('-')[0]
datastore_version = data['datastore'].split('-')[1]
final_flavor = data['mongodb_flavor']
num_instances = data['num_instances_per_shards']
final_flavor = data['flavor']
num_instances = data['num_instances']
root_password = None
if db_capability.is_vertica_datastore(datastore):
final_flavor = data['vertica_flavor']

View File

@ -0,0 +1,19 @@
{% extends "project/database_clusters/_detail_overview.html" %}
{% load i18n sizeformat %}
{% block connection_info %}
<h4>{% trans "Connection Information" %}</h4>
<hr class="header_rule">
<dl class="dl-horizontal">
{% with cluster.ip.0 as ip %}
<dt>{% trans "Host" %}</dt>
{% if not ip %}
<dd>{% trans "Not Assigned" %}</dd>
{% else %}
<dd>{{ ip }}</dd>
<dt>{% trans "Connection Examples" %}</dt>
<dd>redis-cli -h {{ ip }}</dd>
{% endif %} <!-- ends else block -->
{% endwith %}
</dl>
{% endblock %}

View File

@ -110,18 +110,92 @@ class ClustersTests(test.TestCase):
def test_launch_cluster(self):
api.base.is_service_enabled(IsA(http.HttpRequest), 'network')\
.AndReturn(False)
filtered_datastores = self._get_filtered_datastores('mongodb')
trove_api.trove.datastore_flavors(IsA(http.HttpRequest),
'mongodb', '2.6')\
.AndReturn(self.flavors.list())
trove_api.trove.datastore_list(IsA(http.HttpRequest))\
.AndReturn(self.datastores.list())
.AndReturn(filtered_datastores)
trove_api.trove.datastore_version_list(IsA(http.HttpRequest),
IsA(str))\
.AndReturn(self.datastore_versions.list())
.AndReturn(
self._get_filtered_datastore_versions(filtered_datastores))
self.mox.ReplayAll()
res = self.client.get(LAUNCH_URL)
self.assertTemplateUsed(res, 'project/database_clusters/launch.html')
def test_launch_cluster_mongo_fields(self):
datastore = 'mongodb'
fields = self.launch_cluster_fields_setup(datastore, '2.6')
self.assertTrue(self._contains_datastore_in_attribute(
fields['flavor'], datastore))
self.assertTrue(self._contains_datastore_in_attribute(
fields['num_instances'], datastore))
self.assertTrue(self._contains_datastore_in_attribute(
fields['num_shards'], datastore))
self.assertFalse(self._contains_datastore_in_attribute(
fields['root_password'], datastore))
self.assertFalse(self._contains_datastore_in_attribute(
fields['num_instances_vertica'], datastore))
self.assertFalse(self._contains_datastore_in_attribute(
fields['vertica_flavor'], datastore))
def test_launch_cluster_redis_fields(self):
datastore = 'redis'
fields = self.launch_cluster_fields_setup(datastore, '3.0')
self.assertTrue(self._contains_datastore_in_attribute(
fields['flavor'], datastore))
self.assertTrue(self._contains_datastore_in_attribute(
fields['num_instances'], datastore))
self.assertFalse(self._contains_datastore_in_attribute(
fields['num_shards'], datastore))
self.assertFalse(self._contains_datastore_in_attribute(
fields['root_password'], datastore))
self.assertFalse(self._contains_datastore_in_attribute(
fields['num_instances_vertica'], datastore))
self.assertFalse(self._contains_datastore_in_attribute(
fields['vertica_flavor'], datastore))
def test_launch_cluster_vertica_fields(self):
datastore = 'vertica'
fields = self.launch_cluster_fields_setup(datastore, '7.1')
self.assertFalse(self._contains_datastore_in_attribute(
fields['flavor'], datastore))
self.assertFalse(self._contains_datastore_in_attribute(
fields['num_instances'], datastore))
self.assertFalse(self._contains_datastore_in_attribute(
fields['num_shards'], datastore))
self.assertTrue(self._contains_datastore_in_attribute(
fields['root_password'], datastore))
self.assertTrue(self._contains_datastore_in_attribute(
fields['num_instances_vertica'], datastore))
self.assertTrue(self._contains_datastore_in_attribute(
fields['vertica_flavor'], datastore))
@test.create_stubs({trove_api.trove: ('datastore_flavors',
'datastore_list',
'datastore_version_list'),
api.base: ['is_service_enabled']})
def launch_cluster_fields_setup(self, datastore, datastore_version):
api.base.is_service_enabled(IsA(http.HttpRequest), 'network')\
.AndReturn(False)
filtered_datastores = self._get_filtered_datastores(datastore)
trove_api.trove.datastore_flavors(IsA(http.HttpRequest),
datastore, datastore_version)\
.AndReturn(self.flavors.list())
trove_api.trove.datastore_list(IsA(http.HttpRequest))\
.AndReturn(filtered_datastores)
trove_api.trove.datastore_version_list(IsA(http.HttpRequest),
IsA(str))\
.AndReturn(
self._get_filtered_datastore_versions(filtered_datastores))
self.mox.ReplayAll()
res = self.client.get(LAUNCH_URL)
return res.context_data['form'].fields
@test.create_stubs({trove_api.trove: ['datastore_flavors',
'cluster_create',
'datastore_list',
@ -130,13 +204,16 @@ class ClustersTests(test.TestCase):
def test_create_simple_cluster(self):
api.base.is_service_enabled(IsA(http.HttpRequest), 'network')\
.AndReturn(False)
filtered_datastores = self._get_filtered_datastores('mongodb')
trove_api.trove.datastore_flavors(IsA(http.HttpRequest),
'mongodb', '2.6')\
.AndReturn(self.flavors.list())
trove_api.trove.datastore_list(IsA(http.HttpRequest))\
.AndReturn(self.datastores.list())
trove_api.trove.datastore_version_list(IsA(http.HttpRequest), IsA(str))\
.AndReturn(self.datastore_versions.list())
.AndReturn(filtered_datastores)
trove_api.trove.datastore_version_list(IsA(http.HttpRequest),
IsA(str))\
.AndReturn(
self._get_filtered_datastore_versions(filtered_datastores))
cluster_name = u'MyCluster'
cluster_volume = 1
@ -164,7 +241,7 @@ class ClustersTests(test.TestCase):
'num_shards': 1,
'num_instances_per_shards': cluster_instances,
'datastore': cluster_datastore + u'-' + cluster_datastore_version,
'mongodb_flavor': cluster_flavor,
'flavor': cluster_flavor,
'network': cluster_network
}
@ -183,13 +260,16 @@ class ClustersTests(test.TestCase):
.AndReturn(True)
api.neutron.network_list_for_tenant(IsA(http.HttpRequest), '1')\
.AndReturn(self.networks.list())
filtered_datastores = self._get_filtered_datastores('mongodb')
trove_api.trove.datastore_flavors(IsA(http.HttpRequest),
'mongodb', '2.6')\
.AndReturn(self.flavors.list())
trove_api.trove.datastore_list(IsA(http.HttpRequest))\
.AndReturn(self.datastores.list())
trove_api.trove.datastore_version_list(IsA(http.HttpRequest), IsA(str))\
.AndReturn(self.datastore_versions.list())
.AndReturn(filtered_datastores)
trove_api.trove.datastore_version_list(IsA(http.HttpRequest),
IsA(str))\
.AndReturn(
self._get_filtered_datastore_versions(filtered_datastores))
cluster_name = u'MyCluster'
cluster_volume = 1
@ -217,7 +297,7 @@ class ClustersTests(test.TestCase):
'num_shards': 1,
'num_instances_per_shards': cluster_instances,
'datastore': cluster_datastore + u'-' + cluster_datastore_version,
'mongodb_flavor': cluster_flavor,
'flavor': cluster_flavor,
'network': cluster_network
}
@ -233,13 +313,16 @@ class ClustersTests(test.TestCase):
def test_create_simple_cluster_exception(self):
api.neutron.network_list_for_tenant(IsA(http.HttpRequest), '1')\
.AndReturn(self.networks.list())
filtered_datastores = self._get_filtered_datastores('mongodb')
trove_api.trove.datastore_flavors(IsA(http.HttpRequest),
'mongodb', '2.6')\
.AndReturn(self.flavors.list())
trove_api.trove.datastore_list(IsA(http.HttpRequest))\
.AndReturn(self.datastores.list())
trove_api.trove.datastore_version_list(IsA(http.HttpRequest), IsA(str))\
.AndReturn(self.datastore_versions.list())
.AndReturn(filtered_datastores)
trove_api.trove.datastore_version_list(IsA(http.HttpRequest),
IsA(str))\
.AndReturn(
self._get_filtered_datastore_versions(filtered_datastores))
cluster_name = u'MyCluster'
cluster_volume = 1
@ -267,7 +350,7 @@ class ClustersTests(test.TestCase):
'num_shards': 1,
'num_instances_per_shards': cluster_instances,
'datastore': cluster_datastore + u'-' + cluster_datastore_version,
'mongodb_flavor': cluster_flavor,
'flavor': cluster_flavor,
'network': cluster_network
}
@ -292,3 +375,24 @@ class ClustersTests(test.TestCase):
res = self.client.get(details_url)
self.assertTemplateUsed(res, 'horizon/common/_detail.html')
self.assertContains(res, cluster.ip[0])
def _get_filtered_datastores(self, datastore):
filtered_datastore = []
for ds in self.datastores.list():
if datastore in ds.name:
filtered_datastore.append(ds)
return filtered_datastore
def _get_filtered_datastore_versions(self, datastores):
filtered_datastore_versions = []
for ds in datastores:
for dsv in self.datastore_versions.list():
if ds.id == dsv.datastore:
filtered_datastore_versions.append(dsv)
return filtered_datastore_versions
def _contains_datastore_in_attribute(self, field, datastore):
for key, value in field.widget.attrs.iteritems():
if datastore in key:
return True
return False

View File

@ -14,12 +14,28 @@
MONGODB = "mongodb"
REDIS = "redis"
VERTICA = "vertica"
_cluster_capable_datastores = (MONGODB, REDIS, VERTICA)
def is_mongodb_datastore(datastore):
return (datastore is not None) and (MONGODB in datastore.lower())
def is_redis_datastore(datastore):
return (datastore is not None) and (REDIS in datastore.lower())
def is_vertica_datastore(datastore):
return (datastore is not None) and (VERTICA in datastore.lower())
def is_cluster_capable_datastore(datastore):
if datastore is not None:
datastore_lower = datastore.lower()
for ds in _cluster_capable_datastores:
if ds in datastore_lower:
return True
return False

View File

@ -2,7 +2,7 @@
{% load i18n sizeformat %}
{% block connection_info %}
<h4>{% trans "Connection Info" %}</h4>
<h4>{% trans "Connection Information" %}</h4>
<hr class="header_rule">
<dl class="dl-horizontal">
{% with instance.host as host %}
@ -12,8 +12,8 @@
{% else %}
<dd>{{ host }}</dd>
<dt>{% trans "Connection Examples" %}</dt>
<dd>redis-cli -h {{ host }}</dd>
{% endif %} <!-- ends else block -->
<dd>redis-cli -c -h {{ host }}</dd>
{% endif %}
{% endwith %}
</dl>
{% endblock %}

View File

@ -236,6 +236,18 @@ DATASTORE_MONGODB = {
"name": "mongodb"
}
DATASTORE_REDIS = {
"id": "ccb31517-c472-409d-89b4-1a13db6bdd38",
"links": [],
"name": "redis"
}
DATASTORE_VERTICA = {
"id": "ccb31517-c472-409d-89b4-1a13db6bdd39",
"links": [],
"name": "vertica"
}
VERSION_ONE = {
"name": "5.5",
"links": [],
@ -287,6 +299,26 @@ VERSION_MONGODB_2_6 = {
"id": "600a6d52-8347-4e00-8e4c-f4fa9cf96ae9"
}
VERSION_REDIS_3_0 = {
"name": "3.0",
"links": [],
"image": "c7956bb5-920e-4299-b68e-2347d830d938",
"active": 1,
"datastore": "ccb31517-c472-409d-89b4-1a13db6bdd38",
"packages": "3.0",
"id": "600a6d52-8347-4e00-8e4c-f4fa9cf96af0"
}
VERSION_VERTICA_7_1 = {
"name": "7.1",
"links": [],
"image": "c7956bb5-920e-4299-b68e-2347d830d939",
"active": 1,
"datastore": "ccb31517-c472-409d-89b4-1a13db6bdd39",
"packages": "7.1",
"id": "600a6d52-8347-4e00-8e4c-f4fa9cf96af1"
}
def data(TEST):
cluster1 = clusters.Cluster(clusters.Clusters(None),
@ -318,6 +350,16 @@ def data(TEST):
version_mongodb_2_6 = datastores.\
DatastoreVersion(datastores.DatastoreVersions(None),
VERSION_MONGODB_2_6)
datastore_redis = datastores.Datastore(datastores.Datastores(None),
DATASTORE_REDIS)
version_redis_3_0 = datastores.\
DatastoreVersion(datastores.DatastoreVersions(None),
VERSION_REDIS_3_0)
datastore_vertica = datastores.Datastore(datastores.Datastores(None),
DATASTORE_VERTICA)
version_vertica_7_1 = datastores.\
DatastoreVersion(datastores.DatastoreVersions(None),
VERSION_VERTICA_7_1)
TEST.trove_clusters = utils.TestDataContainer()
TEST.trove_clusters.add(cluster1)
@ -338,7 +380,11 @@ def data(TEST):
TEST.datastores = utils.TestDataContainer()
TEST.datastores.add(datastore1)
TEST.datastores.add(datastore_mongodb)
TEST.datastores.add(datastore_redis)
TEST.datastores.add(datastore_vertica)
TEST.database_flavors.add(flavor1, flavor2, flavor3)
TEST.datastore_versions = utils.TestDataContainer()
TEST.datastore_versions.add(version_vertica_7_1)
TEST.datastore_versions.add(version_redis_3_0)
TEST.datastore_versions.add(version_mongodb_2_6)
TEST.datastore_versions.add(version1)