Scenario tests: improve test http server

Replacing existing bash and netcat based test HTTP server with a golang
implementation, to permit testing of connection limits. While desirable
to avoid introducing an additional testing dependency, employing golang
here solves several problems. The static linked binary works well with
CirrOS images for testing, since not other files need be included in the
CirrOS image. The implementation can scale to large number of
connections (5k in a m1.tiny instance, and in excess of the 20k limit
imposed by Apache Bench in a m1.large) and tracks the maximum number of
concurrent connections reached, this allows connection_limit testing.

Change-Id: Ib1320559142ca05177c5cb93f22baee401c17470
This commit is contained in:
Dustin Lundquist 2016-10-04 20:37:53 +00:00
parent 514edb14b5
commit 03cd2ec434
4 changed files with 129 additions and 35 deletions

View File

@ -0,0 +1 @@
golang

View File

@ -0,0 +1 @@
golang

View File

@ -0,0 +1,87 @@
package main
import (
"flag"
"fmt"
"io"
"net/http"
"sync"
"time"
)
var sess_cookie http.Cookie
var resp string
type ConnectionCount struct {
mu sync.Mutex
cur_conn int
max_conn int
total_conn int
}
var scoreboard ConnectionCount
func (cc *ConnectionCount) open() {
cc.mu.Lock()
defer cc.mu.Unlock()
cc.cur_conn++
cc.total_conn++
}
func (cc *ConnectionCount) close() {
cc.mu.Lock()
defer cc.mu.Unlock()
if cc.cur_conn > cc.max_conn {
cc.max_conn = cc.cur_conn
}
cc.cur_conn--
}
func (cc *ConnectionCount) stats() (int, int) {
cc.mu.Lock()
defer cc.mu.Unlock()
return cc.max_conn, cc.total_conn
}
func root_handler(w http.ResponseWriter, r *http.Request) {
scoreboard.open()
defer scoreboard.close()
http.SetCookie(w, &sess_cookie)
io.WriteString(w, resp)
}
func slow_handler(w http.ResponseWriter, r *http.Request) {
scoreboard.open()
defer scoreboard.close()
time.Sleep(3 * time.Second)
http.SetCookie(w, &sess_cookie)
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 main() {
portPtr := flag.Int("port", 8080, "TCP port to listen on")
idPtr := flag.Int("id", 1, "Server ID")
flag.Parse()
resp = fmt.Sprintf("server%d", *idPtr)
sess_cookie.Name = "JESSIONID"
sess_cookie.Value = fmt.Sprintf("%d", *idPtr)
http.HandleFunc("/", root_handler)
http.HandleFunc("/slow", slow_handler)
http.HandleFunc("/stats", stats_handler)
portStr := fmt.Sprintf(":%d", *portPtr)
http.ListenAndServe(portStr, nil)
}

View File

@ -17,7 +17,9 @@ try:
from http import cookiejar as cookielib
except ImportError:
import cookielib
import os
import shlex
import shutil
import socket
import subprocess
import tempfile
@ -47,6 +49,9 @@ from octavia.tests.tempest.v1.clients import pools_client
config = config.CONF
LOG = logging.getLogger(__name__)
HTTPD_SRC = os.path.abspath(
os.path.join(os.path.dirname(__file__),
'../../../contrib/httpd.go'))
class BaseTestCase(manager.NetworkScenarioTest):
@ -221,6 +226,18 @@ class BaseTestCase(manager.NetworkScenarioTest):
waiters.wait_for_server_status(self.servers_client,
value, 'ACTIVE')
def _build_static_httpd(self):
"""Compile test httpd as a static binary
returns file path of resulting binary file
"""
builddir = tempfile.mkdtemp()
shutil.copyfile(HTTPD_SRC, os.path.join(builddir, 'httpd.go'))
self.execute('go build -ldflags '
'"-linkmode external -extldflags -static" '
'httpd.go', cwd=builddir)
return os.path.join(builddir, 'httpd')
def _start_servers(self):
"""Start one or more backends
@ -236,46 +253,30 @@ class BaseTestCase(manager.NetworkScenarioTest):
ip_address=ip,
private_key=private_key)
# Write a backend's response into a file
resp = ('echo -ne "HTTP/1.1 200 OK\r\nContent-Length: 7\r\n'
'Set-Cookie:JSESSIONID=%(s_id)s\r\nConnection: close\r\n'
'Content-Type: text/html; '
'charset=UTF-8\r\n\r\n%(server)s"; cat >/dev/null')
httpd = self._build_static_httpd()
with tempfile.NamedTemporaryFile() as script:
script.write(resp % {'s_id': server_name[-1],
'server': server_name})
script.flush()
with tempfile.NamedTemporaryFile() as key:
key.write(private_key)
key.flush()
self.copy_file_to_host(script.name,
"/tmp/script1",
ip,
username, key.name)
with tempfile.NamedTemporaryFile() as key:
key.write(private_key)
key.flush()
self.copy_file_to_host(httpd,
"/dev/shm/httpd",
ip,
username, key.name)
# Start netcat
start_server = ('while true; do '
'sudo nc -ll -p %(port)s -e sh /tmp/%(script)s; '
'done > /dev/null &')
cmd = start_server % {'port': self.port1,
'script': 'script1'}
# Start httpd
start_server = ('sudo sh -c "ulimit -n 100000; nohup '
'/dev/shm/httpd -id %(id)s '
'-port %(port)s &"')
cmd = start_server % {'id': server_name[-1],
'port': self.port1}
ssh_client.exec_command(cmd)
# In single server case run a second server on an alternate port
if len(self.server_ips) == 1:
with tempfile.NamedTemporaryFile() as script:
script.write(resp % {'s_id': 2,
'server': 'server2'})
script.flush()
with tempfile.NamedTemporaryFile() as key:
key.write(private_key)
key.flush()
self.copy_file_to_host(script.name,
"/tmp/script2", ip,
username, key.name)
cmd = start_server % {'port': self.port2,
'script': 'script2'}
cmd = start_server % {'id': '2',
'port': self.port2}
ssh_client.exec_command(cmd)
# Allow ssh_client connection to fall out of scope
def _create_listener(self, load_balancer_id, default_pool_id=None):
"""Create a listener with HTTP protocol listening on port 80."""
@ -754,9 +755,13 @@ class BaseTestCase(manager.NetworkScenarioTest):
"-i %(pkey)s %(file1)s %(dest)s" % {'pkey': pkey,
'file1': file_from,
'dest': dest})
return self.execute(cmd)
def execute(self, cmd, cwd=None):
args = shlex.split(cmd.encode('utf-8'))
subprocess_args = {'stdout': subprocess.PIPE,
'stderr': subprocess.STDOUT}
'stderr': subprocess.STDOUT,
'cwd': cwd}
proc = subprocess.Popen(args, **subprocess_args)
stdout, stderr = proc.communicate()
if proc.returncode != 0: