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