diff --git a/openstack_dashboard/dashboards/project/instances/tables.py b/openstack_dashboard/dashboards/project/instances/tables.py index 674ebdca28..624a59597b 100644 --- a/openstack_dashboard/dashboards/project/instances/tables.py +++ b/openstack_dashboard/dashboards/project/instances/tables.py @@ -39,6 +39,8 @@ from horizon.utils import filters from openstack_dashboard import api from openstack_dashboard.dashboards.project.floating_ips import workflows from openstack_dashboard.dashboards.project.instances import tabs +from openstack_dashboard.dashboards.project.instances \ + import utils as instance_utils from openstack_dashboard.dashboards.project.instances.workflows \ import resize_instance from openstack_dashboard.dashboards.project.instances.workflows \ @@ -789,8 +791,8 @@ class UpdateRow(tables.Row): def get_data(self, request, instance_id): instance = api.nova.server_get(request, instance_id) try: - instance.full_flavor = api.nova.flavor_get(request, - instance.flavor["id"]) + instance.full_flavor = instance_utils.resolve_flavor(request, + instance) except Exception: exceptions.handle(request, _('Unable to retrieve flavor information ' @@ -1041,7 +1043,7 @@ def get_flavor(instance): "size_disk": size_disk, "size_ram": size_ram, "vcpus": instance.full_flavor.vcpus, - "flavor_id": instance.full_flavor.id + "flavor_id": getattr(instance.full_flavor, 'id', None) } return template.loader.render_to_string(template_name, context) return _("Not available") diff --git a/openstack_dashboard/dashboards/project/instances/tests.py b/openstack_dashboard/dashboards/project/instances/tests.py index 69cf67e407..5eafb48b2f 100644 --- a/openstack_dashboard/dashboards/project/instances/tests.py +++ b/openstack_dashboard/dashboards/project/instances/tests.py @@ -4147,13 +4147,26 @@ class InstanceTests2(InstanceTestBase, InstanceTableTestMixin): def test_disassociate_floating_ip_with_release(self): self._test_disassociate_floating_ip(is_release=True) + def _populate_server_flavor_nova_api_ge_2_47(self, server): + flavor_id = server.flavor['id'] + flavor = self.flavors.get(id=flavor_id) + server.flavor = { + 'original_name': flavor.name, + 'vcpus': flavor.vcpus, + 'ram': flavor.ram, + 'swap': flavor.swap, + 'disk': flavor.disk, + 'ephemeral': flavor.ephemeral, + 'extra_specs': flavor.extra_specs, + } + return server + @helpers.create_mocks({api.nova: ('server_get', 'flavor_list', 'server_group_list', 'tenant_absolute_limits', 'is_feature_available')}) - def test_instance_resize_get(self): - server = self.servers.first() + def _test_instance_resize_get(self, server, nova_api_lt_2_47=False): self.mock_server_get.return_value = server self.mock_flavor_list.return_value = self.flavors.list() self.mock_server_group_list.return_value = self.server_groups.list() @@ -4162,14 +4175,35 @@ class InstanceTests2(InstanceTestBase, InstanceTableTestMixin): url = reverse('horizon:project:instances:resize', args=[server.id]) res = self.client.get(url) + workflow = res.context['workflow'] self.assertTemplateUsed(res, views.WorkflowView.template_name) + self.assertEqual(res.context['workflow'].name, + workflows.ResizeInstance.name) + self.assertContains(res, 'Disk Partition') config_drive_field_label = 'Configuration Drive' self.assertNotContains(res, config_drive_field_label) + step = workflow.get_step("flavor_choice") + self.assertEqual(step.action.initial['old_flavor_name'], + self.flavors.first().name) + + step = workflow.get_step("setadvancedaction") + self.assertEqual(step.action.fields['disk_config'].label, + 'Disk Partition') + self.assertQuerysetEqual(workflow.steps, + ['', + '']) option = '' + + def is_original_flavor(server, flavor, nova_api_lt_2_47): + if nova_api_lt_2_47: + return flavor.id == server.flavor['id'] + else: + return flavor.name == server.flavor['original_name'] + for flavor in self.flavors.list(): - if flavor.id == server.flavor['id']: + if is_original_flavor(server, flavor, nova_api_lt_2_47): self.assertNotContains(res, option % (flavor.id, flavor.name)) else: self.assertContains(res, option % (flavor.id, flavor.name)) @@ -4184,6 +4218,15 @@ class InstanceTests2(InstanceTestBase, InstanceTableTestMixin): self.mock_tenant_absolute_limits.assert_called_once_with( helpers.IsHttpRequest(), reserved=True) + def test_instance_resize_get_nova_api_lt_2_47(self): + server = self.servers.first() + self._test_instance_resize_get(server, nova_api_lt_2_47=True) + + def test_instance_resize_get_nova_api_ge_2_47(self): + server = self.servers.first() + self._populate_server_flavor_nova_api_ge_2_47(server) + self._test_instance_resize_get(server) + @helpers.create_mocks({api.nova: ('server_get',)}) def test_instance_resize_get_server_get_exception(self): server = self.servers.first() @@ -4202,7 +4245,6 @@ class InstanceTests2(InstanceTestBase, InstanceTableTestMixin): 'flavor_list',)}) def test_instance_resize_get_flavor_list_exception(self): server = self.servers.first() - self.mock_server_get.return_value = server self.mock_flavor_list.side_effect = self.exceptions.nova @@ -4216,6 +4258,8 @@ class InstanceTests2(InstanceTestBase, InstanceTableTestMixin): server.id) self.mock_flavor_list.assert_called_once_with(helpers.IsHttpRequest()) + # TODO(amotoki): This is requred only when nova API <=2.46 is used. + # Once server_get() uses nova API >=2.47 only, this test can be droppped. @helpers.create_mocks({api.nova: ('server_get', 'flavor_list', 'flavor_get', diff --git a/openstack_dashboard/dashboards/project/instances/utils.py b/openstack_dashboard/dashboards/project/instances/utils.py index 11bd5dc681..0639690db8 100644 --- a/openstack_dashboard/dashboards/project/instances/utils.py +++ b/openstack_dashboard/dashboards/project/instances/utils.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +from collections import namedtuple import logging from operator import itemgetter @@ -232,3 +233,45 @@ def server_group_field_data(request): return [("", _("Select Server Group")), ] + server_groups_list return [("", _("No server groups available")), ] + + +def resolve_flavor(request, instance, flavors=None, **kwargs): + """Resolves name of instance flavor independent of API microversion + + :param request: django http request object + :param instance: api._nova.Server instance to resolve flavor + :param flavors: dict of flavors already retrieved + :param kwargs: flavor parameters to return if hit some flavor discrepancy + :return: flavor name or default placeholder + """ + def flavor_from_dict(flavor_dict): + """Creates flavor-like objects from dictionary + + :param flavor_dict: dictionary contains vcpu, ram, name, etc. values + :return: novaclient.v2.flavors.Flavor like object + """ + return namedtuple('Flavor', flavor_dict.keys())(*flavor_dict.values()) + + if flavors is None: + flavors = {} + flavor_id = instance.flavor.get('id') + if flavor_id: # Nova API <=2.46 + if flavor_id in flavors: + return flavors[flavor_id] + try: + return api.nova.flavor_get(request, flavor_id) + except Exception: + msg = _('Unable to retrieve flavor information ' + 'for instance "%s".') % instance.id + exceptions.handle(request, msg, ignore=True) + fallback_flavor = { + 'vcpus': 0, 'ram': 0, 'disk': 0, 'ephemeral': 0, 'swap': 0, + 'name': _('Not available'), + 'original_name': _('Not available'), + 'extra_specs': {}, + } + fallback_flavor.update(kwargs) + return flavor_from_dict(fallback_flavor) + else: + instance.flavor['name'] = instance.flavor['original_name'] + return flavor_from_dict(instance.flavor) diff --git a/openstack_dashboard/dashboards/project/instances/views.py b/openstack_dashboard/dashboards/project/instances/views.py index f767c538fa..29f3a1b8dc 100644 --- a/openstack_dashboard/dashboards/project/instances/views.py +++ b/openstack_dashboard/dashboards/project/instances/views.py @@ -50,6 +50,8 @@ from openstack_dashboard.dashboards.project.instances \ import tables as project_tables from openstack_dashboard.dashboards.project.instances \ import tabs as project_tabs +from openstack_dashboard.dashboards.project.instances \ + import utils as instance_utils from openstack_dashboard.dashboards.project.instances \ import workflows as project_workflows from openstack_dashboard.dashboards.project.networks.ports \ @@ -607,19 +609,9 @@ class ResizeView(workflows.WorkflowView): redirect = reverse("horizon:project:instances:index") msg = _('Unable to retrieve instance details.') exceptions.handle(self.request, msg, redirect=redirect) - flavor_id = instance.flavor['id'] flavors = self.get_flavors() - if flavor_id in flavors: - instance.flavor_name = flavors[flavor_id].name - else: - try: - flavor = api.nova.flavor_get(self.request, flavor_id) - instance.flavor_name = flavor.name - except Exception: - msg = _('Unable to retrieve flavor information for instance ' - '"%s".') % instance_id - exceptions.handle(self.request, msg, ignore=True) - instance.flavor_name = _("Not available") + flavor = instance_utils.resolve_flavor(self.request, instance, flavors) + instance.flavor_name = flavor.name return instance @memoized.memoized_method @@ -640,7 +632,6 @@ class ResizeView(workflows.WorkflowView): initial.update( {'instance_id': self.kwargs['instance_id'], 'name': getattr(_object, 'name', None), - 'old_flavor_id': _object.flavor['id'], 'old_flavor_name': getattr(_object, 'flavor_name', ''), 'flavors': self.get_flavors()}) return initial diff --git a/openstack_dashboard/dashboards/project/instances/workflows/resize_instance.py b/openstack_dashboard/dashboards/project/instances/workflows/resize_instance.py index bbc6906c5b..80b12383d7 100644 --- a/openstack_dashboard/dashboards/project/instances/workflows/resize_instance.py +++ b/openstack_dashboard/dashboards/project/instances/workflows/resize_instance.py @@ -47,11 +47,12 @@ class SetFlavorChoiceAction(workflows.Action): "_flavors_and_quotas.html") def populate_flavor_choices(self, request, context): - old_flavor_id = context.get('old_flavor_id') + old_flavor_name = context.get('old_flavor_name') flavors = context.get('flavors').values() # Remove current flavor from the list of flavor choices - flavors = [flavor for flavor in flavors if flavor.id != old_flavor_id] + flavors = [flavor for flavor in flavors + if flavor.name != old_flavor_name] if flavors: if len(flavors) > 1: