8460: Stop existing nginx server, if any, before starting new.
[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     env.pop('ARVADOS_WEBSOCKETS', None)
298     env.pop('ARVADOS_TEST_API_HOST', None)
299     env.pop('ARVADOS_API_HOST', None)
300     env.pop('ARVADOS_API_HOST_INSECURE', None)
301     env.pop('ARVADOS_API_TOKEN', None)
302     start_msg = subprocess.check_output(
303         ['bundle', 'exec',
304          'passenger', 'start', '-d', '-p{}'.format(port),
305          '--pid-file', pid_file,
306          '--log-file', os.path.join(os.getcwd(), 'log/test.log'),
307          '--ssl',
308          '--ssl-certificate', 'tmp/self-signed.pem',
309          '--ssl-certificate-key', 'tmp/self-signed.key'],
310         env=env)
311
312     if not leave_running_atexit:
313         atexit.register(kill_server_pid, pid_file, passenger_root=api_src_dir)
314
315     match = re.search(r'Accessible via: https://(.*?)/', start_msg)
316     if not match:
317         raise Exception(
318             "Passenger did not report endpoint: {}".format(start_msg))
319     my_api_host = match.group(1)
320     os.environ['ARVADOS_API_HOST'] = my_api_host
321
322     # Make sure the server has written its pid file and started
323     # listening on its TCP port
324     find_server_pid(pid_file)
325     _wait_until_port_listens(port)
326
327     reset()
328     os.chdir(restore_cwd)
329
330 def reset():
331     """Reset the test server to fixture state.
332
333     This resets the ARVADOS_TEST_API_HOST provided by a parent process
334     if any, otherwise the server started by run().
335
336     It also resets ARVADOS_* environment vars to point to the test
337     server with admin credentials.
338     """
339     existing_api_host = os.environ.get('ARVADOS_TEST_API_HOST', my_api_host)
340     token = auth_token('admin')
341     httpclient = httplib2.Http(ca_certs=os.path.join(
342         SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.pem'))
343     httpclient.request(
344         'https://{}/database/reset'.format(existing_api_host),
345         'POST',
346         headers={'Authorization': 'OAuth2 {}'.format(token)})
347     os.environ['ARVADOS_API_HOST_INSECURE'] = 'true'
348     os.environ['ARVADOS_API_HOST'] = existing_api_host
349     os.environ['ARVADOS_API_TOKEN'] = token
350
351 def stop(force=False):
352     """Stop the API server, if one is running.
353
354     If force==False, kill it only if we started it ourselves. (This
355     supports the use case where a Python test suite calls run(), but
356     run() just uses the ARVADOS_TEST_API_HOST provided by the parent
357     process, and the test suite cleans up after itself by calling
358     stop(). In this case the test server provided by the parent
359     process should be left alone.)
360
361     If force==True, kill it even if we didn't start it
362     ourselves. (This supports the use case in __main__, where "run"
363     and "stop" happen in different processes.)
364     """
365     global my_api_host
366     if force or my_api_host is not None:
367         kill_server_pid(_pidfile('api'))
368         my_api_host = None
369
370 def run_ws():
371     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
372         return
373     stop_ws()
374     port = find_available_port()
375     conf = os.path.join(TEST_TMPDIR, 'ws.yml')
376     with open(conf, 'w') as f:
377         f.write("""
378 Client:
379   APIHost: {}
380   Insecure: true
381 Listen: :{}
382 LogLevel: {}
383 Postgres:
384   host: {}
385   dbname: {}
386   user: {}
387   password: {}
388   sslmode: require
389         """.format(os.environ['ARVADOS_API_HOST'],
390                    port,
391                    ('info' if os.environ.get('ARVADOS_DEBUG', '') in ['','0'] else 'debug'),
392                    _dbconfig('host'),
393                    _dbconfig('database'),
394                    _dbconfig('username'),
395                    _dbconfig('password')))
396     logf = open(_fifo2stderr('ws'), 'w')
397     ws = subprocess.Popen(
398         ["ws", "-config", conf],
399         stdin=open('/dev/null'), stdout=logf, stderr=logf, close_fds=True)
400     with open(_pidfile('ws'), 'w') as f:
401         f.write(str(ws.pid))
402     _wait_until_port_listens(port)
403     _setport('ws', port)
404     return port
405
406 def stop_ws():
407     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
408         return
409     kill_server_pid(_pidfile('ws'))
410
411 def _start_keep(n, keep_args):
412     keep0 = tempfile.mkdtemp()
413     port = find_available_port()
414     keep_cmd = ["keepstore",
415                 "-volume={}".format(keep0),
416                 "-listen=:{}".format(port),
417                 "-pid="+_pidfile('keep{}'.format(n))]
418
419     for arg, val in keep_args.iteritems():
420         keep_cmd.append("{}={}".format(arg, val))
421
422     logf = open(_fifo2stderr('keep{}'.format(n)), 'w')
423     kp0 = subprocess.Popen(
424         keep_cmd, stdin=open('/dev/null'), stdout=logf, stderr=logf, close_fds=True)
425
426     with open(_pidfile('keep{}'.format(n)), 'w') as f:
427         f.write(str(kp0.pid))
428
429     with open("{}/keep{}.volume".format(TEST_TMPDIR, n), 'w') as f:
430         f.write(keep0)
431
432     _wait_until_port_listens(port)
433
434     return port
435
436 def run_keep(blob_signing_key=None, enforce_permissions=False, num_servers=2):
437     stop_keep(num_servers)
438
439     keep_args = {}
440     if not blob_signing_key:
441         blob_signing_key = 'zfhgfenhffzltr9dixws36j1yhksjoll2grmku38mi7yxd66h5j4q9w4jzanezacp8s6q0ro3hxakfye02152hncy6zml2ed0uc'
442     with open(os.path.join(TEST_TMPDIR, "keep.blob_signing_key"), "w") as f:
443         keep_args['-blob-signing-key-file'] = f.name
444         f.write(blob_signing_key)
445     keep_args['-enforce-permissions'] = str(enforce_permissions).lower()
446     with open(os.path.join(TEST_TMPDIR, "keep.data-manager-token-file"), "w") as f:
447         keep_args['-data-manager-token-file'] = f.name
448         f.write(auth_token('data_manager'))
449     keep_args['-never-delete'] = 'false'
450
451     api = arvados.api(
452         version='v1',
453         host=os.environ['ARVADOS_API_HOST'],
454         token=os.environ['ARVADOS_API_TOKEN'],
455         insecure=True)
456
457     for d in api.keep_services().list(filters=[['service_type','=','disk']]).execute()['items']:
458         api.keep_services().delete(uuid=d['uuid']).execute()
459     for d in api.keep_disks().list().execute()['items']:
460         api.keep_disks().delete(uuid=d['uuid']).execute()
461
462     for d in range(0, num_servers):
463         port = _start_keep(d, keep_args)
464         svc = api.keep_services().create(body={'keep_service': {
465             'uuid': 'zzzzz-bi6l4-keepdisk{:07d}'.format(d),
466             'service_host': 'localhost',
467             'service_port': port,
468             'service_type': 'disk',
469             'service_ssl_flag': False,
470         }}).execute()
471         api.keep_disks().create(body={
472             'keep_disk': {'keep_service_uuid': svc['uuid'] }
473         }).execute()
474
475     # If keepproxy is running, send SIGHUP to make it discover the new
476     # keepstore services.
477     proxypidfile = _pidfile('keepproxy')
478     if os.path.exists(proxypidfile):
479         try:
480             os.kill(int(open(proxypidfile).read()), signal.SIGHUP)
481         except OSError:
482             os.remove(proxypidfile)
483
484 def _stop_keep(n):
485     kill_server_pid(_pidfile('keep{}'.format(n)))
486     if os.path.exists("{}/keep{}.volume".format(TEST_TMPDIR, n)):
487         with open("{}/keep{}.volume".format(TEST_TMPDIR, n), 'r') as r:
488             shutil.rmtree(r.read(), True)
489         os.unlink("{}/keep{}.volume".format(TEST_TMPDIR, n))
490     if os.path.exists(os.path.join(TEST_TMPDIR, "keep.blob_signing_key")):
491         os.remove(os.path.join(TEST_TMPDIR, "keep.blob_signing_key"))
492
493 def stop_keep(num_servers=2):
494     for n in range(0, num_servers):
495         _stop_keep(n)
496
497 def run_keep_proxy():
498     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
499         return
500     stop_keep_proxy()
501
502     port = find_available_port()
503     env = os.environ.copy()
504     env['ARVADOS_API_TOKEN'] = auth_token('anonymous')
505     logf = open(_fifo2stderr('keepproxy'), 'w')
506     kp = subprocess.Popen(
507         ['keepproxy',
508          '-pid='+_pidfile('keepproxy'),
509          '-listen=:{}'.format(port)],
510         env=env, stdin=open('/dev/null'), stdout=logf, stderr=logf, close_fds=True)
511
512     api = arvados.api(
513         version='v1',
514         host=os.environ['ARVADOS_API_HOST'],
515         token=auth_token('admin'),
516         insecure=True)
517     for d in api.keep_services().list(
518             filters=[['service_type','=','proxy']]).execute()['items']:
519         api.keep_services().delete(uuid=d['uuid']).execute()
520     api.keep_services().create(body={'keep_service': {
521         'service_host': 'localhost',
522         'service_port': port,
523         'service_type': 'proxy',
524         'service_ssl_flag': False,
525     }}).execute()
526     os.environ["ARVADOS_KEEP_SERVICES"] = "http://localhost:{}".format(port)
527     _setport('keepproxy', port)
528     _wait_until_port_listens(port)
529
530 def stop_keep_proxy():
531     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
532         return
533     kill_server_pid(_pidfile('keepproxy'))
534
535 def run_arv_git_httpd():
536     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
537         return
538     stop_arv_git_httpd()
539
540     gitdir = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'git')
541     gitport = find_available_port()
542     env = os.environ.copy()
543     env.pop('ARVADOS_API_TOKEN', None)
544     logf = open(_fifo2stderr('arv-git-httpd'), 'w')
545     agh = subprocess.Popen(
546         ['arv-git-httpd',
547          '-repo-root='+gitdir+'/test',
548          '-address=:'+str(gitport)],
549         env=env, stdin=open('/dev/null'), stdout=logf, stderr=logf)
550     with open(_pidfile('arv-git-httpd'), 'w') as f:
551         f.write(str(agh.pid))
552     _setport('arv-git-httpd', gitport)
553     _wait_until_port_listens(gitport)
554
555 def stop_arv_git_httpd():
556     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
557         return
558     kill_server_pid(_pidfile('arv-git-httpd'))
559
560 def run_keep_web():
561     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
562         return
563     stop_keep_web()
564
565     keepwebport = find_available_port()
566     env = os.environ.copy()
567     env['ARVADOS_API_TOKEN'] = auth_token('anonymous')
568     logf = open(_fifo2stderr('keep-web'), 'w')
569     keepweb = subprocess.Popen(
570         ['keep-web',
571          '-allow-anonymous',
572          '-attachment-only-host=download:'+str(keepwebport),
573          '-listen=:'+str(keepwebport)],
574         env=env, stdin=open('/dev/null'), stdout=logf, stderr=logf)
575     with open(_pidfile('keep-web'), 'w') as f:
576         f.write(str(keepweb.pid))
577     _setport('keep-web', keepwebport)
578     _wait_until_port_listens(keepwebport)
579
580 def stop_keep_web():
581     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
582         return
583     kill_server_pid(_pidfile('keep-web'))
584
585 def run_nginx():
586     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
587         return
588     stop_nginx()
589     nginxconf = {}
590     nginxconf['KEEPWEBPORT'] = _getport('keep-web')
591     nginxconf['KEEPWEBDLSSLPORT'] = find_available_port()
592     nginxconf['KEEPWEBSSLPORT'] = find_available_port()
593     nginxconf['KEEPPROXYPORT'] = _getport('keepproxy')
594     nginxconf['KEEPPROXYSSLPORT'] = find_available_port()
595     nginxconf['GITPORT'] = _getport('arv-git-httpd')
596     nginxconf['GITSSLPORT'] = find_available_port()
597     nginxconf['WSPORT'] = _getport('ws')
598     nginxconf['WSSPORT'] = _getport('wss')
599     nginxconf['SSLCERT'] = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.pem')
600     nginxconf['SSLKEY'] = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.key')
601     nginxconf['ACCESSLOG'] = _fifo2stderr('nginx_access_log')
602
603     conftemplatefile = os.path.join(MY_DIRNAME, 'nginx.conf')
604     conffile = os.path.join(TEST_TMPDIR, 'nginx.conf')
605     with open(conffile, 'w') as f:
606         f.write(re.sub(
607             r'{{([A-Z]+)}}',
608             lambda match: str(nginxconf.get(match.group(1))),
609             open(conftemplatefile).read()))
610
611     env = os.environ.copy()
612     env['PATH'] = env['PATH']+':/sbin:/usr/sbin:/usr/local/sbin'
613
614     nginx = subprocess.Popen(
615         ['nginx',
616          '-g', 'error_log stderr info;',
617          '-g', 'pid '+_pidfile('nginx')+';',
618          '-c', conffile],
619         env=env, stdin=open('/dev/null'), stdout=sys.stderr)
620     _setport('keep-web-dl-ssl', nginxconf['KEEPWEBDLSSLPORT'])
621     _setport('keep-web-ssl', nginxconf['KEEPWEBSSLPORT'])
622     _setport('keepproxy-ssl', nginxconf['KEEPPROXYSSLPORT'])
623     _setport('arv-git-httpd-ssl', nginxconf['GITSSLPORT'])
624
625 def stop_nginx():
626     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
627         return
628     kill_server_pid(_pidfile('nginx'))
629
630 def _pidfile(program):
631     return os.path.join(TEST_TMPDIR, program + '.pid')
632
633 def _portfile(program):
634     return os.path.join(TEST_TMPDIR, program + '.port')
635
636 def _setport(program, port):
637     with open(_portfile(program), 'w') as f:
638         f.write(str(port))
639
640 # Returns 9 if program is not up.
641 def _getport(program):
642     try:
643         return int(open(_portfile(program)).read())
644     except IOError:
645         return 9
646
647 def _dbconfig(key):
648     global _cached_db_config
649     if not _cached_db_config:
650         _cached_db_config = yaml.load(open(os.path.join(
651             SERVICES_SRC_DIR, 'api', 'config', 'database.yml')))
652     return _cached_db_config['test'][key]
653
654 def _apiconfig(key):
655     global _cached_config
656     if _cached_config:
657         return _cached_config[key]
658     def _load(f, required=True):
659         fullpath = os.path.join(SERVICES_SRC_DIR, 'api', 'config', f)
660         if not required and not os.path.exists(fullpath):
661             return {}
662         return yaml.load(fullpath)
663     cdefault = _load('application.default.yml')
664     csite = _load('application.yml', required=False)
665     _cached_config = {}
666     for section in [cdefault.get('common',{}), cdefault.get('test',{}),
667                     csite.get('common',{}), csite.get('test',{})]:
668         _cached_config.update(section)
669     return _cached_config[key]
670
671 def fixture(fix):
672     '''load a fixture yaml file'''
673     with open(os.path.join(SERVICES_SRC_DIR, 'api', "test", "fixtures",
674                            fix + ".yml")) as f:
675         yaml_file = f.read()
676         try:
677           trim_index = yaml_file.index("# Test Helper trims the rest of the file")
678           yaml_file = yaml_file[0:trim_index]
679         except ValueError:
680           pass
681         return yaml.load(yaml_file)
682
683 def auth_token(token_name):
684     return fixture("api_client_authorizations")[token_name]["api_token"]
685
686 def authorize_with(token_name):
687     '''token_name is the symbolic name of the token from the api_client_authorizations fixture'''
688     arvados.config.settings()["ARVADOS_API_TOKEN"] = auth_token(token_name)
689     arvados.config.settings()["ARVADOS_API_HOST"] = os.environ.get("ARVADOS_API_HOST")
690     arvados.config.settings()["ARVADOS_API_HOST_INSECURE"] = "true"
691
692 class TestCaseWithServers(unittest.TestCase):
693     """TestCase to start and stop supporting Arvados servers.
694
695     Define any of MAIN_SERVER, KEEP_SERVER, and/or KEEP_PROXY_SERVER
696     class variables as a dictionary of keyword arguments.  If you do,
697     setUpClass will start the corresponding servers by passing these
698     keyword arguments to the run, run_keep, and/or run_keep_server
699     functions, respectively.  It will also set Arvados environment
700     variables to point to these servers appropriately.  If you don't
701     run a Keep or Keep proxy server, setUpClass will set up a
702     temporary directory for Keep local storage, and set it as
703     KEEP_LOCAL_STORE.
704
705     tearDownClass will stop any servers started, and restore the
706     original environment.
707     """
708     MAIN_SERVER = None
709     WS_SERVER = None
710     KEEP_SERVER = None
711     KEEP_PROXY_SERVER = None
712     KEEP_WEB_SERVER = None
713
714     @staticmethod
715     def _restore_dict(src, dest):
716         for key in dest.keys():
717             if key not in src:
718                 del dest[key]
719         dest.update(src)
720
721     @classmethod
722     def setUpClass(cls):
723         cls._orig_environ = os.environ.copy()
724         cls._orig_config = arvados.config.settings().copy()
725         cls._cleanup_funcs = []
726         os.environ.pop('ARVADOS_KEEP_SERVICES', None)
727         os.environ.pop('ARVADOS_EXTERNAL_CLIENT', None)
728         for server_kwargs, start_func, stop_func in (
729                 (cls.MAIN_SERVER, run, reset),
730                 (cls.WS_SERVER, run_ws, stop_ws),
731                 (cls.KEEP_SERVER, run_keep, stop_keep),
732                 (cls.KEEP_PROXY_SERVER, run_keep_proxy, stop_keep_proxy),
733                 (cls.KEEP_WEB_SERVER, run_keep_web, stop_keep_web)):
734             if server_kwargs is not None:
735                 start_func(**server_kwargs)
736                 cls._cleanup_funcs.append(stop_func)
737         if (cls.KEEP_SERVER is None) and (cls.KEEP_PROXY_SERVER is None):
738             cls.local_store = tempfile.mkdtemp()
739             os.environ['KEEP_LOCAL_STORE'] = cls.local_store
740             cls._cleanup_funcs.append(
741                 lambda: shutil.rmtree(cls.local_store, ignore_errors=True))
742         else:
743             os.environ.pop('KEEP_LOCAL_STORE', None)
744         arvados.config.initialize()
745
746     @classmethod
747     def tearDownClass(cls):
748         for clean_func in cls._cleanup_funcs:
749             clean_func()
750         cls._restore_dict(cls._orig_environ, os.environ)
751         cls._restore_dict(cls._orig_config, arvados.config.settings())
752
753
754 if __name__ == "__main__":
755     actions = [
756         'start', 'stop',
757         'start_ws', 'stop_ws',
758         'start_keep', 'stop_keep',
759         'start_keep_proxy', 'stop_keep_proxy',
760         'start_keep-web', 'stop_keep-web',
761         'start_arv-git-httpd', 'stop_arv-git-httpd',
762         'start_nginx', 'stop_nginx',
763     ]
764     parser = argparse.ArgumentParser()
765     parser.add_argument('action', type=str, help="one of {}".format(actions))
766     parser.add_argument('--auth', type=str, metavar='FIXTURE_NAME', help='Print authorization info for given api_client_authorizations fixture')
767     parser.add_argument('--num-keep-servers', metavar='int', type=int, default=2, help="Number of keep servers desired")
768     parser.add_argument('--keep-enforce-permissions', action="store_true", help="Enforce keep permissions")
769
770     args = parser.parse_args()
771
772     if args.action not in actions:
773         print("Unrecognized action '{}'. Actions are: {}.".
774               format(args.action, actions),
775               file=sys.stderr)
776         sys.exit(1)
777     if args.action == 'start':
778         stop(force=('ARVADOS_TEST_API_HOST' not in os.environ))
779         run(leave_running_atexit=True)
780         host = os.environ['ARVADOS_API_HOST']
781         if args.auth is not None:
782             token = auth_token(args.auth)
783             print("export ARVADOS_API_TOKEN={}".format(pipes.quote(token)))
784             print("export ARVADOS_API_HOST={}".format(pipes.quote(host)))
785             print("export ARVADOS_API_HOST_INSECURE=true")
786         else:
787             print(host)
788     elif args.action == 'stop':
789         stop(force=('ARVADOS_TEST_API_HOST' not in os.environ))
790     elif args.action == 'start_ws':
791         run_ws()
792     elif args.action == 'stop_ws':
793         stop_ws()
794     elif args.action == 'start_keep':
795         run_keep(enforce_permissions=args.keep_enforce_permissions, num_servers=args.num_keep_servers)
796     elif args.action == 'stop_keep':
797         stop_keep(num_servers=args.num_keep_servers)
798     elif args.action == 'start_keep_proxy':
799         run_keep_proxy()
800     elif args.action == 'stop_keep_proxy':
801         stop_keep_proxy()
802     elif args.action == 'start_arv-git-httpd':
803         run_arv_git_httpd()
804     elif args.action == 'stop_arv-git-httpd':
805         stop_arv_git_httpd()
806     elif args.action == 'start_keep-web':
807         run_keep_web()
808     elif args.action == 'stop_keep-web':
809         stop_keep_web()
810     elif args.action == 'start_nginx':
811         run_nginx()
812     elif args.action == 'stop_nginx':
813         stop_nginx()
814     else:
815         raise Exception("action recognized but not implemented!?")