Merge "Trove add cluster grow and shrink support"

This commit is contained in:
Jenkins
2016-02-26 08:29:56 +00:00
committed by Gerrit Code Review
15 changed files with 733 additions and 116 deletions

View File

@@ -76,8 +76,25 @@ def cluster_create(request, name, volume, flavor, num_instances,
instances=instances)
def cluster_add_shard(request, cluster_id):
return troveclient(request).clusters.add_shard(cluster_id)
def cluster_grow(request, cluster_id, new_instances):
instances = []
for new_instance in new_instances:
instance = {}
instance["flavorRef"] = new_instance.flavor_id
if new_instance.volume > 0:
instance["volume"] = {'size': new_instance.volume}
if new_instance.name:
instance["name"] = new_instance.name
if new_instance.type:
instance["type"] = new_instance.type
if new_instance.related_to:
instance["related_to"] = new_instance.related_to
instances.append(instance)
return troveclient(request).clusters.grow(cluster_id, instances)
def cluster_shrink(request, cluster_id, instances):
return troveclient(request).clusters.shrink(cluster_id, instances)
def create_cluster_root(request, cluster_id, password):

View File

@@ -0,0 +1,86 @@
# Copyright 2016 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.
from django.core import cache
def get(cluster_id):
if not has_cluster(cluster_id):
manager = ClusterInstanceManager(cluster_id)
cache.cache.set(cluster_id, manager)
return cache.cache.get(cluster_id)
def delete(cluster_id):
manager = get(cluster_id)
manager.clear_instances()
cache.cache.delete(cluster_id)
def update(cluster_id, manager):
cache.cache.set(cluster_id, manager)
def has_cluster(cluster_id):
if cache.cache.get(cluster_id):
return True
else:
return False
class ClusterInstanceManager(object):
instances = []
def __init__(self, cluster_id):
self.cluster_id = cluster_id
def get_instances(self):
return self.instances
def get_instance(self, id):
for instance in self.instances:
if instance.id == id:
return instance
return None
def add_instance(self, id, name, flavor_id,
flavor, volume, type, related_to):
instance = ClusterInstance(id, name, flavor_id, flavor,
volume, type, related_to)
self.instances.append(instance)
update(self.cluster_id, self)
return self.instances
def delete_instance(self, id):
instance = self.get_instance(id)
if instance:
self.instances.remove(instance)
update(self.cluster_id, self)
def clear_instances(self):
del self.instances[:]
class ClusterInstance(object):
def __init__(self, id, name, flavor_id, flavor, volume, type, related_to):
self.id = id
self.name = name
self.flavor_id = flavor_id
self.flavor = flavor
self.volume = volume
self.type = type
self.related_to = related_to

View File

@@ -14,6 +14,7 @@
# under the License.
import logging
import uuid
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _
@@ -26,6 +27,8 @@ from horizon.utils import memoized
from openstack_dashboard import api
from trove_dashboard import api as trove_api
from trove_dashboard.content.database_clusters \
import cluster_manager
from trove_dashboard.content.databases import db_capability
LOG = logging.getLogger(__name__)
@@ -314,39 +317,78 @@ class LaunchForm(forms.SelfHandlingForm):
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"),
class ClusterAddInstanceForm(forms.SelfHandlingForm):
cluster_id = forms.CharField(
required=False,
widget=forms.HiddenInput())
flavor = forms.ChoiceField(
label=_("Flavor"),
help_text=_("Size of image to launch."))
volume = forms.IntegerField(
label=_("Volume Size"),
min_value=0,
initial=1,
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())
help_text=_("Size of the volume in GB."))
name = forms.CharField(
label=_("Name"),
required=False,
help_text=_("Optional name of the instance."))
type = forms.CharField(
label=_("Instance Type"),
required=False,
help_text=_("Optional datastore specific type of the instance."))
related_to = forms.CharField(
label=_("Related To"),
required=False,
help_text=_("Optional datastore specific value that defines the "
"relationship from one instance in the cluster to "
"another."))
def __init__(self, request, *args, **kwargs):
super(ClusterAddInstanceForm, self).__init__(request, *args, **kwargs)
self.fields['flavor'].choices = self.populate_flavor_choices(request)
@memoized.memoized_method
def flavors(self, request):
try:
datastore = None
datastore_version = None
datastore_dict = self.initial.get('datastore', None)
if datastore_dict:
datastore = datastore_dict.get('type', None)
datastore_version = datastore_dict.get('version', None)
return trove_api.trove.datastore_flavors(
request,
datastore_name=datastore,
datastore_version=datastore_version)
except Exception:
LOG.exception("Exception while obtaining flavors list")
self._flavors = []
redirect = reverse('horizon:project:database_clusters:index')
exceptions.handle(request,
_('Unable to obtain flavors.'),
redirect=redirect)
def populate_flavor_choices(self, request):
flavor_list = [(f.id, "%s" % f.name) for f in self.flavors(request)]
return sorted(flavor_list)
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'])
flavor = trove_api.trove.flavor_get(request, data['flavor'])
manager = cluster_manager.get(data['cluster_id'])
manager.add_instance(str(uuid.uuid4()),
data.get('name', None),
data['flavor'],
flavor.name,
data['volume'],
data.get('type', None),
data.get('related_to', None))
except Exception as e:
redirect = reverse("horizon:project:database_clusters:index")
exceptions.handle(request,
_('Unable to add shard. %s') % e.message,
_('Unable to grow cluster. %s') % e.message,
redirect=redirect)
return True

