diff --git a/openstack_dashboard/api/trove.py b/openstack_dashboard/api/trove.py index 481442bbe4..208a1ea01a 100644 --- a/openstack_dashboard/api/trove.py +++ b/openstack_dashboard/api/trove.py @@ -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) diff --git a/openstack_dashboard/dashboards/project/databases/forms.py b/openstack_dashboard/dashboards/project/databases/forms.py index 32b21d41bd..da06ae2c75 100644 --- a/openstack_dashboard/dashboards/project/databases/forms.py +++ b/openstack_dashboard/dashboards/project/databases/forms.py @@ -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 diff --git a/openstack_dashboard/dashboards/project/databases/tables.py b/openstack_dashboard/dashboards/project/databases/tables.py index 23fb017450..ef9f99613a 100644 --- a/openstack_dashboard/dashboards/project/databases/tables.py +++ b/openstack_dashboard/dashboards/project/databases/tables.py @@ -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) diff --git a/openstack_dashboard/dashboards/project/databases/templates/databases/_resize_instance.html b/openstack_dashboard/dashboards/project/databases/templates/databases/_resize_instance.html new file mode 100644 index 0000000000..8de3c3390f --- /dev/null +++ b/openstack_dashboard/dashboards/project/databases/templates/databases/_resize_instance.html @@ -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 %} +
+
+ {% include "horizon/common/_form_fields.html" %} +
+
+
+

{% blocktrans %}Specify a new flavor for the database instance.{% endblocktrans %}

+
+{% endblock %} + +{% block modal-footer %} + + {% trans "Cancel" %} +{% endblock %} + diff --git a/openstack_dashboard/dashboards/project/databases/templates/databases/resize_instance.html b/openstack_dashboard/dashboards/project/databases/templates/databases/resize_instance.html new file mode 100644 index 0000000000..ebbcf83aba --- /dev/null +++ b/openstack_dashboard/dashboards/project/databases/templates/databases/resize_instance.html @@ -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 %} \ No newline at end of file diff --git a/openstack_dashboard/dashboards/project/databases/tests.py b/openstack_dashboard/dashboards/project/databases/tests.py index 6a23686800..501c724924 100644 --- a/openstack_dashboard/dashboards/project/databases/tests.py +++ b/openstack_dashboard/dashboards/project/databases/tests.py @@ -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.") diff --git a/openstack_dashboard/dashboards/project/databases/urls.py b/openstack_dashboard/dashboards/project/databases/urls.py index 701dbf6e79..0b62bebf05 100644 --- a/openstack_dashboard/dashboards/project/databases/urls.py +++ b/openstack_dashboard/dashboards/project/databases/urls.py @@ -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') ) diff --git a/openstack_dashboard/dashboards/project/databases/views.py b/openstack_dashboard/dashboards/project/databases/views.py index 4c2db269e9..191edbb457 100644 --- a/openstack_dashboard/dashboards/project/databases/views.py +++ b/openstack_dashboard/dashboards/project/databases/views.py @@ -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 diff --git a/openstack_dashboard/test/test_data/trove_data.py b/openstack_dashboard/test/test_data/trove_data.py index 40ce597fa2..a791f2acac 100644 --- a/openstack_dashboard/test/test_data/trove_data.py +++ b/openstack_dashboard/test/test_data/trove_data.py @@ -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)