Partially merge tag '0.2.0' into HEAD

Do not include new tests.

octavia-tempest-plugin 0.2.0 release

Change-Id: I2762ac90e1e998f6439df5159c417dfb1ab832e1
Related-PROD: PROD-26588 (PROD:26588)
diff --git a/.gitignore b/.gitignore
index 5992821..78c2e67 100644
--- a/.gitignore
+++ b/.gitignore
@@ -63,3 +63,7 @@
 
 # PyCharm IDE
 .idea
+
+# Others
+.stestr
+tempest.log
diff --git a/.stestr.conf b/.stestr.conf
new file mode 100644
index 0000000..7b8b54e
--- /dev/null
+++ b/.stestr.conf
@@ -0,0 +1,3 @@
+[DEFAULT]
+test_path=${OS_TEST_PATH:-./octavia_tempest_plugin/tests/}
+top_dir=./
diff --git a/.testr.conf b/.testr.conf
deleted file mode 100644
index 6d83b3c..0000000
--- a/.testr.conf
+++ /dev/null
@@ -1,7 +0,0 @@
-[DEFAULT]
-test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} \
-             OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} \
-             OS_TEST_TIMEOUT=${OS_TEST_TIMEOUT:-60} \
-             ${PYTHON:-python} -m subunit.run discover -t ./ . $LISTOPT $IDOPTION
-test_id_option=--load-list $IDFILE
-test_list_option=--list
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index f7ddd9c..8ab7e19 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -1,17 +1,17 @@
 If you would like to contribute to the development of OpenStack, you must
 follow the steps in this page:
 
-   http://docs.openstack.org/infra/manual/developers.html
+   https://docs.openstack.org/infra/manual/developers.html
 
 If you already have a good understanding of how the system works and your
 OpenStack accounts are set up, you can skip to the development workflow
 section of this documentation to learn how changes to OpenStack should be
 submitted for review via the Gerrit tool:
 
-   http://docs.openstack.org/infra/manual/developers.html#development-workflow
+   https://docs.openstack.org/infra/manual/developers.html#development-workflow
 
 Pull requests submitted through GitHub will be ignored.
 
-Bugs should be filed on Launchpad, not GitHub:
+Bugs should be filed on StoryBoard, not GitHub:
 
-   https://bugs.launchpad.net/octavia
+   https://storyboard.openstack.org/#!/project/openstack/octavia-tempest-plugin
diff --git a/HACKING.rst b/HACKING.rst
index 3494c6a..61845a9 100644
--- a/HACKING.rst
+++ b/HACKING.rst
@@ -1,4 +1,4 @@
 octavia-tempest-plugin Style Commandments
 =========================================
 
-Read the OpenStack Style Commandments http://docs.openstack.org/developer/hacking/
+Read the OpenStack Style Commandments https://docs.openstack.org/hacking/latest/
diff --git a/README.rst b/README.rst
index ce445fe..f2b51a7 100644
--- a/README.rst
+++ b/README.rst
@@ -2,8 +2,8 @@
 Team and repository tags
 ========================
 
-.. image:: http://governance.openstack.org/badges/octavia-tempest-plugin.svg
-    :target: http://governance.openstack.org/reference/tags/index.html
+.. image:: https://governance.openstack.org/tc/badges/octavia-tempest-plugin.svg
+    :target: https://governance.openstack.org/tc/reference/tags/index.html
 
 .. Change things from this point on
 
@@ -21,9 +21,9 @@
 https://docs.openstack.org/tempest/latest/plugin.html
 
 * Free software: Apache license
-* Documentation: http://docs.openstack.org/octavia-tempest-plugin/latest
-* Source: http://git.openstack.org/cgit/openstack/octavia-tempest-plugin
-* Bugs: https://storyboard.openstack.org/#!/project/910
+* Documentation: https://docs.openstack.org/octavia-tempest-plugin/latest/
+* Source: https://git.openstack.org/cgit/openstack/octavia-tempest-plugin
+* Bugs: https://storyboard.openstack.org/#!/project/openstack/octavia-tempest-plugin
 
 Installing
 ----------
@@ -41,7 +41,7 @@
 
 To run a single test case, call with full path, for example::
 
-    $ tox -e all-plugin -- octavia_tempest_plugin.tests.v2.scenario.test_basic_ops.BasicOpsTest.test_basic_ops
+    $ tox -e all-plugin -- octavia_tempest_plugin.tests.scenario.v2.test_traffic_ops.TrafficOperationsScenarioTest.test_basic_traffic
 
 To retrieve a list of all tempest tests, run::
 
diff --git a/doc/requirements.txt b/doc/requirements.txt
index 1ade022..467e820 100644
--- a/doc/requirements.txt
+++ b/doc/requirements.txt
@@ -2,6 +2,7 @@
 # of appearance. Changing the order has an impact on the overall integration
 # process, which may cause wedges in the gate later.
 
+sphinxcontrib-apidoc # BSD
 sphinx>=1.6.2,!=1.6.6,!=1.6.7 # BSD
 openstackdocstheme>=1.18.1 # Apache-2.0
 
diff --git a/doc/source/conf.py b/doc/source/conf.py
index 1f18a18..2d5c49f 100755
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -16,7 +16,6 @@
 import sys
 
 import openstackdocstheme
-from sphinx import apidoc
 
 sys.path.insert(0, os.path.abspath('../..'))
 sys.path.insert(0, os.path.abspath('.'))
@@ -29,7 +28,8 @@
     'sphinx.ext.autodoc',
     'sphinx.ext.viewcode',
     'openstackdocstheme',
-    'oslo_config.sphinxext'
+    'oslo_config.sphinxext',
+    'sphinxcontrib.apidoc'
 ]
 
 # autodoc generation is a bit aggressive and a nuisance when doing heavy
@@ -98,22 +98,6 @@
 bug_project = '910'
 bug_tag = 'docs'
 
-# TODO(mordred) We should extract this into a sphinx plugin
-def run_apidoc(_):
-    cur_dir = os.path.abspath(os.path.dirname(__file__))
-    out_dir = os.path.join(cur_dir, '_build', 'modules')
-    module = os.path.join(cur_dir, '..', '..', 'octavia_tempest_plugin')
-    # Keep the order of arguments same as the sphinx-apidoc help, otherwise it
-    # would cause unexpected errors:
-    # sphinx-apidoc [options] -o <output_path> <module_path>
-    # [exclude_pattern, ...]
-    apidoc.main([
-        '--force',
-        '-o',
-        out_dir,
-        module,
-    ])
-
-
-def setup(app):
-    app.connect('builder-inited', run_apidoc)
+apidoc_output_dir = '_build/modules'
+apidoc_module_dir = '../../octavia_tempest_plugin'
+apidoc_excluded_paths = []
diff --git a/octavia_tempest_plugin/clients.py b/octavia_tempest_plugin/clients.py
index aa1949f..1a0a894 100644
--- a/octavia_tempest_plugin/clients.py
+++ b/octavia_tempest_plugin/clients.py
@@ -16,7 +16,21 @@
 from tempest import config
 
 from octavia_tempest_plugin.services.load_balancer.v2 import (
+    amphora_client)
+from octavia_tempest_plugin.services.load_balancer.v2 import (
+    healthmonitor_client)
+from octavia_tempest_plugin.services.load_balancer.v2 import (
+    l7policy_client)
+from octavia_tempest_plugin.services.load_balancer.v2 import (
+    l7rule_client)
+from octavia_tempest_plugin.services.load_balancer.v2 import (
+    listener_client)
+from octavia_tempest_plugin.services.load_balancer.v2 import (
     loadbalancer_client)
+from octavia_tempest_plugin.services.load_balancer.v2 import (
+    member_client)
+from octavia_tempest_plugin.services.load_balancer.v2 import (
+    pool_client)
 
 CONF = config.CONF
 SERVICE_TYPE = 'load-balancer'
@@ -29,3 +43,17 @@
 
         self.loadbalancer_client = loadbalancer_client.LoadbalancerClient(
             self.auth_provider, SERVICE_TYPE, CONF.identity.region)
+        self.listener_client = listener_client.ListenerClient(
+            self.auth_provider, SERVICE_TYPE, CONF.identity.region)
+        self.pool_client = pool_client.PoolClient(
+            self.auth_provider, SERVICE_TYPE, CONF.identity.region)
+        self.member_client = member_client.MemberClient(
+            self.auth_provider, SERVICE_TYPE, CONF.identity.region)
+        self.healthmonitor_client = healthmonitor_client.HealthMonitorClient(
+            self.auth_provider, SERVICE_TYPE, CONF.identity.region)
+        self.l7policy_client = l7policy_client.L7PolicyClient(
+            self.auth_provider, SERVICE_TYPE, CONF.identity.region)
+        self.l7rule_client = l7rule_client.L7RuleClient(
+            self.auth_provider, SERVICE_TYPE, CONF.identity.region)
+        self.amphora_client = amphora_client.AmphoraClient(
+            self.auth_provider, SERVICE_TYPE, CONF.identity.region)
diff --git a/octavia_tempest_plugin/common/constants.py b/octavia_tempest_plugin/common/constants.py
index c7bb82b..4154e7b 100644
--- a/octavia_tempest_plugin/common/constants.py
+++ b/octavia_tempest_plugin/common/constants.py
@@ -37,13 +37,45 @@
 VIP_PORT_ID = 'vip_port_id'
 VIP_SUBNET_ID = 'vip_subnet_id'
 VIP_QOS_POLICY_ID = 'vip_qos_policy_id'
+PROTOCOL = 'protocol'
+PROTOCOL_PORT = 'protocol_port'
+LOADBALANCER_ID = 'loadbalancer_id'
+CONNECTION_LIMIT = 'connection_limit'
+INSERT_HEADERS = 'insert_headers'
+X_FORWARDED_FOR = 'X-Forwarded-For'
+X_FORWARDED_PORT = 'X-Forwarded-Port'
+TIMEOUT_CLIENT_DATA = 'timeout_client_data'
+TIMEOUT_MEMBER_CONNECT = 'timeout_member_connect'
+TIMEOUT_MEMBER_DATA = 'timeout_member_data'
+TIMEOUT_TCP_INSPECT = 'timeout_tcp_inspect'
+DEFAULT_TLS_CONTAINER_REF = 'default_tls_container_ref'
+SNI_CONTAINER_REFS = 'sni_container_refs'
+DEFAULT_POOL_ID = 'default_pool_id'
+L7_POLICIES = 'l7_policies'
 
-# API valid fields
-SHOW_LOAD_BALANCER_RESPONSE_FIELDS = (
-    ADMIN_STATE_UP, CREATED_AT, DESCRIPTION, FLAVOR_ID, ID, LISTENERS, NAME,
-    OPERATING_STATUS, POOLS, PROJECT_ID, PROVIDER, PROVISIONING_STATUS,
-    UPDATED_AT, VIP_ADDRESS, VIP_NETWORK_ID, VIP_PORT_ID, VIP_SUBNET_ID,
-    VIP_QOS_POLICY_ID)
+LB_ALGORITHM = 'lb_algorithm'
+LB_ALGORITHM_ROUND_ROBIN = 'ROUND_ROBIN'
+LB_ALGORITHM_LEAST_CONNECTIONS = 'LEAST_CONNECTIONS'
+LB_ALGORITHM_SOURCE_IP = 'SOURCE_IP'
+SESSION_PERSISTENCE = 'session_persistence'
+LISTENER_ID = 'listener_id'
+LOADBALANCERS = 'loadbalancers'
+
+POOL_ID = 'pool_id'
+ADDRESS = 'address'
+WEIGHT = 'weight'
+BACKUP = 'backup'
+SUBNET_ID = 'subnet_id'
+MONITOR_ADDRESS = 'monitor_address'
+MONITOR_PORT = 'monitor_port'
+
+DELAY = 'delay'
+TIMEOUT = 'timeout'
+MAX_RETRIES = 'max_retries'
+MAX_RETRIES_DOWN = 'max_retries_down'
+HTTP_METHOD = 'http_method'
+URL_PATH = 'url_path'
+EXPECTED_CODES = 'expected_codes'
 
 # Other constants
 ACTIVE = 'ACTIVE'
@@ -54,9 +86,149 @@
 FIELDS = 'fields'
 OFFLINE = 'OFFLINE'
 ONLINE = 'ONLINE'
+NO_MONITOR = 'NO_MONITOR'
+ERROR = 'ERROR'
 SORT = 'sort'
 
+# Protocols
+HTTP = 'HTTP'
+HTTPS = 'HTTPS'
+TCP = 'TCP'
+
+# HTTP Methods
+GET = 'GET'
+POST = 'POST'
+PUT = 'PUT'
+DELETE = 'DELETE'
+
+# HM Types
+HEALTH_MONITOR_PING = 'PING'
+HEALTH_MONITOR_TCP = 'TCP'
+HEALTH_MONITOR_HTTP = 'HTTP'
+HEALTH_MONITOR_HTTPS = 'HTTPS'
+HEALTH_MONITOR_TLS_HELLO = 'TLS-HELLO'
+
+# Session Persistence
+TYPE = 'type'
+COOKIE_NAME = 'cookie_name'
+SESSION_PERSISTENCE_SOURCE_IP = 'SOURCE_IP'
+SESSION_PERSISTENCE_HTTP_COOKIE = 'HTTP_COOKIE'
+SESSION_PERSISTENCE_APP_COOKIE = 'APP_COOKIE'
+
+# L7Policy options
+POSITION = 'position'
+REDIRECT_URL = 'redirect_url'
+REDIRECT_POOL_ID = 'redirect_pool_id'
+
+ACTION = 'action'
+REDIRECT_TO_POOL = 'REDIRECT_TO_POOL'
+REDIRECT_TO_URL = 'REDIRECT_TO_URL'
+REJECT = 'REJECT'
+
+# L7Rule options
+L7POLICY_ID = 'l7policy_id'
+VALUE = 'value'
+COMPARE_TYPE = 'compare_type'
+KEY = 'key'
+INVERT = 'invert'
+# Compare types
+EQUAL_TO = 'EQUAL_TO'
+STARTS_WITH = 'STARTS_WITH'
+ENDS_WITH = 'ENDS_WITH'
+CONTAINS = 'CONTAINS'
+REGEX = 'REGEX'
+# Types
+COOKIE = 'COOKIE'
+FILE_TYPE = 'FILE_TYPE'
+HEADER = 'HEADER'
+HOST_NAME = 'HOST_NAME'
+PATH = 'PATH'
+
 # RBAC options
 ADVANCED = 'advanced'
 OWNERADMIN = 'owner_or_admin'
 NONE = 'none'
+
+# Amphora fields
+COMPUTE_ID = 'compute_id'
+LB_NETWORK_IP = 'lb_network_ip'
+VRRP_IP = 'vrrp_ip'
+HA_IP = 'ha_ip'
+VRRP_PORT_ID = 'vrrp_port_id'
+HA_PORT_ID = 'ha_port_id'
+CERT_EXPIRATION = 'cert_expiration'
+CERT_BUSY = 'cert_busy'
+ROLE = 'role'
+STATUS = 'status'
+VRRP_INTERFACE = 'vrrp_interface'
+VRRP_ID = 'vrrp_id'
+VRRP_PRIORITY = 'vrrp_priority'
+CACHED_ZONE = 'cached_zone'
+IMAGE_ID = 'image_id'
+
+# Amphora roles
+ROLE_STANDALONE = 'STANDALONE'
+ROLE_MASTER = 'MASTER'
+ROLE_BACKUP = 'BACKUP'
+AMPHORA_ROLES = (ROLE_STANDALONE, ROLE_MASTER, ROLE_BACKUP)
+
+# Amphora statuses
+STATUS_BOOTING = 'BOOTING'
+STATUS_ALLOCATED = 'ALLOCATED'
+STATUS_READY = 'READY'
+STATUS_PENDING_CREATE = 'PENDING_CREATE'
+STATUS_PENDING_DELETE = 'PENDING_DELETE'
+STATUS_DELETED = 'DELETED'
+STATUS_ERROR = 'ERROR'
+AMPHORA_STATUSES = (
+    STATUS_BOOTING, STATUS_ALLOCATED, STATUS_READY, STATUS_PENDING_CREATE,
+    STATUS_PENDING_DELETE, STATUS_DELETED, STATUS_ERROR
+)
+
+# API valid fields
+SHOW_LOAD_BALANCER_RESPONSE_FIELDS = (
+    ADMIN_STATE_UP, CREATED_AT, DESCRIPTION, FLAVOR_ID, ID, LISTENERS, NAME,
+    OPERATING_STATUS, POOLS, PROJECT_ID, PROVIDER, PROVISIONING_STATUS,
+    UPDATED_AT, VIP_ADDRESS, VIP_NETWORK_ID, VIP_PORT_ID, VIP_SUBNET_ID,
+    VIP_QOS_POLICY_ID)
+
+SHOW_LISTENER_RESPONSE_FIELDS = [
+    ID, NAME, DESCRIPTION, PROVISIONING_STATUS, OPERATING_STATUS,
+    ADMIN_STATE_UP, PROTOCOL, PROTOCOL_PORT, CONNECTION_LIMIT,
+    DEFAULT_TLS_CONTAINER_REF, SNI_CONTAINER_REFS, PROJECT_ID,
+    DEFAULT_POOL_ID, L7_POLICIES, INSERT_HEADERS, CREATED_AT, UPDATED_AT
+]
+
+SHOW_POOL_RESPONSE_FIELDS = (
+    ID, NAME, DESCRIPTION, PROVISIONING_STATUS, OPERATING_STATUS,
+    ADMIN_STATE_UP, PROTOCOL, LB_ALGORITHM, SESSION_PERSISTENCE,
+    CREATED_AT, UPDATED_AT
+)
+
+SHOW_MEMBER_RESPONSE_FIELDS = [
+    ID, NAME, PROVISIONING_STATUS, OPERATING_STATUS, ADMIN_STATE_UP,
+    ADDRESS, PROTOCOL_PORT, WEIGHT, MONITOR_PORT, MONITOR_ADDRESS
+]
+
+SHOW_HEALTHMONITOR_RESPONSE_FIELDS = (
+    ID, NAME, PROVISIONING_STATUS, OPERATING_STATUS, ADMIN_STATE_UP,
+    TYPE, DELAY, TIMEOUT, MAX_RETRIES, MAX_RETRIES_DOWN, HTTP_METHOD,
+    URL_PATH, EXPECTED_CODES, CREATED_AT, UPDATED_AT
+)
+
+SHOW_L7POLICY_RESPONSE_FIELDS = (
+    ID, NAME, DESCRIPTION, PROVISIONING_STATUS, OPERATING_STATUS,
+    ADMIN_STATE_UP, LISTENER_ID, POSITION, ACTION, REDIRECT_URL,
+    REDIRECT_POOL_ID, CREATED_AT, UPDATED_AT
+)
+
+SHOW_L7RULE_RESPONSE_FIELDS = (
+    ID, ADMIN_STATE_UP, CREATED_AT, UPDATED_AT, TYPE, VALUE, COMPARE_TYPE,
+    KEY, INVERT
+)
+
+SHOW_AMPHORA_RESPONSE_FIELDS = [
+    ID, LOADBALANCER_ID, COMPUTE_ID, LB_NETWORK_IP, VRRP_IP, HA_IP,
+    VRRP_PORT_ID, HA_PORT_ID, CERT_EXPIRATION, CERT_BUSY, ROLE, STATUS,
+    VRRP_INTERFACE, VRRP_ID, VRRP_PRIORITY, CACHED_ZONE
+]
diff --git a/octavia_tempest_plugin/config.py b/octavia_tempest_plugin/config.py
index 70a43aa..1d874a6 100644
--- a/octavia_tempest_plugin/config.py
+++ b/octavia_tempest_plugin/config.py
@@ -51,7 +51,7 @@
                help='Time in seconds between build status checks for '
                     'non-load-balancer resources to build'),
     cfg.IntOpt('build_timeout',
-               default=30,
+               default=300,
                help='Timeout in seconds to wait for non-load-balancer '
                     'resources to build'),
     # load-balancer specific options
@@ -144,3 +144,12 @@
                default=None,
                help='Availability zone to use for creating servers.'),
 ]
+
+lb_feature_enabled_group = cfg.OptGroup(name='loadbalancer-feature-enabled',
+                                        title='Enabled/Disabled LB features')
+LBFeatureEnabledGroup = [
+    cfg.BoolOpt('health_monitor_enabled',
+                default=True,
+                help="Whether Health Monitor is available with provider"
+                     " driver or not."),
+]
diff --git a/octavia_tempest_plugin/plugin.py b/octavia_tempest_plugin/plugin.py
index 5aae722..ec093e7 100644
--- a/octavia_tempest_plugin/plugin.py
+++ b/octavia_tempest_plugin/plugin.py
@@ -35,6 +35,9 @@
                                   project_config.ServiceAvailableGroup)
         config.register_opt_group(conf, project_config.octavia_group,
                                   project_config.OctaviaGroup)
+        config.register_opt_group(conf,
+                                  project_config.lb_feature_enabled_group,
+                                  project_config.LBFeatureEnabledGroup)
 
     def get_opt_lists(self):
         return [
@@ -42,6 +45,8 @@
              project_config.ServiceAvailableGroup),
             (project_config.octavia_group.name,
              project_config.OctaviaGroup),
+            (project_config.lb_feature_enabled_group.name,
+             project_config.LBFeatureEnabledGroup)
         ]
 
     def get_service_clients(self):
