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