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