diff --git a/octavia_tempest_plugin/services/load_balancer/v2/amphora_client.py b/octavia_tempest_plugin/services/load_balancer/v2/amphora_client.py
new file mode 100644
index 0000000..65a8077
--- /dev/null
+++ b/octavia_tempest_plugin/services/load_balancer/v2/amphora_client.py
@@ -0,0 +1,105 @@
+#   Copyright 2018 GoDaddy
+#
+#   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 tempest import config
+
+from octavia_tempest_plugin.services.load_balancer.v2 import base_client
+
+CONF = config.CONF
+
+
+class AmphoraClient(base_client.BaseLBaaSClient):
+
+    root_tag = 'amphora'
+    list_root_tag = 'amphorae'
+    base_uri = '/v2.0/octavia/{object}'
+
+    def show_amphora(self, amphora_id, query_params=None,
+                     return_object_only=True):
+        """Get amphora details.
+
+        :param amphora_id: The amphora ID to query.
+        :param query_params: The optional query parameters to append to the
+                             request. Ex. fields=id&fields=name
+        :param return_object_only: If True, the response returns the object
+                                   inside the root tag. False returns the full
+                                   response from the API.
+        :raises AssertionError: if the expected_code isn't a valid http success
+                                response code
+        :raises BadRequest: If a 400 response code is received
+        :raises Conflict: If a 409 response code is received
+        :raises Forbidden: If a 403 response code is received
+        :raises Gone: If a 410 response code is received
+        :raises InvalidContentType: If a 415 response code is received
+        :raises InvalidHTTPResponseBody: The response body wasn't valid JSON
+        :raises InvalidHttpSuccessCode: if the read code isn't an expected
+                                        http success code
+        :raises NotFound: If a 404 response code is received
+        :raises NotImplemented: If a 501 response code is received
+        :raises OverLimit: If a 413 response code is received and over_limit is
+                           not in the response body
+        :raises RateLimitExceeded: If a 413 response code is received and
+                                   over_limit is in the response body
+        :raises ServerFault: If a 500 response code is received
+        :raises Unauthorized: If a 401 response code is received
+        :raises UnexpectedContentType: If the content-type of the response
+                                       isn't an expect type
+        :raises UnexpectedResponseCode: If a response code above 400 is
+                                        received and it doesn't fall into any
+                                        of the handled checks
+        :raises UnprocessableEntity: If a 422 response code is received and
+                                     couldn't be parsed
+        :returns: An amphora object.
+        """
+        return self._show_object(obj_id=amphora_id,
+                                 query_params=query_params,
+                                 return_object_only=return_object_only)
+
+    def list_amphorae(self, query_params=None, return_object_only=True):
+        """Get a list of amphora objects.
+
+        :param query_params: The optional query parameters to append to the
+                             request. Ex. fields=id&fields=name
+        :param return_object_only: If True, the response returns the object
+                                   inside the root tag. False returns the full
+                                   response from the API.
+        :raises AssertionError: if the expected_code isn't a valid http success
+                                response code
+        :raises BadRequest: If a 400 response code is received
+        :raises Conflict: If a 409 response code is received
+        :raises Forbidden: If a 403 response code is received
+        :raises Gone: If a 410 response code is received
+        :raises InvalidContentType: If a 415 response code is received
+        :raises InvalidHTTPResponseBody: The response body wasn't valid JSON
+        :raises InvalidHttpSuccessCode: if the read code isn't an expected
+                                        http success code
+        :raises NotFound: If a 404 response code is received
+        :raises NotImplemented: If a 501 response code is received
+        :raises OverLimit: If a 413 response code is received and over_limit is
+                           not in the response body
+        :raises RateLimitExceeded: If a 413 response code is received and
+                                   over_limit is in the response body
+        :raises ServerFault: If a 500 response code is received
+        :raises Unauthorized: If a 401 response code is received
+        :raises UnexpectedContentType: If the content-type of the response
+                                       isn't an expect type
+        :raises UnexpectedResponseCode: If a response code above 400 is
+                                        received and it doesn't fall into any
+                                        of the handled checks
+        :raises UnprocessableEntity: If a 422 response code is received and
+                                     couldn't be parsed
+        :returns: A list of amphora objects.
+        """
+        return self._list_objects(query_params=query_params,
+                                  return_object_only=return_object_only)
diff --git a/octavia_tempest_plugin/services/load_balancer/v2/base_client.py b/octavia_tempest_plugin/services/load_balancer/v2/base_client.py
new file mode 100644
index 0000000..97e91d9
--- /dev/null
+++ b/octavia_tempest_plugin/services/load_balancer/v2/base_client.py
@@ -0,0 +1,483 @@
+#   Copyright 2018 GoDaddy
+#
+#   Licensed under the Apache License, Version 2.0 (the "License"); you may
+#   not use this file except in compliance with the License. You may obtain
+#   a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#   WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#   License for the specific language governing permissions and limitations
+#   under the License.
+#
+
+import json
+
+from oslo_log import log as logging
+from tempest import config
+from tempest.lib.common import rest_client
+from tempest.lib.common.utils import test_utils
+from tempest.lib import exceptions
+
+from octavia_tempest_plugin.common import constants as const
+from octavia_tempest_plugin.tests import waiters
+
+CONF = config.CONF
+LOG = logging.getLogger(__name__)
+
+
+class Unset(object):
+    def __bool__(self):
+        return False
+
+    __nonzero__ = __bool__
+
+    def __repr__(self):
+        return 'Unset'
+
+
+class BaseLBaaSClient(rest_client.RestClient):
+
+    root_tag = None
+    list_root_tag = None
+    base_uri = '/v2.0/lbaas/{object}'
+
+    def __init__(self, auth_provider, service, region, **kwargs):
+        super(BaseLBaaSClient, self).__init__(auth_provider, service,
+                                              region, **kwargs)
+        self.timeout = CONF.load_balancer.build_timeout
+        self.build_interval = CONF.load_balancer.build_interval
+        self.uri = self.base_uri.format(object=self.list_root_tag)
+        # Create a method for each object's cleanup
+        # This method should be used (rather than delete) for tempest cleanups.
+        cleanup_func_name = 'cleanup_{}'.format(self.root_tag)
+        if not hasattr(self, cleanup_func_name):
+            setattr(self, cleanup_func_name, self._cleanup_obj)
+
+    def _create_object(self, parent_id=None, return_object_only=True,
+                       **kwargs):
+        """Create an object.
+
+        :param return_object_only: If True, the response returns the object
+                                   inside the root tag. False returns the full
+                                   response from the API.
+        :param **kwargs: All attributes of the object should be passed as
+                         keyword arguments to this function.
+        :raises AssertionError: if the expected_code isn't a valid http success
+                                response code
+        :raises BadRequest: If a 400 response code is received
+        :raises Conflict: If a 409 response code is received
+        :raises Forbidden: If a 403 response code is received
+        :raises Gone: If a 410 response code is received
+        :raises InvalidContentType: If a 415 response code is received
+        :raises InvalidHTTPResponseBody: The response body wasn't valid JSON
+        :raises InvalidHttpSuccessCode: if the read code isn't an expected
+                                        http success code
+        :raises NotFound: If a 404 response code is received
+        :raises NotImplemented: If a 501 response code is received
+        :raises OverLimit: If a 413 response code is received and over_limit is
+                           not in the response body
+        :raises RateLimitExceeded: If a 413 response code is received and
+                                   over_limit is in the response body
+        :raises ServerFault: If a 500 response code is received
+        :raises Unauthorized: If a 401 response code is received
+        :raises UnexpectedContentType: If the content-type of the response
+                                       isn't an expect type
+        :raises UnexpectedResponseCode: If a response code above 400 is
+                                        received and it doesn't fall into any
+                                        of the handled checks
+        :raises UnprocessableEntity: If a 422 response code is received and
+                                     couldn't be parsed
+        :returns: An appropriate object.
+        """
+        obj_dict = {self.root_tag: kwargs}
+
+        if parent_id:
+            request_uri = self.uri.format(parent=parent_id)
+        else:
+            request_uri = self.uri
+
+        response, body = self.post(request_uri, json.dumps(obj_dict))
+        self.expected_success(201, response.status)
+        if return_object_only:
+            return json.loads(body.decode('utf-8'))[self.root_tag]
+        else:
+            return json.loads(body.decode('utf-8'))
+
+    def _show_object(self, obj_id, parent_id=None, query_params=None,
+                     return_object_only=True):
+        """Get object details.
+
+        :param obj_id: The object ID to query.
+        :param query_params: The optional query parameters to append to the
+                             request. Ex. fields=id&fields=name
+        :param return_object_only: If True, the response returns the object
+                                   inside the root tag. False returns the full
+                                   response from the API.
+        :raises AssertionError: if the expected_code isn't a valid http success
+                                response code
+        :raises BadRequest: If a 400 response code is received
+        :raises Conflict: If a 409 response code is received
+        :raises Forbidden: If a 403 response code is received
+        :raises Gone: If a 410 response code is received
+        :raises InvalidContentType: If a 415 response code is received
+        :raises InvalidHTTPResponseBody: The response body wasn't valid JSON
+        :raises InvalidHttpSuccessCode: if the read code isn't an expected
+                                        http success code
+        :raises NotFound: If a 404 response code is received
+        :raises NotImplemented: If a 501 response code is received
+        :raises OverLimit: If a 413 response code is received and over_limit is
+                           not in the response body
+        :raises RateLimitExceeded: If a 413 response code is received and
+                                   over_limit is in the response body
+        :raises ServerFault: If a 500 response code is received
+        :raises Unauthorized: If a 401 response code is received
+        :raises UnexpectedContentType: If the content-type of the response
+                                       isn't an expect type
+        :raises UnexpectedResponseCode: If a response code above 400 is
+                                        received and it doesn't fall into any
+                                        of the handled checks
+        :raises UnprocessableEntity: If a 422 response code is received and
+                                     couldn't be parsed
+        :returns: An appropriate object.
+        """
+        if parent_id:
+            uri = self.uri.format(parent=parent_id)
+        else:
+            uri = self.uri
+
+        if query_params:
+            request_uri = '{0}/{1}?{2}'.format(uri, obj_id, query_params)
+        else:
+            request_uri = '{0}/{1}'.format(uri, obj_id)
+
+        response, body = self.get(request_uri)
+        self.expected_success(200, response.status)
+        if return_object_only:
+            return json.loads(body.decode('utf-8'))[self.root_tag]
+        else:
+            return json.loads(body.decode('utf-8'))
+
+    def _list_objects(self, parent_id=None, query_params=None,
+                      return_object_only=True):
+        """Get a list of the appropriate objects.
+
+        :param query_params: The optional query parameters to append to the
+                             request. Ex. fields=id&fields=name
+        :param return_object_only: If True, the response returns the object
+                                   inside the root tag. False returns the full
+                                   response from the API.
+        :raises AssertionError: if the expected_code isn't a valid http success
+                                response code
+        :raises BadRequest: If a 400 response code is received
+        :raises Conflict: If a 409 response code is received
+        :raises Forbidden: If a 403 response code is received
+        :raises Gone: If a 410 response code is received
+        :raises InvalidContentType: If a 415 response code is received
+        :raises InvalidHTTPResponseBody: The response body wasn't valid JSON
+        :raises InvalidHttpSuccessCode: if the read code isn't an expected
+                                        http success code
+        :raises NotFound: If a 404 response code is received
+        :raises NotImplemented: If a 501 response code is received
+        :raises OverLimit: If a 413 response code is received and over_limit is
+                           not in the response body
+        :raises RateLimitExceeded: If a 413 response code is received and
+                                   over_limit is in the response body
+        :raises ServerFault: If a 500 response code is received
+        :raises Unauthorized: If a 401 response code is received
+        :raises UnexpectedContentType: If the content-type of the response
+                                       isn't an expect type
+        :raises UnexpectedResponseCode: If a response code above 400 is
+                                        received and it doesn't fall into any
+                                        of the handled checks
+        :raises UnprocessableEntity: If a 422 response code is received and
+                                     couldn't be parsed
+        :returns: A list of appropriate objects.
+        """
+        if parent_id:
+            uri = self.uri.format(parent=parent_id)
+        else:
+            uri = self.uri
+
+        if query_params:
+            request_uri = '{0}?{1}'.format(uri, query_params)
+        else:
+            request_uri = uri
+        response, body = self.get(request_uri)
+        self.expected_success(200, response.status)
+        if return_object_only:
+            return json.loads(body.decode('utf-8'))[self.list_root_tag]
+        else:
+            return json.loads(body.decode('utf-8'))
+
+    def _update_object(self, obj_id, parent_id=None, return_object_only=True,
+                       **kwargs):
+        """Update an object.
+
+        :param obj_id: The object ID to update.
+        :param parent_id: The parent object ID, if applicable.
+        :param return_object_only: If True, the response returns the object
+                                   inside the root tag. False returns the full
+                                   response from the API.
+        :param **kwargs: All attributes of the object should be passed as
+                         keyword arguments to this function.
+        :raises AssertionError: if the expected_code isn't a valid http success
+                                response code
+        :raises BadRequest: If a 400 response code is received
+        :raises Conflict: If a 409 response code is received
+        :raises Forbidden: If a 403 response code is received
+        :raises Gone: If a 410 response code is received
+        :raises InvalidContentType: If a 415 response code is received
+        :raises InvalidHTTPResponseBody: The response body wasn't valid JSON
+        :raises InvalidHttpSuccessCode: if the read code isn't an expected
+                                        http success code
+        :raises NotFound: If a 404 response code is received
+        :raises NotImplemented: If a 501 response code is received
+        :raises OverLimit: If a 413 response code is received and over_limit is
+                           not in the response body
+        :raises RateLimitExceeded: If a 413 response code is received and
+                                   over_limit is in the response body
+        :raises ServerFault: If a 500 response code is received
+        :raises Unauthorized: If a 401 response code is received
+        :raises UnexpectedContentType: If the content-type of the response
+                                       isn't an expect type
+        :raises UnexpectedResponseCode: If a response code above 400 is
+                                        received and it doesn't fall into any
+                                        of the handled checks
+        :raises UnprocessableEntity: If a 422 response code is received and
+                                     couldn't be parsed
+        :returns: An appropriate object.
+        """
+        obj_dict = {self.root_tag: kwargs}
+
+        if parent_id:
+            uri = self.uri.format(parent=parent_id)
+        else:
+            uri = self.uri
+
+        request_uri = '{0}/{1}'.format(uri, obj_id)
+        response, body = self.put(request_uri, json.dumps(obj_dict))
+        self.expected_success(200, response.status)
+        if return_object_only:
+            return json.loads(body.decode('utf-8'))[self.root_tag]
+        else:
+            return json.loads(body.decode('utf-8'))
+
+    def _delete_obj(self, obj_id, parent_id=None, ignore_errors=False,
+                    cascade=False):
+        """Delete an object.
+
+        :param obj_id: The object ID to delete.
+        :param ignore_errors: True if errors should be ignored.
+        :param cascade: If true will delete all child objects of an
+                        object, if that object supports it.
+        :raises AssertionError: if the expected_code isn't a valid http success
+                                response code
+        :raises BadRequest: If a 400 response code is received
+        :raises Conflict: If a 409 response code is received
+        :raises Forbidden: If a 403 response code is received
+        :raises Gone: If a 410 response code is received
+        :raises InvalidContentType: If a 415 response code is received
+        :raises InvalidHTTPResponseBody: The response body wasn't valid JSON
+        :raises InvalidHttpSuccessCode: if the read code isn't an expected
+                                        http success code
+        :raises NotFound: If a 404 response code is received
+        :raises NotImplemented: If a 501 response code is received
+        :raises OverLimit: If a 413 response code is received and over_limit is
+                           not in the response body
+        :raises RateLimitExceeded: If a 413 response code is received and
+                                   over_limit is in the response body
+        :raises ServerFault: If a 500 response code is received
+        :raises Unauthorized: If a 401 response code is received
+        :raises UnexpectedContentType: If the content-type of the response
+                                       isn't an expect type
+        :raises UnexpectedResponseCode: If a response code above 400 is
+                                        received and it doesn't fall into any
+                                        of the handled checks
+        :raises UnprocessableEntity: If a 422 response code is received and
+                                     couldn't be parsed
+        :returns: None if ignore_errors is True, the response status code
+                  if not.
+        """
+        if parent_id:
+            uri = self.uri.format(parent=parent_id)
+        else:
+            uri = self.uri
+
+        if cascade:
+            request_uri = '{0}/{1}?cascade=true'.format(uri, obj_id)
+        else:
+            request_uri = '{0}/{1}'.format(uri, obj_id)
+        if ignore_errors:
+            try:
+                response, body = self.delete(request_uri)
+            except Exception:
+                return
+        else:
+            response, body = self.delete(request_uri)
+
+        self.expected_success(204, response.status)
+        return response.status
+
+    def _cleanup_obj(self, obj_id, lb_client=None, lb_id=None, parent_id=None):
+        """Clean up an object (for use in tempest addClassResourceCleanup).
+
+        We always need to wait for the parent LB to be in a mutable state
+        before deleting the child object, and the cleanups will not guarantee
+        this if we just pass the delete function to tempest cleanup.
+        For example, if we add multiple listeners on the same LB to cleanup,
+        tempest will delete the first one and then immediately try to delete
+        the second one, which will fail because the LB will be immutable.
+
+        We also need to wait to return until the parent LB is back in a mutable
+        state so future tests don't break right at the start.
+
+        This function:
+        * Waits until the parent LB is ACTIVE
+        * Deletes the object
+        * Waits until the parent LB is ACTIVE
+
+        :param obj_id: The object ID to clean up.
+        :param lb_client: (Optional) The loadbalancer client, if this isn't the
+                          loadbalancer client already.
+        :param lb_id: (Optional) The ID of the parent loadbalancer, if the main
+                      obj_id is for a sub-object and not a loadbalancer.
+        :return:
+        """
+        if parent_id:
+            uri = self.uri.format(parent=parent_id)
+        else:
+            uri = self.uri
+
+        if lb_client and lb_id:
+            wait_id = lb_id
+            wait_client = lb_client
+            wait_func = lb_client.show_loadbalancer
+        else:
+            wait_id = obj_id
+            wait_client = self
+            wait_func = self._show_object
+
+        LOG.info("Starting cleanup for %s %s...", self.root_tag, obj_id)
+
+        try:
+            request_uri = '{0}/{1}'.format(uri, obj_id)
+            response, body = self.get(request_uri)
+            resp_obj = json.loads(body.decode('utf-8'))[self.root_tag]
+            if (response.status == 404 or
+                    resp_obj['provisioning_status'] == const.DELETED):
+                raise exceptions.NotFound()
+        except exceptions.NotFound:
+            # Already gone, cleanup complete
+            LOG.info("%s %s is already gone. Cleanup considered complete.",
+                     self.root_tag, obj_id)
+            return
+
+        LOG.info("Waiting for %s %s to be ACTIVE...",
+                 wait_client.root_tag, wait_id)
+        try:
+            waiters.wait_for_status(wait_func, wait_id,
+                                    const.PROVISIONING_STATUS,
+                                    const.ACTIVE,
+                                    self.build_interval,
+                                    self.timeout)
+        except exceptions.UnexpectedResponseCode:
+            # Status is ERROR, go ahead with deletion
+            LOG.debug("Found %s %s in ERROR status, proceeding with cleanup.",
+                      wait_client.root_tag, wait_id)
+        except exceptions.TimeoutException:
+            # Timed out, nothing to be done, let errors happen
+            LOG.error("Timeout exceeded waiting to clean up %s %s.",
+                      self.root_tag, obj_id)
+        except exceptions.NotFound:
+            # Already gone, cleanup complete
+            LOG.info("%s %s is already gone. Cleanup considered complete.",
+                     wait_client.root_tag.capitalize(), wait_id)
+            return
+        except Exception as e:
+            # Log that something weird happens, then let the chips fall
+            LOG.error("Cleanup encountered an unknown exception while waiting "
+                      "for %s %s: %s", wait_client.root_tag, wait_id, e)
+
+        uri = '{0}/{1}'.format(uri, obj_id)
+        LOG.info("Cleaning up %s %s...", self.root_tag, obj_id)
+        return_status = test_utils.call_and_ignore_notfound_exc(
+            self.delete, uri)
+
+        if lb_id and lb_client:
+            LOG.info("Waiting for %s %s to be ACTIVE...",
+                     wait_client.root_tag, wait_id)
+            waiters.wait_for_status(wait_func, wait_id,
+                                    const.PROVISIONING_STATUS,
+                                    const.ACTIVE,
+                                    self.build_interval,
+                                    self.timeout)
+
+        LOG.info("Cleanup complete for %s %s...", self.root_tag, obj_id)
+        return return_status
+
+    def is_resource_deleted(self, id):
+        """Check if the object is deleted.
+
+        :param id: The object ID to check.
+        :return: boolean state representing the object's deleted state
+        """
+        try:
+            obj = self._show_object(id)
+            if obj.get(const.PROVISIONING_STATUS) == const.DELETED:
+                return True
+        except exceptions.NotFound:
+            return True
+        return False
+
+    def get_max_api_version(self):
+        """Get the maximum version available on the API endpoint.
+
+        :return: Maximum version string available on the endpoint.
+        """
+        response, body = self.get('/')
+        self.expected_success(200, response.status)
+
+        versions_list = json.loads(body.decode('utf-8'))['versions']
+        current_versions = (version for version in versions_list if
+                            version['status'] == 'CURRENT')
+        max_version = '0.0'
+        for version in current_versions:
+
+            ver_string = version['id']
+            if ver_string.startswith("v"):
+                ver_string = ver_string[1:]
+
+            ver_split = list(map(int, ver_string.split('.')))
+            max_split = list(map(int, max_version.split('.')))
+
+            if len(ver_split) > 2:
+                raise exceptions.InvalidAPIVersionString(version=ver_string)
+
+            if ver_split[0] > max_split[0] or (
+                    ver_split[0] == max_split[0] and
+                    ver_split[1] >= max_split[1]):
+                max_version = ver_string
+
+        if max_version == '0.0':
+            raise exceptions.InvalidAPIVersionString(version=max_version)
+
+        return max_version
+
+    def is_version_supported(self, api_version, version):
+        """Check if a version is supported by the API.
+
+        :param api_version: Reference endpoint API version.
+        :param version: Version to check against API version.
+        :return: boolean if the version is supported.
+        """
+
+        api_split = list(map(int, api_version.split('.')))
+        ver_split = list(map(int, version.split('.')))
+
+        if api_split[0] > ver_split[0] or (
+                api_split[0] == ver_split[0] and api_split[1] >= ver_split[1]):
+            return True
+        return False
diff --git a/octavia_tempest_plugin/services/load_balancer/v2/healthmonitor_client.py b/octavia_tempest_plugin/services/load_balancer/v2/healthmonitor_client.py
new file mode 100644
index 0000000..70dce4c
--- /dev/null
+++ b/octavia_tempest_plugin/services/load_balancer/v2/healthmonitor_client.py
@@ -0,0 +1,263 @@
+#   Copyright 2018 GoDaddy
+#
+#   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 tempest import config
+
+from octavia_tempest_plugin.services.load_balancer.v2 import base_client
+
+CONF = config.CONF
+Unset = base_client.Unset
+
+
+class HealthMonitorClient(base_client.BaseLBaaSClient):
+
+    root_tag = 'healthmonitor'
+    list_root_tag = 'healthmonitors'
+    resource_name = 'healthmonitor'
+
+    def create_healthmonitor(self, pool_id, type, delay, timeout, max_retries,
+                             max_retries_down=Unset, name=Unset,
+                             http_method=Unset, url_path=Unset,
+                             expected_codes=Unset, admin_state_up=Unset,
+                             return_object_only=True):
+        """Create a healthmonitor.
+
+        :param pool_id: The ID of the pool.
+        :param type: The type of health monitor.
+        :param delay: The time, in seconds, between sending probes to members.
+        :param timeout: The maximum time, in seconds, that a monitor waits to
+                        connect before it times out.
+        :param max_retries: The number of successful checks before changing the
+                            operating status of the member to ONLINE.
+        :param max_retries_down: The number of allowed check failures before
+                                 changing the operating status of the member to
+                                 ERROR.
+        :param name: Human-readable name of the resource.
+        :param http_method: The HTTP method that the health monitor uses for
+                            requests.
+        :param url_path: The HTTP URL path of the request sent by the monitor
+                         to test the health of a backend member.
+        :param expected_codes: The list of HTTP status codes expected in
+                               response from the member to declare it healthy.
+        :param admin_state_up: The administrative state of the resource, which
+                               is up (true) or down (false).
+        :param return_object_only: If True, the response returns the object
+                                   inside the root tag. False returns the full
+                                   response from the API.
+        :raises AssertionError: if the expected_code isn't a valid http success
+                                response code
+        :raises BadRequest: If a 400 response code is received
+        :raises Conflict: If a 409 response code is received
+        :raises Forbidden: If a 403 response code is received
+        :raises Gone: If a 410 response code is received
+        :raises InvalidContentType: If a 415 response code is received
+        :raises InvalidHTTPResponseBody: The response body wasn't valid JSON
+        :raises InvalidHttpSuccessCode: if the read code isn't an expected
+                                        http success code
+        :raises NotFound: If a 404 response code is received
+        :raises NotImplemented: If a 501 response code is received
+        :raises OverLimit: If a 413 response code is received and over_limit is
+                           not in the response body
+        :raises RateLimitExceeded: If a 413 response code is received and
+                                   over_limit is in the response body
+        :raises ServerFault: If a 500 response code is received
+        :raises Unauthorized: If a 401 response code is received
+        :raises UnexpectedContentType: If the content-type of the response
+                                       isn't an expect type
+        :raises UnexpectedResponseCode: If a response code above 400 is
+                                        received and it doesn't fall into any
+                                        of the handled checks
+        :raises UnprocessableEntity: If a 422 response code is received and
+                                     couldn't be parsed
+        :returns: A healthmonitor object.
+        """
+        kwargs = {arg: value for arg, value in locals().items()
+                  if arg != 'self' and value is not Unset}
+        return self._create_object(**kwargs)
+
+    def show_healthmonitor(self, healthmonitor_id, query_params=None,
+                           return_object_only=True):
+        """Get healthmonitor details.
+
+        :param healthmonitor_id: The healthmonitor ID to query.
+        :param query_params: The optional query parameters to append to the
+                             request. Ex. fields=id&fields=name
+        :param return_object_only: If True, the response returns the object
+                                   inside the root tag. False returns the full
+                                   response from the API.
+        :raises AssertionError: if the expected_code isn't a valid http success
+                                response code
+        :raises BadRequest: If a 400 response code is received
+        :raises Conflict: If a 409 response code is received
+        :raises Forbidden: If a 403 response code is received
+        :raises Gone: If a 410 response code is received
+        :raises InvalidContentType: If a 415 response code is received
+        :raises InvalidHTTPResponseBody: The response body wasn't valid JSON
+        :raises InvalidHttpSuccessCode: if the read code isn't an expected
+                                        http success code
+        :raises NotFound: If a 404 response code is received
+        :raises NotImplemented: If a 501 response code is received
+        :raises OverLimit: If a 413 response code is received and over_limit is
+                           not in the response body
+        :raises RateLimitExceeded: If a 413 response code is received and
+                                   over_limit is in the response body
+        :raises ServerFault: If a 500 response code is received
+        :raises Unauthorized: If a 401 response code is received
+        :raises UnexpectedContentType: If the content-type of the response
+                                       isn't an expect type
+        :raises UnexpectedResponseCode: If a response code above 400 is
+                                        received and it doesn't fall into any
+                                        of the handled checks
+        :raises UnprocessableEntity: If a 422 response code is received and
+                                     couldn't be parsed
+        :returns: A healthmonitor object.
+        """
+        return self._show_object(obj_id=healthmonitor_id,
+                                 query_params=query_params,
+                                 return_object_only=return_object_only)
+
+    def list_healthmonitors(self, query_params=None, return_object_only=True):
+        """Get a list of healthmonitor objects.
+
+        :param query_params: The optional query parameters to append to the
+                             request. Ex. fields=id&fields=name
+        :param return_object_only: If True, the response returns the object
+                                   inside the root tag. False returns the full
+                                   response from the API.
+        :raises AssertionError: if the expected_code isn't a valid http success
+                                response code
+        :raises BadRequest: If a 400 response code is received
+        :raises Conflict: If a 409 response code is received
+        :raises Forbidden: If a 403 response code is received
+        :raises Gone: If a 410 response code is received
+        :raises InvalidContentType: If a 415 response code is received
+        :raises InvalidHTTPResponseBody: The response body wasn't valid JSON
+        :raises InvalidHttpSuccessCode: if the read code isn't an expected
+                                        http success code
+        :raises NotFound: If a 404 response code is received
+        :raises NotImplemented: If a 501 response code is received
+        :raises OverLimit: If a 413 response code is received and over_limit is
+                           not in the response body
+        :raises RateLimitExceeded: If a 413 response code is received and
+                                   over_limit is in the response body
+        :raises ServerFault: If a 500 response code is received
+        :raises Unauthorized: If a 401 response code is received
+        :raises UnexpectedContentType: If the content-type of the response
+                                       isn't an expect type
+        :raises UnexpectedResponseCode: If a response code above 400 is
+                                        received and it doesn't fall into any
+                                        of the handled checks
+        :raises UnprocessableEntity: If a 422 response code is received and
+                                     couldn't be parsed
+        :returns: A list of healthmonitor objects.
+        """
+        return self._list_objects(query_params=query_params,
+                                  return_object_only=return_object_only)
+
+    def update_healthmonitor(self, healthmonitor_id, delay=Unset,
+                             timeout=Unset, max_retries=Unset,
+                             max_retries_down=Unset, name=Unset,
+                             http_method=Unset, url_path=Unset,
+                             expected_codes=Unset, admin_state_up=Unset,
+                             return_object_only=True):
+        """Update a healthmonitor.
+
+        :param healthmonitor_id: The healthmonitor ID to update.
+        :param delay: The time, in seconds, between sending probes to members.
+        :param timeout: The maximum time, in seconds, that a monitor waits to
+                        connect before it times out.
+        :param max_retries: The number of successful checks before changing the
+                            operating status of the member to ONLINE.
+        :param max_retries_down: The number of allowed check failures before
+                                 changing the operating status of the member to
+                                 ERROR.
+        :param name: Human-readable name of the resource.
+        :param http_method: The HTTP method that the health monitor uses for
+                            requests.
+        :param url_path: The HTTP URL path of the request sent by the monitor
+                         to test the health of a backend member.
+        :param expected_codes: The list of HTTP status codes expected in
+                               response from the member to declare it healthy.
+        :param admin_state_up: The administrative state of the resource, which
+                               is up (true) or down (false).
+        :param return_object_only: If True, the response returns the object
+                                   inside the root tag. False returns the full
+                                   response from the API.
+        :raises AssertionError: if the expected_code isn't a valid http success
+                                response code
+        :raises BadRequest: If a 400 response code is received
+        :raises Conflict: If a 409 response code is received
+        :raises Forbidden: If a 403 response code is received
+        :raises Gone: If a 410 response code is received
+        :raises InvalidContentType: If a 415 response code is received
+        :raises InvalidHTTPResponseBody: The response body wasn't valid JSON
+        :raises InvalidHttpSuccessCode: if the read code isn't an expected
+                                        http success code
+        :raises NotFound: If a 404 response code is received
+        :raises NotImplemented: If a 501 response code is received
+        :raises OverLimit: If a 413 response code is received and over_limit is
+                           not in the response body
+        :raises RateLimitExceeded: If a 413 response code is received and
+                                   over_limit is in the response body
+        :raises ServerFault: If a 500 response code is received
+        :raises Unauthorized: If a 401 response code is received
+        :raises UnexpectedContentType: If the content-type of the response
+                                       isn't an expect type
+        :raises UnexpectedResponseCode: If a response code above 400 is
+                                        received and it doesn't fall into any
+                                        of the handled checks
+        :raises UnprocessableEntity: If a 422 response code is received and
+                                     couldn't be parsed
+        :returns: A healthmonitor object.
+        """
+        kwargs = {arg: value for arg, value in locals().items()
+                  if arg != 'self' and value is not Unset}
+        kwargs['obj_id'] = kwargs.pop('healthmonitor_id')
+        return self._update_object(**kwargs)
+
+    def delete_healthmonitor(self, healthmonitor_id, ignore_errors=False):
+        """Delete a healthmonitor.
+
+        :param healthmonitor_id: The healthmonitor ID to delete.
+        :param ignore_errors: True if errors should be ignored.
+        :raises AssertionError: if the expected_code isn't a valid http success
+                                response code
+        :raises BadRequest: If a 400 response code is received
+        :raises Conflict: If a 409 response code is received
+        :raises Forbidden: If a 403 response code is received
+        :raises Gone: If a 410 response code is received
+        :raises InvalidContentType: If a 415 response code is received
+        :raises InvalidHTTPResponseBody: The response body wasn't valid JSON
+        :raises InvalidHttpSuccessCode: if the read code isn't an expected
+                                        http success code
+        :raises NotFound: If a 404 response code is received
+        :raises NotImplemented: If a 501 response code is received
+        :raises OverLimit: If a 413 response code is received and over_limit is
+                           not in the response body
+        :raises RateLimitExceeded: If a 413 response code is received and
+                                   over_limit is in the response body
+        :raises ServerFault: If a 500 response code is received
+        :raises Unauthorized: If a 401 response code is received
+        :raises UnexpectedContentType: If the content-type of the response
+                                       isn't an expect type
+        :raises UnexpectedResponseCode: If a response code above 400 is
+                                        received and it doesn't fall into any
+                                        of the handled checks
+        :raises UnprocessableEntity: If a 422 response code is received and
+                                     couldn't be parsed
+        :returns: None if ignore_errors is True, the response status code
+                  if not.
+        """
+        return self._delete_obj(obj_id=healthmonitor_id,
+                                ignore_errors=ignore_errors)
diff --git a/octavia_tempest_plugin/services/load_balancer/v2/l7policy_client.py b/octavia_tempest_plugin/services/load_balancer/v2/l7policy_client.py
new file mode 100644
index 0000000..674ec02
--- /dev/null
+++ b/octavia_tempest_plugin/services/load_balancer/v2/l7policy_client.py
@@ -0,0 +1,245 @@
+#   Copyright 2018 GoDaddy
+#
+#   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 tempest import config
+
+from octavia_tempest_plugin.services.load_balancer.v2 import base_client
+
+CONF = config.CONF
+Unset = base_client.Unset
+
+
+class L7PolicyClient(base_client.BaseLBaaSClient):
+
+    root_tag = 'l7policy'
+    list_root_tag = 'l7policies'
+    resource_name = 'l7policy'
+
+    def create_l7policy(self, listener_id, action, name=Unset,
+                        description=Unset, admin_state_up=Unset,
+                        position=Unset, redirect_pool_id=Unset,
+                        redirect_url=Unset, return_object_only=True):
+        """Create a l7policy.
+
+        :param listener_id: The ID of the listener for the l7policy.
+        :param action: The l7policy action.
+        :param name: Human-readable name of the resource.
+        :param description: A human-readable description for the resource.
+        :param admin_state_up: The administrative state of the resource, which
+                               is up (true) or down (false).
+        :param position: The position of this policy on the listener.
+        :param redirect_pool_id: Requests matching this policy will be
+                                 redirected to the pool with this ID.
+        :param redirect_url: Requests matching this policy will be redirected
+                             to this URL.
+        :param return_object_only: If True, the response returns the object
+                                   inside the root tag. False returns the full
+                                   response from the API.
+        :raises AssertionError: if the expected_code isn't a valid http success
+                                response code
+        :raises BadRequest: If a 400 response code is received
+        :raises Conflict: If a 409 response code is received
+        :raises Forbidden: If a 403 response code is received
+        :raises Gone: If a 410 response code is received
+        :raises InvalidContentType: If a 415 response code is received
+        :raises InvalidHTTPResponseBody: The response body wasn't valid JSON
+        :raises InvalidHttpSuccessCode: if the read code isn't an expected
+                                        http success code
+        :raises NotFound: If a 404 response code is received
+        :raises NotImplemented: If a 501 response code is received
+        :raises OverLimit: If a 413 response code is received and over_limit is
+                           not in the response body
+        :raises RateLimitExceeded: If a 413 response code is received and
+                                   over_limit is in the response body
+        :raises ServerFault: If a 500 response code is received
+        :raises Unauthorized: If a 401 response code is received
+        :raises UnexpectedContentType: If the content-type of the response
+                                       isn't an expect type
+        :raises UnexpectedResponseCode: If a response code above 400 is
+                                        received and it doesn't fall into any
+                                        of the handled checks
+        :raises UnprocessableEntity: If a 422 response code is received and
+                                     couldn't be parsed
+        :returns: A l7policy object.
+        """
+        kwargs = {arg: value for arg, value in locals().items()
+                  if arg != 'self' and value is not Unset}
+        return self._create_object(**kwargs)
+
+    def show_l7policy(self, l7policy_id, query_params=None,
+                      return_object_only=True):
+        """Get l7policy details.
+
+        :param l7policy_id: The l7policy ID to query.
+        :param query_params: The optional query parameters to append to the
+                             request. Ex. fields=id&fields=name
+        :param return_object_only: If True, the response returns the object
+                                   inside the root tag. False returns the full
+                                   response from the API.
+        :raises AssertionError: if the expected_code isn't a valid http success
+                                response code
+        :raises BadRequest: If a 400 response code is received
+        :raises Conflict: If a 409 response code is received
+        :raises Forbidden: If a 403 response code is received
+        :raises Gone: If a 410 response code is received
+        :raises InvalidContentType: If a 415 response code is received
+        :raises InvalidHTTPResponseBody: The response body wasn't valid JSON
+        :raises InvalidHttpSuccessCode: if the read code isn't an expected
+                                        http success code
+        :raises NotFound: If a 404 response code is received
+        :raises NotImplemented: If a 501 response code is received
+        :raises OverLimit: If a 413 response code is received and over_limit is
+                           not in the response body
+        :raises RateLimitExceeded: If a 413 response code is received and
+                                   over_limit is in the response body
+        :raises ServerFault: If a 500 response code is received
+        :raises Unauthorized: If a 401 response code is received
+        :raises UnexpectedContentType: If the content-type of the response
+                                       isn't an expect type
+        :raises UnexpectedResponseCode: If a response code above 400 is
+                                        received and it doesn't fall into any
+                                        of the handled checks
+        :raises UnprocessableEntity: If a 422 response code is received and
+                                     couldn't be parsed
+        :returns: A l7policy object.
+        """
+        return self._show_object(obj_id=l7policy_id,
+                                 query_params=query_params,
+                                 return_object_only=return_object_only)
+
+    def list_l7policies(self, query_params=None, return_object_only=True):
+        """Get a list of l7policy objects.
+
+        :param query_params: The optional query parameters to append to the
+                             request. Ex. fields=id&fields=name
+        :param return_object_only: If True, the response returns the object
+                                   inside the root tag. False returns the full
+                                   response from the API.
+        :raises AssertionError: if the expected_code isn't a valid http success
+                                response code
+        :raises BadRequest: If a 400 response code is received
+        :raises Conflict: If a 409 response code is received
+        :raises Forbidden: If a 403 response code is received
+        :raises Gone: If a 410 response code is received
+        :raises InvalidContentType: If a 415 response code is received
+        :raises InvalidHTTPResponseBody: The response body wasn't valid JSON
+        :raises InvalidHttpSuccessCode: if the read code isn't an expected
+                                        http success code
+        :raises NotFound: If a 404 response code is received
+        :raises NotImplemented: If a 501 response code is received
+        :raises OverLimit: If a 413 response code is received and over_limit is
+                           not in the response body
+        :raises RateLimitExceeded: If a 413 response code is received and
+                                   over_limit is in the response body
+        :raises ServerFault: If a 500 response code is received
+        :raises Unauthorized: If a 401 response code is received
+        :raises UnexpectedContentType: If the content-type of the response
+                                       isn't an expect type
+        :raises UnexpectedResponseCode: If a response code above 400 is
+                                        received and it doesn't fall into any
+                                        of the handled checks
+        :raises UnprocessableEntity: If a 422 response code is received and
+                                     couldn't be parsed
+        :returns: A list of l7policy objects.
+        """
+        return self._list_objects(query_params=query_params,
+                                  return_object_only=return_object_only)
+
+    def update_l7policy(self, l7policy_id, action=Unset, name=Unset,
+                        description=Unset, admin_state_up=Unset,
+                        position=Unset, redirect_pool_id=Unset,
+                        redirect_url=Unset, return_object_only=True):
+        """Update a l7policy.
+
+        :param l7policy_id: The l7policy ID to update.
+        :param action: The l7policy action.
+        :param name: Human-readable name of the resource.
+        :param description: A human-readable description for the resource.
+        :param admin_state_up: The administrative state of the resource, which
+                               is up (true) or down (false).
+        :param position: The position of this policy on the listener.
+        :param redirect_pool_id: Requests matching this policy will be
+                                 redirected to the pool with this ID.
+        :param redirect_url: Requests matching this policy will be redirected
+                             to this URL.
+        :param return_object_only: If True, the response returns the object
+                                   inside the root tag. False returns the full
+                                   response from the API.
+        :raises AssertionError: if the expected_code isn't a valid http success
+                                response code
+        :raises BadRequest: If a 400 response code is received
+        :raises Conflict: If a 409 response code is received
+        :raises Forbidden: If a 403 response code is received
+        :raises Gone: If a 410 response code is received
+        :raises InvalidContentType: If a 415 response code is received
+        :raises InvalidHTTPResponseBody: The response body wasn't valid JSON
+        :raises InvalidHttpSuccessCode: if the read code isn't an expected
+                                        http success code
+        :raises NotFound: If a 404 response code is received
+        :raises NotImplemented: If a 501 response code is received
+        :raises OverLimit: If a 413 response code is received and over_limit is
+                           not in the response body
+        :raises RateLimitExceeded: If a 413 response code is received and
+                                   over_limit is in the response body
+        :raises ServerFault: If a 500 response code is received
+        :raises Unauthorized: If a 401 response code is received
+        :raises UnexpectedContentType: If the content-type of the response
+                                       isn't an expect type
+        :raises UnexpectedResponseCode: If a response code above 400 is
+                                        received and it doesn't fall into any
+                                        of the handled checks
+        :raises UnprocessableEntity: If a 422 response code is received and
+                                     couldn't be parsed
+        :returns: A l7policy object.
+        """
+        kwargs = {arg: value for arg, value in locals().items()
+                  if arg != 'self' and value is not Unset}
+        kwargs['obj_id'] = kwargs.pop('l7policy_id')
+        return self._update_object(**kwargs)
+
+    def delete_l7policy(self, l7policy_id, ignore_errors=False):
+        """Delete a l7policy.
+
+        :param l7policy_id: The l7policy ID to delete.
+        :param ignore_errors: True if errors should be ignored.
+        :raises AssertionError: if the expected_code isn't a valid http success
+                                response code
+        :raises BadRequest: If a 400 response code is received
+        :raises Conflict: If a 409 response code is received
+        :raises Forbidden: If a 403 response code is received
+        :raises Gone: If a 410 response code is received
+        :raises InvalidContentType: If a 415 response code is received
+        :raises InvalidHTTPResponseBody: The response body wasn't valid JSON
+        :raises InvalidHttpSuccessCode: if the read code isn't an expected
+                                        http success code
+        :raises NotFound: If a 404 response code is received
+        :raises NotImplemented: If a 501 response code is received
+        :raises OverLimit: If a 413 response code is received and over_limit is
+                           not in the response body
+        :raises RateLimitExceeded: If a 413 response code is received and
+                                   over_limit is in the response body
+        :raises ServerFault: If a 500 response code is received
+        :raises Unauthorized: If a 401 response code is received
+        :raises UnexpectedContentType: If the content-type of the response
+                                       isn't an expect type
+        :raises UnexpectedResponseCode: If a response code above 400 is
+                                        received and it doesn't fall into any
+                                        of the handled checks
+        :raises UnprocessableEntity: If a 422 response code is received and
+                                     couldn't be parsed
+        :returns: None if ignore_errors is True, the response status code
+                  if not.
+        """
+        return self._delete_obj(obj_id=l7policy_id,
+                                ignore_errors=ignore_errors)
diff --git a/octavia_tempest_plugin/services/load_balancer/v2/l7rule_client.py b/octavia_tempest_plugin/services/load_balancer/v2/l7rule_client.py
new file mode 100644
index 0000000..2ca1c71
--- /dev/null
+++ b/octavia_tempest_plugin/services/load_balancer/v2/l7rule_client.py
@@ -0,0 +1,273 @@
+#   Copyright 2018 GoDaddy
+#
+#   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 tempest import config
+
+from octavia_tempest_plugin.services.load_balancer.v2 import base_client
+from octavia_tempest_plugin.services.load_balancer.v2 import l7policy_client
+
+CONF = config.CONF
+Unset = base_client.Unset
+
+
+class L7RuleClient(base_client.BaseLBaaSClient):
+
+    root_tag = 'rule'
+    list_root_tag = 'rules'
+    resource_name = 'l7rule'
+
+    def __init__(self, *args, **kwargs):
+        super(L7RuleClient, self).__init__(*args, **kwargs)
+        l7policy_list_root_tag = l7policy_client.L7PolicyClient.list_root_tag
+        # /v2.0/lbaas/l7policies/<L7POLICY_UUID>/rules
+        self.uri = "{l7policy_base_uri}/{parent}/{object}".format(
+            l7policy_base_uri=self.base_uri.format(
+                object=l7policy_list_root_tag),
+            parent="{parent}",
+            object=self.list_root_tag
+        )
+
+    def create_l7rule(self, l7policy_id, type, value, compare_type,
+                      admin_state_up=Unset, key=Unset, invert=Unset,
+                      return_object_only=True):
+        """Create a l7rule.
+
+        :param l7policy_id: The ID of the l7policy for the l7rule.
+        :param type: The L7 rule type.
+        :param value: The value to use for the comparison.
+        :param compare_type: The comparison type for the L7 rule.
+        :param admin_state_up: The administrative state of the resource, which
+                               is up (true) or down (false).
+        :param key: The key to use for the comparison.
+        :param invert: When true the logic of the rule is inverted.
+        :param return_object_only: If True, the response returns the object
+                                   inside the root tag. False returns the full
+                                   response from the API.
+        :raises AssertionError: if the expected_code isn't a valid http success
+                                response code
+        :raises BadRequest: If a 400 response code is received
+        :raises Conflict: If a 409 response code is received
+        :raises Forbidden: If a 403 response code is received
+        :raises Gone: If a 410 response code is received
+        :raises InvalidContentType: If a 415 response code is received
+        :raises InvalidHTTPResponseBody: The response body wasn't valid JSON
+        :raises InvalidHttpSuccessCode: if the read code isn't an expected
+                                        http success code
+        :raises NotFound: If a 404 response code is received
+        :raises NotImplemented: If a 501 response code is received
+        :raises OverLimit: If a 413 response code is received and over_limit is
+                           not in the response body
+        :raises RateLimitExceeded: If a 413 response code is received and
+                                   over_limit is in the response body
+        :raises ServerFault: If a 500 response code is received
+        :raises Unauthorized: If a 401 response code is received
+        :raises UnexpectedContentType: If the content-type of the response
+                                       isn't an expect type
+        :raises UnexpectedResponseCode: If a response code above 400 is
+                                        received and it doesn't fall into any
+                                        of the handled checks
+        :raises UnprocessableEntity: If a 422 response code is received and
+                                     couldn't be parsed
+        :returns: A l7rule object.
+        """
+        kwargs = {arg: value for arg, value in locals().items()
+                  if arg != 'self' and value is not Unset}
+        kwargs['parent_id'] = kwargs.pop('l7policy_id')
+        return self._create_object(**kwargs)
+
+    def show_l7rule(self, l7rule_id, l7policy_id, query_params=None,
+                    return_object_only=True):
+        """Get l7rule details.
+
+        :param l7rule_id: The l7rule ID to query.
+        :param l7policy_id: The ID of the l7policy for the l7rule.
+        :param query_params: The optional query parameters to append to the
+                             request. Ex. fields=id&fields=name
+        :param return_object_only: If True, the response returns the object
+                                   inside the root tag. False returns the full
+                                   response from the API.
+        :raises AssertionError: if the expected_code isn't a valid http success
+                                response code
+        :raises BadRequest: If a 400 response code is received
+        :raises Conflict: If a 409 response code is received
+        :raises Forbidden: If a 403 response code is received
+        :raises Gone: If a 410 response code is received
+        :raises InvalidContentType: If a 415 response code is received
+        :raises InvalidHTTPResponseBody: The response body wasn't valid JSON
+        :raises InvalidHttpSuccessCode: if the read code isn't an expected
+                                        http success code
+        :raises NotFound: If a 404 response code is received
+        :raises NotImplemented: If a 501 response code is received
+        :raises OverLimit: If a 413 response code is received and over_limit is
+                           not in the response body
+        :raises RateLimitExceeded: If a 413 response code is received and
+                                   over_limit is in the response body
+        :raises ServerFault: If a 500 response code is received
+        :raises Unauthorized: If a 401 response code is received
+        :raises UnexpectedContentType: If the content-type of the response
+                                       isn't an expect type
+        :raises UnexpectedResponseCode: If a response code above 400 is
+                                        received and it doesn't fall into any
+                                        of the handled checks
+        :raises UnprocessableEntity: If a 422 response code is received and
+                                     couldn't be parsed
+        :returns: A l7rule object.
+        """
+        return self._show_object(obj_id=l7rule_id,
+                                 parent_id=l7policy_id,
+                                 query_params=query_params,
+                                 return_object_only=return_object_only)
+
+    def list_l7rules(self, l7policy_id, query_params=None,
+                     return_object_only=True):
+        """Get a list of l7rule objects.
+
+        :param l7policy_id: The ID of the l7policy for the l7rule.
+        :param query_params: The optional query parameters to append to the
+                             request. Ex. fields=id&fields=name
+        :param return_object_only: If True, the response returns the object
+                                   inside the root tag. False returns the full
+                                   response from the API.
+        :raises AssertionError: if the expected_code isn't a valid http success
+                                response code
+        :raises BadRequest: If a 400 response code is received
+        :raises Conflict: If a 409 response code is received
+        :raises Forbidden: If a 403 response code is received
+        :raises Gone: If a 410 response code is received
+        :raises InvalidContentType: If a 415 response code is received
+        :raises InvalidHTTPResponseBody: The response body wasn't valid JSON
+        :raises InvalidHttpSuccessCode: if the read code isn't an expected
+                                        http success code
+        :raises NotFound: If a 404 response code is received
+        :raises NotImplemented: If a 501 response code is received
+        :raises OverLimit: If a 413 response code is received and over_limit is
+                           not in the response body
+        :raises RateLimitExceeded: If a 413 response code is received and
+                                   over_limit is in the response body
+        :raises ServerFault: If a 500 response code is received
+        :raises Unauthorized: If a 401 response code is received
+        :raises UnexpectedContentType: If the content-type of the response
+                                       isn't an expect type
+        :raises UnexpectedResponseCode: If a response code above 400 is
+                                        received and it doesn't fall into any
+                                        of the handled checks
+        :raises UnprocessableEntity: If a 422 response code is received and
+                                     couldn't be parsed
+        :returns: A list of l7rule objects.
+        """
+        return self._list_objects(parent_id=l7policy_id,
+                                  query_params=query_params,
+                                  return_object_only=return_object_only)
+
+    def update_l7rule(self, l7rule_id, l7policy_id, type=Unset, value=Unset,
+                      compare_type=Unset, admin_state_up=Unset, key=Unset,
+                      invert=Unset, return_object_only=True):
+        """Update a l7rule.
+
+        :param l7rule_id: The l7rule ID to update.
+        :param l7policy_id: The ID of the l7policy for the l7rule.
+        :param type: The L7 rule type.
+        :param value: The value to use for the comparison.
+        :param compare_type: The comparison type for the L7 rule.
+        :param admin_state_up: The administrative state of the resource, which
+                               is up (true) or down (false).
+        :param key: The key to use for the comparison.
+        :param invert: When true the logic of the rule is inverted.
+        :param return_object_only: If True, the response returns the object
+                                   inside the root tag. False returns the full
+                                   response from the API.
+        :raises AssertionError: if the expected_code isn't a valid http success
+                                response code
+        :raises BadRequest: If a 400 response code is received
+        :raises Conflict: If a 409 response code is received
+        :raises Forbidden: If a 403 response code is received
+        :raises Gone: If a 410 response code is received
+        :raises InvalidContentType: If a 415 response code is received
+        :raises InvalidHTTPResponseBody: The response body wasn't valid JSON
+        :raises InvalidHttpSuccessCode: if the read code isn't an expected
+                                        http success code
+        :raises NotFound: If a 404 response code is received
+        :raises NotImplemented: If a 501 response code is received
+        :raises OverLimit: If a 413 response code is received and over_limit is
+                           not in the response body
+        :raises RateLimitExceeded: If a 413 response code is received and
+                                   over_limit is in the response body
+        :raises ServerFault: If a 500 response code is received
+        :raises Unauthorized: If a 401 response code is received
+        :raises UnexpectedContentType: If the content-type of the response
+                                       isn't an expect type
+        :raises UnexpectedResponseCode: If a response code above 400 is
+                                        received and it doesn't fall into any
+                                        of the handled checks
+        :raises UnprocessableEntity: If a 422 response code is received and
+                                     couldn't be parsed
+        :returns: A l7rule object.
+        """
+        kwargs = {arg: value for arg, value in locals().items()
+                  if arg != 'self' and value is not Unset}
+        kwargs['obj_id'] = kwargs.pop('l7rule_id')
+        kwargs['parent_id'] = kwargs.pop('l7policy_id')
+        return self._update_object(**kwargs)
+
+    def delete_l7rule(self, l7rule_id, l7policy_id, ignore_errors=False):
+        """Delete a l7rule.
+
+        :param l7rule_id: The l7rule ID to delete.
+        :param l7policy_id: The ID of the l7policy for the l7rule.
+        :param ignore_errors: True if errors should be ignored.
+        :raises AssertionError: if the expected_code isn't a valid http success
+                                response code
+        :raises BadRequest: If a 400 response code is received
+        :raises Conflict: If a 409 response code is received
+        :raises Forbidden: If a 403 response code is received
+        :raises Gone: If a 410 response code is received
+        :raises InvalidContentType: If a 415 response code is received
+        :raises InvalidHTTPResponseBody: The response body wasn't valid JSON
+        :raises InvalidHttpSuccessCode: if the read code isn't an expected
+                                        http success code
+        :raises NotFound: If a 404 response code is received
+        :raises NotImplemented: If a 501 response code is received
+        :raises OverLimit: If a 413 response code is received and over_limit is
+                           not in the response body
+        :raises RateLimitExceeded: If a 413 response code is received and
+                                   over_limit is in the response body
+        :raises ServerFault: If a 500 response code is received
+        :raises Unauthorized: If a 401 response code is received
+        :raises UnexpectedContentType: If the content-type of the response
+                                       isn't an expect type
+        :raises UnexpectedResponseCode: If a response code above 400 is
+                                        received and it doesn't fall into any
+                                        of the handled checks
+        :raises UnprocessableEntity: If a 422 response code is received and
+                                     couldn't be parsed
+        :returns: None if ignore_errors is True, the response status code
+                  if not.
+        """
+        return self._delete_obj(obj_id=l7rule_id,
+                                parent_id=l7policy_id,
+                                ignore_errors=ignore_errors)
+
+    def cleanup_l7rule(self, l7rule_id, l7policy_id, lb_client=None,
+                       lb_id=None):
+        kwargs = {arg: value for arg, value in locals().items()
+                  if arg != 'self' and value is not Unset}
+        kwargs['obj_id'] = kwargs.pop('l7rule_id')
+        kwargs['parent_id'] = kwargs.pop('l7policy_id')
+        return self._cleanup_obj(**kwargs)
+
+    def is_resource_deleted(self, id):
+        # Trying to implement this for l7rules would be impossible, because
+        # they are sub-objects that can't be referenced directly, and this is
+        # used internally in tempest where we have no control over passed args
+        raise NotImplementedError()
diff --git a/octavia_tempest_plugin/services/load_balancer/v2/listener_client.py b/octavia_tempest_plugin/services/load_balancer/v2/listener_client.py
new file mode 100644
index 0000000..082f4b7
--- /dev/null
+++ b/octavia_tempest_plugin/services/load_balancer/v2/listener_client.py
@@ -0,0 +1,293 @@
+#   Copyright 2017 GoDaddy
+#
+#   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 tempest import config
+
+from octavia_tempest_plugin.services.load_balancer.v2 import base_client
+
+CONF = config.CONF
+Unset = base_client.Unset
+
+
+class ListenerClient(base_client.BaseLBaaSClient):
+
+    root_tag = 'listener'
+    list_root_tag = 'listeners'
+
+    def create_listener(self, protocol, protocol_port, loadbalancer_id,
+                        name=Unset, description=Unset, admin_state_up=Unset,
+                        connection_limit=Unset, timeout_client_data=Unset,
+                        timeout_member_connect=Unset,
+                        timeout_member_data=Unset, timeout_tcp_inspect=Unset,
+                        insert_headers=Unset, default_pool_id=Unset,
+                        default_tls_container_ref=Unset,
+                        sni_container_refs=Unset, return_object_only=True):
+        """Create a listener.
+
+        :param protocol: The protocol for the resource.
+        :param protocol_port: The protocol port number for the resource.
+        :param loadbalancer_id: The ID of the load balancer.
+        :param name: Human-readable name of the resource.
+        :param description: A human-readable description for the resource.
+        :param admin_state_up: The administrative state of the resource, which
+                               is up (true) or down (false).
+        :param connection_limit: The maximum number of connections permitted
+                                 for this listener. Default value is -1 which
+                                 represents infinite connections.
+        :param timeout_client_data: Frontend client inactivity timeout in
+                                    milliseconds.
+        :param timeout_member_connect: Backend member connection timeout in
+                                       milliseconds.
+        :param timeout_member_data: Backend member inactivity timeout in
+                                    milliseconds.
+        :param timeout_tcp_inspect: Time, in milliseconds, to wait for
+                                    additional TCP packets for content
+                                    inspection.
+        :param insert_headers: A dictionary of optional headers to insert into
+                               the request before it is sent to the backend
+                               member.
+        :param default_pool_id: The ID of the pool used by the listener if no
+                                L7 policies match.
+        :param default_tls_container_ref: The URI of the key manager service
+                                          secret containing a PKCS12 format
+                                          certificate/key bundle for
+                                          TERMINATED_TLS listeners.
+        :param sni_container_refs: A list of URIs to the key manager service
+                                   secrets containing PKCS12 format
+                                   certificate/key bundles for TERMINATED_TLS
+                                   listeners.
+        :param return_object_only: If True, the response returns the object
+                                   inside the root tag. False returns the full
+                                   response from the API.
+        :raises AssertionError: if the expected_code isn't a valid http success
+                                response code
+        :raises BadRequest: If a 400 response code is received
+        :raises Conflict: If a 409 response code is received
+        :raises Forbidden: If a 403 response code is received
+        :raises Gone: If a 410 response code is received
+        :raises InvalidContentType: If a 415 response code is received
+        :raises InvalidHTTPResponseBody: The response body wasn't valid JSON
+        :raises InvalidHttpSuccessCode: if the read code isn't an expected
+                                        http success code
+        :raises NotFound: If a 404 response code is received
+        :raises NotImplemented: If a 501 response code is received
+        :raises OverLimit: If a 413 response code is received and over_limit is
+                           not in the response body
+        :raises RateLimitExceeded: If a 413 response code is received and
+                                   over_limit is in the response body
+        :raises ServerFault: If a 500 response code is received
+        :raises Unauthorized: If a 401 response code is received
+        :raises UnexpectedContentType: If the content-type of the response
+                                       isn't an expect type
+        :raises UnexpectedResponseCode: If a response code above 400 is
+                                        received and it doesn't fall into any
+                                        of the handled checks
+        :raises UnprocessableEntity: If a 422 response code is received and
+                                     couldn't be parsed
+        :returns: A listener object.
+        """
+        kwargs = {arg: value for arg, value in locals().items()
+                  if arg != 'self' and value is not Unset}
+        return self._create_object(**kwargs)
+
+    def show_listener(self, listener_id, query_params=None,
+                      return_object_only=True):
+        """Get listener details.
+
+        :param listener_id: The listener ID to query.
+        :param query_params: The optional query parameters to append to the
+                             request. Ex. fields=id&fields=name
+        :param return_object_only: If True, the response returns the object
+                                   inside the root tag. False returns the full
+                                   response from the API.
+        :raises AssertionError: if the expected_code isn't a valid http success
+                                response code
+        :raises BadRequest: If a 400 response code is received
+        :raises Conflict: If a 409 response code is received
+        :raises Forbidden: If a 403 response code is received
+        :raises Gone: If a 410 response code is received
+        :raises InvalidContentType: If a 415 response code is received
+        :raises InvalidHTTPResponseBody: The response body wasn't valid JSON
+        :raises InvalidHttpSuccessCode: if the read code isn't an expected
+                                        http success code
+        :raises NotFound: If a 404 response code is received
+        :raises NotImplemented: If a 501 response code is received
+        :raises OverLimit: If a 413 response code is received and over_limit is
+                           not in the response body
+        :raises RateLimitExceeded: If a 413 response code is received and
+                                   over_limit is in the response body
+        :raises ServerFault: If a 500 response code is received
+        :raises Unauthorized: If a 401 response code is received
+        :raises UnexpectedContentType: If the content-type of the response
+                                       isn't an expect type
+        :raises UnexpectedResponseCode: If a response code above 400 is
+                                        received and it doesn't fall into any
+                                        of the handled checks
+        :raises UnprocessableEntity: If a 422 response code is received and
+                                     couldn't be parsed
+        :returns: A listener object.
+        """
+        return self._show_object(obj_id=listener_id,
+                                 query_params=query_params,
+                                 return_object_only=return_object_only)
+
+    def list_listeners(self, query_params=None, return_object_only=True):
+        """Get a list of listener objects.
+
+        :param query_params: The optional query parameters to append to the
+                             request. Ex. fields=id&fields=name
+        :param return_object_only: If True, the response returns the object
+                                   inside the root tag. False returns the full
+                                   response from the API.
+        :raises AssertionError: if the expected_code isn't a valid http success
+                                response code
+        :raises BadRequest: If a 400 response code is received
+        :raises Conflict: If a 409 response code is received
+        :raises Forbidden: If a 403 response code is received
+        :raises Gone: If a 410 response code is received
+        :raises InvalidContentType: If a 415 response code is received
+        :raises InvalidHTTPResponseBody: The response body wasn't valid JSON
+        :raises InvalidHttpSuccessCode: if the read code isn't an expected
+                                        http success code
+        :raises NotFound: If a 404 response code is received
+        :raises NotImplemented: If a 501 response code is received
+        :raises OverLimit: If a 413 response code is received and over_limit is
+                           not in the response body
+        :raises RateLimitExceeded: If a 413 response code is received and
+                                   over_limit is in the response body
+        :raises ServerFault: If a 500 response code is received
+        :raises Unauthorized: If a 401 response code is received
+        :raises UnexpectedContentType: If the content-type of the response
+                                       isn't an expect type
+        :raises UnexpectedResponseCode: If a response code above 400 is
+                                        received and it doesn't fall into any
+                                        of the handled checks
+        :raises UnprocessableEntity: If a 422 response code is received and
+                                     couldn't be parsed
+        :returns: A list of listener objects.
+        """
+        return self._list_objects(query_params=query_params,
+                                  return_object_only=return_object_only)
+
+    def update_listener(self, listener_id, name=Unset, description=Unset,
+                        admin_state_up=Unset, connection_limit=Unset,
+                        timeout_client_data=Unset,
+                        timeout_member_connect=Unset,
+                        timeout_member_data=Unset, timeout_tcp_inspect=Unset,
+                        insert_headers=Unset, default_pool_id=Unset,
+                        default_tls_container_ref=Unset,
+                        sni_container_refs=Unset, return_object_only=True):
+        """Update a listener.
+
+        :param listener_id: The listener ID to update.
+        :param name: Human-readable name of the resource.
+        :param description: A human-readable description for the resource.
+        :param admin_state_up: The administrative state of the resource, which
+                               is up (true) or down (false).
+        :param connection_limit: The maximum number of connections permitted
+                                 for this listener. Default value is -1 which
+                                 represents infinite connections.
+        :param timeout_client_data: Frontend client inactivity timeout in
+                                    milliseconds.
+        :param timeout_member_connect: Backend member connection timeout in
+                                       milliseconds.
+        :param timeout_member_data: Backend member inactivity timeout in
+                                    milliseconds.
+        :param timeout_tcp_inspect: Time, in milliseconds, to wait for
+                                    additional TCP packets for content
+                                    inspection.
+        :param insert_headers: A dictionary of optional headers to insert into
+                               the request before it is sent to the backend
+                               member.
+        :param default_pool_id: The ID of the pool used by the listener if no
+                                L7 policies match.
+        :param default_tls_container_ref: The URI of the key manager service
+                                          secret containing a PKCS12 format
+                                          certificate/key bundle for
+                                          TERMINATED_TLS listeners.
+        :param sni_container_refs: A list of URIs to the key manager service
+                                   secrets containing PKCS12 format
+                                   certificate/key bundles for TERMINATED_TLS
+                                   listeners.
+        :param return_object_only: If True, the response returns the object
+                                   inside the root tag. False returns the full
+                                   response from the API.
+        :raises AssertionError: if the expected_code isn't a valid http success
+                                response code
+        :raises BadRequest: If a 400 response code is received
+        :raises Conflict: If a 409 response code is received
+        :raises Forbidden: If a 403 response code is received
+        :raises Gone: If a 410 response code is received
+        :raises InvalidContentType: If a 415 response code is received
+        :raises InvalidHTTPResponseBody: The response body wasn't valid JSON
+        :raises InvalidHttpSuccessCode: if the read code isn't an expected
+                                        http success code
+        :raises NotFound: If a 404 response code is received
+        :raises NotImplemented: If a 501 response code is received
+        :raises OverLimit: If a 413 response code is received and over_limit is
+                           not in the response body
+        :raises RateLimitExceeded: If a 413 response code is received and
+                                   over_limit is in the response body
+        :raises ServerFault: If a 500 response code is received
+        :raises Unauthorized: If a 401 response code is received
+        :raises UnexpectedContentType: If the content-type of the response
+                                       isn't an expect type
+        :raises UnexpectedResponseCode: If a response code above 400 is
+                                        received and it doesn't fall into any
+                                        of the handled checks
+        :raises UnprocessableEntity: If a 422 response code is received and
+                                     couldn't be parsed
+        :returns: A listener object.
+        """
+        kwargs = {arg: value for arg, value in locals().items()
+                  if arg != 'self' and value is not Unset}
+        kwargs['obj_id'] = kwargs.pop('listener_id')
+        return self._update_object(**kwargs)
+
+    def delete_listener(self, listener_id, ignore_errors=False):
+        """Delete a listener.
+
+        :param listener_id: The listener ID to delete.
+        :param ignore_errors: True if errors should be ignored.
+        :raises AssertionError: if the expected_code isn't a valid http success
+                                response code
+        :raises BadRequest: If a 400 response code is received
+        :raises Conflict: If a 409 response code is received
+        :raises Forbidden: If a 403 response code is received
+        :raises Gone: If a 410 response code is received
+        :raises InvalidContentType: If a 415 response code is received
+        :raises InvalidHTTPResponseBody: The response body wasn't valid JSON
+        :raises InvalidHttpSuccessCode: if the read code isn't an expected
+                                        http success code
+        :raises NotFound: If a 404 response code is received
+        :raises NotImplemented: If a 501 response code is received
+        :raises OverLimit: If a 413 response code is received and over_limit is
+                           not in the response body
+        :raises RateLimitExceeded: If a 413 response code is received and
+                                   over_limit is in the response body
+        :raises ServerFault: If a 500 response code is received
+        :raises Unauthorized: If a 401 response code is received
+        :raises UnexpectedContentType: If the content-type of the response
+                                       isn't an expect type
+        :raises UnexpectedResponseCode: If a response code above 400 is
+                                        received and it doesn't fall into any
+                                        of the handled checks
+        :raises UnprocessableEntity: If a 422 response code is received and
+                                     couldn't be parsed
+        :returns: None if ignore_errors is True, the response status code
+                  if not.
+        """
+        return self._delete_obj(obj_id=listener_id,
+                                ignore_errors=ignore_errors)
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 5564476..6ea83f0 100644
--- a/octavia_tempest_plugin/services/load_balancer/v2/loadbalancer_client.py
+++ b/octavia_tempest_plugin/services/load_balancer/v2/loadbalancer_client.py
@@ -16,134 +16,40 @@
 import json
 
 from tempest import config
