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