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