-from tempest.lib.common import rest_client
+
+from octavia_tempest_plugin.services.load_balancer.v2 import base_client
 
 CONF = config.CONF
+Unset = base_client.Unset
 
 
-class LoadbalancerClient(rest_client.RestClient):
+class LoadbalancerClient(base_client.BaseLBaaSClient):
 
-    _uri = '/v2.0/lbaas/loadbalancers'
+    root_tag = 'loadbalancer'
+    list_root_tag = 'loadbalancers'
 
     def __init__(self, auth_provider, service, region, **kwargs):
         super(LoadbalancerClient, self).__init__(auth_provider, service,
                                                  region, **kwargs)
         self.timeout = CONF.load_balancer.lb_build_timeout
         self.build_interval = CONF.load_balancer.lb_build_interval
-        self.resource_name = 'load balancer'
-        self.get_status = self.show_loadbalancer
 
-    def list_loadbalancers(self, query_params=None, return_object_only=True):
-        """Get a list of load balancers.
+    def create_loadbalancer(self, name=Unset, description=Unset,
+                            admin_state_up=Unset, flavor_id=Unset,
+                            listeners=Unset, project_id=Unset, provider=Unset,
+                            vip_address=Unset, vip_network_id=Unset,
+                            vip_port_id=Unset, vip_qos_policy_id=Unset,
+                            vip_subnet_id=Unset, return_object_only=True):
+        """Create a loadbalancer.
 
