Merge "docs: Add symlink to readme from overview documentation"
diff --git a/.zuul.yaml b/.zuul.yaml
index 60f0d05..21b5679 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -29,8 +29,20 @@
 
 - job:
     name: patrole-base-multinode
-    parent: legacy-dsvm-base-multinode
+    parent: tempest-multinode-full
+    description: |-
+      Patrole base job for multinode and "slow" tests where "slow" tests include:
+
+      * Tests that take more than ~30 seconds to run.
+      * Tests that experience spurious failures related to servers, volumes,
+        backups and similar resources failing to build.
     timeout: 7800
+    branches:
+      - master
+    required-projects:
+      - openstack-infra/devstack-gate
+      - openstack/tempest
+      - openstack/patrole
     irrelevant-files:
       - ^(test-|)requirements.txt$
       - ^.*\.rst$
@@ -38,10 +50,17 @@
       - ^patrole/patrole_tempest_plugin/tests/unit/.*$
       - ^releasenotes/.*
       - ^setup.cfg$
-    required-projects:
-      - openstack-infra/devstack-gate
-      - openstack/patrole
-      - openstack/tempest
+    vars:
+      devstack_localrc:
+        TEMPEST_PLUGINS: "'{{ ansible_user_dir }}/src/git.openstack.org/openstack/patrole'"
+      devstack_plugins:
+        patrole: git://git.openstack.org/openstack/patrole.git
+      devstack_services:
+        tempest: true
+        neutron: true
+      tempest_concurrency: 1
+      tempest_test_regex: (?=.*\[.*\bslow\b.*\])(^patrole_tempest_plugin\.tests\.api)
+      tox_envlist: all-plugin
 
 - job:
     name: patrole-admin
@@ -77,18 +96,18 @@
 - job:
     name: patrole-multinode-admin
     parent: patrole-base-multinode
-    run: playbooks/patrole-multinode-admin/run.yaml
-    post-run: playbooks/patrole-multinode-admin/post.yaml
     voting: false
-    nodeset: legacy-ubuntu-xenial-2-node
+    vars:
+      devstack_localrc:
+        RBAC_TEST_ROLE: admin
 
 - job:
     name: patrole-multinode-member
     parent: patrole-base-multinode
-    run: playbooks/patrole-multinode-member/run.yaml
-    post-run: playbooks/patrole-multinode-member/post.yaml
     voting: false
-    nodeset: legacy-ubuntu-xenial-2-node
+    vars:
+      devstack_localrc:
+        RBAC_TEST_ROLE: member
 
 - job:
     name: patrole-py35-member
diff --git a/doc/source/field_guide/rbac.rst b/doc/source/field_guide/rbac.rst
index 2654d31..a383099 100644
--- a/doc/source/field_guide/rbac.rst
+++ b/doc/source/field_guide/rbac.rst
@@ -53,6 +53,53 @@
 * Patrole is designed to work with any role via configuration settings, but
   on the other hand the projects handpick which roles to test.
 
+Why not use Patrole framework on Tempest tests?
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The Patrole framework can't be applied to existing Tempest tests via
+:ref:`rbac-validation`, because:
+
+* Tempest tests aren't factored the right way: They're not granular enough.
+  They call too many APIs and too many policies are enforced by each test.
+* Tempest tests assume default policy rules: Tempest uses ``os_admin``
+  `credentials`_ for admin APIs and ``os_primary`` for non-admin APIs.
+  This breaks for custom policy overrides.
+* Tempest doesn't have tests that enforce all the policy actions, regardless.
+  Some RBAC tests require that tests be written a very precise way for the
+  server to authorize the expected policy actions.
+
+Why are these tests not in Tempest?
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Patrole should be a separate project that specializes in RBAC tests. This
+was agreed upon during `discussion`_ that led to the approval of the RBAC
+testing framework `spec`_, which was the genesis for Patrole.
+
+Philosophically speaking:
+
+* Tempest supports `API and scenario testing`_. RBAC testing is out of scope.
+* The `OpenStack project structure reform`_ evolved OpenStack "to a more
+  decentralized model where [projects like QA] provide processes and tools to
+  empower projects to do the work themselves". This model resulted in the
+  creation of the `Tempest external plugin interface`_.
+* Tempest supports `plugins`_. Why not use one for RBAC testing?
+
+Practically speaking:
+
+* The Tempest team should not be burdened with having to support Patrole, too.
+  Tempest is a big project and having to absorb RBAC testing is difficult.
+* Tempest already has many in-tree Zuul checks/gates. If Patrole tests lived
+  in Tempest, then adding more Zuul checks/gates for Patrole would only make it
+  harder to get changes merged in Tempest.
+
+.. _credentials: https://docs.openstack.org/tempest/latest/write_tests.html#allocating-credentials
+.. _discussion: https://review.openstack.org/#/c/382672/
+.. _spec: https://specs.openstack.org/openstack/qa-specs/specs/tempest/rbac-policy-testing.html
+.. _API and scenario testing: https://docs.openstack.org/tempest/latest/overview.html#tempest-the-openstack-integration-test-suite
+.. _OpenStack project structure reform: https://governance.openstack.org/tc/resolutions/20141202-project-structure-reform-spec.html#impact-for-horizontal-teams
+.. _Tempest external plugin interface: https://specs.openstack.org/openstack/qa-specs/specs/tempest/implemented/tempest-external-plugin-interface.html
+.. _plugins: https://docs.openstack.org/tempest/latest/plugin.html
+
 
 Scope of these tests
 --------------------
diff --git a/doc/source/framework/overview.rst b/doc/source/framework/overview.rst
index d862770..4902f7b 100644
--- a/doc/source/framework/overview.rst
+++ b/doc/source/framework/overview.rst
@@ -26,9 +26,11 @@
    "Inconsistent" (or failing) cases include:
 
    * Expected result is ``False`` and the test passes. This results in an
-     ``RbacOverPermission`` exception getting thrown.
+     :class:`~rbac_exceptions.RbacOverPermissionException` exception
+     getting thrown.
    * Expected result is ``True`` and the test fails. This results in a
-     ``Forbidden`` exception getting thrown.
+     :class:`~rbac_exceptions.RbacOverPermissionException` exception
+     getting thrown.
 
    For example, a 200 from the API call and a ``False`` result from
    ``oslo.policy`` or a 403 from the API call and a ``True`` result from
diff --git a/etc/patrole.conf.sample b/etc/patrole.conf.sample
index 8e7931b..6de073d 100644
--- a/etc/patrole.conf.sample
+++ b/etc/patrole.conf.sample
@@ -45,7 +45,7 @@
 #
 # YAML definition: allowed
 # test run: not allowed
-# test result: fail (under-permission, e.g. Forbidden exception)
+# test result: fail (under-permission)
 #
 # YAML definition: not allowed
 # test run: allowed
diff --git a/patrole_tempest_plugin/config.py b/patrole_tempest_plugin/config.py
index ee7a6c5..4dc27b9 100644
--- a/patrole_tempest_plugin/config.py
+++ b/patrole_tempest_plugin/config.py
@@ -55,7 +55,7 @@
 
 YAML definition: allowed
 test run: not allowed
-test result: fail (under-permission, e.g. Forbidden exception)
+test result: fail (under-permission)
 
 YAML definition: not allowed
 test run: allowed
diff --git a/patrole_tempest_plugin/rbac_exceptions.py b/patrole_tempest_plugin/rbac_exceptions.py
index e75b8ec..980672a 100644
--- a/patrole_tempest_plugin/rbac_exceptions.py
+++ b/patrole_tempest_plugin/rbac_exceptions.py
@@ -41,8 +41,27 @@
     message = "RBAC resource setup failed"
 
 
-class RbacOverPermission(exceptions.TempestException):
-    message = "Action performed that should not be permitted"
+class RbacOverPermissionException(exceptions.TempestException):
+    """Raised when the expected result is failure but the actual result is
+    pass.
+    """
+    message = "Unauthorized action was allowed to be performed"
+
+
+class RbacUnderPermissionException(exceptions.TempestException):
+    """Raised when the expected result is pass but the actual result is
+    failure.
+    """
+    message = "Authorized action was not allowed to be performed"
+
+
+class RbacExpectedWrongException(exceptions.TempestException):
+    """Raised when the expected exception does not match the actual exception
+    raised, when both are instances of Forbidden or NotFound, indicating
+    the test provides a wrong argument to `expected_error_codes`.
+    """
+    message = ("Expected %(expected)s to be raised but %(actual)s was raised "
+               "instead. Actual exception: %(exception)s")
 
 
 class RbacInvalidService(exceptions.TempestException):
diff --git a/patrole_tempest_plugin/rbac_rule_validation.py b/patrole_tempest_plugin/rbac_rule_validation.py
index 7d48870..2f6759d 100644
--- a/patrole_tempest_plugin/rbac_rule_validation.py
+++ b/patrole_tempest_plugin/rbac_rule_validation.py
@@ -64,9 +64,9 @@
 
     1) If *expected* is True and the test passes (*actual*), this is a success.
     2) If *expected* is True and the test fails (*actual*), this results in a
-       `Forbidden` exception failure.
+       ``RbacUnderPermissionException`` exception failure.
     3) If *expected* is False and the test passes (*actual*), this results in
-       an `OverPermission` exception failure.
+       an ``RbacOverPermissionException`` exception failure.
     4) If *expected* is False and the test fails (*actual*), this is a success.
 
     As such, negative and positive testing can be applied using this decorator.
@@ -82,10 +82,10 @@
 
             Patrole currently only supports custom JSON policy files.
 
-    :param int expected_error_code: Overrides default value of 403 (Forbidden)
-        with endpoint-specific error code. Currently only supports 403 and 404.
-        Support for 404 is needed because some services, like Neutron,
-        intentionally throw a 404 for security reasons.
+    :param int expected_error_code: (DEPRECATED) Overrides default value of 403
+        (Forbidden) with endpoint-specific error code. Currently only supports
+        403 and 404. Support for 404 is needed because some services, like
+        Neutron, intentionally throw a 404 for security reasons.
 
         .. warning::
 
@@ -99,6 +99,7 @@
         in the rules list.
 
         Example::
+
             rules=["api_action1", "api_action2"]
             expected_error_codes=[404, 403]
 
@@ -123,8 +124,10 @@
             })
 
     :raises NotFound: If ``service`` is invalid.
