summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDuk Loi <duk@tesora.com>2016-03-07 14:35:21 -0500
committerAmrith Kumar <amrith.kumar@gmail.com>2017-06-16 01:37:36 +0000
commitf7d0b9f771bb9186cae6e6eeb2b9da3730419e6f (patch)
treec5e4ffe800bd0381fc6cac20ea091d5d88842ce6
parent76833761edeb310f136102a4640dba491d3ae2bb (diff)
Add support for uppercase fieldname to cluster
The datastore flavor field was not properly implemented because not every datastore flavor had it's own flavor pulldown. Properly implemented a datastore pulldown for every flavor that is only visible when the corresponding datastore value is selected. Also added code to protect against uppercase letters in the datastore or datastore version name. Change-Id: I7a8083733a7d613c56652f3884d2682bb54379b3 Closes-Bug: #1614650
Notes
Notes (review): Code-Review+2: Amrith Kumar <amrith.kumar@gmail.com> Workflow+1: Amrith Kumar <amrith.kumar@gmail.com> Verified+2: Jenkins Submitted-by: Jenkins Submitted-at: Fri, 16 Jun 2017 10:52:23 +0000 Reviewed-on: https://review.openstack.org/409721 Project: openstack/trove-dashboard Branch: refs/heads/master
-rw-r--r--trove_dashboard/content/database_clusters/forms.py136
-rw-r--r--trove_dashboard/content/database_clusters/tests.py89
2 files changed, 135 insertions, 90 deletions
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 @@
13# License for the specific language governing permissions and limitations 13# License for the specific language governing permissions and limitations
14# under the License. 14# under the License.
15 15
16import binascii
17import collections
16import logging 18import logging
17import uuid 19import uuid
18 20
@@ -26,10 +28,14 @@ from horizon import messages
26from horizon.utils import memoized 28from horizon.utils import memoized
27from openstack_dashboard import api 29from openstack_dashboard import api
28 30
31from openstack_dashboard.dashboards.project.instances \
32 import utils as instance_utils
29from trove_dashboard import api as trove_api 33from trove_dashboard import api as trove_api
30from trove_dashboard.content.database_clusters \ 34from trove_dashboard.content.database_clusters \
31 import cluster_manager 35 import cluster_manager
32from trove_dashboard.content.databases import db_capability 36from trove_dashboard.content.databases import db_capability
37from trove_dashboard.content.databases.workflows \
38 import create_instance
33 39
34LOG = logging.getLogger(__name__) 40LOG = logging.getLogger(__name__)
35 41
@@ -44,22 +50,6 @@ class LaunchForm(forms.SelfHandlingForm):
44 'class': 'switchable', 50 'class': 'switchable',
45 'data-slug': 'datastore' 51 'data-slug': 'datastore'
46 })) 52 }))
47 flavor = forms.ChoiceField(
48 label=_("Flavor"),
49 help_text=_("Size of instance to launch."),
50 required=False,
51 widget=forms.Select(attrs={
52 'class': 'switched',
53 'data-switch-on': 'datastore',
54 }))
55 vertica_flavor = forms.ChoiceField(
56 label=_("Flavor"),
57 help_text=_("Size of instance to launch."),
58 required=False,
59 widget=forms.Select(attrs={
60 'class': 'switched',
61 'data-switch-on': 'datastore',
62 }))
63 network = forms.ChoiceField( 53 network = forms.ChoiceField(
64 label=_("Network"), 54 label=_("Network"),
65 help_text=_("Network attached to instance."), 55 help_text=_("Network attached to instance."),
@@ -120,7 +110,6 @@ class LaunchForm(forms.SelfHandlingForm):
120 110
121 # (name of field variable, label) 111 # (name of field variable, label)
122 default_fields = [ 112 default_fields = [
123 ('flavor', _('Flavor')),
124 ('num_instances', _('Number of Instances')) 113 ('num_instances', _('Number of Instances'))
125 ] 114 ]
126 mongodb_fields = default_fields + [ 115 mongodb_fields = default_fields + [
@@ -128,7 +117,6 @@ class LaunchForm(forms.SelfHandlingForm):
128 ] 117 ]
129 vertica_fields = [ 118 vertica_fields = [
130 ('num_instances_vertica', ('Number of Instances')), 119 ('num_instances_vertica', ('Number of Instances')),
131 ('vertica_flavor', _('Flavor')),
132 ('root_password', _('Root Password')), 120 ('root_password', _('Root Password')),
133 ] 121 ]
134 122
@@ -137,27 +125,27 @@ class LaunchForm(forms.SelfHandlingForm):
137 125
138 self.fields['datastore'].choices = self.populate_datastore_choices( 126 self.fields['datastore'].choices = self.populate_datastore_choices(
139 request) 127 request)
140 self.populate_flavor_choices(request)
141
142 self.fields['network'].choices = self.populate_network_choices( 128 self.fields['network'].choices = self.populate_network_choices(
143 request) 129 request)
144 130
145 def clean(self): 131 def clean(self):
146 datastore_field_value = self.data.get("datastore", None) 132 datastore_field_value = self.data.get("datastore", None)
147 if datastore_field_value: 133 if datastore_field_value:
148 datastore = datastore_field_value.split(',')[0] 134 datastore, datastore_version = (
135 create_instance.parse_datastore_and_version_text(
136 binascii.unhexlify(datastore_field_value)))
137
138 flavor_field_name = self._build_widget_field_name(
139 datastore, datastore_version)
140 if not self.data.get(flavor_field_name, None):
141 msg = _("The flavor must be specified.")
142 self._errors[flavor_field_name] = self.error_class([msg])
149 143
150 if db_capability.is_vertica_datastore(datastore): 144 if db_capability.is_vertica_datastore(datastore):
151 if not self.data.get("vertica_flavor", None):
152 msg = _("The flavor must be specified.")
153 self._errors["vertica_flavor"] = self.error_class([msg])
154 if not self.data.get("root_password", None): 145 if not self.data.get("root_password", None):
155 msg = _("Password for root user must be specified.") 146 msg = _("Password for root user must be specified.")
156 self._errors["root_password"] = self.error_class([msg]) 147 self._errors["root_password"] = self.error_class([msg])
157 else: 148 else:
158 if not self.data.get("flavor", None):
159 msg = _("The flavor must be specified.")
160 self._errors["flavor"] = self.error_class([msg])
161 if int(self.data.get("num_instances", 0)) < 1: 149 if int(self.data.get("num_instances", 0)) < 1:
162 msg = _("The number of instances must be greater than 1.") 150 msg = _("The number of instances must be greater than 1.")
163 self._errors["num_instances"] = self.error_class([msg]) 151 self._errors["num_instances"] = self.error_class([msg])
@@ -185,24 +173,6 @@ class LaunchForm(forms.SelfHandlingForm):
185 _('Unable to obtain flavors.'), 173 _('Unable to obtain flavors.'),
186 redirect=redirect) 174 redirect=redirect)
187 175
188 def populate_flavor_choices(self, request):
189 valid_flavor = []
190 for ds in self.datastores(request):
191 # TODO(michayu): until capabilities lands
192 field_name = 'flavor'
193 if db_capability.is_vertica_datastore(ds.name):
194 field_name = 'vertica_flavor'
195
196 versions = self.datastore_versions(request, ds.name)
197 for version in versions:
198 if hasattr(version, 'active') and not version.active:
199 continue
200 valid_flavor = self.datastore_flavors(request, ds.name,
201 versions[0].name)
202 if valid_flavor:
203 self.fields[field_name].choices = sorted(
204 [(f.id, "%s" % f.name) for f in valid_flavor])
205
206 @memoized.memoized_method 176 @memoized.memoized_method
207 def populate_network_choices(self, request): 177 def populate_network_choices(self, request):
208 network_list = [] 178 network_list = []
@@ -259,6 +229,7 @@ class LaunchForm(forms.SelfHandlingForm):
259 choices = () 229 choices = ()
260 datastores = self.filter_cluster_datastores(request) 230 datastores = self.filter_cluster_datastores(request)
261 if datastores is not None: 231 if datastores is not None:
232 datastore_flavor_fields = {}
262 for ds in datastores: 233 for ds in datastores:
263 versions = self.datastore_versions(request, ds.name) 234 versions = self.datastore_versions(request, ds.name)
264 if versions: 235 if versions:
@@ -267,18 +238,74 @@ class LaunchForm(forms.SelfHandlingForm):
267 for v in versions: 238 for v in versions:
268 if hasattr(v, 'active') and not v.active: 239 if hasattr(v, 'active') and not v.active:
269 continue 240 continue
270 selection_text = ds.name + ' - ' + v.name 241 selection_text = self._build_datastore_display_text(
271 widget_text = ds.name + '-' + v.name 242 ds.name, v.name)
243 widget_text = self._build_widget_field_name(
244 ds.name, v.name)
272 version_choices = (version_choices + 245 version_choices = (version_choices +
273 ((widget_text, selection_text),)) 246 ((widget_text, selection_text),))
247 k, v = self._add_datastore_flavor_field(request,
248 ds.name,
249 v.name)
250 datastore_flavor_fields[k] = v
274 self._add_attr_to_optional_fields(ds.name, 251 self._add_attr_to_optional_fields(ds.name,
275 widget_text) 252 widget_text)
276 253
277 choices = choices + version_choices 254 choices = choices + version_choices
255 self._insert_datastore_version_fields(datastore_flavor_fields)
278 return choices 256 return choices
279 257
258 def _add_datastore_flavor_field(self,
259 request,
260 datastore,
261 datastore_version):
262 name = self._build_widget_field_name(datastore, datastore_version)
263 attr_key = 'data-datastore-' + name
264 field = forms.ChoiceField(
265 label=_("Flavor"),
266 help_text=_("Size of image to launch."),
267 required=False,
268 widget=forms.Select(attrs={
269 'class': 'switched',
270 'data-switch-on': 'datastore',
271 attr_key: _("Flavor")
272 }))
273 valid_flavors = self.datastore_flavors(request,
274 datastore,
275 datastore_version)
276 if valid_flavors:
277 field.choices = instance_utils.sort_flavor_list(
278 request, valid_flavors)
279
280 return name, field
281
282 def _build_datastore_display_text(self, datastore, datastore_version):
283 return datastore + ' - ' + datastore_version
284
285 def _build_widget_field_name(self, datastore, datastore_version):
286 # Since the fieldnames cannot contain an uppercase character
287 # we generate a hex encoded string representation of the
288 # datastore and version as the fieldname
289 return binascii.hexlify(
290 self._build_datastore_display_text(datastore, datastore_version))
291
292 def _insert_datastore_version_fields(self, datastore_flavor_fields):
293 fields_to_restore_at_the_end = collections.OrderedDict()
294 while True:
295 k, v = self.fields.popitem()
296 if k == 'datastore':
297 self.fields[k] = v
298 break
299 else:
300 fields_to_restore_at_the_end[k] = v
301
302 for k, v in datastore_flavor_fields.iteritems():
303 self.fields[k] = v
304
305 for k in reversed(fields_to_restore_at_the_end.keys()):
306 self.fields[k] = fields_to_restore_at_the_end[k]
307
280 def _add_attr_to_optional_fields(self, datastore, selection_text): 308 def _add_attr_to_optional_fields(self, datastore, selection_text):
281 fields = []
282 if db_capability.is_mongodb_datastore(datastore): 309 if db_capability.is_mongodb_datastore(datastore):
283 fields = self.mongodb_fields 310 fields = self.mongodb_fields
284 elif db_capability.is_vertica_datastore(datastore): 311 elif db_capability.is_vertica_datastore(datastore):
@@ -301,26 +328,29 @@ class LaunchForm(forms.SelfHandlingForm):
301 @sensitive_variables('data') 328 @sensitive_variables('data')
302 def handle(self, request, data): 329 def handle(self, request, data):
303 try: 330 try:
304 datastore, datastore_version = data['datastore'].split('-', 1) 331 datastore, datastore_version = (
332 create_instance.parse_datastore_and_version_text(
333 binascii.unhexlify(data['datastore'])))
305 334
306 final_flavor = data['flavor'] 335 flavor_field_name = self._build_widget_field_name(
336 datastore, datastore_version)
337 flavor = data[flavor_field_name]
307 num_instances = data['num_instances'] 338 num_instances = data['num_instances']
308 root_password = None 339 root_password = None
309 if db_capability.is_vertica_datastore(datastore): 340 if db_capability.is_vertica_datastore(datastore):
310 final_flavor = data['vertica_flavor']
311 root_password = data['root_password'] 341 root_password = data['root_password']
312 num_instances = data['num_instances_vertica'] 342 num_instances = data['num_instances_vertica']
313 LOG.info("Launching cluster with parameters " 343 LOG.info("Launching cluster with parameters "
314 "{name=%s, volume=%s, flavor=%s, " 344 "{name=%s, volume=%s, flavor=%s, "
315 "datastore=%s, datastore_version=%s", 345 "datastore=%s, datastore_version=%s",
316 "locality=%s", 346 "locality=%s",
317 data['name'], data['volume'], final_flavor, 347 data['name'], data['volume'], flavor,
318 datastore, datastore_version, self._get_locality(data)) 348 datastore, datastore_version, self._get_locality(data))
319 349
320 trove_api.trove.cluster_create(request, 350 trove_api.trove.cluster_create(request,
321 data['name'], 351 data['name'],
322 data['volume'], 352 data['volume'],
323 final_flavor, 353 flavor,
324 num_instances, 354 num_instances,
325 datastore=datastore, 355 datastore=datastore,
326 datastore_version=datastore_version, 356 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 @@
14# License for the specific language governing permissions and limitations 14# License for the specific language governing permissions and limitations
15# under the License. 15# under the License.
16 16
17import binascii
17import logging 18import logging
18 19
19from django.core.urlresolvers import reverse 20from django.core.urlresolvers import reverse
@@ -130,54 +131,60 @@ class ClustersTests(test.TestCase):
130 131
131 def test_launch_cluster_mongo_fields(self): 132 def test_launch_cluster_mongo_fields(self):
132 datastore = 'mongodb' 133 datastore = 'mongodb'
133 fields = self.launch_cluster_fields_setup(datastore, '2.6') 134 datastore_version = '2.6'
135 fields = self.launch_cluster_fields_setup(datastore,
136 datastore_version)
137 field_name = self._build_flavor_widget_name(datastore,
138 datastore_version)
134 139
135 self.assertTrue(self._contains_datastore_in_attribute( 140 self.assertTrue(self._contains_datastore_in_attribute(
136 fields['flavor'], datastore)) 141 fields[field_name], field_name))
137 self.assertTrue(self._contains_datastore_in_attribute( 142 self.assertTrue(self._contains_datastore_in_attribute(
138 fields['num_instances'], datastore)) 143 fields['num_instances'], field_name))
139 self.assertTrue(self._contains_datastore_in_attribute( 144 self.assertTrue(self._contains_datastore_in_attribute(
140 fields['num_shards'], datastore)) 145 fields['num_shards'], field_name))
141 self.assertFalse(self._contains_datastore_in_attribute( 146 self.assertFalse(self._contains_datastore_in_attribute(
142 fields['root_password'], datastore)) 147 fields['root_password'], field_name))
143 self.assertFalse(self._contains_datastore_in_attribute( 148 self.assertFalse(self._contains_datastore_in_attribute(
144 fields['num_instances_vertica'], datastore)) 149 fields['num_instances_vertica'], field_name))
145 self.assertFalse(self._contains_datastore_in_attribute(
146 fields['vertica_flavor'], datastore))
147 150
148 def test_launch_cluster_redis_fields(self): 151 def test_launch_cluster_redis_fields(self):
149 datastore = 'redis' 152 datastore = 'redis'
150 fields = self.launch_cluster_fields_setup(datastore, '3.0') 153 datastore_version = '3.0'
154 fields = self.launch_cluster_fields_setup(datastore,
155 datastore_version)
156 field_name = self._build_flavor_widget_name(datastore,
157 datastore_version)
151 158
152 self.assertTrue(self._contains_datastore_in_attribute( 159 self.assertTrue(self._contains_datastore_in_attribute(
153 fields['flavor'], datastore)) 160 fields[field_name], field_name))
154 self.assertTrue(self._contains_datastore_in_attribute( 161 self.assertTrue(self._contains_datastore_in_attribute(
155 fields['num_instances'], datastore)) 162 fields['num_instances'], field_name))
156 self.assertFalse(self._contains_datastore_in_attribute(
157 fields['num_shards'], datastore))
158 self.assertFalse(self._contains_datastore_in_attribute( 163 self.assertFalse(self._contains_datastore_in_attribute(
159 fields['root_password'], datastore)) 164 fields['num_shards'], field_name))
160 self.assertFalse(self._contains_datastore_in_attribute( 165 self.assertFalse(self._contains_datastore_in_attribute(
161 fields['num_instances_vertica'], datastore)) 166 fields['root_password'], field_name))
162 self.assertFalse(self._contains_datastore_in_attribute( 167 self.assertFalse(self._contains_datastore_in_attribute(
163 fields['vertica_flavor'], datastore)) 168 fields['num_instances_vertica'], field_name))
164 169
165 def test_launch_cluster_vertica_fields(self): 170 def test_launch_cluster_vertica_fields(self):
166 datastore = 'vertica' 171 datastore = 'vertica'
167 fields = self.launch_cluster_fields_setup(datastore, '7.1') 172 datastore_version = '7.1'
173 fields = self.launch_cluster_fields_setup(datastore,
174 datastore_version)
175 field_name = self._build_flavor_widget_name(datastore,
176 datastore_version)
168 177
178 self.assertTrue(self._contains_datastore_in_attribute(
179 fields[field_name], field_name))
169 self.assertFalse(self._contains_datastore_in_attribute( 180 self.assertFalse(self._contains_datastore_in_attribute(
170 fields['flavor'], datastore)) 181 fields['num_instances'], field_name))
171 self.assertFalse(self._contains_datastore_in_attribute(
172 fields['num_instances'], datastore))
173 self.assertFalse(self._contains_datastore_in_attribute( 182 self.assertFalse(self._contains_datastore_in_attribute(
174 fields['num_shards'], datastore)) 183 fields['num_shards'], field_name))
175 self.assertTrue(self._contains_datastore_in_attribute( 184 self.assertTrue(self._contains_datastore_in_attribute(
176 fields['root_password'], datastore)) 185 fields['root_password'], field_name))
177 self.assertTrue(self._contains_datastore_in_attribute( 186 self.assertTrue(self._contains_datastore_in_attribute(
178 fields['num_instances_vertica'], datastore)) 187 fields['num_instances_vertica'], field_name))
179 self.assertTrue(self._contains_datastore_in_attribute(
180 fields['vertica_flavor'], datastore))
181 188
182 @test.create_stubs({trove_api.trove: ('datastore_flavors', 189 @test.create_stubs({trove_api.trove: ('datastore_flavors',
183 'datastore_list', 190 'datastore_list',
@@ -238,16 +245,16 @@ class ClustersTests(test.TestCase):
238 root_password=None, 245 root_password=None,
239 locality=None).AndReturn(self.trove_clusters.first()) 246 locality=None).AndReturn(self.trove_clusters.first())
240 247
248 field_name = self._build_flavor_widget_name(cluster_datastore,
249 cluster_datastore_version)
241 self.mox.ReplayAll() 250 self.mox.ReplayAll()
242 post = { 251 post = {
243 'name': cluster_name, 252 'name': cluster_name,
244 'volume': cluster_volume, 253 'volume': cluster_volume,
245 'num_instances': cluster_instances, 254 'num_instances': cluster_instances,
246 'num_shards': 1, 255 'num_shards': 1,
247 'num_instances_per_shards': cluster_instances, 256 'datastore': field_name,
248 'datastore': cluster_datastore + u'-' + cluster_datastore_version, 257 field_name: 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
249 'flavor': cluster_flavor,
250 'network': cluster_network
251 } 258 }
252 259
253 res = self.client.post(LAUNCH_URL, post) 260 res = self.client.post(LAUNCH_URL, post)
@@ -295,16 +302,17 @@ class ClustersTests(test.TestCase):
295 root_password=None, 302 root_password=None,
296 locality=None).AndReturn(self.trove_clusters.first()) 303 locality=None).AndReturn(self.trove_clusters.first())
297 304
305 field_name = self._build_flavor_widget_name(cluster_datastore,
306 cluster_datastore_version)
298 self.mox.ReplayAll() 307 self.mox.ReplayAll()
299 post = { 308 post = {
300 'name': cluster_name, 309 'name': cluster_name,
301 'volume': cluster_volume, 310 'volume': cluster_volume,
302 'num_instances': cluster_instances, 311 'num_instances': cluster_instances,
303 'num_shards': 1, 312 'num_shards': 1,
304 'num_instances_per_shards': cluster_instances, 313 'datastore': field_name,
305 'datastore': cluster_datastore + u'-' + cluster_datastore_version, 314 field_name: 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
306 'flavor': cluster_flavor, 315 'network': cluster_network,
307 'network': cluster_network
308 } 316 }
309 317
310 res = self.client.post(LAUNCH_URL, post) 318 res = self.client.post(LAUNCH_URL, post)
@@ -349,16 +357,16 @@ class ClustersTests(test.TestCase):
349 root_password=None, 357 root_password=None,
350 locality=None).AndReturn(self.trove_clusters.first()) 358 locality=None).AndReturn(self.trove_clusters.first())
351 359
360 field_name = self._build_flavor_widget_name(cluster_datastore,
361 cluster_datastore_version)
352 self.mox.ReplayAll() 362 self.mox.ReplayAll()
353 post = { 363 post = {
354 'name': cluster_name, 364 'name': cluster_name,
355 'volume': cluster_volume, 365 'volume': cluster_volume,
356 'num_instances': cluster_instances, 366 'num_instances': cluster_instances,
357 'num_shards': 1, 367 'num_shards': 1,
358 'num_instances_per_shards': cluster_instances, 368 'datastore': field_name,
359 'datastore': cluster_datastore + u'-' + cluster_datastore_version, 369 field_name: 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
360 'flavor': cluster_flavor,
361 'network': cluster_network
362 } 370 }
363 371
364 res = self.client.post(LAUNCH_URL, post) 372 res = self.client.post(LAUNCH_URL, post)
@@ -649,3 +657,10 @@ class ClustersTests(test.TestCase):
649 if datastore in key: 657 if datastore in key:
650 return True 658 return True
651 return False 659 return False
660
661 def _build_datastore_display_text(self, datastore, datastore_version):
662 return datastore + ' - ' + datastore_version
663
664 def _build_flavor_widget_name(self, datastore, datastore_version):
665 return binascii.hexlify(self._build_datastore_display_text(
666 datastore, datastore_version))