Add skip_if_not_implemented to the service client

This patch adds a decorator that wraps the service client methods adding
the ability to raise a skipException if a call to the API returns a
NotImplementedError or UnsupportedOptionError exception.

This is useful for running the tests against provider drivers that do
not support all of the Octavia API features.

Change-Id: I17d4be65130fadf97d6170d22fb07c72672b7573
diff --git a/octavia_tempest_plugin/common/decorators.py b/octavia_tempest_plugin/common/decorators.py
new file mode 100644
index 0000000..b484497
--- /dev/null
+++ b/octavia_tempest_plugin/common/decorators.py
@@ -0,0 +1,54 @@
+# Copyright 2020 Red Hat, Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+from functools import wraps
+
+import testtools
+
+from oslo_utils import excutils
+from tempest import config
+from tempest.lib import exceptions
+
+CONF = config.CONF
+
+
+def skip_if_not_implemented(f):
+    """A decorator to raise a skip exception for not implemented features.
+
+    This decorator raises a skipException if the method raises a
+    NotImplemented exception. If "skip_if_not_implemented=False"
+    argument was passed to the method, the NotImplemented exception will
+    be raised.
+
+    @param skip_if_not_implemented: If True (default), raise skipException.
+    """
+    @wraps(f)
+    def wrapper(*func_args, **func_kwargs):
+
+        skip = func_kwargs.pop('skip_if_not_implemented', True)
+        if CONF.loadbalancer_feature_enabled.not_implemented_is_error:
+            skip = False
+        try:
+            return f(*func_args, **func_kwargs)
+        except exceptions.NotImplemented as e:
+            with excutils.save_and_reraise_exception():
+                if not skip:
+                    raise
+                message = ("The configured provider driver '{driver}' "
+                           "does not support a feature required for this "
+                           "test.".format(
+                               driver=CONF.load_balancer.provider))
+                if hasattr(e, 'resp_body'):
+                    message = e.resp_body.get('faultstring', message)
+                raise testtools.TestCase.skipException(message)
+    return wrapper
diff --git a/octavia_tempest_plugin/config.py b/octavia_tempest_plugin/config.py
index fc04c33..8dd0403 100644
--- a/octavia_tempest_plugin/config.py
+++ b/octavia_tempest_plugin/config.py
@@ -209,6 +209,12 @@
 lb_feature_enabled_group = cfg.OptGroup(name='loadbalancer-feature-enabled',
                                         title='Enabled/Disabled LB features')
 LBFeatureEnabledGroup = [
+    cfg.BoolOpt('not_implemented_is_error',
+                default=True,
+                help="When True, not-implemented responses from the API are "
+                     "considered an error and test failure. This should be "
+                     "used when a driver should support all of the Octavia "
+                     "API features, such as the reference driver."),
     cfg.BoolOpt('health_monitor_enabled',
                 default=True,
                 help="Whether Health Monitor is available with provider "
diff --git a/octavia_tempest_plugin/services/load_balancer/v2/amphora_client.py b/octavia_tempest_plugin/services/load_balancer/v2/amphora_client.py
index 4094515..aed93b4 100644
--- a/octavia_tempest_plugin/services/load_balancer/v2/amphora_client.py
+++ b/octavia_tempest_plugin/services/load_balancer/v2/amphora_client.py
@@ -16,6 +16,7 @@
 
 from tempest import config
 
+from octavia_tempest_plugin.common.decorators import skip_if_not_implemented
 from octavia_tempest_plugin.services.load_balancer.v2 import base_client
 
 CONF = config.CONF
@@ -28,6 +29,7 @@
     stats_root_tag = 'amphora_stats'
     base_uri = '/v2.0/octavia/{object}'
 
+    @skip_if_not_implemented
     def show_amphora(self, amphora_id, query_params=None,
                      return_object_only=True):
         """Get amphora details.
@@ -69,6 +71,7 @@
                                  query_params=query_params,
                                  return_object_only=return_object_only)
 
+    @skip_if_not_implemented
     def list_amphorae(self, query_params=None, return_object_only=True):
         """Get a list of amphora objects.
 
@@ -107,6 +110,7 @@
         return self._list_objects(query_params=query_params,
                                   return_object_only=return_object_only)
 
+    @skip_if_not_implemented
     def get_amphora_stats(self, amphora_id, query_params=None,
                           return_object_only=True):
         """Get amphora statistics.
@@ -158,6 +162,7 @@
         else:
             return jsonutils.loads(body.decode('utf-8'))
 
+    @skip_if_not_implemented
     def update_amphora_config(self, amphora_id):
         """Update the amphora agent configuration.
 
@@ -193,6 +198,7 @@
         response, body = self.put(uri, '')
         self.expected_success(202, response.status)
 
+    @skip_if_not_implemented
     def amphora_failover(self, amphora_id):
         """Failover an amphora.
 
diff --git a/octavia_tempest_plugin/services/load_balancer/v2/availability_zone_capabilities_client.py b/octavia_tempest_plugin/services/load_balancer/v2/availability_zone_capabilities_client.py
index 92696a7..679af74 100644
--- a/octavia_tempest_plugin/services/load_balancer/v2/availability_zone_capabilities_client.py
+++ b/octavia_tempest_plugin/services/load_balancer/v2/availability_zone_capabilities_client.py
@@ -14,6 +14,7 @@
 #   under the License.
 #
 
+from octavia_tempest_plugin.common.decorators import skip_if_not_implemented
 from octavia_tempest_plugin.services.load_balancer.v2 import base_client
 from octavia_tempest_plugin.services.load_balancer.v2 import provider_client
 
@@ -36,6 +37,7 @@
             object=self.list_root_tag
         )
 
+    @skip_if_not_implemented
     def list_availability_zone_capabilities(self, provider, query_params=None,
                                             return_object_only=True):
         """Get a list of provider availability zone capability objects.
diff --git a/octavia_tempest_plugin/services/load_balancer/v2/availability_zone_client.py b/octavia_tempest_plugin/services/load_balancer/v2/availability_zone_client.py
index c729f21..ab87a85 100644
--- a/octavia_tempest_plugin/services/load_balancer/v2/availability_zone_client.py
+++ b/octavia_tempest_plugin/services/load_balancer/v2/availability_zone_client.py
@@ -16,6 +16,7 @@
 from oslo_log import log as logging
 from tempest.lib import exceptions
 
+from octavia_tempest_plugin.common.decorators import skip_if_not_implemented
 from octavia_tempest_plugin.services.load_balancer.v2 import base_client
 
 LOG = logging.getLogger(__name__)
@@ -33,6 +34,7 @@
         super(AvailabilityZoneClient, self).__init__(*args, **kwargs)
         self.uri = self.base_uri.format(object=self.resource_path)
 
+    @skip_if_not_implemented
     def create_availability_zone(self, name, availability_zone_profile_id,
                                  description=Unset, enabled=Unset,
                                  return_object_only=True):
@@ -75,6 +77,7 @@
                   if arg != 'self' and value is not Unset}
         return self._create_object(**kwargs)
 
+    @skip_if_not_implemented
     def show_availability_zone(self, availability_zone_name, query_params=None,
                                return_object_only=True):
         """Get the availability zone details.
@@ -116,6 +119,7 @@
                                  query_params=query_params,
                                  return_object_only=return_object_only)
 
+    @skip_if_not_implemented
     def list_availability_zones(self, query_params=None,
                                 return_object_only=True):
         """Get a list of availability zone objects.
@@ -155,6 +159,7 @@
         return self._list_objects(query_params=query_params,
                                   return_object_only=return_object_only)
 
+    @skip_if_not_implemented
     def update_availability_zone(self, availability_zone_name,
                                  description=Unset, enabled=Unset,
                                  return_object_only=True):
@@ -195,6 +200,7 @@
         kwargs['obj_id'] = kwargs.pop('availability_zone_name')
         return self._update_object(**kwargs)
 
+    @skip_if_not_implemented
     def delete_availability_zone(self, availability_zone_name,
                                  ignore_errors=False):
         """Delete an availability zone.
diff --git a/octavia_tempest_plugin/services/load_balancer/v2/availability_zone_profile_client.py b/octavia_tempest_plugin/services/load_balancer/v2/availability_zone_profile_client.py
index 1aaab90..071b15f 100644
--- a/octavia_tempest_plugin/services/load_balancer/v2/availability_zone_profile_client.py
+++ b/octavia_tempest_plugin/services/load_balancer/v2/availability_zone_profile_client.py
@@ -16,6 +16,7 @@
 from oslo_log import log as logging
 from tempest.lib import exceptions
 
+from octavia_tempest_plugin.common.decorators import skip_if_not_implemented
 from octavia_tempest_plugin.services.load_balancer.v2 import base_client
 
 LOG = logging.getLogger(__name__)
@@ -33,6 +34,7 @@
         super(AvailabilityZoneProfileClient, self).__init__(*args, **kwargs)
         self.uri = self.base_uri.format(object=self.resource_path)
 
+    @skip_if_not_implemented
     def create_availability_zone_profile(self, name, provider_name,
                                          availability_zone_data,
                                          return_object_only=True):
@@ -73,6 +75,7 @@
                   if arg != 'self' and value is not Unset}
         return self._create_object(**kwargs)
 
+    @skip_if_not_implemented
     def show_availability_zone_profile(self, availability_zone_profile_id,
                                        query_params=None,
                                        return_object_only=True):
@@ -116,6 +119,7 @@
                                  query_params=query_params,
                                  return_object_only=return_object_only)
 
+    @skip_if_not_implemented
     def list_availability_zone_profiles(self, query_params=None,
                                         return_object_only=True):
         """Get a list of availability zone profile objects.
@@ -155,6 +159,7 @@
         return self._list_objects(query_params=query_params,
                                   return_object_only=return_object_only)
 
+    @skip_if_not_implemented
     def update_availability_zone_profile(
             self, availability_zone_profile_id, name=Unset,
             provider_name=Unset, availability_zone_data=Unset,
@@ -199,6 +204,7 @@
         kwargs['obj_id'] = kwargs.pop('availability_zone_profile_id')
         return self._update_object(**kwargs)
 
+    @skip_if_not_implemented
     def delete_availability_zone_profile(self, availability_zone_profile_id,
                                          ignore_errors=False):
         """Delete an availability zone profile.
diff --git a/octavia_tempest_plugin/services/load_balancer/v2/flavor_capabilities_client.py b/octavia_tempest_plugin/services/load_balancer/v2/flavor_capabilities_client.py
index 4c23042..eb07faf 100644
--- a/octavia_tempest_plugin/services/load_balancer/v2/flavor_capabilities_client.py
+++ b/octavia_tempest_plugin/services/load_balancer/v2/flavor_capabilities_client.py
@@ -13,6 +13,7 @@
 #   under the License.
 #
 
+from octavia_tempest_plugin.common.decorators import skip_if_not_implemented
 from octavia_tempest_plugin.services.load_balancer.v2 import base_client
 from octavia_tempest_plugin.services.load_balancer.v2 import provider_client
 
@@ -34,6 +35,7 @@
             object=self.list_root_tag
         )
 
+    @skip_if_not_implemented
     def list_flavor_capabilities(self, provider, query_params=None,
                                  return_object_only=True):
         """Get a list of provider flavor capability objects.
diff --git a/octavia_tempest_plugin/services/load_balancer/v2/flavor_client.py b/octavia_tempest_plugin/services/load_balancer/v2/flavor_client.py
index 085da9e..8a87a33 100644
--- a/octavia_tempest_plugin/services/load_balancer/v2/flavor_client.py
+++ b/octavia_tempest_plugin/services/load_balancer/v2/flavor_client.py
@@ -16,6 +16,7 @@
 from oslo_log import log as logging
 from tempest.lib import exceptions
 
+from octavia_tempest_plugin.common.decorators import skip_if_not_implemented
 from octavia_tempest_plugin.services.load_balancer.v2 import base_client
 
 LOG = logging.getLogger(__name__)
@@ -27,6 +28,7 @@
     root_tag = 'flavor'
     list_root_tag = 'flavors'
 
+    @skip_if_not_implemented
     def create_flavor(self, name, flavor_profile_id, description=Unset,
                       enabled=Unset, return_object_only=True):
         """Create a flavor.
@@ -67,6 +69,7 @@
                   if arg != 'self' and value is not Unset}
         return self._create_object(**kwargs)
 
+    @skip_if_not_implemented
     def show_flavor(self, flavor_id, query_params=None,
                     return_object_only=True):
         """Get the flavor details.
@@ -108,6 +111,7 @@
                                  query_params=query_params,
                                  return_object_only=return_object_only)
 
+    @skip_if_not_implemented
     def list_flavors(self, query_params=None, return_object_only=True):
         """Get a list of flavor objects.
 
@@ -146,6 +150,7 @@
         return self._list_objects(query_params=query_params,
                                   return_object_only=return_object_only)
 
+    @skip_if_not_implemented
     def update_flavor(self, flavor_id, name=Unset, description=Unset,
                       enabled=Unset, return_object_only=True):
         """Update a flavor.
@@ -186,6 +191,7 @@
         kwargs['obj_id'] = kwargs.pop('flavor_id')
         return self._update_object(**kwargs)
 
+    @skip_if_not_implemented
     def delete_flavor(self, flavor_id, ignore_errors=False):
         """Delete a flavor.
 
diff --git a/octavia_tempest_plugin/services/load_balancer/v2/flavor_profile_client.py b/octavia_tempest_plugin/services/load_balancer/v2/flavor_profile_client.py
index 811cff8..bc0f2fb 100644
--- a/octavia_tempest_plugin/services/load_balancer/v2/flavor_profile_client.py
+++ b/octavia_tempest_plugin/services/load_balancer/v2/flavor_profile_client.py
@@ -16,6 +16,7 @@
 from oslo_log import log as logging
 from tempest.lib import exceptions
 
+from octavia_tempest_plugin.common.decorators import skip_if_not_implemented
 from octavia_tempest_plugin.services.load_balancer.v2 import base_client
 
 LOG = logging.getLogger(__name__)
@@ -27,6 +28,7 @@
     root_tag = 'flavorprofile'
     list_root_tag = 'flavorprofiles'
 
+    @skip_if_not_implemented
     def create_flavor_profile(self, name, provider_name, flavor_data,
                               return_object_only=True):
         """Create a flavor profile.
@@ -65,6 +67,7 @@
                   if arg != 'self' and value is not Unset}
         return self._create_object(**kwargs)
 