-        :param query_params: The optional query parameters to append to the
-                             request. Ex. fields=id&fields=name
-        :param return_object_only: If True, the response returns the object
-                                   inside the root tag. False returns the full
-                                   response from the API.
-        :raises AssertionError: if the expected_code isn't a valid http success
-                                response code
-        :raises BadRequest: If a 400 response code is received
-        :raises Conflict: If a 409 response code is received
-        :raises Forbidden: If a 403 response code is received
-        :raises Gone: If a 410 response code is received
-        :raises InvalidContentType: If a 415 response code is received
-        :raises InvalidHTTPResponseBody: The response body wasn't valid JSON
-        :raises InvalidHttpSuccessCode: if the read code isn't an expected
-                                        http success code
-        :raises NotFound: If a 404 response code is received
-        :raises NotImplemented: If a 501 response code is received
-        :raises OverLimit: If a 413 response code is received and over_limit is
-                           not in the response body
-        :raises RateLimitExceeded: If a 413 response code is received and
-                                   over_limit is in the response body
-        :raises ServerFault: If a 500 response code is received
-        :raises Unauthorized: If a 401 response code is received
-        :raises UnexpectedContentType: If the content-type of the response
-                                       isn't an expect type
-        :raises UnexpectedResponseCode: If a response code above 400 is
-                                        received and it doesn't fall into any
-                                        of the handled checks
-        :raises UnprocessableEntity: If a 422 response code is received and
-                                     couldn't be parsed
-        :returns: A list of load balancers object.
-        """
-        if query_params:
-            request_uri = '{0}?{1}'.format(self._uri, query_params)
-        else:
-            request_uri = self._uri
-        response, body = self.get(request_uri)
-        self.expected_success(200, response.status)
-        if return_object_only:
-            return json.loads(body.decode('utf-8'))['loadbalancers']
-        else:
-            return json.loads(body.decode('utf-8'))
-
-    def create_loadbalancer_dict(self, lb_dict, return_object_only=True):
-        """Create a load balancer using a dictionary.
-
-        Example lb_dict::
-
-          lb_dict = {'loadbalancer': {
-              'vip_network_id': 'd0be73da-921a-4e03-9c49-f13f18f7e39f',
-              'name': 'TEMPEST_TEST_LB',
-              'description': 'LB for Tempest tests'}
-          }
-
-        :param lb_dict: A dictionary describing the load balancer.
-        :param return_object_only: If True, the response returns the object
-                                   inside the root tag. False returns the full
-                                   response from the API.
-        :raises AssertionError: if the expected_code isn't a valid http success
-                                response code
-        :raises BadRequest: If a 400 response code is received
-        :raises Conflict: If a 409 response code is received
-        :raises Forbidden: If a 403 response code is received
-        :raises Gone: If a 410 response code is received
-        :raises InvalidContentType: If a 415 response code is received
-        :raises InvalidHTTPResponseBody: The response body wasn't valid JSON
-        :raises InvalidHttpSuccessCode: if the read code isn't an expected
-                                        http success code
-        :raises NotFound: If a 404 response code is received
-        :raises NotImplemented: If a 501 response code is received
-        :raises OverLimit: If a 413 response code is received and over_limit is
-                           not in the response body
-        :raises RateLimitExceeded: If a 413 response code is received and
-                                   over_limit is in the response body
-        :raises ServerFault: If a 500 response code is received
-        :raises Unauthorized: If a 401 response code is received
-        :raises UnexpectedContentType: If the content-type of the response
-                                       isn't an expect type
-        :raises UnexpectedResponseCode: If a response code above 400 is
-                                        received and it doesn't fall into any
-                                        of the handled checks
-        :raises UnprocessableEntity: If a 422 response code is received and
-                                     couldn't be parsed
-        :returns: A load balancer object.
-        """
-        response, body = self.post(self._uri, json.dumps(lb_dict))
-        self.expected_success(201, response.status)
-        if return_object_only:
-            return json.loads(body.decode('utf-8'))['loadbalancer']
-        else:
-            return json.loads(body.decode('utf-8'))
-
-    def create_loadbalancer(self, admin_state_up=None, description=None,
-                            flavor=None, listeners=None, name=None,
-                            project_id=None, provider=None, vip_address=None,
-                            vip_network_id=None, vip_port_id=None,
-                            vip_qos_policy_id=None, vip_subnet_id=None,
-                            return_object_only=True):
-        """Create a load balancer.
-
+        :param name: Human-readable name of the resource.
+        :param description: A human-readable description for the resource.
         :param admin_state_up: The administrative state of the resource, which
                                is up (true) or down (false).
-        :param description: A human-readable description for the resource.
-        :param flavor: The load balancer flavor ID.
+        :param flavor: The loadbalancer flavor ID.
         :param listeners: A list of listner dictionaries.
-        :param name: Human-readable name of the resource.
         :param project_id: The ID of the project owning this resource.
-        :param provider: Provider name for the load balancer.
+        :param provider: Provider name for the loadbalancer.
         :param vip_address: The IP address of the Virtual IP (VIP).
         :param vip_network_id: The ID of the network for the Virtual IP (VIP).
         :param vip_port_id: The ID of the Virtual IP (VIP) port.
