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