Merge "Multi role RBAC validation"
diff --git a/.zuul.yaml b/.zuul.yaml
index 6862b46..e097bd7 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -14,7 +14,9 @@
       - ^(test-|)requirements.txt$
       - ^.*\.rst$
       - ^doc/.*
+      - ^etc/.*$
       - ^patrole/patrole_tempest_plugin/tests/unit/.*$
+      - ^patrole/patrole_tempest_plugin/hacking/.*$
       - ^releasenotes/.*
       - ^setup.cfg$
     vars:
@@ -50,7 +52,9 @@
       - ^(test-|)requirements.txt$
       - ^.*\.rst$
       - ^doc/.*
+      - ^etc/.*$
       - ^patrole/patrole_tempest_plugin/tests/unit/.*$
+      - ^patrole/patrole_tempest_plugin/hacking/.*$
       - ^releasenotes/.*
       - ^setup.cfg$
     vars:
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/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/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_auto_allocated_topology_rbac.py b/patrole_tempest_plugin/tests/api/network/test_auto_allocated_topology_rbac.py
index bcf62d7..7098e55 100644
--- a/patrole_tempest_plugin/tests/api/network/test_auto_allocated_topology_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_auto_allocated_topology_rbac.py
@@ -35,10 +35,44 @@
                                  rules=["get_auto_allocated_topology"],
                                  expected_error_codes=[404])
     def test_show_auto_allocated_topology(self):
-        """Show auto_allocated_topology.
+        """Test show auto_allocated_topology.
 
         RBAC test for the neutron "get_auto_allocated_topology" policy
         """
         with self.rbac_utils.override_role(self):
             self.ntp_client.get_auto_allocated_topology(
                 tenant_id=self.os_primary.credentials.tenant_id)
+
+    def _ensure_network_not_in_use(cls, network_id):
+        ports = cls.ntp_client.list_ports(network_id=network_id)["ports"]
+
+        # Every subnet within network should have a router interface
+        expected_ports_count = len(
+            cls.ntp_client.show_network(network_id)["network"]["subnets"])
+        # Every network should have a single dhcp interface
+        expected_ports_count += 1
+
+        if len(ports) != expected_ports_count:
+            msg = "Auto Allocated Topology in use."
+            cls.skipException(msg)
+
+    @decorators.idempotent_id('A0606AFE-065E-4C09-8E51-58EE7FBA30A2')
+    @decorators.attr(type='slow')
+    @rbac_rule_validation.action(service="neutron",
+                                 rules=["get_auto_allocated_topology",
+                                        "delete_auto_allocated_topology"],
+                                 expected_error_codes=[404, 403])
+    def test_delete_auto_allocated_topology(self):
+        """Test delete auto_allocated_topology.
+
+        RBAC test for the neutron "delete_auto_allocated_topology" policy
+        """
+        tenant_id = self.os_primary.credentials.tenant_id
+        net_id = self.ntp_client.get_auto_allocated_topology(
+            tenant_id=tenant_id)["auto_allocated_topology"]["id"]
+
+        self._ensure_network_not_in_use(net_id)
+
+        with self.rbac_utils.override_role(self):
+            self.ntp_client.delete_auto_allocated_topology(
+                tenant_id=self.os_primary.credentials.tenant_id)
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/setup.cfg b/setup.cfg
index 02ce831..77a039a 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -26,20 +26,6 @@
 [upload_sphinx]
 upload-dir = doc/build/html
 
-[compile_catalog]
-directory = patrole/locale
-domain = patrole
-
-[update_catalog]
-domain = patrole
-output_dir = patrole/locale
-input_file = patrole/locale/patrole.pot
-
-[extract_messages]
-keywords = _ gettext ngettext l_ lazy_gettext
-mapping_file = babel.cfg
-output_file = patrole/locale/patrole.pot
-
 [build_releasenotes]
 all_files = 1
 build-dir = releasenotes/build
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