@@ -178,108 +84,17 @@
                                         of the handled checks
         :raises UnprocessableEntity: If a 422 response code is received and
                                      couldn't be parsed
-        :returns: A load balancer object.
+        :returns: A loadbalancer object.
         """
-        method_args = locals()
-        lb_params = {}
-        for param, value in method_args.items():
-            if param not in ('self',
-                             'return_object_only') and value is not None:
-                lb_params[param] = value
-        lb_dict = {'loadbalancer': lb_params}
-        return self.create_loadbalancer_dict(lb_dict, return_object_only)
-
-    def delete_loadbalancer(self, lb_id, cascade=False, ignore_errors=False):
-        """Delete a load balancer.
-
-        :param lb_id: The load balancer ID to delete.
-        :param cascade: If true will delete all child objects of the
-                        load balancer.
-        :param ignore_errors: True if errors should be ignored.
-        :raises AssertionError: if the expected_code isn't a valid http success
-                                response code
-        :raises BadRequest: If a 400 response code is received
-        :raises Conflict: If a 409 response code is received
-        :raises Forbidden: If a 403 response code is received
-        :raises Gone: If a 410 response code is received
-        :raises InvalidContentType: If a 415 response code is received
-        :raises InvalidHTTPResponseBody: The response body wasn't valid JSON
-        :raises InvalidHttpSuccessCode: if the read code isn't an expected
-                                        http success code
-        :raises NotFound: If a 404 response code is received
-        :raises NotImplemented: If a 501 response code is received
-        :raises OverLimit: If a 413 response code is received and over_limit is
-                           not in the response body
-        :raises RateLimitExceeded: If a 413 response code is received and
-                                   over_limit is in the response body
-        :raises ServerFault: If a 500 response code is received
-        :raises Unauthorized: If a 401 response code is received
-        :raises UnexpectedContentType: If the content-type of the response
-                                       isn't an expect type
-        :raises UnexpectedResponseCode: If a response code above 400 is
-                                        received and it doesn't fall into any
-                                        of the handled checks
-        :raises UnprocessableEntity: If a 422 response code is received and
-                                     couldn't be parsed
-        :returns: None if ignore_errors is True, the response status code
-                  if not.
-        """
-        if cascade:
-            uri = '{0}/{1}?cascade=true'.format(self._uri, lb_id)
-        else:
-            uri = '{0}/{1}'.format(self._uri, lb_id)
-        if ignore_errors:
-            try:
-                response, body = self.delete(uri)
-            except ignore_errors:
-                return
-        else:
-            response, body = self.delete(uri)
-
-        self.expected_success(204, response.status)
-        return response.status
-
-    def failover_loadbalancer(self, lb_id):
-        """Failover a load balancer.
-
-        :param lb_id: The load balancer ID to query.
-        :raises AssertionError: if the expected_code isn't a valid http success
-                                response code
-        :raises BadRequest: If a 400 response code is received
-        :raises Conflict: If a 409 response code is received
-        :raises Forbidden: If a 403 response code is received
-        :raises Gone: If a 410 response code is received
-        :raises InvalidContentType: If a 415 response code is received
-        :raises InvalidHTTPResponseBody: The response body wasn't valid JSON
-        :raises InvalidHttpSuccessCode: if the read code isn't an expected
-                                        http success code
-        :raises NotFound: If a 404 response code is received
-        :raises NotImplemented: If a 501 response code is received
-        :raises OverLimit: If a 413 response code is received and over_limit is
-                           not in the response body
-        :raises RateLimitExceeded: If a 413 response code is received and
-                                   over_limit is in the response body
-        :raises ServerFault: If a 500 response code is received
-        :raises Unauthorized: If a 401 response code is received
-        :raises UnexpectedContentType: If the content-type of the response
-                                       isn't an expect type
-        :raises UnexpectedResponseCode: If a response code above 400 is
-                                        received and it doesn't fall into any
-                                        of the handled checks
-        :raises UnprocessableEntity: If a 422 response code is received and
-                                     couldn't be parsed
-        :returns: None
-        """
-        uri = '{0}/{1}/failover'.format(self._uri, lb_id)
-        response, body = self.put(uri, '')
-        self.expected_success(202, response.status)
-        return
+        kwargs = {arg: value for arg, value in locals().items()
+                  if arg != 'self' and value is not Unset}
+        return self._create_object(**kwargs)
 
     def show_loadbalancer(self, lb_id, query_params=None,
                           return_object_only=True):
-        """Get load balancer details.
+        """Get loadbalancer details.
 
-        :param lb_id: The load balancer ID to query.
+        :param lb_id: The loadbalancer ID to query.
         :param query_params: The optional query parameters to append to the
                              request. Ex. fields=id&fields=name
         :param return_object_only: If True, the response returns the object
@@ -310,25 +125,15 @@
                                         of the handled checks
         :raises UnprocessableEntity: If a 422 response code is received and
                                      couldn't be parsed
-        :returns: A load balancer object.
+        :returns: A loadbalancer object.
         """
-        if query_params:
-            request_uri = '{0}/{1}?{2}'.format(self._uri, lb_id, query_params)
-        else:
-            request_uri = '{0}/{1}'.format(self._uri, lb_id)
+        return self._show_object(obj_id=lb_id,
+                                 query_params=query_params,
+                                 return_object_only=return_object_only)
 
-        response, body = self.get(request_uri)
-        self.expected_success(200, response.status)
-        if return_object_only:
-            return json.loads(body.decode('utf-8'))['loadbalancer']
-        else:
-            return json.loads(body.decode('utf-8'))
+    def list_loadbalancers(self, query_params=None, return_object_only=True):
+        """Get a list of loadbalancer objects.
 
-    def get_loadbalancer_stats(self, lb_id, query_params=None,
-                               return_object_only=True):
-        """Get load balancer statistics.
-
-        :param lb_id: The load balancer ID to query.
         :param query_params: The optional query parameters to append to the
                              request. Ex. fields=id&fields=name
         :param return_object_only: If True, the response returns the object
@@ -359,130 +164,21 @@
                                         of the handled checks
         :raises UnprocessableEntity: If a 422 response code is received and
                                      couldn't be parsed
-        :returns: A load balancer statistics object.
+        :returns: A list of loadbalancer objects.
         """
-        if query_params:
-            request_uri = '{0}/{1}/stats?{2}'.format(self._uri, lb_id,
-                                                     query_params)
-        else:
-            request_uri = '{0}/{1}/stats'.format(self._uri, lb_id)
+        return self._list_objects(query_params=query_params,
+                                  return_object_only=return_object_only)
 
-        response, body = self.get(request_uri)
-        self.expected_success(200, response.status)
-        if return_object_only:
-            return json.loads(body.decode('utf-8'))['stats']
-        else:
-            return json.loads(body.decode('utf-8'))
-
-    def get_loadbalancer_status(self, lb_id, query_params=None,
-                                return_object_only=True):
-        """Get a load balancer status tree.
-
-        :param lb_id: The load balancer ID to query.
-        :param query_params: The optional query parameters to append to the
-                             request. Ex. fields=id&fields=name
-        :param return_object_only: If True, the response returns the object
-                                   inside the root tag. False returns the full
-                                   response from the API.
-        :raises AssertionError: if the expected_code isn't a valid http success
-                                response code
-        :raises BadRequest: If a 400 response code is received
-        :raises Conflict: If a 409 response code is received
-        :raises Forbidden: If a 403 response code is received
-        :raises Gone: If a 410 response code is received
-        :raises InvalidContentType: If a 415 response code is received
-        :raises InvalidHTTPResponseBody: The response body wasn't valid JSON
-        :raises InvalidHttpSuccessCode: if the read code isn't an expected
-                                        http success code
-        :raises NotFound: If a 404 response code is received
-        :raises NotImplemented: If a 501 response code is received
-        :raises OverLimit: If a 413 response code is received and over_limit is
-                           not in the response body
-        :raises RateLimitExceeded: If a 413 response code is received and
-                                   over_limit is in the response body
-        :raises ServerFault: If a 500 response code is received
-        :raises Unauthorized: If a 401 response code is received
-        :raises UnexpectedContentType: If the content-type of the response
-                                       isn't an expect type
-        :raises UnexpectedResponseCode: If a response code above 400 is
-                                        received and it doesn't fall into any
-                                        of the handled checks
-        :raises UnprocessableEntity: If a 422 response code is received and
-                                     couldn't be parsed
-        :returns: A load balancer statuses object.
-        """
-        if query_params:
-            request_uri = '{0}/{1}/status?{2}'.format(self._uri, lb_id,
-                                                      query_params)
-        else:
-            request_uri = '{0}/{1}/status'.format(self._uri, lb_id)
-
-        response, body = self.get(request_uri)
-        self.expected_success(200, response.status)
-        if return_object_only:
-            return json.loads(body.decode('utf-8'))['statuses']
-        else:
-            return json.loads(body.decode('utf-8'))
-
-    def update_loadbalancer_dict(self, lb_id, lb_dict,
-                                 return_object_only=True):
-        """Update a load balancer using a dictionary.
-
-        Example lb_dict::
-
-          lb_dict = {'loadbalancer': {'name': 'TEMPEST_TEST_LB_UPDATED'} }
-
-        :param lb_id: The load balancer ID to update.
-        :param lb_dict: A dictionary of elements to update on the load
-                        balancer.
-        :param return_object_only: If True, the response returns the object
-                                   inside the root tag. False returns the full
-                                   response from the API.
-        :raises AssertionError: if the expected_code isn't a valid http success
-                                response code
-        :raises BadRequest: If a 400 response code is received
-        :raises Conflict: If a 409 response code is received
-        :raises Forbidden: If a 403 response code is received
-        :raises Gone: If a 410 response code is received
-        :raises InvalidContentType: If a 415 response code is received
-        :raises InvalidHTTPResponseBody: The response body wasn't valid JSON
-        :raises InvalidHttpSuccessCode: if the read code isn't an expected
-                                        http success code
-        :raises NotFound: If a 404 response code is received
-        :raises NotImplemented: If a 501 response code is received
-        :raises OverLimit: If a 413 response code is received and over_limit is
-                           not in the response body
-        :raises RateLimitExceeded: If a 413 response code is received and
-                                   over_limit is in the response body
-        :raises ServerFault: If a 500 response code is received
-        :raises Unauthorized: If a 401 response code is received
-        :raises UnexpectedContentType: If the content-type of the response
-                                       isn't an expect type
-        :raises UnexpectedResponseCode: If a response code above 400 is
-                                        received and it doesn't fall into any
-                                        of the handled checks
-        :raises UnprocessableEntity: If a 422 response code is received and
-                                     couldn't be parsed
-        :returns: A load balancer object.
-        """
-        uri = '{0}/{1}'.format(self._uri, lb_id)
-        response, body = self.put(uri, json.dumps(lb_dict))
-        self.expected_success(200, response.status)
-        if return_object_only:
-            return json.loads(body.decode('utf-8'))['loadbalancer']
-        else:
-            return json.loads(body.decode('utf-8'))
-
-    def update_loadbalancer(self, lb_id, admin_state_up=None, description=None,
-                            name=None, vip_qos_policy_id=None,
+    def update_loadbalancer(self, lb_id, name=Unset, description=Unset,
+                            admin_state_up=Unset, vip_qos_policy_id=Unset,
                             return_object_only=True):
-        """Update a load balancer.
+        """Update a loadbalancer.
 
-        :param lb_id: The load balancer ID to update.
+        :param lb_id: The loadbalancer ID to update.
+        :param name: Human-readable name of the resource.
+        :param description: A human-readable description for the resource.
         :param admin_state_up: The administrative state of the resource, which
                                is up (true) or down (false).
-        :param description: A human-readable description for the resource.
-        :param name: Human-readable name of the resource.
         :param vip_qos_policy_id: The ID of the QoS Policy which will apply to
                                   the Virtual IP (VIP).
         :param return_object_only: If True, the response returns the object
@@ -513,14 +209,184 @@
                                         of the handled checks
         :raises UnprocessableEntity: If a 422 response code is received and
                                      couldn't be parsed
-        :returns: A load balancer object.
+        :returns: A loadbalancer object.
         """