-    :raises Forbidden: For item (2) above.
-    :raises RbacOverPermission: For item (3) above.
+    :raises RbacUnderPermissionException: For item (2) above.
+    :raises RbacOverPermissionException: For item (3) above.
+    :raises RbacExpectedWrongException: When a 403 is expected but a 404
+        is raised instead or vice versa.
 
     Examples::
 
@@ -192,7 +195,10 @@
                     rbac_exceptions.RbacMalformedResponse) as e:
                 test_status = 'Denied'
                 if irregular_msg:
-                    LOG.warning(irregular_msg.format(rule, service))
+                    LOG.warning(irregular_msg,
+                                test_func.__name__,
+                                ', '.join(rules),
+                                service)
                 if allowed:
                     msg = ("Role %s was not allowed to perform the following "
                            "actions: %s. Expected allowed actions: %s. "
@@ -201,17 +207,28 @@
                                sorted(set(rules) - set(disallowed_rules)),
                                sorted(disallowed_rules)))
                     LOG.error(msg)
-                    raise exceptions.Forbidden(
+                    raise rbac_exceptions.RbacUnderPermissionException(
                         "%s Exception was: %s" % (msg, e))
-            except Exception as e:
-                with excutils.save_and_reraise_exception():
-                    exc_info = sys.exc_info()
-                    error_details = six.text_type(exc_info[1])
-                    msg = ("An unexpected exception has occurred during test: "
-                           "%s. Exception was: %s" % (test_func.__name__,
-                                                      error_details))
-                    test_status = 'Error, %s' % (error_details)
-                    LOG.error(msg)
+            except Exception as actual_exception:
+                if _check_for_expected_mismatch_exception(expected_exception,
+                                                          actual_exception):
+                    LOG.error('Expected and actual exceptions do not match. '
+                              'Expected: %s. Actual: %s.',
+                              expected_exception,
+                              actual_exception.__class__)
+                    raise rbac_exceptions.RbacExpectedWrongException(
+                        expected=expected_exception,
+                        actual=actual_exception.__class__,
+                        exception=actual_exception)
+                else:
+                    with excutils.save_and_reraise_exception():
+                        exc_info = sys.exc_info()
+                        error_details = six.text_type(exc_info[1])
+                        msg = ("An unexpected exception has occurred during "
+                               "test: %s. Exception was: %s" % (
+                                   test_func.__name__, error_details))
+                        test_status = 'Error, %s' % (error_details)
+                        LOG.error(msg)
             else:
                 if not allowed:
                     msg = (
@@ -221,13 +238,13 @@
                         )
                     )
                     LOG.error(msg)
-                    raise rbac_exceptions.RbacOverPermission(msg)
+                    raise rbac_exceptions.RbacOverPermissionException(msg)
             finally:
                 if CONF.patrole_log.enable_reporting:
                     RBACLOG.info(
-                        "[Service]: %s, [Test]: %s, [Rule]: %s, "
+                        "[Service]: %s, [Test]: %s, [Rules]: %s, "
                         "[Expected]: %s, [Actual]: %s",
-                        service, test_func.__name__, rule,
+                        service, test_func.__name__, ', '.join(rules),
                         "Allowed" if allowed else "Denied",
                         test_status)
 
@@ -236,7 +253,6 @@
 
 
 def _prepare_multi_policy(rule, rules, exp_error_code, exp_error_codes):
-
     if exp_error_codes:
         if not rules:
             msg = ("The `rules` list must be provided if using the "
@@ -331,16 +347,16 @@
     is_allowed = authority.allowed(rule, role)
 
     if is_allowed:
-        LOG.debug("[Action]: %s, [Role]: %s is allowed!", rule,
+        LOG.debug("[Policy action]: %s, [Role]: %s is allowed!", rule,
                   role)
     else:
-        LOG.debug("[Action]: %s, [Role]: %s is NOT allowed!",
+        LOG.debug("[Policy action]: %s, [Role]: %s is NOT allowed!",
                   rule, role)
 
     return is_allowed
 
 
-def _get_exception_type(expected_error_code=403):
+def _get_exception_type(expected_error_code=_DEFAULT_ERROR_CODE):
     """Dynamically calculate the expected exception to be caught.
 
     Dynamically calculate the expected exception to be caught by the test case.
@@ -369,9 +385,10 @@
         expected_exception = exceptions.Forbidden
     elif expected_error_code == 404:
         expected_exception = exceptions.NotFound
-        irregular_msg = ("NotFound exception was caught for policy action "
-                         "{0}. The service {1} throws a 404 instead of a 403, "
-                         "which is irregular.")
+        irregular_msg = ("NotFound exception was caught for test %s. Expected "
+                         "policies which may have caused the error: %s. The "
+                         "service %s throws a 404 instead of a 403, which is "
+                         "irregular.")
 
     return expected_exception, irregular_msg
 
@@ -410,3 +427,12 @@
         formatted_target_data[user_attribute] = attr_value
 
     return formatted_target_data
+
+
+def _check_for_expected_mismatch_exception(expected_exception,
+                                           actual_exception):
+    permission_exceptions = (exceptions.Forbidden, exceptions.NotFound)
+    if isinstance(actual_exception, permission_exceptions):
+        if not isinstance(actual_exception, expected_exception.__class__):
+            return True
+    return False
diff --git a/patrole_tempest_plugin/services/__init__.py b/patrole_tempest_plugin/services/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/patrole_tempest_plugin/services/__init__.py
+++ /dev/null
diff --git a/patrole_tempest_plugin/tests/api/compute/test_server_misc_policy_actions_rbac.py b/patrole_tempest_plugin/tests/api/compute/test_server_misc_policy_actions_rbac.py
index 13faca1..d97f382 100644
--- a/patrole_tempest_plugin/tests/api/compute/test_server_misc_policy_actions_rbac.py
+++ b/patrole_tempest_plugin/tests/api/compute/test_server_misc_policy_actions_rbac.py
@@ -610,9 +610,6 @@
         network_id = self.network['id']
         interface = self.interfaces_client.create_interface(
             self.server['id'], net_id=network_id)['interfaceAttachment']
-        waiters.wait_for_interface_status(
-            self.interfaces_client, self.server['id'],
-            interface['port_id'], 'ACTIVE')
         self.addCleanup(
             waiters.wait_for_interface_detach, self.interfaces_client,
             self.server['id'], interface['port_id'])
@@ -620,6 +617,9 @@
             test_utils.call_and_ignore_notfound_exc,
             self.interfaces_client.delete_interface,
             self.server['id'], interface['port_id'])
+        waiters.wait_for_interface_status(
+            self.interfaces_client, self.server['id'],
+            interface['port_id'], 'ACTIVE')
         return interface
 
     @testtools.skipUnless(CONF.compute_feature_enabled.interface_attach,
@@ -661,9 +661,6 @@
         with self.rbac_utils.override_role(self):
             interface = self.interfaces_client.create_interface(
                 self.server['id'], net_id=network_id)['interfaceAttachment']
-        waiters.wait_for_interface_status(
-            self.interfaces_client, self.server['id'],
-            interface['port_id'], 'ACTIVE')
         self.addCleanup(
             waiters.wait_for_interface_detach, self.interfaces_client,
             self.server['id'], interface['port_id'])
@@ -671,6 +668,9 @@
             test_utils.call_and_ignore_notfound_exc,
             self.interfaces_client.delete_interface,
             self.server['id'], interface['port_id'])
+        waiters.wait_for_interface_status(
+            self.interfaces_client, self.server['id'],
+            interface['port_id'], 'ACTIVE')
 
     @testtools.skipUnless(CONF.compute_feature_enabled.interface_attach,
                           "Interface attachment is not available.")
@@ -740,3 +740,17 @@
         with self.rbac_utils.override_role(self):
             self.servers_client.add_fixed_ip(self.server['id'],
                                              networkId=network_id)
+        # Get the Fixed IP from server.
+        server_detail = self.servers_client.show_server(
+            self.server['id'])['server']
+        fixed_ip = None
+        for ip_set in server_detail['addresses']:
+            for ip in server_detail['addresses'][ip_set]:
+                if ip['OS-EXT-IPS:type'] == 'fixed':
+                    fixed_ip = ip['addr']
+                    break
+            if fixed_ip is not None:
+                break
+        # Remove the fixed IP from server.
+        self.servers_client.remove_fixed_ip(self.server['id'],
+                                            address=fixed_ip)
diff --git a/patrole_tempest_plugin/tests/api/network/test_agents_rbac.py b/patrole_tempest_plugin/tests/api/network/test_agents_rbac.py
index 6b03ebe..2756a10 100644
--- a/patrole_tempest_plugin/tests/api/network/test_agents_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_agents_rbac.py
@@ -50,8 +50,8 @@
 
     @decorators.idempotent_id('8ca68fdb-eaf6-4880-af82-ba0982949dec')
     @rbac_rule_validation.action(service="neutron",
-                                 rule="update_agent",
-                                 expected_error_code=404)
+                                 rules=["get_agent", "update_agent"],
+                                 expected_error_codes=[404, 403])
     def test_update_agent(self):
         """Update agent test.
 
diff --git a/patrole_tempest_plugin/tests/api/network/test_floating_ips_rbac.py b/patrole_tempest_plugin/tests/api/network/test_floating_ips_rbac.py
index 20e4aa7..ed52c34 100644
--- a/patrole_tempest_plugin/tests/api/network/test_floating_ips_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_floating_ips_rbac.py
@@ -73,8 +73,11 @@
         with self.rbac_utils.override_role(self):
             self._create_floatingip()
 
-    @rbac_rule_validation.action(service="neutron",
-                                 rule="create_floatingip:floating_ip_address")
+    @rbac_rule_validation.action(
+        service="neutron",
+        rules=["create_floatingip",
+               "create_floatingip:floating_ip_address"],
+        expected_error_codes=[403, 403])
     @decorators.idempotent_id('a8bb826a-403d-4130-a55d-120a0a660806')
     def test_create_floating_ip_floatingip_address(self):
         """Create floating IP with address.
@@ -87,7 +90,8 @@
             self._create_floatingip(floating_ip_address=fip)
 
     @rbac_rule_validation.action(service="neutron",
-                                 rule="update_floatingip")
+                                 rules=["get_floatingip", "update_floatingip"],
+                                 expected_error_codes=[404, 403])
     @decorators.idempotent_id('2ab1b060-19f8-4ef6-a838-e2ab7b377c63')
     def test_update_floating_ip(self):
         """Update floating IP.
@@ -115,8 +119,8 @@
             self.floating_ips_client.show_floatingip(floating_ip['id'])
 
     @rbac_rule_validation.action(service="neutron",
-                                 rule="delete_floatingip",
-                                 expected_error_code=404)
+                                 rules=["get_floatingip", "delete_floatingip"],
+                                 expected_error_codes=[404, 403])
     @decorators.idempotent_id('2611b068-30d4-4241-a78f-1b801a14db7e')
     def test_delete_floating_ip(self):
         """Delete floating IP.
diff --git a/patrole_tempest_plugin/tests/api/network/test_metering_label_rules_rbac.py b/patrole_tempest_plugin/tests/api/network/test_metering_label_rules_rbac.py
index 7a9d814..adab1e6 100644
--- a/patrole_tempest_plugin/tests/api/network/test_metering_label_rules_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_metering_label_rules_rbac.py
@@ -88,8 +88,9 @@
                 label_rule['id'])
 
     @rbac_rule_validation.action(service="neutron",
-                                 rule="delete_metering_label_rule",
-                                 expected_error_code=404)
+                                 rules=["get_metering_label_rule",
+                                        "delete_metering_label_rule"],
+                                 expected_error_codes=[404, 403])
     @decorators.idempotent_id('e3adc88c-05c0-43a7-8e32-63947ae4890e')
     def test_delete_metering_label_rule(self):
         """Delete metering label rule.
diff --git a/patrole_tempest_plugin/tests/api/network/test_metering_labels_rbac.py b/patrole_tempest_plugin/tests/api/network/test_metering_labels_rbac.py
index abd7326..0231868 100644
--- a/patrole_tempest_plugin/tests/api/network/test_metering_labels_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_metering_labels_rbac.py
@@ -71,8 +71,9 @@
             self.metering_labels_client.show_metering_label(label['id'])
 
     @rbac_rule_validation.action(service="neutron",
-                                 rule="delete_metering_label",
-                                 expected_error_code=404)
+                                 rules=["get_metering_label",
+                                        "delete_metering_label"],
+                                 expected_error_codes=[404, 403])
     @decorators.idempotent_id('1621ccfe-2e3f-4d16-98aa-b620f9d00404')
     def test_delete_metering_label(self):
         """Delete metering label.
diff --git a/patrole_tempest_plugin/tests/api/network/test_network_segments_rbac.py b/patrole_tempest_plugin/tests/api/network/test_network_segments_rbac.py
index 1dee46b..0097c7b 100644
--- a/patrole_tempest_plugin/tests/api/network/test_network_segments_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_network_segments_rbac.py
@@ -66,7 +66,9 @@
         return network
 
     @rbac_rule_validation.action(service="neutron",
-                                 rule="create_network:segments")
+                                 rules=["create_network",
+                                        "create_network:segments"],
+                                 expected_error_codes=[403, 403])
     @decorators.idempotent_id('9e1d0c3d-92e3-40e3-855e-bfbb72ea6e0b')
     def test_create_network_segments(self):
         """Create network with segments.
@@ -77,7 +79,9 @@
             self._create_network_segments()
 
     @rbac_rule_validation.action(service="neutron",
-                                 rule="update_network:segments")
+                                 rules=["get_network", "update_network",
+                                        "update_network:segments"],
+                                 expected_error_codes=[404, 403, 403])
     @decorators.idempotent_id('0f45232a-7b59-4bb1-9a91-db77d0a8cc9b')
     def test_update_network_segments(self):
         """Update network segments.
@@ -92,7 +96,9 @@
                                                 segments=new_segments)
 
     @rbac_rule_validation.action(service="neutron",
-                                 rule="get_network:segments")
+                                 rules=["get_network",
+                                        "get_network:segments"],
+                                 expected_error_codes=[404, 403])
     @decorators.idempotent_id('094ff9b7-0c3b-4515-b19b-b9d2031337bd')
     def test_show_network_segments(self):
         """Show network segments.