+    @skip_if_not_implemented
     def show_flavor_profile(self, flavorprofile_id, query_params=None,
                             return_object_only=True):
         """Get the flavor profile details.
@@ -106,6 +109,7 @@
                                  query_params=query_params,
                                  return_object_only=return_object_only)
 
+    @skip_if_not_implemented
     def list_flavor_profiles(self, query_params=None, return_object_only=True):
         """Get a list of flavor profile objects.
 
@@ -144,6 +148,7 @@
         return self._list_objects(query_params=query_params,
                                   return_object_only=return_object_only)
 
+    @skip_if_not_implemented
     def update_flavor_profile(
             self, flavorprofile_id, name=Unset, provider_name=Unset,
             flavor_data=Unset, return_object_only=True):
@@ -185,6 +190,7 @@
         kwargs['obj_id'] = kwargs.pop('flavorprofile_id')
         return self._update_object(**kwargs)
 
+    @skip_if_not_implemented
     def delete_flavor_profile(self, flavorprofile_id, ignore_errors=False):
         """Delete a flavor profile.
 
diff --git a/octavia_tempest_plugin/services/load_balancer/v2/healthmonitor_client.py b/octavia_tempest_plugin/services/load_balancer/v2/healthmonitor_client.py
index a7d2e6b..4ce362e 100644
--- a/octavia_tempest_plugin/services/load_balancer/v2/healthmonitor_client.py
+++ b/octavia_tempest_plugin/services/load_balancer/v2/healthmonitor_client.py
@@ -14,6 +14,7 @@
 
 from tempest import config
 
+from octavia_tempest_plugin.common.decorators import skip_if_not_implemented
 from octavia_tempest_plugin.services.load_balancer.v2 import base_client
 
 CONF = config.CONF
@@ -26,6 +27,7 @@
     list_root_tag = 'healthmonitors'
     resource_name = 'healthmonitor'
 
+    @skip_if_not_implemented
     def create_healthmonitor(self, pool_id, type, delay, timeout, max_retries,
                              max_retries_down=Unset, name=Unset, tags=Unset,
                              http_method=Unset, url_path=Unset,
@@ -87,6 +89,7 @@
                   if arg != 'self' and value is not Unset}
         return self._create_object(**kwargs)
 
+    @skip_if_not_implemented
     def show_healthmonitor(self, healthmonitor_id, query_params=None,
                            return_object_only=True):
         """Get healthmonitor details.