-        method_args = locals()
-        lb_params = {}
-        for param, value in method_args.items():
-            if param not in ('self', 'lb_id',
-                             'return_object_only') and value is not None:
-                lb_params[param] = value
-        lb_dict = {'loadbalancer': lb_params}
-        return self.update_loadbalancer_dict(lb_id, lb_dict,
-                                             return_object_only)
+        kwargs = {arg: value for arg, value in locals().items()
+                  if arg != 'self' and value is not Unset}
+        kwargs['obj_id'] = kwargs.pop('lb_id')
+        return self._update_object(**kwargs)
+
+    def delete_loadbalancer(self, lb_id, cascade=False, ignore_errors=False):
+        """Delete a loadbalancer.
+
+        :param lb_id: The loadbalancer ID to delete.
+        :param ignore_errors: True if errors should be ignored.
+        :param cascade: If true will delete all child objects of an
+                        object, if that object supports it.
+        :raises AssertionError: if the expected_code isn't a valid http success
+                                response code
+        :raises BadRequest: If a 400 response code is received
+        :raises Conflict: If a 409 response code is received
+        :raises Forbidden: If a 403 response code is received
+        :raises Gone: If a 410 response code is received
+        :raises InvalidContentType: If a 415 response code is received
+        :raises InvalidHTTPResponseBody: The response body wasn't valid JSON
+        :raises InvalidHttpSuccessCode: if the read code isn't an expected
+                                        http success code
+        :raises NotFound: If a 404 response code is received
+        :raises NotImplemented: If a 501 response code is received
+        :raises OverLimit: If a 413 response code is received and over_limit is
+                           not in the response body
+        :raises RateLimitExceeded: If a 413 response code is received and
+                                   over_limit is in the response body
+        :raises ServerFault: If a 500 response code is received
+        :raises Unauthorized: If a 401 response code is received
+        :raises UnexpectedContentType: If the content-type of the response
+                                       isn't an expect type
+        :raises UnexpectedResponseCode: If a response code above 400 is
+                                        received and it doesn't fall into any
+                                        of the handled checks
+        :raises UnprocessableEntity: If a 422 response code is received and
+                                     couldn't be parsed
+        :returns: None if ignore_errors is True, the response status code
+                  if not.
+        """
+        return self._delete_obj(obj_id=lb_id,
+                                ignore_errors=ignore_errors,
+                                cascade=cascade)
+
+    def failover_loadbalancer(self, lb_id):
+        """Failover a loadbalancer.
+
+        :param lb_id: The loadbalancer ID to query.
+        :raises AssertionError: if the expected_code isn't a valid http success
+                                response code
+        :raises BadRequest: If a 400 response code is received
+        :raises Conflict: If a 409 response code is received
+        :raises Forbidden: If a 403 response code is received
+        :raises Gone: If a 410 response code is received
+        :raises InvalidContentType: If a 415 response code is received
+        :raises InvalidHTTPResponseBody: The response body wasn't valid JSON
+        :raises InvalidHttpSuccessCode: if the read code isn't an expected
+                                        http success code
+        :raises NotFound: If a 404 response code is received
+        :raises NotImplemented: If a 501 response code is received
+        :raises OverLimit: If a 413 response code is received and over_limit is
+                           not in the response body
+        :raises RateLimitExceeded: If a 413 response code is received and
+                                   over_limit is in the response body
+        :raises ServerFault: If a 500 response code is received
+        :raises Unauthorized: If a 401 response code is received
+        :raises UnexpectedContentType: If the content-type of the response
+                                       isn't an expect type
+        :raises UnexpectedResponseCode: If a response code above 400 is
+                                        received and it doesn't fall into any
+                                        of the handled checks
+        :raises UnprocessableEntity: If a 422 response code is received and
+                                     couldn't be parsed
+        :returns: None
+        """
+        uri = '{0}/{1}/failover'.format(self.uri, lb_id)
+        response, body = self.put(uri, '')
+        self.expected_success(202, response.status)
+        return
+
+    def get_loadbalancer_stats(self, lb_id, query_params=None,
+                               return_object_only=True):
+        """Get loadbalancer statistics.
+
+        :param lb_id: The loadbalancer ID to query.
+        :param query_params: The optional query parameters to append to the
+                             request. Ex. fields=id&fields=name
+        :param return_object_only: If True, the response returns the object
+                                   inside the root tag. False returns the full
+                                   response from the API.
+        :raises AssertionError: if the expected_code isn't a valid http success
+                                response code
+        :raises BadRequest: If a 400 response code is received
+        :raises Conflict: If a 409 response code is received
+        :raises Forbidden: If a 403 response code is received
+        :raises Gone: If a 410 response code is received
+        :raises InvalidContentType: If a 415 response code is received
+        :raises InvalidHTTPResponseBody: The response body wasn't valid JSON
+        :raises InvalidHttpSuccessCode: if the read code isn't an expected
+                                        http success code
+        :raises NotFound: If a 404 response code is received
+        :raises NotImplemented: If a 501 response code is received
+        :raises OverLimit: If a 413 response code is received and over_limit is
+                           not in the response body
+        :raises RateLimitExceeded: If a 413 response code is received and
+                                   over_limit is in the response body
+        :raises ServerFault: If a 500 response code is received
+        :raises Unauthorized: If a 401 response code is received
+        :raises UnexpectedContentType: If the content-type of the response
+                                       isn't an expect type
+        :raises UnexpectedResponseCode: If a response code above 400 is
+                                        received and it doesn't fall into any
+                                        of the handled checks
+        :raises UnprocessableEntity: If a 422 response code is received and
+                                     couldn't be parsed
+        :returns: A loadbalancer statistics object.
+        """
+        if query_params:
+            request_uri = '{0}/{1}/stats?{2}'.format(self.uri, lb_id,
+                                                     query_params)
+        else:
+            request_uri = '{0}/{1}/stats'.format(self.uri, lb_id)
+
+        response, body = self.get(request_uri)
+        self.expected_success(200, response.status)
+        if return_object_only:
+            return json.loads(body.decode('utf-8'))['stats']
+        else:
+            return json.loads(body.decode('utf-8'))
+
+    def get_loadbalancer_status(self, lb_id, query_params=None,
+                                return_object_only=True):
+        """Get a loadbalancer status tree.
+
+        :param lb_id: The loadbalancer ID to query.
+        :param query_params: The optional query parameters to append to the
+                             request. Ex. fields=id&fields=name
+        :param return_object_only: If True, the response returns the object
+                                   inside the root tag. False returns the full
+                                   response from the API.
+        :raises AssertionError: if the expected_code isn't a valid http success
+                                response code
+        :raises BadRequest: If a 400 response code is received
+        :raises Conflict: If a 409 response code is received
+        :raises Forbidden: If a 403 response code is received
+        :raises Gone: If a 410 response code is received
+        :raises InvalidContentType: If a 415 response code is received
+        :raises InvalidHTTPResponseBody: The response body wasn't valid JSON
+        :raises InvalidHttpSuccessCode: if the read code isn't an expected
+                                        http success code
+        :raises NotFound: If a 404 response code is received
+        :raises NotImplemented: If a 501 response code is received
+        :raises OverLimit: If a 413 response code is received and over_limit is
+                           not in the response body
+        :raises RateLimitExceeded: If a 413 response code is received and
+                                   over_limit is in the response body
+        :raises ServerFault: If a 500 response code is received
+        :raises Unauthorized: If a 401 response code is received
+        :raises UnexpectedContentType: If the content-type of the response
+                                       isn't an expect type
+        :raises UnexpectedResponseCode: If a response code above 400 is
+                                        received and it doesn't fall into any
+                                        of the handled checks
+        :raises UnprocessableEntity: If a 422 response code is received and
+                                     couldn't be parsed
+        :returns: A loadbalancer statuses object.
+        """
+        if query_params:
+            request_uri = '{0}/{1}/status?{2}'.format(self.uri, lb_id,
+                                                      query_params)
+        else:
+            request_uri = '{0}/{1}/status'.format(self.uri, lb_id)
+
+        response, body = self.get(request_uri)
+        self.expected_success(200, response.status)
+        if return_object_only:
+            return json.loads(body.decode('utf-8'))['statuses']
+        else:
+            return json.loads(body.decode('utf-8'))
diff --git a/octavia_tempest_plugin/services/load_balancer/v2/member_client.py b/octavia_tempest_plugin/services/load_balancer/v2/member_client.py
new file mode 100644
index 0000000..3909ac2
--- /dev/null
+++ b/octavia_tempest_plugin/services/load_balancer/v2/member_client.py
@@ -0,0 +1,325 @@
+#   Copyright 2018 GoDaddy
+#
+#   Licensed under the Apache License, Version 2.0 (the "License"); you may
+#   not use this file except in compliance with the License. You may obtain
+#   a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#   WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#   License for the specific language governing permissions and limitations
+#   under the License.
+
+import json
+
+from oslo_log import log as logging
+from tempest import config
+
+from octavia_tempest_plugin.services.load_balancer.v2 import base_client
+from octavia_tempest_plugin.services.load_balancer.v2 import pool_client
+
+CONF = config.CONF
+LOG = logging.getLogger(__name__)
+Unset = base_client.Unset
+
+
+class MemberClient(base_client.BaseLBaaSClient):
+
+    root_tag = 'member'
+    list_root_tag = 'members'
+
+    def __init__(self, *args, **kwargs):
+        super(MemberClient, self).__init__(*args, **kwargs)
+        pool_list_root_tag = pool_client.PoolClient.list_root_tag
+        # /v2.0/lbaas/pools/<POOL_UUID>/members
+        self.uri = "{pool_base_uri}/{parent}/{object}".format(
+            pool_base_uri=self.base_uri.format(object=pool_list_root_tag),
+            parent="{parent}",
+            object=self.list_root_tag
+        )
+
+    def create_member(self, pool_id, address, protocol_port,
+                      name=Unset, admin_state_up=Unset, weight=Unset,
+                      backup=Unset, subnet_id=Unset, monitor_address=Unset,
+                      monitor_port=Unset, return_object_only=True):
+        """Create a member.
+
+        :param pool_id: The ID of the pool where the member will live.
+        :param address: The IP address of the resource.
+        :param protocol_port: The protocol port number for the resource.
+        :param name: Human-readable name of the resource.
+        :param admin_state_up: The administrative state of the resource, which
+                               is up (true) or down (false).
+        :param weight: The weight of a member determines the portion of
+                       requests or connections it services compared to the
+                       other members of the pool.
+        :param backup: Is the member a backup?
+        :param monitor_address: An alternate IP address used for health
+                                monitoring a backend member.
+        :param monitor_port: An alternate protocol port used for health
+                             monitoring a backend member.
+        :param return_object_only: If True, the response returns the object
+                                   inside the root tag. False returns the full
+                                   response from the API.
+        :raises AssertionError: if the expected_code isn't a valid http success
+                                response code
+        :raises BadRequest: If a 400 response code is received
+        :raises Conflict: If a 409 response code is received
+        :raises Forbidden: If a 403 response code is received
+        :raises Gone: If a 410 response code is received
+        :raises InvalidContentType: If a 415 response code is received
+        :raises InvalidHTTPResponseBody: The response body wasn't valid JSON
+        :raises InvalidHttpSuccessCode: if the read code isn't an expected
+                                        http success code
+        :raises NotFound: If a 404 response code is received
+        :raises NotImplemented: If a 501 response code is received
+        :raises OverLimit: If a 413 response code is received and over_limit is
+                           not in the response body
+        :raises RateLimitExceeded: If a 413 response code is received and
+                                   over_limit is in the response body
+        :raises ServerFault: If a 500 response code is received
+        :raises Unauthorized: If a 401 response code is received
+        :raises UnexpectedContentType: If the content-type of the response
+                                       isn't an expect type
+        :raises UnexpectedResponseCode: If a response code above 400 is
+                                        received and it doesn't fall into any
+                                        of the handled checks
+        :raises UnprocessableEntity: If a 422 response code is received and
+                                     couldn't be parsed
+        :returns: A member object.
+        """
+        kwargs = {arg: value for arg, value in locals().items()
+                  if arg != 'self' and value is not Unset}
+        kwargs['parent_id'] = kwargs.pop('pool_id')
+        return self._create_object(**kwargs)
+
+    def show_member(self, member_id, pool_id, query_params=None,
+                    return_object_only=True):
+        """Get member details.
+
+        :param member_id: The member ID to query.
+        :param pool_id: The ID of the pool where the member lives.
+        :param query_params: The optional query parameters to append to the
+                             request. Ex. fields=id&fields=name
+        :param return_object_only: If True, the response returns the object
+                                   inside the root tag. False returns the full
+                                   response from the API.
+        :raises AssertionError: if the expected_code isn't a valid http success
+                                response code
+        :raises BadRequest: If a 400 response code is received
+        :raises Conflict: If a 409 response code is received
+        :raises Forbidden: If a 403 response code is received
+        :raises Gone: If a 410 response code is received
+        :raises InvalidContentType: If a 415 response code is received
+        :raises InvalidHTTPResponseBody: The response body wasn't valid JSON
+        :raises InvalidHttpSuccessCode: if the read code isn't an expected
+                                        http success code
+        :raises NotFound: If a 404 response code is received
+        :raises NotImplemented: If a 501 response code is received
+        :raises OverLimit: If a 413 response code is received and over_limit is
+                           not in the response body
+        :raises RateLimitExceeded: If a 413 response code is received and
+                                   over_limit is in the response body
+        :raises ServerFault: If a 500 response code is received
+        :raises Unauthorized: If a 401 response code is received
+        :raises UnexpectedContentType: If the content-type of the response
+                                       isn't an expect type
+        :raises UnexpectedResponseCode: If a response code above 400 is
+                                        received and it doesn't fall into any
+                                        of the handled checks
+        :raises UnprocessableEntity: If a 422 response code is received and
+                                     couldn't be parsed
+        :returns: A member object.
+        """
+        return self._show_object(obj_id=member_id,
+                                 parent_id=pool_id,
+                                 query_params=query_params,
+                                 return_object_only=return_object_only)
+
+    def list_members(self, pool_id, query_params=None,
+                     return_object_only=True):
+        """Get a list of member objects.
+
+        :param pool_id: The ID of the pool where the members live.
+        :param query_params: The optional query parameters to append to the
+                             request. Ex. fields=id&fields=name
+        :param return_object_only: If True, the response returns the object
+                                   inside the root tag. False returns the full
+                                   response from the API.
+        :raises AssertionError: if the expected_code isn't a valid http success
+                                response code
+        :raises BadRequest: If a 400 response code is received
+        :raises Conflict: If a 409 response code is received
+        :raises Forbidden: If a 403 response code is received
+        :raises Gone: If a 410 response code is received
+        :raises InvalidContentType: If a 415 response code is received
+        :raises InvalidHTTPResponseBody: The response body wasn't valid JSON
+        :raises InvalidHttpSuccessCode: if the read code isn't an expected
+                                        http success code
+        :raises NotFound: If a 404 response code is received
+        :raises NotImplemented: If a 501 response code is received
+        :raises OverLimit: If a 413 response code is received and over_limit is
+                           not in the response body
+        :raises RateLimitExceeded: If a 413 response code is received and
+                                   over_limit is in the response body
+        :raises ServerFault: If a 500 response code is received
+        :raises Unauthorized: If a 401 response code is received
+        :raises UnexpectedContentType: If the content-type of the response
+                                       isn't an expect type
+        :raises UnexpectedResponseCode: If a response code above 400 is
+                                        received and it doesn't fall into any
+                                        of the handled checks
+        :raises UnprocessableEntity: If a 422 response code is received and
+                                     couldn't be parsed
+        :returns: A list of member objects.
+        """
+        return self._list_objects(parent_id=pool_id,
+                                  query_params=query_params,
+                                  return_object_only=return_object_only)
+
+    def update_member(self, member_id, pool_id, name=Unset,
+                      admin_state_up=Unset, weight=Unset, backup=Unset,
+                      monitor_address=Unset, monitor_port=Unset,
+                      return_object_only=True):
+        """Update a member.
+
+        :param member_id: The member ID to update.
+        :param pool_id: The ID of the pool where the member lives.
+        :param name: Human-readable name of the resource.
+        :param admin_state_up: The administrative state of the resource, which
+                               is up (true) or down (false).
+        :param weight: The weight of a member determines the portion of
+                       requests or connections it services compared to the
+                       other members of the pool.
+        :param backup: Is the member a backup?
+        :param monitor_address: An alternate IP address used for health
+                                monitoring a backend member.
+        :param monitor_port: An alternate protocol port used for health
+                             monitoring a backend member.
+        :param return_object_only: If True, the response returns the object
+                                   inside the root tag. False returns the full
+                                   response from the API.
+        :raises AssertionError: if the expected_code isn't a valid http success
+                                response code
+        :raises BadRequest: If a 400 response code is received
+        :raises Conflict: If a 409 response code is received
+        :raises Forbidden: If a 403 response code is received
+        :raises Gone: If a 410 response code is received
+        :raises InvalidContentType: If a 415 response code is received
+        :raises InvalidHTTPResponseBody: The response body wasn't valid JSON
+        :raises InvalidHttpSuccessCode: if the read code isn't an expected
+                                        http success code
+        :raises NotFound: If a 404 response code is received
+        :raises NotImplemented: If a 501 response code is received
+        :raises OverLimit: If a 413 response code is received and over_limit is
+                           not in the response body
+        :raises RateLimitExceeded: If a 413 response code is received and
+                                   over_limit is in the response body
+        :raises ServerFault: If a 500 response code is received
+        :raises Unauthorized: If a 401 response code is received
+        :raises UnexpectedContentType: If the content-type of the response
+                                       isn't an expect type
+        :raises UnexpectedResponseCode: If a response code above 400 is
+                                        received and it doesn't fall into any
+                                        of the handled checks
+        :raises UnprocessableEntity: If a 422 response code is received and
+                                     couldn't be parsed
+        :returns: A member object.
+        """
+        kwargs = {arg: value for arg, value in locals().items()
+                  if arg != 'self' and value is not Unset}
+        kwargs['obj_id'] = kwargs.pop('member_id')
+        kwargs['parent_id'] = kwargs.pop('pool_id')
+        return self._update_object(**kwargs)
+
+    def update_members(self, pool_id, members_list):
+        """Batch update all members on a pool.
+
+        :param pool_id: The ID of the pool where the members live.
+        :param members_list: The list of members to enforce on the pool.
+        :raises AssertionError: if the expected_code isn't a valid http success
+                                response code
+        :raises BadRequest: If a 400 response code is received
+        :raises Conflict: If a 409 response code is received
+        :raises Forbidden: If a 403 response code is received
+        :raises Gone: If a 410 response code is received
+        :raises InvalidContentType: If a 415 response code is received
+        :raises InvalidHTTPResponseBody: The response body wasn't valid JSON
+        :raises InvalidHttpSuccessCode: if the read code isn't an expected
+                                        http success code
+        :raises NotFound: If a 404 response code is received
+        :raises NotImplemented: If a 501 response code is received
+        :raises OverLimit: If a 413 response code is received and over_limit is
+                           not in the response body
+        :raises RateLimitExceeded: If a 413 response code is received and
+                                   over_limit is in the response body
+        :raises ServerFault: If a 500 response code is received
+        :raises Unauthorized: If a 401 response code is received
+        :raises UnexpectedContentType: If the content-type of the response
+                                       isn't an expect type
+        :raises UnexpectedResponseCode: If a response code above 400 is
+                                        received and it doesn't fall into any
+                                        of the handled checks
+        :raises UnprocessableEntity: If a 422 response code is received and
+                                     couldn't be parsed
+        :returns: A member object.
+        """
+        obj_dict = {self.list_root_tag: members_list}
+        request_uri = self.uri.format(parent=pool_id)
+
+        response, body = self.put(request_uri, json.dumps(obj_dict))
+        self.expected_success(202, response.status)
+        return
+
+    def delete_member(self, member_id, pool_id, ignore_errors=False):
+        """Delete a member.
+
+        :param member_id: The member ID to delete.
+        :param pool_id: The ID of the pool where the member lives.
+        :param ignore_errors: True if errors should be ignored.
+        :raises AssertionError: if the expected_code isn't a valid http success
+                                response code
+        :raises BadRequest: If a 400 response code is received
+        :raises Conflict: If a 409 response code is received
+        :raises Forbidden: If a 403 response code is received
+        :raises Gone: If a 410 response code is received
+        :raises InvalidContentType: If a 415 response code is received
+        :raises InvalidHTTPResponseBody: The response body wasn't valid JSON
+        :raises InvalidHttpSuccessCode: if the read code isn't an expected
+                                        http success code
+        :raises NotFound: If a 404 response code is received
+        :raises NotImplemented: If a 501 response code is received
+        :raises OverLimit: If a 413 response code is received and over_limit is
+                           not in the response body
+        :raises RateLimitExceeded: If a 413 response code is received and
+                                   over_limit is in the response body
+        :raises ServerFault: If a 500 response code is received
+        :raises Unauthorized: If a 401 response code is received
+        :raises UnexpectedContentType: If the content-type of the response
+                                       isn't an expect type
+        :raises UnexpectedResponseCode: If a response code above 400 is
+                                        received and it doesn't fall into any
+                                        of the handled checks
+        :raises UnprocessableEntity: If a 422 response code is received and
+                                     couldn't be parsed
+        :returns: None if ignore_errors is True, the response status code
+                  if not.
+        """
+        return self._delete_obj(obj_id=member_id,
+                                parent_id=pool_id,
+                                ignore_errors=ignore_errors)
+
+    def cleanup_member(self, member_id, pool_id, lb_client=None, lb_id=None):
+        kwargs = {arg: value for arg, value in locals().items()
+                  if arg != 'self' and value is not Unset}
+        kwargs['obj_id'] = kwargs.pop('member_id')
+        kwargs['parent_id'] = kwargs.pop('pool_id')
+        return self._cleanup_obj(**kwargs)
+
+    def is_resource_deleted(self, id):
+        # Trying to implement this for members would be impossible, because
+        # they are sub-objects that can't be referenced directly, and this is
+        # used internally in tempest where we have no control over passed args
+        raise NotImplementedError()
diff --git a/octavia_tempest_plugin/services/load_balancer/v2/pool_client.py b/octavia_tempest_plugin/services/load_balancer/v2/pool_client.py
new file mode 100644
index 0000000..46ec38d
--- /dev/null
+++ b/octavia_tempest_plugin/services/load_balancer/v2/pool_client.py
@@ -0,0 +1,241 @@
+#   Copyright 2018 GoDaddy
+#
+#   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 tempest import config
+
+from octavia_tempest_plugin.services.load_balancer.v2 import base_client
+
+CONF = config.CONF
+Unset = base_client.Unset
+
+
+class PoolClient(base_client.BaseLBaaSClient):
+
+    root_tag = 'pool'
+    list_root_tag = 'pools'
+    resource_name = 'pool'
+
+    def create_pool(self, protocol, lb_algorithm, loadbalancer_id=Unset,
+                    listener_id=Unset, name=Unset, description=Unset,
+                    admin_state_up=Unset, session_persistence=Unset,
+                    return_object_only=True):
+        """Create a pool.
+
+        :param protocol: The protocol for the resource.
+        :param lb_algorithm: The load balancing algorithm for the pool.
+        :param loadbalancer_id: The ID of the load balancer for the pool.
+        :param listener_id: The ID of the listener for the pool.
+        :param name: Human-readable name of the resource.
+        :param description: A human-readable description for the resource.
+        :param admin_state_up: The administrative state of the resource, which
+                               is up (true) or down (false).
+        :param session_persistence: A JSON object specifying the session
+                                    persistence for the pool or null for no
+                                    session persistence.
+        :param return_object_only: If True, the response returns the object
+                                   inside the root tag. False returns the full
+                                   response from the API.
+        :raises AssertionError: if the expected_code isn't a valid http success
+                                response code
+        :raises BadRequest: If a 400 response code is received
+        :raises Conflict: If a 409 response code is received
+        :raises Forbidden: If a 403 response code is received
+        :raises Gone: If a 410 response code is received
+        :raises InvalidContentType: If a 415 response code is received
+        :raises InvalidHTTPResponseBody: The response body wasn't valid JSON
+        :raises InvalidHttpSuccessCode: if the read code isn't an expected
+                                        http success code
+        :raises NotFound: If a 404 response code is received
+        :raises NotImplemented: If a 501 response code is received
+        :raises OverLimit: If a 413 response code is received and over_limit is
+                           not in the response body
+        :raises RateLimitExceeded: If a 413 response code is received and
+                                   over_limit is in the response body
+        :raises ServerFault: If a 500 response code is received
+        :raises Unauthorized: If a 401 response code is received
+        :raises UnexpectedContentType: If the content-type of the response
+                                       isn't an expect type
+        :raises UnexpectedResponseCode: If a response code above 400 is
+                                        received and it doesn't fall into any
+                                        of the handled checks
+        :raises UnprocessableEntity: If a 422 response code is received and
+                                     couldn't be parsed
+        :returns: A pool object.
+        """
+        kwargs = {arg: value for arg, value in locals().items()
+                  if arg != 'self' and value is not Unset}
+        return self._create_object(**kwargs)
+
+    def show_pool(self, pool_id, query_params=None, return_object_only=True):
+        """Get pool details.
+
+        :param pool_id: The pool ID to query.
+        :param query_params: The optional query parameters to append to the
+                             request. Ex. fields=id&fields=name
+        :param return_object_only: If True, the response returns the object
+                                   inside the root tag. False returns the full
+                                   response from the API.
+        :raises AssertionError: if the expected_code isn't a valid http success
+                                response code
+        :raises BadRequest: If a 400 response code is received
+        :raises Conflict: If a 409 response code is received
+        :raises Forbidden: If a 403 response code is received
+        :raises Gone: If a 410 response code is received
+        :raises InvalidContentType: If a 415 response code is received
+        :raises InvalidHTTPResponseBody: The response body wasn't valid JSON
+        :raises InvalidHttpSuccessCode: if the read code isn't an expected
+                                        http success code
+        :raises NotFound: If a 404 response code is received
+        :raises NotImplemented: If a 501 response code is received
+        :raises OverLimit: If a 413 response code is received and over_limit is
+                           not in the response body
+        :raises RateLimitExceeded: If a 413 response code is received and
+                                   over_limit is in the response body
+        :raises ServerFault: If a 500 response code is received
+        :raises Unauthorized: If a 401 response code is received
+        :raises UnexpectedContentType: If the content-type of the response
+                                       isn't an expect type
+        :raises UnexpectedResponseCode: If a response code above 400 is
+                                        received and it doesn't fall into any
+                                        of the handled checks
+        :raises UnprocessableEntity: If a 422 response code is received and
+                                     couldn't be parsed
+        :returns: A pool object.
+        """
+        return self._show_object(obj_id=pool_id,
+                                 query_params=query_params,
+                                 return_object_only=return_object_only)
+
+    def list_pools(self, query_params=None, return_object_only=True):
+        """Get a list of pool objects.
+
+        :param query_params: The optional query parameters to append to the
+                             request. Ex. fields=id&fields=name
+        :param return_object_only: If True, the response returns the object
+                                   inside the root tag. False returns the full
+                                   response from the API.
+        :raises AssertionError: if the expected_code isn't a valid http success
+                                response code
+        :raises BadRequest: If a 400 response code is received
+        :raises Conflict: If a 409 response code is received
+        :raises Forbidden: If a 403 response code is received
+        :raises Gone: If a 410 response code is received
+        :raises InvalidContentType: If a 415 response code is received
+        :raises InvalidHTTPResponseBody: The response body wasn't valid JSON
+        :raises InvalidHttpSuccessCode: if the read code isn't an expected
+                                        http success code
+        :raises NotFound: If a 404 response code is received
+        :raises NotImplemented: If a 501 response code is received
+        :raises OverLimit: If a 413 response code is received and over_limit is
+                           not in the response body
+        :raises RateLimitExceeded: If a 413 response code is received and
+                                   over_limit is in the response body
+        :raises ServerFault: If a 500 response code is received
+        :raises Unauthorized: If a 401 response code is received
+        :raises UnexpectedContentType: If the content-type of the response
+                                       isn't an expect type
+        :raises UnexpectedResponseCode: If a response code above 400 is
+                                        received and it doesn't fall into any
+                                        of the handled checks
+        :raises UnprocessableEntity: If a 422 response code is received and
+                                     couldn't be parsed
+        :returns: A list of pool objects.
+        """
+        return self._list_objects(query_params=query_params,
+                                  return_object_only=return_object_only)
+
+    def update_pool(self, pool_id, lb_algorithm=Unset, name=Unset,
+                    description=Unset, admin_state_up=Unset,
+                    session_persistence=Unset, return_object_only=True):
+        """Update a pool.
+
+        :param pool_id: The pool ID to update.
+        :param lb_algorithm: The load balancing algorithm for the pool.
+        :param name: Human-readable name of the resource.
+        :param description: A human-readable description for the resource.
+        :param admin_state_up: The administrative state of the resource, which
+                               is up (true) or down (false).
+        :param session_persistence: A JSON object specifying the session
+                                    persistence for the pool or null for no
+                                    session persistence.
+        :param return_object_only: If True, the response returns the object
+                                   inside the root tag. False returns the full
+                                   response from the API.
+        :raises AssertionError: if the expected_code isn't a valid http success
+                                response code
+        :raises BadRequest: If a 400 response code is received
+        :raises Conflict: If a 409 response code is received
+        :raises Forbidden: If a 403 response code is received
+        :raises Gone: If a 410 response code is received
+        :raises InvalidContentType: If a 415 response code is received
+        :raises InvalidHTTPResponseBody: The response body wasn't valid JSON
+        :raises InvalidHttpSuccessCode: if the read code isn't an expected
+                                        http success code
+        :raises NotFound: If a 404 response code is received
+        :raises NotImplemented: If a 501 response code is received
+        :raises OverLimit: If a 413 response code is received and over_limit is
+                           not in the response body
+        :raises RateLimitExceeded: If a 413 response code is received and
+                                   over_limit is in the response body
+        :raises ServerFault: If a 500 response code is received
+        :raises Unauthorized: If a 401 response code is received
+        :raises UnexpectedContentType: If the content-type of the response
+                                       isn't an expect type
+        :raises UnexpectedResponseCode: If a response code above 400 is
+                                        received and it doesn't fall into any
+                                        of the handled checks
+        :raises UnprocessableEntity: If a 422 response code is received and
+                                     couldn't be parsed
+        :returns: A pool object.
+        """
+        kwargs = {arg: value for arg, value in locals().items()
+                  if arg != 'self' and value is not Unset}
+        kwargs['obj_id'] = kwargs.pop('pool_id')
+        return self._update_object(**kwargs)
+
+    def delete_pool(self, pool_id, ignore_errors=False):
+        """Delete a pool.
+
+        :param pool_id: The pool ID to delete.
+        :param ignore_errors: True if errors should be ignored.
+        :raises AssertionError: if the expected_code isn't a valid http success
+                                response code
+        :raises BadRequest: If a 400 response code is received
+        :raises Conflict: If a 409 response code is received
+        :raises Forbidden: If a 403 response code is received
+        :raises Gone: If a 410 response code is received
+        :raises InvalidContentType: If a 415 response code is received
+        :raises InvalidHTTPResponseBody: The response body wasn't valid JSON
+        :raises InvalidHttpSuccessCode: if the read code isn't an expected
+                                        http success code
+        :raises NotFound: If a 404 response code is received
+        :raises NotImplemented: If a 501 response code is received
+        :raises OverLimit: If a 413 response code is received and over_limit is
+                           not in the response body
+        :raises RateLimitExceeded: If a 413 response code is received and
+                                   over_limit is in the response body
+        :raises ServerFault: If a 500 response code is received
+        :raises Unauthorized: If a 401 response code is received
+        :raises UnexpectedContentType: If the content-type of the response
+                                       isn't an expect type
+        :raises UnexpectedResponseCode: If a response code above 400 is
+                                        received and it doesn't fall into any
+                                        of the handled checks
+        :raises UnprocessableEntity: If a 422 response code is received and
+                                     couldn't be parsed
+        :returns: None if ignore_errors is True, the response status code
+                  if not.
+        """
+        return self._delete_obj(obj_id=pool_id,
+                                ignore_errors=ignore_errors)
diff --git a/octavia_tempest_plugin/tests/api/v2/test_load_balancer.py b/octavia_tempest_plugin/tests/api/v2/test_load_balancer.py
index b62d4bd..935f7a9 100644
--- a/octavia_tempest_plugin/tests/api/v2/test_load_balancer.py
+++ b/octavia_tempest_plugin/tests/api/v2/test_load_balancer.py
@@ -15,13 +15,13 @@
 #    under the License.
 
 import testtools
+import time
 from uuid import UUID
 
 from dateutil import parser
 
 from tempest import config
 from tempest.lib.common.utils import data_utils
-from tempest.lib.common.utils import test_utils
 from tempest.lib import decorators
 from tempest.lib import exceptions
 
