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))