@@ -113,4 +119,4 @@
             LOG.info("NotFound or Forbidden exception are not thrown when "
                      "role doesn't have access to the endpoint. Instead, "
                      "the response will have an empty network body.")
-            raise rbac_exceptions.RbacMalformedResponse(True)
+            raise rbac_exceptions.RbacMalformedResponse(empty=True)
diff --git a/patrole_tempest_plugin/tests/api/network/test_networks_rbac.py b/patrole_tempest_plugin/tests/api/network/test_networks_rbac.py
index 932683d..1a0e186 100644
--- a/patrole_tempest_plugin/tests/api/network/test_networks_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_networks_rbac.py
@@ -99,7 +99,9 @@
             self._create_network()
 
     @rbac_rule_validation.action(service="neutron",
-                                 rule="create_network:shared")
+                                 rules=["create_network",
+                                        "create_network:shared"],
+                                 expected_error_codes=[403, 403])
     @decorators.idempotent_id('ccabf2a9-28c8-44b2-80e6-ffd65d43eef2')
     def test_create_network_shared(self):
 
@@ -112,7 +114,9 @@
 
     @utils.requires_ext(extension='external-net', service='network')
     @rbac_rule_validation.action(service="neutron",
-                                 rule="create_network:router:external")
+                                 rules=["create_network",
+                                        "create_network:router:external"],
+                                 expected_error_codes=[403, 403])
     @decorators.idempotent_id('51adf2a7-739c-41e0-8857-3b4c460cbd24')
     def test_create_network_router_external(self):
 
@@ -124,8 +128,11 @@
             self._create_network(router_external=True)
 
     @utils.requires_ext(extension='provider', service='network')
-    @rbac_rule_validation.action(service="neutron",
-                                 rule="create_network:provider:network_type")
+    @rbac_rule_validation.action(
+        service="neutron",
+        rules=["create_network",
+               "create_network:provider:network_type"],
+        expected_error_codes=[403, 403])
     @decorators.idempotent_id('3c42f7b8-b80c-44ef-8fa4-69ec4b1836bc')
     def test_create_network_provider_network_type(self):
 
@@ -139,7 +146,9 @@
     @utils.requires_ext(extension='provider', service='network')
     @rbac_rule_validation.action(
         service="neutron",
-        rule="create_network:provider:segmentation_id")
+        rules=["create_network",
+               "create_network:provider:segmentation_id"],
+        expected_error_codes=[403, 403])
     @decorators.idempotent_id('b9decb7b-68ef-4504-b99b-41edbf7d2af5')
     def test_create_network_provider_segmentation_id(self):
 
@@ -152,7 +161,8 @@
                                  provider_segmentation_id=200)
 
     @rbac_rule_validation.action(service="neutron",
-                                 rule="update_network")
+                                 rules=["get_network", "update_network"],
+                                 expected_error_codes=[404, 403])
     @decorators.idempotent_id('6485bb4e-e110-48ae-83e1-3ec8b40c3107')
     def test_update_network(self):
 
@@ -167,7 +177,10 @@
             self._update_network(name=updated_name)
 
     @rbac_rule_validation.action(service="neutron",
-                                 rule="update_network:shared")
+                                 rules=["get_network",
+                                        "update_network",
+                                        "update_network:shared"],
+                                 expected_error_codes=[404, 403, 403])
     @decorators.idempotent_id('37ea3e33-47d9-49fc-9bba-1af98fbd46d6')
     def test_update_network_shared(self):
 
@@ -181,7 +194,10 @@
 
     @utils.requires_ext(extension='external-net', service='network')
     @rbac_rule_validation.action(service="neutron",
-                                 rule="update_network:router:external")
+                                 rules=["get_network",
+                                        "update_network",
+                                        "update_network:router:external"],
+                                 expected_error_codes=[404, 403, 403])
     @decorators.idempotent_id('34884c22-499b-4960-97f1-e2ed8522a9c9')
     def test_update_network_router_external(self):
 
@@ -194,7 +210,8 @@
             self._update_network(net_id=network['id'], router_external=True)
 
     @rbac_rule_validation.action(service="neutron",
-                                 rule="get_network")
+                                 rule="get_network",
+                                 expected_error_code=404)
     @decorators.idempotent_id('0eb62d04-338a-4ff4-a8fa-534e52110534')
     def test_show_network(self):
 
@@ -207,7 +224,9 @@
 
     @utils.requires_ext(extension='external-net', service='network')
     @rbac_rule_validation.action(service="neutron",
-                                 rule="get_network:router:external")
+                                 rules=["get_network",
+                                        "get_network:router:external"],
+                                 expected_error_codes=[404, 403])
     @decorators.idempotent_id('529e4814-22e9-413f-af48-8fefcd637344')
     def test_show_network_router_external(self):
 
@@ -218,12 +237,17 @@
         kwargs = {'fields': 'router:external'}
 
         with self.rbac_utils.override_role(self):
-            self.networks_client.show_network(self.network['id'],
-                                              **kwargs)
+            retrieved_network = self.networks_client.show_network(
+                self.network['id'], **kwargs)['network']
+
+        if len(retrieved_network) == 0:
+            raise rbac_exceptions.RbacMalformedResponse(empty=True)
 
     @utils.requires_ext(extension='provider', service='network')
     @rbac_rule_validation.action(service="neutron",
-                                 rule="get_network:provider:network_type")
+                                 rules=["get_network",
+                                        "get_network:provider:network_type"],
+                                 expected_error_codes=[404, 403])
     @decorators.idempotent_id('6521dd60-0950-458b-8491-09d3c84ac0f4')
     def test_show_network_provider_network_type(self):
 
@@ -238,11 +262,14 @@
                 self.network['id'], **kwargs)['network']
 
         if len(retrieved_network) == 0:
-            raise rbac_exceptions.RbacMalformedResponse(True)
+            raise rbac_exceptions.RbacMalformedResponse(empty=True)
 
     @utils.requires_ext(extension='provider', service='network')
-    @rbac_rule_validation.action(service="neutron",
-                                 rule="get_network:provider:physical_network")
+    @rbac_rule_validation.action(
+        service="neutron",
+        rules=["get_network",
+               "get_network:provider:physical_network"],
+        expected_error_codes=[404, 403])
     @decorators.idempotent_id('c049f11a-240c-4a85-ad43-a4d3fd0a5e39')
     def test_show_network_provider_physical_network(self):
 
@@ -260,8 +287,11 @@
             raise rbac_exceptions.RbacMalformedResponse(empty=True)
 
     @utils.requires_ext(extension='provider', service='network')
-    @rbac_rule_validation.action(service="neutron",
-                                 rule="get_network:provider:segmentation_id")
+    @rbac_rule_validation.action(
+        service="neutron",
+        rules=["get_network",
+               "get_network:provider:segmentation_id"],
+        expected_error_codes=[404, 403])
     @decorators.idempotent_id('38d9f085-6365-4f81-bac9-c53c294d727e')
     def test_show_network_provider_segmentation_id(self):
 