@@ -128,6 +131,7 @@
                                  query_params=query_params,
                                  return_object_only=return_object_only)
 
+    @skip_if_not_implemented
     def list_healthmonitors(self, query_params=None, return_object_only=True):
         """Get a list of healthmonitor objects.
 
@@ -166,6 +170,7 @@
         return self._list_objects(query_params=query_params,
                                   return_object_only=return_object_only)
 
+    @skip_if_not_implemented
     def update_healthmonitor(self, healthmonitor_id, delay=Unset,
                              timeout=Unset, max_retries=Unset,
                              max_retries_down=Unset, name=Unset, tags=Unset,
@@ -228,6 +233,7 @@
         kwargs['obj_id'] = kwargs.pop('healthmonitor_id')
         return self._update_object(**kwargs)
 
+    @skip_if_not_implemented
     def delete_healthmonitor(self, healthmonitor_id, ignore_errors=False):
         """Delete a healthmonitor.
 
diff --git a/octavia_tempest_plugin/services/load_balancer/v2/l7policy_client.py b/octavia_tempest_plugin/services/load_balancer/v2/l7policy_client.py
index 1feb72f..36eef40 100644
--- a/octavia_tempest_plugin/services/load_balancer/v2/l7policy_client.py
+++ b/octavia_tempest_plugin/services/load_balancer/v2/l7policy_client.py
@@ -14,6 +14,7 @@
 
 from tempest import config
 
