Merge branch 'master' into 14965-arv-mount-py-three
[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, warn=True):
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', 'netstat'])
185     except subprocess.CalledProcessError:
186         print("WARNING: No `netstat` -- 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         if re.search(r'\ntcp.*:'+str(port)+' .* LISTEN *\n', subprocess.check_output(['netstat', '-Wln']).decode()):
194             return True
195         time.sleep(0.1)
196     if warn:
197         print(
198             "WARNING: Nothing is listening on port {} (waited {} seconds).".
199             format(port, timeout),
200             file=sys.stderr)
201     return False
202
203 def _logfilename(label):
204     """Set up a labelled log file, and return a path to write logs to.
205
206     Normally, the returned path is {tmpdir}/{label}.log.
207
208     In debug mode, logs are also written to stderr, with [label]
209     prepended to each line. The returned path is a FIFO.
210
211     +label+ should contain only alphanumerics: it is also used as part
212     of the FIFO filename.
213
214     """
215     logfilename = os.path.join(TEST_TMPDIR, label+'.log')
216     if not os.environ.get('ARVADOS_DEBUG', ''):
217         return logfilename
218     fifo = os.path.join(TEST_TMPDIR, label+'.fifo')
219     try:
220         os.remove(fifo)
221     except OSError as error:
222         if error.errno != errno.ENOENT:
223             raise
224     os.mkfifo(fifo, 0o700)
225     stdbuf = ['stdbuf', '-i0', '-oL', '-eL']
226     # open(fifo, 'r') would block waiting for someone to open the fifo
227     # for writing, so we need a separate cat process to open it for
228     # us.
229     cat = subprocess.Popen(
230         stdbuf+['cat', fifo],
231         stdin=open('/dev/null'),
232         stdout=subprocess.PIPE)
233     tee = subprocess.Popen(
234         stdbuf+['tee', '-a', logfilename],
235         stdin=cat.stdout,
236         stdout=subprocess.PIPE)
237     subprocess.Popen(
238         stdbuf+['sed', '-e', 's/^/['+label+'] /'],
239         stdin=tee.stdout,
240         stdout=sys.stderr)
241     return fifo
242
243 def run(leave_running_atexit=False):
244     """Ensure an API server is running, and ARVADOS_API_* env vars have
245     admin credentials for it.
246
247     If ARVADOS_TEST_API_HOST is set, a parent process has started a
248     test server for us to use: we just need to reset() it using the
249     admin token fixture.
250
251     If a previous call to run() started a new server process, and it
252     is still running, we just need to reset() it to fixture state and
253     return.
254
255     If neither of those options work out, we'll really start a new
256     server.
257     """
258     global my_api_host
259
260     # Delete cached discovery documents.
261     #
262     # This will clear cached docs that belong to other processes (like
263     # concurrent test suites) even if they're still running. They should
264     # be able to tolerate that.
265     for fn in glob.glob(os.path.join(
266             str(arvados.http_cache('discovery')),
267             '*,arvados,v1,rest,*')):
268         os.unlink(fn)
269
270     pid_file = _pidfile('api')
271     pid_file_ok = find_server_pid(pid_file, 0)
272
273     existing_api_host = os.environ.get('ARVADOS_TEST_API_HOST', my_api_host)
274     if existing_api_host and pid_file_ok:
275         if existing_api_host == my_api_host:
276             try:
277                 return reset()
278             except:
279                 # Fall through to shutdown-and-start case.
280                 pass
281         else:
282             # Server was provided by parent. Can't recover if it's
283             # unresettable.
284             return reset()
285
286     # Before trying to start up our own server, call stop() to avoid
287     # "Phusion Passenger Standalone is already running on PID 12345".
288     # (If we've gotten this far, ARVADOS_TEST_API_HOST isn't set, so
289     # we know the server is ours to kill.)
290     stop(force=True)
291
292     restore_cwd = os.getcwd()
293     api_src_dir = os.path.join(SERVICES_SRC_DIR, 'api')
294     os.chdir(api_src_dir)
295
296     # Either we haven't started a server of our own yet, or it has
297     # died, or we have lost our credentials, or something else is
298     # preventing us from calling reset(). Start a new one.
299
300     if not os.path.exists('tmp'):
301         os.makedirs('tmp')
302
303     if not os.path.exists('tmp/api'):
304         os.makedirs('tmp/api')
305
306     if not os.path.exists('tmp/logs'):
307         os.makedirs('tmp/logs')
308
309     # Install the git repository fixtures.
310     gitdir = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'git')
311     gittarball = os.path.join(SERVICES_SRC_DIR, 'api', 'test', 'test.git.tar')
312     if not os.path.isdir(gitdir):
313         os.makedirs(gitdir)
314     subprocess.check_output(['tar', '-xC', gitdir, '-f', gittarball])
315
316     # The nginx proxy isn't listening here yet, but we need to choose
317     # the wss:// port now so we can write the API server config file.
318     wss_port = find_available_port()
319     _setport('wss', wss_port)
320
321     port = find_available_port()
322     env = os.environ.copy()
323     env['RAILS_ENV'] = 'test'
324     env['ARVADOS_TEST_WSS_PORT'] = str(wss_port)
325     env.pop('ARVADOS_WEBSOCKETS', None)
326     env.pop('ARVADOS_TEST_API_HOST', None)
327     env.pop('ARVADOS_API_HOST', None)
328     env.pop('ARVADOS_API_HOST_INSECURE', None)
329     env.pop('ARVADOS_API_TOKEN', None)
330     start_msg = subprocess.check_output(
331         ['bundle', 'exec',
332          'passenger', 'start', '-d', '-p{}'.format(port),
333          '--pid-file', pid_file,
334          '--log-file', os.path.join(os.getcwd(), 'log/test.log'),
335          '--ssl',
336          '--ssl-certificate', 'tmp/self-signed.pem',
337          '--ssl-certificate-key', 'tmp/self-signed.key'],
338         env=env)
339
340     if not leave_running_atexit:
341         atexit.register(kill_server_pid, pid_file, passenger_root=api_src_dir)
342
343     match = re.search(r'Accessible via: https://(.*?)/', start_msg)
344     if not match:
345         raise Exception(
346             "Passenger did not report endpoint: {}".format(start_msg))
347     my_api_host = match.group(1)
348     os.environ['ARVADOS_API_HOST'] = my_api_host
349
350     # Make sure the server has written its pid file and started
351     # listening on its TCP port
352     find_server_pid(pid_file)
353     _wait_until_port_listens(port)
354
355     reset()
356     os.chdir(restore_cwd)
357
358 def reset():
359     """Reset the test server to fixture state.
360
361     This resets the ARVADOS_TEST_API_HOST provided by a parent process
362     if any, otherwise the server started by run().
363
364     It also resets ARVADOS_* environment vars to point to the test
365     server with admin credentials.
366     """
367     existing_api_host = os.environ.get('ARVADOS_TEST_API_HOST', my_api_host)
368     token = auth_token('admin')
369     httpclient = httplib2.Http(ca_certs=os.path.join(
370         SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.pem'))
371     httpclient.request(
372         'https://{}/database/reset'.format(existing_api_host),
373         'POST',
374         headers={'Authorization': 'OAuth2 {}'.format(token)})
375     os.environ['ARVADOS_API_HOST_INSECURE'] = 'true'
376     os.environ['ARVADOS_API_TOKEN'] = token
377     if _wait_until_port_listens(_getport('controller-ssl'), timeout=0.5, warn=False):
378         os.environ['ARVADOS_API_HOST'] = '0.0.0.0:'+str(_getport('controller-ssl'))
379     else:
380         os.environ['ARVADOS_API_HOST'] = existing_api_host
381
382 def stop(force=False):
383     """Stop the API server, if one is running.
384
385     If force==False, kill it only if we started it ourselves. (This
386     supports the use case where a Python test suite calls run(), but
387     run() just uses the ARVADOS_TEST_API_HOST provided by the parent
388     process, and the test suite cleans up after itself by calling
389     stop(). In this case the test server provided by the parent
390     process should be left alone.)
391
392     If force==True, kill it even if we didn't start it
393     ourselves. (This supports the use case in __main__, where "run"
394     and "stop" happen in different processes.)
395     """
396     global my_api_host
397     if force or my_api_host is not None:
398         kill_server_pid(_pidfile('api'))
399         my_api_host = None
400
401 def run_controller():
402     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
403         return
404     stop_controller()
405     rails_api_port = int(string.split(os.environ.get('ARVADOS_TEST_API_HOST', my_api_host), ':')[-1])
406     port = find_available_port()
407     conf = os.path.join(TEST_TMPDIR, 'arvados.yml')
408     with open(conf, 'w') as f:
409         f.write("""
410 Clusters:
411   zzzzz:
412     EnableBetaController14287: {beta14287}
413     ManagementToken: e687950a23c3a9bceec28c6223a06c79
414     API:
415       RequestTimeout: 30s
416     Logging:
417         Level: "{loglevel}"
418     HTTPRequestTimeout: 30s
419     PostgreSQL:
420       ConnectionPool: 32
421       Connection:
422         host: {dbhost}
423         dbname: {dbname}
424         user: {dbuser}
425         password: {dbpass}
426     TLS:
427       Insecure: true
428     Services:
429       Controller:
430         InternalURLs:
431           "http://localhost:{controllerport}": {{}}
432       RailsAPI:
433         InternalURLs:
434           "https://localhost:{railsport}": {{}}
435         """.format(
436             beta14287=('true' if '14287' in os.environ.get('ARVADOS_EXPERIMENTAL', '') else 'false'),
437             loglevel=('info' if os.environ.get('ARVADOS_DEBUG', '') in ['','0'] else 'debug'),
438             dbhost=_dbconfig('host'),
439             dbname=_dbconfig('dbname'),
440             dbuser=_dbconfig('user'),
441             dbpass=_dbconfig('password'),
442             controllerport=port,
443             railsport=rails_api_port,
444         ))
445     logf = open(_logfilename('controller'), 'a')
446     controller = subprocess.Popen(
447         ["arvados-server", "controller", "-config", conf],
448         stdin=open('/dev/null'), stdout=logf, stderr=logf, close_fds=True)
449     with open(_pidfile('controller'), 'w') as f:
450         f.write(str(controller.pid))
451     _wait_until_port_listens(port)
452     _setport('controller', port)
453     return port
454
455 def stop_controller():
456     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
457         return
458     kill_server_pid(_pidfile('controller'))
459
460 def run_ws():
461     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
462         return
463     stop_ws()
464     port = find_available_port()
465     conf = os.path.join(TEST_TMPDIR, 'ws.yml')
466     with open(conf, 'w') as f:
467         f.write("""
468 Client:
469   APIHost: {}
470   Insecure: true
471 Listen: :{}
472 LogLevel: {}
473 Postgres:
474   host: {}
475   dbname: {}
476   user: {}
477   password: {}
478   sslmode: require
479         """.format(os.environ['ARVADOS_API_HOST'],
480                    port,
481                    ('info' if os.environ.get('ARVADOS_DEBUG', '') in ['','0'] else 'debug'),
482                    _dbconfig('host'),
483                    _dbconfig('dbname'),
484                    _dbconfig('user'),
485                    _dbconfig('password')))
486     logf = open(_logfilename('ws'), 'a')
487     ws = subprocess.Popen(
488         ["ws", "-config", conf],
489         stdin=open('/dev/null'), stdout=logf, stderr=logf, close_fds=True)
490     with open(_pidfile('ws'), 'w') as f:
491         f.write(str(ws.pid))
492     _wait_until_port_listens(port)
493     _setport('ws', port)
494     return port
495
496 def stop_ws():
497     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
498         return
499     kill_server_pid(_pidfile('ws'))
500
501 def _start_keep(n, keep_args):
502     keep0 = tempfile.mkdtemp()
503     port = find_available_port()
504     keep_cmd = ["keepstore",
505                 "-volume={}".format(keep0),
506                 "-listen=:{}".format(port),
507                 "-pid="+_pidfile('keep{}'.format(n))]
508
509     for arg, val in keep_args.items():
510         keep_cmd.append("{}={}".format(arg, val))
511
512     with open(_logfilename('keep{}'.format(n)), 'a') as logf:
513         with open('/dev/null') as _stdin:
514             kp0 = subprocess.Popen(
515                 keep_cmd, stdin=_stdin, stdout=logf, stderr=logf, close_fds=True)
516
517     with open(_pidfile('keep{}'.format(n)), 'w') as f:
518         f.write(str(kp0.pid))
519
520     with open("{}/keep{}.volume".format(TEST_TMPDIR, n), 'w') as f:
521         f.write(keep0)
522
523     _wait_until_port_listens(port)
524
525     return port
526
527 def run_keep(blob_signing_key=None, enforce_permissions=False, num_servers=2):
528     stop_keep(num_servers)
529
530     keep_args = {}
531     if not blob_signing_key:
532         blob_signing_key = 'zfhgfenhffzltr9dixws36j1yhksjoll2grmku38mi7yxd66h5j4q9w4jzanezacp8s6q0ro3hxakfye02152hncy6zml2ed0uc'
533     with open(os.path.join(TEST_TMPDIR, "keep.blob_signing_key"), "w") as f:
534         keep_args['-blob-signing-key-file'] = f.name
535         f.write(blob_signing_key)
536     keep_args['-enforce-permissions'] = str(enforce_permissions).lower()
537     with open(os.path.join(TEST_TMPDIR, "keep.data-manager-token-file"), "w") as f:
538         keep_args['-data-manager-token-file'] = f.name
539         f.write(auth_token('data_manager'))
540     keep_args['-never-delete'] = 'false'
541
542     api = arvados.api(
543         version='v1',
544         host=os.environ['ARVADOS_API_HOST'],
545         token=os.environ['ARVADOS_API_TOKEN'],
546         insecure=True)
547
548     for d in api.keep_services().list(filters=[['service_type','=','disk']]).execute()['items']:
549         api.keep_services().delete(uuid=d['uuid']).execute()
550     for d in api.keep_disks().list().execute()['items']:
551         api.keep_disks().delete(uuid=d['uuid']).execute()
552
553     for d in range(0, num_servers):
554         port = _start_keep(d, keep_args)
555         svc = api.keep_services().create(body={'keep_service': {
556             'uuid': 'zzzzz-bi6l4-keepdisk{:07d}'.format(d),
557             'service_host': 'localhost',
558             'service_port': port,
559             'service_type': 'disk',
560             'service_ssl_flag': False,
561         }}).execute()
562         api.keep_disks().create(body={
563             'keep_disk': {'keep_service_uuid': svc['uuid'] }
564         }).execute()
565
566     # If keepproxy and/or keep-web is running, send SIGHUP to make
567     # them discover the new keepstore services.
568     for svc in ('keepproxy', 'keep-web'):
569         pidfile = _pidfile('keepproxy')
570         if os.path.exists(pidfile):
571             try:
572                 with open(pidfile) as pid:
573                     os.kill(int(pid.read()), signal.SIGHUP)
574             except OSError:
575                 os.remove(pidfile)
576
577 def _stop_keep(n):
578     kill_server_pid(_pidfile('keep{}'.format(n)))
579     if os.path.exists("{}/keep{}.volume".format(TEST_TMPDIR, n)):
580         with open("{}/keep{}.volume".format(TEST_TMPDIR, n), 'r') as r:
581             shutil.rmtree(r.read(), True)
582         os.unlink("{}/keep{}.volume".format(TEST_TMPDIR, n))
583     if os.path.exists(os.path.join(TEST_TMPDIR, "keep.blob_signing_key")):
584         os.remove(os.path.join(TEST_TMPDIR, "keep.blob_signing_key"))
585
586 def stop_keep(num_servers=2):
587     for n in range(0, num_servers):
588         _stop_keep(n)
589
590 def run_keep_proxy():
591     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
592         os.environ["ARVADOS_KEEP_SERVICES"] = "http://localhost:{}".format(_getport('keepproxy'))
593         return
594     stop_keep_proxy()
595
596     port = find_available_port()
597     env = os.environ.copy()
598     env['ARVADOS_API_TOKEN'] = auth_token('anonymous')
599     logf = open(_logfilename('keepproxy'), 'a')
600     kp = subprocess.Popen(
601         ['keepproxy',
602          '-pid='+_pidfile('keepproxy'),
603          '-listen=:{}'.format(port)],
604         env=env, stdin=open('/dev/null'), stdout=logf, stderr=logf, close_fds=True)
605
606     api = arvados.api(
607         version='v1',
608         host=os.environ['ARVADOS_API_HOST'],
609         token=auth_token('admin'),
610         insecure=True)
611     for d in api.keep_services().list(
612             filters=[['service_type','=','proxy']]).execute()['items']:
613         api.keep_services().delete(uuid=d['uuid']).execute()
614     api.keep_services().create(body={'keep_service': {
615         'service_host': 'localhost',
616         'service_port': port,
617         'service_type': 'proxy',
618         'service_ssl_flag': False,
619     }}).execute()
620     os.environ["ARVADOS_KEEP_SERVICES"] = "http://localhost:{}".format(port)
621     _setport('keepproxy', port)
622     _wait_until_port_listens(port)
623
624 def stop_keep_proxy():
625     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
626         return
627     kill_server_pid(_pidfile('keepproxy'))
628
629 def run_arv_git_httpd():
630     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
631         return
632     stop_arv_git_httpd()
633
634     gitdir = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'git')
635     gitport = find_available_port()
636     env = os.environ.copy()
637     env.pop('ARVADOS_API_TOKEN', None)
638     logf = open(_logfilename('arv-git-httpd'), 'a')
639     agh = subprocess.Popen(
640         ['arv-git-httpd',
641          '-repo-root='+gitdir+'/test',
642          '-management-token=e687950a23c3a9bceec28c6223a06c79',
643          '-address=:'+str(gitport)],
644         env=env, stdin=open('/dev/null'), stdout=logf, stderr=logf)
645     with open(_pidfile('arv-git-httpd'), 'w') as f:
646         f.write(str(agh.pid))
647     _setport('arv-git-httpd', gitport)
648     _wait_until_port_listens(gitport)
649
650 def stop_arv_git_httpd():
651     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
652         return
653     kill_server_pid(_pidfile('arv-git-httpd'))
654
655 def run_keep_web():
656     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
657         return
658     stop_keep_web()
659
660     keepwebport = find_available_port()
661     env = os.environ.copy()
662     env['ARVADOS_API_TOKEN'] = auth_token('anonymous')
663     logf = open(_logfilename('keep-web'), 'a')
664     keepweb = subprocess.Popen(
665         ['keep-web',
666          '-allow-anonymous',
667          '-attachment-only-host=download',
668          '-management-token=e687950a23c3a9bceec28c6223a06c79',
669          '-listen=:'+str(keepwebport)],
670         env=env, stdin=open('/dev/null'), stdout=logf, stderr=logf)
671     with open(_pidfile('keep-web'), 'w') as f:
672         f.write(str(keepweb.pid))
673     _setport('keep-web', keepwebport)
674     _wait_until_port_listens(keepwebport)
675
676 def stop_keep_web():
677     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
678         return
679     kill_server_pid(_pidfile('keep-web'))
680
681 def run_nginx():
682     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
683         return
684     stop_nginx()
685     nginxconf = {}
686     nginxconf['CONTROLLERPORT'] = _getport('controller')
687     nginxconf['CONTROLLERSSLPORT'] = find_available_port()
688     nginxconf['KEEPWEBPORT'] = _getport('keep-web')
689     nginxconf['KEEPWEBDLSSLPORT'] = find_available_port()
690     nginxconf['KEEPWEBSSLPORT'] = find_available_port()
691     nginxconf['KEEPPROXYPORT'] = _getport('keepproxy')
692     nginxconf['KEEPPROXYSSLPORT'] = find_available_port()
693     nginxconf['GITPORT'] = _getport('arv-git-httpd')
694     nginxconf['GITSSLPORT'] = find_available_port()
695     nginxconf['WSPORT'] = _getport('ws')
696     nginxconf['WSSPORT'] = _getport('wss')
697     nginxconf['SSLCERT'] = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.pem')
698     nginxconf['SSLKEY'] = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.key')
699     nginxconf['ACCESSLOG'] = _logfilename('nginx_access')
700     nginxconf['ERRORLOG'] = _logfilename('nginx_error')
701     nginxconf['TMPDIR'] = TEST_TMPDIR
702
703     conftemplatefile = os.path.join(MY_DIRNAME, 'nginx.conf')
704     conffile = os.path.join(TEST_TMPDIR, 'nginx.conf')
705     with open(conffile, 'w') as f:
706         f.write(re.sub(
707             r'{{([A-Z]+)}}',
708             lambda match: str(nginxconf.get(match.group(1))),
709             open(conftemplatefile).read()))
710
711     env = os.environ.copy()
712     env['PATH'] = env['PATH']+':/sbin:/usr/sbin:/usr/local/sbin'
713
714     nginx = subprocess.Popen(
715         ['nginx',
716          '-g', 'error_log stderr info;',
717          '-g', 'pid '+_pidfile('nginx')+';',
718          '-c', conffile],
719         env=env, stdin=open('/dev/null'), stdout=sys.stderr)
720     _setport('controller-ssl', nginxconf['CONTROLLERSSLPORT'])
721     _setport('keep-web-dl-ssl', nginxconf['KEEPWEBDLSSLPORT'])
722     _setport('keep-web-ssl', nginxconf['KEEPWEBSSLPORT'])
723     _setport('keepproxy-ssl', nginxconf['KEEPPROXYSSLPORT'])
724     _setport('arv-git-httpd-ssl', nginxconf['GITSSLPORT'])
725
726 def stop_nginx():
727     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
728         return
729     kill_server_pid(_pidfile('nginx'))
730
731 def _pidfile(program):
732     return os.path.join(TEST_TMPDIR, program + '.pid')
733
734 def _portfile(program):
735     return os.path.join(TEST_TMPDIR, program + '.port')
736
737 def _setport(program, port):
738     with open(_portfile(program), 'w') as f:
739         f.write(str(port))
740
741 # Returns 9 if program is not up.
742 def _getport(program):
743     try:
744         with open(_portfile(program)) as prog:
745             return int(prog.read())
746     except IOError:
747         return 9
748
749 def _dbconfig(key):
750     global _cached_db_config
751     if not _cached_db_config:
752         if "ARVADOS_CONFIG" in os.environ:
753             _cached_db_config = list(yaml.safe_load(open(os.environ["ARVADOS_CONFIG"]))["Clusters"].values())[0]["PostgreSQL"]["Connection"]
754         else:
755             _cached_db_config = yaml.safe_load(open(os.path.join(
756                 SERVICES_SRC_DIR, 'api', 'config', 'database.yml')))["test"]
757             _cached_db_config["dbname"] = _cached_db_config["database"]
758             _cached_db_config["user"] = _cached_db_config["username"]
759     return _cached_db_config[key]
760
761 def _apiconfig(key):
762     global _cached_config
763     if _cached_config:
764         return _cached_config[key]
765     def _load(f, required=True):
766         fullpath = os.path.join(SERVICES_SRC_DIR, 'api', 'config', f)
767         if not required and not os.path.exists(fullpath):
768             return {}
769         return yaml.safe_load(fullpath)
770     cdefault = _load('application.default.yml')
771     csite = _load('application.yml', required=False)
772     _cached_config = {}
773     for section in [cdefault.get('common',{}), cdefault.get('test',{}),
774                     csite.get('common',{}), csite.get('test',{})]:
775         _cached_config.update(section)
776     return _cached_config[key]
777
778 def fixture(fix):
779     '''load a fixture yaml file'''
780     with open(os.path.join(SERVICES_SRC_DIR, 'api', "test", "fixtures",
781                            fix + ".yml")) as f:
782         yaml_file = f.read()
783         try:
784           trim_index = yaml_file.index("# Test Helper trims the rest of the file")
785           yaml_file = yaml_file[0:trim_index]
786         except ValueError:
787           pass
788         return yaml.safe_load(yaml_file)
789
790 def auth_token(token_name):
791     return fixture("api_client_authorizations")[token_name]["api_token"]
792
793 def authorize_with(token_name):
794     '''token_name is the symbolic name of the token from the api_client_authorizations fixture'''
795     arvados.config.settings()["ARVADOS_API_TOKEN"] = auth_token(token_name)
796     arvados.config.settings()["ARVADOS_API_HOST"] = os.environ.get("ARVADOS_API_HOST")
797     arvados.config.settings()["ARVADOS_API_HOST_INSECURE"] = "true"
798
799 class TestCaseWithServers(unittest.TestCase):
800     """TestCase to start and stop supporting Arvados servers.
801
802     Define any of MAIN_SERVER, KEEP_SERVER, and/or KEEP_PROXY_SERVER
803     class variables as a dictionary of keyword arguments.  If you do,
804     setUpClass will start the corresponding servers by passing these
805     keyword arguments to the run, run_keep, and/or run_keep_server
806     functions, respectively.  It will also set Arvados environment
807     variables to point to these servers appropriately.  If you don't
808     run a Keep or Keep proxy server, setUpClass will set up a
809     temporary directory for Keep local storage, and set it as
810     KEEP_LOCAL_STORE.
811
812     tearDownClass will stop any servers started, and restore the
813     original environment.
814     """
815     MAIN_SERVER = None
816     WS_SERVER = None
817     KEEP_SERVER = None
818     KEEP_PROXY_SERVER = None
819     KEEP_WEB_SERVER = None
820
821     @staticmethod
822     def _restore_dict(src, dest):
823         for key in list(dest.keys()):
824             if key not in src:
825                 del dest[key]
826         dest.update(src)
827
828     @classmethod
829     def setUpClass(cls):
830         cls._orig_environ = os.environ.copy()
831         cls._orig_config = arvados.config.settings().copy()
832         cls._cleanup_funcs = []
833         os.environ.pop('ARVADOS_KEEP_SERVICES', None)
834         os.environ.pop('ARVADOS_EXTERNAL_CLIENT', None)
835         for server_kwargs, start_func, stop_func in (
836                 (cls.MAIN_SERVER, run, reset),
837                 (cls.WS_SERVER, run_ws, stop_ws),
838                 (cls.KEEP_SERVER, run_keep, stop_keep),
839                 (cls.KEEP_PROXY_SERVER, run_keep_proxy, stop_keep_proxy),
840                 (cls.KEEP_WEB_SERVER, run_keep_web, stop_keep_web)):
841             if server_kwargs is not None:
842                 start_func(**server_kwargs)
843                 cls._cleanup_funcs.append(stop_func)
844         if (cls.KEEP_SERVER is None) and (cls.KEEP_PROXY_SERVER is None):
845             cls.local_store = tempfile.mkdtemp()
846             os.environ['KEEP_LOCAL_STORE'] = cls.local_store
847             cls._cleanup_funcs.append(
848                 lambda: shutil.rmtree(cls.local_store, ignore_errors=True))
849         else:
850             os.environ.pop('KEEP_LOCAL_STORE', None)
851         arvados.config.initialize()
852
853     @classmethod
854     def tearDownClass(cls):
855         for clean_func in cls._cleanup_funcs:
856             clean_func()
857         cls._restore_dict(cls._orig_environ, os.environ)
858         cls._restore_dict(cls._orig_config, arvados.config.settings())
859
860
861 if __name__ == "__main__":
862     actions = [
863         'start', 'stop',
864         'start_ws', 'stop_ws',
865         'start_controller', 'stop_controller',
866         'start_keep', 'stop_keep',
867         'start_keep_proxy', 'stop_keep_proxy',
868         'start_keep-web', 'stop_keep-web',
869         'start_arv-git-httpd', 'stop_arv-git-httpd',
870         'start_nginx', 'stop_nginx',
871     ]
872     parser = argparse.ArgumentParser()
873     parser.add_argument('action', type=str, help="one of {}".format(actions))
874     parser.add_argument('--auth', type=str, metavar='FIXTURE_NAME', help='Print authorization info for given api_client_authorizations fixture')
875     parser.add_argument('--num-keep-servers', metavar='int', type=int, default=2, help="Number of keep servers desired")
876     parser.add_argument('--keep-enforce-permissions', action="store_true", help="Enforce keep permissions")
877
878     args = parser.parse_args()
879
880     if args.action not in actions:
881         print("Unrecognized action '{}'. Actions are: {}.".
882               format(args.action, actions),
883               file=sys.stderr)
884         sys.exit(1)
885     if args.action == 'start':
886         stop(force=('ARVADOS_TEST_API_HOST' not in os.environ))
887         run(leave_running_atexit=True)
888         host = os.environ['ARVADOS_API_HOST']
889         if args.auth is not None:
890             token = auth_token(args.auth)
891             print("export ARVADOS_API_TOKEN={}".format(pipes.quote(token)))
892             print("export ARVADOS_API_HOST={}".format(pipes.quote(host)))
893             print("export ARVADOS_API_HOST_INSECURE=true")
894         else:
895             print(host)
896     elif args.action == 'stop':
897         stop(force=('ARVADOS_TEST_API_HOST' not in os.environ))
898     elif args.action == 'start_ws':
899         run_ws()
900     elif args.action == 'stop_ws':
901         stop_ws()
902     elif args.action == 'start_controller':
903         run_controller()
904     elif args.action == 'stop_controller':
905         stop_controller()
906     elif args.action == 'start_keep':
907         run_keep(enforce_permissions=args.keep_enforce_permissions, num_servers=args.num_keep_servers)
908     elif args.action == 'stop_keep':
909         stop_keep(num_servers=args.num_keep_servers)
910     elif args.action == 'start_keep_proxy':
911         run_keep_proxy()
912     elif args.action == 'stop_keep_proxy':
913         stop_keep_proxy()
914     elif args.action == 'start_arv-git-httpd':
915         run_arv_git_httpd()
916     elif args.action == 'stop_arv-git-httpd':
917         stop_arv_git_httpd()
918     elif args.action == 'start_keep-web':
919         run_keep_web()
920     elif args.action == 'stop_keep-web':
921         stop_keep_web()
922     elif args.action == 'start_nginx':
923         run_nginx()
924         print("export ARVADOS_API_HOST=0.0.0.0:{}".format(_getport('controller-ssl')))
925     elif args.action == 'stop_nginx':
926         stop_nginx()
927     else:
928         raise Exception("action recognized but not implemented!?")