@@ -278,11 +308,9 @@
         if len(retrieved_network) == 0:
             raise rbac_exceptions.RbacMalformedResponse(empty=True)
 
-        key = retrieved_network.get('provider:segmentation_id', "NotFound")
-        self.assertNotEqual(key, "NotFound")
-
     @rbac_rule_validation.action(service="neutron",
-                                 rule="delete_network")
+                                 rules=["get_network", "delete_network"],
+                                 expected_error_codes=[404, 403])
     @decorators.idempotent_id('56ca50ed-ac58-49d6-b239-ed39e7124d5c')
     def test_delete_network(self):
 
diff --git a/patrole_tempest_plugin/tests/api/network/test_ports_rbac.py b/patrole_tempest_plugin/tests/api/network/test_ports_rbac.py
index a8c7d68..2cf3cd6 100644
--- a/patrole_tempest_plugin/tests/api/network/test_ports_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_ports_rbac.py
@@ -68,7 +68,9 @@
 
     @decorators.idempotent_id('045ee797-4962-4913-b96a-5d7ea04099e7')
     @rbac_rule_validation.action(service="neutron",
-                                 rule="create_port:device_owner")
+                                 rules=["create_port",
+                                        "create_port:device_owner"],
+                                 expected_error_codes=[403, 403])
     def test_create_port_device_owner(self):
         with self.rbac_utils.override_role(self):
             self.create_port(self.network,
@@ -76,14 +78,18 @@
 
     @decorators.idempotent_id('c4fa8844-f5ef-4daa-bfa2-b89897dfaedf')
     @rbac_rule_validation.action(service="neutron",
-                                 rule="create_port:port_security_enabled")
+                                 rules=["create_port",
+                                        "create_port:port_security_enabled"],
+                                 expected_error_codes=[403, 403])
     def test_create_port_security_enabled(self):
         with self.rbac_utils.override_role(self):
             self.create_port(self.network, port_security_enabled=True)
 
     @utils.requires_ext(extension='binding', service='network')
     @rbac_rule_validation.action(service="neutron",
-                                 rule="create_port:binding:host_id")
+                                 rules=["create_port",
+                                        "create_port:binding:host_id"],
+                                 expected_error_codes=[403, 403])
     @decorators.idempotent_id('a54bd6b8-a7eb-4101-bfe8-093930b0d660')
     def test_create_port_binding_host_id(self):
 
@@ -95,7 +101,9 @@
 
     @utils.requires_ext(extension='binding', service='network')
     @rbac_rule_validation.action(service="neutron",
-                                 rule="create_port:binding:profile")
+                                 rules=["create_port",
+                                        "create_port:binding:profile"],
+                                 expected_error_codes=[403, 403])
     @decorators.idempotent_id('98fa38ab-c2ed-46a0-99f0-59f18cbd257a')
     def test_create_port_binding_profile(self):
 
@@ -111,7 +119,9 @@
         CONF.policy_feature_enabled.create_port_fixed_ips_ip_address_policy,
         '"create_port:fixed_ips:ip_address" must be available in the cloud.')
     @rbac_rule_validation.action(service="neutron",
-                                 rule="create_port:fixed_ips:ip_address")
+                                 rules=["create_port",
+                                        "create_port:fixed_ips:ip_address"],
+                                 expected_error_codes=[403, 403])
     @decorators.idempotent_id('2551e10d-006a-413c-925a-8c6f834c09ac')
     def test_create_port_fixed_ips_ip_address(self):
 
@@ -126,7 +136,9 @@
             self.create_port(**post_body)
 
     @rbac_rule_validation.action(service="neutron",
-                                 rule="create_port:mac_address")
+                                 rules=["create_port",
+                                        "create_port:mac_address"],
+                                 expected_error_codes=[403, 403])
     @decorators.idempotent_id('aee6d0be-a7f3-452f-aefc-796b4eb9c9a8')
     def test_create_port_mac_address(self):
 
@@ -137,7 +149,9 @@
             self.create_port(**post_body)
 
     @rbac_rule_validation.action(service="neutron",
-                                 rule="create_port:allowed_address_pairs")
+                                 rules=["create_port",
+                                        "create_port:allowed_address_pairs"],
+                                 expected_error_codes=[403, 403])
     @decorators.idempotent_id('b638d1f4-d903-4ca8-aa2a-6fd603c5ec3a')
     def test_create_port_allowed_address_pairs(self):
 
@@ -161,7 +175,9 @@
 
     @utils.requires_ext(extension='binding', service='network')
     @rbac_rule_validation.action(service="neutron",
-                                 rule="get_port:binding:vif_type")
+                                 rules=["get_port",
+                                        "get_port:binding:vif_type"],
+                                 expected_error_codes=[404, 403])
     @decorators.idempotent_id('125aff0b-8fed-4f8e-8410-338616594b06')
     def test_show_port_binding_vif_type(self):
 
@@ -179,7 +195,9 @@
 
     @utils.requires_ext(extension='binding', service='network')
     @rbac_rule_validation.action(service="neutron",
-                                 rule="get_port:binding:vif_details")
+                                 rules=["get_port",
+                                        "get_port:binding:vif_details"],
+                                 expected_error_codes=[404, 403])
     @decorators.idempotent_id('e42bfd77-fcce-45ee-9728-3424300f0d6f')
     def test_show_port_binding_vif_details(self):
 
@@ -197,7 +215,9 @@
 
     @utils.requires_ext(extension='binding', service='network')
     @rbac_rule_validation.action(service="neutron",
-                                 rule="get_port:binding:host_id")
+                                 rules=["get_port",
+                                        "get_port:binding:host_id"],
+                                 expected_error_codes=[404, 403])
     @decorators.idempotent_id('8e61bcdc-6f81-443c-833e-44410266551e')
     def test_show_port_binding_host_id(self):
 
@@ -218,7 +238,9 @@
 
     @utils.requires_ext(extension='binding', service='network')
     @rbac_rule_validation.action(service="neutron",
-                                 rule="get_port:binding:profile")
+                                 rules=["get_port",
+                                        "get_port:binding:profile"],
+                                 expected_error_codes=[404, 403])
     @decorators.idempotent_id('d497cea9-c4ad-42e0-acc9-8d257d6b01fc')
     def test_show_port_binding_profile(self):
 
@@ -239,7 +261,8 @@
                 attribute='binding:profile')
 
     @rbac_rule_validation.action(service="neutron",
-                                 rule="update_port")
+                                 rules=["get_port", "update_port"],
+                                 expected_error_codes=[404, 403])
     @decorators.idempotent_id('afa80981-3c59-42fd-9531-3bcb2cd03711')
     def test_update_port(self):
         with self.rbac_utils.override_role(self):
@@ -250,7 +273,9 @@
 
     @decorators.idempotent_id('08d70f59-67cb-4fb1-bd6c-a5e59dd5db2b')
     @rbac_rule_validation.action(service="neutron",
-                                 rule="update_port:device_owner")
+                                 rules=["get_port", "update_port",
+                                        "update_port:device_owner"],
+                                 expected_error_codes=[404, 403, 403])
     def test_update_port_device_owner(self):
         original_device_owner = self.port['device_owner']
 
@@ -261,7 +286,9 @@
                         device_owner=original_device_owner)
 
     @rbac_rule_validation.action(service="neutron",
-                                 rule="update_port:mac_address")
+                                 rules=["get_port", "update_port",
+                                        "update_port:mac_address"],
+                                 expected_error_codes=[404, 403, 403])
     @decorators.idempotent_id('507140c8-7b14-4d63-b627-2103691d887e')
     def test_update_port_mac_address(self):
         original_mac_address = self.port['mac_address']
@@ -276,7 +303,9 @@
         CONF.policy_feature_enabled.update_port_fixed_ips_ip_address_policy,
         '"update_port:fixed_ips:ip_address" must be available in the cloud.')
     @rbac_rule_validation.action(service="neutron",
-                                 rule="update_port:fixed_ips:ip_address")
+                                 rules=["get_port", "update_port",
+                                        "update_port:fixed_ips:ip_address"],
+                                 expected_error_codes=[404, 403, 403])
     @decorators.idempotent_id('c091c825-532b-4c6f-a14f-affd3259c1c3')
     def test_update_port_fixed_ips_ip_address(self):
 
@@ -291,15 +320,20 @@
             self.ports_client.update_port(port['id'], fixed_ips=fixed_ips)
 
     @rbac_rule_validation.action(service="neutron",
-                                 rule="update_port:port_security_enabled")
+                                 rules=["get_port", "update_port",
+                                        "update_port:port_security_enabled"],
+                                 expected_error_codes=[404, 403, 403])
     @decorators.idempotent_id('795541af-6652-4e35-9581-fd58224f7545')
     def test_update_port_security_enabled(self):
         with self.rbac_utils.override_role(self):
-            self.ports_client.update_port(self.port['id'], security_groups=[])
+            self.ports_client.update_port(self.port['id'],
+                                          port_security_enabled=True)
 
     @utils.requires_ext(extension='binding', service='network')
     @rbac_rule_validation.action(service="neutron",
-                                 rule="update_port:binding:host_id")
+                                 rules=["get_port", "update_port",
+                                        "update_port:binding:host_id"],
+                                 expected_error_codes=[404, 403, 403])
     @decorators.idempotent_id('24206a72-0d90-4712-918c-5c9a1ebef64d')
     def test_update_port_binding_host_id(self):
 
@@ -315,7 +349,9 @@
 
     @utils.requires_ext(extension='binding', service='network')
     @rbac_rule_validation.action(service="neutron",
-                                 rule="update_port:binding:profile")
+                                 rules=["get_port", "update_port",
+                                        "update_port:binding:profile"],
+                                 expected_error_codes=[404, 403, 403])
     @decorators.idempotent_id('990ea8d1-9257-4f71-a3bf-d6d0914625c5')
     def test_update_port_binding_profile(self):
 
@@ -333,7 +369,9 @@
             self.ports_client.update_port(**updated_body)
 
     @rbac_rule_validation.action(service="neutron",
-                                 rule="update_port:allowed_address_pairs")
+                                 rules=["get_port", "update_port",
+                                        "update_port:allowed_address_pairs"],
+                                 expected_error_codes=[404, 403, 403])
     @decorators.idempotent_id('729c2151-bb49-4f4f-9d58-3ed8819b7582')
     def test_update_port_allowed_address_pairs(self):
 
