Add support for uppercase fieldname to cluster

The datastore flavor field was not properly implemented because
not every datastore flavor had it's own flavor pulldown.
Properly implemented a datastore pulldown for every flavor that
is only visible when the corresponding datastore value is selected.

Also added code to protect against uppercase letters in the
datastore or datastore version name.

Change-Id: I7a8083733a7d613c56652f3884d2682bb54379b3
Closes-Bug: #1614650
This commit is contained in:
Duk Loi 2016-03-07 14:35:21 -05:00 committed by Amrith Kumar
parent 76833761ed
commit f7d0b9f771
2 changed files with 136 additions and 91 deletions

View File

@ -13,6 +13,8 @@
# License for the specific language governing permissions and limitations
# under the License.
import binascii
import collections
import logging
import uuid
@ -26,10 +28,14 @@ from horizon import messages
from horizon.utils import memoized
from openstack_dashboard import api
from openstack_dashboard.dashboards.project.instances \
import utils as instance_utils
from trove_dashboard import api as trove_api
from trove_dashboard.content.database_clusters \
import cluster_manager
from trove_dashboard.content.databases import db_capability
from trove_dashboard.content.databases.workflows \
import create_instance
LOG = logging.getLogger(__name__)
@ -44,22 +50,6 @@ class LaunchForm(forms.SelfHandlingForm):
'class': 'switchable',
'data-slug': 'datastore'
}))
flavor = forms.ChoiceField(
label=_("Flavor"),
help_text=_("Size of instance to launch."),
required=False,
widget=forms.Select(attrs={
'class': 'switched',
'data-switch-on': 'datastore',
}))
vertica_flavor = forms.ChoiceField(
label=_("Flavor"),
help_text=_("Size of instance to launch."),
required=False,
widget=forms.Select(attrs={
'class': 'switched',
'data-switch-on': 'datastore',
}))
network = forms.ChoiceField(
label=_("Network"),
help_text=_("Network attached to instance."),
@ -120,7 +110,6 @@ class LaunchForm(forms.SelfHandlingForm):
# (name of field variable, label)
default_fields = [
('flavor', _('Flavor')),
('num_instances', _('Number of Instances'))
]
mongodb_fields = default_fields + [
@ -128,7 +117,6 @@ class LaunchForm(forms.SelfHandlingForm):
]
vertica_fields = [
('num_instances_vertica', ('Number of Instances')),
('vertica_flavor', _('Flavor')),
('root_password', _('Root Password')),
]
@ -137,27 +125,27 @@ class LaunchForm(forms.SelfHandlingForm):
self.fields['datastore'].choices = self.populate_datastore_choices(
request)
self.populate_flavor_choices(request)
self.fields['network'].choices = self.populate_network_choices(
request)
def clean(self):
datastore_field_value = self.data.get("datastore", None)
if datastore_field_value:
datastore = datastore_field_value.split(',')[0]
datastore, datastore_version = (
create_instance.parse_datastore_and_version_text(
binascii.unhexlify(datastore_field_value)))
flavor_field_name = self._build_widget_field_name(
datastore, datastore_version)
if not self.data.get(flavor_field_name, None):
msg = _("The flavor must be specified.")
self._errors[flavor_field_name] = self.error_class([msg])
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])
@ -185,24 +173,6 @@ class LaunchForm(forms.SelfHandlingForm):
_('Unable to obtain flavors.'),
redirect=redirect)
def populate_flavor_choices(self, request):
valid_flavor = []
for ds in self.datastores(request):
# TODO(michayu): until capabilities lands
field_name = 'flavor'
if db_capability.is_vertica_datastore(ds.name):
field_name = 'vertica_flavor'
versions = self.datastore_versions(request, ds.name)
for version in versions:
if hasattr(version, 'active') and not version.active:
continue
valid_flavor = self.datastore_flavors(request, ds.name,
versions[0].name)
if valid_flavor:
self.fields[field_name].choices = sorted(
[(f.id, "%s" % f.name) for f in valid_flavor])
@memoized.memoized_method
def populate_network_choices(self, request):
network_list = []
@ -259,6 +229,7 @@ class LaunchForm(forms.SelfHandlingForm):
choices = ()
datastores = self.filter_cluster_datastores(request)
if datastores is not None:
datastore_flavor_fields = {}
for ds in datastores:
versions = self.datastore_versions(request, ds.name)
if versions:
@ -267,18 +238,74 @@ class LaunchForm(forms.SelfHandlingForm):
for v in versions:
if hasattr(v, 'active') and not v.active:
continue
selection_text = ds.name + ' - ' + v.name
widget_text = ds.name + '-' + v.name
selection_text = self._build_datastore_display_text(
ds.name, v.name)
widget_text = self._build_widget_field_name(
ds.name, v.name)
version_choices = (version_choices +
((widget_text, selection_text),))
k, v = self._add_datastore_flavor_field(request,
ds.name,
v.name)
datastore_flavor_fields[k] = v
self._add_attr_to_optional_fields(ds.name,
widget_text)
choices = choices + version_choices
self._insert_datastore_version_fields(datastore_flavor_fields)
return choices
def _add_datastore_flavor_field(self,
request,
datastore,
datastore_version):
name = self._build_widget_field_name(datastore, datastore_version)
attr_key = 'data-datastore-' + name
field = forms.ChoiceField(
label=_("Flavor"),
help_text=_("Size of image to launch."),
required=False,
widget=forms.Select(attrs={
'class': 'switched',
'data-switch-on': 'datastore',
attr_key: _("Flavor")
}))
valid_flavors = self.datastore_flavors(request,
datastore,
datastore_version)
if valid_flavors:
field.choices = instance_utils.sort_flavor_list(
request, valid_flavors)
return name, field
def _build_datastore_display_text(self, datastore, datastore_version):
return datastore + ' - ' + datastore_version
def _build_widget_field_name(self, datastore, datastore_version):
# Since the fieldnames cannot contain an uppercase character
# we generate a hex encoded string representation of the
# datastore and version as the fieldname
return binascii.hexlify(
self._build_datastore_display_text(datastore, datastore_version))
def _insert_datastore_version_fields(self, datastore_flavor_fields):
fields_to_restore_at_the_end = collections.OrderedDict()
while True:
k, v = self.fields.popitem()
if k == 'datastore':
self.fields[k] = v
break
else:
fields_to_restore_at_the_end[k] = v
for k, v in datastore_flavor_fields.iteritems():
self.fields[k] = v
for k in reversed(fields_to_restore_at_the_end.keys()):
self.fields[k] = fields_to_restore_at_the_end[k]
def _add_attr_to_optional_fields(self, datastore, selection_text):
fields = []
if db_capability.is_mongodb_datastore(datastore):
fields = self.mongodb_fields
elif db_capability.is_vertica_datastore(datastore):
@ -301,26 +328,29 @@ class LaunchForm(forms.SelfHandlingForm):
@sensitive_variables('data')
def handle(self, request, data):
try:
datastore, datastore_version = data['datastore'].split('-', 1)
datastore, datastore_version = (
create_instance.parse_datastore_and_version_text(
binascii.unhexlify(data['datastore'])))
final_flavor = data['flavor']
flavor_field_name = self._build_widget_field_name(
datastore, datastore_version)
flavor = data[flavor_field_name]
num_instances = data['num_instances']
root_password = None
if db_capability.is_vertica_datastore(datastore):
final_flavor = data['vertica_flavor']
root_password = data['root_password']
num_instances = data['num_instances_vertica']
LOG.info("Launching cluster with parameters "
"{name=%s, volume=%s, flavor=%s, "
"datastore=%s, datastore_version=%s",
"locality=%s",
data['name'], data['volume'], final_flavor,
data['name'], data['volume'], flavor,
datastore, datastore_version, self._get_locality(data))
trove_api.trove.cluster_create(request,
data['name'],
data['volume'],
final_flavor,
flavor,
num_instances,
datastore=datastore,
datastore_version=datastore_version,

View File

@ -14,6 +14,7 @@
# License for the specific language governing permissions and limitations
# under the License.
import binascii
import logging
from django.core.urlresolvers import reverse
@ -130,54 +131,60 @@ class ClustersTests(test.TestCase):
def test_launch_cluster_mongo_fields(self):
datastore = 'mongodb'
fields = self.launch_cluster_fields_setup(datastore, '2.6')
datastore_version = '2.6'
fields = self.launch_cluster_fields_setup(datastore,
datastore_version)
field_name = self._build_flavor_widget_name(datastore,
datastore_version)
self.assertTrue(self._contains_datastore_in_attribute(
fields['flavor'], datastore))
fields[field_name], field_name))
self.assertTrue(self._contains_datastore_in_attribute(
fields['num_instances'], datastore))
fields['num_instances'], field_name))
self.assertTrue(self._contains_datastore_in_attribute(
fields['num_shards'], datastore))
fields['num_shards'], field_name))
self.assertFalse(self._contains_datastore_in_attribute(
fields['root_password'], datastore))
fields['root_password'], field_name))
self.assertFalse(self._contains_datastore_in_attribute(
fields['num_instances_vertica'], datastore))
self.assertFalse(self._contains_datastore_in_attribute(
fields['vertica_flavor'], datastore))
fields['num_instances_vertica'], field_name))
def test_launch_cluster_redis_fields(self):
datastore = 'redis'
fields = self.launch_cluster_fields_setup(datastore, '3.0')
datastore_version = '3.0'
fields = self.launch_cluster_fields_setup(datastore,
datastore_version)
field_name = self._build_flavor_widget_name(datastore,
datastore_version)
self.assertTrue(self._contains_datastore_in_attribute(
fields['flavor'], datastore))
fields[field_name], field_name))
self.assertTrue(self._contains_datastore_in_attribute(
fields['num_instances'], datastore))
fields['num_instances'], field_name))
self.assertFalse(self._contains_datastore_in_attribute(
fields['num_shards'], datastore))
fields['num_shards'], field_name))
self.assertFalse(self._contains_datastore_in_attribute(
fields['root_password'], datastore))
fields['root_password'], field_name))
self.assertFalse(self._contains_datastore_in_attribute(
fields['num_instances_vertica'], datastore))
self.assertFalse(self._contains_datastore_in_attribute(
fields['vertica_flavor'], datastore))
fields['num_instances_vertica'], field_name))
def test_launch_cluster_vertica_fields(self):
datastore = 'vertica'
fields = self.launch_cluster_fields_setup(datastore, '7.1')
datastore_version = '7.1'
fields = self.launch_cluster_fields_setup(datastore,
datastore_version)
field_name = self._build_flavor_widget_name(datastore,
datastore_version)
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))
fields[field_name], field_name))
self.assertFalse(self._contains_datastore_in_attribute(
fields['num_instances'], field_name))
self.assertFalse(self._contains_datastore_in_attribute(
fields['num_shards'], field_name))
self.assertTrue(self._contains_datastore_in_attribute(
fields['num_instances_vertica'], datastore))
fields['root_password'], field_name))
self.assertTrue(self._contains_datastore_in_attribute(
fields['vertica_flavor'], datastore))
fields['num_instances_vertica'], field_name))
@test.create_stubs({trove_api.trove: ('datastore_flavors',
'datastore_list',
@ -238,16 +245,16 @@ class ClustersTests(test.TestCase):
root_password=None,
locality=None).AndReturn(self.trove_clusters.first())
field_name = self._build_flavor_widget_name(cluster_datastore,
cluster_datastore_version)
self.mox.ReplayAll()
post = {
'name': cluster_name,
'volume': cluster_volume,
'num_instances': cluster_instances,
'num_shards': 1,
'num_instances_per_shards': cluster_instances,
'datastore': cluster_datastore + u'-' + cluster_datastore_version,
'flavor': cluster_flavor,
'network': cluster_network
'datastore': field_name,
field_name: 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
}
res = self.client.post(LAUNCH_URL, post)
@ -295,16 +302,17 @@ class ClustersTests(test.TestCase):
root_password=None,
locality=None).AndReturn(self.trove_clusters.first())
field_name = self._build_flavor_widget_name(cluster_datastore,
cluster_datastore_version)
self.mox.ReplayAll()
post = {
'name': cluster_name,
'volume': cluster_volume,
'num_instances': cluster_instances,
'num_shards': 1,
'num_instances_per_shards': cluster_instances,
'datastore': cluster_datastore + u'-' + cluster_datastore_version,
'flavor': cluster_flavor,
'network': cluster_network
'datastore': field_name,
field_name: 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
'network': cluster_network,
}
res = self.client.post(LAUNCH_URL, post)
@ -349,16 +357,16 @@ class ClustersTests(test.TestCase):
root_password=None,
locality=None).AndReturn(self.trove_clusters.first())
field_name = self._build_flavor_widget_name(cluster_datastore,
cluster_datastore_version)
self.mox.ReplayAll()
post = {
'name': cluster_name,
'volume': cluster_volume,
'num_instances': cluster_instances,
'num_shards': 1,
'num_instances_per_shards': cluster_instances,
'datastore': cluster_datastore + u'-' + cluster_datastore_version,
'flavor': cluster_flavor,
'network': cluster_network
'datastore': field_name,
field_name: 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
}
res = self.client.post(LAUNCH_URL, post)
@ -649,3 +657,10 @@ class ClustersTests(test.TestCase):
if datastore in key:
return True
return False
def _build_datastore_display_text(self, datastore, datastore_version):
return datastore + ' - ' + datastore_version
def _build_flavor_widget_name(self, datastore, datastore_version):
return binascii.hexlify(self._build_datastore_display_text(
datastore, datastore_version))