X-Git-Url: https://git.arvados.org/arvados.git/blobdiff_plain/13181107ecaeaa92e5d96b05270e56b2d807af39..5549904bbd5dec9bafe60e36d4ea1abe6b791f19:/sdk/python/tests/run_test_server.py diff --git a/sdk/python/tests/run_test_server.py b/sdk/python/tests/run_test_server.py index 534c767b87..b9502f0f8e 100644 --- a/sdk/python/tests/run_test_server.py +++ b/sdk/python/tests/run_test_server.py @@ -9,7 +9,9 @@ import random import re import shutil import signal +import socket import subprocess +import string import sys import tempfile import time @@ -23,7 +25,7 @@ if __name__ == '__main__' and os.path.exists( # Add the Python SDK source to the library path. sys.path.insert(1, os.path.dirname(MY_DIRNAME)) -import arvados.api +import arvados import arvados.config ARVADOS_DIR = os.path.realpath(os.path.join(MY_DIRNAME, '../../..')) @@ -61,50 +63,101 @@ def find_server_pid(PID_PATH, wait=10): return server_pid -def kill_server_pid(pidfile, wait=10): +def kill_server_pid(pidfile, wait=10, passenger_root=False): # Must re-import modules in order to work during atexit import os import signal + import subprocess import time try: + if passenger_root: + # First try to shut down nicely + restore_cwd = os.getcwd() + os.chdir(passenger_root) + subprocess.call([ + 'bundle', 'exec', 'passenger', 'stop', '--pid-file', pidfile]) + os.chdir(restore_cwd) now = time.time() timeout = now + wait with open(pidfile, 'r') as f: server_pid = int(f.read()) while now <= timeout: - os.kill(server_pid, signal.SIGTERM) - os.getpgid(server_pid) # throw OSError if no such pid - now = time.time() + if not passenger_root or timeout - now < wait / 2: + # Half timeout has elapsed. Start sending SIGTERM + os.kill(server_pid, signal.SIGTERM) + # Raise OSError if process has disappeared + os.getpgid(server_pid) time.sleep(0.1) + now = time.time() except IOError: pass except OSError: pass +def find_available_port(): + """Return an IPv4 port number that is not in use right now. + + We assume whoever needs to use the returned port is able to reuse + a recently used port without waiting for TIME_WAIT (see + SO_REUSEADDR / SO_REUSEPORT). + + Some opportunity for races here, but it's better than choosing + something at random and not checking at all. If all of our servers + (hey Passenger) knew that listening on port 0 was a thing, the OS + would take care of the races, and this wouldn't be needed at all. + """ + + sock = socket.socket() + sock.bind(('0.0.0.0', 0)) + port = sock.getsockname()[1] + sock.close() + return port + def run(leave_running_atexit=False): """Ensure an API server is running, and ARVADOS_API_* env vars have admin credentials for it. + + If ARVADOS_TEST_API_HOST is set, a parent process has started a + test server for us to use: we just need to reset() it using the + admin token fixture. + + If a previous call to run() started a new server process, and it + is still running, we just need to reset() it to fixture state and + return. + + If neither of those options work out, we'll really start a new + server. """ global my_api_host # Delete cached discovery document. shutil.rmtree(arvados.http_cache('discovery')) - os.environ['ARVADOS_API_TOKEN'] = auth_token('admin') - os.environ['ARVADOS_API_HOST_INSECURE'] = 'true' - pid_file = os.path.join(SERVICES_SRC_DIR, 'api', SERVER_PID_PATH) pid_file_ok = find_server_pid(pid_file, 0) - if pid_file_ok: - try: - reset() - return - except: - pass + existing_api_host = os.environ.get('ARVADOS_TEST_API_HOST', my_api_host) + if existing_api_host and pid_file_ok: + if existing_api_host == my_api_host: + try: + return reset() + except: + # Fall through to shutdown-and-start case. + pass + else: + # Server was provided by parent. Can't recover if it's + # unresettable. + return reset() + + # Before trying to start up our own server, call stop() to avoid + # "Phusion Passenger Standalone is already running on PID 12345". + # (If we've gotten this far, ARVADOS_TEST_API_HOST isn't set, so + # we know the server is ours to kill.) + stop(force=True) restore_cwd = os.getcwd() - os.chdir(os.path.join(SERVICES_SRC_DIR, 'api')) + api_src_dir = os.path.join(SERVICES_SRC_DIR, 'api') + os.chdir(api_src_dir) # Either we haven't started a server of our own yet, or it has # died, or we have lost our credentials, or something else is @@ -122,9 +175,10 @@ def run(leave_running_atexit=False): '-out', 'tmp/self-signed.pem', '-keyout', 'tmp/self-signed.key', '-days', '3650', - '-subj', '/CN=0.0.0.0']) + '-subj', '/CN=0.0.0.0'], + stdout=sys.stderr) - port = random.randint(20000, 40000) + port = find_available_port() env = os.environ.copy() env['RAILS_ENV'] = 'test' env['ARVADOS_WEBSOCKETS'] = 'yes' @@ -143,7 +197,7 @@ def run(leave_running_atexit=False): env=env) if not leave_running_atexit: - atexit.register(kill_server_pid, pid_file) + atexit.register(kill_server_pid, pid_file, passenger_root=api_src_dir) match = re.search(r'Accessible via: https://(.*?)/', start_msg) if not match: @@ -159,17 +213,39 @@ def run(leave_running_atexit=False): os.chdir(restore_cwd) def reset(): + """Reset the test server to fixture state. + + This resets the ARVADOS_TEST_API_HOST provided by a parent process + if any, otherwise the server started by run(). + + It also resets ARVADOS_* environment vars to point to the test + server with admin credentials. + """ + existing_api_host = os.environ.get('ARVADOS_TEST_API_HOST', my_api_host) token = auth_token('admin') httpclient = httplib2.Http(ca_certs=os.path.join( SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.pem')) httpclient.request( - 'https://{}/database/reset'.format(os.environ['ARVADOS_API_HOST']), + 'https://{}/database/reset'.format(existing_api_host), 'POST', headers={'Authorization': 'OAuth2 {}'.format(token)}) + os.environ['ARVADOS_API_HOST_INSECURE'] = 'true' + os.environ['ARVADOS_API_HOST'] = existing_api_host + os.environ['ARVADOS_API_TOKEN'] = token def stop(force=False): - """Stop the API server, if one is running. If force==True, kill it - even if we didn't start it ourselves. + """Stop the API server, if one is running. + + If force==False, kill it only if we started it ourselves. (This + supports the use case where a Python test suite calls run(), but + run() just uses the ARVADOS_TEST_API_HOST provided by the parent + process, and the test suite cleans up after itself by calling + stop(). In this case the test server provided by the parent + process should be left alone.) + + If force==True, kill it even if we didn't start it + ourselves. (This supports the use case in __main__, where "run" + and "stop" happen in different processes.) """ global my_api_host if force or my_api_host is not None: @@ -178,7 +254,7 @@ def stop(force=False): def _start_keep(n, keep_args): keep0 = tempfile.mkdtemp() - port = random.randint(20000, 40000) + port = find_available_port() keep_cmd = ["keepstore", "-volumes={}".format(keep0), "-listen=:{}".format(port), @@ -208,7 +284,7 @@ def run_keep(blob_signing_key=None, enforce_permissions=False): keep_args['--enforce-permissions'] = 'true' api = arvados.api( - 'v1', cache=False, + version='v1', host=os.environ['ARVADOS_API_HOST'], token=os.environ['ARVADOS_API_TOKEN'], insecure=True) @@ -247,7 +323,7 @@ def run_keep_proxy(): stop_keep_proxy() admin_token = auth_token('admin') - port = random.randint(20000,40000) + port = find_available_port() env = os.environ.copy() env['ARVADOS_API_TOKEN'] = admin_token kp = subprocess.Popen( @@ -257,7 +333,7 @@ def run_keep_proxy(): env=env) api = arvados.api( - 'v1', cache=False, + version='v1', host=os.environ['ARVADOS_API_HOST'], token=admin_token, insecure=True)