Merge branch 'master' into 4951-request-vm
[arvados.git] / sdk / python / tests / run_test_server.py
index ab1c7ffd1a270803e419e98557c0abfd8b40f2e9..5fee9eb0ec193d0cd4c3496480bf8b103727ea80 100644 (file)
@@ -9,6 +9,7 @@ import random
 import re
 import shutil
 import signal
 import re
 import shutil
 import signal
+import socket
 import subprocess
 import string
 import sys
 import subprocess
 import string
 import sys
@@ -94,28 +95,22 @@ def kill_server_pid(pidfile, wait=10, passenger_root=False):
         pass
 
 def find_available_port():
         pass
 
 def find_available_port():
-    """Return a port number that is not in use right now.
+    """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.
     """
 
     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
+
+    sock = socket.socket()
+    sock.bind(('0.0.0.0', 0))
+    port = sock.getsockname()[1]
+    sock.close()
     return port
 
 def run(leave_running_atexit=False):
     return port
 
 def run(leave_running_atexit=False):
@@ -138,28 +133,27 @@ def run(leave_running_atexit=False):
     # Delete cached discovery document.
     shutil.rmtree(arvados.http_cache('discovery'))
 
     # 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)
 
     existing_api_host = os.environ.get('ARVADOS_TEST_API_HOST', my_api_host)
     if existing_api_host and pid_file_ok:
     pid_file = os.path.join(SERVICES_SRC_DIR, 'api', SERVER_PID_PATH)
     pid_file_ok = find_server_pid(pid_file, 0)
 
     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
+        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".
 
     # 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))
+    # (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()
     api_src_dir = os.path.join(SERVICES_SRC_DIR, 'api')
 
     restore_cwd = os.getcwd()
     api_src_dir = os.path.join(SERVICES_SRC_DIR, 'api')
@@ -181,7 +175,8 @@ def run(leave_running_atexit=False):
             '-out', 'tmp/self-signed.pem',
             '-keyout', 'tmp/self-signed.key',
             '-days', '3650',
             '-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 = find_available_port()
     env = os.environ.copy()
 
     port = find_available_port()
     env = os.environ.copy()
@@ -222,6 +217,9 @@ def reset():
 
     This resets the ARVADOS_TEST_API_HOST provided by a parent process
     if any, otherwise the server started by run().
 
     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')
     """
     existing_api_host = os.environ.get('ARVADOS_TEST_API_HOST', my_api_host)
     token = auth_token('admin')
@@ -231,6 +229,9 @@ def reset():
         'https://{}/database/reset'.format(existing_api_host),
         'POST',
         headers={'Authorization': 'OAuth2 {}'.format(token)})
         '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.
 
 def stop(force=False):
     """Stop the API server, if one is running.