Merge branch '8567-docker-migrator' refs #8567
[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(
243             str(arvados.http_cache('discovery')),
244             '*,arvados,v1,rest,*')):
245         os.unlink(fn)
246
247     pid_file = _pidfile('api')
248     pid_file_ok = find_server_pid(pid_file, 0)
249
250     existing_api_host = os.environ.get('ARVADOS_TEST_API_HOST', my_api_host)
251     if existing_api_host and pid_file_ok:
252         if existing_api_host == my_api_host:
253             try:
254                 return reset()
255             except:
256                 # Fall through to shutdown-and-start case.
257                 pass
258         else:
259             # Server was provided by parent. Can't recover if it's
260             # unresettable.
261             return reset()
262
263     # Before trying to start up our own server, call stop() to avoid
264     # "Phusion Passenger Standalone is already running on PID 12345".
265     # (If we've gotten this far, ARVADOS_TEST_API_HOST isn't set, so
266     # we know the server is ours to kill.)
267     stop(force=True)
268
269     restore_cwd = os.getcwd()
270     api_src_dir = os.path.join(SERVICES_SRC_DIR, 'api')
271     os.chdir(api_src_dir)
272
273     # Either we haven't started a server of our own yet, or it has
274     # died, or we have lost our credentials, or something else is
275     # preventing us from calling reset(). Start a new one.
276
277     if not os.path.exists('tmp'):
278         os.makedirs('tmp')
279
280     if not os.path.exists('tmp/api'):
281         os.makedirs('tmp/api')
282
283     if not os.path.exists('tmp/logs'):
284         os.makedirs('tmp/logs')
285
286     if not os.path.exists('tmp/self-signed.pem'):
287         # We assume here that either passenger reports its listening
288         # address as https:/0.0.0.0:port/. If it reports "127.0.0.1"
289         # then the certificate won't match the host and reset() will
290         # fail certificate verification. If it reports "localhost",
291         # clients (notably Python SDK's websocket client) might
292         # resolve localhost as ::1 and then fail to connect.
293         subprocess.check_call([
294             'openssl', 'req', '-new', '-x509', '-nodes',
295             '-out', 'tmp/self-signed.pem',
296             '-keyout', 'tmp/self-signed.key',
297             '-days', '3650',
298             '-subj', '/CN=0.0.0.0'],
299         stdout=sys.stderr)
300
301     # Install the git repository fixtures.
302     gitdir = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'git')
303     gittarball = os.path.join(SERVICES_SRC_DIR, 'api', 'test', 'test.git.tar')
304     if not os.path.isdir(gitdir):
305         os.makedirs(gitdir)
306     subprocess.check_output(['tar', '-xC', gitdir, '-f', gittarball])
307
308     # The nginx proxy isn't listening here yet, but we need to choose
309     # the wss:// port now so we can write the API server config file.
310     wss_port = find_available_port()
311     _setport('wss', wss_port)
312
313     port = find_available_port()
314     env = os.environ.copy()
315     env['RAILS_ENV'] = 'test'
316     env['ARVADOS_TEST_WSS_PORT'] = str(wss_port)
317     if env.get('ARVADOS_TEST_EXPERIMENTAL_WS'):
318         env.pop('ARVADOS_WEBSOCKETS', None)
319     else:
320         env['ARVADOS_WEBSOCKETS'] = 'yes'
321     env.pop('ARVADOS_TEST_API_HOST', None)
322     env.pop('ARVADOS_API_HOST', None)
323     env.pop('ARVADOS_API_HOST_INSECURE', None)
324     env.pop('ARVADOS_API_TOKEN', None)
325     start_msg = subprocess.check_output(
326         ['bundle', 'exec',
327          'passenger', 'start', '-d', '-p{}'.format(port),
328          '--pid-file', pid_file,
329          '--log-file', os.path.join(os.getcwd(), 'log/test.log'),
330          '--ssl',
331          '--ssl-certificate', 'tmp/self-signed.pem',
332          '--ssl-certificate-key', 'tmp/self-signed.key'],
333         env=env)
334
335     if not leave_running_atexit:
336         atexit.register(kill_server_pid, pid_file, passenger_root=api_src_dir)
337
338     match = re.search(r'Accessible via: https://(.*?)/', start_msg)
339     if not match:
340         raise Exception(
341             "Passenger did not report endpoint: {}".format(start_msg))
342     my_api_host = match.group(1)
343     os.environ['ARVADOS_API_HOST'] = my_api_host
344
345     # Make sure the server has written its pid file and started
346     # listening on its TCP port
347     find_server_pid(pid_file)
348     _wait_until_port_listens(port)
349
350     reset()
351     os.chdir(restore_cwd)
352
353 def reset():
354     """Reset the test server to fixture state.
355
356     This resets the ARVADOS_TEST_API_HOST provided by a parent process
357     if any, otherwise the server started by run().
358
359     It also resets ARVADOS_* environment vars to point to the test
360     server with admin credentials.
361     """
362     existing_api_host = os.environ.get('ARVADOS_TEST_API_HOST', my_api_host)
363     token = auth_token('admin')
364     httpclient = httplib2.Http(ca_certs=os.path.join(
365         SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.pem'))
366     httpclient.request(
367         'https://{}/database/reset'.format(existing_api_host),
368         'POST',
369         headers={'Authorization': 'OAuth2 {}'.format(token)})
370     os.environ['ARVADOS_API_HOST_INSECURE'] = 'true'
371     os.environ['ARVADOS_API_HOST'] = existing_api_host
372     os.environ['ARVADOS_API_TOKEN'] = token
373
374 def stop(force=False):
375     """Stop the API server, if one is running.
376
377     If force==False, kill it only if we started it ourselves. (This
378     supports the use case where a Python test suite calls run(), but
379     run() just uses the ARVADOS_TEST_API_HOST provided by the parent
380     process, and the test suite cleans up after itself by calling
381     stop(). In this case the test server provided by the parent
382     process should be left alone.)
383
384     If force==True, kill it even if we didn't start it
385     ourselves. (This supports the use case in __main__, where "run"
386     and "stop" happen in different processes.)
387     """
388     global my_api_host
389     if force or my_api_host is not None:
390         kill_server_pid(_pidfile('api'))
391         my_api_host = None
392
393 def run_ws():
394     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
395         return
396     stop_ws()
397     port = find_available_port()
398     conf = os.path.join(TEST_TMPDIR, 'ws.yml')
399     with open(conf, 'w') as f:
400         f.write("""
401 Client:
402   APIHost: {}
403   Insecure: true
404 Listen: :{}
405 LogLevel: {}
406 Postgres:
407   host: {}
408   dbname: {}
409   user: {}
410   password: {}
411   sslmode: require
412         """.format(os.environ['ARVADOS_API_HOST'],
413                    port,
414                    ('info' if os.environ.get('ARVADOS_DEBUG', '') in ['','0'] else 'debug'),
415                    _dbconfig('host'),
416                    _dbconfig('database'),
417                    _dbconfig('username'),
418                    _dbconfig('password')))
419     logf = open(_fifo2stderr('ws'), 'w')
420     ws = subprocess.Popen(
421         ["ws", "-config", conf],
422         stdin=open('/dev/null'), stdout=logf, stderr=logf, close_fds=True)
423     with open(_pidfile('ws'), 'w') as f:
424         f.write(str(ws.pid))
425     _wait_until_port_listens(port)
426     _setport('ws', port)
427     return port
428
429 def stop_ws():
430     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
431         return
432     kill_server_pid(_pidfile('ws'))
433
434 def _start_keep(n, keep_args):
435     keep0 = tempfile.mkdtemp()
436     port = find_available_port()
437     keep_cmd = ["keepstore",
438                 "-volume={}".format(keep0),
439                 "-listen=:{}".format(port),
440                 "-pid="+_pidfile('keep{}'.format(n))]
441
442     for arg, val in keep_args.iteritems():
443         keep_cmd.append("{}={}".format(arg, val))
444
445     logf = open(_fifo2stderr('keep{}'.format(n)), 'w')
446     kp0 = subprocess.Popen(
447         keep_cmd, stdin=open('/dev/null'), stdout=logf, stderr=logf, close_fds=True)
448
449     with open(_pidfile('keep{}'.format(n)), 'w') as f:
450         f.write(str(kp0.pid))
451
452     with open("{}/keep{}.volume".format(TEST_TMPDIR, n), 'w') as f:
453         f.write(keep0)
454
455     _wait_until_port_listens(port)
456
457     return port
458
459 def run_keep(blob_signing_key=None, enforce_permissions=False, num_servers=2):
460     stop_keep(num_servers)
461
462     keep_args = {}
463     if not blob_signing_key:
464         blob_signing_key = 'zfhgfenhffzltr9dixws36j1yhksjoll2grmku38mi7yxd66h5j4q9w4jzanezacp8s6q0ro3hxakfye02152hncy6zml2ed0uc'
465     with open(os.path.join(TEST_TMPDIR, "keep.blob_signing_key"), "w") as f:
466         keep_args['-blob-signing-key-file'] = f.name
467         f.write(blob_signing_key)
468     keep_args['-enforce-permissions'] = str(enforce_permissions).lower()
469     with open(os.path.join(TEST_TMPDIR, "keep.data-manager-token-file"), "w") as f:
470         keep_args['-data-manager-token-file'] = f.name
471         f.write(auth_token('data_manager'))
472     keep_args['-never-delete'] = 'false'
473
474     api = arvados.api(
475         version='v1',
476         host=os.environ['ARVADOS_API_HOST'],
477         token=os.environ['ARVADOS_API_TOKEN'],
478         insecure=True)
479
480     for d in api.keep_services().list(filters=[['service_type','=','disk']]).execute()['items']:
481         api.keep_services().delete(uuid=d['uuid']).execute()
482     for d in api.keep_disks().list().execute()['items']:
483         api.keep_disks().delete(uuid=d['uuid']).execute()
484
485     for d in range(0, num_servers):
486         port = _start_keep(d, keep_args)
487         svc = api.keep_services().create(body={'keep_service': {
488             'uuid': 'zzzzz-bi6l4-keepdisk{:07d}'.format(d),
489             'service_host': 'localhost',
490             'service_port': port,
491             'service_type': 'disk',
492             'service_ssl_flag': False,
493         }}).execute()
494         api.keep_disks().create(body={
495             'keep_disk': {'keep_service_uuid': svc['uuid'] }
496         }).execute()
497
498     # If keepproxy is running, send SIGHUP to make it discover the new
499     # keepstore services.
500     proxypidfile = _pidfile('keepproxy')
501     if os.path.exists(proxypidfile):
502         try:
503             os.kill(int(open(proxypidfile).read()), signal.SIGHUP)
504         except OSError:
505             os.remove(proxypidfile)
506
507 def _stop_keep(n):
508     kill_server_pid(_pidfile('keep{}'.format(n)))
509     if os.path.exists("{}/keep{}.volume".format(TEST_TMPDIR, n)):
510         with open("{}/keep{}.volume".format(TEST_TMPDIR, n), 'r') as r:
511             shutil.rmtree(r.read(), True)
512         os.unlink("{}/keep{}.volume".format(TEST_TMPDIR, n))
513     if os.path.exists(os.path.join(TEST_TMPDIR, "keep.blob_signing_key")):
514         os.remove(os.path.join(TEST_TMPDIR, "keep.blob_signing_key"))
515
516 def stop_keep(num_servers=2):
517     for n in range(0, num_servers):
518         _stop_keep(n)
519
520 def run_keep_proxy():
521     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
522         return
523     stop_keep_proxy()
524
525     port = find_available_port()
526     env = os.environ.copy()
527     env['ARVADOS_API_TOKEN'] = auth_token('anonymous')
528     logf = open(_fifo2stderr('keepproxy'), 'w')
529     kp = subprocess.Popen(
530         ['keepproxy',
531          '-pid='+_pidfile('keepproxy'),
532          '-listen=:{}'.format(port)],
533         env=env, stdin=open('/dev/null'), stdout=logf, stderr=logf, close_fds=True)
534
535     api = arvados.api(
536         version='v1',
537         host=os.environ['ARVADOS_API_HOST'],
538         token=auth_token('admin'),
539         insecure=True)
540     for d in api.keep_services().list(
541             filters=[['service_type','=','proxy']]).execute()['items']:
542         api.keep_services().delete(uuid=d['uuid']).execute()
543     api.keep_services().create(body={'keep_service': {
544         'service_host': 'localhost',
545         'service_port': port,
546         'service_type': 'proxy',
547         'service_ssl_flag': False,
548     }}).execute()
549     os.environ["ARVADOS_KEEP_SERVICES"] = "http://localhost:{}".format(port)
550     _setport('keepproxy', port)
551     _wait_until_port_listens(port)
552
553 def stop_keep_proxy():
554     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
555         return
556     kill_server_pid(_pidfile('keepproxy'))
557
558 def run_arv_git_httpd():
559     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
560         return
561     stop_arv_git_httpd()
562
563     gitdir = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'git')
564     gitport = find_available_port()
565     env = os.environ.copy()
566     env.pop('ARVADOS_API_TOKEN', None)
567     logf = open(_fifo2stderr('arv-git-httpd'), 'w')
568     agh = subprocess.Popen(
569         ['arv-git-httpd',
570          '-repo-root='+gitdir+'/test',
571          '-address=:'+str(gitport)],
572         env=env, stdin=open('/dev/null'), stdout=logf, stderr=logf)
573     with open(_pidfile('arv-git-httpd'), 'w') as f:
574         f.write(str(agh.pid))
575     _setport('arv-git-httpd', gitport)
576     _wait_until_port_listens(gitport)
577
578 def stop_arv_git_httpd():
579     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
580         return
581     kill_server_pid(_pidfile('arv-git-httpd'))
582
583 def run_keep_web():
584     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
585         return
586     stop_keep_web()
587
588     keepwebport = find_available_port()
589     env = os.environ.copy()
590     env['ARVADOS_API_TOKEN'] = auth_token('anonymous')
591     logf = open(_fifo2stderr('keep-web'), 'w')
592     keepweb = subprocess.Popen(
593         ['keep-web',
594          '-allow-anonymous',
595          '-attachment-only-host=download:'+str(keepwebport),
596          '-listen=:'+str(keepwebport)],
597         env=env, stdin=open('/dev/null'), stdout=logf, stderr=logf)
598     with open(_pidfile('keep-web'), 'w') as f:
599         f.write(str(keepweb.pid))
600     _setport('keep-web', keepwebport)
601     _wait_until_port_listens(keepwebport)
602
603 def stop_keep_web():
604     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
605         return
606     kill_server_pid(_pidfile('keep-web'))
607
608 def run_nginx():
609     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
610         return
611     stop_nginx()
612     nginxconf = {}
613     nginxconf['KEEPWEBPORT'] = _getport('keep-web')
614     nginxconf['KEEPWEBDLSSLPORT'] = find_available_port()
615     nginxconf['KEEPWEBSSLPORT'] = find_available_port()
616     nginxconf['KEEPPROXYPORT'] = _getport('keepproxy')
617     nginxconf['KEEPPROXYSSLPORT'] = find_available_port()
618     nginxconf['GITPORT'] = _getport('arv-git-httpd')
619     nginxconf['GITSSLPORT'] = find_available_port()
620     nginxconf['WSPORT'] = _getport('ws')
621     nginxconf['WSSPORT'] = _getport('wss')
622     nginxconf['SSLCERT'] = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.pem')
623     nginxconf['SSLKEY'] = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.key')
624     nginxconf['ACCESSLOG'] = _fifo2stderr('nginx_access_log')
625
626     conftemplatefile = os.path.join(MY_DIRNAME, 'nginx.conf')
627     conffile = os.path.join(TEST_TMPDIR, 'nginx.conf')
628     with open(conffile, 'w') as f:
629         f.write(re.sub(
630             r'{{([A-Z]+)}}',
631             lambda match: str(nginxconf.get(match.group(1))),
632             open(conftemplatefile).read()))
633
634     env = os.environ.copy()
635     env['PATH'] = env['PATH']+':/sbin:/usr/sbin:/usr/local/sbin'
636
637     nginx = subprocess.Popen(
638         ['nginx',
639          '-g', 'error_log stderr info;',
640          '-g', 'pid '+_pidfile('nginx')+';',
641          '-c', conffile],
642         env=env, stdin=open('/dev/null'), stdout=sys.stderr)
643     _setport('keep-web-dl-ssl', nginxconf['KEEPWEBDLSSLPORT'])
644     _setport('keep-web-ssl', nginxconf['KEEPWEBSSLPORT'])
645     _setport('keepproxy-ssl', nginxconf['KEEPPROXYSSLPORT'])
646     _setport('arv-git-httpd-ssl', nginxconf['GITSSLPORT'])
647
648 def stop_nginx():
649     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
650         return
651     kill_server_pid(_pidfile('nginx'))
652
653 def _pidfile(program):
654     return os.path.join(TEST_TMPDIR, program + '.pid')
655
656 def _portfile(program):
657     return os.path.join(TEST_TMPDIR, program + '.port')
658
659 def _setport(program, port):
660     with open(_portfile(program), 'w') as f:
661         f.write(str(port))
662
663 # Returns 9 if program is not up.
664 def _getport(program):
665     try:
666         return int(open(_portfile(program)).read())
667     except IOError:
668         return 9
669
670 def _dbconfig(key):
671     global _cached_db_config
672     if not _cached_db_config:
673         _cached_db_config = yaml.load(open(os.path.join(
674             SERVICES_SRC_DIR, 'api', 'config', 'database.yml')))
675     return _cached_db_config['test'][key]
676
677 def _apiconfig(key):
678     global _cached_config
679     if _cached_config:
680         return _cached_config[key]
681     def _load(f, required=True):
682         fullpath = os.path.join(SERVICES_SRC_DIR, 'api', 'config', f)
683         if not required and not os.path.exists(fullpath):
684             return {}
685         return yaml.load(fullpath)
686     cdefault = _load('application.default.yml')
687     csite = _load('application.yml', required=False)
688     _cached_config = {}
689     for section in [cdefault.get('common',{}), cdefault.get('test',{}),
690                     csite.get('common',{}), csite.get('test',{})]:
691         _cached_config.update(section)
692     return _cached_config[key]
693
694 def fixture(fix):
695     '''load a fixture yaml file'''
696     with open(os.path.join(SERVICES_SRC_DIR, 'api', "test", "fixtures",
697                            fix + ".yml")) as f:
698         yaml_file = f.read()
699         try:
700           trim_index = yaml_file.index("# Test Helper trims the rest of the file")
701           yaml_file = yaml_file[0:trim_index]
702         except ValueError:
703           pass
704         return yaml.load(yaml_file)
705
706 def auth_token(token_name):
707     return fixture("api_client_authorizations")[token_name]["api_token"]
708
709 def authorize_with(token_name):
710     '''token_name is the symbolic name of the token from the api_client_authorizations fixture'''
711     arvados.config.settings()["ARVADOS_API_TOKEN"] = auth_token(token_name)
712     arvados.config.settings()["ARVADOS_API_HOST"] = os.environ.get("ARVADOS_API_HOST")
713     arvados.config.settings()["ARVADOS_API_HOST_INSECURE"] = "true"
714
715 class TestCaseWithServers(unittest.TestCase):
716     """TestCase to start and stop supporting Arvados servers.
717
718     Define any of MAIN_SERVER, KEEP_SERVER, and/or KEEP_PROXY_SERVER
719     class variables as a dictionary of keyword arguments.  If you do,
720     setUpClass will start the corresponding servers by passing these
721     keyword arguments to the run, run_keep, and/or run_keep_server
722     functions, respectively.  It will also set Arvados environment
723     variables to point to these servers appropriately.  If you don't
724     run a Keep or Keep proxy server, setUpClass will set up a
725     temporary directory for Keep local storage, and set it as
726     KEEP_LOCAL_STORE.
727
728     tearDownClass will stop any servers started, and restore the
729     original environment.
730     """
731     MAIN_SERVER = None
732     WS_SERVER = None
733     KEEP_SERVER = None
734     KEEP_PROXY_SERVER = None
735     KEEP_WEB_SERVER = None
736
737     @staticmethod
738     def _restore_dict(src, dest):
739         for key in dest.keys():
740             if key not in src:
741                 del dest[key]
742         dest.update(src)
743
744     @classmethod
745     def setUpClass(cls):
746         cls._orig_environ = os.environ.copy()
747         cls._orig_config = arvados.config.settings().copy()
748         cls._cleanup_funcs = []
749         os.environ.pop('ARVADOS_KEEP_SERVICES', None)
750         os.environ.pop('ARVADOS_EXTERNAL_CLIENT', None)
751         for server_kwargs, start_func, stop_func in (
752                 (cls.MAIN_SERVER, run, reset),
753                 (cls.WS_SERVER, run_ws, stop_ws),
754                 (cls.KEEP_SERVER, run_keep, stop_keep),
755                 (cls.KEEP_PROXY_SERVER, run_keep_proxy, stop_keep_proxy),
756                 (cls.KEEP_WEB_SERVER, run_keep_web, stop_keep_web)):
757             if server_kwargs is not None:
758                 start_func(**server_kwargs)
759                 cls._cleanup_funcs.append(stop_func)
760         if (cls.KEEP_SERVER is None) and (cls.KEEP_PROXY_SERVER is None):
761             cls.local_store = tempfile.mkdtemp()
762             os.environ['KEEP_LOCAL_STORE'] = cls.local_store
763             cls._cleanup_funcs.append(
764                 lambda: shutil.rmtree(cls.local_store, ignore_errors=True))
765         else:
766             os.environ.pop('KEEP_LOCAL_STORE', None)
767         arvados.config.initialize()
768
769     @classmethod
770     def tearDownClass(cls):
771         for clean_func in cls._cleanup_funcs:
772             clean_func()
773         cls._restore_dict(cls._orig_environ, os.environ)
774         cls._restore_dict(cls._orig_config, arvados.config.settings())
775
776
777 if __name__ == "__main__":
778     actions = [
779         'start', 'stop',
780         'start_ws', 'stop_ws',
781         'start_keep', 'stop_keep',
782         'start_keep_proxy', 'stop_keep_proxy',
783         'start_keep-web', 'stop_keep-web',
784         'start_arv-git-httpd', 'stop_arv-git-httpd',
785         'start_nginx', 'stop_nginx',
786     ]
787     parser = argparse.ArgumentParser()
788     parser.add_argument('action', type=str, help="one of {}".format(actions))
789     parser.add_argument('--auth', type=str, metavar='FIXTURE_NAME', help='Print authorization info for given api_client_authorizations fixture')
790     parser.add_argument('--num-keep-servers', metavar='int', type=int, default=2, help="Number of keep servers desired")
791     parser.add_argument('--keep-enforce-permissions', action="store_true", help="Enforce keep permissions")
792
793     args = parser.parse_args()
794
795     if args.action not in actions:
796         print("Unrecognized action '{}'. Actions are: {}.".
797               format(args.action, actions),
798               file=sys.stderr)
799         sys.exit(1)
800     if args.action == 'start':
801         stop(force=('ARVADOS_TEST_API_HOST' not in os.environ))
802         run(leave_running_atexit=True)
803         host = os.environ['ARVADOS_API_HOST']
804         if args.auth is not None:
805             token = auth_token(args.auth)
806             print("export ARVADOS_API_TOKEN={}".format(pipes.quote(token)))
807             print("export ARVADOS_API_HOST={}".format(pipes.quote(host)))
808             print("export ARVADOS_API_HOST_INSECURE=true")
809         else:
810             print(host)
811     elif args.action == 'stop':
812         stop(force=('ARVADOS_TEST_API_HOST' not in os.environ))
813     elif args.action == 'start_ws':
814         run_ws()
815     elif args.action == 'stop_ws':
816         stop_ws()
817     elif args.action == 'start_keep':
818         run_keep(enforce_permissions=args.keep_enforce_permissions, num_servers=args.num_keep_servers)
819     elif args.action == 'stop_keep':
820         stop_keep(num_servers=args.num_keep_servers)
821     elif args.action == 'start_keep_proxy':
822         run_keep_proxy()
823     elif args.action == 'stop_keep_proxy':
824         stop_keep_proxy()
825     elif args.action == 'start_arv-git-httpd':
826         run_arv_git_httpd()
827     elif args.action == 'stop_arv-git-httpd':
828         stop_arv_git_httpd()
829     elif args.action == 'start_keep-web':
830         run_keep_web()
831     elif args.action == 'stop_keep-web':
832         stop_keep_web()
833     elif args.action == 'start_nginx':
834         run_nginx()
835     elif args.action == 'stop_nginx':
836         stop_nginx()
837     else:
838         raise Exception("action recognized but not implemented!?")