import shutil
import signal
import subprocess
+import string
import sys
import tempfile
import time
return server_pid
-def kill_server_pid(pidfile, wait=10, passenger=False):
+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:
+ if passenger_root:
# First try to shut down nicely
restore_cwd = os.getcwd()
- os.chdir(os.path.join(SERVICES_SRC_DIR, 'api'))
+ os.chdir(passenger_root)
subprocess.call([
'bundle', 'exec', 'passenger', 'stop', '--pid-file', pidfile])
os.chdir(restore_cwd)
with open(pidfile, 'r') as f:
server_pid = int(f.read())
while now <= timeout:
- if not passenger or timeout - now < wait / 2:
+ 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
except OSError:
pass
+def find_available_port():
+ """Return a port number that is not in use right now.
+
+ 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.
+ """
+ port = None
+ while port is None:
+ port = random.randint(20000, 40000)
+ port_hex = ':%04x ' % port
+ try:
+ with open('/proc/net/tcp', 'r') as f:
+ for line in f:
+ if 0 <= string.find(line, port_hex):
+ port = None
+ break
+ except OSError:
+ # This isn't going so well. Just use the random port.
+ pass
+ except IOError:
+ pass
+ 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
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:
+ existing_api_host = os.environ.get('ARVADOS_TEST_API_HOST', my_api_host)
+ if existing_api_host and pid_file_ok:
try:
+ os.environ['ARVADOS_API_HOST'] = existing_api_host
reset()
return
except:
pass
+ # Before trying to start up our own server, call stop() to avoid
+ # "Phusion Passenger Standalone is already running on PID 12345".
+ # We want to kill it if it's our own _or_ it's some stale
+ # left-over server. But if it's been deliberately provided to us
+ # by a parent process, we don't want to force-kill it. That'll
+ # just wreck things for the next test suite that tries to use it.
+ stop(force=('ARVADOS_TEST_API_HOST' not in os.environ))
+
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
'-days', '3650',
'-subj', '/CN=0.0.0.0'])
- port = random.randint(20000, 40000)
+ port = find_available_port()
env = os.environ.copy()
env['RAILS_ENV'] = 'test'
env['ARVADOS_WEBSOCKETS'] = 'yes'
env=env)
if not leave_running_atexit:
- atexit.register(kill_server_pid, pid_file, passenger=True)
+ atexit.register(kill_server_pid, pid_file, passenger_root=api_src_dir)
match = re.search(r'Accessible via: https://(.*?)/', start_msg)
if not match:
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().
+ """
+ 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)})
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:
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),
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(