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