@@ -349,8 +387,8 @@
                                           allowed_address_pairs=address_pairs)
 
     @rbac_rule_validation.action(service="neutron",
-                                 rule="delete_port",
-                                 expected_error_code=404)
+                                 rules=["get_port", "delete_port"],
+                                 expected_error_codes=[404, 403])
     @decorators.idempotent_id('1cf8e582-bc09-46cb-b32a-82bf991ad56f')
     def test_delete_port(self):
 
diff --git a/patrole_tempest_plugin/tests/api/network/test_routers_rbac.py b/patrole_tempest_plugin/tests/api/network/test_routers_rbac.py
index 812b0c1..a3d973d 100644
--- a/patrole_tempest_plugin/tests/api/network/test_routers_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_routers_rbac.py
@@ -71,7 +71,9 @@
     @decorators.idempotent_id('6139eb97-95c0-40d8-a109-99de11ab2e5e')
     @utils.requires_ext(extension='l3-ha', service='network')
     @rbac_rule_validation.action(service="neutron",
-                                 rule="create_router:ha")
+                                 rules=["create_router",
+                                        "create_router:ha"],
+                                 expected_error_codes=[403, 403])
     def test_create_high_availability_router(self):
         """Create high-availability router
 
@@ -85,7 +87,9 @@
     @decorators.idempotent_id('c6254ca6-2728-412d-803d-d4aa3935e56d')
     @utils.requires_ext(extension='dvr', service='network')
     @rbac_rule_validation.action(service="neutron",
-                                 rule="create_router:distributed")
+                                 rules=["create_router",
+                                        "create_router:distributed"],
+                                 expected_error_codes=[403, 403])
     def test_create_distributed_router(self):
         """Create distributed router
 
@@ -99,7 +103,9 @@
     @utils.requires_ext(extension='ext-gw-mode', service='network')
     @rbac_rule_validation.action(
         service="neutron",
-        rule="create_router:external_gateway_info:enable_snat")
+        rules=["create_router",
+               "create_router:external_gateway_info:enable_snat"],
+        expected_error_codes=[403, 403])
     @decorators.idempotent_id('3c5acd49-0ec7-4109-ab51-640557b48ebc')
     def test_create_router_enable_snat(self):
         """Create Router Snat
@@ -119,7 +125,9 @@
 
     @rbac_rule_validation.action(
         service="neutron",
-        rule="create_router:external_gateway_info:external_fixed_ips")
+        rules=["create_router",
+               "create_router:external_gateway_info:external_fixed_ips"],
+        expected_error_codes=[403, 403])
     @decorators.idempotent_id('d0354369-a040-4349-b869-645c8aed13cd')
     def test_create_router_external_fixed_ips(self):
         """Create Router Fixed IPs
@@ -158,7 +166,9 @@
     @decorators.idempotent_id('3ed26ea2-b419-410c-b4b5-576c1edafa06')
     @utils.requires_ext(extension='dvr', service='network')
     @rbac_rule_validation.action(service="neutron",
-                                 rule="get_router:distributed")
+                                 rules=["get_router",
+                                        "get_router:distributed"],
+                                 expected_error_codes=[404, 403])
     def test_show_distributed_router(self):
         """Get distributed router
 
@@ -179,7 +189,8 @@
     @decorators.idempotent_id('defc502c-4159-4824-b4d9-3cdcc39015b2')
     @utils.requires_ext(extension='l3-ha', service='network')
     @rbac_rule_validation.action(service="neutron",
-                                 rule="get_router:ha")
+                                 rules=["get_router", "get_router:ha"],
+                                 expected_error_codes=[404, 403])
     def test_show_high_availability_router(self):
         """GET high-availability router
 
@@ -197,8 +208,9 @@
             raise rbac_exceptions.RbacMalformedResponse(
                 attribute='ha')
 
-    @rbac_rule_validation.action(
-        service="neutron", rule="update_router")
+    @rbac_rule_validation.action(service="neutron",
+                                 rules=["get_router", "update_router"],
+                                 expected_error_codes=[404, 403])
     @decorators.idempotent_id('3d182f4e-0023-4218-9aa0-ea2b0ae0bd7a')
     def test_update_router(self):
         """Update Router
@@ -210,8 +222,10 @@
         with self.rbac_utils.override_role(self):
             self.routers_client.update_router(self.router['id'], name=new_name)
 
-    @rbac_rule_validation.action(
-        service="neutron", rule="update_router:external_gateway_info")
+    @rbac_rule_validation.action(service="neutron",
+                                 rules=["get_router", "update_router",
+                                        "update_router:external_gateway_info"],
+                                 expected_error_codes=[404, 403, 403])
     @decorators.idempotent_id('5a6ae104-a9c3-4b56-8622-e1a0a0194474')
     def test_update_router_external_gateway_info(self):
         """Update Router External Gateway Info
@@ -225,7 +239,10 @@
 
     @rbac_rule_validation.action(
         service="neutron",
-        rule="update_router:external_gateway_info:network_id")
+        rules=["get_router", "update_router",
+               "update_router:external_gateway_info",
+               "update_router:external_gateway_info:network_id"],
+        expected_error_codes=[404, 403, 403, 403])
     @decorators.idempotent_id('f1fc5a23-e3d8-44f0-b7bc-47006ad9d3d4')
     def test_update_router_external_gateway_info_network_id(self):
         """Update Router External Gateway Info Network Id
@@ -245,7 +262,10 @@
     @utils.requires_ext(extension='ext-gw-mode', service='network')
     @rbac_rule_validation.action(
         service="neutron",
-        rule="update_router:external_gateway_info:enable_snat")
+        rules=["get_router", "update_router",
+               "update_router:external_gateway_info",
+               "update_router:external_gateway_info:enable_snat"],
+        expected_error_codes=[404, 403, 403, 403])
     @decorators.idempotent_id('515a2954-3d79-4695-aeb9-d1c222765840')
     def test_update_router_enable_snat(self):
         """Update Router External Gateway Info Enable Snat
@@ -265,7 +285,10 @@
 
     @rbac_rule_validation.action(
         service="neutron",
-        rule="update_router:external_gateway_info:external_fixed_ips")
+        rules=["get_router", "update_router",
+               "update_router:external_gateway_info",
+               "update_router:external_gateway_info:external_fixed_ips"],
+        expected_error_codes=[404, 403, 403, 403])
     @decorators.idempotent_id('f429e5ee-8f0a-4667-963e-72dd95d5adee')
     def test_update_router_external_fixed_ips(self):
         """Update Router External Gateway Info External Fixed Ips
@@ -291,7 +314,9 @@
     @decorators.idempotent_id('ddc20731-dea1-4321-9abf-8772bf0b5977')
     @utils.requires_ext(extension='l3-ha', service='network')
     @rbac_rule_validation.action(service="neutron",
-                                 rule="update_router:ha")
+                                 rules=["get_router", "update_router",
+                                        "update_router:ha"],
+                                 expected_error_codes=[404, 403, 403])
     def test_update_high_availability_router(self):
         """Update high-availability router
 
@@ -305,7 +330,9 @@
     @decorators.idempotent_id('e1932c19-8f73-41cd-b5d2-84c7ae5d530c')
     @utils.requires_ext(extension='dvr', service='network')
     @rbac_rule_validation.action(service="neutron",
-                                 rule="update_router:distributed")
+                                 rules=["get_router", "update_router",
+                                        "update_router:distributed"],
+                                 expected_error_codes=[404, 403, 403])
     def test_update_distributed_router(self):
         """Update distributed router
 
@@ -318,7 +345,8 @@
                         distributed=False)
 
     @rbac_rule_validation.action(service="neutron",
-                                 rule="delete_router")
+                                 rules=["get_router", "delete_router"],
+                                 expected_error_codes=[404, 403])
     @decorators.idempotent_id('c0634dd5-0467-48f7-a4ae-1014d8edb2a7')
     def test_delete_router(self):
         """Delete Router
@@ -352,7 +380,9 @@
             subnet_id=subnet['id'])
 
     @rbac_rule_validation.action(service="neutron",
-                                 rule="remove_router_interface")
+                                 rules=["get_router",
+                                        "remove_router_interface"],
+                                 expected_error_codes=[404, 403])
     @decorators.idempotent_id('ff2593a4-2bff-4c27-97d3-dd3702b27dfb')
     def test_remove_router_interface(self):
         """Remove Router Interface
diff --git a/patrole_tempest_plugin/tests/api/network/test_security_groups_rbac.py b/patrole_tempest_plugin/tests/api/network/test_security_groups_rbac.py
index 8889e75..1cf841d 100644
--- a/patrole_tempest_plugin/tests/api/network/test_security_groups_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_security_groups_rbac.py
@@ -81,15 +81,16 @@
                                  rule="get_security_group",
                                  expected_error_code=404)
     @decorators.idempotent_id('56335e77-aef2-4b54-86c7-7f772034b585')
-    def test_show_security_groups(self):
+    def test_show_security_group(self):
 
         with self.rbac_utils.override_role(self):
             self.security_groups_client.show_security_group(
                 self.secgroup['id'])
 
     @rbac_rule_validation.action(service="neutron",
-                                 rule="delete_security_group",
-                                 expected_error_code=404)
+                                 rules=["get_security_group",
+                                        "delete_security_group"],
+                                 expected_error_codes=[404, 403])
     @decorators.idempotent_id('0b1330fd-dd28-40f3-ad73-966052e4b3de')
     def test_delete_security_group(self):
 
@@ -100,8 +101,9 @@
             self.security_groups_client.delete_security_group(secgroup_id)
 
     @rbac_rule_validation.action(service="neutron",
-                                 rule="update_security_group",
-                                 expected_error_code=404)
+                                 rules=["get_security_group",
+                                        "update_security_group"],
+                                 expected_error_codes=[404, 403])
     @decorators.idempotent_id('56c5e4dc-f8aa-11e6-bc64-92361f002671')
     def test_update_security_group(self):
 
@@ -135,8 +137,9 @@
             self._create_security_group_rule()
 
     @rbac_rule_validation.action(service="neutron",
-                                 rule="delete_security_group_rule",
-                                 expected_error_code=404)
+                                 rules=["get_security_group_rule",
+                                        "delete_security_group_rule"],
+                                 expected_error_codes=[404, 403])
     @decorators.idempotent_id('2262539e-b7d9-438c-acf9-a5ce0613be28')
     def test_delete_security_group_rule(self):
 
diff --git a/patrole_tempest_plugin/tests/api/network/test_subnetpools_rbac.py b/patrole_tempest_plugin/tests/api/network/test_subnetpools_rbac.py
index fe14c92..124b59a 100644
--- a/patrole_tempest_plugin/tests/api/network/test_subnetpools_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_subnetpools_rbac.py
@@ -64,7 +64,9 @@
             self._create_subnetpool()
 
     @rbac_rule_validation.action(service="neutron",
-                                 rule="create_subnetpool:shared")
+                                 rules=["create_subnetpool",
+                                        "create_subnetpool:shared"],
+                                 expected_error_codes=[403, 403])
     @decorators.idempotent_id('cf730989-0d47-40bc-b39a-99e7de484723')
     def test_create_subnetpool_shared(self):
         """Create subnetpool shared.
