Merge "pbr: Remove unused translation sections from setup.cfg"
diff --git a/HACKING.rst b/HACKING.rst
index 28a977d..87e3b1f 100644
--- a/HACKING.rst
+++ b/HACKING.rst
@@ -33,12 +33,15 @@
 The following are Patrole's specific Commandments:
 
 - [P100] The ``rbac_rule_validation.action`` decorator must be applied to
-  an RBAC test
+  all RBAC tests
 - [P101] RBAC test filenames must end with "_rbac.py"; for example,
   test_servers_rbac.py, not test_servers.py
 - [P102] RBAC test class names must end in 'RbacTest'
 - [P103] ``self.client`` must not be used as a client alias; this allows for
   code that is more maintainable and easier to read
+- [P104] RBAC `plugin test class`_ names must end in 'PluginRbacTest'
+
+.. _plugin test class: https://github.com/openstack/patrole/tree/master/patrole_tempest_plugin/tests/api/network#neutron-plugin-tests
 
 Role Overriding
 ---------------
diff --git a/devstack/plugin.sh b/devstack/plugin.sh
index a6259f4..502b68c 100644
--- a/devstack/plugin.sh
+++ b/devstack/plugin.sh
@@ -27,10 +27,10 @@
         iniset $TEMPEST_CONFIG policy-feature-enabled volume_extension_volume_actions_reserve_policy False
         iniset $TEMPEST_CONFIG policy-feature-enabled volume_extension_volume_actions_unreserve_policy False
 
+        # TODO(cl566n): Remove these once stable/pike becomes EOL.
         # These policies were removed in Stein but are available in Pike.
         iniset $TEMPEST_CONFIG policy-feature-enabled removed_nova_policies_stein False
-
-        # TODO(cl566n): Policies used by Patrole testing. Remove these once stable/pike becomes EOL.
+        iniset $TEMPEST_CONFIG policy-feature-enabled removed_keystone_policies_stein False
         iniset $TEMPEST_CONFIG policy-feature-enabled added_cinder_policies_stein False
     fi
 
@@ -39,16 +39,17 @@
             RBAC_TEST_ROLE="Member"
         fi
 
+        # TODO(cl566n): Remove these once stable/queens becomes EOL.
         # These policies were removed in Stein but are available in Queens.
         iniset $TEMPEST_CONFIG policy-feature-enabled removed_nova_policies_stein False
-
-        # TODO(cl566n): Policies used by Patrole testing. Remove these once stable/queens becomes EOL.
+        iniset $TEMPEST_CONFIG policy-feature-enabled removed_keystone_policies_stein False
         iniset $TEMPEST_CONFIG policy-feature-enabled added_cinder_policies_stein False
     fi
 
     if [[ ${DEVSTACK_SERIES} == 'rocky' ]]; then
         # TODO(cl566n): Policies used by Patrole testing. Remove these once stable/rocky becomes EOL.
         iniset $TEMPEST_CONFIG policy-feature-enabled added_cinder_policies_stein False
+        iniset $TEMPEST_CONFIG policy-feature-enabled removed_keystone_policies_stein False
     fi
 
     iniset $TEMPEST_CONFIG patrole rbac_test_role $RBAC_TEST_ROLE
diff --git a/patrole_tempest_plugin/config.py b/patrole_tempest_plugin/config.py
index 56a786b..dc0ed25 100644
--- a/patrole_tempest_plugin/config.py
+++ b/patrole_tempest_plugin/config.py
@@ -162,11 +162,16 @@
                 help="""Are the Nova API extension policies available in the
 cloud (e.g. os_compute_api:os-extended-availability-zone)? These policies were
 removed in Stein because Nova API extension concept was removed in Pike."""),
+    cfg.BoolOpt('removed_keystone_policies_stein',
+                default=True,
+                help="""Are the obsolete Keystone policies available in the
+cloud (e.g. identity:[create|update|get|delete]_credential)? These policies
+were removed in Stein."""),
     cfg.BoolOpt('added_cinder_policies_stein',
                 default=True,
-                help="""Are the Cinder API extension policies available in the
-cloud (e.g. [create|update|get|delete]_encryption_policy)? These policies are
-added in Stein.""")
+                help="""Are the Cinder Stein policies available in the cloud
+(e.g. [create|update|get|delete]_encryption_policy)? These policies are added
+in Stein.""")
 ]
 
 
