# Copyright 2015 HP Software, LLC # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import collections import uuid from django.urls import reverse from django.utils.translation import ugettext_lazy as _ from django.views.decorators.debug import sensitive_variables # noqa import six from horizon import exceptions from horizon import forms from horizon import messages from horizon.utils import memoized from openstack_dashboard import api from oslo_log import log as logging from openstack_dashboard.dashboards.project.instances \ import utils as instance_utils from trove_dashboard import api as trove_api from trove_dashboard.content.database_clusters \ import cluster_manager from trove_dashboard.content.databases import db_capability from trove_dashboard.content.databases.workflows \ import create_instance from trove_dashboard.utils import common as common_utils LOG = logging.getLogger(__name__) class LaunchForm(forms.SelfHandlingForm): name = forms.CharField(label=_("Cluster Name"), max_length=80) datastore = forms.ChoiceField( label=_("Datastore"), help_text=_("Type and version of datastore."), widget=forms.Select(attrs={ 'class': 'switchable', 'data-slug': 'datastore' })) network = forms.ChoiceField( label=_("Network"), help_text=_("Network attached to instance."), required=False) volume = forms.IntegerField( label=_("Volume Size"), min_value=0, initial=1, help_text=_("Size of the volume in GB.")) locality = forms.ChoiceField( label=_("Locality"), choices=[("", "None"), ("affinity", "affinity"), ("anti-affinity", "anti-affinity")], required=False, help_text=_("Specify whether instances in the cluster will " "be created on the same hypervisor (affinity) or on " "different hypervisors (anti-affinity).")) root_password = forms.CharField( label=_("Root Password"), required=False, help_text=_("Password for root user."), widget=forms.PasswordInput(attrs={ 'class': 'switched', 'data-switch-on': 'datastore', })) num_instances_vertica = forms.IntegerField( label=_("Number of Instances"), min_value=3, initial=3, required=False, help_text=_("Number of instances in the cluster. (Read only)"), widget=forms.TextInput(attrs={ 'readonly': 'readonly', 'class': 'switched', 'data-switch-on': 'datastore', })) num_shards = forms.IntegerField( label=_("Number of Shards"), min_value=1, initial=1, required=False, help_text=_("Number of shards. (Read only)"), widget=forms.TextInput(attrs={ 'readonly': 'readonly', 'class': 'switched', 'data-switch-on': 'datastore', })) num_instances = forms.IntegerField( label=_("Number of Instances"), initial=3, required=False, help_text=_("Number of instances in the cluster."), widget=forms.TextInput(attrs={ 'class': 'switched', 'data-switch-on': 'datastore', })) # (name of field variable, label) default_fields = [ ('num_instances', _('Number of Instances')) ] mongodb_fields = default_fields + [ ('num_shards', _('Number of Shards')), ] vertica_fields = [ ('num_instances_vertica', ('Number of Instances')), ('root_password', _('Root Password')), ] def __init__(self, request, *args, **kwargs): super(LaunchForm, self).__init__(request, *args, **kwargs) self.fields['datastore'].choices = self.populate_datastore_choices( request) self.fields['network'].choices = self.populate_network_choices( request) def clean(self): datastore_field_value = self.data.get("datastore", None) if datastore_field_value: datastore, datastore_version = ( create_instance.parse_datastore_and_version_text( common_utils.unhexlify(datastore_field_value))) flavor_field_name = self._build_widget_field_name( datastore, datastore_version) if not self.data.get(flavor_field_name, None): msg = _("The flavor must be specified.") self._errors[flavor_field_name] = self.error_class([msg]) if db_capability.is_vertica_datastore(datastore): if not self.data.get("root_password", None): msg = _("Password for root user must be specified.") self._errors["root_password"] = self.error_class([msg]) else: if int(self.data.get("num_instances", 0)) < 1: msg = _("The number of instances must be greater than 1.") self._errors["num_instances"] = self.error_class([msg]) if db_capability.is_mongodb_datastore(datastore): if int(self.data.get("num_shards", 0)) < 1: msg = _("The number of shards must be greater than 1.") self._errors["num_shards"] = self.error_class([msg]) if not self.data.get("locality", None): self.cleaned_data["locality"] = None return self.cleaned_data @memoized.memoized_method def datastore_flavors(self, request, datastore_name, datastore_version): try: return trove_api.trove.datastore_flavors( request, datastore_name, datastore_version) except Exception: LOG.exception("Exception while obtaining flavors list") self._flavors = [] redirect = reverse('horizon:project:database_clusters:index') exceptions.handle(request, _('Unable to obtain flavors.'), redirect=redirect) @memoized.memoized_method def populate_network_choices(self, request): network_list = [] try: if api.base.is_service_enabled(request, 'network'): tenant_id = self.request.user.tenant_id networks = api.neutron.network_list_for_tenant(request, tenant_id) network_list = [(network.id, network.name_or_id) for network in networks] else: self.fields['network'].widget = forms.HiddenInput() except exceptions.ServiceCatalogException: network_list = [] redirect = reverse('horizon:project:database_clusters:index') exceptions.handle(request, _('Unable to retrieve networks.'), redirect=redirect) return network_list @memoized.memoized_method def datastores(self, request): try: return trove_api.trove.datastore_list(request) except Exception: LOG.exception("Exception while obtaining datastores list") self._datastores = [] redirect = reverse('horizon:project:database_clusters:index') exceptions.handle(request, _('Unable to obtain datastores.'), redirect=redirect) def filter_cluster_datastores(self, request): datastores = [] for ds in self.datastores(request): # TODO(michayu): until capabilities lands if db_capability.is_cluster_capable_datastore(ds.name): datastores.append(ds) return datastores @memoized.memoized_method def datastore_versions(self, request, datastore): try: return trove_api.trove.datastore_version_list(request, datastore) except Exception: LOG.exception("Exception while obtaining datastore version list") self._datastore_versions = [] redirect = reverse('horizon:project:database_clusters:index') exceptions.handle(request, _('Unable to obtain datastore versions.'), redirect=redirect) def populate_datastore_choices(self, request): choices = () datastores = self.filter_cluster_datastores(request) if datastores is not None: datastore_flavor_fields = {} for ds in datastores: versions = self.datastore_versions(request, ds.name) if versions: # only add to choices if datastore has at least one version version_choices = () for v in versions: # NOTE(zhaochao): troveclient API resources are lazy # loading objects. When an attribute is not found, the # get() method of the Manager object will be called # with the ID of the resource. However for # datastore_versions, the get() method is expecting two # arguments: datastore and datastore_version(name), so # TypeError will be raised as not enough arguments are # passed. In Python 2.x, hasattr() won't reraise the # exception(which is not correct), but reraise under # Python 3(which should be correct). # Use v.to_dict() to verify the 'active' info instead. if not v.to_dict().get('active', True): 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 + ((widget_text, selection_text),)) k, v = self._add_datastore_flavor_field(request, ds.name, v.name) datastore_flavor_fields[k] = v self._add_attr_to_optional_fields(ds.name, widget_text) choices = choices + version_choices self._insert_datastore_version_fields(datastore_flavor_fields) 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 = 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: field.choices = instance_utils.sort_flavor_list( request, valid_flavors) return name, field 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 common_utils.hexlify( self._build_datastore_display_text(datastore, datastore_version)) def _insert_datastore_version_fields(self, datastore_flavor_fields): fields_to_restore_at_the_end = collections.OrderedDict() while True: k, v = self.fields.popitem() if k == 'datastore': self.fields[k] = v break else: fields_to_restore_at_the_end[k] = v for k, v in datastore_flavor_fields.items(): self.fields[k] = v for k in reversed(list(fields_to_restore_at_the_end.keys())): self.fields[k] = fields_to_restore_at_the_end[k] def _add_attr_to_optional_fields(self, datastore, selection_text): if db_capability.is_mongodb_datastore(datastore): fields = self.mongodb_fields elif db_capability.is_vertica_datastore(datastore): fields = self.vertica_fields else: fields = self.default_fields for field in fields: attr_key = 'data-datastore-' + selection_text widget = self.fields[field[0]].widget if attr_key not in widget.attrs: widget.attrs[attr_key] = field[1] def _get_locality(self, data): locality = None if data.get('locality'): locality = data['locality'] return locality @sensitive_variables('data') def handle(self, request, data): try: datastore, datastore_version = ( create_instance.parse_datastore_and_version_text( common_utils.unhexlify(data['datastore']))) flavor_field_name = self._build_widget_field_name( datastore, datastore_version) flavor = data[flavor_field_name] num_instances = data['num_instances'] root_password = None if db_capability.is_vertica_datastore(datastore): root_password = data['root_password'] num_instances = data['num_instances_vertica'] LOG.info("Launching cluster with parameters " "{name=%s, volume=%s, flavor=%s, " "datastore=%s, datastore_version=%s", "locality=%s", data['name'], data['volume'], flavor, datastore, datastore_version, self._get_locality(data)) trove_api.trove.cluster_create(request, data['name'], data['volume'], flavor, num_instances, datastore=datastore, datastore_version=datastore_version, nics=data['network'], root_password=root_password, locality=self._get_locality(data)) messages.success(request, _('Launched cluster "%s"') % data['name']) return True except Exception as e: redirect = reverse("horizon:project:database_clusters:index") exceptions.handle(request, _('Unable to launch cluster. %s') % six.text_type(e), redirect=redirect) class ClusterAddInstanceForm(forms.SelfHandlingForm): cluster_id = forms.CharField( required=False, widget=forms.HiddenInput()) flavor = forms.ChoiceField( label=_("Flavor"), help_text=_("Size of image to launch.")) volume = forms.IntegerField( label=_("Volume Size"), min_value=0, initial=1, help_text=_("Size of the volume in GB.")) name = forms.CharField( label=_("Name"), required=False, help_text=_("Optional name of the instance.")) type = forms.CharField( label=_("Instance Type"), required=False, help_text=_("Optional datastore specific type of the instance.")) related_to = forms.CharField( label=_("Related To"), required=False, help_text=_("Optional datastore specific value that defines the " "relationship from one instance in the cluster to " "another.")) network = forms.ChoiceField( label=_("Network"), help_text=_("Network attached to instance."), required=False) def __init__(self, request, *args, **kwargs): super(ClusterAddInstanceForm, self).__init__(request, *args, **kwargs) self.fields['cluster_id'].initial = kwargs['initial']['cluster_id'] self.fields['flavor'].choices = self.populate_flavor_choices(request) self.fields['network'].choices = self.populate_network_choices( request) @memoized.memoized_method def flavors(self, request): try: datastore = None datastore_version = None datastore_dict = self.initial.get('datastore', None) if datastore_dict: datastore = datastore_dict.get('type', None) datastore_version = datastore_dict.get('version', None) return trove_api.trove.datastore_flavors( request, datastore_name=datastore, datastore_version=datastore_version) except Exception: LOG.exception("Exception while obtaining flavors list") self._flavors = [] redirect = reverse('horizon:project:database_clusters:index') exceptions.handle(request, _('Unable to obtain flavors.'), redirect=redirect) def populate_flavor_choices(self, request): flavor_list = [(f.id, "%s" % f.name) for f in self.flavors(request)] return sorted(flavor_list) @memoized.memoized_method def populate_network_choices(self, request): network_list = [] try: if api.base.is_service_enabled(request, 'network'): tenant_id = self.request.user.tenant_id networks = api.neutron.network_list_for_tenant(request, tenant_id) network_list = [(network.id, network.name_or_id) for network in networks] else: self.fields['network'].widget = forms.HiddenInput() except exceptions.ServiceCatalogException: network_list = [] redirect = reverse('horizon:project:database_clusters:index') exceptions.handle(request, _('Unable to retrieve networks.'), redirect=redirect) return network_list def handle(self, request, data): try: flavor = trove_api.trove.flavor_get(request, data['flavor']) manager = cluster_manager.get(data['cluster_id']) manager.add_instance(str(uuid.uuid4()), data.get('name', None), data['flavor'], flavor.name, data['volume'], data.get('type', None), data.get('related_to', None), data.get('network', None)) except Exception as e: redirect = reverse("horizon:project:database_clusters:index") exceptions.handle(request, _('Unable to grow cluster. %s') % six.text_type(e), redirect=redirect) return True class ResetPasswordForm(forms.SelfHandlingForm): cluster_id = forms.CharField(widget=forms.HiddenInput()) password = forms.CharField(widget=forms.PasswordInput(), label=_("New Password"), help_text=_("New password for cluster access.")) @sensitive_variables('data') def handle(self, request, data): password = data.get("password") cluster_id = data.get("cluster_id") try: trove_api.trove.create_cluster_root(request, cluster_id, password) messages.success(request, _('Root password updated for ' 'cluster "%s"') % cluster_id) except Exception as e: redirect = reverse("horizon:project:database_clusters:index") exceptions.handle(request, _('Unable to reset password. %s') % six.text_type(e), redirect=redirect) return True