@@ -88,7 +90,9 @@
             self.subnetpools_client.show_subnetpool(subnetpool['id'])
 
     @rbac_rule_validation.action(service="neutron",
-                                 rule="update_subnetpool")
+                                 rules=["get_subnetpool",
+                                        "update_subnetpool"],
+                                 expected_error_codes=[404, 403])
     @decorators.idempotent_id('1e79cead-5081-4be2-a4f7-484c0f443b9b')
     def test_update_subnetpool(self):
         """Update subnetpool.
@@ -102,7 +106,9 @@
 
     @decorators.idempotent_id('a16f4e5c-0675-415f-b636-00af00638693')
     @rbac_rule_validation.action(service="neutron",
-                                 rule="update_subnetpool:is_default")
+                                 rules=["update_subnetpool",
+                                        "update_subnetpool:is_default"],
+                                 expected_error_codes=[403, 403])
     def test_update_subnetpool_is_default(self):
         """Update default subnetpool.
 
@@ -122,7 +128,9 @@
                 default_pool['id'], description=original_desc, is_default=True)
 
     @rbac_rule_validation.action(service="neutron",
-                                 rule="delete_subnetpool")
+                                 rules=["get_subnetpool",
+                                        "delete_subnetpool"],
+                                 expected_error_codes=[404, 403])
     @decorators.idempotent_id('50f5944e-43e5-457b-ab50-fb48a73f0d3e')
     def test_delete_subnetpool(self):
         """Delete subnetpool.
diff --git a/patrole_tempest_plugin/tests/api/network/test_subnets_rbac.py b/patrole_tempest_plugin/tests/api/network/test_subnets_rbac.py
index 12d20fa..77d4b42 100644
--- a/patrole_tempest_plugin/tests/api/network/test_subnets_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_subnets_rbac.py
@@ -50,7 +50,8 @@
 
     @decorators.idempotent_id('c02618e7-bb20-4abd-83c8-6eec2af08752')
     @rbac_rule_validation.action(service="neutron",
-                                 rule="get_subnet")
+                                 rule="get_subnet",
+                                 expected_error_code=404)
     def test_show_subnet(self):
         """Show subnet.
 
@@ -76,7 +77,8 @@
 
     @decorators.idempotent_id('f36cd821-dd22-4bd0-b43d-110fc4b553eb')
     @rbac_rule_validation.action(service="neutron",
-                                 rule="update_subnet")
+                                 rules=["get_subnet", "update_subnet"],
+                                 expected_error_codes=[404, 403])
     def test_update_subnet(self):
         """Update subnet.
 
@@ -90,7 +92,8 @@
 
     @decorators.idempotent_id('bcfc7153-bbd1-43a4-a908-b3e1b0cde0dc')
     @rbac_rule_validation.action(service="neutron",
-                                 rule="delete_subnet")
+                                 rules=["get_subnet", "delete_subnet"],
+                                 expected_error_codes=[404, 403])
     def test_delete_subnet(self):
         """Delete subnet.
 
diff --git a/patrole_tempest_plugin/tests/unit/test_rbac_rule_validation.py b/patrole_tempest_plugin/tests/unit/test_rbac_rule_validation.py
index 2ae860c..2e275dc 100644
--- a/patrole_tempest_plugin/tests/unit/test_rbac_rule_validation.py
+++ b/patrole_tempest_plugin/tests/unit/test_rbac_rule_validation.py
@@ -64,7 +64,7 @@
         """
         mock_authority.PolicyAuthority.return_value.allowed.return_value = True
 
-        @rbac_rv.action(mock.sentinel.service, mock.sentinel.action)
+        @rbac_rv.action(mock.sentinel.service, rules=[mock.sentinel.action])
         def test_policy(*args):
             pass
 
@@ -83,7 +83,7 @@
         mock_authority.PolicyAuthority.return_value.allowed.return_value =\
             False
 
-        @rbac_rv.action(mock.sentinel.service, mock.sentinel.action)
+        @rbac_rv.action(mock.sentinel.service, rules=[mock.sentinel.action])
         def test_policy(*args):
             raise exceptions.Forbidden()
 
@@ -95,22 +95,24 @@
     @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
     def test_rule_validation_forbidden_negative(self, mock_authority,
                                                 mock_log):
-        """Test Forbidden error is thrown and have permission fails.
+        """Test RbacUnderPermissionException error is thrown and have
+        permission fails.
 
         Negative test case: if Forbidden is thrown and the user should be
-        allowed to perform the action, then the Forbidden exception should be
-        raised.
+        allowed to perform the action, then the RbacUnderPermissionException
+        exception should be raised.
         """
         mock_authority.PolicyAuthority.return_value.allowed.return_value = True
 
-        @rbac_rv.action(mock.sentinel.service, mock.sentinel.action)
+        @rbac_rv.action(mock.sentinel.service, rules=[mock.sentinel.action])
         def test_policy(*args):
             raise exceptions.Forbidden()
 
         test_re = ("Role Member was not allowed to perform the following "
                    "actions: \[%s\].*" % (mock.sentinel.action))
-        self.assertRaisesRegex(exceptions.Forbidden, test_re, test_policy,
-                               self.mock_test_args)
+        self.assertRaisesRegex(
+            rbac_exceptions.RbacUnderPermissionException, test_re, test_policy,
+            self.mock_test_args)
         self.assertRegex(mock_log.error.mock_calls[0][1][0], test_re)
 
     @mock.patch.object(rbac_rv, 'LOG', autospec=True)
@@ -125,7 +127,7 @@
         mock_authority.PolicyAuthority.return_value.allowed.return_value =\
             False
 
-        @rbac_rv.action(mock.sentinel.service, mock.sentinel.action)
+        @rbac_rv.action(mock.sentinel.service, rules=[mock.sentinel.action])
         def test_policy(*args):
             raise rbac_exceptions.RbacMalformedResponse()
 
@@ -143,14 +145,15 @@
         """
         mock_authority.PolicyAuthority.return_value.allowed.return_value = True
 
-        @rbac_rv.action(mock.sentinel.service, mock.sentinel.action)
+        @rbac_rv.action(mock.sentinel.service, rules=[mock.sentinel.action])
         def test_policy(*args):
             raise rbac_exceptions.RbacMalformedResponse()
 
         test_re = ("Role Member was not allowed to perform the following "
                    "actions: \[%s\].*" % (mock.sentinel.action))
-        self.assertRaisesRegex(exceptions.Forbidden, test_re, test_policy,
-                               self.mock_test_args)
+        self.assertRaisesRegex(
+            rbac_exceptions.RbacUnderPermissionException, test_re, test_policy,
+            self.mock_test_args)
         self.assertRegex(mock_log.error.mock_calls[0][1][0], test_re)
 
     @mock.patch.object(rbac_rv, 'LOG', autospec=True)
@@ -165,7 +168,7 @@
         mock_authority.PolicyAuthority.return_value.allowed.return_value =\
             False
 
-        @rbac_rv.action(mock.sentinel.service, mock.sentinel.action)
+        @rbac_rv.action(mock.sentinel.service, rules=[mock.sentinel.action])
         def test_policy(*args):
             raise rbac_exceptions.RbacConflictingPolicies()
 
@@ -184,14 +187,15 @@
         """
         mock_authority.PolicyAuthority.return_value.allowed.return_value = True
 
-        @rbac_rv.action(mock.sentinel.service, mock.sentinel.action)
+        @rbac_rv.action(mock.sentinel.service, rules=[mock.sentinel.action])
         def test_policy(*args):
             raise rbac_exceptions.RbacConflictingPolicies()
 
         test_re = ("Role Member was not allowed to perform the following "
                    "actions: \[%s\].*" % (mock.sentinel.action))
-        self.assertRaisesRegex(exceptions.Forbidden, test_re, test_policy,
-                               self.mock_test_args)
+        self.assertRaisesRegex(
+            rbac_exceptions.RbacUnderPermissionException, test_re, test_policy,
+            self.mock_test_args)
         self.assertRegex(mock_log.error.mock_calls[0][1][0], test_re)
 
     @mock.patch.object(rbac_rv, 'LOG', autospec=True)
@@ -206,22 +210,20 @@
         2) Test have permission and 404 is expected but 403 is thrown throws
            exception.
         """
-        @rbac_rv.action(mock.sentinel.service, mock.sentinel.action,
+        @rbac_rv.action(mock.sentinel.service, rules=[mock.sentinel.action],
                         expected_error_code=404)
         def test_policy(*args):
             raise exceptions.Forbidden('Test message')
 
-        error_msg = ("An unexpected exception has occurred during test: "
-                     "test_policy. Exception was: Forbidden\nDetails: Test "
-                     "message")
+        error_re = r'Expected .* to be raised but .* was raised instead'
 
         for allowed in [True, False]:
             mock_authority.PolicyAuthority.return_value.allowed.\
                 return_value = allowed
-            self.assertRaisesRegex(exceptions.Forbidden, 'Test message',
-                                   test_policy, self.mock_test_args)
-            self.assertIn(error_msg, mock_log.error.mock_calls[0][1][0])
-            mock_log.error.reset_mock()
+            self.assertRaisesRegex(
+                rbac_exceptions.RbacExpectedWrongException, error_re,
+                test_policy, self.mock_test_args)
+            self.assertTrue(mock_log.error.called)
 
     @mock.patch.object(rbac_rv, 'LOG', autospec=True)
     @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
@@ -236,14 +238,16 @@
         In both cases, a LOG.warning is called with the "irregular message"
         that signals to user that a 404 was expected and caught.
         """
-        @rbac_rv.action(mock.sentinel.service, mock.sentinel.action,
+        policy_names = ['foo:bar']
+
+        @rbac_rv.action(mock.sentinel.service, rules=policy_names,
                         expected_error_code=404)
         def test_policy(*args):
             raise exceptions.NotFound()
 
         expected_errors = [
             ("Role Member was not allowed to perform the following "
-             "actions: \[%s\].*" % (mock.sentinel.action)),
+             "actions: \['%s'\].*" % policy_names[0]),
             None
         ]
 