+from octavia_tempest_plugin.common.decorators import skip_if_not_implemented
 from octavia_tempest_plugin.services.load_balancer.v2 import base_client
 
 CONF = config.CONF
@@ -26,6 +27,7 @@
     list_root_tag = 'l7policies'
     resource_name = 'l7policy'
 
+    @skip_if_not_implemented
     def create_l7policy(self, listener_id, action, name=Unset,
                         description=Unset, tags=Unset, admin_state_up=Unset,
                         position=Unset, redirect_pool_id=Unset,
@@ -78,6 +80,7 @@
                   if arg != 'self' and value is not Unset}
         return self._create_object(**kwargs)
 
+    @skip_if_not_implemented
     def show_l7policy(self, l7policy_id, query_params=None,
                       return_object_only=True):
         """Get l7policy details.
@@ -119,6 +122,7 @@
                                  query_params=query_params,
                                  return_object_only=return_object_only)
 
+    @skip_if_not_implemented
     def list_l7policies(self, query_params=None, return_object_only=True):
         """Get a list of l7policy objects.
 
@@ -157,6 +161,7 @@
         return self._list_objects(query_params=query_params,
                                   return_object_only=return_object_only)
 
+    @skip_if_not_implemented
     def update_l7policy(self, l7policy_id, action=Unset, name=Unset,
                         description=Unset, tags=Unset, admin_state_up=Unset,
                         position=Unset, redirect_pool_id=Unset,
@@ -210,6 +215,7 @@
         kwargs['obj_id'] = kwargs.pop('l7policy_id')
         return self._update_object(**kwargs)
 
+    @skip_if_not_implemented
     def delete_l7policy(self, l7policy_id, ignore_errors=False):
         """Delete a l7policy.
 
diff --git a/octavia_tempest_plugin/services/load_balancer/v2/l7rule_client.py b/octavia_tempest_plugin/services/load_balancer/v2/l7rule_client.py
index be5434d..da40af2 100644
--- a/octavia_tempest_plugin/services/load_balancer/v2/l7rule_client.py
+++ b/octavia_tempest_plugin/services/load_balancer/v2/l7rule_client.py
@@ -14,6 +14,7 @@
 
 from tempest import config
 
+from octavia_tempest_plugin.common.decorators import skip_if_not_implemented
 from octavia_tempest_plugin.services.load_balancer.v2 import base_client
 from octavia_tempest_plugin.services.load_balancer.v2 import l7policy_client
 
@@ -38,6 +39,7 @@
             object=self.list_root_tag
         )
 
+    @skip_if_not_implemented
     def create_l7rule(self, l7policy_id, type, value, compare_type, tags=Unset,
                       admin_state_up=Unset, key=Unset, invert=Unset,
                       return_object_only=True):
@@ -87,6 +89,7 @@
         kwargs['parent_id'] = kwargs.pop('l7policy_id')
         return self._create_object(**kwargs)
 
+    @skip_if_not_implemented
     def show_l7rule(self, l7rule_id, l7policy_id, query_params=None,
                     return_object_only=True):
         """Get l7rule details.
@@ -130,6 +133,7 @@
                                  query_params=query_params,
                                  return_object_only=return_object_only)
 
