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