Merge "Add neutron plugin to octavia-dsvm-base job"
diff --git a/devstack/plugin.sh b/devstack/plugin.sh
index 4b8f60b..5eac133 100644
--- a/devstack/plugin.sh
+++ b/devstack/plugin.sh
@@ -16,9 +16,11 @@
     fi
 
     go_path=$(find $DEST/tempest/.tox/tempest/ -name test_server.go)
-    bin_path=${go_path%.go}.bin
+    sudo mkdir -m755 -p /opt/octavia-tempest-plugin
+    sudo chown $STACK_USER /opt/octavia-tempest-plugin
     CGO_ENABLED=0 GOOS=linux go build \
-        -a -ldflags '-s -w -extldflags -static' -o $bin_path \
+        -a -ldflags '-s -w -extldflags -static' \
+        -o /opt/octavia-tempest-plugin/test_server.bin \
         ${DEST}/octavia-tempest-plugin/octavia_tempest_plugin/contrib/test_server/test_server.go
 }
 
@@ -26,11 +28,11 @@
     case "$2" in
         install)
             # Install dev library if
-            # - the release is more recent than stein (devstack in stein would
-            #   try to install it in a python2 env, but octavia-tempest-plugin is
-            #   now a python3-only project)
+            # - the release is more recent than train (devstack in train would
+            #   try to install it in a python2 env, but octavia-tempest-plugin
+            #   is now a python3-only project)
             # - or the user explicitly requests it (INSTALL_TEMPEST=True)
-            if [[ "$DEVSTACK_SERIES" != "stein" ]] || [[ "$(trueorfalse False INSTALL_TEMPEST)" == "True" ]]; then
+            if [[ ! "$DEVSTACK_SERIES" =~ (stein|train) ]] || [[ "$(trueorfalse False INSTALL_TEMPEST)" == "True" ]]; then
                 echo_summary "Installing octavia-tempest-plugin"
                 install_octavia_tempest_plugin
             fi
diff --git a/octavia_tempest_plugin/common/constants.py b/octavia_tempest_plugin/common/constants.py
index 1f0a738..927aa84 100644
--- a/octavia_tempest_plugin/common/constants.py
+++ b/octavia_tempest_plugin/common/constants.py
@@ -289,3 +289,13 @@
 
 SHOW_AVAILABILITY_ZONE_FIELDS = [
     NAME, DESCRIPTION, ENABLED, AVAILABILITY_ZONE_PROFILE_ID]
+
+# Paths inside the test webservers
+CERT_PEM = 'cert.pem'
+KEY_PEM = 'key.pem'
+CLIENT_CA_PEM = 'client_ca.pem'
+DEV_SHM_PATH = '/dev/shm/'
+TEST_SERVER_BINARY = DEV_SHM_PATH + 'test_server.bin'
+TEST_SERVER_CERT = DEV_SHM_PATH + CERT_PEM
+TEST_SERVER_KEY = DEV_SHM_PATH + KEY_PEM
+TEST_SERVER_CLIENT_CA = DEV_SHM_PATH + CLIENT_CA_PEM
diff --git a/octavia_tempest_plugin/config.py b/octavia_tempest_plugin/config.py
index 77d2f6e..f44bf96 100644
--- a/octavia_tempest_plugin/config.py
+++ b/octavia_tempest_plugin/config.py
@@ -213,6 +213,10 @@
                default='/var/log/octavia-amphora.log',
                help='File path, on the tempest system, to the amphora admin '
                     'log file.'),
+    cfg.StrOpt('test_server_path',
+               default='/opt/octavia-tempest-plugin/test_server.bin',
+               help='Filesystem path to the test web server that will be '
+                    'installed in the web server VMs.'),
 ]
 
 lb_feature_enabled_group = cfg.OptGroup(name='loadbalancer-feature-enabled',
diff --git a/octavia_tempest_plugin/contrib/test_server/README.rst b/octavia_tempest_plugin/contrib/test_server/README.rst
index f6ec4bb..66a6030 100644
--- a/octavia_tempest_plugin/contrib/test_server/README.rst
+++ b/octavia_tempest_plugin/contrib/test_server/README.rst
@@ -44,20 +44,24 @@
 
   Usage of ./test_server.bin:
     -cert string
-          Server side PEM format certificate.
+          Server side PEM format certificate file path.
     -client_ca string
-          Client side PEM format CA certificate.
+          Client auth PEM format CA certificate file path.
     -https_port int
           HTTPS port to listen on, -1 is disabled. (default -1)
+    -https_client_auth_port int
+          HTTPS with client authentication port to listen on, -1 is disabled.
+          (default -1)
     -id string
           Server ID (default "1")
     -key string
-          Server side PEM format key.
+          Server side PEM format key file path.
     -port int
           Port to listen on (default 8080)
 
 If -https_port is not specified, the server will not accept HTTPS requests.
 When --https_port is specified, -cert and -key are required parameters.
-If -https_port is specified, the -client_ca parameter is optional. When
--client_ca is specified, it will configure the HTTPS port to require a valid
-client certificate to connect.
+
+If -https_client_auth_port is specified, the -client_ca parameter is required.
+When -client_ca is specified, it will configure the HTTPS client auth port to
+require a valid client certificate to connect.
diff --git a/octavia_tempest_plugin/contrib/test_server/test_server.go b/octavia_tempest_plugin/contrib/test_server/test_server.go
index f8bc1e0..fa8f8d7 100644
--- a/octavia_tempest_plugin/contrib/test_server/test_server.go
+++ b/octavia_tempest_plugin/contrib/test_server/test_server.go
@@ -12,64 +12,93 @@
 	"net"
 	"net/http"
 	"os"
+	"strconv"
 	"sync"
 	"time"
 )
 