diff --git a/patrole_tempest_plugin/hacking/checks.py b/patrole_tempest_plugin/hacking/checks.py
index d106da8..1f06258 100644
--- a/patrole_tempest_plugin/hacking/checks.py
+++ b/patrole_tempest_plugin/hacking/checks.py
@@ -36,6 +36,8 @@
 RULE_VALIDATION_DECORATOR = re.compile(
     r'\s*@rbac_rule_validation.action\(.*')
 IDEMPOTENT_ID_DECORATOR = re.compile(r'\s*@decorators\.idempotent_id\((.*)\)')
+PLUGIN_RBAC_TEST = re.compile(
+    r"class .+\(.+PluginRbacTest\)|class .+PluginRbacTest\(.+\)")
 
 have_rbac_decorator = False
 
@@ -211,6 +213,44 @@
             return 0, "Do not use 'self.client' as a service client alias"
 
 
+def no_plugin_rbac_test_suffix_in_plugin_test_class_name(physical_line,
+                                                         filename):
+    """Check that Plugin RBAC class names end with "PluginRbacTest"
+
+    P104
+    """
+    suffix = "PluginRbacTest"
+    if "patrole_tempest_plugin/tests/api" in filename:
+        if PLUGIN_RBAC_TEST.match(physical_line):
+            subclass, superclass = physical_line.split('(')
+            subclass = subclass.split('class')[1].strip()
+            superclass = superclass.split(')')[0].strip()
+            if "." in superclass:
+                superclass = superclass.split(".")[1]
+
+            both_have = all(
+                clazz.endswith(suffix) for clazz in [subclass, superclass])
+            none_have = not any(
+                clazz.endswith(suffix) for clazz in [subclass, superclass])
+
+            if not (both_have or none_have):
+                if (subclass.startswith("Base") and
+                        superclass.startswith("Base")):
+                    return
+
+                # Case 1: Subclass of "BasePluginRbacTest" must end in `suffix`
+                # Case 2: Subclass that ends in `suffix` must inherit from base
+                # class ending in `suffix`.
+                if not subclass.endswith(suffix):
+                    error = ("Plugin RBAC test subclasses must end in "
+                             "'PluginRbacTest'")
+                    return len(subclass) - 1, error
+                elif not superclass.endswith(suffix):
+                    error = ("Plugin RBAC test subclasses must inherit from a "
+                             "'PluginRbacTest' base class")
+                    return len(superclass) - 1, error
+
+
 def factory(register):
     register(import_no_clients_in_api_tests)
     register(no_setup_teardown_class_for_tests)
@@ -223,3 +263,4 @@
     register(no_rbac_rule_validation_decorator)
     register(no_rbac_suffix_in_test_filename)
     register(no_rbac_test_suffix_in_test_class_name)
+    register(no_plugin_rbac_test_suffix_in_plugin_test_class_name)
diff --git a/patrole_tempest_plugin/tests/api/identity/v3/test_credentials_rbac.py b/patrole_tempest_plugin/tests/api/identity/v3/test_credentials_rbac.py
index 977a830..26e34da 100644
--- a/patrole_tempest_plugin/tests/api/identity/v3/test_credentials_rbac.py
+++ b/patrole_tempest_plugin/tests/api/identity/v3/test_credentials_rbac.py
@@ -13,13 +13,21 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+import testtools
+
+from tempest import config
 from tempest.lib.common.utils import data_utils
 from tempest.lib import decorators
 
 from patrole_tempest_plugin import rbac_rule_validation
 from patrole_tempest_plugin.tests.api.identity import rbac_base
 
+CONF = config.CONF
 
+
+@testtools.skipIf(
+    CONF.policy_feature_enabled.removed_keystone_policies_stein,
+    "This policy is unavailable in Stein so cannot be tested.")
 class IdentityCredentialsV3RbacTest(rbac_base.BaseIdentityV3RbacTest):
 
     def _create_user_project_and_credential(self):
diff --git a/patrole_tempest_plugin/tests/api/network/test_address_scope_rbac.py b/patrole_tempest_plugin/tests/api/network/test_address_scope_rbac.py
index db28b82..893942e 100644
--- a/patrole_tempest_plugin/tests/api/network/test_address_scope_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_address_scope_rbac.py
@@ -23,18 +23,18 @@
 from patrole_tempest_plugin.tests.api.network import rbac_base as base
 
 
