From 03cd2ec43471d93e8cc82f58e6e4ee81e566411a Mon Sep 17 00:00:00 2001 From: Dustin Lundquist Date: Tue, 4 Oct 2016 20:37:53 +0000 Subject: [PATCH] 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 --- devstack/files/debs/octavia | 1 + devstack/files/rpms/octavia | 1 + octavia/tests/contrib/httpd.go | 87 +++++++++++++++++++++++ octavia/tests/tempest/v1/scenario/base.py | 75 ++++++++++--------- 4 files changed, 129 insertions(+), 35 deletions(-) create mode 100644 devstack/files/debs/octavia create mode 100644 devstack/files/rpms/octavia create mode 100644 octavia/tests/contrib/httpd.go diff --git a/devstack/files/debs/octavia b/devstack/files/debs/octavia new file mode 100644 index 0000000000..f5267c2664 --- /dev/null +++ b/devstack/files/debs/octavia @@ -0,0 +1 @@ +golang diff --git a/devstack/files/rpms/octavia b/devstack/files/rpms/octavia new file mode 100644 index 0000000000..f5267c2664 --- /dev/null +++ b/devstack/files/rpms/octavia @@ -0,0 +1 @@ +golang diff --git a/octavia/tests/contrib/httpd.go b/octavia/tests/contrib/httpd.go new file mode 100644 index 0000000000..23814e301c --- /dev/null +++ b/octavia/tests/contrib/httpd.go @@ -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) +} diff --git a/octavia/tests/tempest/v1/scenario/base.py b/octavia/tests/tempest/v1/scenario/base.py index 47b820b7f7..8e3c5d3c63 100644 --- a/octavia/tests/tempest/v1/scenario/base.py +++ b/octavia/tests/tempest/v1/scenario/base.py @@ -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: