diff --git a/trove_dashboard/content/databases/tests.py b/trove_dashboard/content/databases/tests.py index 6537055..0fcbf26 100644 --- a/trove_dashboard/content/databases/tests.py +++ b/trove_dashboard/content/databases/tests.py @@ -13,6 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. +import binascii import logging import django @@ -128,7 +129,7 @@ class DatabaseTests(test.TestCase): self.assertMessageCount(res, error=1) @test.create_stubs({ - api.trove: ('flavor_list', 'backup_list', + api.trove: ('datastore_flavors', 'backup_list', 'datastore_list', 'datastore_version_list', 'instance_list'), dash_api.cinder: ('volume_type_list',), @@ -137,8 +138,10 @@ class DatabaseTests(test.TestCase): }) def test_launch_instance(self): policy.check((), IsA(http.HttpRequest)).MultipleTimes().AndReturn(True) - api.trove.flavor_list(IsA(http.HttpRequest)).AndReturn( - self.flavors.list()) + api.trove.datastore_flavors(IsA(http.HttpRequest), + IsA(six.string_types), + IsA(six.string_types)).\ + MultipleTimes().AndReturn(self.flavors.list()) api.trove.backup_list(IsA(http.HttpRequest)).AndReturn( self.database_backups.list()) api.trove.instance_list(IsA(http.HttpRequest)).AndReturn( @@ -197,7 +200,7 @@ class DatabaseTests(test.TestCase): log.setLevel(level) @test.create_stubs({ - api.trove: ('flavor_list', 'backup_list', 'instance_create', + api.trove: ('datastore_flavors', 'backup_list', 'instance_create', 'datastore_list', 'datastore_version_list', 'instance_list'), dash_api.cinder: ('volume_type_list',), @@ -206,8 +209,10 @@ class DatabaseTests(test.TestCase): }) def test_create_simple_instance(self): policy.check((), IsA(http.HttpRequest)).MultipleTimes().AndReturn(True) - api.trove.flavor_list(IsA(http.HttpRequest)).AndReturn( - self.flavors.list()) + api.trove.datastore_flavors(IsA(http.HttpRequest), + IsA(six.string_types), + IsA(six.string_types)).\ + MultipleTimes().AndReturn(self.flavors.list()) api.trove.backup_list(IsA(http.HttpRequest)).AndReturn( self.database_backups.list()) @@ -236,6 +241,10 @@ class DatabaseTests(test.TestCase): nics = [{"net-id": self.networks.first().id, "v4-fixed-ip": ''}] + datastore = 'mysql' + datastore_version = '5.5' + field_name = self._build_flavor_widget_name(datastore, + datastore_version) # Actual create database call api.trove.instance_create( IsA(http.HttpRequest), @@ -243,8 +252,8 @@ class DatabaseTests(test.TestCase): IsA(int), IsA(six.text_type), databases=None, - datastore=IsA(six.text_type), - datastore_version=IsA(six.text_type), + datastore=datastore, + datastore_version=datastore_version, restore_point=None, replica_of=None, users=None, @@ -257,8 +266,9 @@ class DatabaseTests(test.TestCase): 'name': "MyDB", 'volume': '1', 'flavor': 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', + 'datastore': field_name, + field_name: 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'network': self.networks.first().id, - 'datastore': 'mysql,5.5', 'volume_type': 'no_type' } @@ -266,7 +276,7 @@ class DatabaseTests(test.TestCase): self.assertRedirectsNoFollow(res, INDEX_URL) @test.create_stubs({ - api.trove: ('flavor_list', 'backup_list', 'instance_create', + api.trove: ('datastore_flavors', 'backup_list', 'instance_create', 'datastore_list', 'datastore_version_list', 'instance_list'), dash_api.cinder: ('volume_type_list',), @@ -276,8 +286,10 @@ class DatabaseTests(test.TestCase): def test_create_simple_instance_exception(self): policy.check((), IsA(http.HttpRequest)).MultipleTimes().AndReturn(True) trove_exception = self.exceptions.nova - api.trove.flavor_list(IsA(http.HttpRequest)).AndReturn( - self.flavors.list()) + api.trove.datastore_flavors(IsA(http.HttpRequest), + IsA(six.string_types), + IsA(six.string_types)).\ + MultipleTimes().AndReturn(self.flavors.list()) api.trove.backup_list(IsA(http.HttpRequest)).AndReturn( self.database_backups.list()) @@ -306,6 +318,10 @@ class DatabaseTests(test.TestCase): nics = [{"net-id": self.networks.first().id, "v4-fixed-ip": ''}] + datastore = 'mysql' + datastore_version = '5.5' + field_name = self._build_flavor_widget_name(datastore, + datastore_version) # Actual create database call api.trove.instance_create( IsA(http.HttpRequest), @@ -313,8 +329,8 @@ class DatabaseTests(test.TestCase): IsA(int), IsA(six.text_type), databases=None, - datastore=IsA(six.text_type), - datastore_version=IsA(six.text_type), + datastore=datastore, + datastore_version=datastore_version, restore_point=None, replica_of=None, users=None, @@ -327,8 +343,9 @@ class DatabaseTests(test.TestCase): 'name': "MyDB", 'volume': '1', 'flavor': 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', + 'datastore': field_name, + field_name: 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'network': self.networks.first().id, - 'datastore': 'mysql,5.5', 'volume_type': 'no_type' } @@ -964,7 +981,7 @@ class DatabaseTests(test.TestCase): self.assertRedirectsNoFollow(res, INDEX_URL) @test.create_stubs({ - api.trove: ('flavor_list', 'backup_list', 'instance_create', + api.trove: ('datastore_flavors', 'backup_list', 'instance_create', 'datastore_list', 'datastore_version_list', 'instance_list_all', 'instance_get'), dash_api.cinder: ('volume_type_list',), @@ -973,8 +990,10 @@ class DatabaseTests(test.TestCase): }) def test_create_replica_instance(self): policy.check((), IsA(http.HttpRequest)).MultipleTimes().AndReturn(True) - api.trove.flavor_list(IsA(http.HttpRequest)).AndReturn( - self.flavors.list()) + api.trove.datastore_flavors(IsA(http.HttpRequest), + IsA(six.string_types), + IsA(six.string_types)).\ + MultipleTimes().AndReturn(self.flavors.list()) api.trove.backup_list(IsA(http.HttpRequest)).AndReturn( self.database_backups.list()) @@ -1005,6 +1024,10 @@ class DatabaseTests(test.TestCase): api.trove.instance_get(IsA(http.HttpRequest), IsA(six.text_type))\ .AndReturn(self.databases.first()) + datastore = 'mysql' + datastore_version = '5.5' + field_name = self._build_flavor_widget_name(datastore, + datastore_version) # Actual create database call api.trove.instance_create( IsA(http.HttpRequest), @@ -1012,8 +1035,8 @@ class DatabaseTests(test.TestCase): IsA(int), IsA(six.text_type), databases=None, - datastore=IsA(six.text_type), - datastore_version=IsA(six.text_type), + datastore=datastore, + datastore_version=datastore_version, restore_point=None, replica_of=self.databases.first().id, users=None, @@ -1026,8 +1049,9 @@ class DatabaseTests(test.TestCase): 'name': "MyDB", 'volume': '1', 'flavor': 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', + 'datastore': field_name, + field_name: 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'network': self.networks.first().id, - 'datastore': 'mysql,5.5', 'initial_state': 'master', 'master': self.databases.first().id, 'replica_count': 2, @@ -1159,3 +1183,10 @@ class DatabaseTests(test.TestCase): advanced_page = create_instance.AdvancedAction(request, None) choices = advanced_page.populate_master_choices(request, None) self.assertTrue(len(choices) == len(self.databases.list()) + 1) + + 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)) diff --git a/trove_dashboard/content/databases/workflows/create_instance.py b/trove_dashboard/content/databases/workflows/create_instance.py index f2c9038..1508a1c 100644 --- a/trove_dashboard/content/databases/workflows/create_instance.py +++ b/trove_dashboard/content/databases/workflows/create_instance.py @@ -12,6 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. +import binascii import logging from django.conf import settings @@ -34,10 +35,15 @@ from trove_dashboard import api LOG = logging.getLogger(__name__) +def parse_datastore_and_version_text(datastore_and_version): + if datastore_and_version: + datastore, datastore_version = datastore_and_version.split('-', 1) + return datastore.strip(), datastore_version.strip() + return None, None + + class SetInstanceDetailsAction(workflows.Action): name = forms.CharField(max_length=80, label=_("Instance Name")) - flavor = forms.ChoiceField(label=_("Flavor"), - help_text=_("Size of image to launch.")) volume = forms.IntegerField(label=_("Volume Size"), min_value=0, initial=1, @@ -46,24 +52,53 @@ class SetInstanceDetailsAction(workflows.Action): label=_("Volume Type"), required=False, help_text=_("Applicable only if the volume size is specified.")) - datastore = forms.ChoiceField(label=_("Datastore"), - help_text=_( - "Type and version of datastore.")) + datastore = forms.ChoiceField( + label=_("Datastore"), + help_text=_("Type and version of datastore."), + widget=forms.Select(attrs={ + 'class': 'switchable', + 'data-slug': 'datastore' + })) class Meta(object): name = _("Details") help_text_template = "project/databases/_launch_details_help.html" def clean(self): - if self.data.get("datastore", None) == "select_datastore_type_version": + datastore_and_version = self.data.get("datastore", None) + if not datastore_and_version: msg = _("You must select a datastore type and version.") self._errors["datastore"] = self.error_class([msg]) + else: + datastore, datastore_version = parse_datastore_and_version_text( + binascii.unhexlify(datastore_and_version)) + field_name = self._build_flavor_field_name(datastore, + datastore_version) + flavor = self.data.get(field_name, None) + if not flavor: + msg = _("You must select a flavor.") + self._errors[field_name] = self.error_class([msg]) + return self.cleaned_data + def handle(self, request, context): + datastore_and_version = context["datastore"] + if datastore_and_version: + datastore, datastore_version = parse_datastore_and_version_text( + binascii.unhexlify(context["datastore"])) + field_name = self._build_flavor_field_name(datastore, + datastore_version) + flavor = self.data[field_name] + if flavor: + context["flavor"] = flavor + return context + return None + @memoized.memoized_method - def flavors(self, request): + def datastore_flavors(self, request, datastore_name, datastore_version): try: - return api.trove.flavor_list(request) + return api.trove.datastore_flavors( + request, datastore_name, datastore_version) except Exception: LOG.exception("Exception while obtaining flavors list") redirect = reverse("horizon:project:databases:index") @@ -71,12 +106,6 @@ class SetInstanceDetailsAction(workflows.Action): _('Unable to obtain flavors.'), redirect=redirect) - def populate_flavor_choices(self, request, context): - flavors = self.flavors(request) - if flavors: - return instance_utils.sort_flavor_list(request, flavors) - return [] - @memoized.memoized_method def populate_volume_type_choices(self, request, context): try: @@ -106,36 +135,66 @@ class SetInstanceDetailsAction(workflows.Action): def populate_datastore_choices(self, request, context): choices = () - set_initial = False datastores = self.datastores(request) if datastores is not None: - num_datastores_with_one_version = 0 for ds in datastores: versions = self.datastore_versions(request, ds.name) - if not set_initial: - if len(versions) >= 2: - set_initial = True - elif len(versions) == 1: - num_datastores_with_one_version += 1 - if num_datastores_with_one_version > 1: - set_initial = True if versions: # only add to choices if datastore has at least one version version_choices = () for v in versions: if hasattr(v, 'active') and not v.active: continue + 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 + - ((ds.name + ',' + v.name, v.name),)) - datastore_choices = (ds.name, version_choices) - choices = choices + (datastore_choices,) - if set_initial: - # prepend choice to force user to choose - initial = (('select_datastore_type_version', - _('Select datastore type and version'))) - choices = (initial,) + choices + ((widget_text, selection_text),)) + self._add_datastore_flavor_field(request, + ds.name, + v.name) + choices = choices + version_choices 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_name = self._build_flavor_field_name(datastore, + datastore_version) + self.fields[field_name] = 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: + self.fields[field_name].choices = instance_utils.sort_flavor_list( + request, valid_flavors) + + 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 _build_flavor_field_name(self, datastore, datastore_version): + return self._build_widget_field_name(datastore, + datastore_version) + TROVE_ADD_USER_PERMS = getattr(settings, 'TROVE_ADD_USER_PERMS', []) TROVE_ADD_DATABASE_PERMS = getattr(settings, 'TROVE_ADD_DATABASE_PERMS', []) @@ -387,8 +446,8 @@ class LaunchInstance(workflows.Workflow): def handle(self, request, context): try: - datastore = self.context['datastore'].split(',')[0] - datastore_version = self.context['datastore'].split(',')[1] + datastore, datastore_version = parse_datastore_and_version_text( + binascii.unhexlify(self.context['datastore'])) LOG.info("Launching database instance with parameters " "{name=%s, volume=%s, volume_type=%s, flavor=%s, " "datastore=%s, datastore_version=%s, "