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