Refactoring of stub webserver

Main goals - avoid "path math". There was an ugly path math in stub
webserver start phase. Also there was mostly same path math in lookup of
static files served by this stub webserver.

Change-Id: Id0c348ceefb257dff0950adcd778edd69e6ad11e
This commit is contained in:
Dmitry Bogun 2016-12-09 17:02:07 +02:00
parent e6cc50bcff
commit cd7b36904e
5 changed files with 218 additions and 172 deletions

View File

@ -13,6 +13,7 @@
# License for the specific language governing permissions and limitations
# under the License.
import errno
import os
import shutil
import subprocess
@ -26,7 +27,6 @@ from oslo_config import cfg
from ramdisk_func_test import conf
from ramdisk_func_test import utils
from ramdisk_func_test.base import TemplateEngine
from ramdisk_func_test.base import ABS_PATH
from ramdisk_func_test.network import Network
from ramdisk_func_test.node import Node
@ -157,13 +157,11 @@ class Environment(object):
port = CONF.stub_webserver_port
LOG.info("Starting stub webserver (at IP {0} port {1}, path to tenant "
"images folder is '{2}')".format(self.network.address, port,
self.tenant_images_dir))
CONF.tenant_images_dir))
# TODO(max_lobur) make webserver singletone
self.webserver = subprocess.Popen(
['python',
os.path.join(ABS_PATH, 'webserver/server.py'),
self.network.address, port, self.tenant_images_dir], shell=False)
cmd = ['ramdisk-stub-webserver', self.network.address, str(port)]
self.webserver = subprocess.Popen(cmd, shell=False)
def get_url_for_image(self, image_name, source_type):
if source_type == 'swift':
@ -208,14 +206,23 @@ class Environment(object):
def _teardown_webserver(self):
LOG.info("Stopping stub web server ...")
self.webserver.terminate()
for i in range(0, 15):
if self.webserver.poll() is not None:
LOG.info("Stub web server has stopped.")
try:
self.webserver.terminate()
for i in range(0, 15):
if self.webserver.poll() is not None:
LOG.info("Stub web server has stopped.")
break
time.sleep(1)
else:
LOG.warning(
'15 seconds have passed since sending SIGTERM to the stub '
'web server. It is still alive. Send SIGKILL.')
self.webserver.kill()
self.webserver.wait() # collect zombie
except OSError as e:
if e.errno == errno.ESRCH:
return
time.sleep(1)
LOG.warning("Cannot terminate web server in 15 sec!")
raise
def _delete_workdir(self):
LOG.info("Deleting workdir {0}".format(CONF.ramdisk_func_test_workdir))

View File

