Initial support for database clustering in Horizon
Added a separate panel for clusters. This panel contains the Clusters Table with table actions to Launch and Terminate clusters. There are row actions Add Shard and Reset Password (and their associated dialogs) that are specific to MongoDB and Vertica respectively. The Clusters Details will include the following tabs: - Overview - Instances (table of instances belonging to this cluster) The launch panel has custom fields for MongoDB and Vertica. The custom fields will be dynamically shown based on the datastore selected. Added a db_capability utility to aid in identifying the specific datastores. Added network selection dropdown if neutron is enabled. Co-Authored-By: Sushil Kumar <sushil.kumar3@hp.com> Co-Authored-By: Saurabh Surana <saurabh.surana@hp.com> Co-Authored-By: Duk Loi <duk@tesora.com> Co-Authored-By: Anna Shen <ruiyuan.shen@hp.com> Change-Id: I047f4d37449070adfd0ea66ad010982f35c049aa Implements: blueprint database-clustering-support
This commit is contained in:
parent
16e0d7c1c3
commit
57295f42ba
@ -42,6 +42,57 @@ def troveclient(request):
|
||||
return c
|
||||
|
||||
|
||||
def cluster_list(request, marker=None):
|
||||
page_size = utils.get_page_size(request)
|
||||
return troveclient(request).clusters.list(limit=page_size, marker=marker)
|
||||
|
||||
|
||||
def cluster_get(request, cluster_id):
|
||||
return troveclient(request).clusters.get(cluster_id)
|
||||
|
||||
|
||||
def cluster_delete(request, cluster_id):
|
||||
return troveclient(request).clusters.delete(cluster_id)
|
||||
|
||||
|
||||
def cluster_create(request, name, volume, flavor, num_instances,
|
||||
datastore, datastore_version,
|
||||
nics=None, root_password=None):
|
||||
# TODO(dklyle): adding to support trove without volume
|
||||
# support for now until API supports checking for volume support
|
||||
if volume > 0:
|
||||
volume_params = {'size': volume}
|
||||
else:
|
||||
volume_params = None
|
||||
instances = []
|
||||
for i in range(num_instances):
|
||||
instance = {}
|
||||
instance["flavorRef"] = flavor
|
||||
instance["volume"] = volume_params
|
||||
if nics:
|
||||
instance["nics"] = [{"net-id": nics}]
|
||||
instances.append(instance)
|
||||
|
||||
# TODO(saurabhs): vertica needs root password on cluster create
|
||||
return troveclient(request).clusters.create(
|
||||
name,
|
||||
datastore,
|
||||
datastore_version,
|
||||
instances=instances)
|
||||
|
||||
|
||||
def cluster_add_shard(request, cluster_id):
|
||||
return troveclient(request).clusters.add_shard(cluster_id)
|
||||
|
||||
|
||||
def create_cluster_root(request, cluster_id, password):
|
||||
# It appears the code below depends on this trove change
|
||||
# https://review.openstack.org/#/c/166954/. Comment out when that
|
||||
# change merges.
|
||||
# return troveclient(request).cluster.reset_root_password(cluster_id)
|
||||
troveclient(request).root.create_cluster_root(cluster_id, password)
|
||||
|
||||
|
||||
def instance_list(request, marker=None):
|
||||
page_size = utils.get_page_size(request)
|
||||
return troveclient(request).instances.list(limit=page_size, marker=marker)
|
||||
@ -130,6 +181,19 @@ def flavor_list(request):
|
||||
return troveclient(request).flavors.list()
|
||||
|
||||
|
||||
def datastore_flavors(request, datastore_name=None,
|
||||
datastore_version=None):
|
||||
# if datastore info is available then get datastore specific flavors
|
||||
if datastore_name and datastore_version:
|
||||
try:
|
||||
return troveclient(request).flavors.\
|
||||
list_datastore_version_associated_flavors(datastore_name,
|
||||
datastore_version)
|
||||
except Exception:
|
||||
LOG.warn("Failed to retrieve datastore specific flavors")
|
||||
return flavor_list(request)
|
||||
|
||||
|
||||
def flavor_get(request, flavor_id):
|
||||
return troveclient(request).flavors.get(flavor_id)
|
||||
|
||||
|
@ -0,0 +1,375 @@
|
||||
# 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 logging
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.views.decorators.debug import sensitive_variables # noqa
|
||||
|
||||
from horizon import exceptions
|
||||
from horizon import forms
|
||||
from horizon import messages
|
||||
from horizon.utils import memoized
|
||||
|
||||
from openstack_dashboard import api
|
||||
from openstack_dashboard.contrib.trove import api as trove_api
|
||||
from openstack_dashboard.contrib.trove.content.databases import db_capability
|
||||
|
||||
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'
|
||||
}))
|
||||
mongodb_flavor = forms.ChoiceField(
|
||||
label=_("Flavor"),
|
||||
help_text=_("Size of instance to launch."),
|
||||
required=False,
|
||||
widget=forms.Select(attrs={
|
||||
'class': 'switched',
|
||||
'data-switch-on': 'datastore',
|
||||
}))
|
||||
vertica_flavor = forms.ChoiceField(
|
||||
label=_("Flavor"),
|
||||
help_text=_("Size of instance to launch."),
|
||||
required=False,
|
||||
widget=forms.Select(attrs={
|
||||
'class': 'switched',
|
||||
'data-switch-on': '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."))
|
||||
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_per_shards = forms.IntegerField(
|
||||
label=_("Instances Per Shard"),
|
||||
initial=3,
|
||||
required=False,
|
||||
help_text=_("Number of instances per shard. (Read only)"),
|
||||
widget=forms.TextInput(attrs={
|
||||
'readonly': 'readonly',
|
||||
'class': 'switched',
|
||||
'data-switch-on': 'datastore',
|
||||
}))
|
||||
|
||||
# (name of field variable, label)
|
||||
mongodb_fields = [
|
||||
('mongodb_flavor', _('Flavor')),
|
||||
('num_shards', _('Number of Shards')),
|
||||
('num_instances_per_shards', _('Instances Per Shard'))
|
||||
]
|
||||
vertica_fields = [
|
||||
('num_instances_vertica', ('Number of Instances')),
|
||||
('vertica_flavor', _('Flavor')),
|
||||
('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.populate_flavor_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_field_value.split(',')[0]
|
||||
|
||||
if db_capability.is_mongodb_datastore(datastore):
|
||||
if self.data.get("num_shards", None) < 1:
|
||||
msg = _("The number of shards must be greater than 1.")
|
||||
self._errors["num_shards"] = self.error_class([msg])
|
||||
|
||||
elif db_capability.is_vertica_datastore(datastore):
|
||||
if not self.data.get("vertica_flavor", None):
|
||||
msg = _("The flavor must be specified.")
|
||||
self._errors["vertica_flavor"] = self.error_class([msg])
|
||||
if not self.data.get("root_password", None):
|
||||
msg = _("Password for root user must be specified.")
|
||||
self._errors["root_password"] = self.error_class([msg])
|
||||
|
||||
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)
|
||||
|
||||
def populate_flavor_choices(self, request):
|
||||
valid_flavor = []
|
||||
for ds in self.datastores(request):
|
||||
# TODO(michayu): until capabilities lands
|
||||
if db_capability.is_mongodb_datastore(ds.name):
|
||||
versions = self.datastore_versions(request, ds.name)
|
||||
for version in versions:
|
||||
if version.name == "inactive":
|
||||
continue
|
||||
valid_flavor = self.datastore_flavors(request, ds.name,
|
||||
versions[0].name)
|
||||
if valid_flavor:
|
||||
self.fields['mongodb_flavor'].choices = sorted(
|
||||
[(f.id, "%s" % f.name) for f in valid_flavor])
|
||||
|
||||
if db_capability.is_vertica_datastore(ds.name):
|
||||
versions = self.datastore_versions(request, ds.name)
|
||||
for version in versions:
|
||||
if version.name == "inactive":
|
||||
continue
|
||||
valid_flavor = self.datastore_flavors(request, ds.name,
|
||||
versions[0].name)
|
||||
if valid_flavor:
|
||||
self.fields['vertica_flavor'].choices = sorted(
|
||||
[(f.id, "%s" % f.name) for f in valid_flavor])
|
||||
|
||||
@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_vertica_datastore(ds.name)
|
||||
or db_capability.is_mongodb_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:
|
||||
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:
|
||||
if "inactive" in v.name:
|
||||
continue
|
||||
selection_text = ds.name + ' - ' + v.name
|
||||
widget_text = ds.name + '-' + v.name
|
||||
version_choices = (version_choices +
|
||||
((widget_text, selection_text),))
|
||||
self._add_attr_to_optional_fields(ds.name,
|
||||
widget_text)
|
||||
|
||||
choices = choices + version_choices
|
||||
return choices
|
||||
|
||||
def _add_attr_to_optional_fields(self, datastore, selection_text):
|
||||
fields = []
|
||||
if db_capability.is_mongodb_datastore(datastore):
|
||||
fields = self.mongodb_fields
|
||||
elif db_capability.is_vertica_datastore(datastore):
|
||||
fields = self.vertica_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]
|
||||
|
||||
@sensitive_variables('data')
|
||||
def handle(self, request, data):
|
||||
try:
|
||||
datastore = data['datastore'].split('-')[0]
|
||||
datastore_version = data['datastore'].split('-')[1]
|
||||
|
||||
final_flavor = data['mongodb_flavor']
|
||||
num_instances = data['num_instances_per_shards']
|
||||
root_password = None
|
||||
if db_capability.is_vertica_datastore(datastore):
|
||||
final_flavor = data['vertica_flavor']
|
||||
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",
|
||||
data['name'], data['volume'], final_flavor,
|
||||
datastore, datastore_version)
|
||||
|
||||
trove_api.trove.cluster_create(request,
|
||||
data['name'],
|
||||
data['volume'],
|
||||
final_flavor,
|
||||
num_instances,
|
||||
datastore=datastore,
|
||||
datastore_version=datastore_version,
|
||||
nics=data['network'],
|
||||
root_password=root_password)
|
||||
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') % e.message,
|
||||
redirect=redirect)
|
||||
|
||||
|
||||
class AddShardForm(forms.SelfHandlingForm):
|
||||
name = forms.CharField(
|
||||
label=_("Cluster Name"),
|
||||
max_length=80,
|
||||
widget=forms.TextInput(attrs={'readonly': 'readonly'}))
|
||||
num_shards = forms.IntegerField(
|
||||
label=_("Number of Shards"),
|
||||
initial=1,
|
||||
widget=forms.TextInput(attrs={'readonly': 'readonly'}))
|
||||
num_instances = forms.IntegerField(label=_("Instances Per Shard"),
|
||||
initial=3,
|
||||
widget=forms.TextInput(
|
||||
attrs={'readonly': 'readonly'}))
|
||||
cluster_id = forms.CharField(required=False,
|
||||
widget=forms.HiddenInput())
|
||||
|
||||
def handle(self, request, data):
|
||||
try:
|
||||
LOG.info("Adding shard with parameters "
|
||||
"{name=%s, num_shards=%s, num_instances=%s, "
|
||||
"cluster_id=%s}",
|
||||
data['name'],
|
||||
data['num_shards'],
|
||||
data['num_instances'],
|
||||
data['cluster_id'])
|
||||
trove_api.trove.cluster_add_shard(request, data['cluster_id'])
|
||||
|
||||
messages.success(request,
|
||||
_('Added shard to "%s"') % data['name'])
|
||||
except Exception as e:
|
||||
redirect = reverse("horizon:project:database_clusters:index")
|
||||
exceptions.handle(request,
|
||||
_('Unable to add shard. %s') % e.message,
|
||||
redirect=redirect)
|
||||
return True
|
||||
|
||||
|
||||
class ResetPasswordForm(forms.SelfHandlingForm):
|
||||
cluster_id = forms.CharField(widget=forms.HiddenInput())
|
||||
password = forms.CharField(widget=forms.PasswordInput(),
|
||||
label=_("New Password"),
|
||||
required=True,
|
||||
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') %
|
||||
e.message, redirect=redirect)
|
||||
return True
|
@ -0,0 +1,26 @@
|
||||
# Copyright (c) 2014 eBay Software Foundation
|
||||
# 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.
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
import horizon
|
||||
|
||||
|
||||
class Clusters(horizon.Panel):
|
||||
name = _("Clusters")
|
||||
slug = 'database_clusters'
|
||||
permissions = ('openstack.services.database',
|
||||
'openstack.services.object-store',)
|
@ -0,0 +1,205 @@
|
||||
# Copyright (c) 2014 eBay Software Foundation
|
||||
# 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.
|
||||
|
||||
from django.core import urlresolvers
|
||||
from django.template.defaultfilters import title # noqa
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import ungettext_lazy
|
||||
|
||||
from horizon import tables
|
||||
from horizon.templatetags import sizeformat
|
||||
from horizon.utils import filters
|
||||
from horizon.utils import memoized
|
||||
from openstack_dashboard.contrib.trove import api
|
||||
from openstack_dashboard.contrib.trove.content.databases import db_capability
|
||||
|
||||
ACTIVE_STATES = ("ACTIVE",)
|
||||
|
||||
|
||||
class TerminateCluster(tables.BatchAction):
|
||||
name = "terminate"
|
||||
icon = "remove"
|
||||
classes = ('btn-danger',)
|
||||
help_text = _("Terminated cluster is not recoverable.")
|
||||
|
||||
@staticmethod
|
||||
def action_present(count):
|
||||
return ungettext_lazy(
|
||||
u"Terminate Cluster",
|
||||
u"Terminate Clusters",
|
||||
count
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def action_past(count):
|
||||
return ungettext_lazy(
|
||||
u"Scheduled termination of Cluster",
|
||||
u"Scheduled termination of Clusters",
|
||||
count
|
||||
)
|
||||
|
||||
def action(self, request, obj_id):
|
||||
api.trove.cluster_delete(request, obj_id)
|
||||
|
||||
|
||||
class LaunchLink(tables.LinkAction):
|
||||
name = "launch"
|
||||
verbose_name = _("Launch Cluster")
|
||||
url = "horizon:project:database_clusters:launch"
|
||||
classes = ("btn-launch", "ajax-modal")
|
||||
icon = "cloud-upload"
|
||||
|
||||
|
||||
class AddShard(tables.LinkAction):
|
||||
name = "add_shard"
|
||||
verbose_name = _("Add Shard")
|
||||
url = "horizon:project:database_clusters:add_shard"
|
||||
classes = ("ajax-modal",)
|
||||
icon = "plus"
|
||||
|
||||
def allowed(self, request, cluster=None):
|
||||
if (cluster and cluster.task["name"] == 'NONE' and
|
||||
db_capability.is_mongodb_datastore(cluster.datastore['type'])):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class ResetPassword(tables.LinkAction):
|
||||
name = "reset_password"
|
||||
verbose_name = _("Reset Root Password")
|
||||
url = "horizon:project:database_clusters:reset_password"
|
||||
classes = ("ajax-modal",)
|
||||
|
||||
def allowed(self, request, cluster=None):
|
||||
if (cluster and cluster.task["name"] == 'NONE' and
|
||||
db_capability.is_vertica_datastore(cluster.datastore['type'])):
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_link_url(self, datum):
|
||||
cluster_id = self.table.get_object_id(datum)
|
||||
return urlresolvers.reverse(self.url, args=[cluster_id])
|
||||
|
||||
|
||||
class UpdateRow(tables.Row):
|
||||
ajax = True
|
||||
|
||||
@memoized.memoized_method
|
||||
def get_data(self, request, cluster_id):
|
||||
cluster = api.trove.cluster_get(request, cluster_id)
|
||||
try:
|
||||
# TODO(michayu): assumption that cluster is homogeneous
|
||||
flavor_id = cluster.instances[0]['flavor']['id']
|
||||
cluster.full_flavor = api.trove.flavor_get(request, flavor_id)
|
||||
except Exception:
|
||||
pass
|
||||
return cluster
|
||||
|
||||
|
||||
def get_datastore(cluster):
|
||||
return cluster.datastore["type"]
|
||||
|
||||
|
||||
def get_datastore_version(cluster):
|
||||
return cluster.datastore["version"]
|
||||
|
||||
|
||||
def get_size(cluster):
|
||||
if db_capability.is_vertica_datastore(cluster.datastore['type']):
|
||||
return "3"
|
||||
|
||||
if hasattr(cluster, "full_flavor"):
|
||||
size_string = _("%(name)s | %(RAM)s RAM | %(instances)s instances")
|
||||
vals = {'name': cluster.full_flavor.name,
|
||||
'RAM': sizeformat.mbformat(cluster.full_flavor.ram),
|
||||
'instances': len(cluster.instances)}
|
||||
return size_string % vals
|
||||
return _("Not available")
|
||||
|
||||
|
||||
def get_task(cluster):
|
||||
return cluster.task["name"]
|
||||
|
||||
|
||||
class ClustersTable(tables.DataTable):
|
||||
TASK_CHOICES = (
|
||||
("none", True),
|
||||
)
|
||||
name = tables.Column("name",
|
||||
link=("horizon:project:database_clusters:detail"),
|
||||
verbose_name=_("Cluster Name"))
|
||||
datastore = tables.Column(get_datastore,
|
||||
verbose_name=_("Datastore"))
|
||||
datastore_version = tables.Column(get_datastore_version,
|
||||
verbose_name=_("Datastore Version"))
|
||||
size = tables.Column(get_size,
|
||||
verbose_name=_("Cluster Size"),
|
||||
attrs={'data-type': 'size'})
|
||||
task = tables.Column(get_task,
|
||||
filters=(title, filters.replace_underscores),
|
||||
verbose_name=_("Current Task"),
|
||||
status=True,
|
||||
status_choices=TASK_CHOICES)
|
||||
|
||||
class Meta(object):
|
||||
name = "clusters"
|
||||
verbose_name = _("Clusters")
|
||||
status_columns = ["task"]
|
||||
row_class = UpdateRow
|
||||
table_actions = (LaunchLink, TerminateCluster)
|
||||
row_actions = (AddShard, ResetPassword, TerminateCluster)
|
||||
|
||||
|
||||
def get_instance_size(instance):
|
||||
if hasattr(instance, "full_flavor"):
|
||||
size_string = _("%(name)s | %(RAM)s RAM")
|
||||
vals = {'name': instance.full_flavor.name,
|
||||
'RAM': sizeformat.mbformat(instance.full_flavor.ram)}
|
||||
return size_string % vals
|
||||
return _("Not available")
|
||||
|
||||
|
||||
def get_instance_type(instance):
|
||||
if hasattr(instance, "type"):
|
||||
return instance.type
|
||||
return _("Not available")
|
||||
|
||||
|
||||
def get_host(instance):
|
||||
if hasattr(instance, "hostname"):
|
||||
return instance.hostname
|
||||
elif hasattr(instance, "ip") and instance.ip:
|
||||
return instance.ip[0]
|
||||
return _("Not Assigned")
|
||||
|
||||
|
||||
class InstancesTable(tables.DataTable):
|
||||
name = tables.Column("name",
|
||||
verbose_name=_("Name"))
|
||||
type = tables.Column(get_instance_type,
|
||||
verbose_name=_("Type"))
|
||||
host = tables.Column(get_host,
|
||||
verbose_name=_("Host"))
|
||||
size = tables.Column(get_instance_size,
|
||||
verbose_name=_("Size"),
|
||||
attrs={'data-type': 'size'})
|
||||
status = tables.Column("status",
|
||||
filters=(title, filters.replace_underscores),
|
||||
verbose_name=_("Status"))
|
||||
|
||||
class Meta(object):
|
||||
name = "instances"
|
||||
verbose_name = _("Instances")
|
@ -0,0 +1,84 @@
|
||||
# Copyright (c) 2014 eBay Software Foundation
|
||||
# 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.
|
||||
|
||||
from django import template
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from horizon import exceptions
|
||||
from horizon import tabs
|
||||
from openstack_dashboard.contrib.trove import api
|
||||
from openstack_dashboard.contrib.trove.content.database_clusters import tables
|
||||
|
||||
|
||||
class OverviewTab(tabs.Tab):
|
||||
name = _("Overview")
|
||||
slug = "overview"
|
||||
|
||||
def get_context_data(self, request):
|
||||
return {"cluster": self.tab_group.kwargs['cluster']}
|
||||
|
||||
def get_template_name(self, request):
|
||||
cluster = self.tab_group.kwargs['cluster']
|
||||
template_file = ('project/database_clusters/_detail_overview_%s.html'
|
||||
% cluster.datastore['type'])
|
||||
try:
|
||||
template.loader.get_template(template_file)
|
||||
return template_file
|
||||
except template.TemplateDoesNotExist:
|
||||
# This datastore type does not have a template file
|
||||
# Just use the base template file
|
||||
return ('project/database_clusters/_detail_overview.html')
|
||||
|
||||
|
||||
class InstancesTab(tabs.TableTab):
|
||||
table_classes = (tables.InstancesTable,)
|
||||
name = _("Instances")
|
||||
slug = "instances_tab"
|
||||
cluster = None
|
||||
template_name = "horizon/common/_detail_table.html"
|
||||
preload = True
|
||||
|
||||
def get_instances_data(self):
|
||||
cluster = self.tab_group.kwargs['cluster']
|
||||
data = []
|
||||
try:
|
||||
instances = api.trove.cluster_get(self.request,
|
||||
cluster.id).instances
|
||||
for instance in instances:
|
||||
instance_info = api.trove.instance_get(self.request,
|
||||
instance['id'])
|
||||
flavor_id = instance_info.flavor['id']
|
||||
instance_info.full_flavor = api.trove.flavor_get(self.request,
|
||||
flavor_id)
|
||||
if "type" in instance:
|
||||
instance_info.type = instance["type"]
|
||||
if "ip" in instance:
|
||||
instance_info.ip = instance["ip"]
|
||||
if "hostname" in instance:
|
||||
instance_info.hostname = instance["hostname"]
|
||||
|
||||
data.append(instance_info)
|
||||
except Exception:
|
||||
msg = _('Unable to get instances data.')
|
||||
exceptions.handle(self.request, msg)
|
||||
data = []
|
||||
return data
|
||||
|
||||
|
||||
class ClusterDetailTabs(tabs.TabGroup):
|
||||
slug = "cluster_details"
|
||||
tabs = (OverviewTab, InstancesTab)
|
||||
sticky = True
|
@ -0,0 +1,25 @@
|
||||
{% extends "horizon/common/_modal_form.html" %}
|
||||
{% load i18n %}
|
||||
{% load url from future %}
|
||||
|
||||
{% block form_id %}add_shard_form{% endblock %}
|
||||
{% block form_action %}{% url "horizon:project:database_clusters:add_shard" cluster_id %}{% endblock %}
|
||||
|
||||
{% block modal_id %}add_shard_modal{% endblock %}
|
||||
{% block modal-header %}{% trans "Add Shard" %}{% endblock %}
|
||||
|
||||
{% block modal-body %}
|
||||
<div class="left">
|
||||
<fieldset>
|
||||
{% include "horizon/common/_form_fields.html" %}
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="right">
|
||||
<p>{% blocktrans %}Specify the details for adding additional shards.{% endblocktrans %}</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-footer %}
|
||||
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Add Shard" %}" />
|
||||
<a href="{% url "horizon:project:database_clusters:index" %}" class="btn btn-default secondary cancel close">{% trans "Cancel" %}</a>
|
||||
{% endblock %}
|
@ -0,0 +1,27 @@
|
||||
{% load i18n sizeformat %}
|
||||
|
||||
<h3>{% trans "Cluster Overview" %}</h3>
|
||||
|
||||
<div class="status row detail">
|
||||
<h4>{% trans "Information" %}</h4>
|
||||
<hr class="header_rule">
|
||||
<dl class="dl-horizontal">
|
||||
<dt>{% trans "Name" %}</dt>
|
||||
<dd>{{ cluster.name }}</dd>
|
||||
<dt>{% trans "ID" %}</dt>
|
||||
<dd>{{ cluster.id }}</dd>
|
||||
<dt>{% trans "Datastore" %}</dt>
|
||||
<dd>{{ cluster.datastore.type }}</dd>
|
||||
<dt>{% trans "Datastore Version" %}</dt>
|
||||
<dd>{{ cluster.datastore.version }}</dd>
|
||||
<dt>{% trans "Current Task" %}</dt>
|
||||
<dd>{{ cluster.task.name|title }}</dd>
|
||||
<dt>{% trans "RAM" %}</dt>
|
||||
<dd>{{ cluster.full_flavor.ram|mbformat }}</dd>
|
||||
<dt>{% trans "Number of Instances" %}</dt>
|
||||
<dd>{{ cluster.num_instances }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
{% block connection_info %}
|
||||
{% endblock %}
|
@ -0,0 +1,27 @@
|
||||
{% extends "project/database_clusters/_detail_overview.html" %}
|
||||
{% load i18n sizeformat %}
|
||||
|
||||
{% block connection_info %}
|
||||
<div class="addresses row detail">
|
||||
<h4>{% trans "Connection Information" %}</h4>
|
||||
<hr class="header_rule">
|
||||
<dl>
|
||||
{% with cluster.ip.0 as ip %}
|
||||
<dt>{% trans "Host" %}</dt>
|
||||
<dd>
|
||||
{% if not ip %}
|
||||
{% trans "Not Assigned" %}
|
||||
</dd>
|
||||
{% else %}
|
||||
{{ ip }}
|
||||
</dd>
|
||||
<dt>{% trans "Database Port" %}</dt>
|
||||
<dd>27017</dd>
|
||||
<dt>{% trans "Connection Examples" %}</dt>
|
||||
<dd>mongo --host {{ ip }}</dd>
|
||||
<dd>mongodb://[{% trans "USERNAME" %}:{% trans "PASSWORD" %}@]{{ ip }}:27017/{% trans "DATABASE" %}</dd>
|
||||
{% endif %} <!-- ends else block -->
|
||||
{% endwith %}
|
||||
</dl>
|
||||
</div>
|
||||
{% endblock %}
|
@ -0,0 +1,29 @@
|
||||
{% load i18n sizeformat %}
|
||||
|
||||
<h3>{% trans "Cluster Overview" %}</h3>
|
||||
|
||||
<div class="status row detail">
|
||||
<h4>{% trans "Information" %}</h4>
|
||||
<hr class="header_rule">
|
||||
<dl class="dl-horizontal">
|
||||
<dt>{% trans "Name" %}</dt>
|
||||
<dd>{{ cluster.name }}</dd>
|
||||
<dt>{% trans "ID" %}</dt>
|
||||
<dd>{{ cluster.id }}</dd>
|
||||
<dt>{% trans "Datastore" %}</dt>
|
||||
<dd>{{ cluster.datastore.type }}</dd>
|
||||
<dt>{% trans "Datastore Version" %}</dt>
|
||||
<dd>{{ cluster.datastore.version }}</dd>
|
||||
<dt>{% trans "Current Task" %}</dt>
|
||||
<dd>{{ cluster.task.name|title }}</dd>
|
||||
<dt>{% trans "RAM" %}</dt>
|
||||
<dd>{{ cluster.full_flavor.ram|mbformat }}</dd>
|
||||
<dt>{% trans "Number of Instances" %}</dt>
|
||||
<dd>{{ cluster.num_instances }}</dd>
|
||||
<dt>{% trans "Managment Console" %}</dt>
|
||||
<dd><a href="{{ cluster.mgmt_url }}" target="_blank">{{ cluster.mgmt_url }}</a></dd>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
{% block connection_info %}
|
||||
{% endblock %}
|
@ -0,0 +1,22 @@
|
||||
{% extends "horizon/common/_modal_form.html" %}
|
||||
{% load i18n %}
|
||||
{% load url from future %}
|
||||
|
||||
{% block form_id %}launch_form{% endblock %}
|
||||
{% block form_action %}{% url "horizon:project:database_clusters:launch" %}{% endblock %}
|
||||
|
||||
{% block modal_id %}launch_modal{% endblock %}
|
||||
{% block modal-header %}{% trans "Launch Cluster" %}{% endblock %}
|
||||
|
||||
{% block modal-body %}
|
||||
<div class="center">
|
||||
<fieldset>
|
||||
{% include "horizon/common/_form_fields.html" %}
|
||||
</fieldset>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-footer %}
|
||||
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Launch" %}" />
|
||||
<a href="{% url "horizon:project:database_clusters:index" %}" class="btn btn-default secondary cancel close">{% trans "Cancel" %}</a>
|
||||
{% endblock %}
|
@ -0,0 +1,25 @@
|
||||
{% extends "horizon/common/_modal_form.html" %}
|
||||
{% load i18n %}
|
||||
{% load url from future %}
|
||||
|
||||
{% block form_id %}reset_password_form{% endblock %}
|
||||
{% block form_action %}{% url "horizon:project:database_clusters:reset_password" cluster_id %}{% endblock %}
|
||||
|
||||
{% block modal_id %}reset_password_modal{% endblock %}
|
||||
{% block modal-header %}{% trans "Reset Root Password" %}{% endblock %}
|
||||
|
||||
{% block modal-body %}
|
||||
<div class="left">
|
||||
<fieldset>
|
||||
{% include "horizon/common/_form_fields.html" %}
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="right">
|
||||
<p>{% blocktrans %}Specify the new root password for vertica cluster.{% endblocktrans %}</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-footer %}
|
||||
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Reset Root Password" %}" />
|
||||
<a href="{% url "horizon:project:database_clusters:index" %}" class="btn btn-default secondary cancel close">{% trans "Cancel" %}</a>
|
||||
{% endblock %}
|
@ -0,0 +1,7 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Add Shard" %}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{% include "project/database_clusters/_add_shard.html" %}
|
||||
{% endblock %}
|
@ -0,0 +1,12 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n sizeformat %}
|
||||
{% block title %}{% trans "Cluster Detail" %}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<div class="row-fluid">
|
||||
<div class="col-sm-12">
|
||||
{{ tab_group.render }}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -0,0 +1,11 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Clusters" %}{% endblock %}
|
||||
|
||||
{% block page_header %}
|
||||
{% include "horizon/common/_page_header.html" with title=_("Clusters") %}
|
||||
{% endblock page_header %}
|
||||
|
||||
{% block main %}
|
||||
{{ table.render }}
|
||||
{% endblock %}
|
@ -0,0 +1,11 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Launch Cluster" %}{% endblock %}
|
||||
|
||||
{% block page_header %}
|
||||
{% include "horizon/common/_page_header.html" with title=_("Launch Cluster") %}
|
||||
{% endblock page_header %}
|
||||
|
||||
{% block main %}
|
||||
{% include 'project/database_clusters/_launch.html' %}
|
||||
{% endblock %}
|
@ -0,0 +1,7 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Reset Root Password" %}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{% include "project/database_clusters/_reset_password.html" %}
|
||||
{% endblock %}
|
@ -0,0 +1,295 @@
|
||||
# Copyright (c) 2014 eBay Software Foundation
|
||||
# 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.
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from django import http
|
||||
|
||||
from mox3.mox import IsA # noqa
|
||||
|
||||
from openstack_dashboard import api
|
||||
from openstack_dashboard.contrib.trove import api as trove_api
|
||||
from openstack_dashboard.test import helpers as test
|
||||
|
||||
from troveclient import common
|
||||
|
||||
|
||||
INDEX_URL = reverse('horizon:project:database_clusters:index')
|
||||
LAUNCH_URL = reverse('horizon:project:database_clusters:launch')
|
||||
DETAILS_URL = reverse('horizon:project:database_clusters:detail', args=['id'])
|
||||
ADD_SHARD_VIEWNAME = 'horizon:project:database_clusters:add_shard'
|
||||
RESET_PASSWORD_VIEWNAME = 'horizon:project:database_clusters:reset_password'
|
||||
|
||||
|
||||
class ClustersTests(test.TestCase):
|
||||
@test.create_stubs({trove_api.trove: ('cluster_list',
|
||||
'flavor_list')})
|
||||
def test_index(self):
|
||||
clusters = common.Paginated(self.trove_clusters.list())
|
||||
trove_api.trove.cluster_list(IsA(http.HttpRequest), marker=None)\
|
||||
.AndReturn(clusters)
|
||||
trove_api.trove.flavor_list(IsA(http.HttpRequest))\
|
||||
.AndReturn(self.flavors.list())
|
||||
|
||||
self.mox.ReplayAll()
|
||||
res = self.client.get(INDEX_URL)
|
||||
self.assertTemplateUsed(res, 'project/database_clusters/index.html')
|
||||
|
||||
@test.create_stubs({trove_api.trove: ('cluster_list',
|
||||
'flavor_list')})
|
||||
def test_index_flavor_exception(self):
|
||||
clusters = common.Paginated(self.trove_clusters.list())
|
||||
trove_api.trove.cluster_list(IsA(http.HttpRequest), marker=None)\
|
||||
.AndReturn(clusters)
|
||||
trove_api.trove.flavor_list(IsA(http.HttpRequest))\
|
||||
.AndRaise(self.exceptions.trove)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
res = self.client.get(INDEX_URL)
|
||||
self.assertTemplateUsed(res, 'project/database_clusters/index.html')
|
||||
self.assertMessageCount(res, error=1)
|
||||
|
||||
@test.create_stubs({trove_api.trove: ('cluster_list',)})
|
||||
def test_index_list_exception(self):
|
||||
trove_api.trove.cluster_list(IsA(http.HttpRequest), marker=None)\
|
||||
.AndRaise(self.exceptions.trove)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
res = self.client.get(INDEX_URL)
|
||||
self.assertTemplateUsed(res, 'project/database_clusters/index.html')
|
||||
self.assertMessageCount(res, error=1)
|
||||
|
||||
@test.create_stubs({trove_api.trove: ('cluster_list',
|
||||
'flavor_list')})
|
||||
def test_index_pagination(self):
|
||||
clusters = self.trove_clusters.list()
|
||||
last_record = clusters[0]
|
||||
clusters = common.Paginated(clusters, next_marker="foo")
|
||||
trove_api.trove.cluster_list(IsA(http.HttpRequest), marker=None)\
|
||||
.AndReturn(clusters)
|
||||
trove_api.trove.flavor_list(IsA(http.HttpRequest))\
|
||||
.AndReturn(self.flavors.list())
|
||||
|
||||
self.mox.ReplayAll()
|
||||
res = self.client.get(INDEX_URL)
|
||||
self.assertTemplateUsed(res, 'project/database_clusters/index.html')
|
||||
self.assertContains(
|
||||
res, 'marker=' + last_record.id)
|
||||
|
||||
@test.create_stubs({trove_api.trove: ('cluster_list',
|
||||
'flavor_list')})
|
||||
def test_index_flavor_list_exception(self):
|
||||
clusters = common.Paginated(self.trove_clusters.list())
|
||||
trove_api.trove.cluster_list(IsA(http.HttpRequest), marker=None)\
|
||||
.AndReturn(clusters)
|
||||
trove_api.trove.flavor_list(IsA(http.HttpRequest))\
|
||||
.AndRaise(self.exceptions.trove)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self.client.get(INDEX_URL)
|
||||
|
||||
self.assertTemplateUsed(res, 'project/database_clusters/index.html')
|
||||
self.assertMessageCount(res, error=1)
|
||||
|
||||
@test.create_stubs({trove_api.trove: ('datastore_flavors',
|
||||
'datastore_list',
|
||||
'datastore_version_list'),
|
||||
api.base: ['is_service_enabled']})
|
||||
def test_launch_cluster(self):
|
||||
api.base.is_service_enabled(IsA(http.HttpRequest), 'network')\
|
||||
.AndReturn(False)
|
||||
trove_api.trove.datastore_flavors(IsA(http.HttpRequest),
|
||||
'mongodb', '2.6')\
|
||||
.AndReturn(self.flavors.list())
|
||||
trove_api.trove.datastore_list(IsA(http.HttpRequest))\
|
||||
.AndReturn(self.datastores.list())
|
||||
trove_api.trove.datastore_version_list(IsA(http.HttpRequest),
|
||||
IsA(str))\
|
||||
.AndReturn(self.datastore_versions.list())
|
||||
self.mox.ReplayAll()
|
||||
res = self.client.get(LAUNCH_URL)
|
||||
self.assertTemplateUsed(res, 'project/database_clusters/launch.html')
|
||||
|
||||
@test.create_stubs({trove_api.trove: ['datastore_flavors',
|
||||
'cluster_create',
|
||||
'datastore_list',
|
||||
'datastore_version_list'],
|
||||
api.base: ['is_service_enabled']})
|
||||
def test_create_simple_cluster(self):
|
||||
api.base.is_service_enabled(IsA(http.HttpRequest), 'network')\
|
||||
.AndReturn(False)
|
||||
trove_api.trove.datastore_flavors(IsA(http.HttpRequest),
|
||||
'mongodb', '2.6')\
|
||||
.AndReturn(self.flavors.list())
|
||||
trove_api.trove.datastore_list(IsA(http.HttpRequest))\
|
||||
.AndReturn(self.datastores.list())
|
||||
trove_api.trove.datastore_version_list(IsA(http.HttpRequest), IsA(str))\
|
||||
.AndReturn(self.datastore_versions.list())
|
||||
|
||||
cluster_name = u'MyCluster'
|
||||
cluster_volume = 1
|
||||
cluster_flavor = u'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
|
||||
cluster_instances = 3
|
||||
cluster_datastore = u'mongodb'
|
||||
cluster_datastore_version = u'2.6'
|
||||
cluster_network = u''
|
||||
trove_api.trove.cluster_create(
|
||||
IsA(http.HttpRequest),
|
||||
cluster_name,
|
||||
cluster_volume,
|
||||
cluster_flavor,
|
||||
cluster_instances,
|
||||
datastore=cluster_datastore,
|
||||
datastore_version=cluster_datastore_version,
|
||||
nics=cluster_network,
|
||||
root_password=None).AndReturn(self.trove_clusters.first())
|
||||
|
||||
self.mox.ReplayAll()
|
||||
post = {
|
||||
'name': cluster_name,
|
||||
'volume': cluster_volume,
|
||||
'num_instances': cluster_instances,
|
||||
'num_shards': 1,
|
||||
'num_instances_per_shards': cluster_instances,
|
||||
'datastore': cluster_datastore + u'-' + cluster_datastore_version,
|
||||
'mongodb_flavor': cluster_flavor,
|
||||
'network': cluster_network
|
||||
}
|
||||
|
||||
res = self.client.post(LAUNCH_URL, post)
|
||||
self.assertNoFormErrors(res)
|
||||
self.assertMessageCount(success=1)
|
||||
|
||||
@test.create_stubs({trove_api.trove: ['datastore_flavors',
|
||||
'cluster_create',
|
||||
'datastore_list',
|
||||
'datastore_version_list'],
|
||||
api.neutron: ['network_list_for_tenant'],
|
||||
api.base: ['is_service_enabled']})
|
||||
def test_create_simple_cluster_neutron(self):
|
||||
api.base.is_service_enabled(IsA(http.HttpRequest), 'network')\
|
||||
.AndReturn(True)
|
||||
api.neutron.network_list_for_tenant(IsA(http.HttpRequest), '1')\
|
||||
.AndReturn(self.networks.list())
|
||||
trove_api.trove.datastore_flavors(IsA(http.HttpRequest),
|
||||
'mongodb', '2.6')\
|
||||
.AndReturn(self.flavors.list())
|
||||
trove_api.trove.datastore_list(IsA(http.HttpRequest))\
|
||||
.AndReturn(self.datastores.list())
|
||||
trove_api.trove.datastore_version_list(IsA(http.HttpRequest), IsA(str))\
|
||||
.AndReturn(self.datastore_versions.list())
|
||||
|
||||
cluster_name = u'MyCluster'
|
||||
cluster_volume = 1
|
||||
cluster_flavor = u'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
|
||||
cluster_instances = 3
|
||||
cluster_datastore = u'mongodb'
|
||||
cluster_datastore_version = u'2.6'
|
||||
cluster_network = u'82288d84-e0a5-42ac-95be-e6af08727e42'
|
||||
trove_api.trove.cluster_create(
|
||||
IsA(http.HttpRequest),
|
||||
cluster_name,
|
||||
cluster_volume,
|
||||
cluster_flavor,
|
||||
cluster_instances,
|
||||
datastore=cluster_datastore,
|
||||
datastore_version=cluster_datastore_version,
|
||||
nics=cluster_network,
|
||||
root_password=None).AndReturn(self.trove_clusters.first())
|
||||
|
||||
self.mox.ReplayAll()
|
||||
post = {
|
||||
'name': cluster_name,
|
||||
'volume': cluster_volume,
|
||||
'num_instances': cluster_instances,
|
||||
'num_shards': 1,
|
||||
'num_instances_per_shards': cluster_instances,
|
||||
'datastore': cluster_datastore + u'-' + cluster_datastore_version,
|
||||
'mongodb_flavor': cluster_flavor,
|
||||
'network': cluster_network
|
||||
}
|
||||
|
||||
res = self.client.post(LAUNCH_URL, post)
|
||||
self.assertNoFormErrors(res)
|
||||
self.assertMessageCount(success=1)
|
||||
|
||||
@test.create_stubs({trove_api.trove: ['datastore_flavors',
|
||||
'cluster_create',
|
||||
'datastore_list',
|
||||
'datastore_version_list'],
|
||||
api.neutron: ['network_list_for_tenant']})
|
||||
def test_create_simple_cluster_exception(self):
|
||||
api.neutron.network_list_for_tenant(IsA(http.HttpRequest), '1')\
|
||||
.AndReturn(self.networks.list())
|
||||
trove_api.trove.datastore_flavors(IsA(http.HttpRequest),
|
||||
'mongodb', '2.6')\
|
||||
.AndReturn(self.flavors.list())
|
||||
trove_api.trove.datastore_list(IsA(http.HttpRequest))\
|
||||
.AndReturn(self.datastores.list())
|
||||
trove_api.trove.datastore_version_list(IsA(http.HttpRequest), IsA(str))\
|
||||
.AndReturn(self.datastore_versions.list())
|
||||
|
||||
cluster_name = u'MyCluster'
|
||||
cluster_volume = 1
|
||||
cluster_flavor = u'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
|
||||
cluster_instances = 3
|
||||
cluster_datastore = u'mongodb'
|
||||
cluster_datastore_version = u'2.6'
|
||||
cluster_network = u'82288d84-e0a5-42ac-95be-e6af08727e42'
|
||||
trove_api.trove.cluster_create(
|
||||
IsA(http.HttpRequest),
|
||||
cluster_name,
|
||||
cluster_volume,
|
||||
cluster_flavor,
|
||||
cluster_instances,
|
||||
datastore=cluster_datastore,
|
||||
datastore_version=cluster_datastore_version,
|
||||
nics=cluster_network,
|
||||
root_password=None).AndReturn(self.trove_clusters.first())
|
||||
|
||||
self.mox.ReplayAll()
|
||||
post = {
|
||||
'name': cluster_name,
|
||||
'volume': cluster_volume,
|
||||
'num_instances': cluster_instances,
|
||||
'num_shards': 1,
|
||||
'num_instances_per_shards': cluster_instances,
|
||||
'datastore': cluster_datastore + u'-' + cluster_datastore_version,
|
||||
'mongodb_flavor': cluster_flavor,
|
||||
'network': cluster_network
|
||||
}
|
||||
|
||||
res = self.client.post(LAUNCH_URL, post)
|
||||
self.assertRedirectsNoFollow(res, INDEX_URL)
|
||||
|
||||
@test.create_stubs({trove_api.trove: ('cluster_get',
|
||||
'instance_get',
|
||||
'flavor_get',)})
|
||||
def test_details(self):
|
||||
cluster = self.trove_clusters.first()
|
||||
trove_api.trove.cluster_get(IsA(http.HttpRequest), cluster.id)\
|
||||
.MultipleTimes().AndReturn(cluster)
|
||||
trove_api.trove.instance_get(IsA(http.HttpRequest), IsA(str))\
|
||||
.MultipleTimes().AndReturn(self.databases.first())
|
||||
trove_api.trove.flavor_get(IsA(http.HttpRequest), IsA(str))\
|
||||
.MultipleTimes().AndReturn(self.flavors.first())
|
||||
self.mox.ReplayAll()
|
||||
|
||||
details_url = reverse('horizon:project:database_clusters:detail',
|
||||
args=[cluster.id])
|
||||
res = self.client.get(details_url)
|
||||
self.assertTemplateUsed(res, 'project/database_clusters/detail.html')
|
||||
self.assertContains(res, cluster.ip[0])
|
@ -0,0 +1,34 @@
|
||||
# Copyright (c) 2014 eBay Software Foundation
|
||||
# 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.
|
||||
|
||||
from django.conf.urls import patterns # noqa
|
||||
from django.conf.urls import url # noqa
|
||||
|
||||
from openstack_dashboard.contrib.trove.content.database_clusters import views
|
||||
|
||||
CLUSTERS = r'^(?P<cluster_id>[^/]+)/%s$'
|
||||
|
||||
urlpatterns = patterns(
|
||||
'',
|
||||
url(r'^$', views.IndexView.as_view(), name='index'),
|
||||
url(r'^launch$', views.LaunchClusterView.as_view(), name='launch'),
|
||||
url(r'^(?P<cluster_id>[^/]+)/$', views.DetailView.as_view(),
|
||||
name='detail'),
|
||||
url(CLUSTERS % 'add_shard', views.AddShardView.as_view(),
|
||||
name='add_shard'),
|
||||
url(CLUSTERS % 'reset_password', views.ResetPasswordView.as_view(),
|
||||
name='reset_password'),
|
||||
)
|
@ -0,0 +1,212 @@
|
||||
# Copyright (c) 2014 eBay Software Foundation
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Views for managing database clusters.
|
||||
"""
|
||||
from collections import OrderedDict
|
||||
import logging
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.core.urlresolvers import reverse_lazy
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from horizon import exceptions
|
||||
from horizon import forms as horizon_forms
|
||||
from horizon import tables as horizon_tables
|
||||
from horizon import tabs as horizon_tabs
|
||||
from horizon.utils import memoized
|
||||
|
||||
from openstack_dashboard.contrib.trove import api
|
||||
from openstack_dashboard.contrib.trove.content.database_clusters import forms
|
||||
from openstack_dashboard.contrib.trove.content.database_clusters import tables
|
||||
from openstack_dashboard.contrib.trove.content.database_clusters import tabs
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class IndexView(horizon_tables.DataTableView):
|
||||
table_class = tables.ClustersTable
|
||||
template_name = 'project/database_clusters/index.html'
|
||||
|
||||
def has_more_data(self, table):
|
||||
return self._more
|
||||
|
||||
@memoized.memoized_method
|
||||
def get_flavors(self):
|
||||
try:
|
||||
flavors = api.trove.flavor_list(self.request)
|
||||
except Exception:
|
||||
flavors = []
|
||||
msg = _('Unable to retrieve database size information.')
|
||||
exceptions.handle(self.request, msg)
|
||||
return OrderedDict((unicode(flavor.id), flavor) for flavor in flavors)
|
||||
|
||||
def _extra_data(self, cluster):
|
||||
try:
|
||||
cluster_flavor = cluster.instances[0]["flavor"]["id"]
|
||||
flavors = self.get_flavors()
|
||||
flavor = flavors.get(cluster_flavor)
|
||||
if flavor is not None:
|
||||
cluster.full_flavor = flavor
|
||||
except Exception:
|
||||
# ignore any errors and just return cluster unaltered
|
||||
pass
|
||||
return cluster
|
||||
|
||||
def get_data(self):
|
||||
marker = self.request.GET.get(
|
||||
tables.ClustersTable._meta.pagination_param)
|
||||
# Gather our clusters
|
||||
try:
|
||||
clusters = api.trove.cluster_list(self.request, marker=marker)
|
||||
self._more = clusters.next or False
|
||||
except Exception:
|
||||
self._more = False
|
||||
clusters = []
|
||||
msg = _('Unable to retrieve database clusters.')
|
||||
exceptions.handle(self.request, msg)
|
||||
|
||||
map(self._extra_data, clusters)
|
||||
|
||||
return clusters
|
||||
|
||||
|
||||
class LaunchClusterView(horizon_forms.ModalFormView):
|
||||
form_class = forms.LaunchForm
|
||||
template_name = 'project/database_clusters/launch.html'
|
||||
success_url = reverse_lazy('horizon:project:database_clusters:index')
|
||||
|
||||
|
||||
class DetailView(horizon_tabs.TabbedTableView):
|
||||
tab_group_class = tabs.ClusterDetailTabs
|
||||
template_name = 'project/database_clusters/detail.html'
|
||||
|
||||
page_title = _("Cluster Details: {{ cluster.name }}")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(DetailView, self).get_context_data(**kwargs)
|
||||
context["cluster"] = self.get_data()
|
||||
return context
|
||||
|
||||
@memoized.memoized_method
|
||||
def get_data(self):
|
||||
try:
|
||||
cluster_id = self.kwargs['cluster_id']
|
||||
cluster = api.trove.cluster_get(self.request, cluster_id)
|
||||
except Exception:
|
||||
redirect = reverse('horizon:project:database_clusters:index')
|
||||
msg = _('Unable to retrieve details '
|
||||
'for database cluster: %s') % cluster_id
|
||||
exceptions.handle(self.request, msg, redirect=redirect)
|
||||
try:
|
||||
cluster.full_flavor = api.trove.flavor_get(
|
||||
self.request, cluster.instances[0]["flavor"]["id"])
|
||||
except Exception:
|
||||
LOG.error('Unable to retrieve flavor details'
|
||||
' for database cluster: %s' % cluster_id)
|
||||
cluster.num_instances = len(cluster.instances)
|
||||
|
||||
# Todo(saurabhs) Set mgmt_url to dispaly Mgmt Console URL on
|
||||
# cluster details page
|
||||
# for instance in cluster.instances:
|
||||
# if instance['type'] == "master":
|
||||
# cluster.mgmt_url = "https://%s:5450/webui" % instance['ip'][0]
|
||||
|
||||
return cluster
|
||||
|
||||
def get_tabs(self, request, *args, **kwargs):
|
||||
cluster = self.get_data()
|
||||
return self.tab_group_class(request, cluster=cluster, **kwargs)
|
||||
|
||||
|
||||
class AddShardView(horizon_forms.ModalFormView):
|
||||
form_class = forms.AddShardForm
|
||||
template_name = 'project/database_clusters/add_shard.html'
|
||||
success_url = reverse_lazy('horizon:project:database_clusters:index')
|
||||
page_title = _("Add Shard")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(AddShardView, self).get_context_data(**kwargs)
|
||||
context["cluster_id"] = self.kwargs['cluster_id']
|
||||
return context
|
||||
|
||||
def get_object(self, *args, **kwargs):
|
||||
if not hasattr(self, "_object"):
|
||||
cluster_id = self.kwargs['cluster_id']
|
||||
try:
|
||||
self._object = api.trove.cluster_get(self.request, cluster_id)
|
||||
# TODO(michayu): assumption that cluster is homogeneous
|
||||
flavor_id = self._object.instances[0]['flavor']['id']
|
||||
flavors = self.get_flavors()
|
||||
if flavor_id in flavors:
|
||||
self._object.flavor_name = flavors[flavor_id].name
|
||||
else:
|
||||
flavor = api.trove.flavor_get(self.request, flavor_id)
|
||||
self._object.flavor_name = flavor.name
|
||||
except Exception:
|
||||
redirect = reverse("horizon:project:database_clusters:index")
|
||||
msg = _('Unable to retrieve cluster details.')
|
||||
exceptions.handle(self.request, msg, redirect=redirect)
|
||||
return self._object
|
||||
|
||||
def get_flavors(self, *args, **kwargs):
|
||||
if not hasattr(self, "_flavors"):
|
||||
try:
|
||||
flavors = api.trove.flavor_list(self.request)
|
||||
self._flavors = OrderedDict([(str(flavor.id), flavor)
|
||||
for flavor in flavors])
|
||||
except Exception:
|
||||
redirect = reverse("horizon:project:database_clusters:index")
|
||||
exceptions.handle(
|
||||
self.request,
|
||||
_('Unable to retrieve flavors.'), redirect=redirect)
|
||||
return self._flavors
|
||||
|
||||
def get_initial(self):
|
||||
initial = super(AddShardView, self).get_initial()
|
||||
_object = self.get_object()
|
||||
if _object:
|
||||
initial.update(
|
||||
{'cluster_id': self.kwargs['cluster_id'],
|
||||
'name': getattr(_object, 'name', None)})
|
||||
return initial
|
||||
|
||||
|
||||
class ResetPasswordView(horizon_forms.ModalFormView):
|
||||
form_class = forms.ResetPasswordForm
|
||||
template_name = 'project/database_clusters/reset_password.html'
|
||||
success_url = reverse_lazy('horizon:project:database_clusters:index')
|
||||
page_title = _("Reset Root Password")
|
||||
|
||||
@memoized.memoized_method
|
||||
def get_object(self, *args, **kwargs):
|
||||
cluster_id = self.kwargs['cluster_id']
|
||||
try:
|
||||
return api.trove.cluster_get(self.request, cluster_id)
|
||||
except Exception:
|
||||
msg = _('Unable to retrieve cluster details.')
|
||||
redirect = reverse('horizon:project:database_clusters:index')
|
||||
exceptions.handle(self.request, msg, redirect=redirect)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(ResetPasswordView, self).get_context_data(**kwargs)
|
||||
context['cluster_id'] = self.kwargs['cluster_id']
|
||||
return context
|
||||
|
||||
def get_initial(self):
|
||||
return {'cluster_id': self.kwargs['cluster_id']}
|
@ -0,0 +1,25 @@
|
||||
# Copyright 2015 Tesora Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
|
||||
MONGODB = "mongodb"
|
||||
VERTICA = "vertica"
|
||||
|
||||
|
||||
def is_mongodb_datastore(datastore):
|
||||
return (datastore is not None) and (MONGODB in datastore.lower())
|
||||
|
||||
|
||||
def is_vertica_datastore(datastore):
|
||||
return (datastore is not None) and (VERTICA in datastore.lower())
|
@ -3,7 +3,7 @@
|
||||
|
||||
{% block connection_info %}
|
||||
<div class="addresses row detail">
|
||||
<h4>{% trans "Connection Info" %}</h4>
|
||||
<h4>{% trans "Connection Information" %}</h4>
|
||||
<hr class="header_rule">
|
||||
<dl class="dl-horizontal">
|
||||
{% with instance.host as host %}
|
||||
|
@ -3,15 +3,22 @@
|
||||
|
||||
{% block connection_info %}
|
||||
<div class="addresses row detail">
|
||||
<h4>{% trans "Connection Info" %}</h4>
|
||||
<h4>{% trans "Connection Information" %}</h4>
|
||||
<hr class="header_rule">
|
||||
<dl class="dl-horizontal">
|
||||
{% if instance.cluster_id %}
|
||||
<a href="/project/clusters/{{instance.cluster_id}}">Link</a> to Cluster Details for Connection Info
|
||||
{% else %}
|
||||
<dl>
|
||||
{% with instance.host as host %}
|
||||
<dt>{% trans "Host" %}</dt>
|
||||
<dd>
|
||||
{% if not host %}
|
||||
<dd>{% trans "Not Assigned" %}</dd>
|
||||
{% trans "Not Assigned" %}
|
||||
</dd>
|
||||
{% else %}
|
||||
<dd>{{ host }}</dd>
|
||||
{{ host }}
|
||||
</dd>
|
||||
<dt>{% trans "Database Port" %}</dt>
|
||||
<dd>27017</dd>
|
||||
<dt>{% trans "Connection Examples" %}</dt>
|
||||
@ -20,5 +27,6 @@
|
||||
{% endif %} <!-- ends else block -->
|
||||
{% endwith %}
|
||||
</dl>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
@ -140,7 +140,7 @@ class DatabaseTests(test.TestCase):
|
||||
self.datastores.list())
|
||||
# Mock datastore versions
|
||||
api.trove.datastore_version_list(IsA(http.HttpRequest), IsA(str)).\
|
||||
AndReturn(self.datastore_versions.list())
|
||||
MultipleTimes().AndReturn(self.datastore_versions.list())
|
||||
|
||||
dash_api.neutron.network_list(IsA(http.HttpRequest),
|
||||
tenant_id=self.tenant.id,
|
||||
@ -207,7 +207,7 @@ class DatabaseTests(test.TestCase):
|
||||
|
||||
# Mock datastore versions
|
||||
api.trove.datastore_version_list(IsA(http.HttpRequest), IsA(str))\
|
||||
.AndReturn(self.datastore_versions.list())
|
||||
.MultipleTimes().AndReturn(self.datastore_versions.list())
|
||||
|
||||
dash_api.neutron.network_list(IsA(http.HttpRequest),
|
||||
tenant_id=self.tenant.id,
|
||||
@ -268,7 +268,7 @@ class DatabaseTests(test.TestCase):
|
||||
|
||||
# Mock datastore versions
|
||||
api.trove.datastore_version_list(IsA(http.HttpRequest), IsA(str))\
|
||||
.AndReturn(self.datastore_versions.list())
|
||||
.MultipleTimes().AndReturn(self.datastore_versions.list())
|
||||
|
||||
dash_api.neutron.network_list(IsA(http.HttpRequest),
|
||||
tenant_id=self.tenant.id,
|
||||
@ -499,7 +499,7 @@ class DatabaseTests(test.TestCase):
|
||||
|
||||
api.trove.datastore_version_list(IsA(http.HttpRequest),
|
||||
IsA(str))\
|
||||
.AndReturn(self.datastore_versions.list())
|
||||
.MultipleTimes().AndReturn(self.datastore_versions.list())
|
||||
|
||||
dash_api.neutron.network_list(IsA(http.HttpRequest),
|
||||
tenant_id=self.tenant.id,
|
||||
|
@ -102,7 +102,7 @@ class SetInstanceDetailsAction(workflows.Action):
|
||||
num_datastores_with_one_version += 1
|
||||
if num_datastores_with_one_version > 1:
|
||||
set_initial = True
|
||||
if len(versions) > 0:
|
||||
if versions:
|
||||
# only add to choices if datastore has at least one version
|
||||
version_choices = ()
|
||||
for v in versions:
|
||||
|
@ -0,0 +1,25 @@
|
||||
# Copyright [2015] Hewlett-Packard Development Company, L.P.
|
||||
# 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.
|
||||
|
||||
# The slug of the panel to be added to HORIZON_CONFIG. Required.
|
||||
PANEL = 'database_clusters'
|
||||
# The slug of the dashboard the PANEL associated with. Required.
|
||||
PANEL_DASHBOARD = 'project'
|
||||
# The slug of the panel group the PANEL is associated with.
|
||||
PANEL_GROUP = 'database'
|
||||
|
||||
# Python panel class of the PANEL to be added.
|
||||
ADD_PANEL = ('openstack_dashboard.contrib.trove.'
|
||||
'content.database_clusters.panel.Clusters')
|
@ -1,4 +1,5 @@
|
||||
# Copyright 2013 Rackspace Hosting.
|
||||
# Copyright 2015 HP Software, LLC
|
||||
#
|
||||
# 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
|
||||
@ -13,6 +14,7 @@
|
||||
# under the License.
|
||||
|
||||
from troveclient.v1 import backups
|
||||
from troveclient.v1 import clusters
|
||||
from troveclient.v1 import databases
|
||||
from troveclient.v1 import datastores
|
||||
from troveclient.v1 import flavors
|
||||
@ -22,6 +24,104 @@ from troveclient.v1 import users
|
||||
from openstack_dashboard.test.test_data import utils
|
||||
|
||||
|
||||
CLUSTER_DATA_ONE = {
|
||||
"status": "ACTIVE",
|
||||
"id": "dfbbd9ca-b5e1-4028-adb7-f78643e17998",
|
||||
"name": "Test Cluster",
|
||||
"created": "2014-04-25T20:19:23",
|
||||
"updated": "2014-04-25T20:19:23",
|
||||
"links": [],
|
||||
"datastore": {
|
||||
"type": "mongodb",
|
||||
"version": "2.6"
|
||||
},
|
||||
"ip": ["10.0.0.1"],
|
||||
"instances": [
|
||||
{
|
||||
"id": "416b0b16-ba55-4302-bbd3-ff566032e1c1",
|
||||
"shard_id": "5415b62f-f301-4e84-ba90-8ab0734d15a7",
|
||||
"flavor": {
|
||||
"id": "7",
|
||||
"links": []
|
||||
},
|
||||
"volume": {
|
||||
"size": 100
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "965ef811-7c1d-47fc-89f2-a89dfdd23ef2",
|
||||
"shard_id": "5415b62f-f301-4e84-ba90-8ab0734d15a7",
|
||||
"flavor": {
|
||||
"id": "7",
|
||||
"links": []
|
||||
},
|
||||
"volume": {
|
||||
"size": 100
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "3642f41c-e8ad-4164-a089-3891bf7f2d2b",
|
||||
"shard_id": "5415b62f-f301-4e84-ba90-8ab0734d15a7",
|
||||
"flavor": {
|
||||
"id": "7",
|
||||
"links": []
|
||||
},
|
||||
"volume": {
|
||||
"size": 100
|
||||
}
|
||||
}
|
||||
],
|
||||
"task": {
|
||||
"name": "test_task"
|
||||
}
|
||||
}
|
||||
|
||||
CLUSTER_DATA_TWO = {
|
||||
"status": "ACTIVE",
|
||||
"id": "dfbbd9ca-b5e1-4028-adb7-f78643e17998",
|
||||
"name": "Test Cluster",
|
||||
"created": "2014-04-25T20:19:23",
|
||||
"updated": "2014-04-25T20:19:23",
|
||||
"links": [],
|
||||
"datastore": {
|
||||
"type": "vertica",
|
||||
"version": "7.1"
|
||||
},
|
||||
"ip": ["10.0.0.1"],
|
||||
"instances": [
|
||||
{
|
||||
"id": "416b0b16-ba55-4302-bbd3-ff566032e1c1",
|
||||
"flavor": {
|
||||
"id": "7",
|
||||
"links": []
|
||||
},
|
||||
"volume": {
|
||||
"size": 100
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "965ef811-7c1d-47fc-89f2-a89dfdd23ef2",
|
||||
"flavor": {
|
||||
"id": "7",
|
||||
"links": []
|
||||
},
|
||||
"volume": {
|
||||
"size": 100
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "3642f41c-e8ad-4164-a089-3891bf7f2d2b",
|
||||
"flavor": {
|
||||
"id": "7",
|
||||
"links": []
|
||||
},
|
||||
"volume": {
|
||||
"size": 100
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
DATABASE_DATA_ONE = {
|
||||
"status": "ACTIVE",
|
||||
"updated": "2013-08-12T22:00:09",
|
||||
@ -130,6 +230,12 @@ DATASTORE_TWO = {
|
||||
"name": "mysql"
|
||||
}
|
||||
|
||||
DATASTORE_MONGODB = {
|
||||
"id": "ccb31517-c472-409d-89b4-1a13db6bdd37",
|
||||
"links": [],
|
||||
"name": "mongodb"
|
||||
}
|
||||
|
||||
VERSION_ONE = {
|
||||
"name": "5.5",
|
||||
"links": [],
|
||||
@ -171,8 +277,22 @@ FLAVOR_THREE = {
|
||||
"name": "test.1"
|
||||
}
|
||||
|
||||
VERSION_MONGODB_2_6 = {
|
||||
"name": "2.6",
|
||||
"links": [],
|
||||
"image": "c7956bb5-920e-4299-b68e-2347d830d937",
|
||||
"active": 1,
|
||||
"datastore": "ccb31517-c472-409d-89b4-1a13db6bdd37",
|
||||
"packages": "2.6",
|
||||
"id": "600a6d52-8347-4e00-8e4c-f4fa9cf96ae9"
|
||||
}
|
||||
|
||||
|
||||
def data(TEST):
|
||||
cluster1 = clusters.Cluster(clusters.Clusters(None),
|
||||
CLUSTER_DATA_ONE)
|
||||
cluster2 = clusters.Cluster(clusters.Clusters(None),
|
||||
CLUSTER_DATA_TWO)
|
||||
database1 = instances.Instance(instances.Instances(None),
|
||||
DATABASE_DATA_ONE)
|
||||
database2 = instances.Instance(instances.Instances(None),
|
||||
@ -186,18 +306,22 @@ def data(TEST):
|
||||
|
||||
datastore1 = datastores.Datastore(datastores.Datastores(None),
|
||||
DATASTORE_ONE)
|
||||
|
||||
version1 = datastores.\
|
||||
DatastoreVersion(datastores.DatastoreVersions(None),
|
||||
VERSION_ONE)
|
||||
version2 = datastores.\
|
||||
DatastoreVersion(datastores.DatastoreVersions(None),
|
||||
VERSION_TWO)
|
||||
|
||||
flavor1 = flavors.Flavor(flavors.Flavors(None), FLAVOR_ONE)
|
||||
flavor2 = flavors.Flavor(flavors.Flavors(None), FLAVOR_TWO)
|
||||
flavor3 = flavors.Flavor(flavors.Flavors(None), FLAVOR_THREE)
|
||||
datastore_mongodb = datastores.Datastore(datastores.Datastores(None),
|
||||
DATASTORE_MONGODB)
|
||||
version_mongodb_2_6 = datastores.\
|
||||
DatastoreVersion(datastores.DatastoreVersions(None),
|
||||
VERSION_MONGODB_2_6)
|
||||
|
||||
TEST.trove_clusters = utils.TestDataContainer()
|
||||
TEST.trove_clusters.add(cluster1)
|
||||
TEST.trove_clusters.add(cluster2)
|
||||
TEST.databases = utils.TestDataContainer()
|
||||
TEST.database_backups = utils.TestDataContainer()
|
||||
TEST.database_users = utils.TestDataContainer()
|
||||
@ -213,7 +337,8 @@ def data(TEST):
|
||||
TEST.database_user_dbs.add(user_db1)
|
||||
TEST.datastores = utils.TestDataContainer()
|
||||
TEST.datastores.add(datastore1)
|
||||
TEST.datastore_versions = utils.TestDataContainer()
|
||||
TEST.datastore_versions.add(version1)
|
||||
TEST.datastore_versions.add(version2)
|
||||
TEST.datastores.add(datastore_mongodb)
|
||||
TEST.database_flavors.add(flavor1, flavor2, flavor3)
|
||||
TEST.datastore_versions = utils.TestDataContainer()
|
||||
TEST.datastore_versions.add(version_mongodb_2_6)
|
||||
TEST.datastore_versions.add(version1)
|
||||
|
Loading…
Reference in New Issue
Block a user