Remove dependency on novaclient list_extensions API

The novaclient list_extensions API binding was removed in the
16.0.0 release [1]. The ability to enable/disable extensions
in nova has been deprecated since Liberty [2] and was removed
in Newton [3].

For horizon this only matters for the OPENSTACK_NOVA_EXTENSIONS_BLACKLIST
config setting and some javascript code used to compile panels based on
enabled extensions. In order to work with novaclient 16.0.0+, this
change removes the list_extensions usage since all extensions
are enabled in nova and thus for horizon a nova extension is only
not supported if it's in the configured blacklist. To continue supporting
the javascript code which uses the getExtensions function, the extension
names are hard-coded. Note that the method meant to test that code,
_test_extension_list, was wrong but never ran because of the underscore
prefix on the method name. That is fixed here.

[1] https://review.opendev.org/686516/
[2] https://review.opendev.org/214592/
[3] https://review.opendev.org/351362/

Change-Id: Iebb1e78c718b931d632445e4de6d7a29ccb92be2
Closes-Bug: #1847959
This commit is contained in:
Matt Riedemann 2019-10-22 13:14:13 -04:00
parent 85a1dddf12
commit b148c92075
5 changed files with 124 additions and 26 deletions

View File

@ -30,7 +30,6 @@ from django.utils.translation import ugettext_lazy as _
from novaclient import api_versions from novaclient import api_versions
from novaclient import exceptions as nova_exceptions from novaclient import exceptions as nova_exceptions
from novaclient.v2 import instance_action as nova_instance_action from novaclient.v2 import instance_action as nova_instance_action
from novaclient.v2 import list_extensions as nova_list_extensions
from novaclient.v2 import servers as nova_servers from novaclient.v2 import servers as nova_servers
from horizon import exceptions as horizon_exceptions from horizon import exceptions as horizon_exceptions
@ -49,6 +48,113 @@ INSTANCE_ACTIVE_STATE = 'ACTIVE'
VOLUME_STATE_AVAILABLE = "available" VOLUME_STATE_AVAILABLE = "available"
DEFAULT_QUOTA_NAME = 'default' DEFAULT_QUOTA_NAME = 'default'
# python-novaclient 16.0.0 removed the list_extensions module and the
# GET /extensions API is deprecated since Newton. Furthermore, the ability
# to enable/disable compute API extensions was also removed in Newton.
# Therefore we hard-code the list of extensions here until the
# OPENSTACK_NOVA_EXTENSIONS_BLACKLIST setting is no longer used.
EXTENSIONS = (
'AccessIPs',
'AdminActions',
'AdminPassword',
'Agents',
'Aggregates',
'AssistedVolumeSnapshots',
'AttachInterfaces',
'AvailabilityZone',
'BareMetalExtStatus',
'BareMetalNodes',
'BlockDeviceMapping',
'BlockDeviceMappingV2Boot',
'CellCapacities',
'Cells',
'Certificates',
'Cloudpipe',
'CloudpipeUpdate',
'ConfigDrive',
'ConsoleAuthTokens',
'ConsoleOutput',
'Consoles',
'CreateBackup',
'Createserverext',
'DeferredDelete',
'DiskConfig',
'Evacuate',
'ExtendedAvailabilityZone',
'ExtendedEvacuateFindHost',
'ExtendedFloatingIps',
'ExtendedHypervisors',
'ExtendedIps',
'ExtendedIpsMac',
'ExtendedNetworks',
'ExtendedQuotas',
'ExtendedRescueWithImage',
'ExtendedServerAttributes',
'ExtendedServices',
'ExtendedServicesDelete',
'ExtendedStatus',
'ExtendedStatus',
'ExtendedVolumes',
'FixedIPs',
'FlavorAccess',
'FlavorDisabled',
'FlavorExtraData',
'FlavorExtraSpecs',
'FlavorManage',
'FlavorRxtx',
'FlavorSwap',
'FloatingIpDns',
'FloatingIpPools',
'FloatingIps',
'FloatingIpsBulk',
'Fping',
'HideServerAddresses',
'Hosts',
'HypervisorStatus',
'Hypervisors',
'ImageSize',
'InstanceActions',
'Keypairs',
'LockServer',
'MigrateServer',
'Migrations',
'Multinic',
'MultipleCreate',
'NetworkAssociationSupport',
'Networks',
'OSInstanceUsageAuditLog',
'OSTenantNetworks',
'PauseServer',
'Personality',
'PreserveEphemeralOnRebuild',
'QuotaClasses',
'Quotas',
'Rescue',
'SchedulerHints',
'SecurityGroupDefaultRules',
'SecurityGroups',
'ServerDiagnostics',
'ServerExternalEvents',
'ServerGroupQuotas',
'ServerGroups',
'ServerListMultiStatus',
'ServerPassword',
'ServerSortKeys',
'ServerStartStop',
'ServerUsage',
'Services',
'Shelve',
'SimpleTenantUsage',
'SuspendServer',
'UsedLimits',
'UsedLimitsForAdmin',
'UserData',
'UserQuotas',
'VirtualInterfaces',
'VolumeAttachmentUpdate',
'Volumes'
)
get_microversion = _nova.get_microversion get_microversion = _nova.get_microversion
server_get = _nova.server_get server_get = _nova.server_get
@ -1010,13 +1116,10 @@ def interface_detach(request, server, port_id):
@profiler.trace @profiler.trace
@memoized.memoized @memoized.memoized
def list_extensions(request): def list_extensions(request):
"""List all nova extensions, except the ones in the blacklist.""" """List all nova extension names, except the ones in the blacklist."""
blacklist = set(settings.OPENSTACK_NOVA_EXTENSIONS_BLACKLIST) blacklist = set(settings.OPENSTACK_NOVA_EXTENSIONS_BLACKLIST)
nova_api = _nova.novaclient(request)
return tuple( return tuple(
extension for extension in extension for extension in EXTENSIONS if extension not in blacklist
nova_list_extensions.ListExtManager(nova_api).show_all()
if extension.name not in blacklist
) )
@ -1028,10 +1131,7 @@ def extension_supported(extension_name, request):
Example values for the extension_name include AdminActions, ConsoleOutput, Example values for the extension_name include AdminActions, ConsoleOutput,
etc. etc.
""" """
for ext in list_extensions(request): return extension_name in list_extensions(request)
if ext.name == extension_name:
return True
return False
@profiler.trace @profiler.trace