@ -0,0 +1,188 @@
#!/usr/bin/env python
#
# Copyright 2016 Cray Inc., All Rights Reserved
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Mock web-server.
It gets 2 positional parameters:
- host
- port
For GET requests:
- If URL contains '/fake' at the beginning, mock web-server returns content
of ./stubfile
- If URL is like '/tenant_images/<name>' at the beginning, mock web-server
returns content of <name> file from folder specified in third positional
parameter
- Fof all other cases, it tries to return appropriate file (e.g.
'/tmp/banana.txt' for URL 'http://host:port/tmp/banana.txt')
For POST requests:
- If URL contains '/v1/nodes/<node_id>/vendor_passthru', mock web-server
creates empty file with 'callback' name at subfolder named by <node_id>, in
fpa_func_test working dir (and returns 202, with content of ./stubfile)
"""
import argparse
import os
import SimpleHTTPServer
import SocketServer
import logging
import pkg_resources
import signal
import sys
import re
from ramdisk_func_test import conf
logging.basicConfig(filename='/tmp/mock-web-server.log',
level=logging.DEBUG,
format='%(asctime)s %(message)s',
datefmt='%m/%d/%Y %I:%M:%S %p')
CONF = conf.CONF
CONF.import_opt('tenant_images_dir', 'ramdisk_func_test.environment')
CONF.import_opt('ramdisk_func_test_workdir', 'ramdisk_func_test.utils')
LOG = logging.getLogger(__name__)
httpd = None
class RequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
def __init__(self, ctx, *args, **kwargs):
self.ctx = ctx
SimpleHTTPServer.SimpleHTTPRequestHandler.__init__(
self, *args, **kwargs)
def do_GET(self):
LOG.info('Got GET request: %s', self.path)
fake_check = re.match(r'/fake', self.path)
tenant_images_check = re.match(r'/tenant_images/(.*)$', self.path)
if fake_check is not None:
LOG.info("This is 'fake' request.")
self.path = os.path.join(self.ctx.htdocs, 'stubfile')
elif tenant_images_check is not None:
LOG.info("This is 'tenant-images' request: %s", self.path)
tenant_image = tenant_images_check.group(1)
self.path = os.path.join(self.ctx.images_path, tenant_image)
return SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self)
def do_POST(self):
callback_check = re.search(
r'/v1/nodes/([^/]*)/vendor_passthru', self.path)
if callback_check is not None:
callback_file_path = os.path.join(
CONF.ramdisk_func_test_workdir, callback_check.group(1),
'callback')
open(callback_file_path, 'a').close()
LOG.info("Got callback: %s", self.path)
self.path = os.path.join(self.ctx.htdocs, 'stubfile')
return SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self)
def send_head(self):
"""Common code for GET and HEAD commands.
This sends the response code and MIME headers.
Return value is either a file object (which has to be copied
to the output file by the caller unless the command was HEAD,
and must be closed by the caller under all circumstances), or
None, in which case the caller has nothing further to do.
"""
path = self.path
ctype = self.guess_type(path)
try:
# Always read in binary mode. Opening files in text mode may cause
# newline translations, making the actual size of the content
# transmitted *less* than the content-length!
payload = open(path, 'rb')
except IOError:
self.send_error(404, "File not found ({0})".format(path))
return None
if self.command == 'POST':
self.send_response(202)
else:
self.send_response(200)
stat = os.fstat(payload.fileno())
self.send_header("Content-type", ctype)
self.send_header("Content-Length", str(stat.st_size))
self.send_header("Last-Modified", self.date_time_string(stat.st_mtime))
self.end_headers()
return payload
class HandlerFactory(object):
def __init__(self, ctx, halder_class):
self.ctx = ctx
self.handler_class = halder_class
def __call__(self, *args, **kwargs):
return self.handler_class(self.ctx, *args, **kwargs)
class Context(object):
def __init__(self):
self.images_path = CONF.tenant_images_dir
self.htdocs = pkg_resources.resource_filename(__name__, 'data')
def signal_term_handler(signal, frame):
LOG.info("ramdisk-func-test stub web server terminating ...")
try:
httpd.server_close()
except Exception:
LOG.error('Cannot close server!', exc_info=True)
sys.exit(1)
LOG.info("ramdisk-func-test stub web server has terminated.")
sys.exit(0)
def main():
global httpd
argp = argparse.ArgumentParser()
argp.add_argument('address', help='Bind address')
argp.add_argument('port', help='Bind port', type=int)
cli = argp.parse_args()
bind = (cli.address, cli.port)
handler = HandlerFactory(Context(), RequestHandler)
try:
SocketServer.TCPServer.allow_reuse_address = True
httpd = SocketServer.TCPServer(bind, handler)
except Exception:
LOG.error('=' * 80)
LOG.error('Error in webserver start stage', exc_info=True)
sys.exit(1)
LOG.info('=' * 80)
LOG.info('ramdisk-func-test stub webserver started at %s:%s '
'(tenant-images path is %s)',
cli.address, cli.port, handler.ctx.images_path)
signal.signal(signal.SIGTERM, signal_term_handler)
httpd.serve_forever()

View File

@ -1,158 +0,0 @@
#!/usr/bin/env python
#
# Copyright 2016 Cray Inc., All Rights Reserved
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import os
import SimpleHTTPServer
import SocketServer
import logging
import signal
import sys
import traceback
import re
from ramdisk_func_test import conf
from ramdisk_func_test.base import ABS_PATH
CONF = conf.CONF
LOG = logging.getLogger(__name__)
logging.basicConfig(filename='/tmp/mock-web-server.log',
level=logging.DEBUG,
format='%(asctime)s %(message)s',
datefmt='%m/%d/%Y %I:%M:%S %p')
class MyRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
path_to_images_folder = None
@classmethod
def _set_path_to_images_folder(cls, path):
cls.path_to_images_folder = path
def do_GET(self):
LOG.info("Got GET request: {0} ".format(self.path))
fake_check = re.match(r'/fake', self.path)
tenant_images_check = re.match(r'/tenant_images/', self.path)
if fake_check is not None:
LOG.info("This is 'fake' request.")
self.path = os.path.join(ABS_PATH, 'webserver', 'stubfile')
elif tenant_images_check is not None:
LOG.info("This is 'tenant-images' request: {0} ".format(self.path))
tenant_images_name = re.match(
r'/tenant_images/(.*)', self.path).group(1)
self.path = os.path.join(
self.path_to_images_folder, tenant_images_name)
return SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self)
def do_POST(self):
callback_check = re.search(
r'/v1/nodes/([^/]*)/vendor_passthru', self.path)
if callback_check is not None:
callback_file_path = os.path.join(
CONF.ramdisk_func_test_workdir, callback_check.group(1),
'callback')
open(callback_file_path, 'a').close()
LOG.info("Got callback: {0} ".format(self.path))
self.path = os.path.join(ABS_PATH, 'webserver', 'stubfile')
return SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self)
def send_head(self):
"""Common code for GET and HEAD commands.
This sends the response code and MIME headers.
Return value is either a file object (which has to be copied
to the output file by the caller unless the command was HEAD,
and must be closed by the caller under all circumstances), or
None, in which case the caller has nothing further to do.
"""
f = None
path = self.path
ctype = self.guess_type(path)
try:
# Always read in binary mode. Opening files in text mode may cause
# newline translations, making the actual size of the content
# transmitted *less* than the content-length!
f = open(path, 'rb')
except IOError:
self.send_error(404, "File not found ({0})".format(path))
return None
if self.command == 'POST':
self.send_response(202)
else:
self.send_response(200)
self.send_header("Content-type", ctype)
fs = os.fstat(f.fileno())
self.send_header("Content-Length", str(fs[6]))
self.send_header("Last-Modified", self.date_time_string(fs.st_mtime))
self.end_headers()
return f
Handler = MyRequestHandler
httpd = None
def signal_term_handler(s, f):
LOG.info("ramdisk-func-test stub web server terminating ...")
try:
httpd.server_close()
except Exception:
LOG.error("Cannot close server!")
sys.exit(1)
LOG.info("ramdisk-func-test stub web server has terminated.")
sys.exit(0)
signal.signal(signal.SIGTERM, signal_term_handler)
if __name__ == "__main__":
try:
host = sys.argv[1]
port = int(sys.argv[2])
path_to_images_folder = sys.argv[3]
except IndexError:
LOG.error("Mock web-server cannot get enough valid parameters!")
exit(1)
Handler._set_path_to_images_folder(path_to_images_folder)
try:
SocketServer.TCPServer.allow_reuse_address = True
httpd = SocketServer.TCPServer((host, port), Handler)
except Exception:
LOG.error("="*80)
LOG.error("Cannot start: {0}".format(traceback.format_exc()))
exit(1)
LOG.info("="*80)
LOG.info("ramdisk-func-test stub webserver started at {0}:{1} "
"(tenant-images path is '{2}')".format(host, port,
path_to_images_folder))
httpd.serve_forever()

View File

@ -14,14 +14,20 @@
# under the License.
from setuptools import setup
from setuptools import find_packages
setup(
name='ramdisk-func-test',
version='0.1.0',
packages=['ramdisk_func_test'],
packages=find_packages(),
classifiers=[
'Programming Language :: Python :: 2.7',
],
entry_points={
'console_scripts':
'ramdisk-stub-webserver = ramdisk_func_test.webserver:main'
},
install_requires=[
'stevedore>=1.3.0,<1.4.0', # Not used. Prevents pip dependency conflict.
# This corresponds to openstack global-requirements.txt
@ -31,6 +37,9 @@ setup(
'pyyaml',
'sh',
],
package_data={
'ramdisk_func_test.webserver': ['data/*']
},
url='',
license='Apache License, Version 2.0',
author='',