+    @skip_if_not_implemented
     def list_l7rules(self, l7policy_id, query_params=None,
                      return_object_only=True):
         """Get a list of l7rule objects.
@@ -171,6 +175,7 @@
                                   query_params=query_params,
                                   return_object_only=return_object_only)
 
+    @skip_if_not_implemented
     def update_l7rule(self, l7rule_id, l7policy_id, type=Unset, value=Unset,
                       compare_type=Unset, tags=Unset, admin_state_up=Unset,
                       key=Unset, invert=Unset, return_object_only=True):
@@ -222,6 +227,7 @@
         kwargs['parent_id'] = kwargs.pop('l7policy_id')
         return self._update_object(**kwargs)
 
+    @skip_if_not_implemented
     def delete_l7rule(self, l7rule_id, l7policy_id, ignore_errors=False):
         """Delete a l7rule.
 
diff --git a/octavia_tempest_plugin/services/load_balancer/v2/listener_client.py b/octavia_tempest_plugin/services/load_balancer/v2/listener_client.py
index eb7690a..c059a84 100644
--- a/octavia_tempest_plugin/services/load_balancer/v2/listener_client.py
+++ b/octavia_tempest_plugin/services/load_balancer/v2/listener_client.py
@@ -17,6 +17,7 @@
 
 from tempest import config
 