@@ -254,18 +258,21 @@
             error_re = expected_errors[pos]
 
             if error_re:
-                self.assertRaisesRegex(exceptions.Forbidden, error_re,
-                                       test_policy, self.mock_test_args)
+                self.assertRaisesRegex(
+                    rbac_exceptions.RbacUnderPermissionException, error_re,
+                    test_policy, self.mock_test_args)
                 self.assertRegex(mock_log.error.mock_calls[0][1][0], error_re)
             else:
                 test_policy(self.mock_test_args)
                 mock_log.error.assert_not_called()
 
             mock_log.warning.assert_called_with(
-                "NotFound exception was caught for policy action {0}. The "
-                "service {1} throws a 404 instead of a 403, which is "
-                "irregular.".format(mock.sentinel.action,
-                                    mock.sentinel.service))
+                "NotFound exception was caught for test %s. Expected policies "
+                "which may have caused the error: %s. The service %s throws a "
+                "404 instead of a 403, which is irregular.",
+                test_policy.__name__,
+                ', '.join(policy_names),
+                mock.sentinel.service)
 
             mock_log.warning.reset_mock()
             mock_log.error.reset_mock()
@@ -274,7 +281,7 @@
     @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
     def test_rule_validation_overpermission_negative(self, mock_authority,
                                                      mock_log):
-        """Test that OverPermission is correctly handled.
+        """Test that RbacOverPermissionException is correctly handled.
 
         Tests that case where no exception is thrown but the Patrole framework
         says that the role should not be allowed to perform the policy action.
@@ -282,11 +289,11 @@
         mock_authority.PolicyAuthority.return_value.allowed.return_value =\
             False
 
-        @rbac_rv.action(mock.sentinel.service, mock.sentinel.action)
+        @rbac_rv.action(mock.sentinel.service, rules=[mock.sentinel.action])
         def test_policy_expect_forbidden(*args):
             pass
 
-        @rbac_rv.action(mock.sentinel.service, mock.sentinel.action,
+        @rbac_rv.action(mock.sentinel.service, rules=[mock.sentinel.action],
                         expected_error_code=404)
         def test_policy_expect_not_found(*args):
             pass
@@ -295,7 +302,7 @@
             test_policy_expect_forbidden, test_policy_expect_not_found):
 
             error_re = ".*OverPermission: .* \[%s\]$" % mock.sentinel.action
-            self.assertRaisesRegex(rbac_exceptions.RbacOverPermission,
+            self.assertRaisesRegex(rbac_exceptions.RbacOverPermissionException,
                                    error_re, test_policy, self.mock_test_args)
             self.assertRegex(mock_log.error.mock_calls[0][1][0], error_re)
             mock_log.error.reset_mock()
@@ -324,9 +331,10 @@
     def test_get_exception_type_404(self, _):
         """Test that getting a 404 exception type returns NotFound."""
         expected_exception = exceptions.NotFound
-        expected_irregular_msg = ("NotFound exception was caught for policy "
-                                  "action {0}. The service {1} throws a 404 "
-                                  "instead of a 403, which is irregular.")
+        expected_irregular_msg = (
+            "NotFound exception was caught for test %s. Expected policies "
+            "which may have caused the error: %s. The service %s throws a "
+            "404 instead of a 403, which is irregular.")
 
         actual_exception, actual_irregular_msg = \
             rbac_rv._get_exception_type(404)
@@ -336,7 +344,7 @@
 
     @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
     def test_get_exception_type_403(self, _):
-        """Test that getting a 404 exception type returns Forbidden."""
+        """Test that getting a 403 exception type returns Forbidden."""
         expected_exception = exceptions.Forbidden
         expected_irregular_msg = None
 
@@ -371,6 +379,12 @@
 
             mock_log.error.reset_mock()
 
+
+class RBACRuleValidationLoggingTest(BaseRBACRuleValidationTest):
+    """Test class for validating the RBAC log, dedicated to just logging
+    Patrole RBAC validation work flows.
+    """
+
     @mock.patch.object(rbac_rv, 'RBACLOG', autospec=True)
     @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
     def test_rbac_report_logging_disabled(self, mock_authority, mock_rbaclog):
@@ -382,7 +396,7 @@
 
         mock_authority.PolicyAuthority.return_value.allowed.return_value = True
 
-        @rbac_rv.action(mock.sentinel.service, mock.sentinel.action)
+        @rbac_rv.action(mock.sentinel.service, rules=[mock.sentinel.action])
         def test_policy(*args):
             pass
 
@@ -399,17 +413,19 @@
             fixtures.ConfPatcher(enable_reporting=True, group='patrole_log'))
 
         mock_authority.PolicyAuthority.return_value.allowed.return_value = True
+        policy_names = ['foo:bar', 'baz:qux']
 
-        @rbac_rv.action(mock.sentinel.service, mock.sentinel.action)
+        @rbac_rv.action(mock.sentinel.service, rules=policy_names)
         def test_policy(*args):
             pass
 
         test_policy(self.mock_test_args)
         mock_rbaclog.info.assert_called_once_with(
-            "[Service]: %s, [Test]: %s, [Rule]: %s, "
+            "[Service]: %s, [Test]: %s, [Rules]: %s, "
             "[Expected]: %s, [Actual]: %s",
-            mock.sentinel.service, 'test_policy',
-            mock.sentinel.action,
+            mock.sentinel.service,
+            'test_policy',
+            ', '.join(policy_names),
             "Allowed",
             "Allowed")
 
@@ -449,7 +465,8 @@
     def test_rule_validation_multi_policy_overpermission_failure(
             self, mock_authority, mock_log):
         """Test that when expected result is unauthorized and test passes that
-        the overall evaluation results in an OverPermission getting raised.
+        the overall evaluation results in an RbacOverPermissionException
+        getting raised.
         """
 
         rules = [
@@ -467,8 +484,9 @@
                 allowed_list)
 
             error_re = ".*OverPermission: .* \[%s\]$" % fail_on_action
-            self.assertRaisesRegex(rbac_exceptions.RbacOverPermission,
-                                   error_re, test_policy, self.mock_test_args)
+            self.assertRaisesRegex(
+                rbac_exceptions.RbacOverPermissionException, error_re,
+                test_policy, self.mock_test_args)
             mock_log.debug.assert_any_call(
                 "%s: Expecting %d to be raised for policy name: %s",
                 'test_policy', 403, fail_on_action)
@@ -518,7 +536,7 @@
             self, mock_authority, mock_log):
         """Test that when the expected result is authorized and the test
         fails (with a Forbidden error code) that the overall evaluation
-        results a Forbidden getting raised.
+        results in a RbacUnderPermissionException getting raised.
         """
 
         # NOTE: Avoid mock.sentinel here due to weird sorting with them.
@@ -536,8 +554,9 @@
                     "actions: %s. Expected allowed actions: %s. Expected "
                     "disallowed actions: []." % (rules, rules)).replace(
                         '[', '\[').replace(']', '\]')
-        self.assertRaisesRegex(exceptions.Forbidden, error_re, test_policy,
-                               self.mock_test_args)
+        self.assertRaisesRegex(
+            rbac_exceptions.RbacUnderPermissionException, error_re,
+            test_policy, self.mock_test_args)
         self.assertRegex(mock_log.error.mock_calls[0][1][0], error_re)
         self._assert_policy_authority_called_with(rules, mock_authority)
 
@@ -545,7 +564,7 @@
     @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
     def test_rule_validation_multi_actions_forbidden(
             self, mock_authority, mock_log):
-        """Test that when the expected result is forbidden because
+        """Test that when the expected result is Forbidden because
         two of the actions fail and the first action specifies 403,
         verify that the overall evaluation results in success.
         """
@@ -583,8 +602,8 @@
         """
 
         rules = [
-            mock.sentinel.action1, mock.sentinel.action2,
-            mock.sentinel.action3, mock.sentinel.action4
+            'mock.sentinel.action1', 'mock.sentinel.action2',
+            'mock.sentinel.action3', 'mock.sentinel.action4'
         ]
         exp_ecodes = [403, 404, 403, 403]
 
@@ -603,8 +622,8 @@
             mock_log.error.assert_not_called()
             self._assert_policy_authority_called_with(rules, mock_authority)
 
-        _do_test([True, False, False, True], mock.sentinel.action2)
-        _do_test([True, False, True, False], mock.sentinel.action2)
+        _do_test([True, False, False, True], 'mock.sentinel.action2')
+        _do_test([True, False, True, False], 'mock.sentinel.action2')
 
     @mock.patch.object(rbac_rv, 'LOG', autospec=True)
     def test_prepare_multi_policy_allowed_usages(self, mock_log):
