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),