Support for resizing a trove instance

Added menu item and dialog to allow the
user to select a new flavor for their
trove instance

Change-Id: I5ef42067887e1f1f1f5ee3224df7b6391be0a375
Implements: blueprint trove-resize-instance-dialog
This commit is contained in:
Andrew Bramley 2014-07-17 21:10:55 -04:00
parent fd6e194463
commit 178f608c2b
9 changed files with 252 additions and 6 deletions

View File

@ -80,6 +80,11 @@ def instance_resize_volume(request, instance_id, size):
return troveclient(request).instances.resize_volume(instance_id, size)
def instance_resize(request, instance_id, flavor_id):
return troveclient(request).instances.resize_instance(instance_id,
flavor_id)
def instance_backups(request, instance_id):
return troveclient(request).instances.backups(instance_id)

View File

@ -53,3 +53,46 @@ class ResizeVolumeForm(forms.SelfHandlingForm):
exceptions.handle(request, _('Unable to resize volume. %s') %
e.message, redirect=redirect)
return True
class ResizeInstanceForm(forms.SelfHandlingForm):
instance_id = forms.CharField(widget=forms.HiddenInput())
old_flavor_name = forms.CharField(label=_("Old Flavor"),
required=False,
widget=forms.TextInput(
attrs={'readonly': 'readonly'}))
new_flavor = forms.ChoiceField(label=_("New Flavor"),
help_text=_("Choose a new instance "
"flavor."))
def __init__(self, request, *args, **kwargs):
super(ResizeInstanceForm, self).__init__(request, *args, **kwargs)
choices = kwargs.get('initial', {}).get('flavors')
if choices:
choices.insert(0, ("", _("Select a new flavor")))
else:
choices.insert(0, ("", _("No flavors available")))
self.fields['new_flavor'].choices = choices
def clean(self):
cleaned_data = super(ResizeInstanceForm, self).clean()
flavor = cleaned_data.get('new_flavor', None)
if flavor is None or flavor == self.initial['old_flavor_id']:
raise forms.ValidationError(_('Please choose a new flavor that '
'is not the same as the old one.'))
return cleaned_data
def handle(self, request, data):
instance = data.get('instance_id')
flavor = data.get('new_flavor')
try:
api.trove.instance_resize(request, instance, flavor)
messages.success(request, _('Resizing instance "%s"') % instance)
except Exception as e:
redirect = reverse("horizon:project:databases:index")
exceptions.handle(request, _('Unable to resize instance. %s') %
e.message, redirect=redirect)
return True

View File

@ -173,6 +173,21 @@ class ResizeVolume(tables.LinkAction):
return urlresolvers.reverse(self.url, args=[instance_id])
class ResizeInstance(tables.LinkAction):
name = "resize_instance"
verbose_name = _("Resize Instance")
url = "horizon:project:databases:resize_instance"
classes = ("ajax-modal", "btn-resize")
def allowed(self, request, instance=None):
return ((instance.status in ACTIVE_STATES
or instance.status == 'SHUTOFF'))
def get_link_url(self, datum):
instance_id = self.table.get_object_id(datum)
return urlresolvers.reverse(self.url, args=[instance_id])
class UpdateRow(tables.Row):
ajax = True
@ -272,6 +287,7 @@ class InstancesTable(tables.DataTable):
table_actions = (LaunchLink, TerminateInstance)
row_actions = (CreateBackup,
ResizeVolume,
ResizeInstance,
RestartInstance,
TerminateInstance)

View File

