X-Git-Url: https://git.arvados.org/arvados.git/blobdiff_plain/a4acb3ae95b2fc7f4b5f1e174c910a54cc6681da..a8378b8deaa2bbf9d2c154d9d9bb072538c288cc:/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 f8f8b18d76..d10e60c22f 100644 --- a/sdk/python/tests/run_test_server.py +++ b/sdk/python/tests/run_test_server.py @@ -4,6 +4,7 @@ from __future__ import print_function import argparse import atexit import errno +import glob import httplib2 import os import pipes @@ -12,8 +13,8 @@ import re import shutil import signal import socket -import subprocess import string +import subprocess import sys import tempfile import time @@ -32,11 +33,20 @@ import arvados.config ARVADOS_DIR = os.path.realpath(os.path.join(MY_DIRNAME, '../../..')) SERVICES_SRC_DIR = os.path.join(ARVADOS_DIR, 'services') -SERVER_PID_PATH = 'tmp/pids/test-server.pid' if 'GOPATH' in os.environ: + # Add all GOPATH bin dirs to PATH -- but insert them after the + # ruby gems bin dir, to ensure "bundle" runs the Ruby bundler + # command, not the golang.org/x/tools/cmd/bundle command. gopaths = os.environ['GOPATH'].split(':') - gobins = [os.path.join(path, 'bin') for path in gopaths] - os.environ['PATH'] = ':'.join(gobins) + ':' + os.environ['PATH'] + addbins = [os.path.join(path, 'bin') for path in gopaths] + newbins = [] + for path in os.environ['PATH'].split(':'): + newbins.append(path) + if os.path.exists(os.path.join(path, 'bundle')): + newbins += addbins + addbins = [] + newbins += addbins + os.environ['PATH'] = ':'.join(newbins) TEST_TMPDIR = os.path.join(ARVADOS_DIR, 'tmp') if not os.path.exists(TEST_TMPDIR): @@ -44,6 +54,7 @@ if not os.path.exists(TEST_TMPDIR): my_api_host = None _cached_config = {} +_cached_db_config = {} def find_server_pid(PID_PATH, wait=10): now = time.time() @@ -70,28 +81,74 @@ def kill_server_pid(pidfile, wait=10, passenger_root=False): 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: - 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) + + now = time.time() + startTERM = now + deadline = now + wait + + 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) + # Use up to half of the +wait+ period waiting for "passenger + # stop" to work. If the process hasn't exited by then, start + # sending TERM signals. + startTERM += wait/2 + + server_pid = None + while now <= deadline and server_pid is None: + try: + with open(pidfile, 'r') as f: + server_pid = int(f.read()) + except IOError: + # No pidfile = nothing to kill. + return + except ValueError as error: + # Pidfile exists, but we can't parse it. Perhaps the + # server has created the file but hasn't written its PID + # yet? + print("Parse error reading pidfile {}: {}".format(pidfile, error), + file=sys.stderr) time.sleep(0.1) now = time.time() - except EnvironmentError: - pass + + while now <= deadline: + try: + exited, _ = os.waitpid(server_pid, os.WNOHANG) + if exited > 0: + _remove_pidfile(pidfile) + return + except OSError: + # already exited, or isn't our child process + pass + try: + if now >= startTERM: + os.kill(server_pid, signal.SIGTERM) + print("Sent SIGTERM to {} ({})".format(server_pid, pidfile), + file=sys.stderr) + except OSError as error: + if error.errno == errno.ESRCH: + # Thrown by os.getpgid() or os.kill() if the process + # does not exist, i.e., our work here is done. + _remove_pidfile(pidfile) + return + raise + time.sleep(0.1) + now = time.time() + + print("Server PID {} ({}) did not exit, giving up after {}s". + format(server_pid, pidfile, wait), + file=sys.stderr) + +def _remove_pidfile(pidfile): + try: + os.unlink(pidfile) + except: + if os.path.lexists(pidfile): + raise def find_available_port(): """Return an IPv4 port number that is not in use right now. @@ -122,7 +179,8 @@ def _wait_until_port_listens(port, timeout=10): subprocess.check_output(['which', 'lsof']) except subprocess.CalledProcessError: print("WARNING: No `lsof` -- cannot wait for port to listen. "+ - "Sleeping 0.5 and hoping for the best.") + "Sleeping 0.5 and hoping for the best.", + file=sys.stderr) time.sleep(0.5) return deadline = time.time() + timeout @@ -139,6 +197,26 @@ def _wait_until_port_listens(port, timeout=10): format(port, timeout), file=sys.stderr) +def _fifo2stderr(label): + """Create a fifo, and copy it to stderr, prepending label to each line. + + Return value is the path to the new FIFO. + + +label+ should contain only alphanumerics: it is also used as part + of the FIFO filename. + """ + fifo = os.path.join(TEST_TMPDIR, label+'.fifo') + try: + os.remove(fifo) + except OSError as error: + if error.errno != errno.ENOENT: + raise + os.mkfifo(fifo, 0700) + subprocess.Popen( + ['stdbuf', '-i0', '-oL', '-eL', 'sed', '-e', 's/^/['+label+'] /', fifo], + stdout=sys.stderr) + return fifo + def run(leave_running_atexit=False): """Ensure an API server is running, and ARVADOS_API_* env vars have admin credentials for it. @@ -156,10 +234,17 @@ def run(leave_running_atexit=False): """ global my_api_host - # Delete cached discovery document. - shutil.rmtree(arvados.http_cache('discovery')) - - pid_file = os.path.join(SERVICES_SRC_DIR, 'api', SERVER_PID_PATH) + # Delete cached discovery documents. + # + # This will clear cached docs that belong to other processes (like + # concurrent test suites) even if they're still running. They should + # be able to tolerate that. + for fn in glob.glob(os.path.join( + str(arvados.http_cache('discovery')), + '*,arvados,v1,rest,*')): + os.unlink(fn) + + pid_file = _pidfile('api') pid_file_ok = find_server_pid(pid_file, 0) existing_api_host = os.environ.get('ARVADOS_TEST_API_HOST', my_api_host) @@ -220,10 +305,19 @@ def run(leave_running_atexit=False): os.makedirs(gitdir) subprocess.check_output(['tar', '-xC', gitdir, '-f', gittarball]) + # The nginx proxy isn't listening here yet, but we need to choose + # the wss:// port now so we can write the API server config file. + wss_port = find_available_port() + _setport('wss', wss_port) + port = find_available_port() env = os.environ.copy() env['RAILS_ENV'] = 'test' - env['ARVADOS_WEBSOCKETS'] = 'yes' + env['ARVADOS_TEST_WSS_PORT'] = str(wss_port) + if env.get('ARVADOS_TEST_EXPERIMENTAL_WS'): + env.pop('ARVADOS_WEBSOCKETS', None) + else: + env['ARVADOS_WEBSOCKETS'] = 'yes' env.pop('ARVADOS_TEST_API_HOST', None) env.pop('ARVADOS_API_HOST', None) env.pop('ARVADOS_API_HOST_INSECURE', None) @@ -231,7 +325,7 @@ def run(leave_running_atexit=False): start_msg = subprocess.check_output( ['bundle', 'exec', 'passenger', 'start', '-d', '-p{}'.format(port), - '--pid-file', os.path.join(os.getcwd(), pid_file), + '--pid-file', pid_file, '--log-file', os.path.join(os.getcwd(), 'log/test.log'), '--ssl', '--ssl-certificate', 'tmp/self-signed.pem', @@ -293,9 +387,50 @@ def stop(force=False): """ global my_api_host if force or my_api_host is not None: - kill_server_pid(os.path.join(SERVICES_SRC_DIR, 'api', SERVER_PID_PATH)) + kill_server_pid(_pidfile('api')) my_api_host = None +def run_ws(): + if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ: + return + stop_ws() + port = find_available_port() + conf = os.path.join(TEST_TMPDIR, 'ws.yml') + with open(conf, 'w') as f: + f.write(""" +Client: + APIHost: {} + Insecure: true +Listen: :{} +LogLevel: {} +Postgres: + host: {} + dbname: {} + user: {} + password: {} + sslmode: require + """.format(os.environ['ARVADOS_API_HOST'], + port, + ('info' if os.environ.get('ARVADOS_DEBUG', '') in ['','0'] else 'debug'), + _dbconfig('host'), + _dbconfig('database'), + _dbconfig('username'), + _dbconfig('password'))) + logf = open(_fifo2stderr('ws'), 'w') + ws = subprocess.Popen( + ["ws", "-config", conf], + stdin=open('/dev/null'), stdout=logf, stderr=logf, close_fds=True) + with open(_pidfile('ws'), 'w') as f: + f.write(str(ws.pid)) + _wait_until_port_listens(port) + _setport('ws', port) + return port + +def stop_ws(): + if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ: + return + kill_server_pid(_pidfile('ws')) + def _start_keep(n, keep_args): keep0 = tempfile.mkdtemp() port = find_available_port() @@ -307,9 +442,10 @@ def _start_keep(n, keep_args): for arg, val in keep_args.iteritems(): keep_cmd.append("{}={}".format(arg, val)) - logf = open(os.path.join(TEST_TMPDIR, 'keep{}.log'.format(n)), 'a+') + logf = open(_fifo2stderr('keep{}'.format(n)), 'w') kp0 = subprocess.Popen( keep_cmd, stdin=open('/dev/null'), stdout=logf, stderr=logf, close_fds=True) + with open(_pidfile('keep{}'.format(n)), 'w') as f: f.write(str(kp0.pid)) @@ -329,11 +465,10 @@ def run_keep(blob_signing_key=None, enforce_permissions=False, num_servers=2): with open(os.path.join(TEST_TMPDIR, "keep.blob_signing_key"), "w") as f: keep_args['-blob-signing-key-file'] = f.name f.write(blob_signing_key) - if enforce_permissions: - keep_args['-enforce-permissions'] = 'true' + keep_args['-enforce-permissions'] = str(enforce_permissions).lower() with open(os.path.join(TEST_TMPDIR, "keep.data-manager-token-file"), "w") as f: keep_args['-data-manager-token-file'] = f.name - f.write(os.environ['ARVADOS_API_TOKEN']) + f.write(auth_token('data_manager')) keep_args['-never-delete'] = 'false' api = arvados.api( @@ -364,10 +499,13 @@ def run_keep(blob_signing_key=None, enforce_permissions=False, num_servers=2): # keepstore services. proxypidfile = _pidfile('keepproxy') if os.path.exists(proxypidfile): - os.kill(int(open(proxypidfile).read()), signal.SIGHUP) + try: + os.kill(int(open(proxypidfile).read()), signal.SIGHUP) + except OSError: + os.remove(proxypidfile) def _stop_keep(n): - kill_server_pid(_pidfile('keep{}'.format(n)), 0) + kill_server_pid(_pidfile('keep{}'.format(n))) if os.path.exists("{}/keep{}.volume".format(TEST_TMPDIR, n)): with open("{}/keep{}.volume".format(TEST_TMPDIR, n), 'r') as r: shutil.rmtree(r.read(), True) @@ -384,20 +522,20 @@ def run_keep_proxy(): return stop_keep_proxy() - admin_token = auth_token('admin') port = find_available_port() env = os.environ.copy() - env['ARVADOS_API_TOKEN'] = admin_token + env['ARVADOS_API_TOKEN'] = auth_token('anonymous') + logf = open(_fifo2stderr('keepproxy'), 'w') kp = subprocess.Popen( ['keepproxy', '-pid='+_pidfile('keepproxy'), '-listen=:{}'.format(port)], - env=env, stdin=open('/dev/null'), stdout=sys.stderr) + env=env, stdin=open('/dev/null'), stdout=logf, stderr=logf, close_fds=True) api = arvados.api( version='v1', host=os.environ['ARVADOS_API_HOST'], - token=admin_token, + token=auth_token('admin'), insecure=True) for d in api.keep_services().list( filters=[['service_type','=','proxy']]).execute()['items']: @@ -408,14 +546,14 @@ def run_keep_proxy(): 'service_type': 'proxy', 'service_ssl_flag': False, }}).execute() - os.environ["ARVADOS_KEEP_PROXY"] = "http://localhost:{}".format(port) + os.environ["ARVADOS_KEEP_SERVICES"] = "http://localhost:{}".format(port) _setport('keepproxy', port) _wait_until_port_listens(port) def stop_keep_proxy(): if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ: return - kill_server_pid(_pidfile('keepproxy'), wait=0) + kill_server_pid(_pidfile('keepproxy')) def run_arv_git_httpd(): if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ: @@ -426,11 +564,12 @@ def run_arv_git_httpd(): gitport = find_available_port() env = os.environ.copy() env.pop('ARVADOS_API_TOKEN', None) + logf = open(_fifo2stderr('arv-git-httpd'), 'w') agh = subprocess.Popen( ['arv-git-httpd', '-repo-root='+gitdir+'/test', '-address=:'+str(gitport)], - env=env, stdin=open('/dev/null'), stdout=sys.stderr) + env=env, stdin=open('/dev/null'), stdout=logf, stderr=logf) with open(_pidfile('arv-git-httpd'), 'w') as f: f.write(str(agh.pid)) _setport('arv-git-httpd', gitport) @@ -439,7 +578,7 @@ def run_arv_git_httpd(): def stop_arv_git_httpd(): if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ: return - kill_server_pid(_pidfile('arv-git-httpd'), wait=0) + kill_server_pid(_pidfile('arv-git-httpd')) def run_keep_web(): if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ: @@ -448,12 +587,14 @@ def run_keep_web(): keepwebport = find_available_port() env = os.environ.copy() - env.pop('ARVADOS_API_TOKEN', None) + env['ARVADOS_API_TOKEN'] = auth_token('anonymous') + logf = open(_fifo2stderr('keep-web'), 'w') keepweb = subprocess.Popen( ['keep-web', - '-attachment-only-host=localhost:'+str(keepwebport), - '-address=:'+str(keepwebport)], - env=env, stdin=open('/dev/null'), stdout=sys.stderr) + '-allow-anonymous', + '-attachment-only-host=download:'+str(keepwebport), + '-listen=:'+str(keepwebport)], + env=env, stdin=open('/dev/null'), stdout=logf, stderr=logf) with open(_pidfile('keep-web'), 'w') as f: f.write(str(keepweb.pid)) _setport('keep-web', keepwebport) @@ -462,21 +603,25 @@ def run_keep_web(): def stop_keep_web(): if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ: return - kill_server_pid(_pidfile('keep-web'), wait=0) + kill_server_pid(_pidfile('keep-web')) def run_nginx(): if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ: return + stop_nginx() nginxconf = {} nginxconf['KEEPWEBPORT'] = _getport('keep-web') + nginxconf['KEEPWEBDLSSLPORT'] = find_available_port() nginxconf['KEEPWEBSSLPORT'] = find_available_port() nginxconf['KEEPPROXYPORT'] = _getport('keepproxy') nginxconf['KEEPPROXYSSLPORT'] = find_available_port() nginxconf['GITPORT'] = _getport('arv-git-httpd') nginxconf['GITSSLPORT'] = find_available_port() + nginxconf['WSPORT'] = _getport('ws') + nginxconf['WSSPORT'] = _getport('wss') nginxconf['SSLCERT'] = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.pem') nginxconf['SSLKEY'] = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.key') - nginxconf['ACCESSLOG'] = os.path.join(TEST_TMPDIR, 'nginx_access_log.fifo') + nginxconf['ACCESSLOG'] = _fifo2stderr('nginx_access_log') conftemplatefile = os.path.join(MY_DIRNAME, 'nginx.conf') conffile = os.path.join(TEST_TMPDIR, 'nginx.conf') @@ -489,22 +634,13 @@ def run_nginx(): env = os.environ.copy() env['PATH'] = env['PATH']+':/sbin:/usr/sbin:/usr/local/sbin' - try: - os.remove(nginxconf['ACCESSLOG']) - except OSError as error: - if error.errno != errno.ENOENT: - raise - - os.mkfifo(nginxconf['ACCESSLOG'], 0700) nginx = subprocess.Popen( ['nginx', '-g', 'error_log stderr info;', '-g', 'pid '+_pidfile('nginx')+';', '-c', conffile], env=env, stdin=open('/dev/null'), stdout=sys.stderr) - cat_access = subprocess.Popen( - ['cat', nginxconf['ACCESSLOG']], - stdout=sys.stderr) + _setport('keep-web-dl-ssl', nginxconf['KEEPWEBDLSSLPORT']) _setport('keep-web-ssl', nginxconf['KEEPWEBSSLPORT']) _setport('keepproxy-ssl', nginxconf['KEEPPROXYSSLPORT']) _setport('arv-git-httpd-ssl', nginxconf['GITSSLPORT']) @@ -512,7 +648,7 @@ def run_nginx(): def stop_nginx(): if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ: return - kill_server_pid(_pidfile('nginx'), wait=0) + kill_server_pid(_pidfile('nginx')) def _pidfile(program): return os.path.join(TEST_TMPDIR, program + '.pid') @@ -531,7 +667,15 @@ def _getport(program): except IOError: return 9 +def _dbconfig(key): + global _cached_db_config + if not _cached_db_config: + _cached_db_config = yaml.load(open(os.path.join( + SERVICES_SRC_DIR, 'api', 'config', 'database.yml'))) + return _cached_db_config['test'][key] + def _apiconfig(key): + global _cached_config if _cached_config: return _cached_config[key] def _load(f, required=True): @@ -585,6 +729,7 @@ class TestCaseWithServers(unittest.TestCase): original environment. """ MAIN_SERVER = None + WS_SERVER = None KEEP_SERVER = None KEEP_PROXY_SERVER = None KEEP_WEB_SERVER = None @@ -601,10 +746,11 @@ class TestCaseWithServers(unittest.TestCase): cls._orig_environ = os.environ.copy() cls._orig_config = arvados.config.settings().copy() cls._cleanup_funcs = [] - os.environ.pop('ARVADOS_KEEP_PROXY', None) + os.environ.pop('ARVADOS_KEEP_SERVICES', None) os.environ.pop('ARVADOS_EXTERNAL_CLIENT', None) for server_kwargs, start_func, stop_func in ( (cls.MAIN_SERVER, run, reset), + (cls.WS_SERVER, run_ws, stop_ws), (cls.KEEP_SERVER, run_keep, stop_keep), (cls.KEEP_PROXY_SERVER, run_keep_proxy, stop_keep_proxy), (cls.KEEP_WEB_SERVER, run_keep_web, stop_keep_web)): @@ -631,6 +777,7 @@ class TestCaseWithServers(unittest.TestCase): if __name__ == "__main__": actions = [ 'start', 'stop', + 'start_ws', 'stop_ws', 'start_keep', 'stop_keep', 'start_keep_proxy', 'stop_keep_proxy', 'start_keep-web', 'stop_keep-web', @@ -646,7 +793,9 @@ if __name__ == "__main__": args = parser.parse_args() if args.action not in actions: - print("Unrecognized action '{}'. Actions are: {}.".format(args.action, actions), file=sys.stderr) + print("Unrecognized action '{}'. Actions are: {}.". + format(args.action, actions), + file=sys.stderr) sys.exit(1) if args.action == 'start': stop(force=('ARVADOS_TEST_API_HOST' not in os.environ)) @@ -661,10 +810,14 @@ if __name__ == "__main__": print(host) elif args.action == 'stop': stop(force=('ARVADOS_TEST_API_HOST' not in os.environ)) + elif args.action == 'start_ws': + run_ws() + elif args.action == 'stop_ws': + stop_ws() elif args.action == 'start_keep': run_keep(enforce_permissions=args.keep_enforce_permissions, num_servers=args.num_keep_servers) elif args.action == 'stop_keep': - stop_keep() + stop_keep(num_servers=args.num_keep_servers) elif args.action == 'start_keep_proxy': run_keep_proxy() elif args.action == 'stop_keep_proxy':