@@ -51,7 +51,7 @@
         """Tests load balancer create and basic show APIs.
 
         * Tests that users without the load balancer member role cannot
-        *   create load balancers.
+          create load balancers.
         * Create a fully populated load balancer.
         * Show load balancer details.
         * Validate the show reflects the requested values.
@@ -69,7 +69,7 @@
                      # vip_qos_policy_id=lb_qos_policy_id)
                      const.NAME: lb_name}
 
-        self._setup_lb_network_kwargs(lb_kwargs, ip_version)
+        self._setup_lb_network_kwargs(lb_kwargs, ip_version, use_fixed_ip=True)
 
         # Test that a user without the load balancer role cannot
         # create a load balancer
@@ -82,8 +82,7 @@
         lb = self.mem_lb_client.create_loadbalancer(**lb_kwargs)
 
         self.addClassResourceCleanup(
-            test_utils.call_and_ignore_notfound_exc,
-            self.mem_lb_client.delete_loadbalancer,
+            self.mem_lb_client.cleanup_loadbalancer,
             lb[const.ID])
 
         lb = waiters.wait_for_status(self.mem_lb_client.show_loadbalancer,
@@ -91,6 +90,12 @@
                                      const.ACTIVE,
                                      CONF.load_balancer.lb_build_interval,
                                      CONF.load_balancer.lb_build_timeout)
+        if not CONF.load_balancer.test_with_noop:
+            lb = waiters.wait_for_status(self.mem_lb_client.show_loadbalancer,
+                                         lb[const.ID], const.OPERATING_STATUS,
+                                         const.ONLINE,
+                                         CONF.load_balancer.check_interval,
+                                         CONF.load_balancer.check_timeout)
 
         self.assertTrue(lb[const.ADMIN_STATE_UP])
         parser.parse(lb[const.CREATED_AT])
@@ -103,6 +108,7 @@
             self.assertEqual(const.OFFLINE, lb[const.OPERATING_STATUS])
         else:
             self.assertEqual(const.ONLINE, lb[const.OPERATING_STATUS])
+
         self.assertEqual(self.os_roles_lb_member.credentials.project_id,
                          lb[const.PROJECT_ID])
         self.assertEqual(CONF.load_balancer.provider, lb[const.PROVIDER])
@@ -141,8 +147,7 @@
         lb = self.mem_lb_client.create_loadbalancer(
             name=lb_name, vip_network_id=self.lb_member_vip_net[const.ID])
         self.addClassResourceCleanup(
-            test_utils.call_and_ignore_notfound_exc,
-            self.mem_lb_client.delete_loadbalancer,
+            self.mem_lb_client.cleanup_loadbalancer,
             lb[const.ID])
 
         lb = waiters.wait_for_status(self.mem_lb_client.show_loadbalancer,
@@ -188,8 +193,7 @@
         lb = self.mem_lb_client.create_loadbalancer(
             name=lb_name, vip_network_id=self.lb_member_vip_net[const.ID])
         self.addClassResourceCleanup(
-            test_utils.call_and_ignore_notfound_exc,
-            self.mem_lb_client.delete_loadbalancer,
+            self.mem_lb_client.cleanup_loadbalancer,
             lb[const.ID])
 
         lb = waiters.wait_for_status(self.mem_lb_client.show_loadbalancer,
@@ -224,7 +228,7 @@
             CONF.load_balancer.lb_build_interval,
             CONF.load_balancer.lb_build_timeout)
 
-    # Helper functions for test load balancer list
+    # Helper functions for test loadbalancer list
     def _filter_lbs_by_id(self, lbs, ids):
         return [lb for lb in lbs if lb['id'] not in ids]
 
@@ -252,7 +256,7 @@
         pretest_lb_ids = [lb['id'] for lb in pretest_lbs]
 
         lb_name = data_utils.rand_name("lb_member_lb2-list")
-        lb_description = 'B'
+        lb_description = data_utils.rand_name('B')
 
         lb = self.mem_lb_client.create_loadbalancer(
             admin_state_up=True,
@@ -264,9 +268,8 @@
             # TODO(johnsom) Add QoS
             # vip_qos_policy_id=lb_qos_policy_id)
             vip_network_id=self.lb_member_vip_net[const.ID])
-        self.addClassResourceCleanup(
-            test_utils.call_and_ignore_notfound_exc,
-            self.mem_lb_client.delete_loadbalancer,
+        self.addCleanup(
+            self.mem_lb_client.cleanup_loadbalancer,
             lb[const.ID])
 
         lb1 = waiters.wait_for_status(self.mem_lb_client.show_loadbalancer,
@@ -275,18 +278,28 @@
                                       const.ACTIVE,
                                       CONF.load_balancer.lb_build_interval,
                                       CONF.load_balancer.lb_build_timeout)
+        if not CONF.load_balancer.test_with_noop:
+            lb1 = waiters.wait_for_status(self.mem_lb_client.show_loadbalancer,
+                                          lb[const.ID], const.OPERATING_STATUS,
+                                          const.ONLINE,
+                                          CONF.load_balancer.check_interval,
+                                          CONF.load_balancer.check_timeout)
+
+        # Time resolution for created_at is only to the second, and we need to
+        # ensure that each object has a distinct creation time. Delaying one
+        # second is both a simple and a reliable way to accomplish this.
+        time.sleep(1)
 
         lb_name = data_utils.rand_name("lb_member_lb1-list")
-        lb_description = 'A'
+        lb_description = data_utils.rand_name('A')
 
         lb = self.mem_lb_client.create_loadbalancer(
             admin_state_up=True,
             description=lb_description,
             name=lb_name,
             vip_network_id=self.lb_member_vip_net[const.ID])
-        self.addClassResourceCleanup(
-            test_utils.call_and_ignore_notfound_exc,
-            self.mem_lb_client.delete_loadbalancer,
+        self.addCleanup(
+            self.mem_lb_client.cleanup_loadbalancer,
             lb[const.ID])
 
         lb2 = waiters.wait_for_status(self.mem_lb_client.show_loadbalancer,
@@ -295,18 +308,28 @@
                                       const.ACTIVE,
                                       CONF.load_balancer.lb_build_interval,
                                       CONF.load_balancer.lb_build_timeout)
+        if not CONF.load_balancer.test_with_noop:
+            lb2 = waiters.wait_for_status(self.mem_lb_client.show_loadbalancer,
+                                          lb[const.ID], const.OPERATING_STATUS,
+                                          const.ONLINE,
+                                          CONF.load_balancer.check_interval,
+                                          CONF.load_balancer.check_timeout)
+
+        # Time resolution for created_at is only to the second, and we need to
+        # ensure that each object has a distinct creation time. Delaying one
+        # second is both a simple and a reliable way to accomplish this.
+        time.sleep(1)
 
         lb_name = data_utils.rand_name("lb_member_lb3-list")
-        lb_description = 'C'
+        lb_description = data_utils.rand_name('C')
 
         lb = self.mem_lb_client.create_loadbalancer(
             admin_state_up=False,
             description=lb_description,
             name=lb_name,
             vip_network_id=self.lb_member_vip_net[const.ID])
-        self.addClassResourceCleanup(
-            test_utils.call_and_ignore_notfound_exc,
-            self.mem_lb_client.delete_loadbalancer,
+        self.addCleanup(
+            self.mem_lb_client.cleanup_loadbalancer,
             lb[const.ID])
 
         lb3 = waiters.wait_for_status(self.mem_lb_client.show_loadbalancer,
@@ -411,25 +434,6 @@
         self.assertEqual(lb2[const.DESCRIPTION], lbs[1][const.DESCRIPTION])
         self.assertEqual(lb1[const.DESCRIPTION], lbs[0][const.DESCRIPTION])
 
-        # Attempt to clean up so that one full test run doesn't start 10+
-        # amps before the cleanup phase fires
-        created_lb_ids = lb1[const.ID], lb2[const.ID], lb3[const.ID]
-        for lb_id in created_lb_ids:
-            try:
-                self.mem_lb_client.delete_loadbalancer(lb_id)
-            except Exception:
-                pass
-
-        for lb_id in created_lb_ids:
-            try:
-                waiters.wait_for_deleted_status_or_not_found(
-                    self.mem_lb_client.show_loadbalancer, lb_id,
-                    const.PROVISIONING_STATUS,
-                    CONF.load_balancer.lb_build_interval,
-                    CONF.load_balancer.lb_build_timeout)
-            except Exception:
-                pass
-
     @decorators.idempotent_id('826ae612-8717-4c64-a8a7-cb9570a85870')
     def test_load_balancer_show(self):
         """Tests load balancer show API.
@@ -451,13 +455,12 @@
                      const.PROVIDER: CONF.load_balancer.provider,
                      const.NAME: lb_name}
 
-        self._setup_lb_network_kwargs(lb_kwargs, 4)
+        self._setup_lb_network_kwargs(lb_kwargs, 4, use_fixed_ip=True)
 
         lb = self.mem_lb_client.create_loadbalancer(**lb_kwargs)
 
         self.addClassResourceCleanup(
-            test_utils.call_and_ignore_notfound_exc,
-            self.mem_lb_client.delete_loadbalancer,
+            self.mem_lb_client.cleanup_loadbalancer,
             lb[const.ID])
 
         lb = waiters.wait_for_status(self.mem_lb_client.show_loadbalancer,
@@ -528,7 +531,7 @@
 
     @decorators.idempotent_id('b75a4d15-49d2-4149-a745-635eed1aacc3')
     def test_load_balancer_update(self):
-        """Tests load balancer show API and field filtering.
+        """Tests load balancer update and show APIs.
 
         * Create a fully populated load balancer.
         * Show load balancer details.
@@ -536,7 +539,7 @@
         * Validates that other accounts cannot update the load balancer.
         * Update the load balancer details.
         * Show load balancer details.
-        * Validate the show reflects the initial values.
+        * Validate the show reflects the updated values.
         """
         lb_name = data_utils.rand_name("lb_member_lb1-update")
         lb_description = data_utils.arbitrary_string(size=255)
@@ -550,13 +553,12 @@
                      # vip_qos_policy_id=lb_qos_policy_id)
                      const.NAME: lb_name}
 
-        self._setup_lb_network_kwargs(lb_kwargs, 4)
+        self._setup_lb_network_kwargs(lb_kwargs, 4, use_fixed_ip=True)
 
         lb = self.mem_lb_client.create_loadbalancer(**lb_kwargs)
 
         self.addClassResourceCleanup(
-            test_utils.call_and_ignore_notfound_exc,
-            self.mem_lb_client.delete_loadbalancer,
+            self.mem_lb_client.cleanup_loadbalancer,
             lb[const.ID])
 
         lb = waiters.wait_for_status(self.mem_lb_client.show_loadbalancer,
@@ -660,8 +662,7 @@
         lb = self.mem_lb_client.create_loadbalancer(
             name=lb_name, vip_network_id=self.lb_member_vip_net[const.ID])
         self.addClassResourceCleanup(
-            test_utils.call_and_ignore_notfound_exc,
-            self.mem_lb_client.delete_loadbalancer,
+            self.mem_lb_client.cleanup_loadbalancer,
             lb[const.ID])
 
         lb = waiters.wait_for_status(self.mem_lb_client.show_loadbalancer,
@@ -722,8 +723,7 @@
         lb = self.mem_lb_client.create_loadbalancer(
             name=lb_name, vip_network_id=self.lb_member_vip_net[const.ID])
         self.addClassResourceCleanup(
-            test_utils.call_and_ignore_notfound_exc,
-            self.mem_lb_client.delete_loadbalancer,
+            self.mem_lb_client.cleanup_loadbalancer,
             lb[const.ID])
 
         lb = waiters.wait_for_status(self.mem_lb_client.show_loadbalancer,
@@ -731,6 +731,12 @@
                                      const.ACTIVE,
                                      CONF.load_balancer.lb_build_interval,
                                      CONF.load_balancer.lb_build_timeout)
+        if not CONF.load_balancer.test_with_noop:
+            lb = waiters.wait_for_status(self.mem_lb_client.show_loadbalancer,
+                                         lb[const.ID], const.OPERATING_STATUS,
+                                         const.ONLINE,
+                                         CONF.load_balancer.check_interval,
+                                         CONF.load_balancer.check_timeout)
 
         # Test that a user, without the load balancer member role, cannot
         # use this method
@@ -790,8 +796,7 @@
         lb = self.mem_lb_client.create_loadbalancer(
             name=lb_name, vip_network_id=self.lb_member_vip_net[const.ID])
         self.addClassResourceCleanup(
-            test_utils.call_and_ignore_notfound_exc,
-            self.mem_lb_client.delete_loadbalancer,
+            self.mem_lb_client.cleanup_loadbalancer,
             lb[const.ID])
 
         lb = waiters.wait_for_status(self.mem_lb_client.show_loadbalancer,
diff --git a/octavia_tempest_plugin/tests/scenario/v2/test_load_balancer.py b/octavia_tempest_plugin/tests/scenario/v2/test_load_balancer.py
index e56a9bb..a10784b 100644
--- a/octavia_tempest_plugin/tests/scenario/v2/test_load_balancer.py
+++ b/octavia_tempest_plugin/tests/scenario/v2/test_load_balancer.py
@@ -19,7 +19,6 @@
 
 from tempest import config
 from tempest.lib.common.utils import data_utils
-from tempest.lib.common.utils import test_utils
 from tempest.lib import decorators
 
 from octavia_tempest_plugin.common import constants as const
@@ -61,8 +60,7 @@
 
         lb = self.mem_lb_client.create_loadbalancer(**lb_kwargs)
         self.addClassResourceCleanup(
-            test_utils.call_and_ignore_notfound_exc,
-            self.mem_lb_client.delete_loadbalancer,
+            self.mem_lb_client.cleanup_loadbalancer,
             lb[const.ID])
 
         lb = waiters.wait_for_status(self.mem_lb_client.show_loadbalancer,
@@ -85,8 +83,6 @@
                          lb[const.VIP_NETWORK_ID])
         self.assertIsNotNone(lb[const.VIP_PORT_ID])
         if lb_kwargs[const.VIP_SUBNET_ID]:
-            self.assertEqual(lb_kwargs[const.VIP_ADDRESS],
-                             lb[const.VIP_ADDRESS])
             self.assertEqual(lb_kwargs[const.VIP_SUBNET_ID],
                              lb[const.VIP_SUBNET_ID])
 
@@ -113,8 +109,8 @@
         # Load balancer delete
         self.mem_lb_client.delete_loadbalancer(lb[const.ID], cascade=True)
 
-        waiters.wait_for_status(self.mem_lb_client.show_loadbalancer,
-                                lb[const.ID], const.PROVISIONING_STATUS,
-                                const.DELETED,
-                                CONF.load_balancer.lb_build_interval,
-                                CONF.load_balancer.lb_build_timeout)
+        waiters.wait_for_deleted_status_or_not_found(
+            self.mem_lb_client.show_loadbalancer, lb[const.ID],
+            const.PROVISIONING_STATUS,
+            CONF.load_balancer.check_interval,
+            CONF.load_balancer.check_timeout)
diff --git a/octavia_tempest_plugin/tests/test_base.py b/octavia_tempest_plugin/tests/test_base.py
index 978068a..c70c2f1 100644
--- a/octavia_tempest_plugin/tests/test_base.py
+++ b/octavia_tempest_plugin/tests/test_base.py
@@ -15,18 +15,19 @@
 import ipaddress
 import pkg_resources
 import random
+import requests
 import shlex
 import six
 import string
 import subprocess
 import tempfile
+import time
 
 from oslo_log import log as logging
 from oslo_utils import uuidutils
 from tempest import config
 from tempest.lib.common.utils import data_utils
 from tempest.lib.common.utils.linux import remote_client
-from tempest.lib.common.utils import test_utils
 from tempest.lib import exceptions
 from tempest import test
 
@@ -50,6 +51,9 @@
                    ['lb_admin', CONF.load_balancer.admin_role]]
 
     client_manager = clients.ManagerV2
+    webserver1_response = 1
+    webserver2_response = 5
+    used_ips = []
 
     @classmethod
     def skip_checks(cls):
@@ -104,6 +108,14 @@
         cls.lb_mem_servers_client = cls.os_roles_lb_member.servers_client
         cls.lb_mem_subnet_client = cls.os_roles_lb_member.subnets_client
         cls.mem_lb_client = cls.os_roles_lb_member.loadbalancer_client
+        cls.mem_listener_client = cls.os_roles_lb_member.listener_client
+        cls.mem_pool_client = cls.os_roles_lb_member.pool_client
+        cls.mem_member_client = cls.os_roles_lb_member.member_client
+        cls.mem_healthmonitor_client = (
+            cls.os_roles_lb_member.healthmonitor_client)
+        cls.mem_l7policy_client = cls.os_roles_lb_member.l7policy_client
+        cls.mem_l7rule_client = cls.os_roles_lb_member.l7rule_client
+        cls.mem_amphora_client = cls.os_roles_lb_member.amphora_client
 
     @classmethod
     def resource_setup(cls):
@@ -112,6 +124,8 @@
 
         conf_lb = CONF.load_balancer
 
+        cls.api_version = cls.mem_lb_client.get_max_api_version()
+
         if conf_lb.test_subnet_override and not conf_lb.test_network_override:
             raise exceptions.InvalidConfiguration(
                 "Configuration value test_network_override must be "
@@ -177,184 +191,16 @@
         if cls.lb_member_2_subnet:
             LOG.debug('Octavia Setup: lb_member_2_subnet = {}'.format(
                 cls.lb_member_2_subnet[const.ID]))
-        if cls.lb_member_vip_ipv6_subnet:
-            LOG.debug('Octavia Setup: lb_member_vip_ipv6_subnet = {}'.format(
-                cls.lb_member_vip_ipv6_subnet[const.ID]))
-        if cls.lb_member_1_ipv6_subnet:
-            LOG.debug('Octavia Setup: lb_member_1_ipv6_subnet = {}'.format(
-                cls.lb_member_1_ipv6_subnet[const.ID]))
-        if cls.lb_member_2_ipv6_subnet:
-            LOG.debug('Octavia Setup: lb_member_2_ipv6_subnet = {}'.format(
-                cls.lb_member_2_ipv6_subnet[const.ID]))
-
-        # If validation is disabled in this cloud, we won't be able to
-        # start the webservers, so don't even boot them.
-        if not CONF.validation.run_validation:
-            return
-
-        # Create a keypair for the webservers
-        keypair_name = data_utils.rand_name('lb_member_keypair')
-        result = cls.lb_mem_keypairs_client.create_keypair(
-            name=keypair_name)
-        cls.lb_member_keypair = result['keypair']
-        LOG.info('lb_member_keypair: {}'.format(cls.lb_member_keypair))
-        cls.addClassResourceCleanup(
-            waiters.wait_for_not_found,
-            cls.lb_mem_keypairs_client.delete_keypair,
-            cls.lb_mem_keypairs_client.show_keypair,
-            keypair_name)
-
-        if (CONF.load_balancer.enable_security_groups and
-                CONF.network_feature_enabled.port_security):
-            # Set up the security group for the webservers
-            SG_name = data_utils.rand_name('lb_member_SG')
-            cls.lb_member_sec_group = (
-                cls.lb_mem_SG_client.create_security_group(
-                    name=SG_name)['security_group'])
-            cls.addClassResourceCleanup(
-                waiters.wait_for_not_found,
-                cls.lb_mem_SG_client.delete_security_group,
-                cls.lb_mem_SG_client.show_security_group,
-                cls.lb_member_sec_group['id'])
-
-            # Create a security group rule to allow 80-81 (test webservers)
-            SGr = cls.lb_mem_SGr_client.create_security_group_rule(
-                direction='ingress',
-                security_group_id=cls.lb_member_sec_group['id'],
-                protocol='tcp',
-                ethertype='IPv4',
-                port_range_min=80,
-                port_range_max=81)['security_group_rule']
-            cls.addClassResourceCleanup(
-                waiters.wait_for_not_found,
-                cls.lb_mem_SGr_client.delete_security_group_rule,
-                cls.lb_mem_SGr_client.show_security_group_rule,
-                SGr['id'])
-            # Create a security group rule to allow 22 (ssh)
-            SGr = cls.lb_mem_SGr_client.create_security_group_rule(
-                direction='ingress',
-                security_group_id=cls.lb_member_sec_group['id'],
-                protocol='tcp',
-                ethertype='IPv4',
-                port_range_min=22,
-                port_range_max=22)['security_group_rule']
-            cls.addClassResourceCleanup(
-                waiters.wait_for_not_found,
-                cls.lb_mem_SGr_client.delete_security_group_rule,
-                cls.lb_mem_SGr_client.show_security_group_rule,
-                SGr['id'])
-            if CONF.load_balancer.test_with_ipv6:
-                # Create a security group rule to allow 80-81 (test webservers)
-                SGr = cls.lb_mem_SGr_client.create_security_group_rule(
-                    direction='ingress',
-                    security_group_id=cls.lb_member_sec_group['id'],
-                    protocol='tcp',
-                    ethertype='IPv6',
-                    port_range_min=80,
-                    port_range_max=81)['security_group_rule']
-                cls.addClassResourceCleanup(
-                    waiters.wait_for_not_found,
-                    cls.lb_mem_SGr_client.delete_security_group_rule,
-                    cls.lb_mem_SGr_client.show_security_group_rule,
-                    SGr['id'])
-                # Create a security group rule to allow 22 (ssh)
-                SGr = cls.lb_mem_SGr_client.create_security_group_rule(
-                    direction='ingress',
-                    security_group_id=cls.lb_member_sec_group['id'],
-                    protocol='tcp',
-                    ethertype='IPv6',
-                    port_range_min=22,
-                    port_range_max=22)['security_group_rule']
-                cls.addClassResourceCleanup(
-                    waiters.wait_for_not_found,
-                    cls.lb_mem_SGr_client.delete_security_group_rule,
-                    cls.lb_mem_SGr_client.show_security_group_rule,
-                    SGr['id'])
-
-            LOG.info('lb_member_sec_group: {}'.format(cls.lb_member_sec_group))
-
-        # Create webserver 1 instance
-        server_details = cls._create_webserver('lb_member_webserver1',
-                                               cls.lb_member_1_net)
-
-        cls.lb_member_webserver1 = server_details['server']
-        cls.webserver1_ip = server_details.get('ipv4_address')
-        cls.webserver1_ipv6 = server_details.get('ipv6_address')
-        cls.webserver1_public_ip = server_details['public_ipv4_address']
-
-        LOG.debug('Octavia Setup: lb_member_webserver1 = {}'.format(
-            cls.lb_member_webserver1[const.ID]))
-        LOG.debug('Octavia Setup: webserver1_ip = {}'.format(
-            cls.webserver1_ip))
-        LOG.debug('Octavia Setup: webserver1_ipv6 = {}'.format(
-            cls.webserver1_ipv6))
-        LOG.debug('Octavia Setup: webserver1_public_ip = {}'.format(
-            cls.webserver1_public_ip))
-
-        cls._install_start_webserver(cls.webserver1_public_ip,
-                                     cls.lb_member_keypair['private_key'], 1)
-
-        # Validate webserver 1
-        cls._validate_webserver(cls.webserver1_public_ip, 1)
-
-        # Create webserver 2 instance
-        server_details = cls._create_webserver('lb_member_webserver2',
-                                               cls.lb_member_2_net)
-
-        cls.lb_member_webserver2 = server_details['server']
-        cls.webserver2_ip = server_details.get('ipv4_address')
-        cls.webserver2_ipv6 = server_details.get('ipv6_address')
-        cls.webserver2_public_ip = server_details['public_ipv4_address']
-
-        LOG.debug('Octavia Setup: lb_member_webserver2 = {}'.format(
-            cls.lb_member_webserver2[const.ID]))
-        LOG.debug('Octavia Setup: webserver2_ip = {}'.format(
-            cls.webserver2_ip))
-        LOG.debug('Octavia Setup: webserver2_ipv6 = {}'.format(
-            cls.webserver2_ipv6))
-        LOG.debug('Octavia Setup: webserver2_public_ip = {}'.format(
-            cls.webserver2_public_ip))
-
-        cls._install_start_webserver(cls.webserver2_public_ip,
-                                     cls.lb_member_keypair['private_key'], 5)
-
-        # Validate webserver 2
-        cls._validate_webserver(cls.webserver2_public_ip, 5)
-
-    @classmethod
-    def _install_start_webserver(cls, ip_address, ssh_key, start_id):
-        local_file = pkg_resources.resource_filename(
-            'octavia_tempest_plugin.contrib.httpd', 'httpd.bin')
-        dest_file = '/dev/shm/httpd.bin'
-
-        linux_client = remote_client.RemoteClient(
-            ip_address, CONF.validation.image_ssh_user, pkey=ssh_key)
-        linux_client.validate_authentication()
-
-        with tempfile.NamedTemporaryFile() as key:
-            key.write(ssh_key.encode('utf-8'))
-            key.flush()
-            cmd = ("scp -v -o UserKnownHostsFile=/dev/null "
-                   "-o StrictHostKeyChecking=no "
-                   "-o ConnectTimeout={0} -o ConnectionAttempts={1} "
-                   "-i {2} {3} {4}@{5}:{6}").format(
-                CONF.load_balancer.scp_connection_timeout,
-                CONF.load_balancer.scp_connection_attempts,
-                key.name, local_file, CONF.validation.image_ssh_user,
-                ip_address, dest_file)
-            args = shlex.split(cmd)
-            subprocess_args = {'stdout': subprocess.PIPE,
-                               'stderr': subprocess.STDOUT,
-                               'cwd': None}
-            proc = subprocess.Popen(args, **subprocess_args)
-            stdout, stderr = proc.communicate()
-            if proc.returncode != 0:
-                raise exceptions.CommandFailed(proc.returncode, cmd,
-                                               stdout, stderr)
-        linux_client.exec_command('sudo screen -d -m {0} -port 80 '
-                                  '-id {1}'.format(dest_file, start_id))
-        linux_client.exec_command('sudo screen -d -m {0} -port 81 '
-                                  '-id {1}'.format(dest_file, start_id + 1))
+        if CONF.load_balancer.test_with_ipv6:
+            if cls.lb_member_vip_ipv6_subnet:
+                LOG.debug('Octavia Setup: lb_member_vip_ipv6_subnet = '
+                          '{}'.format(cls.lb_member_vip_ipv6_subnet[const.ID]))
+            if cls.lb_member_1_ipv6_subnet:
+                LOG.debug('Octavia Setup: lb_member_1_ipv6_subnet = {}'.format(
+                    cls.lb_member_1_ipv6_subnet[const.ID]))
+            if cls.lb_member_2_ipv6_subnet:
+                LOG.debug('Octavia Setup: lb_member_2_ipv6_subnet = {}'.format(
+                    cls.lb_member_2_ipv6_subnet[const.ID]))
 
     @classmethod
     def _create_networks(cls):
@@ -517,6 +363,184 @@
                 cls.lb_mem_subnet_client.show_subnet,
                 cls.lb_member_2_ipv6_subnet['id'])
 
+    @classmethod
+    def _setup_lb_network_kwargs(cls, lb_kwargs, ip_version=None,
+                                 use_fixed_ip=False):
+        if not ip_version:
+            ip_version = 6 if CONF.load_balancer.test_with_ipv6 else 4
+        if cls.lb_member_vip_subnet:
+            ip_index = data_utils.rand_int_id(start=10, end=100)
+            while ip_index in cls.used_ips:
+                ip_index = data_utils.rand_int_id(start=10, end=100)
+            cls.used_ips.append(ip_index)
+            if ip_version == 4:
+                network = ipaddress.IPv4Network(
+                    six.u(CONF.load_balancer.vip_subnet_cidr))
+                lb_vip_address = str(network[ip_index])
+                subnet_id = cls.lb_member_vip_subnet[const.ID]
+            else:
+                network = ipaddress.IPv6Network(
+                    six.u(CONF.load_balancer.vip_ipv6_subnet_cidr))
+                lb_vip_address = str(network[ip_index])
+                subnet_id = cls.lb_member_vip_ipv6_subnet[const.ID]
+            lb_kwargs[const.VIP_SUBNET_ID] = subnet_id
+            if use_fixed_ip:
+                lb_kwargs[const.VIP_ADDRESS] = lb_vip_address
+            if CONF.load_balancer.test_with_noop:
+                lb_kwargs[const.VIP_NETWORK_ID] = (
+                    cls.lb_member_vip_net[const.ID])
+        else:
+            lb_kwargs[const.VIP_NETWORK_ID] = cls.lb_member_vip_net[const.ID]
+            lb_kwargs[const.VIP_SUBNET_ID] = None
+
+
+class LoadBalancerBaseTestWithCompute(LoadBalancerBaseTest):
+    @classmethod
+    def resource_setup(cls):
+        super(LoadBalancerBaseTestWithCompute, cls).resource_setup()
+        # If validation is disabled in this cloud, we won't be able to
+        # start the webservers, so don't even boot them.
+        if not CONF.validation.run_validation:
+            return
+
+        # Create a keypair for the webservers
+        keypair_name = data_utils.rand_name('lb_member_keypair')
+        result = cls.lb_mem_keypairs_client.create_keypair(
+            name=keypair_name)
+        cls.lb_member_keypair = result['keypair']
+        LOG.info('lb_member_keypair: {}'.format(cls.lb_member_keypair))
+        cls.addClassResourceCleanup(
+            waiters.wait_for_not_found,
+            cls.lb_mem_keypairs_client.delete_keypair,
+            cls.lb_mem_keypairs_client.show_keypair,
+            keypair_name)
+
+        if (CONF.load_balancer.enable_security_groups and
+                CONF.network_feature_enabled.port_security):
+            # Set up the security group for the webservers
+            SG_name = data_utils.rand_name('lb_member_SG')
+            cls.lb_member_sec_group = (
+                cls.lb_mem_SG_client.create_security_group(
+                    name=SG_name)['security_group'])
+            cls.addClassResourceCleanup(
+                waiters.wait_for_not_found,
+                cls.lb_mem_SG_client.delete_security_group,
+                cls.lb_mem_SG_client.show_security_group,
+                cls.lb_member_sec_group['id'])
+
+            # Create a security group rule to allow 80-81 (test webservers)
+            SGr = cls.lb_mem_SGr_client.create_security_group_rule(
+                direction='ingress',
+                security_group_id=cls.lb_member_sec_group['id'],
+                protocol='tcp',
+                ethertype='IPv4',
+                port_range_min=80,
+                port_range_max=81)['security_group_rule']
+            cls.addClassResourceCleanup(
+                waiters.wait_for_not_found,
+                cls.lb_mem_SGr_client.delete_security_group_rule,
+                cls.lb_mem_SGr_client.show_security_group_rule,
+                SGr['id'])
+            # Create a security group rule to allow 22 (ssh)
+            SGr = cls.lb_mem_SGr_client.create_security_group_rule(
+                direction='ingress',
+                security_group_id=cls.lb_member_sec_group['id'],
+                protocol='tcp',
+                ethertype='IPv4',
+                port_range_min=22,
+                port_range_max=22)['security_group_rule']
+            cls.addClassResourceCleanup(
+                waiters.wait_for_not_found,
+                cls.lb_mem_SGr_client.delete_security_group_rule,
+                cls.lb_mem_SGr_client.show_security_group_rule,
+                SGr['id'])
+            if CONF.load_balancer.test_with_ipv6:
+                # Create a security group rule to allow 80-81 (test webservers)
+                SGr = cls.lb_mem_SGr_client.create_security_group_rule(
+                    direction='ingress',
+                    security_group_id=cls.lb_member_sec_group['id'],
+                    protocol='tcp',
+                    ethertype='IPv6',
+                    port_range_min=80,
+                    port_range_max=81)['security_group_rule']
+                cls.addClassResourceCleanup(
+                    waiters.wait_for_not_found,
+                    cls.lb_mem_SGr_client.delete_security_group_rule,
+                    cls.lb_mem_SGr_client.show_security_group_rule,
+                    SGr['id'])
+                # Create a security group rule to allow 22 (ssh)
+                SGr = cls.lb_mem_SGr_client.create_security_group_rule(
+                    direction='ingress',
+                    security_group_id=cls.lb_member_sec_group['id'],
+                    protocol='tcp',
+                    ethertype='IPv6',
+                    port_range_min=22,
+                    port_range_max=22)['security_group_rule']
+                cls.addClassResourceCleanup(
+                    waiters.wait_for_not_found,
+                    cls.lb_mem_SGr_client.delete_security_group_rule,
+                    cls.lb_mem_SGr_client.show_security_group_rule,
+                    SGr['id'])
+
+            LOG.info('lb_member_sec_group: {}'.format(cls.lb_member_sec_group))
+
+        # Create webserver 1 instance
+        server_details = cls._create_webserver('lb_member_webserver1',
+                                               cls.lb_member_1_net)
+
+        cls.lb_member_webserver1 = server_details['server']
+        cls.webserver1_ip = server_details.get('ipv4_address')
+        cls.webserver1_ipv6 = server_details.get('ipv6_address')
+        cls.webserver1_public_ip = server_details['public_ipv4_address']
+
+        LOG.debug('Octavia Setup: lb_member_webserver1 = {}'.format(
+            cls.lb_member_webserver1[const.ID]))
+        LOG.debug('Octavia Setup: webserver1_ip = {}'.format(
+            cls.webserver1_ip))
+        LOG.debug('Octavia Setup: webserver1_ipv6 = {}'.format(
+            cls.webserver1_ipv6))
+        LOG.debug('Octavia Setup: webserver1_public_ip = {}'.format(
+            cls.webserver1_public_ip))
+
+        # Create webserver 2 instance
+        server_details = cls._create_webserver('lb_member_webserver2',
+                                               cls.lb_member_2_net)
+
+        cls.lb_member_webserver2 = server_details['server']
+        cls.webserver2_ip = server_details.get('ipv4_address')
+        cls.webserver2_ipv6 = server_details.get('ipv6_address')
+        cls.webserver2_public_ip = server_details['public_ipv4_address']
+
+        LOG.debug('Octavia Setup: lb_member_webserver2 = {}'.format(
+            cls.lb_member_webserver2[const.ID]))
+        LOG.debug('Octavia Setup: webserver2_ip = {}'.format(
+            cls.webserver2_ip))
+        LOG.debug('Octavia Setup: webserver2_ipv6 = {}'.format(
+            cls.webserver2_ipv6))
+        LOG.debug('Octavia Setup: webserver2_public_ip = {}'.format(
+            cls.webserver2_public_ip))
+
+        # Set up serving on webserver 1
+        cls._install_start_webserver(cls.webserver1_public_ip,
+                                     cls.lb_member_keypair['private_key'],
+                                     cls.webserver1_response)
+
+        # Validate webserver 1
+        cls._validate_webserver(cls.webserver1_public_ip,
+                                cls.webserver1_response)
+
+        # Set up serving on webserver 2
+        cls._install_start_webserver(cls.webserver2_public_ip,
+                                     cls.lb_member_keypair['private_key'],
+                                     cls.webserver2_response)
+
+        # Validate webserver 2
+        cls._validate_webserver(cls.webserver2_public_ip,
+                                cls.webserver2_response)
+
+    @classmethod
+    def _create_networks(cls):
+        super(LoadBalancerBaseTestWithCompute, cls)._create_networks()
         # Create a router for the subnets (required for the floating IP)
         router_name = data_utils.rand_name("lb_member_router")
         result = cls.lb_mem_routers_client.create_router(
@@ -548,7 +572,6 @@
             subnet_id=cls.lb_member_1_subnet['id'])
         cls.addClassResourceCleanup(
             waiters.wait_for_not_found,
-            test_utils.call_and_ignore_notfound_exc,
             cls.lb_mem_routers_client.remove_router_interface,
             cls.lb_mem_routers_client.remove_router_interface,
             cls.lb_member_router['id'], subnet_id=cls.lb_member_1_subnet['id'])
@@ -653,31 +676,118 @@
         return webserver_details
 
     @classmethod
+    def _install_start_webserver(cls, ip_address, ssh_key, start_id):
+        local_file = pkg_resources.resource_filename(
+            'octavia_tempest_plugin.contrib.httpd', 'httpd.bin')
+        dest_file = '/dev/shm/httpd.bin'
+
+        linux_client = remote_client.RemoteClient(
+            ip_address, CONF.validation.image_ssh_user, pkey=ssh_key)
+        linux_client.validate_authentication()
+
+        with tempfile.NamedTemporaryFile() as key:
+            key.write(ssh_key.encode('utf-8'))
+            key.flush()
+            cmd = ("scp -v -o UserKnownHostsFile=/dev/null "
+                   "-o StrictHostKeyChecking=no "
+                   "-o ConnectTimeout={0} -o ConnectionAttempts={1} "
+                   "-i {2} {3} {4}@{5}:{6}").format(
+                CONF.load_balancer.scp_connection_timeout,
+                CONF.load_balancer.scp_connection_attempts,
+                key.name, local_file, CONF.validation.image_ssh_user,
+                ip_address, dest_file)
+            args = shlex.split(cmd)
+            subprocess_args = {'stdout': subprocess.PIPE,
+                               'stderr': subprocess.STDOUT,
+                               'cwd': None}
+            proc = subprocess.Popen(args, **subprocess_args)
+            stdout, stderr = proc.communicate()
+            if proc.returncode != 0:
+                raise exceptions.CommandFailed(proc.returncode, cmd,
+                                               stdout, stderr)
+        linux_client.exec_command('sudo screen -d -m {0} -port 80 '
+                                  '-id {1}'.format(dest_file, start_id))
+        linux_client.exec_command('sudo screen -d -m {0} -port 81 '
+                                  '-id {1}'.format(dest_file, start_id + 1))
+
+    @classmethod
     def _validate_webserver(cls, ip_address, start_id):
         URL = 'http://{0}'.format(ip_address)
         validators.validate_URL_response(URL, expected_body=str(start_id))
         URL = 'http://{0}:81'.format(ip_address)
         validators.validate_URL_response(URL, expected_body=str(start_id + 1))
 
-    @classmethod
-    def _setup_lb_network_kwargs(cls, lb_kwargs, ip_version):
-        if cls.lb_member_vip_subnet:
-            ip_index = data_utils.rand_int_id(start=10, end=100)
-            if ip_version == 4:
-                network = ipaddress.IPv4Network(
-                    six.u(CONF.load_balancer.vip_subnet_cidr))
-                lb_vip_address = str(network[ip_index])
-                subnet_id = cls.lb_member_vip_subnet[const.ID]
-            else:
-                network = ipaddress.IPv6Network(
-                    six.u(CONF.load_balancer.vip_ipv6_subnet_cidr))
-                lb_vip_address = str(network[ip_index])
-                subnet_id = cls.lb_member_vip_ipv6_subnet[const.ID]
-            lb_kwargs[const.VIP_SUBNET_ID] = subnet_id
-            lb_kwargs[const.VIP_ADDRESS] = lb_vip_address
-            if CONF.load_balancer.test_with_noop:
-                lb_kwargs[const.VIP_NETWORK_ID] = (
-                    cls.lb_member_vip_net[const.ID])
-        else:
-            lb_kwargs[const.VIP_NETWORK_ID] = cls.lb_member_vip_net[const.ID]
-            lb_kwargs[const.VIP_SUBNET_ID] = None
+    def _wait_for_lb_functional(self, vip_address):
+        session = requests.Session()
+        start = time.time()
+
+        while time.time() - start < CONF.load_balancer.build_timeout:
+            try:
+                session.get("http://{0}".format(vip_address), timeout=2)
+                time.sleep(1)
+                return
+            except Exception:
+                LOG.warning('Server is not passing initial traffic. Waiting.')
+                time.sleep(1)
+        LOG.error('Server did not begin passing traffic within the timeout '
+                  'period. Failing test.')
+        raise Exception()
+
+    def check_members_balanced(self, vip_address, traffic_member_count=2):
+        session = requests.Session()
+        response_counts = {}
+
+        self._wait_for_lb_functional(vip_address)
+
+        # Send a number requests to lb vip
+        for i in range(20):
+            try:
+                r = session.get('http://{0}'.format(vip_address),
+                                timeout=2)
+
+                if r.content in response_counts:
+                    response_counts[r.content] += 1
+                else:
+                    response_counts[r.content] = 1
+
+            except Exception:
+                LOG.exception('Failed to send request to loadbalancer vip')
+                raise Exception('Failed to connect to lb')
+
+        LOG.debug('Loadbalancer response totals: %s', response_counts)
+        # Ensure the correct number of members
+        self.assertEqual(traffic_member_count, len(response_counts))
+
+        # Ensure both members got the same number of responses
+        self.assertEqual(1, len(set(response_counts.values())))
+
+    def assertConsistentResponse(self, response, url, method='GET', repeat=10,
+                                 redirect=False, timeout=2, **kwargs):
+        """Assert that a request to URL gets the expected response.
+
+        :param response: Expected response in format (status_code, content).
+        :param url: The URL to request.
+        :param method: The HTTP method to use (GET, POST, PUT, etc)
+        :param repeat: How many times to test the response.
+        :param data: Optional data to send in the request.
+        :param headers: Optional headers to send in the request.
+        :param cookies: Optional cookies to send in the request.
+        :param redirect: Is the request a redirect? If true, assume the passed
+                         content should be the next URL in the chain.
+        :return: boolean success status
+
+        :raises: testtools.matchers.MismatchError
+        """
+        session = requests.Session()
+        response_code, response_content = response
+
+        for i in range(0, repeat):
+            req = session.request(method, url, allow_redirects=not redirect,
+                                  timeout=timeout, **kwargs)
+            if response_code:
+                self.assertEqual(response_code, req.status_code)
+            if redirect:
+                self.assertTrue(req.is_redirect)
+                self.assertEqual(response_content, req.next.url)
+            elif response_content:
+                self.assertEqual(six.text_type(response_content), req.text)
diff --git a/octavia_tempest_plugin/tests/waiters.py b/octavia_tempest_plugin/tests/waiters.py
index 44138a4..5abb26e 100644
--- a/octavia_tempest_plugin/tests/waiters.py
+++ b/octavia_tempest_plugin/tests/waiters.py
@@ -27,7 +27,8 @@
 
 
 def wait_for_status(show_client, id, status_key, status,
-                    check_interval, check_timeout, root_tag=None):
+                    check_interval, check_timeout, root_tag=None,
+                    **kwargs):
     """Waits for an object to reach a specific status.
 
     :param show_client: The tempest service client show method.
@@ -51,11 +52,11 @@
     while True:
         if status == const.DELETED:
             try:
-                response = show_client(id)
+                response = show_client(id, **kwargs)
             except exceptions.NotFound:
                 return
         else:
-            response = show_client(id)
+            response = show_client(id, **kwargs)
 
         if root_tag:
             object_details = response[root_tag]
@@ -106,7 +107,7 @@
     :returns: None
     """
     try:
-        return delete_func(*args, **kwargs)
+        delete_func(*args, **kwargs)
     except exceptions.NotFound:
         return
     start = int(time.time())
@@ -127,7 +128,7 @@
 
 def wait_for_deleted_status_or_not_found(
         show_client, id, status_key, check_interval, check_timeout,
-        root_tag=None):
+        root_tag=None, **kwargs):
     """Waits for an object to reach a DELETED status or be not found (404).
 
     :param show_client: The tempest service client show method.
@@ -149,7 +150,7 @@
              'found(404)'.format(name=show_client.__name__))
     while True:
         try:
-            response = show_client(id)
+            response = show_client(id, **kwargs)
         except exceptions.NotFound:
             return
 
diff --git a/playbooks/Octavia-DSVM/pre.yaml b/playbooks/Octavia-DSVM/pre.yaml
index 1e7987c..9d6beb7 100644
--- a/playbooks/Octavia-DSVM/pre.yaml
+++ b/playbooks/Octavia-DSVM/pre.yaml
@@ -2,10 +2,11 @@
   name: Octavia DSVM jobs pre-run playbook
   tasks:
     - shell:
+        executable: /bin/bash
         cmd: |
           set -e
           set -x
-          if $(egrep --quiet '(vmx|svm)' /proc/cpuinfo) && [[ ! $(hostname) =~ "ovh" ]]; then
+          if $(egrep --quiet '(vmx|svm)' /proc/cpuinfo) && [[ ( ! $(hostname) =~ "ovh" && ! $(hostname) =~ "limestone" ) ]]; then
               export DEVSTACK_GATE_LIBVIRT_TYPE=kvm
           fi
 
diff --git a/setup.cfg b/setup.cfg
index b452614..17480f6 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -5,7 +5,7 @@
     README.rst
 author = OpenStack
 author-email = openstack-dev@lists.openstack.org
-home-page = http://www.openstack.org/developer/octavia-tempest-plugin
+home-page = https://docs.openstack.org/octavia-tempest-plugin/latest/
 classifier =
     Environment :: OpenStack
     Intended Audience :: Information Technology
diff --git a/test-requirements.txt b/test-requirements.txt
index 8ad5962..47c128f 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -7,6 +7,6 @@
 coverage!=4.4,>=4.0 # Apache-2.0
 python-subunit>=1.0.0 # Apache-2.0/BSD
 oslotest>=3.2.0 # Apache-2.0
-testrepository>=0.0.18 # Apache-2.0/BSD
+stestr>=2.0.0 # Apache-2.0
 testscenarios>=0.4 # Apache-2.0/BSD
 testtools>=2.2.0 # MIT
diff --git a/tox.ini b/tox.ini
index f168afd..d7d9ddd 100644
--- a/tox.ini
+++ b/tox.ini
@@ -10,18 +10,35 @@
    VIRTUAL_ENV={envdir}
    PYTHONWARNINGS=default::DeprecationWarning
 deps = -r{toxinidir}/test-requirements.txt
-commands = python setup.py test --slowest --testr-args='{posargs}'
+commands =
+  stestr run {posargs}
+  stestr slowest
 
 [testenv:pep8]
+basepython = python3
 commands = flake8 {posargs}
 
 [testenv:venv]
+basepython = python3
 commands = {posargs}
 
 [testenv:cover]
-commands = python setup.py test --coverage --testr-args='{posargs}'
+basepython = python3
+setenv =
+  {[testenv]setenv}
+  PYTHON=coverage run --source octavia_tempest_plugin --parallel-mode
+whitelist_externals =
+  find
+commands =
+  find octavia_tempest_plugin -type f -name "*.pyc" -delete
+  coverage erase
+  stestr run {posargs}
+  coverage combine
+  coverage html -d cover
+  coverage xml -o cover/coverage.xml
 
 [testenv:docs]
+basepython = python3
 deps =
     -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt}
     -r{toxinidir}/requirements.txt
@@ -32,6 +49,7 @@
   sphinx-build -W -b html doc/source doc/build/html
 
 [testenv:releasenotes]
+basepython = python3
 deps =
     -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt}
     -r{toxinidir}/requirements.txt
@@ -40,6 +58,7 @@
   sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html
 
 [testenv:debug]
+basepython = python3
 commands = oslo_debug_helper {posargs}
 
 [flake8]
@@ -51,6 +70,7 @@
 exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build
 
 [testenv:genconfig]
+basepython = python3
 whitelist_externals = mkdir
 commands =
          mkdir -p etc
diff --git a/zuul.d/jobs.yaml b/zuul.d/jobs.yaml
index c56bee8..c7c48db 100644
--- a/zuul.d/jobs.yaml
+++ b/zuul.d/jobs.yaml
@@ -15,7 +15,7 @@
       - ^releasenotes/.*$
     vars:
       devstack_localrc:
-        TEMPEST_PLUGINS: "'{{ ansible_user_dir }}/src/git.openstack.org/openstack/octavia-tempest-plugin'"
+        TEMPEST_PLUGINS: "'/opt/stack/octavia-tempest-plugin'"
       devstack_local_conf:
         post-config:
           $OCTAVIA_CONF:
@@ -51,17 +51,22 @@
 - job:
     name: octavia-dsvm-live-base
     parent: octavia-dsvm-base
+    timeout: 9000
     required-projects:
       - openstack/barbican
       - openstack/diskimage-builder
       - openstack/python-barbicanclient
     vars:
+      devstack_localrc:
+        DIB_LOCAL_ELEMENTS: openstack-ci-mirrors
       devstack_services:
         barbican: true
         neutron-qos: true
       devstack_plugins:
         barbican: https://github.com/openstack/barbican.git
         neutron: https://github.com/openstack/neutron.git
+      zuul_copy_output:
+        '/var/log/dib-build' : logs
 
 - job:
     name: octavia-dsvm-noop-base
@@ -106,8 +111,18 @@
         USE_PYTHON3: true
 
 - job:
+    name: octavia-v2-dsvm-noop-api-stable-rocky
+    parent: octavia-v2-dsvm-noop-api
+    override-checkout: stable/rocky
+
+- job:
+    name: octavia-v2-dsvm-noop-api-stable-queens
+    parent: octavia-v2-dsvm-noop-api
+    override-checkout: stable/queens
+
+- job:
     name: octavia-v2-dsvm-scenario
-    parent: octavia-dsvm-base
+    parent: octavia-dsvm-live-base
     vars:
       devstack_local_conf:
         post-config:
@@ -124,3 +139,31 @@
     vars:
       devstack_localrc:
         USE_PYTHON3: true
+
+- job:
+    name: octavia-v2-dsvm-scenario-stable-rocky
+    parent: octavia-v2-dsvm-scenario
+    override-checkout: stable/rocky
+
+- job:
+    name: octavia-v2-dsvm-scenario-stable-queens
+    parent: octavia-v2-dsvm-scenario
+    override-checkout: stable/queens
+
+- job:
+    name: octavia-v2-dsvm-scenario-centos-7
+    parent: octavia-v2-dsvm-scenario
+    nodeset: devstack-single-node-centos-7
+    vars:
+      devstack_localrc:
+        OCTAVIA_AMP_BASE_OS: centos
+        OCTAVIA_AMP_DISTRIBUTION_RELEASE_ID: 7
+        OCTAVIA_AMP_IMAGE_SIZE: 3
+
+- job:
+    name: octavia-v2-dsvm-scenario-ubuntu-bionic
+    parent: octavia-v2-dsvm-scenario
+    vars:
+      devstack_localrc:
+        OCTAVIA_AMP_BASE_OS: ubuntu
+        OCTAVIA_AMP_DISTRIBUTION_RELEASE_ID: bionic
diff --git a/zuul.d/projects.yaml b/zuul.d/projects.yaml
index f09a634..90956d8 100644
--- a/zuul.d/projects.yaml
+++ b/zuul.d/projects.yaml
@@ -1,16 +1,32 @@
 # Note: Some official OpenStack wide jobs are still defined in the
 #       project-config repository
 - project:
+    templates:
+      - check-requirements
+      - publish-openstack-docs-pti
+      - tempest-plugin-jobs
     check:
       jobs:
         - octavia-v2-dsvm-noop-api
+        - octavia-v2-dsvm-noop-api-stable-rocky
+        - octavia-v2-dsvm-noop-api-stable-queens
         - octavia-v2-dsvm-noop-py35-api
         - octavia-v2-dsvm-scenario
+        - octavia-v2-dsvm-scenario-stable-rocky
+        - octavia-v2-dsvm-scenario-stable-queens
         - octavia-v2-dsvm-py35-scenario
+        - octavia-v2-dsvm-scenario-centos-7
+        - octavia-v2-dsvm-scenario-ubuntu-bionic:
+            voting: false
     gate:
       queue: octavia
       jobs:
         - octavia-v2-dsvm-noop-api
+        - octavia-v2-dsvm-noop-api-stable-rocky
+        - octavia-v2-dsvm-noop-api-stable-queens
         - octavia-v2-dsvm-noop-py35-api
         - octavia-v2-dsvm-scenario
+        - octavia-v2-dsvm-scenario-stable-rocky
+        - octavia-v2-dsvm-scenario-stable-queens
         - octavia-v2-dsvm-py35-scenario
+        - octavia-v2-dsvm-scenario-centos-7