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). Closes-Bug: #1940834 Co-Authored-By: Akihiro Motoki <amotoki@gmail.com> Change-Id: Id2f38acfc27cdf93cc4341422873e512aaff716a
This commit is contained in:
parent
18545ca921
commit
d269b1640f
openstack_dashboard/dashboards/project/instances
@ -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 \
|
||||
@ -782,8 +784,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 '
|
||||
@ -1034,7 +1036,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")
|
||||
|
@ -2155,16 +2155,30 @@ 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',
|
||||
'tenant_absolute_limits',
|
||||
'is_feature_available')})
|
||||
def test_instance_resize_get(self):
|
||||
server = self.servers.first()
|
||||
flavor = self.flavors.first()
|
||||
'is_feature_available',
|
||||
'flavor_get')})
|
||||
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_tenant_absolute_limits.return_value = self.limits['absolute']
|
||||
self.mock_flavor_get.return_value = self.flavors.first()
|
||||
|
||||
url = reverse('horizon:project:instances:resize', args=[server.id])
|
||||
res = self.client.get(url)
|
||||
@ -2179,7 +2193,8 @@ class InstanceTests2(InstanceTestBase, InstanceTableTestMixin):
|
||||
self.assertNotContains(res, config_drive_field_label)
|
||||
|
||||
step = workflow.get_step("flavor_choice")
|
||||
self.assertEqual(step.action.initial['old_flavor_id'], flavor.id)
|
||||
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,
|
||||
@ -2188,8 +2203,15 @@ class InstanceTests2(InstanceTestBase, InstanceTableTestMixin):
|
||||
['<SetFlavorChoice: flavor_choice>',
|
||||
'<SetAdvanced: setadvancedaction>'])
|
||||
option = '<option value="%s">%s</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))
|
||||
@ -2201,6 +2223,20 @@ class InstanceTests2(InstanceTestBase, InstanceTableTestMixin):
|
||||
mock.call(helpers.IsHttpRequest()))
|
||||
self.mock_tenant_absolute_limits.assert_called_once_with(
|
||||
helpers.IsHttpRequest(), reserved=True)
|
||||
if nova_api_lt_2_47:
|
||||
self.mock_flavor_get.assert_called_once_with(
|
||||
helpers.IsHttpRequest(), server.flavor['id'])
|
||||
else:
|
||||
self.mock_flavor_get.assert_not_called()
|
||||
|
||||
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):
|
||||
@ -2217,10 +2253,10 @@ class InstanceTests2(InstanceTestBase, InstanceTableTestMixin):
|
||||
helpers.IsHttpRequest(), server.id)
|
||||
|
||||
@helpers.create_mocks({api.nova: ('server_get',
|
||||
'flavor_list',)})
|
||||
def test_instance_resize_get_flavor_list_exception(self):
|
||||
server = self.servers.first()
|
||||
|
||||
'flavor_list',
|
||||
'flavor_get')})
|
||||
def _test_instance_resize_get_flavor_list_exception(
|
||||
self, server, nova_api_lt_2_47=False):
|
||||
self.mock_server_get.return_value = server
|
||||
self.mock_flavor_list.side_effect = self.exceptions.nova
|
||||
|
||||
@ -2233,7 +2269,24 @@ class InstanceTests2(InstanceTestBase, InstanceTableTestMixin):
|
||||
self.mock_server_get.assert_called_once_with(helpers.IsHttpRequest(),
|
||||
server.id)
|
||||
self.mock_flavor_list.assert_called_once_with(helpers.IsHttpRequest())
|
||||
if nova_api_lt_2_47:
|
||||
self.mock_flavor_get.assert_called_once_with(
|
||||
helpers.IsHttpRequest(), server.flavor['id'])
|
||||
else:
|
||||
self.mock_flavor_get.assert_not_called()
|
||||
|
||||
def test_instance_resize_get_flavor_list_exception_nova_api_lt_2_47(self):
|
||||
server = self.servers.first()
|
||||
self._test_instance_resize_get_flavor_list_exception(
|
||||
server, nova_api_lt_2_47=True)
|
||||
|
||||
def test_instance_resize_get_flavor_list_exception(self):
|
||||
server = self.servers.first()
|
||||
self._populate_server_flavor_nova_api_ge_2_47(server)
|
||||
self._test_instance_resize_get_flavor_list_exception(server)
|
||||
|
||||
# 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',
|
||||
|
@ -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
|
||||
|
||||
@ -17,7 +18,6 @@ from django.conf import settings
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from horizon import exceptions
|
||||
|
||||
from openstack_dashboard import api
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
@ -232,3 +232,40 @@ def server_group_field_data(request):
|
||||
return [("", _("Select Server Group")), ] + server_groups_list
|
||||
|
||||
return [("", _("No server groups available")), ]
|
||||
|
||||
|
||||
def resolve_flavor(request, instance, **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 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())
|
||||
|
||||
flavor_id = instance.flavor.get('id')
|
||||
if flavor_id: # Nova API <=2.46
|
||||
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)
|
||||
|
@ -49,6 +49,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 \
|
||||
@ -586,19 +588,8 @@ 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")
|
||||
instance.flavor_name = instance_utils.resolve_flavor(self.request,
|
||||
instance).name
|
||||
return instance
|
||||
|
||||
@memoized.memoized_method
|
||||
@ -619,7 +610,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
|
||||
|
@ -62,11 +62,11 @@ 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:
|
||||
|
Loading…
x
Reference in New Issue
Block a user