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
This commit is contained in:
Duk Loi 2016-02-24 16:36:10 -05:00 committed by Ali Adil
parent acb3d96ef0
commit e1c44b2092
2 changed files with 144 additions and 54 deletions

View File

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

View File

@ -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, "