11308: Merge branch 'master' into 11308-python3
[arvados.git] / sdk / python / tests / run_test_server.py
1 from __future__ import print_function
2 from __future__ import division
3 from builtins import str
4 from builtins import range
5 import argparse
6 import atexit
7 import errno
8 import glob
9 import httplib2
10 import os
11 import pipes
12 import random
13 import re
14 import shutil
15 import signal
16 import socket
17 import string
18 import subprocess
19 import sys
20 import tempfile
21 import time
22 import unittest
23 import yaml
24
25 MY_DIRNAME = os.path.dirname(os.path.realpath(__file__))
26 if __name__ == '__main__' and os.path.exists(
27       os.path.join(MY_DIRNAME, '..', 'arvados', '__init__.py')):
28     # We're being launched to support another test suite.
29     # Add the Python SDK source to the library path.
30     sys.path.insert(1, os.path.dirname(MY_DIRNAME))
31
32 import arvados
33 import arvados.config
34
35 ARVADOS_DIR = os.path.realpath(os.path.join(MY_DIRNAME, '../../..'))
36 SERVICES_SRC_DIR = os.path.join(ARVADOS_DIR, 'services')
37 if 'GOPATH' in os.environ:
38     # Add all GOPATH bin dirs to PATH -- but insert them after the
39     # ruby gems bin dir, to ensure "bundle" runs the Ruby bundler
40     # command, not the golang.org/x/tools/cmd/bundle command.
41     gopaths = os.environ['GOPATH'].split(':')
42     addbins = [os.path.join(path, 'bin') for path in gopaths]
43     newbins = []
44     for path in os.environ['PATH'].split(':'):
45         newbins.append(path)
46         if os.path.exists(os.path.join(path, 'bundle')):
47             newbins += addbins
48             addbins = []
49     newbins += addbins
50     os.environ['PATH'] = ':'.join(newbins)
51
52 TEST_TMPDIR = os.path.join(ARVADOS_DIR, 'tmp')
53 if not os.path.exists(TEST_TMPDIR):
54     os.mkdir(TEST_TMPDIR)
55
56 my_api_host = None
57 _cached_config = {}
58 _cached_db_config = {}
59
60 def find_server_pid(PID_PATH, wait=10):
61     now = time.time()
62     timeout = now + wait
63     good_pid = False
64     while (not good_pid) and (now <= timeout):
65         time.sleep(0.2)
66         try:
67             with open(PID_PATH, 'r') as f:
68                 server_pid = int(f.read())
69             good_pid = (os.kill(server_pid, 0) is None)
70         except EnvironmentError:
71             good_pid = False
72         now = time.time()
73
74     if not good_pid:
75         return None
76
77     return server_pid
78
79 def kill_server_pid(pidfile, wait=10, passenger_root=False):
80     # Must re-import modules in order to work during atexit
81     import os
82     import signal
83     import subprocess
84     import time
85
86     now = time.time()
87     startTERM = now
88     deadline = now + wait
89
90     if passenger_root:
91         # First try to shut down nicely
92         restore_cwd = os.getcwd()
93         os.chdir(passenger_root)
94         subprocess.call([
95             'bundle', 'exec', 'passenger', 'stop', '--pid-file', pidfile])
96         os.chdir(restore_cwd)
97         # Use up to half of the +wait+ period waiting for "passenger
98         # stop" to work. If the process hasn't exited by then, start
99         # sending TERM signals.
100         startTERM += wait//2
101
102     server_pid = None
103     while now <= deadline and server_pid is None:
104         try:
105             with open(pidfile, 'r') as f:
106                 server_pid = int(f.read())
107         except IOError:
108             # No pidfile = nothing to kill.
109             return
110         except ValueError as error:
111             # Pidfile exists, but we can't parse it. Perhaps the
112             # server has created the file but hasn't written its PID
113             # yet?
114             print("Parse error reading pidfile {}: {}".format(pidfile, error),
115                   file=sys.stderr)
116             time.sleep(0.1)
117             now = time.time()
118
119     while now <= deadline:
120         try:
121             exited, _ = os.waitpid(server_pid, os.WNOHANG)
122             if exited > 0:
123                 _remove_pidfile(pidfile)
124                 return
125         except OSError:
126             # already exited, or isn't our child process
127             pass
128         try:
129             if now >= startTERM:
130                 os.kill(server_pid, signal.SIGTERM)
131                 print("Sent SIGTERM to {} ({})".format(server_pid, pidfile),
132                       file=sys.stderr)
133         except OSError as error:
134             if error.errno == errno.ESRCH:
135                 # Thrown by os.getpgid() or os.kill() if the process
136                 # does not exist, i.e., our work here is done.
137                 _remove_pidfile(pidfile)
138                 return
139             raise
140         time.sleep(0.1)
141         now = time.time()
142
143     print("Server PID {} ({}) did not exit, giving up after {}s".
144           format(server_pid, pidfile, wait),
145           file=sys.stderr)
146
147 def _remove_pidfile(pidfile):
148     try:
149         os.unlink(pidfile)
150     except:
151         if os.path.lexists(pidfile):
152             raise
153
154 def find_available_port():
155     """Return an IPv4 port number that is not in use right now.
156
157     We assume whoever needs to use the returned port is able to reuse
158     a recently used port without waiting for TIME_WAIT (see
159     SO_REUSEADDR / SO_REUSEPORT).
160
161     Some opportunity for races here, but it's better than choosing
162     something at random and not checking at all. If all of our servers
163     (hey Passenger) knew that listening on port 0 was a thing, the OS
164     would take care of the races, and this wouldn't be needed at all.
165     """
166
167     sock = socket.socket()
168     sock.bind(('0.0.0.0', 0))
169     port = sock.getsockname()[1]
170     sock.close()
171     return port
172
173 def _wait_until_port_listens(port, timeout=10):
174     """Wait for a process to start listening on the given port.
175
176     If nothing listens on the port within the specified timeout (given
177     in seconds), print a warning on stderr before returning.
178     """
179     try:
180         subprocess.check_output(['which', 'lsof'])
181     except subprocess.CalledProcessError:
182         print("WARNING: No `lsof` -- cannot wait for port to listen. "+
183               "Sleeping 0.5 and hoping for the best.",
184               file=sys.stderr)
185         time.sleep(0.5)
186         return
187     deadline = time.time() + timeout
188     while time.time() < deadline:
189         try:
190             subprocess.check_output(
191                 ['lsof', '-t', '-i', 'tcp:'+str(port)])
192         except subprocess.CalledProcessError:
193             time.sleep(0.1)
194             continue
195         return
196     print(
197         "WARNING: Nothing is listening on port {} (waited {} seconds).".
198         format(port, timeout),
199         file=sys.stderr)
200
201 def _fifo2stderr(label):
202     """Create a fifo, and copy it to stderr, prepending label to each line.
203
204     Return value is the path to the new FIFO.
205
206     +label+ should contain only alphanumerics: it is also used as part
207     of the FIFO filename.
208     """
209     fifo = os.path.join(TEST_TMPDIR, label+'.fifo')
210     try:
211         os.remove(fifo)
212     except OSError as error:
213         if error.errno != errno.ENOENT:
214             raise
215     os.mkfifo(fifo, 0o700)
216     subprocess.Popen(
217         ['stdbuf', '-i0', '-oL', '-eL', 'sed', '-e', 's/^/['+label+'] /', fifo],
218         stdout=sys.stderr)
219     return fifo
220
221 def run(leave_running_atexit=False):
222     """Ensure an API server is running, and ARVADOS_API_* env vars have
223     admin credentials for it.
224
225     If ARVADOS_TEST_API_HOST is set, a parent process has started a
226     test server for us to use: we just need to reset() it using the
227     admin token fixture.
228
229     If a previous call to run() started a new server process, and it
230     is still running, we just need to reset() it to fixture state and
231     return.
232
233     If neither of those options work out, we'll really start a new
234     server.
235     """
236     global my_api_host
237
238     # Delete cached discovery documents.
239     #
240     # This will clear cached docs that belong to other processes (like
241     # concurrent test suites) even if they're still running. They should
242     # be able to tolerate that.
243     for fn in glob.glob(os.path.join(
244             str(arvados.http_cache('discovery')),
245             '*,arvados,v1,rest,*')):
246         os.unlink(fn)
247
248     pid_file = _pidfile('api')
249     pid_file_ok = find_server_pid(pid_file, 0)
250
251     existing_api_host = os.environ.get('ARVADOS_TEST_API_HOST', my_api_host)
252     if existing_api_host and pid_file_ok:
253         if existing_api_host == my_api_host:
254             try:
255                 return reset()
256             except:
257                 # Fall through to shutdown-and-start case.
258                 pass
259         else:
260             # Server was provided by parent. Can't recover if it's
261             # unresettable.
262             return reset()
263
264     # Before trying to start up our own server, call stop() to avoid
265     # "Phusion Passenger Standalone is already running on PID 12345".
266     # (If we've gotten this far, ARVADOS_TEST_API_HOST isn't set, so
267     # we know the server is ours to kill.)
268     stop(force=True)
269
270     restore_cwd = os.getcwd()
271     api_src_dir = os.path.join(SERVICES_SRC_DIR, 'api')
272     os.chdir(api_src_dir)
273
274     # Either we haven't started a server of our own yet, or it has
275     # died, or we have lost our credentials, or something else is
276     # preventing us from calling reset(). Start a new one.
277
278     if not os.path.exists('tmp'):
279         os.makedirs('tmp')
280
281     if not os.path.exists('tmp/api'):
282         os.makedirs('tmp/api')
283
284     if not os.path.exists('tmp/logs'):
285         os.makedirs('tmp/logs')
286
287     if not os.path.exists('tmp/self-signed.pem'):
288         # We assume here that either passenger reports its listening
289         # address as https:/0.0.0.0:port/. If it reports "127.0.0.1"
290         # then the certificate won't match the host and reset() will
291         # fail certificate verification. If it reports "localhost",
292         # clients (notably Python SDK's websocket client) might
293         # resolve localhost as ::1 and then fail to connect.
294         subprocess.check_call([
295             'openssl', 'req', '-new', '-x509', '-nodes',
296             '-out', 'tmp/self-signed.pem',
297             '-keyout', 'tmp/self-signed.key',
298             '-days', '3650',
299             '-subj', '/CN=0.0.0.0'],
300         stdout=sys.stderr)
301
302     # Install the git repository fixtures.
303     gitdir = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'git')
304     gittarball = os.path.join(SERVICES_SRC_DIR, 'api', 'test', 'test.git.tar')
305     if not os.path.isdir(gitdir):
306         os.makedirs(gitdir)
307     subprocess.check_output(['tar', '-xC', gitdir, '-f', gittarball])
308
309     # The nginx proxy isn't listening here yet, but we need to choose
310     # the wss:// port now so we can write the API server config file.
311     wss_port = find_available_port()
312     _setport('wss', wss_port)
313
314     port = find_available_port()
315     env = os.environ.copy()
316     env['RAILS_ENV'] = 'test'
317     env['ARVADOS_TEST_WSS_PORT'] = str(wss_port)
318     env.pop('ARVADOS_WEBSOCKETS', None)
319     env.pop('ARVADOS_TEST_API_HOST', None)
320     env.pop('ARVADOS_API_HOST', None)
321     env.pop('ARVADOS_API_HOST_INSECURE', None)
322     env.pop('ARVADOS_API_TOKEN', None)
323     start_msg = subprocess.check_output(
324         ['bundle', 'exec',
325          'passenger', 'start', '-d', '-p{}'.format(port),
326          '--pid-file', pid_file,
327          '--log-file', os.path.join(os.getcwd(), 'log/test.log'),
328          '--ssl',
329          '--ssl-certificate', 'tmp/self-signed.pem',
330          '--ssl-certificate-key', 'tmp/self-signed.key'],
331         env=env)
332
333     if not leave_running_atexit:
334         atexit.register(kill_server_pid, pid_file, passenger_root=api_src_dir)
335
336     match = re.search(r'Accessible via: https://(.*?)/', start_msg)
337     if not match:
338         raise Exception(
339             "Passenger did not report endpoint: {}".format(start_msg))
340     my_api_host = match.group(1)
341     os.environ['ARVADOS_API_HOST'] = my_api_host
342
343     # Make sure the server has written its pid file and started
344     # listening on its TCP port
345     find_server_pid(pid_file)
346     _wait_until_port_listens(port)
347
348     reset()
349     os.chdir(restore_cwd)
350
351 def reset():
352     """Reset the test server to fixture state.
353
354     This resets the ARVADOS_TEST_API_HOST provided by a parent process
355     if any, otherwise the server started by run().
356
357     It also resets ARVADOS_* environment vars to point to the test
358     server with admin credentials.
359     """
360     existing_api_host = os.environ.get('ARVADOS_TEST_API_HOST', my_api_host)
361     token = auth_token('admin')
362     httpclient = httplib2.Http(ca_certs=os.path.join(
363         SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.pem'))
364     httpclient.request(
365         'https://{}/database/reset'.format(existing_api_host),
366         'POST',
367         headers={'Authorization': 'OAuth2 {}'.format(token)})
368     os.environ['ARVADOS_API_HOST_INSECURE'] = 'true'
369     os.environ['ARVADOS_API_HOST'] = existing_api_host
370     os.environ['ARVADOS_API_TOKEN'] = token
371
372 def stop(force=False):
373     """Stop the API server, if one is running.
374
375     If force==False, kill it only if we started it ourselves. (This
376     supports the use case where a Python test suite calls run(), but
377     run() just uses the ARVADOS_TEST_API_HOST provided by the parent
378     process, and the test suite cleans up after itself by calling
379     stop(). In this case the test server provided by the parent
380     process should be left alone.)
381
382     If force==True, kill it even if we didn't start it
383     ourselves. (This supports the use case in __main__, where "run"
384     and "stop" happen in different processes.)
385     """
386     global my_api_host
387     if force or my_api_host is not None:
388         kill_server_pid(_pidfile('api'))
389         my_api_host = None
390
391 def run_ws():
392     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
393         return
394     stop_ws()
395     port = find_available_port()
396     conf = os.path.join(TEST_TMPDIR, 'ws.yml')
397     with open(conf, 'w') as f:
398         f.write("""
399 Client:
400   APIHost: {}
401   Insecure: true
402 Listen: :{}
403 LogLevel: {}
404 Postgres:
405   host: {}
406   dbname: {}
407   user: {}
408   password: {}
409   sslmode: require
410         """.format(os.environ['ARVADOS_API_HOST'],
411                    port,
412                    ('info' if os.environ.get('ARVADOS_DEBUG', '') in ['','0'] else 'debug'),
413                    _dbconfig('host'),
414                    _dbconfig('database'),
415                    _dbconfig('username'),
416                    _dbconfig('password')))
417     logf = open(_fifo2stderr('ws'), 'w')
418     ws = subprocess.Popen(
419         ["ws", "-config", conf],
420         stdin=open('/dev/null'), stdout=logf, stderr=logf, close_fds=True)
421     with open(_pidfile('ws'), 'w') as f:
422         f.write(str(ws.pid))
423     _wait_until_port_listens(port)
424     _setport('ws', port)
425     return port
426
427 def stop_ws():
428     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
429         return
430     kill_server_pid(_pidfile('ws'))
431
432 def _start_keep(n, keep_args):
433     keep0 = tempfile.mkdtemp()
434     port = find_available_port()
435     keep_cmd = ["keepstore",
436                 "-volume={}".format(keep0),
437                 "-listen=:{}".format(port),
438                 "-pid="+_pidfile('keep{}'.format(n))]
439
440     for arg, val in keep_args.items():
441         keep_cmd.append("{}={}".format(arg, val))
442
443     logf = open(_fifo2stderr('keep{}'.format(n)), 'w')
444     kp0 = subprocess.Popen(
445         keep_cmd, stdin=open('/dev/null'), stdout=logf, stderr=logf, close_fds=True)
446
447     with open(_pidfile('keep{}'.format(n)), 'w') as f:
448         f.write(str(kp0.pid))
449
450     with open("{}/keep{}.volume".format(TEST_TMPDIR, n), 'w') as f:
451         f.write(keep0)
452
453     _wait_until_port_listens(port)
454
455     return port
456
457 def run_keep(blob_signing_key=None, enforce_permissions=False, num_servers=2):
458     stop_keep(num_servers)
459
460     keep_args = {}
461     if not blob_signing_key:
462         blob_signing_key = 'zfhgfenhffzltr9dixws36j1yhksjoll2grmku38mi7yxd66h5j4q9w4jzanezacp8s6q0ro3hxakfye02152hncy6zml2ed0uc'
463     with open(os.path.join(TEST_TMPDIR, "keep.blob_signing_key"), "w") as f:
464         keep_args['-blob-signing-key-file'] = f.name
465         f.write(blob_signing_key)
466     keep_args['-enforce-permissions'] = str(enforce_permissions).lower()
467     with open(os.path.join(TEST_TMPDIR, "keep.data-manager-token-file"), "w") as f:
468         keep_args['-data-manager-token-file'] = f.name
469         f.write(auth_token('data_manager'))
470     keep_args['-never-delete'] = 'false'
471
472     api = arvados.api(
473         version='v1',
474         host=os.environ['ARVADOS_API_HOST'],
475         token=os.environ['ARVADOS_API_TOKEN'],
476         insecure=True)
477
478     for d in api.keep_services().list(filters=[['service_type','=','disk']]).execute()['items']:
479         api.keep_services().delete(uuid=d['uuid']).execute()
480     for d in api.keep_disks().list().execute()['items']:
481         api.keep_disks().delete(uuid=d['uuid']).execute()
482
483     for d in range(0, num_servers):
484         port = _start_keep(d, keep_args)
485         svc = api.keep_services().create(body={'keep_service': {
486             'uuid': 'zzzzz-bi6l4-keepdisk{:07d}'.format(d),
487             'service_host': 'localhost',
488             'service_port': port,
489             'service_type': 'disk',
490             'service_ssl_flag': False,
491         }}).execute()
492         api.keep_disks().create(body={
493             'keep_disk': {'keep_service_uuid': svc['uuid'] }
494         }).execute()
495
496     # If keepproxy is running, send SIGHUP to make it discover the new
497     # keepstore services.
498     proxypidfile = _pidfile('keepproxy')
499     if os.path.exists(proxypidfile):
500         try:
501             os.kill(int(open(proxypidfile).read()), signal.SIGHUP)
502         except OSError:
503             os.remove(proxypidfile)
504
505 def _stop_keep(n):
506     kill_server_pid(_pidfile('keep{}'.format(n)))
507     if os.path.exists("{}/keep{}.volume".format(TEST_TMPDIR, n)):
508         with open("{}/keep{}.volume".format(TEST_TMPDIR, n), 'r') as r:
509             shutil.rmtree(r.read(), True)
510         os.unlink("{}/keep{}.volume".format(TEST_TMPDIR, n))
511     if os.path.exists(os.path.join(TEST_TMPDIR, "keep.blob_signing_key")):
512         os.remove(os.path.join(TEST_TMPDIR, "keep.blob_signing_key"))
513
514 def stop_keep(num_servers=2):
515     for n in range(0, num_servers):
516         _stop_keep(n)
517
518 def run_keep_proxy():
519     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
520         return
521     stop_keep_proxy()
522
523     port = find_available_port()
524     env = os.environ.copy()
525     env['ARVADOS_API_TOKEN'] = auth_token('anonymous')
526     logf = open(_fifo2stderr('keepproxy'), 'w')
527     kp = subprocess.Popen(
528         ['keepproxy',
529          '-pid='+_pidfile('keepproxy'),
530          '-listen=:{}'.format(port)],
531         env=env, stdin=open('/dev/null'), stdout=logf, stderr=logf, close_fds=True)
532
533     api = arvados.api(
534         version='v1',
535         host=os.environ['ARVADOS_API_HOST'],
536         token=auth_token('admin'),
537         insecure=True)
538     for d in api.keep_services().list(
539             filters=[['service_type','=','proxy']]).execute()['items']:
540         api.keep_services().delete(uuid=d['uuid']).execute()
541     api.keep_services().create(body={'keep_service': {
542         'service_host': 'localhost',
543         'service_port': port,
544         'service_type': 'proxy',
545         'service_ssl_flag': False,
546     }}).execute()
547     os.environ["ARVADOS_KEEP_SERVICES"] = "http://localhost:{}".format(port)
548     _setport('keepproxy', port)
549     _wait_until_port_listens(port)
550
551 def stop_keep_proxy():
552     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
553         return
554     kill_server_pid(_pidfile('keepproxy'))
555
556 def run_arv_git_httpd():
557     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
558         return
559     stop_arv_git_httpd()
560
561     gitdir = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'git')
562     gitport = find_available_port()
563     env = os.environ.copy()
564     env.pop('ARVADOS_API_TOKEN', None)
565     logf = open(_fifo2stderr('arv-git-httpd'), 'w')
566     agh = subprocess.Popen(
567         ['arv-git-httpd',
568          '-repo-root='+gitdir+'/test',
569          '-address=:'+str(gitport)],
570         env=env, stdin=open('/dev/null'), stdout=logf, stderr=logf)
571     with open(_pidfile('arv-git-httpd'), 'w') as f:
572         f.write(str(agh.pid))
573     _setport('arv-git-httpd', gitport)
574     _wait_until_port_listens(gitport)
575
576 def stop_arv_git_httpd():
577     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
578         return
579     kill_server_pid(_pidfile('arv-git-httpd'))
580
581 def run_keep_web():
582     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
583         return
584     stop_keep_web()
585
586     keepwebport = find_available_port()
587     env = os.environ.copy()
588     env['ARVADOS_API_TOKEN'] = auth_token('anonymous')
589     logf = open(_fifo2stderr('keep-web'), 'w')
590     keepweb = subprocess.Popen(
591         ['keep-web',
592          '-allow-anonymous',
593          '-attachment-only-host=download:'+str(keepwebport),
594          '-listen=:'+str(keepwebport)],
595         env=env, stdin=open('/dev/null'), stdout=logf, stderr=logf)
596     with open(_pidfile('keep-web'), 'w') as f:
597         f.write(str(keepweb.pid))
598     _setport('keep-web', keepwebport)
599     _wait_until_port_listens(keepwebport)
600
601 def stop_keep_web():
602     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
603         return
604     kill_server_pid(_pidfile('keep-web'))
605
606 def run_nginx():
607     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
608         return
609     stop_nginx()
610     nginxconf = {}
611     nginxconf['KEEPWEBPORT'] = _getport('keep-web')
612     nginxconf['KEEPWEBDLSSLPORT'] = find_available_port()
613     nginxconf['KEEPWEBSSLPORT'] = find_available_port()
614     nginxconf['KEEPPROXYPORT'] = _getport('keepproxy')
615     nginxconf['KEEPPROXYSSLPORT'] = find_available_port()
616     nginxconf['GITPORT'] = _getport('arv-git-httpd')
617     nginxconf['GITSSLPORT'] = find_available_port()
618     nginxconf['WSPORT'] = _getport('ws')
619     nginxconf['WSSPORT'] = _getport('wss')
620     nginxconf['SSLCERT'] = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.pem')
621     nginxconf['SSLKEY'] = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.key')
622     nginxconf['ACCESSLOG'] = _fifo2stderr('nginx_access_log')
623
624     conftemplatefile = os.path.join(MY_DIRNAME, 'nginx.conf')
625     conffile = os.path.join(TEST_TMPDIR, 'nginx.conf')
626     with open(conffile, 'w') as f:
627         f.write(re.sub(
628             r'{{([A-Z]+)}}',
629             lambda match: str(nginxconf.get(match.group(1))),
630             open(conftemplatefile).read()))
631
632     env = os.environ.copy()
633     env['PATH'] = env['PATH']+':/sbin:/usr/sbin:/usr/local/sbin'
634
635     nginx = subprocess.Popen(
636         ['nginx',
637          '-g', 'error_log stderr info;',
638          '-g', 'pid '+_pidfile('nginx')+';',
639          '-c', conffile],
640         env=env, stdin=open('/dev/null'), stdout=sys.stderr)
641     _setport('keep-web-dl-ssl', nginxconf['KEEPWEBDLSSLPORT'])
642     _setport('keep-web-ssl', nginxconf['KEEPWEBSSLPORT'])
643     _setport('keepproxy-ssl', nginxconf['KEEPPROXYSSLPORT'])
644     _setport('arv-git-httpd-ssl', nginxconf['GITSSLPORT'])
645
646 def stop_nginx():
647     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
648         return
649     kill_server_pid(_pidfile('nginx'))
650
651 def _pidfile(program):
652     return os.path.join(TEST_TMPDIR, program + '.pid')
653
654 def _portfile(program):
655     return os.path.join(TEST_TMPDIR, program + '.port')
656
657 def _setport(program, port):
658     with open(_portfile(program), 'w') as f:
659         f.write(str(port))
660
661 # Returns 9 if program is not up.
662 def _getport(program):
663     try:
664         return int(open(_portfile(program)).read())
665     except IOError:
666         return 9
667
668 def _dbconfig(key):
669     global _cached_db_config
670     if not _cached_db_config:
671         _cached_db_config = yaml.load(open(os.path.join(
672             SERVICES_SRC_DIR, 'api', 'config', 'database.yml')))
673     return _cached_db_config['test'][key]
674
675 def _apiconfig(key):
676     global _cached_config
677     if _cached_config:
678         return _cached_config[key]
679     def _load(f, required=True):
680         fullpath = os.path.join(SERVICES_SRC_DIR, 'api', 'config', f)
681         if not required and not os.path.exists(fullpath):
682             return {}
683         return yaml.load(fullpath)
684     cdefault = _load('application.default.yml')
685     csite = _load('application.yml', required=False)
686     _cached_config = {}
687     for section in [cdefault.get('common',{}), cdefault.get('test',{}),
688                     csite.get('common',{}), csite.get('test',{})]:
689         _cached_config.update(section)
690     return _cached_config[key]
691
692 def fixture(fix):
693     '''load a fixture yaml file'''
694     with open(os.path.join(SERVICES_SRC_DIR, 'api', "test", "fixtures",
695                            fix + ".yml")) as f:
696         yaml_file = f.read()
697         try:
698           trim_index = yaml_file.index("# Test Helper trims the rest of the file")
699           yaml_file = yaml_file[0:trim_index]
700         except ValueError:
701           pass
702         return yaml.load(yaml_file)
703
704 def auth_token(token_name):
705     return fixture("api_client_authorizations")[token_name]["api_token"]
706
707 def authorize_with(token_name):
708     '''token_name is the symbolic name of the token from the api_client_authorizations fixture'''
709     arvados.config.settings()["ARVADOS_API_TOKEN"] = auth_token(token_name)
710     arvados.config.settings()["ARVADOS_API_HOST"] = os.environ.get("ARVADOS_API_HOST")
711     arvados.config.settings()["ARVADOS_API_HOST_INSECURE"] = "true"
712
713 class TestCaseWithServers(unittest.TestCase):
714     """TestCase to start and stop supporting Arvados servers.
715
716     Define any of MAIN_SERVER, KEEP_SERVER, and/or KEEP_PROXY_SERVER
717     class variables as a dictionary of keyword arguments.  If you do,
718     setUpClass will start the corresponding servers by passing these
719     keyword arguments to the run, run_keep, and/or run_keep_server
720     functions, respectively.  It will also set Arvados environment
721     variables to point to these servers appropriately.  If you don't
722     run a Keep or Keep proxy server, setUpClass will set up a
723     temporary directory for Keep local storage, and set it as
724     KEEP_LOCAL_STORE.
725
726     tearDownClass will stop any servers started, and restore the
727     original environment.
728     """
729     MAIN_SERVER = None
730     WS_SERVER = None
731     KEEP_SERVER = None
732     KEEP_PROXY_SERVER = None
733     KEEP_WEB_SERVER = None
734
735     @staticmethod
736     def _restore_dict(src, dest):
737         for key in list(dest.keys()):
738             if key not in src:
739                 del dest[key]
740         dest.update(src)
741
742     @classmethod
743     def setUpClass(cls):
744         cls._orig_environ = os.environ.copy()
745         cls._orig_config = arvados.config.settings().copy()
746         cls._cleanup_funcs = []
747         os.environ.pop('ARVADOS_KEEP_SERVICES', None)
748         os.environ.pop('ARVADOS_EXTERNAL_CLIENT', None)
749         for server_kwargs, start_func, stop_func in (
750                 (cls.MAIN_SERVER, run, reset),
751                 (cls.WS_SERVER, run_ws, stop_ws),
752                 (cls.KEEP_SERVER, run_keep, stop_keep),
753                 (cls.KEEP_PROXY_SERVER, run_keep_proxy, stop_keep_proxy),
754                 (cls.KEEP_WEB_SERVER, run_keep_web, stop_keep_web)):
755             if server_kwargs is not None:
756                 start_func(**server_kwargs)
757                 cls._cleanup_funcs.append(stop_func)
758         if (cls.KEEP_SERVER is None) and (cls.KEEP_PROXY_SERVER is None):
759             cls.local_store = tempfile.mkdtemp()
760             os.environ['KEEP_LOCAL_STORE'] = cls.local_store
761             cls._cleanup_funcs.append(
762                 lambda: shutil.rmtree(cls.local_store, ignore_errors=True))
763         else:
764             os.environ.pop('KEEP_LOCAL_STORE', None)
765         arvados.config.initialize()
766
767     @classmethod
768     def tearDownClass(cls):
769         for clean_func in cls._cleanup_funcs:
770             clean_func()
771         cls._restore_dict(cls._orig_environ, os.environ)
772         cls._restore_dict(cls._orig_config, arvados.config.settings())
773
774
775 if __name__ == "__main__":
776     actions = [
777         'start', 'stop',
778         'start_ws', 'stop_ws',
779         'start_keep', 'stop_keep',
780         'start_keep_proxy', 'stop_keep_proxy',
781         'start_keep-web', 'stop_keep-web',
782         'start_arv-git-httpd', 'stop_arv-git-httpd',
783         'start_nginx', 'stop_nginx',
784     ]
785     parser = argparse.ArgumentParser()
786     parser.add_argument('action', type=str, help="one of {}".format(actions))
787     parser.add_argument('--auth', type=str, metavar='FIXTURE_NAME', help='Print authorization info for given api_client_authorizations fixture')
788     parser.add_argument('--num-keep-servers', metavar='int', type=int, default=2, help="Number of keep servers desired")
789     parser.add_argument('--keep-enforce-permissions', action="store_true", help="Enforce keep permissions")
790
791     args = parser.parse_args()
792
793     if args.action not in actions:
794         print("Unrecognized action '{}'. Actions are: {}.".
795               format(args.action, actions),
796               file=sys.stderr)
797         sys.exit(1)
798     if args.action == 'start':
799         stop(force=('ARVADOS_TEST_API_HOST' not in os.environ))
800         run(leave_running_atexit=True)
801         host = os.environ['ARVADOS_API_HOST']
802         if args.auth is not None:
803             token = auth_token(args.auth)
804             print("export ARVADOS_API_TOKEN={}".format(pipes.quote(token)))
805             print("export ARVADOS_API_HOST={}".format(pipes.quote(host)))
806             print("export ARVADOS_API_HOST_INSECURE=true")
807         else:
808             print(host)
809     elif args.action == 'stop':
810         stop(force=('ARVADOS_TEST_API_HOST' not in os.environ))
811     elif args.action == 'start_ws':
812         run_ws()
813     elif args.action == 'stop_ws':
814         stop_ws()
815     elif args.action == 'start_keep':
816         run_keep(enforce_permissions=args.keep_enforce_permissions, num_servers=args.num_keep_servers)
817     elif args.action == 'stop_keep':
818         stop_keep(num_servers=args.num_keep_servers)
819     elif args.action == 'start_keep_proxy':
820         run_keep_proxy()
821     elif args.action == 'stop_keep_proxy':
822         stop_keep_proxy()
823     elif args.action == 'start_arv-git-httpd':
824         run_arv_git_httpd()
825     elif args.action == 'stop_arv-git-httpd':
826         stop_arv_git_httpd()
827     elif args.action == 'start_keep-web':
828         run_keep_web()
829     elif args.action == 'stop_keep-web':
830         stop_keep_web()
831     elif args.action == 'start_nginx':
832         run_nginx()
833     elif args.action == 'stop_nginx':
834         stop_nginx()
835     else:
836         raise Exception("action recognized but not implemented!?")