+from octavia_tempest_plugin.common.decorators import skip_if_not_implemented
 from octavia_tempest_plugin.services.load_balancer.v2 import base_client
 
 CONF = config.CONF
@@ -28,6 +29,7 @@
     root_tag = 'listener'
     list_root_tag = 'listeners'
 
+    @skip_if_not_implemented
     def create_listener(self, protocol, protocol_port, loadbalancer_id,
                         name=Unset, description=Unset, tags=Unset,
                         admin_state_up=Unset, connection_limit=Unset,
@@ -120,6 +122,7 @@
                   if arg != 'self' and value is not Unset}
         return self._create_object(**kwargs)
 
+    @skip_if_not_implemented
     def show_listener(self, listener_id, query_params=None,
                       return_object_only=True):
         """Get listener details.
@@ -161,6 +164,7 @@
                                  query_params=query_params,
                                  return_object_only=return_object_only)
 
+    @skip_if_not_implemented
     def list_listeners(self, query_params=None, return_object_only=True):
         """Get a list of listener objects.
 
@@ -199,6 +203,7 @@
         return self._list_objects(query_params=query_params,
                                   return_object_only=return_object_only)
 
+    @skip_if_not_implemented
     def update_listener(self, listener_id, name=Unset, description=Unset,
                         tags=Unset, admin_state_up=Unset,
                         connection_limit=Unset, timeout_client_data=Unset,
@@ -289,6 +294,7 @@
         kwargs['obj_id'] = kwargs.pop('listener_id')
         return self._update_object(**kwargs)
 
+    @skip_if_not_implemented
     def delete_listener(self, listener_id, ignore_errors=False):
         """Delete a listener.
 
@@ -325,6 +331,7 @@
         return self._delete_obj(obj_id=listener_id,
                                 ignore_errors=ignore_errors)
 
+    @skip_if_not_implemented
     def get_listener_stats(self, listener_id, query_params=None,
                            return_object_only=True):
         """Get listener statistics.
diff --git a/octavia_tempest_plugin/services/load_balancer/v2/loadbalancer_client.py b/octavia_tempest_plugin/services/load_balancer/v2/loadbalancer_client.py
index 0fafbf2..9499d89 100644
--- a/octavia_tempest_plugin/services/load_balancer/v2/loadbalancer_client.py
+++ b/octavia_tempest_plugin/services/load_balancer/v2/loadbalancer_client.py
@@ -17,6 +17,7 @@
 
 from tempest import config
 
+from octavia_tempest_plugin.common.decorators import skip_if_not_implemented
 from octavia_tempest_plugin.services.load_balancer.v2 import base_client
 
 CONF = config.CONF
@@ -34,6 +35,7 @@
         self.timeout = CONF.load_balancer.lb_build_timeout
         self.build_interval = CONF.load_balancer.lb_build_interval
 
+    @skip_if_not_implemented
     def create_loadbalancer(self, name=Unset, description=Unset,
                             admin_state_up=Unset, flavor_id=Unset,
                             listeners=Unset, project_id=Unset, provider=Unset,
@@ -92,6 +94,7 @@
                   if arg != 'self' and value is not Unset}
         return self._create_object(**kwargs)
 
+    @skip_if_not_implemented
     def show_loadbalancer(self, lb_id, query_params=None,
                           return_object_only=True):
         """Get loadbalancer details.
@@ -133,6 +136,7 @@
                                  query_params=query_params,
                                  return_object_only=return_object_only)
 
+    @skip_if_not_implemented
     def list_loadbalancers(self, query_params=None, return_object_only=True):
         """Get a list of loadbalancer objects.
 
@@ -171,6 +175,7 @@
         return self._list_objects(query_params=query_params,
                                   return_object_only=return_object_only)
 
+    @skip_if_not_implemented
     def update_loadbalancer(self, lb_id, name=Unset, description=Unset,
                             tags=Unset, admin_state_up=Unset,
                             vip_qos_policy_id=Unset,
@@ -220,6 +225,7 @@
         kwargs['obj_id'] = kwargs.pop('lb_id')
         return self._update_object(**kwargs)
 
+    @skip_if_not_implemented
     def delete_loadbalancer(self, lb_id, cascade=False, ignore_errors=False):
         """Delete a loadbalancer.
 
@@ -259,6 +265,7 @@
                                 ignore_errors=ignore_errors,
                                 cascade=cascade)
 
+    @skip_if_not_implemented
     def failover_loadbalancer(self, lb_id):
         """Failover a loadbalancer.
 
@@ -295,6 +302,7 @@
         self.expected_success(202, response.status)
         return
 
+    @skip_if_not_implemented
     def get_loadbalancer_stats(self, lb_id, query_params=None,
                                return_object_only=True):
         """Get loadbalancer statistics.
@@ -345,6 +353,7 @@
         else:
             return jsonutils.loads(body.decode('utf-8'))
 
+    @skip_if_not_implemented
     def get_loadbalancer_status(self, lb_id, query_params=None,
                                 return_object_only=True):
         """Get a loadbalancer status tree.
diff --git a/octavia_tempest_plugin/services/load_balancer/v2/member_client.py b/octavia_tempest_plugin/services/load_balancer/v2/member_client.py
index 0f0d639..c0d83da 100644
--- a/octavia_tempest_plugin/services/load_balancer/v2/member_client.py
+++ b/octavia_tempest_plugin/services/load_balancer/v2/member_client.py
@@ -16,6 +16,7 @@
 from oslo_serialization import jsonutils
 from tempest import config
 
+from octavia_tempest_plugin.common.decorators import skip_if_not_implemented
 from octavia_tempest_plugin.services.load_balancer.v2 import base_client
 from octavia_tempest_plugin.services.load_balancer.v2 import pool_client
 
@@ -39,6 +40,7 @@
             object=self.list_root_tag
         )
 
+    @skip_if_not_implemented
     def create_member(self, pool_id, address, protocol_port,
                       name=Unset, tags=Unset, admin_state_up=Unset,
                       weight=Unset,
@@ -98,6 +100,7 @@
         kwargs['parent_id'] = kwargs.pop('pool_id')
         return self._create_object(**kwargs)
 
+    @skip_if_not_implemented
     def show_member(self, member_id, pool_id, query_params=None,
                     return_object_only=True):
         """Get member details.
@@ -141,6 +144,7 @@
                                  query_params=query_params,
                                  return_object_only=return_object_only)
 
+    @skip_if_not_implemented
     def list_members(self, pool_id, query_params=None,
                      return_object_only=True):
         """Get a list of member objects.
@@ -182,6 +186,7 @@
                                   query_params=query_params,
                                   return_object_only=return_object_only)
 
+    @skip_if_not_implemented
     def update_member(self, member_id, pool_id, name=Unset, tags=Unset,
                       admin_state_up=Unset, weight=Unset, backup=Unset,
                       monitor_address=Unset, monitor_port=Unset,
@@ -238,6 +243,7 @@
         kwargs['parent_id'] = kwargs.pop('pool_id')
         return self._update_object(**kwargs)
 
+    @skip_if_not_implemented
     def update_members(self, pool_id, members_list):
         """Batch update all members on a pool.
 
@@ -277,6 +283,7 @@
         self.expected_success(202, response.status)
         return
 
+    @skip_if_not_implemented
     def delete_member(self, member_id, pool_id, ignore_errors=False):
         """Delete a member.
 
diff --git a/octavia_tempest_plugin/services/load_balancer/v2/pool_client.py b/octavia_tempest_plugin/services/load_balancer/v2/pool_client.py
index 58db1af..98f4bfa 100644
--- a/octavia_tempest_plugin/services/load_balancer/v2/pool_client.py
+++ b/octavia_tempest_plugin/services/load_balancer/v2/pool_client.py
@@ -14,6 +14,7 @@
 
 from tempest import config
 
+from octavia_tempest_plugin.common.decorators import skip_if_not_implemented
 from octavia_tempest_plugin.services.load_balancer.v2 import base_client
 
 CONF = config.CONF
@@ -26,6 +27,7 @@
     list_root_tag = 'pools'
     resource_name = 'pool'
 
+    @skip_if_not_implemented
     def create_pool(self, protocol, lb_algorithm, loadbalancer_id=Unset,
                     listener_id=Unset, name=Unset, description=Unset,
                     tags=Unset,
@@ -79,6 +81,7 @@
                   if arg != 'self' and value is not Unset}
         return self._create_object(**kwargs)
 
+    @skip_if_not_implemented
     def show_pool(self, pool_id, query_params=None, return_object_only=True):
         """Get pool details.
 
@@ -119,6 +122,7 @@
                                  query_params=query_params,
                                  return_object_only=return_object_only)
 
+    @skip_if_not_implemented
     def list_pools(self, query_params=None, return_object_only=True):
         """Get a list of pool objects.
 
@@ -157,6 +161,7 @@
         return self._list_objects(query_params=query_params,
                                   return_object_only=return_object_only)
 
+    @skip_if_not_implemented
     def update_pool(self, pool_id, lb_algorithm=Unset, name=Unset,
                     description=Unset, tags=Unset, admin_state_up=Unset,
                     session_persistence=Unset, return_object_only=True):
@@ -207,6 +212,7 @@
         kwargs['obj_id'] = kwargs.pop('pool_id')
         return self._update_object(**kwargs)
 
+    @skip_if_not_implemented
     def delete_pool(self, pool_id, ignore_errors=False):
         """Delete a pool.
 
diff --git a/octavia_tempest_plugin/services/load_balancer/v2/provider_client.py b/octavia_tempest_plugin/services/load_balancer/v2/provider_client.py
index cbef1df..319826e 100644
--- a/octavia_tempest_plugin/services/load_balancer/v2/provider_client.py
+++ b/octavia_tempest_plugin/services/load_balancer/v2/provider_client.py
@@ -13,6 +13,7 @@
 #   under the License.
 #
 
+from octavia_tempest_plugin.common.decorators import skip_if_not_implemented
 from octavia_tempest_plugin.services.load_balancer.v2 import base_client
 
 Unset = base_client.Unset
@@ -22,6 +23,7 @@
 
     list_root_tag = 'providers'
 
+    @skip_if_not_implemented
     def list_providers(self, query_params=None, return_object_only=True):
         """Get a list of provider objects.
 
diff --git a/requirements.txt b/requirements.txt
index b30b450..e5b93b8 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -16,4 +16,5 @@
 six>=1.10.0 # MIT
 tempest>=17.1.0 # Apache-2.0
 tenacity>=4.4.0 # Apache-2.0
+testtools>=2.2.0 # MIT
 keystoneauth1>=3.3.0 # Apache-2.0