-class AddressScopeRbacTest(base.BaseNetworkPluginRbacTest):
+class AddressScopePluginRbacTest(base.BaseNetworkPluginRbacTest):
 
     @classmethod
     def skip_checks(cls):
-        super(AddressScopeRbacTest, cls).skip_checks()
+        super(AddressScopePluginRbacTest, cls).skip_checks()
         if not utils.is_extension_enabled('address-scope', 'network'):
             msg = "address-scope extension not enabled."
             raise cls.skipException(msg)
 
     @classmethod
     def resource_setup(cls):
-        super(AddressScopeRbacTest, cls).resource_setup()
+        super(AddressScopePluginRbacTest, cls).resource_setup()
         cls.network = cls.create_network()
 
     def _create_address_scope(self, name=None, **kwargs):
diff --git a/patrole_tempest_plugin/tests/api/network/test_qos_rbac.py b/patrole_tempest_plugin/tests/api/network/test_qos_rbac.py
index 20f9e61..aae326c 100644
--- a/patrole_tempest_plugin/tests/api/network/test_qos_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_qos_rbac.py
@@ -22,18 +22,18 @@
 from patrole_tempest_plugin.tests.api.network import rbac_base as base
 
 
-class QosRbacTest(base.BaseNetworkPluginRbacTest):
+class QosPluginRbacTest(base.BaseNetworkPluginRbacTest):
 
     @classmethod
     def skip_checks(cls):
-        super(QosRbacTest, cls).skip_checks()
+        super(QosPluginRbacTest, cls).skip_checks()
         if not utils.is_extension_enabled('qos', 'network'):
             msg = "qos extension not enabled."
             raise cls.skipException(msg)
 
     @classmethod
     def resource_setup(cls):
-        super(QosRbacTest, cls).resource_setup()
+        super(QosPluginRbacTest, cls).resource_setup()
         cls.network = cls.create_network()
 
     def create_policy(self, name=None):
diff --git a/patrole_tempest_plugin/tests/api/volume/test_volume_transfers_rbac.py b/patrole_tempest_plugin/tests/api/volume/test_volume_transfers_rbac.py
index a18a370..b7e45f9 100644
--- a/patrole_tempest_plugin/tests/api/volume/test_volume_transfers_rbac.py
+++ b/patrole_tempest_plugin/tests/api/volume/test_volume_transfers_rbac.py
@@ -54,12 +54,17 @@
     def test_create_volume_transfer(self):
         with self.rbac_utils.override_role(self):
             self._create_transfer()
+        waiters.wait_for_volume_resource_status(
+            self.volumes_client, self.volume['id'], 'awaiting-transfer')
 
     @rbac_rule_validation.action(service="cinder",
                                  rules=["volume:get_transfer"])
     @decorators.idempotent_id('7a0925d3-ed97-4c25-8299-e5cdabe2eb55')
     def test_get_volume_transfer(self):
         transfer = self._create_transfer()
+        waiters.wait_for_volume_resource_status(
+            self.volumes_client, self.volume['id'], 'awaiting-transfer')
+
         with self.rbac_utils.override_role(self):
             self.transfers_client.show_volume_transfer(transfer['id'])
 
@@ -82,15 +87,23 @@
     @decorators.idempotent_id('987f2a11-d657-4984-a6c9-28f06c1cd014')
     def test_accept_volume_transfer(self):
         transfer = self._create_transfer()
+        waiters.wait_for_volume_resource_status(
+            self.volumes_client, self.volume['id'], 'awaiting-transfer')
+
         with self.rbac_utils.override_role(self):
             self.transfers_client.accept_volume_transfer(
                 transfer['id'], auth_key=transfer['auth_key'])
+        waiters.wait_for_volume_resource_status(self.volumes_client,
+                                                self.volume['id'], 'available')
 
     @rbac_rule_validation.action(service="cinder",
                                  rules=["volume:delete_transfer"])
     @decorators.idempotent_id('4672187e-7fff-454b-832a-5c8865dda868')
     def test_delete_volume_transfer(self):
         transfer = self._create_transfer()