View File

@@ -14,19 +14,27 @@
# License for the specific language governing permissions and limitations
# under the License.
import logging
from django.core import urlresolvers
from django import shortcuts
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 messages
from horizon import tables
from horizon.templatetags import sizeformat
from horizon.utils import filters
from horizon.utils import functions
from horizon.utils import memoized
from trove_dashboard import api
from trove_dashboard.content.database_clusters import cluster_manager
from trove_dashboard.content.databases import db_capability
LOG = logging.getLogger(__name__)
ACTIVE_STATES = ("ACTIVE",)
@@ -64,16 +72,29 @@ class LaunchLink(tables.LinkAction):
icon = "cloud-upload"
class AddShard(tables.LinkAction):
name = "add_shard"
verbose_name = _("Add Shard")
url = "horizon:project:database_clusters:add_shard"
classes = ("ajax-modal",)
class ClusterGrow(tables.LinkAction):
name = "cluster_grow"
verbose_name = _("Grow Cluster")
url = "horizon:project:database_clusters:cluster_grow_details"
icon = "plus"
def allowed(self, request, cluster=None):
if (cluster and cluster.task["name"] == 'NONE' and
db_capability.is_mongodb_datastore(cluster.datastore['type'])):
db_capability.can_modify_cluster(cluster.datastore['type'])):
return True
return False
class ClusterShrink(tables.LinkAction):
name = "cluster_shrink"
verbose_name = _("Shrink Cluster")
url = "horizon:project:database_clusters:cluster_shrink_details"
classes = ("btn-danger",)
icon = "remove"
def allowed(self, request, cluster=None):
if (cluster and cluster.task["name"] == 'NONE' and
db_capability.can_modify_cluster(cluster.datastore['type'])):
return True
return False
@@ -163,7 +184,8 @@ class ClustersTable(tables.DataTable):
status_columns = ["task"]
row_class = UpdateRow
table_actions = (LaunchLink, DeleteCluster)
row_actions = (AddShard, ResetPassword, DeleteCluster)
row_actions = (ClusterGrow, ClusterShrink, ResetPassword,
DeleteCluster)
def get_instance_size(instance):
@@ -206,3 +228,208 @@ class InstancesTable(tables.DataTable):
class Meta(object):
name = "instances"
verbose_name = _("Instances")
class ClusterShrinkAction(tables.BatchAction):
name = "cluster_shrink_action"
icon = "remove"
classes = ('btn-danger',)
success_url = 'horizon:project:database_clusters:index'
help_text = _("Shrinking a cluster is not recoverable.")
@staticmethod
def action_present(count):
return ungettext_lazy(
u"Shrink Cluster",
u"Shrink Cluster",
count
)
@staticmethod
def action_past(count):
return ungettext_lazy(
u"Scheduled Shrinking of Cluster",
u"Scheduled Shrinking of Cluster",
count
)
def handle(self, table, request, obj_ids):
datum_display_objs = []
for datum_id in obj_ids:
datum = table.get_object_by_id(datum_id)
datum_display = table.get_object_display(datum) or datum_id
datum_display_objs.append(datum_display)
display_str = functions.lazy_join(", ", datum_display_objs)
try:
cluster_id = table.kwargs['cluster_id']
data = [{'id': instance_id} for instance_id in obj_ids]
api.trove.cluster_shrink(request, cluster_id, data)
LOG.info('%s: "%s"' %
(self._get_action_name(past=True),
display_str))
msg = _('Removed instances from cluster.')
messages.info(request, msg)
except Exception as ex:
LOG.error('Action %(action)s failed with %(ex)s for %(data)s' %
{'action': self._get_action_name(past=True).lower(),
'ex': ex.message,
'data': display_str})
msg = _('Unable to remove instances from cluster: %s')
messages.error(request, msg % ex.message)
return shortcuts.redirect(self.get_success_url(request))
class ClusterShrinkInstancesTable(tables.DataTable):
name = tables.Column("name",
verbose_name=_("Name"))
status = tables.Column("status",
filters=(title, filters.replace_underscores),
verbose_name=_("Status"))
class Meta(object):
name = "shrink_cluster_table"
verbose_name = _("Instances")
table_actions = (ClusterShrinkAction,)
row_actions = (ClusterShrinkAction,)
class ClusterGrowAddInstance(tables.LinkAction):
name = "cluster_grow_add_instance"
verbose_name = _("Add Instance")
url = "horizon:project:database_clusters:add_instance"
classes = ("ajax-modal",)
def get_link_url(self):
return urlresolvers.reverse(
self.url, args=[self.table.kwargs['cluster_id']])
class ClusterGrowRemoveInstance(tables.BatchAction):
name = "cluster_grow_remove_instance"
@staticmethod
def action_present(count):
return ungettext_lazy(
u"Remove Instance",
u"Remove Instances",
count
)
@staticmethod
def action_past(count):
return ungettext_lazy(
u"Removed Instance",
u"Removed Instances",
count
)
def action(self, request, datum_id):
manager = cluster_manager.get(self.table.kwargs['cluster_id'])
manager.delete_instance(datum_id)
def handle(self, table, request, obj_ids):
action_success = []
action_failure = []
action_not_allowed = []
for datum_id in obj_ids:
datum = table.get_object_by_id(datum_id)
datum_display = table.get_object_display(datum) or datum_id
if not table._filter_action(self, request, datum):
action_not_allowed.append(datum_display)
LOG.warning('Permission denied to %s: "%s"' %
(self._get_action_name(past=True).lower(),
datum_display))
continue
try:
self.action(request, datum_id)
# Call update to invoke changes if needed
self.update(request, datum)
action_success.append(datum_display)
self.success_ids.append(datum_id)
LOG.info('%s: "%s"' %
(self._get_action_name(past=True), datum_display))
except Exception as ex:
# Handle the exception but silence it since we'll display
# an aggregate error message later. Otherwise we'd get
# multiple error messages displayed to the user.
action_failure.append(datum_display)
action_description = (
self._get_action_name(past=True).lower(), datum_display)
LOG.error(
'Action %(action)s Failed for %(reason)s', {
'action': action_description, 'reason': ex})
if action_not_allowed:
msg = _('You are not allowed to %(action)s: %(objs)s')
params = {"action":
self._get_action_name(action_not_allowed).lower(),
"objs": functions.lazy_join(", ", action_not_allowed)}
messages.error(request, msg % params)
if action_failure:
msg = _('Unable to %(action)s: %(objs)s')
params = {"action": self._get_action_name(action_failure).lower(),
"objs": functions.lazy_join(", ", action_failure)}
messages.error(request, msg % params)
return shortcuts.redirect(self.get_success_url(request))
class ClusterGrowAction(tables.Action):
name = "grow_cluster_action"
verbose_name = _("Grow Cluster")
verbose_name_plural = _("Grow Cluster")
requires_input = False
icon = "plus"
def handle(self, table, request, obj_ids):
if not table.data:
msg = _("Cannot grow cluster. No instances specified.")
messages.info(request, msg)
return shortcuts.redirect(request.build_absolute_uri())
datum_display_objs = []
for instance in table.data:
msg = _("[flavor=%(flavor)s, volume=%(volume)s, name=%(name)s, "
"type=%(type)s, related_to=%(related_to)s]")
params = {"flavor": instance.flavor_id, "volume": instance.volume,
"name": instance.name, "type": instance.type,
"related_to": instance.related_to}
datum_display_objs.append(msg % params)
display_str = functions.lazy_join(", ", datum_display_objs)
cluster_id = table.kwargs['cluster_id']
try:
api.trove.cluster_grow(request, cluster_id, table.data)
LOG.info('%s: "%s"' % (_("Grow Cluster"), display_str))
msg = _('Scheduled growing of cluster.')
messages.success(request, msg)
except Exception as ex:
LOG.error('Action grow cluster failed with %(ex)s for %(data)s' %
{'ex': ex.message,
'data': display_str})
msg = _('Unable to grow cluster: %s')
messages.error(request, msg % ex.message)
finally:
cluster_manager.delete(cluster_id)
return shortcuts.redirect(urlresolvers.reverse(
"horizon:project:database_clusters:index"))
class ClusterGrowInstancesTable(tables.DataTable):
id = tables.Column("id", hidden=True)
name = tables.Column("name", verbose_name=_("Name"))
flavor = tables.Column("flavor", verbose_name=_("Flavor"))
flavor_id = tables.Column("flavor_id", hidden=True)
volume = tables.Column("volume", verbose_name=_("Volume"))
type = tables.Column("type", verbose_name=_("Instance Type"))
related_to = tables.Column("related_to", verbose_name=_("Related To"))
class Meta(object):
name = "cluster_grow_instances_table"
verbose_name = _("Instances")
table_actions = (ClusterGrowAddInstance, ClusterGrowRemoveInstance,
ClusterGrowAction)
row_actions = (ClusterGrowRemoveInstance,)

View File

@@ -0,0 +1,8 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% block modal-body-right %}
<p>{% trans "Specify the details of the instance to be added to the cluster." %}</p>
<p>{% trans "The name field is optional. If the field is left blank a name will be generated when the cluster is grown." %}</p>
<p>{% trans "The 'Instance Type' and 'Related To' fields are datastore specific and optional. See the Trove documentation for more information on using these fields." %}</p>
{% endblock %}

View File

@@ -1,25 +0,0 @@
{% 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,5 @@
{% extends "base.html" %}
{% block main %}
{% include "project/database_clusters/_add_instance.html" %}
{% endblock %}

View File

@@ -1,7 +0,0 @@
{% 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,14 @@
{% extends 'base.html' %}
{% load i18n %}
{% block main %}
<hr>
<div class="help_text">
{% trans "Specify the instances to be added to the cluster. When all the instances are specified click 'Grow Cluster' to perform the grow operation." %}
</div>
<div class="row">
<div class="col-sm-12">
{{ table.render }}
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,14 @@
{% extends 'base.html' %}
{% load i18n %}
{% block main %}
<hr>
<div class="help_text">
{% trans "Select the instance(s) that will be removed from the cluster." %}
</div>
<div class="row">
<div class="col-sm-12">
{{ table.render }}
</div>
</div>
{% endblock %}

View File

@@ -23,12 +23,14 @@ from openstack_dashboard import api
from troveclient import common
from trove_dashboard import api as trove_api
from trove_dashboard.content.database_clusters \
import cluster_manager
from trove_dashboard.content.database_clusters import tables
from trove_dashboard.test import helpers as test
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'
@@ -376,6 +378,179 @@ class ClustersTests(test.TestCase):
self.assertTemplateUsed(res, 'horizon/common/_detail.html')
self.assertContains(res, cluster.ip[0])
@test.create_stubs(
{trove_api.trove: ('cluster_get',
'cluster_grow'),
cluster_manager: ('get',)})
def test_grow_cluster(self):
cluster = self.trove_clusters.first()
trove_api.trove.cluster_get(IsA(http.HttpRequest), cluster.id)\
.AndReturn(cluster)
cluster_volume = 1
flavor = self.flavors.first()
cluster_flavor = flavor.id
cluster_flavor_name = flavor.name
instances = [
cluster_manager.ClusterInstance("id1", "name1", cluster_flavor,
cluster_flavor_name,
cluster_volume, "master", None),
cluster_manager.ClusterInstance("id2", "name2", cluster_flavor,
cluster_flavor_name,
cluster_volume, "slave", "master"),
cluster_manager.ClusterInstance("id3", None, cluster_flavor,
cluster_flavor_name,
cluster_volume, None, None),
]
manager = cluster_manager.ClusterInstanceManager(cluster.id)
manager.instances = instances
cluster_manager.get(cluster.id).MultipleTimes().AndReturn(manager)
trove_api.trove.cluster_grow(IsA(http.HttpRequest),
cluster.id,
instances)
self.mox.ReplayAll()
url = reverse('horizon:project:database_clusters:cluster_grow_details',
args=[cluster.id])
res = self.client.get(url)
self.assertTemplateUsed(
res, 'project/database_clusters/cluster_grow_details.html')
table = res.context_data[
"".join([tables.ClusterGrowInstancesTable.Meta.name, '_table'])]
self.assertEqual(len(cluster.instances), len(table.data))
action = "".join([tables.ClusterGrowInstancesTable.Meta.name, '__',
tables.ClusterGrowRemoveInstance.name, '__',
'id1'])
self.client.post(url, {'action': action})
self.assertEqual(len(cluster.instances) - 1, len(table.data))
action = "".join([tables.ClusterGrowInstancesTable.Meta.name, '__',
tables.ClusterGrowAction.name, '__',
cluster.id])
res = self.client.post(url, {'action': action})
self.assertMessageCount(success=1)
self.assertRedirectsNoFollow(res, INDEX_URL)
@test.create_stubs({trove_api.trove: ('cluster_get',)})
def test_grow_cluster_no_instances(self):
cluster = self.trove_clusters.first()
trove_api.trove.cluster_get(IsA(http.HttpRequest), cluster.id)\
.AndReturn(cluster)
self.mox.ReplayAll()
url = reverse('horizon:project:database_clusters:cluster_grow_details',
args=[cluster.id])
res = self.client.get(url)
self.assertTemplateUsed(
res, 'project/database_clusters/cluster_grow_details.html')
action = "".join([tables.ClusterGrowInstancesTable.Meta.name, '__',
tables.ClusterGrowAction.name, '__',
cluster.id])
self.client.post(url, {'action': action})
self.assertMessageCount(info=1)
@test.create_stubs(
{trove_api.trove: ('cluster_get',
'cluster_grow',),
cluster_manager: ('get',)})
def test_grow_cluster_exception(self):
cluster = self.trove_clusters.first()
trove_api.trove.cluster_get(IsA(http.HttpRequest), cluster.id)\
.AndReturn(cluster)
cluster_volume = 1
flavor = self.flavors.first()
cluster_flavor = flavor.id
cluster_flavor_name = flavor.name
instances = [
cluster_manager.ClusterInstance("id1", "name1", cluster_flavor,
cluster_flavor_name,
cluster_volume, "master", None),
cluster_manager.ClusterInstance("id2", "name2", cluster_flavor,
cluster_flavor_name,
cluster_volume, "slave", "master"),
cluster_manager.ClusterInstance("id3", None, cluster_flavor,
cluster_flavor_name,
cluster_volume, None, None),
]
manager = cluster_manager.ClusterInstanceManager(cluster.id)
manager.instances = instances
cluster_manager.get(cluster.id).MultipleTimes().AndReturn(manager)
trove_api.trove.cluster_grow(IsA(http.HttpRequest),
cluster.id,
instances).AndRaise(self.exceptions.trove)
self.mox.ReplayAll()
url = reverse('horizon:project:database_clusters:cluster_grow_details',
args=[cluster.id])
res = self.client.get(url)
self.assertTemplateUsed(
res, 'project/database_clusters/cluster_grow_details.html')
action = "".join([tables.ClusterGrowInstancesTable.Meta.name, '__',
tables.ClusterGrowAction.name, '__',
cluster.id])
res = self.client.post(url, {'action': action})
self.assertMessageCount(error=1)
self.assertRedirectsNoFollow(res, INDEX_URL)
@test.create_stubs({trove_api.trove: ('cluster_get',
'cluster_shrink')})
def test_shrink_cluster(self):
cluster = self.trove_clusters.first()
trove_api.trove.cluster_get(IsA(http.HttpRequest), cluster.id)\
.MultipleTimes().AndReturn(cluster)
instance_id = cluster.instances[0]['id']
cluster_instances = [{'id': instance_id}]
trove_api.trove.cluster_shrink(IsA(http.HttpRequest),
cluster.id,
cluster_instances)
self.mox.ReplayAll()
url = reverse(
'horizon:project:database_clusters:cluster_shrink_details',
args=[cluster.id])
res = self.client.get(url)
self.assertTemplateUsed(
res, 'project/database_clusters/cluster_shrink_details.html')
table = res.context_data[
"".join([tables.ClusterShrinkInstancesTable.Meta.name, '_table'])]
self.assertEqual(len(cluster.instances), len(table.data))
action = "".join([tables.ClusterShrinkInstancesTable.Meta.name, '__',
tables.ClusterShrinkAction.name, '__',
instance_id])
res = self.client.post(url, {'action': action})
self.assertNoFormErrors(res)
self.assertMessageCount(info=1)
self.assertRedirectsNoFollow(res, INDEX_URL)
@test.create_stubs({trove_api.trove: ('cluster_get',
'cluster_shrink')})
def test_shrink_cluster_exception(self):
cluster = self.trove_clusters.first()
trove_api.trove.cluster_get(IsA(http.HttpRequest), cluster.id)\
.MultipleTimes().AndReturn(cluster)
cluster_id = cluster.instances[0]['id']
cluster_instances = [cluster_id]
trove_api.trove.cluster_shrink(IsA(http.HttpRequest),
cluster.id,
cluster_instances)\
.AndRaise(self.exceptions.trove)
self.mox.ReplayAll()
url = reverse(
'horizon:project:database_clusters:cluster_shrink_details',
args=[cluster.id])
action = "".join([tables.ClusterShrinkInstancesTable.Meta.name, '__',
tables.ClusterShrinkAction.name, '__',
cluster_id])
res = self.client.post(url, {'action': action})
self.assertMessageCount(error=1)
self.assertRedirectsNoFollow(res, INDEX_URL)
def _get_filtered_datastores(self, datastore):
filtered_datastore = []
for ds in self.datastores.list():

View File

@@ -27,8 +27,16 @@ urlpatterns = patterns(
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(),
url(CLUSTERS % 'cluster_grow_details',
views.ClusterGrowView.as_view(),
name='cluster_grow_details'),
url(CLUSTERS % 'add_instance',
views.ClusterAddInstancesView.as_view(),
name='add_instance'),
url(CLUSTERS % 'cluster_shrink_details',
views.ClusterShrinkView.as_view(),
name='cluster_shrink_details'),
url(CLUSTERS % 'reset_password',
views.ResetPasswordView.as_view(),
name='reset_password'),
)

View File

@@ -33,6 +33,8 @@ from horizon import tabs as horizon_tabs
from horizon.utils import memoized
from trove_dashboard import api
from trove_dashboard.content.database_clusters \
import cluster_manager
from trove_dashboard.content.database_clusters import forms
from trove_dashboard.content.database_clusters import tables
from trove_dashboard.content.database_clusters import tabs
@@ -137,57 +139,92 @@ class DetailView(horizon_tabs.TabbedTableView):
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")
class ClusterGrowView(horizon_tables.DataTableView):
table_class = tables.ClusterGrowInstancesTable
template_name = 'project/database_clusters/cluster_grow_details.html'
page_title = _("Grow Cluster: {{cluster_name}}")
def get_data(self):
manager = cluster_manager.get(self.kwargs['cluster_id'])
return manager.get_instances()
def get_context_data(self, **kwargs):
context = super(AddShardView, self).get_context_data(**kwargs)
context["cluster_id"] = self.kwargs['cluster_id']
context = super(ClusterGrowView, self).get_context_data(**kwargs)
context['cluster_id'] = self.kwargs['cluster_id']
cluster = self.get_cluster(self.kwargs['cluster_id'])
context['cluster_name'] = cluster.name
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
@memoized.memoized_method
def get_cluster(self, cluster_id):
try:
return api.trove.cluster_get(self.request, cluster_id)
except Exception:
redirect = reverse("horizon:project:database_clusters:index")
msg = _('Unable to retrieve cluster details.')
exceptions.handle(self.request, msg, redirect=redirect)
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 ClusterAddInstancesView(horizon_forms.ModalFormView):
form_class = forms.ClusterAddInstanceForm
form_id = "cluster_add_instances_form"
modal_header = _("Add Instance")
modal_id = "cluster_add_instances_modal"
template_name = "project/database_clusters/add_instance.html"
submit_label = _("Add")
submit_url = "horizon:project:database_clusters:add_instance"
success_url = "horizon:project:database_clusters:cluster_grow_details"
cancel_url = "horizon:project:database_clusters:cluster_grow_details"
page_title = _("Add Instance")
def get_context_data(self, **kwargs):
context = (super(ClusterAddInstancesView, self)
.get_context_data(**kwargs))
context['cluster_id'] = self.kwargs['cluster_id']
args = (self.kwargs['cluster_id'],)
context['submit_url'] = reverse(self.submit_url, args=args)
return context
def get_success_url(self):
return reverse(self.success_url, args=[self.kwargs['cluster_id']])
def get_cancel_url(self):
return reverse(self.cancel_url, args=[self.kwargs['cluster_id']])
class ClusterInstance(object):
def __init__(self, id, name, status):
self.id = id
self.name = name
self.status = status
class ClusterShrinkView(horizon_tables.DataTableView):
table_class = tables.ClusterShrinkInstancesTable
template_name = "project/database_clusters/cluster_shrink_details.html"
page_title = _("Shrink Cluster: {{cluster_name}}")
@memoized.memoized_method
def get_cluster(self, cluster_id):
try:
return api.trove.cluster_get(self.request, cluster_id)
except Exception:
redirect = reverse("horizon:project:database_clusters:index")
msg = _('Unable to retrieve cluster details.')
exceptions.handle(self.request, msg, redirect=redirect)
def get_data(self):
cluster = self.get_cluster(self.kwargs['cluster_id'])
instances = [ClusterInstance(i['id'], i['name'], i['status'])
for i in cluster.instances]
return instances
def get_context_data(self, **kwargs):
context = super(ClusterShrinkView, self).get_context_data(**kwargs)
context['cluster_id'] = self.kwargs['cluster_id']
cluster = self.get_cluster(self.kwargs['cluster_id'])
context['cluster_name'] = cluster.name
return context
class ResetPasswordView(horizon_forms.ModalFormView):

View File

@@ -21,6 +21,10 @@ VERTICA = "vertica"
_cluster_capable_datastores = (MONGODB, PERCONA_CLUSTER, REDIS, VERTICA)
def can_modify_cluster(datastore):
return (is_mongodb_datastore(datastore) or is_redis_datastore(datastore))
def is_mongodb_datastore(datastore):
return (datastore is not None) and (MONGODB in datastore.lower())

View File

@@ -40,6 +40,8 @@ CLUSTER_DATA_ONE = {
{
"id": "416b0b16-ba55-4302-bbd3-ff566032e1c1",
"shard_id": "5415b62f-f301-4e84-ba90-8ab0734d15a7",
"name": "inst1",
"status": "ACTIVE",
"flavor": {
"id": "7",
"links": []
@@ -51,6 +53,8 @@ CLUSTER_DATA_ONE = {
{
"id": "965ef811-7c1d-47fc-89f2-a89dfdd23ef2",
"shard_id": "5415b62f-f301-4e84-ba90-8ab0734d15a7",
"name": "inst2",
"status": "ACTIVE",
"flavor": {
"id": "7",
"links": []
@@ -62,6 +66,8 @@ CLUSTER_DATA_ONE = {
{
"id": "3642f41c-e8ad-4164-a089-3891bf7f2d2b",
"shard_id": "5415b62f-f301-4e84-ba90-8ab0734d15a7",
"name": "inst3",
"status": "ACTIVE",
"flavor": {
"id": "7",
"links": []
@@ -91,6 +97,8 @@ CLUSTER_DATA_TWO = {
"instances": [
{
"id": "416b0b16-ba55-4302-bbd3-ff566032e1c1",
"name": "inst1",
"status": "ACTIVE",
"flavor": {
"id": "7",
"links": []
@@ -101,6 +109,8 @@ CLUSTER_DATA_TWO = {
},
{
"id": "965ef811-7c1d-47fc-89f2-a89dfdd23ef2",
"name": "inst2",
"status": "ACTIVE",
"flavor": {
"id": "7",
"links": []
@@ -111,6 +121,8 @@ CLUSTER_DATA_TWO = {
},
{
"id": "3642f41c-e8ad-4164-a089-3891bf7f2d2b",
"name": "inst3",
"status": "ACTIVE",
"flavor": {
"id": "7",
"links": []