View File

@ -512,13 +512,13 @@ class Extensions(generic.View):
"""Get a list of extensions. """Get a list of extensions.
The listing result is an object with property "items". Each item is The listing result is an object with property "items". Each item is
an image. an object with property "name".
Example GET: Example GET:
http://localhost/api/nova/extensions http://localhost/api/nova/extensions
""" """
result = api.nova.list_extensions(request) result = api.nova.list_extensions(request)
return {'items': [e.to_dict() for e in result]} return {'items': [{'name': e} for e in result]}
@urls.register @urls.register

View File

@ -548,12 +548,7 @@
* { * {
* "items": [ * "items": [
* { * {
* "alias": "NMN", * "name": "Multinic"
* "description": "Multiple network support.",
* "links": [],
* "name": "Multinic",
* "namespace": "http://docs.openstack.org/compute/ext/multinic/api/v1.1",
* "updated": "2011-06-09T00:00:00Z"
* } * }
* ] * ]
* } * }

View File

@ -487,15 +487,9 @@ class NovaRestTestCase(test.TestCase):
# Extensions # Extensions
# #
@test.create_mocks({api.nova: ['list_extensions']}) @test.create_mocks({api.nova: ['list_extensions']})
@mock.patch.object(settings, def test_extension_list(self):
'OPENSTACK_NOVA_EXTENSIONS_BLACKLIST', ['baz'])
def _test_extension_list(self):
request = self.mock_rest_request() request = self.mock_rest_request()
self.mock_list_extensions.return_value = [ self.mock_list_extensions.return_value = ['foo', 'bar']
mock.Mock(**{'to_dict.return_value': {'name': 'foo'}}),
mock.Mock(**{'to_dict.return_value': {'name': 'bar'}}),
mock.Mock(**{'to_dict.return_value': {'name': 'baz'}}),
]
response = nova.Extensions().get(request) response = nova.Extensions().get(request)
self.assertStatusCode(response, 200) self.assertStatusCode(response, 200)
self.assertEqual({"items": [{"name": "foo"}, {"name": "bar"}]}, self.assertEqual({"items": [{"name": "foo"}, {"name": "bar"}]},

View File

@ -72,6 +72,15 @@ class ComputeApiTests(test.APIMockTestCase):
# To handle upgrade_api # To handle upgrade_api
mock_novaclient.api_version = api_versions.APIVersion(version) mock_novaclient.api_version = api_versions.APIVersion(version)
@override_settings(OPENSTACK_NOVA_EXTENSIONS_BLACKLIST=['ConsoleOutput'])
def test_extension_supported(self):
self.assertTrue(api.nova.extension_supported(
'Evacuate', mock.sentinel.request))
self.assertFalse(api.nova.extension_supported(
'ConsoleOutput', mock.sentinel.request))
self.assertFalse(api.nova.extension_supported(
'DoesNotExist', mock.sentinel.request))
@mock.patch.object(api._nova, 'novaclient') @mock.patch.object(api._nova, 'novaclient')
def test_server_reboot(self, mock_novaclient): def test_server_reboot(self, mock_novaclient):
server = self.servers.first() server = self.servers.first()