Merge "Add support for instance datastore-flavors"

This commit is contained in:
Jenkins 2016-09-09 15:11:10 +00:00 committed by Gerrit Code Review
commit 77fcb993ae
2 changed files with 144 additions and 54 deletions

View File

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