diff -Nru horizon-19.2.0/debian/changelog horizon-19.2.0/debian/changelog --- horizon-19.2.0/debian/changelog 2021-04-14 15:57:44.000000000 +0530 +++ horizon-19.2.0/debian/changelog 2022-03-24 09:18:35.000000000 +0530 @@ -1,3 +1,10 @@ +horizon (4:19.2.0-0ubuntu1~cloud0ubuntu1) UNRELEASED; urgency=medium + + * d/p/0001-Fix-for-Resize-instance-button.patch: Fixes resize instance + widget (LP#1940834) + + -- Hemanth Nakkina Thu, 24 Mar 2022 09:18:35 +0530 + horizon (4:19.2.0-0ubuntu1~cloud0) focal-wallaby; urgency=medium * New upstream release for the Ubuntu Cloud Archive. diff -Nru horizon-19.2.0/debian/patches/0001-Fix-for-Resize-instance-button.patch horizon-19.2.0/debian/patches/0001-Fix-for-Resize-instance-button.patch --- horizon-19.2.0/debian/patches/0001-Fix-for-Resize-instance-button.patch 1970-01-01 05:30:00.000000000 +0530 +++ horizon-19.2.0/debian/patches/0001-Fix-for-Resize-instance-button.patch 2022-03-24 09:18:29.000000000 +0530 @@ -0,0 +1,283 @@ +From 89e3e51e5f4bd61268c218caf502718870b44019 Mon Sep 17 00:00:00 2001 +From: Thomas Goirand +Date: Thu, 17 Feb 2022 10:36:31 +0100 +Subject: [PATCH] Fix for "Resize instance" button + +Currently, "Resize instance" widget is not working because it relies on +legacy Nova API v2.46, obsoleted in Pike release. Proposed patch make +Horizon use current Nova API (>=2.47). + +Note: It also cherry-picks a7956cd004 from the master branch +which avoids the extra call of flavor_get in resize server form. + +Closes-Bug: #1940834 +Co-Authored-By: Akihiro Motoki +Change-Id: Id2f38acfc27cdf93cc4341422873e512aaff716a +(cherry picked from commit 4f4e8800904dc98a696c5aebe3ffcc947e2deabc) +--- + .../dashboards/project/instances/tables.py | 8 +-- + .../dashboards/project/instances/tests.py | 52 +++++++++++++++++-- + .../dashboards/project/instances/utils.py | 43 +++++++++++++++ + .../dashboards/project/instances/views.py | 17 ++---- + .../instances/workflows/resize_instance.py | 5 +- + 5 files changed, 103 insertions(+), 22 deletions(-) + +diff --git a/openstack_dashboard/dashboards/project/instances/tables.py b/openstack_dashboard/dashboards/project/instances/tables.py +index 674ebdca2..624a59597 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 8fc35415b..c359b6efe 100644 +--- a/openstack_dashboard/dashboards/project/instances/tests.py ++++ b/openstack_dashboard/dashboards/project/instances/tests.py +@@ -4139,13 +4139,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() +@@ -4154,14 +4167,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)) +@@ -4176,6 +4210,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() +@@ -4194,7 +4237,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 + +@@ -4208,6 +4250,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 11bd5dc68..0639690db 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 f767c538f..29f3a1b8d 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 bbc6906c5..80b12383d 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: +-- +2.25.1 + diff -Nru horizon-19.2.0/debian/patches/series horizon-19.2.0/debian/patches/series --- horizon-19.2.0/debian/patches/series 2021-04-13 23:29:07.000000000 +0530 +++ horizon-19.2.0/debian/patches/series 2022-03-24 09:18:29.000000000 +0530 @@ -2,3 +2,4 @@ fix-dashboard-manage.patch ubuntu_settings.patch embedded-xstatic.patch +0001-Fix-for-Resize-instance-button.patch