diff --git a/playbooks/patrole-multinode-admin/post.yaml b/playbooks/patrole-multinode-admin/post.yaml
deleted file mode 100644
index dac8753..0000000
--- a/playbooks/patrole-multinode-admin/post.yaml
+++ /dev/null
@@ -1,80 +0,0 @@
-- hosts: primary
-  tasks:
-
-    - name: Copy files from {{ ansible_user_dir }}/workspace/ on node
-      synchronize:
-        src: '{{ ansible_user_dir }}/workspace/'
-        dest: '{{ zuul.executor.log_root }}'
-        mode: pull
-        copy_links: true
-        verify_host: true
-        rsync_opts:
-          - --include=**/*nose_results.html
-          - --include=*/
-          - --exclude=*
-          - --prune-empty-dirs
-
-    - name: Copy files from {{ ansible_user_dir }}/workspace/ on node
-      synchronize:
-        src: '{{ ansible_user_dir }}/workspace/'
-        dest: '{{ zuul.executor.log_root }}'
-        mode: pull
-        copy_links: true
-        verify_host: true
-        rsync_opts:
-          - --include=**/*testr_results.html.gz
-          - --include=*/
-          - --exclude=*
-          - --prune-empty-dirs
-
-    - name: Copy files from {{ ansible_user_dir }}/workspace/ on node
-      synchronize:
-        src: '{{ ansible_user_dir }}/workspace/'
-        dest: '{{ zuul.executor.log_root }}'
-        mode: pull
-        copy_links: true
-        verify_host: true
-        rsync_opts:
-          - --include=/.testrepository/tmp*
-          - --include=*/
-          - --exclude=*
-          - --prune-empty-dirs
-
-    - name: Copy files from {{ ansible_user_dir }}/workspace/ on node
-      synchronize:
-        src: '{{ ansible_user_dir }}/workspace/'
-        dest: '{{ zuul.executor.log_root }}'
-        mode: pull
-        copy_links: true
-        verify_host: true
-        rsync_opts:
-          - --include=**/*testrepository.subunit.gz
-          - --include=*/
-          - --exclude=*
-          - --prune-empty-dirs
-
-    - name: Copy files from {{ ansible_user_dir }}/workspace/ on node
-      synchronize:
-        src: '{{ ansible_user_dir }}/workspace/'
-        dest: '{{ zuul.executor.log_root }}/tox'
-        mode: pull
-        copy_links: true
-        verify_host: true
-        rsync_opts:
-          - --include=/.tox/*/log/*
-          - --include=*/
-          - --exclude=*
-          - --prune-empty-dirs
-
-    - name: Copy files from {{ ansible_user_dir }}/workspace/ on node
-      synchronize:
-        src: '{{ ansible_user_dir }}/workspace/'
-        dest: '{{ zuul.executor.log_root }}'
-        mode: pull
-        copy_links: true
-        verify_host: true
-        rsync_opts:
-          - --include=/logs/**
-          - --include=*/
-          - --exclude=*
-          - --prune-empty-dirs
diff --git a/playbooks/patrole-multinode-admin/run.yaml b/playbooks/patrole-multinode-admin/run.yaml
deleted file mode 100644
index bece4e2..0000000
--- a/playbooks/patrole-multinode-admin/run.yaml
+++ /dev/null
@@ -1,63 +0,0 @@
-- hosts: primary
-  name: Autoconverted job legacy-tempest-dsvm-patrole-multinode-admin from old job
-    gate-tempest-dsvm-patrole-multinode-admin-ubuntu-xenial-nv
-  tasks:
-
-    - name: Ensure legacy workspace directory
-      file:
-        path: '{{ ansible_user_dir }}/workspace'
-        state: directory
-
-    - shell:
-        cmd: |
-          set -e
-          set -x
-          cat > clonemap.yaml << EOF
-          clonemap:
-            - name: openstack-infra/devstack-gate
-              dest: devstack-gate
-          EOF
-          /usr/zuul-env/bin/zuul-cloner -m clonemap.yaml --cache-dir /opt/git \
-              git://git.openstack.org \
-              openstack-infra/devstack-gate
-        executable: /bin/bash
-        chdir: '{{ ansible_user_dir }}/workspace'
-      environment: '{{ zuul | zuul_legacy_vars }}'
-
-    - shell:
-        cmd: |
-          set -e
-          set -x
-          cat << 'EOF' >>"/tmp/dg-local.conf"
-          [[local|localrc]]
-          enable_plugin patrole git://git.openstack.org/openstack/patrole
-          TEMPEST_PLUGINS='/opt/stack/new/patrole'
-          # Needed by Patrole devstack plugin
-          RBAC_TEST_ROLE=admin
-          EOF
-        executable: /bin/bash
-        chdir: '{{ ansible_user_dir }}/workspace'
-      environment: '{{ zuul | zuul_legacy_vars }}'
-
-    - shell:
-        cmd: |
-          set -e
-          set -x
-          export PYTHONUNBUFFERED=true
-          # Ensure that tempest set up is executed, but do not automatically
-          # execute tempest tests; they are executed in post_test_hook.
-          export DEVSTACK_GATE_TEMPEST=1
-          export DEVSTACK_GATE_NEUTRON=1
-          export DEVSTACK_GATE_TOPOLOGY="multinode"
-          export DEVSTACK_GATE_TEMPEST_REGEX='(?=.*\[.*\bslow\b.*\])(^patrole_tempest_plugin\.tests\.api)'
-          export DEVSTACK_GATE_TEMPEST_ALL_PLUGINS=1
-          export PROJECTS="openstack/patrole $PROJECTS"
-          export BRANCH_OVERRIDE=default
-          if [ "$BRANCH_OVERRIDE" != "default" ] ; then
-              export OVERRIDE_ZUUL_BRANCH=$BRANCH_OVERRIDE
-          fi
-          cp devstack-gate/devstack-vm-gate-wrap.sh ./safe-devstack-vm-gate-wrap.sh
-          ./safe-devstack-vm-gate-wrap.sh
-        executable: /bin/bash
-        chdir: '{{ ansible_user_dir }}/workspace'
-      environment: '{{ zuul | zuul_legacy_vars }}'
diff --git a/playbooks/patrole-multinode-member/post.yaml b/playbooks/patrole-multinode-member/post.yaml
deleted file mode 100644
index dac8753..0000000
--- a/playbooks/patrole-multinode-member/post.yaml
+++ /dev/null
@@ -1,80 +0,0 @@
-- hosts: primary
-  tasks:
-
-    - name: Copy files from {{ ansible_user_dir }}/workspace/ on node
-      synchronize:
-        src: '{{ ansible_user_dir }}/workspace/'
-        dest: '{{ zuul.executor.log_root }}'
-        mode: pull
-        copy_links: true
-        verify_host: true
-        rsync_opts:
-          - --include=**/*nose_results.html
-          - --include=*/
-          - --exclude=*
-          - --prune-empty-dirs
-
-    - name: Copy files from {{ ansible_user_dir }}/workspace/ on node
-      synchronize:
-        src: '{{ ansible_user_dir }}/workspace/'
-        dest: '{{ zuul.executor.log_root }}'
-        mode: pull
-        copy_links: true
-        verify_host: true
-        rsync_opts:
-          - --include=**/*testr_results.html.gz
-          - --include=*/
-          - --exclude=*
-          - --prune-empty-dirs
-
-    - name: Copy files from {{ ansible_user_dir }}/workspace/ on node
-      synchronize:
-        src: '{{ ansible_user_dir }}/workspace/'
-        dest: '{{ zuul.executor.log_root }}'
-        mode: pull
-        copy_links: true
-        verify_host: true
-        rsync_opts:
-          - --include=/.testrepository/tmp*
-          - --include=*/
-          - --exclude=*
-          - --prune-empty-dirs
-
-    - name: Copy files from {{ ansible_user_dir }}/workspace/ on node
-      synchronize:
-        src: '{{ ansible_user_dir }}/workspace/'
-        dest: '{{ zuul.executor.log_root }}'
-        mode: pull
-        copy_links: true
-        verify_host: true
-        rsync_opts:
-          - --include=**/*testrepository.subunit.gz
-          - --include=*/
-          - --exclude=*
-          - --prune-empty-dirs
-
-    - name: Copy files from {{ ansible_user_dir }}/workspace/ on node
-      synchronize:
-        src: '{{ ansible_user_dir }}/workspace/'
-        dest: '{{ zuul.executor.log_root }}/tox'
-        mode: pull
-        copy_links: true
-        verify_host: true
-        rsync_opts:
-          - --include=/.tox/*/log/*
-          - --include=*/
-          - --exclude=*
-          - --prune-empty-dirs
-
-    - name: Copy files from {{ ansible_user_dir }}/workspace/ on node
-      synchronize:
-        src: '{{ ansible_user_dir }}/workspace/'
-        dest: '{{ zuul.executor.log_root }}'
-        mode: pull
-        copy_links: true
-        verify_host: true
-        rsync_opts:
-          - --include=/logs/**
-          - --include=*/
-          - --exclude=*
-          - --prune-empty-dirs
diff --git a/playbooks/patrole-multinode-member/run.yaml b/playbooks/patrole-multinode-member/run.yaml
deleted file mode 100644
index 4c7b70f..0000000
--- a/playbooks/patrole-multinode-member/run.yaml
+++ /dev/null
@@ -1,63 +0,0 @@
-- hosts: primary
-  name: Autoconverted job legacy-tempest-dsvm-patrole-multinode-member from old job
-    gate-tempest-dsvm-patrole-multinode-member-ubuntu-xenial-nv
-  tasks:
-
-    - name: Ensure legacy workspace directory
-      file:
-        path: '{{ ansible_user_dir }}/workspace'
-        state: directory
-
-    - shell:
-        cmd: |
-          set -e
-          set -x
-          cat > clonemap.yaml << EOF
-          clonemap:
-            - name: openstack-infra/devstack-gate
-              dest: devstack-gate
-          EOF
-          /usr/zuul-env/bin/zuul-cloner -m clonemap.yaml --cache-dir /opt/git \
-              git://git.openstack.org \
-              openstack-infra/devstack-gate
-        executable: /bin/bash
-        chdir: '{{ ansible_user_dir }}/workspace'
-      environment: '{{ zuul | zuul_legacy_vars }}'
-
-    - shell:
-        cmd: |
-          set -e
-          set -x
-          cat << 'EOF' >>"/tmp/dg-local.conf"
-          [[local|localrc]]
-          enable_plugin patrole git://git.openstack.org/openstack/patrole
-          TEMPEST_PLUGINS='/opt/stack/new/patrole'
-          # Needed by Patrole devstack plugin
-          RBAC_TEST_ROLE=member
-          EOF
-        executable: /bin/bash
-        chdir: '{{ ansible_user_dir }}/workspace'
-      environment: '{{ zuul | zuul_legacy_vars }}'
-
-    - shell:
-        cmd: |
-          set -e
-          set -x
-          export PYTHONUNBUFFERED=true
-          # Ensure that tempest set up is executed, but do not automatically
-          # execute tempest tests; they are executed in post_test_hook.
-          export DEVSTACK_GATE_TEMPEST=1
-          export DEVSTACK_GATE_NEUTRON=1
-          export DEVSTACK_GATE_TOPOLOGY="multinode"
-          export DEVSTACK_GATE_TEMPEST_REGEX='(?=.*\[.*\bslow\b.*\])(^patrole_tempest_plugin\.tests\.api)'
-          export DEVSTACK_GATE_TEMPEST_ALL_PLUGINS=1
-          export PROJECTS="openstack/patrole $PROJECTS"
-          export BRANCH_OVERRIDE=default
-          if [ "$BRANCH_OVERRIDE" != "default" ] ; then
-              export OVERRIDE_ZUUL_BRANCH=$BRANCH_OVERRIDE
-          fi
-          cp devstack-gate/devstack-vm-gate-wrap.sh ./safe-devstack-vm-gate-wrap.sh
-          ./safe-devstack-vm-gate-wrap.sh
-        executable: /bin/bash
-        chdir: '{{ ansible_user_dir }}/workspace'
-      environment: '{{ zuul | zuul_legacy_vars }}'