From 2dddc39a0b2fe6bca26847edf02658e14400c059 Mon Sep 17 00:00:00 2001 From: Kieran Spear Date: Mon, 7 Oct 2013 13:17:05 +1100 Subject: [PATCH] Show an error message on instance launch error Since instance creation happens asynchronously, all that currently happens on the Horizon side is the state changes from Creating to Error. This change checks the instance state during the Ajax request on the backend and returns an Ajax error message popup if the status is 'ERROR'. Change-Id: I259e27b7b6c73a38098514a701c235feb81af143 Closes-bug: 1236168. --- openstack_dashboard/api/nova.py | 2 +- .../dashboards/project/instances/tables.py | 26 ++++++ .../dashboards/project/instances/tests.py | 86 +++++++++++++++++++ 3 files changed, 113 insertions(+), 1 deletion(-) diff --git a/openstack_dashboard/api/nova.py b/openstack_dashboard/api/nova.py index 1e9b1d7424..2686cc5616 100644 --- a/openstack_dashboard/api/nova.py +++ b/openstack_dashboard/api/nova.py @@ -71,7 +71,7 @@ class Server(base.APIResourceWrapper): """ _attrs = ['addresses', 'attrs', 'id', 'image', 'links', 'metadata', 'name', 'private_ip', 'public_ip', 'status', 'uuid', - 'image_name', 'VirtualInterfaces', 'flavor', 'key_name', + 'image_name', 'VirtualInterfaces', 'flavor', 'key_name', 'fault', 'tenant_id', 'user_id', 'OS-EXT-STS:power_state', 'OS-EXT-STS:task_state', 'OS-EXT-SRV-ATTR:instance_name', 'OS-EXT-SRV-ATTR:host', 'created'] diff --git a/openstack_dashboard/dashboards/project/instances/tables.py b/openstack_dashboard/dashboards/project/instances/tables.py index 87ee0dccbe..c409decd2a 100644 --- a/openstack_dashboard/dashboards/project/instances/tables.py +++ b/openstack_dashboard/dashboards/project/instances/tables.py @@ -431,6 +431,29 @@ class SimpleDisassociateIP(tables.Action): return shortcuts.redirect("horizon:project:instances:index") +def instance_fault_to_friendly_message(instance): + fault = getattr(instance, 'fault', {}) + message = fault.get('message', _("Unknown")) + default_message = _("Please try again later [Error: %s].") % message + fault_map = { + 'NoValidHost': _("There is not enough capacity for this " + "flavor in the selected availability zone. " + "Try again later or select a different availability " + "zone.") + } + return fault_map.get(message, default_message) + + +def get_instance_error(instance): + if instance.status.lower() != 'error': + return None + message = instance_fault_to_friendly_message(instance) + preamble = _('Failed to launch instance "%s"' + ) % instance.name or instance.id + message = string_concat(preamble, ': ', message) + return message + + class UpdateRow(tables.Row): ajax = True @@ -438,6 +461,9 @@ class UpdateRow(tables.Row): instance = api.nova.server_get(request, instance_id) instance.full_flavor = api.nova.flavor_get(request, instance.flavor["id"]) + error = get_instance_error(instance) + if error: + messages.error(request, error) return instance diff --git a/openstack_dashboard/dashboards/project/instances/tests.py b/openstack_dashboard/dashboards/project/instances/tests.py index 0667b9c5a4..f982e40249 100644 --- a/openstack_dashboard/dashboards/project/instances/tests.py +++ b/openstack_dashboard/dashboards/project/instances/tests.py @@ -18,6 +18,7 @@ # License for the specific language governing permissions and limitations # under the License. +import json import uuid from django.core.urlresolvers import reverse # noqa @@ -2043,3 +2044,88 @@ class InstanceTests(test.TestCase): password=password, confirm_password=password) self.assertRedirectsNoFollow(res, INDEX_URL) + + +class InstanceAjaxTests(test.TestCase): + @test.create_stubs({api.nova: ("server_get", + "flavor_get", + "extension_supported"), + api.neutron: ("is_extension_supported",)}) + def test_row_update(self): + server = self.servers.first() + instance_id = server.id + flavor_id = server.flavor["id"] + flavors = self.flavors.list() + full_flavors = SortedDict([(f.id, f) for f in flavors]) + + api.nova.extension_supported('AdminActions', IsA(http.HttpRequest))\ + .MultipleTimes().AndReturn(True) + api.neutron.is_extension_supported(IsA(http.HttpRequest), + 'security-group')\ + .MultipleTimes().AndReturn(True) + api.nova.server_get(IsA(http.HttpRequest), instance_id)\ + .AndReturn(server) + api.nova.flavor_get(IsA(http.HttpRequest), flavor_id)\ + .AndReturn(full_flavors[flavor_id]) + + self.mox.ReplayAll() + + params = {'action': 'row_update', + 'table': 'instances', + 'obj_id': instance_id, + } + res = self.client.get('?'.join((INDEX_URL, urlencode(params))), + HTTP_X_REQUESTED_WITH='XMLHttpRequest') + self.assertContains(res, server.name) + + @test.create_stubs({api.nova: ("server_get", + "flavor_get", + "extension_supported"), + api.neutron: ("is_extension_supported",)}) + def test_row_update_instance_error(self): + server = self.servers.first() + instance_id = server.id + flavor_id = server.flavor["id"] + flavors = self.flavors.list() + full_flavors = SortedDict([(f.id, f) for f in flavors]) + + server.status = 'ERROR' + server.fault = {"message": "NoValidHost", + "code": 500, + "details": "No valid host was found. \n " + "File \"/mnt/stack/nova/nova/" + "scheduler/filter_scheduler.py\", " + "line 105, in schedule_run_instance\n " + "raise exception.NoValidHost" + "(reason=\"\")\n", + "created": "2013-10-07T00:08:32Z"} + + api.nova.extension_supported('AdminActions', IsA(http.HttpRequest))\ + .MultipleTimes().AndReturn(True) + api.neutron.is_extension_supported(IsA(http.HttpRequest), + 'security-group')\ + .MultipleTimes().AndReturn(True) + api.nova.server_get(IsA(http.HttpRequest), instance_id)\ + .AndReturn(server) + api.nova.flavor_get(IsA(http.HttpRequest), flavor_id)\ + .AndReturn(full_flavors[flavor_id]) + + self.mox.ReplayAll() + + params = {'action': 'row_update', + 'table': 'instances', + 'obj_id': instance_id, + } + res = self.client.get('?'.join((INDEX_URL, urlencode(params))), + HTTP_X_REQUESTED_WITH='XMLHttpRequest') + self.assertContains(res, server.name) + self.assertTrue(res.has_header('X-Horizon-Messages')) + messages = json.loads(res['X-Horizon-Messages']) + self.assertEqual(len(messages), 1) + # (Pdb) messages + # [[u'error', u'Failed to launch instance "server_1": \ + # There is not enough capacity for this flavor in the \ + # selected availability zone. Try again later or select \ + # a different availability zone.', u'']] + self.assertEqual(messages[0][0], 'error') + self.assertTrue(messages[0][1].startswith('Failed'))