ddaf10204a
Replace django.core.urlresolves with django.urls (In Django 2.0) The django.core.urlresolvers module is removed in favor of its new location, django.urls. It was deprecated in Django 1.10: https://docs.djangoproject.com/en/2.0/releases/1.10/#id3 The arguments of include() has been changed in Django 1.9 and the older style was dropped in Django 2.0. https://docs.djangoproject.com/en/2.0/releases/1.9/#passing-a-3-tuple-or-an-app-name-to-include Add py35dj20 job to test Django 2.0 integration. Also drops older Django unit tests from tox.ini as horizon dropped Django <=1.10 support in Rocky. Depends-On: https://review.openstack.org/#/c/571061/ Change-Id: I122f8ad81807386517149f37aa8d63c76daac533
509 lines
21 KiB
Python
509 lines
21 KiB
Python
# 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
|