+        waiters.wait_for_volume_resource_status(
+            self.volumes_client, self.volume['id'], 'awaiting-transfer')
+
         with self.rbac_utils.override_role(self):
             self.transfers_client.delete_volume_transfer(transfer['id'])
         waiters.wait_for_volume_resource_status(
diff --git a/patrole_tempest_plugin/tests/unit/test_hacking.py b/patrole_tempest_plugin/tests/unit/test_hacking.py
index 6096c24..d35b816 100644
--- a/patrole_tempest_plugin/tests/unit/test_hacking.py
+++ b/patrole_tempest_plugin/tests/unit/test_hacking.py
@@ -256,3 +256,53 @@
         self.assertTrue(checks.no_client_alias_in_test_cases(
             "  cls.client",
             "./patrole_tempest_plugin/tests/api/fake_test_rbac.py"))
+
+    def test_no_plugin_rbac_test_suffix_in_plugin_test_class_name(self):
+        check = checks.no_plugin_rbac_test_suffix_in_plugin_test_class_name
+
+        # Passing cases: these do not inherit from "PluginRbacTest" base class.
+        self.assertFalse(check(
+            "class FakeRbacTest(BaseFakeRbacTest)",
+            "./patrole_tempest_plugin/tests/api/fake_test_rbac.py"))
+        self.assertFalse(check(
+            "class FakeRbacTest(base.BaseFakeRbacTest)",
+            "./patrole_tempest_plugin/tests/api/fake_test_rbac.py"))
+
+        # Passing cases: these **do** end in correct test class suffix.
+        self.assertFalse(check(
+            "class FakePluginRbacTest(BaseFakePluginRbacTest)",
+            "./patrole_tempest_plugin/tests/api/fake_test_rbac.py"))
+        self.assertFalse(check(
+            "class FakePluginRbacTest(base.BaseFakePluginRbacTest)",
+            "./patrole_tempest_plugin/tests/api/fake_test_rbac.py"))
+
+        # Passing cases: plugin base class inherits from another base class.
+        self.assertFalse(check(
+            "class BaseFakePluginRbacTest(base.BaseFakeRbacTest)",
+            "./patrole_tempest_plugin/tests/api/fake_test_rbac.py"))
+        self.assertFalse(check(
+            "class BaseFakePluginRbacTest(BaseFakeRbacTest)",
+            "./patrole_tempest_plugin/tests/api/fake_test_rbac.py"))
+
+        # Failing cases: these **do not** end in correct test class suffix.
+        # Case 1: RbacTest subclass doesn't end in PluginRbacTest.
+        self.assertTrue(check(
+            "class FakeRbacTest(base.BaseFakePluginRbacTest)",
+            "./patrole_tempest_plugin/tests/api/fake_test_rbac.py"))
+        self.assertTrue(check(
+            "class FakeRbacTest(BaseFakePluginRbacTest)",
+            "./patrole_tempest_plugin/tests/api/fake_test_rbac.py"))
+        self.assertTrue(check(
+            "class FakeRbacTest(BaseFakeNetworkPluginRbacTest)",
+            "./patrole_tempest_plugin/tests/api/network/fake_test_rbac.py"))
+        # Case 2: PluginRbacTest subclass doesn't inherit from
+        # BasePluginRbacTest.
+        self.assertTrue(check(
+            "class FakePluginRbacTest(base.BaseFakeRbacTest)",
+            "./patrole_tempest_plugin/tests/api/fake_test_rbac.py"))
+        self.assertTrue(check(
+            "class FakePluginRbacTest(BaseFakeRbacTest)",
+            "./patrole_tempest_plugin/tests/api/fake_test_rbac.py"))
+        self.assertTrue(check(
+            "class FakeNeutronPluginRbacTest(BaseFakeNeutronRbacTest)",
+            "./patrole_tempest_plugin/tests/api/fake_test_rbac.py"))
diff --git a/releasenotes/notes/removed-keystone-policies-stein-feature-flag-6cfebbf64ed525d7.yaml b/releasenotes/notes/removed-keystone-policies-stein-feature-flag-6cfebbf64ed525d7.yaml
new file mode 100644
index 0000000..3bed287
--- /dev/null
+++ b/releasenotes/notes/removed-keystone-policies-stein-feature-flag-6cfebbf64ed525d7.yaml
@@ -0,0 +1,8 @@
+---
+features:
+  - |
+    Added new feature flag called ``removed_keystone_policies_stein`` under
+    the configuration group ``[policy-feature-enabled]`` for skipping Keystone
+    tests whose policies were removed in Stein. This feature flag is currently
+    applied to credentials-related policies, e.g.:
+    identity:[create|update|get|delete]_credential
diff --git a/tox.ini b/tox.ini
index ea9abf1..bc829d2 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
 [tox]
-minversion = 1.6
+minversion = 2.0
 envlist = pep8,py35,py27
 skipsdist = True