diff --git a/doc/source/index.rst b/doc/source/index.rst index ca54b128c5..3e82ece53f 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -92,6 +92,7 @@ the following topic guides. topics/workflows topics/tables topics/policy + topics/microversion_support topics/angularjs topics/testing topics/javascript_testing diff --git a/doc/source/topics/microversion_support.rst b/doc/source/topics/microversion_support.rst new file mode 100644 index 0000000000..c67b08ab5d --- /dev/null +++ b/doc/source/topics/microversion_support.rst @@ -0,0 +1,47 @@ +============================ +Horizon Microversion Support +============================ + +Introduction +============ + +Several services use API microversions, which allows consumers of that API to +specify an exact version when making a request. This can be useful in ensuring +a feature continues to work as expected across many service releases. + +Adding a feature that was introduced in a microversion +====================================================== + +1. Add the feature to the ``MICROVERSION_FEATURES`` dict in + ``openstack_dashboard/api/microversions.py`` under the appropriate + service name. The feature should have at least two versions listed; the + minimum version (i.e. the version that introduced the feature) and + the current working version. Providing multiple versions reduces project + maintenance overheads and helps Horizon work with older service + deployments. + +2. Use the ``is_feature_available`` function for your service to show or hide + the function.:: + + from openstack_dashboard.api import service + + ... + + def allowed(self, request): + return service.is_feature_available('feature') + +3. Send the correct microversion with ``get_microversion`` function in the API + layer.:: + + def resource_list(request): + try: + microversion = get_microversion(request, 'feature') + client = serviceclient(request, microversion) + return client.resource_list() + +Microversion references +======================= + +:Nova: http://docs.openstack.org/developer/nova/api_microversion_history.html +:Cinder: http://docs.openstack.org/developer/cinder/devref/api_microversion_history.html +:API-WG: http://specs.openstack.org/openstack/api-wg/guidelines/microversion_specification.html diff --git a/openstack_dashboard/api/microversions.py b/openstack_dashboard/api/microversions.py new file mode 100644 index 0000000000..03f0173d70 --- /dev/null +++ b/openstack_dashboard/api/microversions.py @@ -0,0 +1,56 @@ +# Copyright 2017 Cisco Systems +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging + +LOG = logging.getLogger(__name__) + +# A list of features and their supported microversions. Note that these are +# explicit functioning versions, not a range. +# There should be a minimum of two versions per feature. The first entry in +# this list should always be the lowest possible API microversion for a +# feature i.e. the version at which that feature was introduced. The second +# entry should be the current service version when the feature was added to +# horizon. +# Further documentation can be found at +# http://docs.openstack.org/developer/horizon/topics/microversion_support.html +MICROVERSION_FEATURES = { + "nova": { + "locked_attribute": ["2.9", "2.42"] + }, + "cinder": { + "consistency_groups": ["2.0", "3.10"], + "message_list": ["3.5", "3.29"] + } +} + + +# NOTE(robcresswell): Since each client implements their own wrapper class for +# API objects, we'll need to allow that to be passed in. In the future this +# should be replaced by some common handling in Oslo. +def get_microversion_for_feature(service, feature, wrapper_class, + min_ver, max_ver): + """Retrieves that highest known functional microversion for a feature""" + try: + service_features = MICROVERSION_FEATURES[service] + except KeyError: + LOG.debug("'%s' could not be found in the MICROVERSION_FEATURES " + "dict" % service) + return None + feature_versions = service_features[feature] + for version in reversed(feature_versions): + microversion = wrapper_class(version) + if microversion.matches(min_ver, max_ver): + return microversion + return None diff --git a/openstack_dashboard/api/nova.py b/openstack_dashboard/api/nova.py index cfc0cd4616..bcbfa1c829 100644 --- a/openstack_dashboard/api/nova.py +++ b/openstack_dashboard/api/nova.py @@ -43,6 +43,7 @@ from horizon.utils.memoized import memoized from horizon.utils.memoized import memoized_with_request from openstack_dashboard.api import base +from openstack_dashboard.api import microversions from openstack_dashboard.api import network_base from openstack_dashboard.contrib.developer.profiler import api as profiler @@ -52,7 +53,6 @@ LOG = logging.getLogger(__name__) VERSIONS = base.APIVersionManager("compute", preferred_version=2) VERSIONS.load_supported_version(1.1, {"client": nova_client, "version": 1.1}) VERSIONS.load_supported_version(2, {"client": nova_client, "version": 2}) -VERSIONS.load_supported_version(2.9, {"client": nova_client, "version": 2.9}) # API static values INSTANCE_ACTIVE_STATE = 'ACTIVE' @@ -62,6 +62,17 @@ INSECURE = getattr(settings, 'OPENSTACK_SSL_NO_VERIFY', False) CACERT = getattr(settings, 'OPENSTACK_SSL_CACERT', None) +def get_microversion(request, feature): + client = novaclient(request) + min_ver, max_ver = api_versions._get_server_version_range(client) + return (microversions.get_microversion_for_feature( + 'nova', feature, api_versions.APIVersion, min_ver, max_ver)) + + +def is_feature_available(request, feature): + return bool(get_microversion(request, feature)) + + class VNCConsole(base.APIDictWrapper): """Wrapper for the "console" dictionary. @@ -849,12 +860,14 @@ def server_stop(request, instance_id): @profiler.trace def server_lock(request, instance_id): - novaclient(request).servers.lock(instance_id) + microversion = get_microversion(request, "locked_attribute") + novaclient(request, version=microversion).servers.lock(instance_id) @profiler.trace def server_unlock(request, instance_id): - novaclient(request).servers.unlock(instance_id) + microversion = get_microversion(request, "locked_attribute") + novaclient(request, version=microversion).servers.unlock(instance_id) @profiler.trace diff --git a/openstack_dashboard/dashboards/project/instances/tables.py b/openstack_dashboard/dashboards/project/instances/tables.py index 7735bfec59..b7f74cfcd9 100644 --- a/openstack_dashboard/dashboards/project/instances/tables.py +++ b/openstack_dashboard/dashboards/project/instances/tables.py @@ -884,11 +884,12 @@ class LockInstance(policy.PolicyTargetMixin, tables.BatchAction): # to only allow unlocked instances to be locked def allowed(self, request, instance): - # if not locked, lock should be available if getattr(instance, 'locked', False): return False if not api.nova.extension_supported('AdminActions', request): return False + if not api.nova.is_feature_available(request, "locked_attribute"): + return False return True def action(self, request, obj_id): @@ -921,6 +922,8 @@ class UnlockInstance(policy.PolicyTargetMixin, tables.BatchAction): return False if not api.nova.extension_supported('AdminActions', request): return False + if not api.nova.is_feature_available(request, "locked_attribute"): + return False return True def action(self, request, obj_id): diff --git a/openstack_dashboard/dashboards/project/instances/tests.py b/openstack_dashboard/dashboards/project/instances/tests.py index 954f828bcd..6787f9c97c 100644 --- a/openstack_dashboard/dashboards/project/instances/tests.py +++ b/openstack_dashboard/dashboards/project/instances/tests.py @@ -120,6 +120,7 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): 'server_list', 'tenant_absolute_limits', 'extension_supported', + 'is_feature_available', ), api.glance: ('image_list_detailed',), api.network: ( @@ -130,9 +131,11 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): }) def _get_index(self): servers = self.servers.list() - api.nova.extension_supported('AdminActions', - IsA(http.HttpRequest)) \ + api.nova.extension_supported('AdminActions', IsA(http.HttpRequest)) \ .MultipleTimes().AndReturn(True) + api.nova.is_feature_available( + IsA(http.HttpRequest), 'locked_attribute' + ).MultipleTimes().AndReturn(True) api.nova.extension_supported('Shelve', IsA(http.HttpRequest)) \ .MultipleTimes().AndReturn(True) api.nova.flavor_list(IsA(http.HttpRequest)) \ @@ -191,7 +194,8 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): @helpers.create_stubs({ api.nova: ('flavor_list', 'server_list', 'flavor_get', - 'tenant_absolute_limits', 'extension_supported',), + 'tenant_absolute_limits', 'extension_supported', + 'is_feature_available',), api.glance: ('image_list_detailed',), api.network: ('floating_ip_simple_associate_supported', 'floating_ip_supported', @@ -202,9 +206,11 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): flavors = self.flavors.list() full_flavors = OrderedDict([(f.id, f) for f in flavors]) search_opts = {'marker': None, 'paginate': True} - api.nova.extension_supported('AdminActions', - IsA(http.HttpRequest)) \ + api.nova.extension_supported('AdminActions', IsA(http.HttpRequest)) \ .MultipleTimes().AndReturn(True) + api.nova.is_feature_available( + IsA(http.HttpRequest), 'locked_attribute' + ).MultipleTimes().AndReturn(True) api.nova.extension_supported('Shelve', IsA(http.HttpRequest)) \ .MultipleTimes().AndReturn(True) api.nova.server_list(IsA(http.HttpRequest), search_opts=search_opts) \ @@ -235,7 +241,7 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): @helpers.create_stubs({ api.nova: ('flavor_list', 'server_list', 'tenant_absolute_limits', - 'extension_supported',), + 'extension_supported', 'is_feature_available',), api.glance: ('image_list_detailed',), api.network: ('floating_ip_simple_associate_supported', 'floating_ip_supported', @@ -248,9 +254,11 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): servers = self.servers.list() servers[0] = volume_server - api.nova.extension_supported('AdminActions', - IsA(http.HttpRequest)) \ + api.nova.extension_supported('AdminActions', IsA(http.HttpRequest)) \ .MultipleTimes().AndReturn(True) + api.nova.is_feature_available( + IsA(http.HttpRequest), 'locked_attribute' + ).MultipleTimes().AndReturn(True) api.nova.extension_supported('Shelve', IsA(http.HttpRequest)) \ .MultipleTimes().AndReturn(True) api.nova.flavor_list(IsA(http.HttpRequest)) \ @@ -380,15 +388,15 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): @helpers.create_stubs({api.nova: ('server_pause', 'server_list', 'flavor_list', - 'extension_supported',), + 'extension_supported', + 'is_feature_available',), api.glance: ('image_list_detailed',), api.network: ('servers_update_addresses',)}) def test_pause_instance(self): servers = self.servers.list() server = servers[0] - api.nova.extension_supported('AdminActions', - IsA(http.HttpRequest)) \ + api.nova.extension_supported('AdminActions', IsA(http.HttpRequest)) \ .MultipleTimes().AndReturn(True) api.nova.flavor_list(IsA(http.HttpRequest)) \ .AndReturn(self.flavors.list()) @@ -410,15 +418,15 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): @helpers.create_stubs({api.nova: ('server_pause', 'server_list', 'flavor_list', - 'extension_supported',), + 'extension_supported', + 'is_feature_available',), api.glance: ('image_list_detailed',), api.network: ('servers_update_addresses',)}) def test_pause_instance_exception(self): servers = self.servers.list() server = servers[0] - api.nova.extension_supported('AdminActions', - IsA(http.HttpRequest)) \ + api.nova.extension_supported('AdminActions', IsA(http.HttpRequest)) \ .MultipleTimes().AndReturn(True) api.nova.flavor_list(IsA(http.HttpRequest)) \ .AndReturn(self.flavors.list()) @@ -441,15 +449,15 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): @helpers.create_stubs({api.nova: ('server_unpause', 'server_list', 'flavor_list', - 'extension_supported',), + 'extension_supported', + 'is_feature_available',), api.glance: ('image_list_detailed',), api.network: ('servers_update_addresses',)}) def test_unpause_instance(self): servers = self.servers.list() server = servers[0] server.status = "PAUSED" - api.nova.extension_supported('AdminActions', - IsA(http.HttpRequest)) \ + api.nova.extension_supported('AdminActions', IsA(http.HttpRequest)) \ .MultipleTimes().AndReturn(True) api.nova.flavor_list(IsA(http.HttpRequest)) \ .AndReturn(self.flavors.list()) @@ -471,7 +479,8 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): @helpers.create_stubs({api.nova: ('server_unpause', 'server_list', 'flavor_list', - 'extension_supported',), + 'extension_supported', + 'is_feature_available',), api.glance: ('image_list_detailed',), api.network: ('servers_update_addresses',)}) def test_unpause_instance_exception(self): @@ -479,8 +488,7 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): server = servers[0] server.status = "PAUSED" - api.nova.extension_supported('AdminActions', - IsA(http.HttpRequest)) \ + api.nova.extension_supported('AdminActions', IsA(http.HttpRequest)) \ .MultipleTimes().AndReturn(True) api.nova.flavor_list(IsA(http.HttpRequest)) \ .AndReturn(self.flavors.list()) @@ -584,15 +592,15 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): @helpers.create_stubs({api.nova: ('server_suspend', 'server_list', 'flavor_list', - 'extension_supported',), + 'extension_supported', + 'is_feature_available',), api.glance: ('image_list_detailed',), api.network: ('servers_update_addresses',)}) def test_suspend_instance(self): servers = self.servers.list() server = servers[0] - api.nova.extension_supported('AdminActions', - IsA(http.HttpRequest)) \ + api.nova.extension_supported('AdminActions', IsA(http.HttpRequest)) \ .MultipleTimes().AndReturn(True) api.nova.flavor_list(IsA(http.HttpRequest)) \ .AndReturn(self.flavors.list()) @@ -615,15 +623,15 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): @helpers.create_stubs({api.nova: ('server_suspend', 'server_list', 'flavor_list', - 'extension_supported',), + 'extension_supported', + 'is_feature_available',), api.glance: ('image_list_detailed',), api.network: ('servers_update_addresses',)}) def test_suspend_instance_exception(self): servers = self.servers.list() server = servers[0] - api.nova.extension_supported('AdminActions', - IsA(http.HttpRequest)) \ + api.nova.extension_supported('AdminActions', IsA(http.HttpRequest)) \ .MultipleTimes().AndReturn(True) api.nova.flavor_list(IsA(http.HttpRequest)) \ .AndReturn(self.flavors.list()) @@ -633,8 +641,9 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): api.nova.server_list(IsA(http.HttpRequest), search_opts=search_opts) \ .AndReturn([servers, False]) api.network.servers_update_addresses(IsA(http.HttpRequest), servers) - api.nova.server_suspend(IsA(http.HttpRequest), six.text_type(server.id)) \ - .AndRaise(self.exceptions.nova) + api.nova.server_suspend( + IsA(http.HttpRequest), six.text_type(server.id) + ).AndRaise(self.exceptions.nova) self.mox.ReplayAll() @@ -646,7 +655,8 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): @helpers.create_stubs({api.nova: ('server_resume', 'server_list', 'flavor_list', - 'extension_supported',), + 'extension_supported', + 'is_feature_available',), api.glance: ('image_list_detailed',), api.network: ('servers_update_addresses',)}) def test_resume_instance(self): @@ -654,8 +664,7 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): server = servers[0] server.status = "SUSPENDED" - api.nova.extension_supported('AdminActions', - IsA(http.HttpRequest)) \ + api.nova.extension_supported('AdminActions', IsA(http.HttpRequest)) \ .MultipleTimes().AndReturn(True) api.nova.flavor_list(IsA(http.HttpRequest)) \ .AndReturn(self.flavors.list()) @@ -677,7 +686,8 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): @helpers.create_stubs({api.nova: ('server_resume', 'server_list', 'flavor_list', - 'extension_supported',), + 'extension_supported', + 'is_feature_available'), api.glance: ('image_list_detailed',), api.network: ('servers_update_addresses',)}) def test_resume_instance_exception(self): @@ -685,8 +695,7 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): server = servers[0] server.status = "SUSPENDED" - api.nova.extension_supported('AdminActions', - IsA(http.HttpRequest)) \ + api.nova.extension_supported('AdminActions', IsA(http.HttpRequest)) \ .MultipleTimes().AndReturn(True) api.nova.flavor_list(IsA(http.HttpRequest)) \ .AndReturn(self.flavors.list()) @@ -710,7 +719,8 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): @helpers.create_stubs({api.nova: ('server_shelve', 'server_list', 'flavor_list', - 'extension_supported',), + 'extension_supported', + 'is_feature_available',), api.glance: ('image_list_detailed',), api.network: ('servers_update_addresses',)}) def test_shelve_instance(self): @@ -739,7 +749,8 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): @helpers.create_stubs({api.nova: ('server_shelve', 'server_list', 'flavor_list', - 'extension_supported',), + 'extension_supported', + 'is_feature_available',), api.glance: ('image_list_detailed',), api.network: ('servers_update_addresses',)}) def test_shelve_instance_exception(self): @@ -770,7 +781,8 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): @helpers.create_stubs({api.nova: ('server_unshelve', 'server_list', 'flavor_list', - 'extension_supported',), + 'extension_supported', + 'is_feature_available',), api.glance: ('image_list_detailed',), api.network: ('servers_update_addresses',)}) def test_unshelve_instance(self): @@ -801,7 +813,8 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): @helpers.create_stubs({api.nova: ('server_unshelve', 'server_list', 'flavor_list', - 'extension_supported',), + 'extension_supported', + 'is_feature_available',), api.glance: ('image_list_detailed',), api.network: ('servers_update_addresses',)}) def test_unshelve_instance_exception(self): @@ -833,7 +846,8 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): @helpers.create_stubs({api.nova: ('server_lock', 'server_list', 'flavor_list', - 'extension_supported',), + 'extension_supported', + 'is_feature_available',), api.glance: ('image_list_detailed',), api.network: ('servers_update_addresses',)}) def test_lock_instance(self): @@ -842,6 +856,9 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): api.nova.extension_supported('AdminActions', IsA( http.HttpRequest)).MultipleTimes().AndReturn(True) + api.nova.is_feature_available( + IsA(http.HttpRequest), 'locked_attribute' + ).MultipleTimes().AndReturn(True) api.glance.image_list_detailed(IgnoreArg()).AndReturn(( self.images.list(), False, False)) api.nova.flavor_list(IsA(http.HttpRequest)) \ @@ -863,7 +880,8 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): @helpers.create_stubs({api.nova: ('server_lock', 'server_list', 'flavor_list', - 'extension_supported',), + 'extension_supported', + 'is_feature_available',), api.glance: ('image_list_detailed',), api.network: ('servers_update_addresses',)}) def test_lock_instance_exception(self): @@ -872,6 +890,9 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): api.nova.extension_supported('AdminActions', IsA( http.HttpRequest)).MultipleTimes().AndReturn(True) + api.nova.is_feature_available( + IsA(http.HttpRequest), 'locked_attribute' + ).MultipleTimes().AndReturn(True) api.glance.image_list_detailed(IgnoreArg()).AndReturn(( self.images.list(), False, False)) api.nova.flavor_list(IsA(http.HttpRequest)) \ @@ -894,7 +915,8 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): @helpers.create_stubs({api.nova: ('server_unlock', 'server_list', 'flavor_list', - 'extension_supported',), + 'extension_supported', + 'is_feature_available'), api.glance: ('image_list_detailed',), api.network: ('servers_update_addresses',)}) def test_unlock_instance(self): @@ -902,6 +924,9 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): server = servers[0] api.nova.extension_supported('AdminActions', IsA( http.HttpRequest)).MultipleTimes().AndReturn(True) + api.nova.is_feature_available( + IsA(http.HttpRequest), 'locked_attribute' + ).AndReturn(True) api.nova.flavor_list(IsA(http.HttpRequest)) \ .AndReturn(self.flavors.list()) api.glance.image_list_detailed(IgnoreArg()).AndReturn(( @@ -923,7 +948,8 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): @helpers.create_stubs({api.nova: ('server_unlock', 'server_list', 'flavor_list', - 'extension_supported',), + 'extension_supported', + 'is_feature_available'), api.glance: ('image_list_detailed',), api.network: ('servers_update_addresses',)}) def test_unlock_instance_exception(self): @@ -932,6 +958,9 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): api.nova.extension_supported('AdminActions', IsA( http.HttpRequest)).MultipleTimes().AndReturn(True) + api.nova.is_feature_available(IsA( + http.HttpRequest), 'locked_attribute' + ).MultipleTimes().AndReturn(True) api.glance.image_list_detailed(IgnoreArg()).AndReturn(( self.images.list(), False, False)) search_opts = {'marker': None, 'paginate': True} @@ -956,7 +985,8 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): "server_get", "instance_volumes_list", "flavor_get", - "extension_supported" + "extension_supported", + 'is_feature_available', ), api.network: ( "server_security_groups", @@ -1002,6 +1032,9 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): .MultipleTimes().AndReturn(True) api.nova.extension_supported('AdminActions', IsA(http.HttpRequest)) \ .MultipleTimes().AndReturn(True) + api.nova.is_feature_available( + IsA(http.HttpRequest), 'locked_attribute' + ).MultipleTimes().AndReturn(True) api.nova.extension_supported('Shelve', IsA(http.HttpRequest)) \ .MultipleTimes().AndReturn(True) @@ -1365,7 +1398,7 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): @helpers.create_stubs({ api.nova: ('flavor_list', 'server_list', 'tenant_absolute_limits', - 'extension_supported',), + 'extension_supported', 'is_feature_available',), api.glance: ('image_list_detailed',), api.network: ('floating_ip_simple_associate_supported', 'floating_ip_supported', @@ -1373,9 +1406,11 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): }) def _test_instances_index_retrieve_password_action(self): servers = self.servers.list() - api.nova.extension_supported('AdminActions', - IsA(http.HttpRequest)) \ + api.nova.extension_supported('AdminActions', IsA(http.HttpRequest)) \ .MultipleTimes().AndReturn(True) + api.nova.is_feature_available( + IsA(http.HttpRequest), 'locked_attribute' + ).MultipleTimes().AndReturn(True) api.nova.extension_supported('Shelve', IsA(http.HttpRequest)) \ .MultipleTimes().AndReturn(True) api.nova.flavor_list(IsA(http.HttpRequest)) \ @@ -1556,6 +1591,7 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): self.assertRedirectsNoFollow(res, INDEX_URL) @helpers.create_stubs({api.nova: ('extension_supported', + 'is_feature_available', 'flavor_list', 'keypair_list', 'server_group_list', @@ -1791,6 +1827,7 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): self.test_launch_instance_get(only_one_network=True) @helpers.create_stubs({api.nova: ('extension_supported', + 'is_feature_available', 'flavor_list', 'keypair_list', 'server_group_list', @@ -1890,6 +1927,7 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): 'port_create', 'port_list'), api.nova: ('extension_supported', + 'is_feature_available', 'flavor_list', 'keypair_list', 'availability_zone_list', @@ -2001,6 +2039,7 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): 'port_create', 'port_list'), api.nova: ('extension_supported', + 'is_feature_available', 'flavor_list', 'keypair_list', 'availability_zone_list', @@ -2123,6 +2162,7 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): 'port_list'), api.nova: ('server_create', 'extension_supported', + 'is_feature_available', 'flavor_list', 'keypair_list', 'availability_zone_list', @@ -2222,6 +2262,7 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): api.neutron: ('network_list', 'port_list'), api.nova: ('extension_supported', + 'is_feature_available', 'flavor_list', 'keypair_list', 'availability_zone_list'), @@ -2298,6 +2339,7 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): 'port_create', 'port_list'), api.nova: ('extension_supported', + 'is_feature_available', 'flavor_list', 'keypair_list', 'availability_zone_list', @@ -2423,6 +2465,7 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): 'port_create', 'port_list'), api.nova: ('extension_supported', + 'is_feature_available', 'flavor_list', 'keypair_list', 'availability_zone_list', @@ -2494,6 +2537,7 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): 'volume_snapshot_list',), api.network: ('security_group_list',), api.nova: ('extension_supported', + 'is_feature_available', 'flavor_list', 'keypair_list', 'availability_zone_list',), @@ -2549,6 +2593,7 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): 'port_delete', 'port_list'), api.nova: ('extension_supported', + 'is_feature_available', 'flavor_list', 'keypair_list', 'availability_zone_list', @@ -2656,6 +2701,7 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): api.neutron: ('network_list', 'port_list'), api.nova: ('extension_supported', + 'is_feature_available', 'flavor_list', 'keypair_list', 'availability_zone_list',), @@ -2734,6 +2780,7 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): api.neutron: ('network_list', 'port_list'), api.nova: ('extension_supported', + 'is_feature_available', 'flavor_list', 'keypair_list', 'server_group_list', @@ -2839,6 +2886,7 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): api.neutron: ('network_list', 'port_list'), api.nova: ('extension_supported', + 'is_feature_available', 'flavor_list', 'keypair_list', 'availability_zone_list',), @@ -2936,6 +2984,7 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): api.neutron: ('network_list', 'port_list'), api.nova: ('extension_supported', + 'is_feature_available', 'flavor_list', 'keypair_list', 'availability_zone_list',), @@ -3079,6 +3128,7 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): api.neutron: ('network_list', 'port_list'), api.nova: ('extension_supported', + 'is_feature_available', 'flavor_list', 'keypair_list', 'server_group_list', @@ -3192,7 +3242,7 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): @helpers.create_stubs({ api.nova: ('flavor_list', 'server_list', 'tenant_absolute_limits', - 'extension_supported',), + 'extension_supported', 'is_feature_available',), api.glance: ('image_list_detailed',), api.network: ('floating_ip_simple_associate_supported', 'floating_ip_supported', @@ -3203,9 +3253,11 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): limits = self.limits['absolute'] limits['totalInstancesUsed'] = 0 - api.nova.extension_supported('AdminActions', - IsA(http.HttpRequest)) \ + api.nova.extension_supported('AdminActions', IsA(http.HttpRequest)) \ .MultipleTimes().AndReturn(True) + api.nova.is_feature_available( + IsA(http.HttpRequest), 'locked_attribute' + ).MultipleTimes().AndReturn(True) api.nova.extension_supported('Shelve', IsA(http.HttpRequest)) \ .MultipleTimes().AndReturn(True) api.nova.flavor_list(IsA(http.HttpRequest)) \ @@ -3239,7 +3291,7 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): @helpers.create_stubs({ api.nova: ('flavor_list', 'server_list', 'tenant_absolute_limits', - 'extension_supported',), + 'extension_supported', 'is_feature_available',), api.glance: ('image_list_detailed',), api.network: ('floating_ip_simple_associate_supported', 'floating_ip_supported', @@ -3250,9 +3302,11 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): limits = self.limits['absolute'] limits['totalInstancesUsed'] = limits['maxTotalInstances'] - api.nova.extension_supported('AdminActions', - IsA(http.HttpRequest)) \ + api.nova.extension_supported('AdminActions', IsA(http.HttpRequest)) \ .MultipleTimes().AndReturn(True) + api.nova.is_feature_available( + IsA(http.HttpRequest), 'locked_attribute' + ).MultipleTimes().AndReturn(True) api.nova.extension_supported('Shelve', IsA(http.HttpRequest)) \ .MultipleTimes().AndReturn(True) api.nova.flavor_list(IsA(http.HttpRequest)) \ @@ -3287,6 +3341,7 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): api.neutron: ('network_list', 'port_list'), api.nova: ('extension_supported', + 'is_feature_available', 'flavor_list', 'keypair_list', 'availability_zone_list', @@ -3388,7 +3443,7 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): @helpers.create_stubs({ api.nova: ('flavor_list', 'server_list', 'tenant_absolute_limits', - 'extension_supported',), + 'extension_supported', 'is_feature_available',), api.glance: ('image_list_detailed',), api.network: ('floating_ip_simple_associate_supported', 'floating_ip_supported', @@ -3398,9 +3453,12 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): servers = self.servers.list() server = self.servers.first() server.status = "VERIFY_RESIZE" - api.nova.extension_supported('AdminActions', - IsA(http.HttpRequest)) \ - .MultipleTimes().AndReturn(True) + api.nova.extension_supported( + 'AdminActions', IsA(http.HttpRequest) + ).MultipleTimes().AndReturn(True) + api.nova.is_feature_available( + IsA(http.HttpRequest), 'locked_attribute' + ).MultipleTimes().AndReturn(True) api.nova.extension_supported('Shelve', IsA(http.HttpRequest)) \ .MultipleTimes().AndReturn(True) api.nova.flavor_list(IsA(http.HttpRequest)) \ @@ -3425,6 +3483,7 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): self.assertContains(res, "instances__revert") @helpers.create_stubs({api.nova: ('extension_supported', + 'is_feature_available', 'flavor_list', 'keypair_list', 'availability_zone_list'), @@ -3550,6 +3609,7 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): @helpers.create_stubs({api.nova: ('server_get', 'flavor_list', 'tenant_absolute_limits', + 'is_feature_available', 'extension_supported')}) def test_instance_resize_get(self): server = self.servers.first() @@ -3622,6 +3682,7 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): 'flavor_list', 'flavor_get', 'tenant_absolute_limits', + 'is_feature_available', 'extension_supported')}) def test_instance_resize_get_current_flavor_not_found(self): server = self.servers.first() @@ -3659,6 +3720,7 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): instance_resize_post_stubs = { api.nova: ('server_get', 'server_resize', 'flavor_list', 'flavor_get', + 'is_feature_available', 'extension_supported')} @helpers.create_stubs(instance_resize_post_stubs) @@ -3712,7 +3774,8 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): self.assertRedirectsNoFollow(res, INDEX_URL) @helpers.create_stubs({api.glance: ('image_list_detailed',), - api.nova: ('extension_supported',)}) + api.nova: ('extension_supported', + 'is_feature_available',)}) def test_rebuild_instance_get(self, expect_password_fields=True): server = self.servers.first() self._mock_glance_image_list_detailed(self.images.list()) @@ -3754,7 +3817,8 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): instance_rebuild_post_stubs = { api.nova: ('server_rebuild', - 'extension_supported'), + 'extension_supported', + 'is_feature_available',), api.glance: ('image_list_detailed',)} @helpers.create_stubs(instance_rebuild_post_stubs) @@ -3879,7 +3943,7 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): @django.test.utils.override_settings(API_RESULT_PAGE_SIZE=2) @helpers.create_stubs({ api.nova: ('flavor_list', 'server_list', 'tenant_absolute_limits', - 'extension_supported',), + 'extension_supported', 'is_feature_available',), api.glance: ('image_list_detailed',), api.network: ('floating_ip_simple_associate_supported', 'floating_ip_supported', @@ -3892,9 +3956,12 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): page_size = getattr(settings, 'API_RESULT_PAGE_SIZE', 2) servers = self.servers.list()[:3] - api.nova.extension_supported('AdminActions', - IsA(http.HttpRequest)) \ - .MultipleTimes().AndReturn(True) + api.nova.extension_supported( + 'AdminActions', IsA(http.HttpRequest) + ).MultipleTimes().AndReturn(True) + api.nova.is_feature_available( + IsA(http.HttpRequest), 'locked_attribute' + ).MultipleTimes().AndReturn(True) api.nova.extension_supported('Shelve', IsA(http.HttpRequest)) \ .MultipleTimes().AndReturn(True) api.nova.flavor_list(IsA(http.HttpRequest)) \ @@ -4032,7 +4099,8 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): class InstanceAjaxTests(helpers.TestCase): @helpers.create_stubs({api.nova: ("server_get", "flavor_get", - "extension_supported"), + "extension_supported", + "is_feature_available"), api.network: ('servers_update_addresses',), api.neutron: ("is_extension_supported",)}) def test_row_update(self): @@ -4042,8 +4110,11 @@ class InstanceAjaxTests(helpers.TestCase): flavors = self.flavors.list() full_flavors = OrderedDict([(f.id, f) for f in flavors]) - api.nova.extension_supported('AdminActions', IsA(http.HttpRequest))\ + api.nova.extension_supported('AdminActions', IsA(http.HttpRequest)) \ .MultipleTimes().AndReturn(True) + api.nova.is_feature_available( + IsA(http.HttpRequest), 'locked_attribute' + ).MultipleTimes().AndReturn(True) api.nova.extension_supported('Shelve', IsA(http.HttpRequest)) \ .MultipleTimes().AndReturn(True) api.neutron.is_extension_supported(IsA(http.HttpRequest), @@ -4066,6 +4137,7 @@ class InstanceAjaxTests(helpers.TestCase): @helpers.create_stubs({api.nova: ("server_get", "flavor_get", + 'is_feature_available', "extension_supported"), api.neutron: ("is_extension_supported",), api.network: ('servers_update_addresses',)}) @@ -4089,6 +4161,9 @@ class InstanceAjaxTests(helpers.TestCase): api.nova.extension_supported('AdminActions', IsA(http.HttpRequest))\ .MultipleTimes().AndReturn(True) + api.nova.is_feature_available( + IsA(http.HttpRequest), 'locked_attribute' + ).MultipleTimes().AndReturn(True) api.nova.extension_supported('Shelve', IsA(http.HttpRequest)) \ .MultipleTimes().AndReturn(True) api.neutron.is_extension_supported(IsA(http.HttpRequest), @@ -4121,6 +4196,7 @@ class InstanceAjaxTests(helpers.TestCase): @helpers.create_stubs({api.nova: ("server_get", "flavor_get", + 'is_feature_available', "extension_supported"), api.neutron: ("is_extension_supported", "servers_update_addresses",)}) @@ -4128,8 +4204,12 @@ class InstanceAjaxTests(helpers.TestCase): server = self.servers.first() instance_id = server.id - api.nova.extension_supported('AdminActions', IsA(http.HttpRequest))\ - .MultipleTimes().AndReturn(True) + api.nova.extension_supported( + 'AdminActions', IsA(http.HttpRequest) + ).MultipleTimes().AndReturn(True) + api.nova.is_feature_available( + IsA(http.HttpRequest), 'locked_attribute' + ).MultipleTimes().AndReturn(True) api.nova.extension_supported('Shelve', IsA(http.HttpRequest)) \ .MultipleTimes().AndReturn(True) api.neutron.is_extension_supported(IsA(http.HttpRequest),