-var sess_cookie http.Cookie
+var sessCookie http.Cookie
 var resp string
 
-type ConnectionCount struct {
-	mu         sync.Mutex
-	cur_conn   int
-	max_conn   int
-	total_conn int
+type connectionCount struct {
+	mu        sync.Mutex
+	curConn   int
+	maxConn   int
+	totalConn int
 }
 
-var scoreboard ConnectionCount
+var scoreboard connectionCount
 
-func (cc *ConnectionCount) open() {
+func (cc *connectionCount) open() {
 	cc.mu.Lock()
 	defer cc.mu.Unlock()
 
-	cc.cur_conn++
-	cc.total_conn++
+	cc.curConn++
+	cc.totalConn++
 }
 
-func (cc *ConnectionCount) close() {
+func (cc *connectionCount) close() {
 	cc.mu.Lock()
 	defer cc.mu.Unlock()
 
-	if cc.cur_conn > cc.max_conn {
-		cc.max_conn = cc.cur_conn
+	if cc.curConn > cc.maxConn {
+		cc.maxConn = cc.curConn
 	}
-	cc.cur_conn--
+	cc.curConn--
 }
 
-func (cc *ConnectionCount) stats() (int, int) {
+func (cc *connectionCount) stats() (int, int) {
 	cc.mu.Lock()
 	defer cc.mu.Unlock()
 
-	return cc.max_conn, cc.total_conn
+	return cc.maxConn, cc.totalConn
 }
 
-func (cc *ConnectionCount) reset() {
+func (cc *connectionCount) reset() {
 	cc.mu.Lock()
 	defer cc.mu.Unlock()
 
-	cc.max_conn = 0
-	cc.total_conn = 0
+	cc.maxConn = 0
+	cc.totalConn = 0
 }
 
-func root_handler(w http.ResponseWriter, r *http.Request) {
+func rootHandler(w http.ResponseWriter, r *http.Request) {
 	scoreboard.open()
 	defer scoreboard.close()
 
-	http.SetCookie(w, &sess_cookie)
+	http.SetCookie(w, &sessCookie)
 	io.WriteString(w, resp)
 }
 
-func slow_handler(w http.ResponseWriter, r *http.Request) {
+func requestHandler(w http.ResponseWriter, r *http.Request) {
+	scoreboard.open()
+	defer scoreboard.close()
+
+	http.SetCookie(w, &sessCookie)
+
+	params := r.URL.Query()
+	if value, ok := params["response_code"]; ok {
+		if responseCode, err := strconv.Atoi(value[0]); err == nil {
+			w.WriteHeader(responseCode)
+		}
+	}
+
+	io.WriteString(w, fmt.Sprintf("%s %s %s\n",
+	                              r.Method, r.RequestURI, r.Proto))
+
+	io.WriteString(w, fmt.Sprintf("Host: %s\n", r.Host))
+
+	for key, values := range r.Header {
+		for _, value := range values {
+			header := fmt.Sprintf("%s: %s\n", key, value)
+			io.WriteString(w, header)
+		}
+	}
+	io.WriteString(w, "\n")
+	io.WriteString(w, resp)
+}
+
+func slowHandler(w http.ResponseWriter, r *http.Request) {
 	scoreboard.open()
 	defer scoreboard.close()
 
@@ -79,59 +108,61 @@
 	}
 
 	time.Sleep(delay)
-	http.SetCookie(w, &sess_cookie)
+	http.SetCookie(w, &sessCookie)
 	io.WriteString(w, resp)
 }
 
-func stats_handler(w http.ResponseWriter, r *http.Request) {
-	http.SetCookie(w, &sess_cookie)
-	max_conn, total_conn := scoreboard.stats()
-	fmt.Fprintf(w, "max_conn=%d\ntotal_conn=%d\n", max_conn, total_conn)
+func statsHandler(w http.ResponseWriter, r *http.Request) {
+	http.SetCookie(w, &sessCookie)
+	maxConn, totalConn := scoreboard.stats()
+	fmt.Fprintf(w, "maxConn=%d\ntotalConn=%d\n", maxConn, totalConn)
 }
 
-func https_wrapper(base_handler func(http.ResponseWriter,
+func httpsWrapper(baseHandler func(http.ResponseWriter,
 	*http.Request)) http.Handler {
 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 
 		w.Header().Add("Strict-Transport-Security",
 			"max-age=66012000; includeSubDomains")
-		base_handler(w, r)
+		baseHandler(w, r)
 	})
 }
 
-func reset_handler(w http.ResponseWriter, r *http.Request) {
-	http.SetCookie(w, &sess_cookie)
+func resetHandler(w http.ResponseWriter, r *http.Request) {
+	http.SetCookie(w, &sessCookie)
 	scoreboard.reset()
 	fmt.Fprintf(w, "reset\n")
 }
 
-func http_setup(id string) {
-	sess_cookie.Name = "JSESSIONID"
-	sess_cookie.Value = id
+func httpSetup(id string) {
+	sessCookie.Name = "JSESSIONID"
+	sessCookie.Value = id
 
-	http.HandleFunc("/", root_handler)
-	http.HandleFunc("/slow", slow_handler)
-	http.HandleFunc("/stats", stats_handler)
-	http.HandleFunc("/reset", reset_handler)
+	http.HandleFunc("/", rootHandler)
+	http.HandleFunc("/slow", slowHandler)
+	http.HandleFunc("/stats", statsHandler)
+	http.HandleFunc("/reset", resetHandler)
+	http.HandleFunc("/request", requestHandler)
 }
 
-func http_serve(port int, id string) {
+func httpServe(port int, id string) {
 	portStr := fmt.Sprintf(":%d", port)
 	log.Fatal(http.ListenAndServe(portStr, nil))
 }
 
-func https_serve(port int, id string, cert tls.Certificate,
-	certpool *x509.CertPool, server_cert_pem string,
-	server_key_pem string) {
+func httpsServe(port int, id string, cert tls.Certificate,
+	certpool *x509.CertPool, serverCertPem string,
+	serverKeyPem string) {
 	mux := http.NewServeMux()
-	mux.Handle("/", https_wrapper(root_handler))
-	mux.Handle("/slow", https_wrapper(slow_handler))
-	mux.Handle("/stats", https_wrapper(stats_handler))
-	mux.Handle("/reset", https_wrapper(reset_handler))
+	mux.Handle("/", httpsWrapper(rootHandler))
+	mux.Handle("/slow", httpsWrapper(slowHandler))
+	mux.Handle("/stats", httpsWrapper(statsHandler))
+	mux.Handle("/reset", httpsWrapper(resetHandler))
+	mux.Handle("/request", httpsWrapper(requestHandler))
 
-	var tls_config *tls.Config
+	var tlsConfig *tls.Config
 	if certpool != nil {
-		tls_config = &tls.Config{
+		tlsConfig = &tls.Config{
 			Certificates: []tls.Certificate{cert},
 			ClientAuth:   tls.RequireAndVerifyClientCert,
 			ClientCAs:    certpool,
@@ -147,7 +178,7 @@
 			},
 		}
 	} else {
-		tls_config = &tls.Config{
+		tlsConfig = &tls.Config{
 			Certificates: []tls.Certificate{cert},
 			ClientAuth:   tls.NoClientCert,
 			MinVersion:   tls.VersionTLS12,
@@ -160,21 +191,22 @@
 				tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
 				tls.TLS_RSA_WITH_AES_256_CBC_SHA,
 			},
+			NextProtos: []string{"h2", "http/1.1", "http/1.0"},
 		}
 	}
-	tls_config.Rand = rand.Reader
+	tlsConfig.Rand = rand.Reader
 	portStr := fmt.Sprintf(":%d", port)
 	srv := &http.Server{
 		Addr:      portStr,
 		Handler:   mux,
-		TLSConfig: tls_config,
+		TLSConfig: tlsConfig,
 		TLSNextProto: make(map[string]func(*http.Server, *tls.Conn,
 			http.Handler), 0),
 	}
-	log.Fatal(srv.ListenAndServeTLS(server_cert_pem, server_key_pem))
+	log.Fatal(srv.ListenAndServeTLS(serverCertPem, serverKeyPem))
 }
 
-func udp_serve(port int, id string) {
+func udpServe(port int, id string) {
 	portStr := fmt.Sprintf("0.0.0.0:%d", port)
 
 	pc, err := net.ListenPacket("udp", portStr)
@@ -202,44 +234,53 @@
 func main() {
 	portPtr := flag.Int("port", 8080, "Port to listen on")
 	idPtr := flag.String("id", "1", "Server ID")
-	https_portPtr := flag.Int("https_port", -1,
+	httpsPortPtr := flag.Int("https_port", -1,
 		"HTTPS port to listen on, -1 is disabled.")
-	server_cert_pem := flag.String("cert", "",
-		"Server side PEM format certificate.")
-	server_key := flag.String("key", "", "Server side PEM format key.")
-	client_ca_cert_pem := flag.String("client_ca", "",
-		"Client side PEM format CA certificate.")
+	httpsClientAuthPortPtr := flag.Int("https_client_auth_port", -1,
+		"HTTPS with client authentication port to listen on, -1 is disabled.")
+	serverCertPem := flag.String("cert", "",
+		"Server side PEM format certificate file path.")
+	serverKey := flag.String("key", "",
+        "Server side PEM format key file path.")
+	clientCaCertPem := flag.String("client_ca", "",
+		"Client auth PEM format CA certificate file path.")
 
 	flag.Parse()
 
 	resp = fmt.Sprintf("%s", *idPtr)
 
-	http_setup(*idPtr)
+	httpSetup(*idPtr)
 
-	if *https_portPtr > -1 {
-		cert, err := tls.LoadX509KeyPair(*server_cert_pem, *server_key)
+	if *httpsPortPtr > -1 {
+		cert, err := tls.LoadX509KeyPair(*serverCertPem, *serverKey)
+		if err != nil {
+			fmt.Println("Error load server certificate and key.")
+			os.Exit(1)
+		}
+		go httpsServe(*httpsPortPtr, *idPtr, cert, nil,
+			*serverCertPem, *serverKey)
+	}
+
+	if *httpsClientAuthPortPtr > -1 {
+		cert, err := tls.LoadX509KeyPair(*serverCertPem, *serverKey)
 		if err != nil {
 			fmt.Println("Error load server certificate and key.\n")
 			os.Exit(1)
 		}
 		certpool := x509.NewCertPool()
-		if *client_ca_cert_pem != "" {
-			ca_pem, err := ioutil.ReadFile(*client_ca_cert_pem)
-			if err != nil {
-				fmt.Println("Error load client side CA cert.\n")
-				os.Exit(1)
-			}
-			if !certpool.AppendCertsFromPEM(ca_pem) {
-				fmt.Println("Can't parse client side certificate authority")
-				os.Exit(1)
-			}
-		} else {
-			certpool = nil
+		caPem, err := ioutil.ReadFile(*clientCaCertPem)
+		if err != nil {
+			fmt.Println("Error loading client auth CA cert.\n")
+			os.Exit(1)
 		}
-		go https_serve(*https_portPtr, *idPtr, cert, certpool,
-			*server_cert_pem, *server_key)
+		if !certpool.AppendCertsFromPEM(caPem) {
+			fmt.Println("Can't parse client auth certificate authority")
+			os.Exit(1)
+		}
+		go httpsServe(*httpsClientAuthPortPtr, *idPtr, cert, certpool,
+			*serverCertPem, *serverKey)
 	}
 
-	go http_serve(*portPtr, *idPtr)
-	udp_serve(*portPtr, *idPtr)
+	go httpServe(*portPtr, *idPtr)
+	udpServe(*portPtr, *idPtr)
 }
diff --git a/octavia_tempest_plugin/tests/api/v2/test_pool.py b/octavia_tempest_plugin/tests/api/v2/test_pool.py
index 52de166..3ab4892 100644
--- a/octavia_tempest_plugin/tests/api/v2/test_pool.py
+++ b/octavia_tempest_plugin/tests/api/v2/test_pool.py
@@ -443,6 +443,8 @@
                 const.ONLINE,
                 CONF.load_balancer.build_interval,
                 CONF.load_balancer.build_timeout)
+        else:
+            self.assertEqual(const.OFFLINE, pool[const.OPERATING_STATUS])
 
         self.assertEqual(pool_name, pool[const.NAME])
         self.assertEqual(pool_description, pool[const.DESCRIPTION])
diff --git a/octavia_tempest_plugin/tests/scenario/v2/test_pool.py b/octavia_tempest_plugin/tests/scenario/v2/test_pool.py
index ae2ef67..6df4c9c 100644
--- a/octavia_tempest_plugin/tests/scenario/v2/test_pool.py
+++ b/octavia_tempest_plugin/tests/scenario/v2/test_pool.py
@@ -434,7 +434,6 @@
         parser.parse(pool[const.CREATED_AT])
         parser.parse(pool[const.UPDATED_AT])
         UUID(pool[const.ID])
-        self.assertEqual(const.OFFLINE, pool[const.OPERATING_STATUS])
         self.assertEqual(pool_protocol, pool[const.PROTOCOL])
         self.assertEqual(1, len(pool[const.LOADBALANCERS]))
         self.assertEqual(self.lb_id, pool[const.LOADBALANCERS][0][const.ID])
@@ -443,6 +442,7 @@
             self.assertEqual(listener_id, pool[const.LISTENERS][0][const.ID])
         else:
             self.assertEmpty(pool[const.LISTENERS])
+            self.assertEqual(const.OFFLINE, pool[const.OPERATING_STATUS])
         self.assertEqual(algorithm, pool[const.LB_ALGORITHM])
 
         if session_persistence == const.SESSION_PERSISTENCE_APP_COOKIE:
@@ -510,6 +510,13 @@
         self.assertEqual(new_description, pool[const.DESCRIPTION])
         self.assertTrue(pool[const.ADMIN_STATE_UP])
         self.assertEqual(algorithm, pool[const.LB_ALGORITHM])
+        if listener_protocol is not None:
+            pool = waiters.wait_for_status(
+                self.mem_pool_client.show_pool,
+                pool[const.ID], const.OPERATING_STATUS,
+                const.ONLINE,
+                CONF.load_balancer.build_interval,
+                CONF.load_balancer.build_timeout)
 
         if session_persistence == const.SESSION_PERSISTENCE_APP_COOKIE:
             self.assertIsNotNone(pool.get(const.SESSION_PERSISTENCE))
diff --git a/octavia_tempest_plugin/tests/scenario/v2/test_traffic_ops.py b/octavia_tempest_plugin/tests/scenario/v2/test_traffic_ops.py
index 92e5ecb..356327b 100644
--- a/octavia_tempest_plugin/tests/scenario/v2/test_traffic_ops.py
+++ b/octavia_tempest_plugin/tests/scenario/v2/test_traffic_ops.py
@@ -928,9 +928,11 @@
             listener_id, pool_id = self._listener_pool_create(
                 const.TCP, 60092,
                 pool_algorithm=const.LB_ALGORITHM_SOURCE_IP_PORT)
+            # Without a delay this can trigger a "Cannot assign requested
+            # address" warning setting the source port, leading to failure
             self._test_basic_traffic(
                 const.TCP, 60092, listener_id, pool_id, traffic_member_count=1,
-                persistent=False, source_port=60092)
+                persistent=False, source_port=60092, delay=0.2)
         except exceptions.NotImplemented as e:
             message = ("The configured provider driver '{driver}' "
                        "does not support a feature required for this "
diff --git a/octavia_tempest_plugin/tests/test_base.py b/octavia_tempest_plugin/tests/test_base.py
index f260e88..887c644 100644
--- a/octavia_tempest_plugin/tests/test_base.py
+++ b/octavia_tempest_plugin/tests/test_base.py
@@ -13,13 +13,14 @@
 #    under the License.
 
 import ipaddress
-import pkg_resources
+import os
 import random
 import shlex
 import string
 import subprocess
 import tempfile
 
+from cryptography.hazmat.primitives import serialization
 from oslo_log import log as logging
 from oslo_utils import uuidutils
 from tempest import config
@@ -30,6 +31,7 @@
 import tenacity
 
 from octavia_tempest_plugin import clients
+from octavia_tempest_plugin.common import cert_utils
 from octavia_tempest_plugin.common import constants as const
 from octavia_tempest_plugin.tests import validators
 from octavia_tempest_plugin.tests import waiters
@@ -637,6 +639,9 @@
 
             LOG.info('lb_member_sec_group: {}'.format(cls.lb_member_sec_group))
 
+        # Setup backend member reencryption PKI
+        cls._create_backend_reencryption_pki()
+
         # Create webserver 1 instance
         server_details = cls._create_webserver('lb_member_webserver1',
                                                cls.lb_member_1_net)
@@ -700,7 +705,7 @@
         # Set up serving on webserver 2
         cls._install_start_webserver(cls.webserver2_public_ip,
                                      cls.lb_member_keypair['private_key'],
-                                     cls.webserver2_response)
+                                     cls.webserver2_response, revoke_cert=True)
 
         # Validate webserver 2
         cls._validate_webserver(cls.webserver2_public_ip,
@@ -848,10 +853,9 @@
         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.test_server', 'test_server.bin')
-        dest_file = '/dev/shm/test_server.bin'
+    def _install_start_webserver(cls, ip_address, ssh_key, start_id,
+                                 revoke_cert=False):
+        local_file = CONF.load_balancer.test_server_path
 
         linux_client = remote_client.RemoteClient(
             ip_address, CONF.validation.image_ssh_user, pkey=ssh_key)
@@ -867,7 +871,7 @@
                 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)
+                ip_address, const.TEST_SERVER_BINARY)
             args = shlex.split(cmd)
             subprocess_args = {'stdout': subprocess.PIPE,
                                'stderr': subprocess.STDOUT,
@@ -878,6 +882,9 @@
                 raise exceptions.CommandFailed(proc.returncode, cmd,
                                                stdout, stderr)
 
+            cls._load_member_pki_content(ip_address, key,
+                                         revoke_cert=revoke_cert)
+
         # Enabling memory overcommit allows to run golang static binaries
         # compiled with a recent golang toolchain (>=1.11). Those binaries
         # allocate a large amount of virtual memory at init time, and this
@@ -888,10 +895,16 @@
         linux_client.exec_command('sudo sh -c "echo 1 > '
                                   '/proc/sys/vm/overcommit_memory"')
 
-        linux_client.exec_command('sudo screen -d -m {0} -port 80 '
-                                  '-id {1}'.format(dest_file, start_id))
+        # The initial process also supports HTTPS and HTTPS with client auth
+        linux_client.exec_command(
+            'sudo screen -d -m {0} -port 80 -id {1} -https_port 443 -cert {2} '
+            '-key {3} -https_client_auth_port 9443 -client_ca {4}'.format(
+                const.TEST_SERVER_BINARY, start_id, const.TEST_SERVER_CERT,
+                const.TEST_SERVER_KEY, const.TEST_SERVER_CLIENT_CA))
+
         linux_client.exec_command('sudo screen -d -m {0} -port 81 '
-                                  '-id {1}'.format(dest_file, start_id + 1))
+                                  '-id {1}'.format(const.TEST_SERVER_BINARY,
+                                                   start_id + 1))
 
     # Cirros does not configure the assigned IPv6 address by default
     # so enable it manually like tempest does here:
@@ -926,3 +939,106 @@
             raise Exception("Response from test server doesn't match the "
                             "expected value ({0} != {1}).".format(
                                 res, str(start_id + 1)))
+
+    @classmethod
+    def _create_backend_reencryption_pki(cls):
+        # Create a CA self-signed cert and key for the member test servers
+        cls.member_ca_cert, cls.member_ca_key = (
+            cert_utils.generate_ca_cert_and_key())
+
+        LOG.debug('Member CA Cert: %s', cls.member_ca_cert.public_bytes(
+            serialization.Encoding.PEM))
+        LOG.debug('Member CA private Key: %s', cls.member_ca_key.private_bytes(
+            encoding=serialization.Encoding.PEM,
+            format=serialization.PrivateFormat.TraditionalOpenSSL,
+            encryption_algorithm=serialization.NoEncryption()))
+        LOG.debug('Member CA public Key: %s',
+                  cls.member_ca_key.public_key().public_bytes(
+                      encoding=serialization.Encoding.PEM,
+                      format=serialization.PublicFormat.SubjectPublicKeyInfo))
+
+        # Create the member client authentication CA
+        cls.member_client_ca_cert, member_client_ca_key = (
+            cert_utils.generate_ca_cert_and_key())
+
+        # Create client cert and key
+        cls.member_client_cn = uuidutils.generate_uuid()
+        cls.member_client_cert, cls.member_client_key = (
+            cert_utils.generate_client_cert_and_key(
+                cls.member_client_ca_cert, member_client_ca_key,
+                cls.member_client_cn))
+        # Note: We are not revoking a client cert here as we don't need to
+        #       test the backend web server CRL checking.
+
+    @classmethod
+    def _load_member_pki_content(cls, ip_address, ssh_key, revoke_cert=False):
+        # Create webserver certificate and key
+        cert, key = cert_utils.generate_server_cert_and_key(
+            cls.member_ca_cert, cls.member_ca_key, ip_address)
+
+        LOG.debug('%s Cert: %s', ip_address, cert.public_bytes(
+            serialization.Encoding.PEM))
+        LOG.debug('%s private Key: %s', ip_address, key.private_bytes(
+            encoding=serialization.Encoding.PEM,
+            format=serialization.PrivateFormat.TraditionalOpenSSL,
+            encryption_algorithm=serialization.NoEncryption()))
+        public_key = key.public_key()
+        LOG.debug('%s public Key: %s', ip_address, public_key.public_bytes(
+            encoding=serialization.Encoding.PEM,
+            format=serialization.PublicFormat.SubjectPublicKeyInfo))
+
+        # Create a CRL with a revoked certificate
+        if revoke_cert:
+            # Create a CRL with webserver 2 revoked
+            cls.member_crl = cert_utils.generate_certificate_revocation_list(
+                cls.member_ca_cert, cls.member_ca_key, cert)
+
+        # Load the certificate, key, and client CA certificate into the
+        # test server.
+        with tempfile.TemporaryDirectory() as tmpdir:
+            os.umask(0)
+            files_to_send = []
+            cert_filename = os.path.join(tmpdir, const.CERT_PEM)
+            files_to_send.append(cert_filename)
+            with open(os.open(cert_filename, os.O_CREAT | os.O_WRONLY,
+                              0o700), 'w') as fh:
+                fh.write(cert.public_bytes(
+                    serialization.Encoding.PEM).decode('utf-8'))
+                fh.flush()
+            key_filename = os.path.join(tmpdir, const.KEY_PEM)
+            files_to_send.append(key_filename)
+            with open(os.open(key_filename, os.O_CREAT | os.O_WRONLY,
+                              0o700), 'w') as fh:
+                fh.write(key.private_bytes(
+                    encoding=serialization.Encoding.PEM,
+                    format=serialization.PrivateFormat.TraditionalOpenSSL,
+                    encryption_algorithm=serialization.NoEncryption()).decode(
+                        'utf-8'))
+                fh.flush()
+            client_ca_filename = os.path.join(tmpdir, const.CLIENT_CA_PEM)
+            files_to_send.append(client_ca_filename)
+            with open(os.open(client_ca_filename, os.O_CREAT | os.O_WRONLY,
+                              0o700), 'w') as fh:
+                fh.write(cls.member_client_ca_cert.public_bytes(
+                    serialization.Encoding.PEM).decode('utf-8'))
+                fh.flush()
+
+            # For security, we don't want to use a shell that can glob
+            # the file names, so iterate over them.
+            subprocess_args = {'stdout': subprocess.PIPE,
+                               'stderr': subprocess.STDOUT,
+                               'cwd': None}
+            cmd = ("scp -v -o UserKnownHostsFile=/dev/null "
+                   "-o StrictHostKeyChecking=no "
+                   "-o ConnectTimeout={0} -o ConnectionAttempts={1} "
+                   "-i {2} {3} {4} {5} {6}@{7}:{8}").format(
+                CONF.load_balancer.scp_connection_timeout,
+                CONF.load_balancer.scp_connection_attempts,
+                ssh_key.name, cert_filename, key_filename, client_ca_filename,
+                CONF.validation.image_ssh_user, ip_address, const.DEV_SHM_PATH)
+            args = shlex.split(cmd)
+            proc = subprocess.Popen(args, **subprocess_args)
+            stdout, stderr = proc.communicate()
+            if proc.returncode != 0:
+                raise exceptions.CommandFailed(proc.returncode, cmd,
+                                               stdout, stderr)
diff --git a/octavia_tempest_plugin/tests/validators.py b/octavia_tempest_plugin/tests/validators.py
index a93e2eb..5972d54 100644
--- a/octavia_tempest_plugin/tests/validators.py
+++ b/octavia_tempest_plugin/tests/validators.py
@@ -284,6 +284,9 @@
             HTTPS_verify, requests_session=requests_session,
             source_port=source_port)
 
+        if source_port:
+            LOG.debug('Using source port %s for request(s)', source_port)
+
         response_counts = {}
         # Send a number requests to lb vip
         for i in range(repeat):
diff --git a/octavia_tempest_plugin/tests/waiters.py b/octavia_tempest_plugin/tests/waiters.py
index e0d9d2d..fa6c112 100644
--- a/octavia_tempest_plugin/tests/waiters.py
+++ b/octavia_tempest_plugin/tests/waiters.py
@@ -68,7 +68,7 @@
             LOG.info('{name}\'s status updated to {status}.'.format(
                 name=show_client.__name__, status=status))
             return object_details
-        elif object_details[status_key] == 'ERROR':
+        elif object_details[status_key] == 'ERROR' and not error_ok:
             message = ('{name} {field} updated to an invalid state of '
                        'ERROR'.format(name=show_client.__name__,
                                       field=status_key))
@@ -76,9 +76,9 @@
             if caller:
                 message = '({caller}) {message}'.format(caller=caller,
                                                         message=message)
-            if not error_ok:
-                raise exceptions.UnexpectedResponseCode(message)
-        elif int(time.time()) - start >= check_timeout:
+            raise exceptions.UnexpectedResponseCode(message)
+
+        if int(time.time()) - start >= check_timeout:
             message = (
                 '{name} {field} failed to update to {expected_status} within '
                 'the required time {timeout}. Current status of {name}: '
diff --git a/releasenotes/notes/test-server-path-3845f619090ba016.yaml b/releasenotes/notes/test-server-path-3845f619090ba016.yaml
new file mode 100644
index 0000000..51f79dd
--- /dev/null
+++ b/releasenotes/notes/test-server-path-3845f619090ba016.yaml
@@ -0,0 +1,7 @@
+---
+other:
+  - |
+    The Octavia tempest plugin now as a configuration setting for the path to
+    the test server. By default it will expect the test server to now be
+    located in /opt/octavia-tempest-plugin/test_server.bin. The devstack
+    plugin has been updated to place the test_server.bin in that location.
diff --git a/zuul.d/jobs.yaml b/zuul.d/jobs.yaml
index 8d4ae71..b37636a 100644
--- a/zuul.d/jobs.yaml
+++ b/zuul.d/jobs.yaml
@@ -473,11 +473,6 @@
     override-checkout: stable/train
 
 - job:
-    name: octavia-v2-dsvm-noop-api-stable-stein
-    parent: octavia-v2-dsvm-noop-api
-    override-checkout: stable/stein
-
-- job:
     name: octavia-v2-dsvm-scenario
     parent: octavia-dsvm-live-base
     vars:
@@ -541,20 +536,6 @@
     parent: octavia-v2-dsvm-scenario
     override-checkout: stable/train
 
-- job:
-    name: octavia-v2-dsvm-scenario-stable-stein
-    parent: octavia-v2-dsvm-scenario
-    override-checkout: stable/stein
-    required-projects:
-      - name: openstack/diskimage-builder
-        override-checkout: 2.30.0
-    vars:
-      devstack_local_conf:
-        test-config:
-          "$TEMPEST_CONFIG":
-            loadbalancer-feature-enabled:
-              log_offload_enabled: False
-
 # Legacy jobs for the transition to the act-stdby two node jobs
 - job:
     name: octavia-v2-dsvm-scenario-two-node
@@ -681,14 +662,6 @@
     override-checkout: stable/train
 
 - job:
-    name: octavia-v2-dsvm-tls-barbican-stable-stein
-    parent: octavia-v2-dsvm-tls-barbican
-    override-checkout: stable/stein
-    required-projects:
-      - name: openstack/diskimage-builder
-        override-checkout: 2.30.0
-
-- job:
     name: octavia-v2-dsvm-tls-barbican-stable-rocky
     parent: octavia-v2-dsvm-tls-barbican
     nodeset: openstack-single-node-xenial
@@ -736,14 +709,6 @@
     override-checkout: stable/train
 
 - job:
-    name: octavia-v2-dsvm-spare-pool-stable-stein
-    parent: octavia-v2-dsvm-spare-pool
-    override-checkout: stable/stein
-    required-projects:
-      - name: openstack/diskimage-builder
-        override-checkout: 2.30.0
-
-- job:
     name: octavia-v2-dsvm-cinder-amphora
     parent: octavia-v2-dsvm-scenario
     required-projects:
@@ -882,14 +847,6 @@
     parent: octavia-v2-act-stdby-dsvm-scenario
     override-checkout: stable/train
 
-- job:
-    name: octavia-v2-act-stdby-dsvm-scenario-stable-stein
-    parent: octavia-v2-act-stdby-dsvm-scenario
-    override-checkout: stable/stein
-    required-projects:
-      - name: openstack/diskimage-builder
-        override-checkout: 2.30.0
-
 ######### Third party jobs ##########
 
 - job:
diff --git a/zuul.d/projects.yaml b/zuul.d/projects.yaml
index d66f616..9f7385d 100644
--- a/zuul.d/projects.yaml
+++ b/zuul.d/projects.yaml
@@ -11,15 +11,12 @@
         - octavia-v2-dsvm-noop-api
         - octavia-v2-dsvm-noop-api-stable-ussuri
         - octavia-v2-dsvm-noop-api-stable-train
-        - octavia-v2-dsvm-noop-api-stable-stein
         - octavia-v2-dsvm-scenario
         - octavia-v2-dsvm-scenario-stable-ussuri
         - octavia-v2-dsvm-scenario-stable-train
-        - octavia-v2-dsvm-scenario-stable-stein
         - octavia-v2-dsvm-tls-barbican
         - octavia-v2-dsvm-tls-barbican-stable-ussuri
         - octavia-v2-dsvm-tls-barbican-stable-train
-        - octavia-v2-dsvm-tls-barbican-stable-stein
         - octavia-v2-dsvm-scenario-ipv6-only:
             voting: false
         - octavia-v2-dsvm-scenario-centos-8:
@@ -32,16 +29,12 @@
             voting: false
         - octavia-v2-act-stdby-dsvm-scenario-stable-train:
             voting: false
-        - octavia-v2-act-stdby-dsvm-scenario-stable-stein:
-            voting: false
         - octavia-v2-dsvm-spare-pool:
             voting: false
         - octavia-v2-dsvm-spare-pool-stable-ussuri:
             voting: false
         - octavia-v2-dsvm-spare-pool-stable-train:
             voting: false
-        - octavia-v2-dsvm-spare-pool-stable-stein:
-            voting: false
         - octavia-v2-dsvm-cinder-amphora:
             voting: false
         # Third party provider jobs
@@ -56,12 +49,9 @@
         - octavia-v2-dsvm-noop-api
         - octavia-v2-dsvm-noop-api-stable-ussuri
         - octavia-v2-dsvm-noop-api-stable-train
-        - octavia-v2-dsvm-noop-api-stable-stein
         - octavia-v2-dsvm-scenario
         - octavia-v2-dsvm-scenario-stable-ussuri
         - octavia-v2-dsvm-scenario-stable-train
-        - octavia-v2-dsvm-scenario-stable-stein
         - octavia-v2-dsvm-tls-barbican
         - octavia-v2-dsvm-tls-barbican-stable-ussuri
         - octavia-v2-dsvm-tls-barbican-stable-train
-        - octavia-v2-dsvm-tls-barbican-stable-stein