Merge "Initial support for database clustering in Horizon"

This commit is contained in:
Jenkins 2015-09-22 06:06:26 +00:00 committed by Gerrit Code Review
commit e8f84ef157
27 changed files with 1712 additions and 31 deletions

View File

@ -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)

View File

@ -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

View File

@ -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',)

View File

@ -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")

View File

@ -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

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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])

View File

@ -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'),
)

View File

@ -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']}

View File

@ -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())

View File

@ -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 %}

View File

@ -3,22 +3,30 @@
{% 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 %}
<dt>{% trans "Host" %}</dt>
{% if not host %}
<dd>{% trans "Not Assigned" %}</dd>
{% else %}
<dd>{{ host }}</dd>
<dt>{% trans "Database Port" %}</dt>
<dd>27017</dd>
<dt>{% trans "Connection Examples" %}</dt>
<dd>mongo --host {{ host }}</dd>
<dd>mongodb://[{% trans "USERNAME" %}:{% trans "PASSWORD" %}@]{{ host }}:27017/{% trans "DATABASE" %}</dd>
{% endif %} <!-- ends else block -->
{% endwith %}
</dl>
</div>
{% 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 %}
{% trans "Not Assigned" %}
</dd>
{% else %}
{{ host }}
</dd>
<dt>{% trans "Database Port" %}</dt>
<dd>27017</dd>
<dt>{% trans "Connection Examples" %}</dt>
<dd>mongo --host {{ host }}</dd>
<dd>mongodb://[{% trans "USERNAME" %}:{% trans "PASSWORD" %}@]{{ host }}:27017/{% trans "DATABASE" %}</dd>
{% endif %} <!-- ends else block -->
{% endwith %}
</dl>
{% endif %}
</div>
{% endblock %}

View File

@ -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,

View File

@ -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:

View File

@ -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')

View File

@ -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)