From e1c44b209218374893b1efdfdbb7376a0332d72b Mon Sep 17 00:00:00 2001 From: Duk Loi Date: Wed, 24 Feb 2016 16:36:10 -0500 Subject: [PATCH] Add support for instance datastore-flavors Update the launch wizard to use datastore-flavors instead of flavor-list. When selecting a datastore the flavor list will change to display only flavors for the selected datastore. If the datastore flavor is not configured then the default flavor list will be used. Updated unit tests to use the datastore flavors. Change-Id: I4bf0033a3e6b2a117f93db2c0c16a6d02642f368 Closes-Bug: #1549367 --- trove_dashboard/content/databases/tests.py | 73 +++++++--- .../databases/workflows/create_instance.py | 125 +++++++++++++----- 2 files changed, 144 insertions(+), 54 deletions(-) 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, "