@ -0,0 +1,26 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% load url from future %}
{% block form_id %}resize_instance_form{% endblock %}
{% block form_action %}{% url "horizon:project:databases:resize_instance" instance_id %}{% endblock %}
{% block modal_id %}resize_instance_modal{% endblock %}
{% block modal-header %}{% trans "Resize Database Instance" %}{% endblock %}
{% block modal-body %}
<div class="left">
<fieldset>
{% include "horizon/common/_form_fields.html" %}
</fieldset>
</div>
<div class="right">
<p>{% blocktrans %}Specify a new flavor for the database instance.{% endblocktrans %}</p>
</div>
{% endblock %}
{% block modal-footer %}
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Resize Database Instance" %}" />
<a href="{% url "horizon:project:databases:index" %}" class="btn btn-default secondary cancel close">{% trans "Cancel" %}</a>
{% endblock %}

View File

@ -0,0 +1,11 @@
{% extends "base.html" %}
{% load i18n %}
{% block title %}{% trans "Resize Database Instance" %}{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Resize Database Instance") %}
{% endblock %}
{% block main %}
{% include "project/databases/_resize_instance.html" %}
{% endblock %}

View File

@ -339,8 +339,7 @@ class DatabaseTests(test.TestCase):
self.assertRedirectsNoFollow(res, url)
@test.create_stubs({
api.trove: ('instance_get', 'instance_resize_volume'),
})
api.trove: ('instance_get', 'instance_resize_volume')})
def test_resize_volume(self):
database = self.databases.first()
database_id = database.id
@ -367,9 +366,7 @@ class DatabaseTests(test.TestCase):
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
@test.create_stubs({
api.trove: ('instance_get', 'instance_resize_volume'),
})
@test.create_stubs({api.trove: ('instance_get', )})
def test_resize_volume_bad_value(self):
database = self.databases.first()
database_id = database.id
@ -390,3 +387,62 @@ class DatabaseTests(test.TestCase):
res = self.client.post(url, post)
self.assertContains(
res, "New size for volume must be greater than current size.")
@test.create_stubs(
{api.trove: ('instance_get',
'flavor_list',
'instance_resize')})
def test_resize_instance(self):
database = self.databases.first()
# views.py: DetailView.get_data
api.trove.instance_get(IsA(http.HttpRequest), database.id)\
.AndReturn(database)
api.trove.flavor_list(IsA(http.HttpRequest)).\
AndReturn(self.database_flavors.list())
old_flavor = self.database_flavors.list()[0]
new_flavor = self.database_flavors.list()[1]
api.trove.instance_resize(IsA(http.HttpRequest),
database.id,
new_flavor.id).AndReturn(None)
self.mox.ReplayAll()
url = reverse('horizon:project:databases:resize_instance',
args=[database.id])
post = {
'instance_id': database.id,
'old_flavor_name': old_flavor.name,
'old_flavor_id': old_flavor.id,
'new_flavor': new_flavor.id
}
res = self.client.post(url, post)
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
@test.create_stubs(
{api.trove: ('instance_get', 'flavor_list')})
def test_resize_instance_bad_value(self):
database = self.databases.first()
api.trove.instance_get(IsA(http.HttpRequest),
database.id).AndReturn(database)
api.trove.flavor_list(IsA(http.HttpRequest)).\
AndReturn(self.database_flavors.list())
old_flavor = self.database_flavors.list()[0]
self.mox.ReplayAll()
url = reverse('horizon:project:databases:resize_instance',
args=[database.id])
post = {
'instance_id': database.id,
'old_flavor_name': old_flavor.name,
'old_flavor_id': old_flavor.id,
'new_flavor': old_flavor.id
}
res = self.client.post(url, post)
self.assertContains(res,
"Please choose a new flavor that is "
"not the same as the old one.")

View File

@ -27,5 +27,7 @@ urlpatterns = patterns(
url(r'^launch$', views.LaunchInstanceView.as_view(), name='launch'),
url(INSTANCES % '', views.DetailView.as_view(), name='detail'),
url(INSTANCES % 'resize_volume', views.ResizeVolumeView.as_view(),
name='resize_volume')
name='resize_volume'),
url(INSTANCES % 'resize_instance', views.ResizeInstanceView.as_view(),
name='resize_instance')
)

View File

