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