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:
parent
fd6e194463
commit
178f608c2b
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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 %}
|
||||
|
@ -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 %}
|
@ -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.")
|
||||
|
@ -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')
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user