@ -35,6 +35,9 @@ from openstack_dashboard.dashboards.project.databases import tables
from openstack_dashboard.dashboards.project.databases import tabs
from openstack_dashboard.dashboards.project.databases import workflows
from openstack_dashboard.dashboards.project.instances \
import utils as instance_utils
LOG = logging.getLogger(__name__)
@ -156,3 +159,58 @@ class ResizeVolumeView(horizon_forms.ModalFormView):
instance = self.get_object()
return {'instance_id': self.kwargs['instance_id'],
'orig_size': instance.volume.get('size', 0)}
class ResizeInstanceView(horizon_forms.ModalFormView):
form_class = forms.ResizeInstanceForm
template_name = 'project/databases/resize_instance.html'
success_url = reverse_lazy('horizon:project:databases:index')
@memoized.memoized_method
def get_object(self, *args, **kwargs):
instance_id = self.kwargs['instance_id']
try:
instance = api.trove.instance_get(self.request, instance_id)
flavor_id = instance.flavor['id']
flavors = {}
for i, j in self.get_flavors():
flavors[str(i)] = j
if flavor_id in flavors:
instance.flavor_name = flavors[flavor_id]
else:
flavor = api.trove.flavor_get(self.request, flavor_id)
instance.flavor_name = flavor.name
return instance
except Exception:
redirect = reverse('horizon:project:databases:index')
msg = _('Unable to retrieve instance details.')
exceptions.handle(self.request, msg, redirect=redirect)
def get_context_data(self, **kwargs):
context = super(ResizeInstanceView, self).get_context_data(**kwargs)
context['instance_id'] = self.kwargs['instance_id']
return context
@memoized.memoized_method
def get_flavors(self, *args, **kwargs):
try:
flavors = api.trove.flavor_list(self.request)
return instance_utils.sort_flavor_list(self.request, flavors)
except Exception:
redirect = reverse("horizon:project:databases:index")
exceptions.handle(self.request,
_('Unable to retrieve flavors.'),
redirect=redirect)
def get_initial(self):
initial = super(ResizeInstanceView, self).get_initial()
obj = self.get_object()
if obj:
initial.update({'instance_id': self.kwargs['instance_id'],
'old_flavor_id': obj.flavor['id'],
'old_flavor_name': getattr(obj,
'flavor_name', ''),
'flavors': self.get_flavors()})
return initial

View File

@ -15,6 +15,7 @@
from troveclient.v1 import backups
from troveclient.v1 import databases
from troveclient.v1 import datastores
from troveclient.v1 import flavors
from troveclient.v1 import instances
from troveclient.v1 import users
@ -149,6 +150,27 @@ VERSION_TWO = {
"id": "500a6d52-8347-4e00-8e4c-f4fa9cf96ae9"
}
FLAVOR_ONE = {
"ram": 512,
"id": "1",
"links": [],
"name": "m1.tiny"
}
FLAVOR_TWO = {
"ram": 768,
"id": "10",
"links": [],
"name": "eph.rd-smaller"
}
FLAVOR_THREE = {
"ram": 800,
"id": "100",
"links": [],
"name": "test.1"
}
def data(TEST):
database1 = instances.Instance(instances.Instances(None),
@ -172,10 +194,16 @@ def data(TEST):
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)
TEST.databases = utils.TestDataContainer()
TEST.database_backups = utils.TestDataContainer()
TEST.database_users = utils.TestDataContainer()
TEST.database_user_dbs = utils.TestDataContainer()
TEST.database_flavors = utils.TestDataContainer()
TEST.databases.add(database1)
TEST.databases.add(database2)
TEST.database_backups.add(bkup1)
@ -188,3 +216,4 @@ def data(TEST):
TEST.datastore_versions = utils.TestDataContainer()
TEST.datastore_versions.add(version1)
TEST.datastore_versions.add(version2)
TEST.database_flavors.add(flavor1, flavor2, flavor3)