From f7d0b9f771bb9186cae6e6eeb2b9da3730419e6f Mon Sep 17 00:00:00 2001 From: Duk Loi Date: Mon, 7 Mar 2016 14:35:21 -0500 Subject: [PATCH] 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 --- .../content/database_clusters/forms.py | 136 +++++++++++------- .../content/database_clusters/tests.py | 91 +++++++----- 2 files changed, 136 insertions(+), 91 deletions(-) diff --git a/trove_dashboard/content/database_clusters/forms.py b/trove_dashboard/content/database_clusters/forms.py index c2742d7..b4bb224 100644 --- a/trove_dashboard/content/database_clusters/forms.py +++ b/trove_dashboard/content/database_clusters/forms.py @@ -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, diff --git a/trove_dashboard/content/database_clusters/tests.py b/trove_dashboard/content/database_clusters/tests.py index 72e1231..da57539 100644 --- a/trove_dashboard/content/database_clusters/tests.py +++ b/trove_dashboard/content/database_clusters/tests.py @@ -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))