From b148c9207580731863e4dc49771e132a8c31edbc Mon Sep 17 00:00:00 2001 From: Matt Riedemann Date: Tue, 22 Oct 2019 13:14:13 -0400 Subject: [PATCH] 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 --- openstack_dashboard/api/nova.py | 120 ++++++++++++++++-- openstack_dashboard/api/rest/nova.py | 4 +- .../openstack-service-api/nova.service.js | 7 +- .../test/unit/api/rest/test_nova.py | 10 +- .../test/unit/api/test_nova.py | 9 ++ 5 files changed, 124 insertions(+), 26 deletions(-) diff --git a/openstack_dashboard/api/nova.py b/openstack_dashboard/api/nova.py index 55efd67457..4f8d97bf93 100644 --- a/openstack_dashboard/api/nova.py +++ b/openstack_dashboard/api/nova.py @@ -30,7 +30,6 @@ from django.utils.translation import ugettext_lazy as _ from novaclient import api_versions from novaclient import exceptions as nova_exceptions 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 horizon import exceptions as horizon_exceptions @@ -49,6 +48,113 @@ INSTANCE_ACTIVE_STATE = 'ACTIVE' VOLUME_STATE_AVAILABLE = "available" 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 server_get = _nova.server_get @@ -1010,13 +1116,10 @@ def interface_detach(request, server, port_id): @profiler.trace @memoized.memoized 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) - nova_api = _nova.novaclient(request) return tuple( - extension for extension in - nova_list_extensions.ListExtManager(nova_api).show_all() - if extension.name not in blacklist + extension for extension in EXTENSIONS if extension not in blacklist ) @@ -1028,10 +1131,7 @@ def extension_supported(extension_name, request): Example values for the extension_name include AdminActions, ConsoleOutput, etc. """ - for ext in list_extensions(request): - if ext.name == extension_name: - return True - return False + return extension_name in list_extensions(request) @profiler.trace diff --git a/openstack_dashboard/api/rest/nova.py b/openstack_dashboard/api/rest/nova.py index 6608d71d2e..5c53281b89 100644 --- a/openstack_dashboard/api/rest/nova.py +++ b/openstack_dashboard/api/rest/nova.py @@ -512,13 +512,13 @@ class Extensions(generic.View): """Get a list of extensions. The listing result is an object with property "items". Each item is - an image. + an object with property "name". Example GET: http://localhost/api/nova/extensions """ 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 diff --git a/openstack_dashboard/static/app/core/openstack-service-api/nova.service.js b/openstack_dashboard/static/app/core/openstack-service-api/nova.service.js index abd9aeeb0a..674cd2f8af 100644 --- a/openstack_dashboard/static/app/core/openstack-service-api/nova.service.js +++ b/openstack_dashboard/static/app/core/openstack-service-api/nova.service.js @@ -548,12 +548,7 @@ * { * "items": [ * { - * "alias": "NMN", - * "description": "Multiple network support.", - * "links": [], - * "name": "Multinic", - * "namespace": "http://docs.openstack.org/compute/ext/multinic/api/v1.1", - * "updated": "2011-06-09T00:00:00Z" + * "name": "Multinic" * } * ] * } diff --git a/openstack_dashboard/test/unit/api/rest/test_nova.py b/openstack_dashboard/test/unit/api/rest/test_nova.py index e3792c18a2..148880866a 100644 --- a/openstack_dashboard/test/unit/api/rest/test_nova.py +++ b/openstack_dashboard/test/unit/api/rest/test_nova.py @@ -487,15 +487,9 @@ class NovaRestTestCase(test.TestCase): # Extensions # @test.create_mocks({api.nova: ['list_extensions']}) - @mock.patch.object(settings, - 'OPENSTACK_NOVA_EXTENSIONS_BLACKLIST', ['baz']) - def _test_extension_list(self): + def test_extension_list(self): request = self.mock_rest_request() - self.mock_list_extensions.return_value = [ - mock.Mock(**{'to_dict.return_value': {'name': 'foo'}}), - mock.Mock(**{'to_dict.return_value': {'name': 'bar'}}), - mock.Mock(**{'to_dict.return_value': {'name': 'baz'}}), - ] + self.mock_list_extensions.return_value = ['foo', 'bar'] response = nova.Extensions().get(request) self.assertStatusCode(response, 200) self.assertEqual({"items": [{"name": "foo"}, {"name": "bar"}]}, diff --git a/openstack_dashboard/test/unit/api/test_nova.py b/openstack_dashboard/test/unit/api/test_nova.py index 744d7dd7a4..dfcea794b5 100644 --- a/openstack_dashboard/test/unit/api/test_nova.py +++ b/openstack_dashboard/test/unit/api/test_nova.py @@ -72,6 +72,15 @@ class ComputeApiTests(test.APIMockTestCase): # To handle upgrade_api 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') def test_server_reboot(self, mock_novaclient): server = self.servers.first()