bca372ccdd32da49cccb2d146014dc7c67be39e7
[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     logf = open(_logfilename('railsapi'), 'a')
330     railsapi = subprocess.Popen(
331         ['bundle', 'exec',
332          'passenger', 'start', '-p{}'.format(port),
333          '--pid-file', pid_file,
334          '--log-file', '/dev/stdout',
335          '--ssl',
336          '--ssl-certificate', 'tmp/self-signed.pem',
337          '--ssl-certificate-key', 'tmp/self-signed.key'],
338         env=env, stdin=open('/dev/null'), stdout=logf, stderr=logf)
339
340     if not leave_running_atexit:
341         atexit.register(kill_server_pid, pid_file, passenger_root=api_src_dir)
342
343     my_api_host = "127.0.0.1:"+str(port)
344     os.environ['ARVADOS_API_HOST'] = my_api_host
345
346     # Make sure the server has written its pid file and started
347     # listening on its TCP port
348     find_server_pid(pid_file)
349     _wait_until_port_listens(port)
350
351     reset()
352     os.chdir(restore_cwd)
353
354 def reset():
355     """Reset the test server to fixture state.
356
357     This resets the ARVADOS_TEST_API_HOST provided by a parent process
358     if any, otherwise the server started by run().
359
360     It also resets ARVADOS_* environment vars to point to the test
361     server with admin credentials.
362     """
363     existing_api_host = os.environ.get('ARVADOS_TEST_API_HOST', my_api_host)
364     token = auth_token('admin')
365     httpclient = httplib2.Http(ca_certs=os.path.join(
366         SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.pem'))
367     httpclient.request(
368         'https://{}/database/reset'.format(existing_api_host),
369         'POST',
370         headers={'Authorization': 'OAuth2 {}'.format(token), 'Connection':'close'})
371
372     os.environ['ARVADOS_API_HOST_INSECURE'] = 'true'
373     os.environ['ARVADOS_API_TOKEN'] = token
374     os.environ['ARVADOS_API_HOST'] = existing_api_host
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 get_config():
396     with open(os.environ["ARVADOS_CONFIG"]) as f:
397         return yaml.safe_load(f)
398
399 def internal_port_from_config(service, idx=0):
400     return int(urlparse(
401         sorted(list(get_config()["Clusters"]["zzzzz"]["Services"][service]["InternalURLs"].keys()))[idx]).
402                netloc.split(":")[1])
403
404 def external_port_from_config(service):
405     return int(urlparse(get_config()["Clusters"]["zzzzz"]["Services"][service]["ExternalURL"]).netloc.split(":")[1])
406
407 def run_controller():
408     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
409         return
410     stop_controller()
411     logf = open(_logfilename('controller'), 'a')
412     port = internal_port_from_config("Controller")
413     controller = subprocess.Popen(
414         ["arvados-server", "controller"],
415         stdin=open('/dev/null'), stdout=logf, stderr=logf, close_fds=True)
416     with open(_pidfile('controller'), 'w') as f:
417         f.write(str(controller.pid))
418     _wait_until_port_listens(port)
419     return port
420
421 def stop_controller():
422     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
423         return
424     kill_server_pid(_pidfile('controller'))
425
426 def run_ws():
427     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
428         return
429     stop_ws()
430     port = internal_port_from_config("Websocket")
431     logf = open(_logfilename('ws'), 'a')
432     ws = subprocess.Popen(["ws"],
433         stdin=open('/dev/null'), stdout=logf, stderr=logf, close_fds=True)
434     with open(_pidfile('ws'), 'w') as f:
435         f.write(str(ws.pid))
436     _wait_until_port_listens(port)
437     return port
438
439 def stop_ws():
440     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
441         return
442     kill_server_pid(_pidfile('ws'))
443
444 def _start_keep(n, blob_signing=False):
445     datadir = os.path.join(TEST_TMPDIR, "keep%d.data"%n)
446     if os.path.exists(datadir):
447         shutil.rmtree(datadir)
448     os.mkdir(datadir)
449     port = internal_port_from_config("Keepstore", idx=n)
450
451     # Currently, if there are multiple InternalURLs for a single host,
452     # the only way to tell a keepstore process which one it's supposed
453     # to listen on is to supply a redacted version of the config, with
454     # the other InternalURLs removed.
455     conf = os.path.join(TEST_TMPDIR, "keep%d.yaml"%n)
456     confdata = get_config()
457     confdata['Clusters']['zzzzz']['Services']['Keepstore']['InternalURLs'] = {"http://127.0.0.1:%d"%port: {}}
458     confdata['Clusters']['zzzzz']['Collections']['BlobSigning'] = blob_signing
459     with open(conf, 'w') as f:
460         yaml.safe_dump(confdata, f)
461     keep_cmd = ["keepstore", "-config", conf]
462
463     with open(_logfilename('keep{}'.format(n)), 'a') as logf:
464         with open('/dev/null') as _stdin:
465             child = subprocess.Popen(
466                 keep_cmd, stdin=_stdin, stdout=logf, stderr=logf, close_fds=True)
467
468     print('child.pid is %d'%child.pid, file=sys.stderr)
469     with open(_pidfile('keep{}'.format(n)), 'w') as f:
470         f.write(str(child.pid))
471
472     _wait_until_port_listens(port)
473
474     return port
475
476 def run_keep(num_servers=2, **kwargs):
477     stop_keep(num_servers)
478
479     api = arvados.api(
480         version='v1',
481         host=os.environ['ARVADOS_API_HOST'],
482         token=os.environ['ARVADOS_API_TOKEN'],
483         insecure=True)
484
485     for d in api.keep_services().list(filters=[['service_type','=','disk']]).execute()['items']:
486         api.keep_services().delete(uuid=d['uuid']).execute()
487     for d in api.keep_disks().list().execute()['items']:
488         api.keep_disks().delete(uuid=d['uuid']).execute()
489
490     for d in range(0, num_servers):
491         port = _start_keep(d, **kwargs)
492         svc = api.keep_services().create(body={'keep_service': {
493             'uuid': 'zzzzz-bi6l4-keepdisk{:07d}'.format(d),
494             'service_host': 'localhost',
495             'service_port': port,
496             'service_type': 'disk',
497             'service_ssl_flag': False,
498         }}).execute()
499         api.keep_disks().create(body={
500             'keep_disk': {'keep_service_uuid': svc['uuid'] }
501         }).execute()
502
503     # If keepproxy and/or keep-web is running, send SIGHUP to make
504     # them discover the new keepstore services.
505     for svc in ('keepproxy', 'keep-web'):
506         pidfile = _pidfile('keepproxy')
507         if os.path.exists(pidfile):
508             try:
509                 with open(pidfile) as pid:
510                     os.kill(int(pid.read()), signal.SIGHUP)
511             except OSError:
512                 os.remove(pidfile)
513
514 def _stop_keep(n):
515     kill_server_pid(_pidfile('keep{}'.format(n)))
516
517 def stop_keep(num_servers=2):
518     for n in range(0, num_servers):
519         _stop_keep(n)
520
521 def run_keep_proxy():
522     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
523         os.environ["ARVADOS_KEEP_SERVICES"] = "http://localhost:{}".format(internal_port_from_config('Keepproxy'))
524         return
525     stop_keep_proxy()
526
527     port = internal_port_from_config("Keepproxy")
528     env = os.environ.copy()
529     env['ARVADOS_API_TOKEN'] = auth_token('anonymous')
530     logf = open(_logfilename('keepproxy'), 'a')
531     kp = subprocess.Popen(
532         ['keepproxy'], env=env, stdin=open('/dev/null'), stdout=logf, stderr=logf, close_fds=True)
533
534     with open(_pidfile('keepproxy'), 'w') as f:
535         f.write(str(kp.pid))
536     _wait_until_port_listens(port)
537
538     print("Using API %s token %s" % (os.environ['ARVADOS_API_HOST'], auth_token('admin')), file=sys.stdout)
539     api = arvados.api(
540         version='v1',
541         host=os.environ['ARVADOS_API_HOST'],
542         token=auth_token('admin'),
543         insecure=True)
544     for d in api.keep_services().list(
545             filters=[['service_type','=','proxy']]).execute()['items']:
546         api.keep_services().delete(uuid=d['uuid']).execute()
547     api.keep_services().create(body={'keep_service': {
548         'service_host': 'localhost',
549         'service_port': port,
550         'service_type': 'proxy',
551         'service_ssl_flag': False,
552     }}).execute()
553     os.environ["ARVADOS_KEEP_SERVICES"] = "http://localhost:{}".format(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     gitport = internal_port_from_config("GitHTTP")
567     env = os.environ.copy()
568     env.pop('ARVADOS_API_TOKEN', None)
569     logf = open(_logfilename('arv-git-httpd'), 'a')
570     agh = subprocess.Popen(['arv-git-httpd'],
571         env=env, stdin=open('/dev/null'), stdout=logf, stderr=logf)
572     with open(_pidfile('arv-git-httpd'), 'w') as f:
573         f.write(str(agh.pid))
574     _wait_until_port_listens(gitport)
575
576 def stop_arv_git_httpd():
577     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
578         return
579     kill_server_pid(_pidfile('arv-git-httpd'))
580
581 def run_keep_web():
582     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
583         return
584     stop_keep_web()
585
586     keepwebport = internal_port_from_config("WebDAV")
587     env = os.environ.copy()
588     logf = open(_logfilename('keep-web'), 'a')
589     keepweb = subprocess.Popen(
590         ['keep-web'],
591         env=env, stdin=open('/dev/null'), stdout=logf, stderr=logf)
592     with open(_pidfile('keep-web'), 'w') as f:
593         f.write(str(keepweb.pid))
594     _wait_until_port_listens(keepwebport)
595
596 def stop_keep_web():
597     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
598         return
599     kill_server_pid(_pidfile('keep-web'))
600
601 def run_nginx():
602     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
603         return
604     stop_nginx()
605     nginxconf = {}
606     nginxconf['LISTENHOST'] = 'localhost'
607     nginxconf['CONTROLLERPORT'] = internal_port_from_config("Controller")
608     nginxconf['CONTROLLERSSLPORT'] = external_port_from_config("Controller")
609     nginxconf['KEEPWEBPORT'] = internal_port_from_config("WebDAV")
610     nginxconf['KEEPWEBDLSSLPORT'] = external_port_from_config("WebDAVDownload")
611     nginxconf['KEEPWEBSSLPORT'] = external_port_from_config("WebDAV")
612     nginxconf['KEEPPROXYPORT'] = internal_port_from_config("Keepproxy")
613     nginxconf['KEEPPROXYSSLPORT'] = external_port_from_config("Keepproxy")
614     nginxconf['GITPORT'] = internal_port_from_config("GitHTTP")
615     nginxconf['GITSSLPORT'] = external_port_from_config("GitHTTP")
616     nginxconf['WSPORT'] = internal_port_from_config("Websocket")
617     nginxconf['WSSSLPORT'] = external_port_from_config("Websocket")
618     nginxconf['WORKBENCH1PORT'] = internal_port_from_config("Workbench1")
619     nginxconf['WORKBENCH1SSLPORT'] = external_port_from_config("Workbench1")
620     nginxconf['SSLCERT'] = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.pem')
621     nginxconf['SSLKEY'] = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.key')
622     nginxconf['ACCESSLOG'] = _logfilename('nginx_access')
623     nginxconf['ERRORLOG'] = _logfilename('nginx_error')
624     nginxconf['TMPDIR'] = TEST_TMPDIR
625
626     conftemplatefile = os.path.join(MY_DIRNAME, 'nginx.conf')
627     conffile = os.path.join(TEST_TMPDIR, 'nginx.conf')
628     with open(conffile, 'w') as f:
629         f.write(re.sub(
630             r'{{([A-Z]+[A-Z0-9]+)}}',
631             lambda match: str(nginxconf.get(match.group(1))),
632             open(conftemplatefile).read()))
633
634     env = os.environ.copy()
635     env['PATH'] = env['PATH']+':/sbin:/usr/sbin:/usr/local/sbin'
636
637     nginx = subprocess.Popen(
638         ['nginx',
639          '-g', 'error_log stderr info;',
640          '-g', 'pid '+_pidfile('nginx')+';',
641          '-c', conffile],
642         env=env, stdin=open('/dev/null'), stdout=sys.stderr)
643     _wait_until_port_listens(nginxconf['CONTROLLERSSLPORT'])
644
645 def setup_config():
646     rails_api_port = find_available_port()
647     controller_port = find_available_port()
648     controller_external_port = find_available_port()
649     websocket_port = find_available_port()
650     websocket_external_port = find_available_port()
651     workbench1_port = find_available_port()
652     workbench1_external_port = find_available_port()
653     git_httpd_port = find_available_port()
654     git_httpd_external_port = find_available_port()
655     keepproxy_port = find_available_port()
656     keepproxy_external_port = find_available_port()
657     keepstore_ports = sorted([str(find_available_port()) for _ in xrange(0,4)])
658     keep_web_port = find_available_port()
659     keep_web_external_port = find_available_port()
660     keep_web_dl_port = find_available_port()
661     keep_web_dl_external_port = find_available_port()
662
663     dbconf = os.path.join(os.environ["CONFIGSRC"], "config.yml")
664
665     print("Getting config from %s" % dbconf, file=sys.stderr)
666
667     pgconnection = yaml.safe_load(open(dbconf))["Clusters"]["zzzzz"]["PostgreSQL"]["Connection"]
668
669     localhost = "127.0.0.1"
670     services = {
671         "RailsAPI": {
672             "InternalURLs": {
673                 "https://%s:%s"%(localhost, rails_api_port): {},
674             },
675         },
676         "Controller": {
677             "ExternalURL": "https://%s:%s" % (localhost, controller_external_port),
678             "InternalURLs": {
679                 "http://%s:%s"%(localhost, controller_port): {},
680             },
681         },
682         "Websocket": {
683             "ExternalURL": "wss://%s:%s/websocket" % (localhost, websocket_external_port),
684             "InternalURLs": {
685                 "http://%s:%s"%(localhost, websocket_port): {},
686             },
687         },
688         "Workbench1": {
689             "ExternalURL": "https://%s:%s/" % (localhost, workbench1_external_port),
690             "InternalURLs": {
691                 "http://%s:%s"%(localhost, workbench1_port): {},
692             },
693         },
694         "GitHTTP": {
695             "ExternalURL": "https://%s:%s" % (localhost, git_httpd_external_port),
696             "InternalURLs": {
697                 "http://%s:%s"%(localhost, git_httpd_port): {}
698             },
699         },
700         "Keepstore": {
701             "InternalURLs": {
702                 "http://%s:%s"%(localhost, port): {} for port in keepstore_ports
703             },
704         },
705         "Keepproxy": {
706             "ExternalURL": "https://%s:%s" % (localhost, keepproxy_external_port),
707             "InternalURLs": {
708                 "http://%s:%s"%(localhost, keepproxy_port): {},
709             },
710         },
711         "WebDAV": {
712             "ExternalURL": "https://%s:%s" % (localhost, keep_web_external_port),
713             "InternalURLs": {
714                 "http://%s:%s"%(localhost, keep_web_port): {},
715             },
716         },
717         "WebDAVDownload": {
718             "ExternalURL": "https://%s:%s" % (localhost, keep_web_dl_external_port),
719             "InternalURLs": {
720                 "http://%s:%s"%(localhost, keep_web_dl_port): {},
721             },
722         },
723         "SSO": {
724             "ExternalURL": "http://localhost:3002",
725         },
726     }
727
728     config = {
729         "Clusters": {
730             "zzzzz": {
731                 "ManagementToken": "e687950a23c3a9bceec28c6223a06c79",
732                 "SystemRootToken": auth_token('system_user'),
733                 "API": {
734                     "RequestTimeout": "30s",
735                     "RailsSessionSecretToken": "e24205c490ac07e028fd5f8a692dcb398bcd654eff1aef5f9fe6891994b18483",
736                 },
737                 "Login": {
738                     "ProviderAppID": "arvados-server",
739                     "ProviderAppSecret": "608dbf356a327e2d0d4932b60161e212c2d8d8f5e25690d7b622f850a990cd33",
740                 },
741                 "SystemLogs": {
742                     "LogLevel": ('info' if os.environ.get('ARVADOS_DEBUG', '') in ['','0'] else 'debug'),
743                 },
744                 "PostgreSQL": {
745                     "Connection": pgconnection,
746                 },
747                 "TLS": {
748                     "Insecure": True,
749                 },
750                 "Services": services,
751                 "Users": {
752                     "AnonymousUserToken": auth_token('anonymous'),
753                     "UserProfileNotificationAddress": "arvados@example.com",
754                 },
755                 "Collections": {
756                     "BlobSigningKey": "zfhgfenhffzltr9dixws36j1yhksjoll2grmku38mi7yxd66h5j4q9w4jzanezacp8s6q0ro3hxakfye02152hncy6zml2ed0uc",
757                     "TrustAllContent": True,
758                     "ForwardSlashNameSubstitution": "/",
759                     "TrashSweepInterval": "-1s",
760                 },
761                 "Git": {
762                     "Repositories": os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'git', 'test'),
763                 },
764                 "Containers": {
765                     "JobsAPI": {
766                         "GitInternalDir": os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'internal.git'),
767                     },
768                     "SupportedDockerImageFormats": {"v1": {}},
769                 },
770                 "Volumes": {
771                     "zzzzz-nyw5e-%015d"%n: {
772                         "AccessViaHosts": {
773                             "http://%s:%s" % (localhost, keepstore_ports[n]): {},
774                         },
775                         "Driver": "Directory",
776                         "DriverParameters": {
777                             "Root": os.path.join(TEST_TMPDIR, "keep%d.data"%n),
778                         },
779                     } for n in range(len(keepstore_ports))
780                 },
781             },
782         },
783     }
784
785     conf = os.path.join(TEST_TMPDIR, 'arvados.yml')
786     with open(conf, 'w') as f:
787         yaml.safe_dump(config, f)
788
789     ex = "export ARVADOS_CONFIG="+conf
790     print(ex)
791
792
793 def stop_nginx():
794     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
795         return
796     kill_server_pid(_pidfile('nginx'))
797
798 def _pidfile(program):
799     return os.path.join(TEST_TMPDIR, program + '.pid')
800
801 def fixture(fix):
802     '''load a fixture yaml file'''
803     with open(os.path.join(SERVICES_SRC_DIR, 'api', "test", "fixtures",
804                            fix + ".yml")) as f:
805         yaml_file = f.read()
806         try:
807           trim_index = yaml_file.index("# Test Helper trims the rest of the file")
808           yaml_file = yaml_file[0:trim_index]
809         except ValueError:
810           pass
811         return yaml.safe_load(yaml_file)
812
813 def auth_token(token_name):
814     return fixture("api_client_authorizations")[token_name]["api_token"]
815
816 def authorize_with(token_name):
817     '''token_name is the symbolic name of the token from the api_client_authorizations fixture'''
818     arvados.config.settings()["ARVADOS_API_TOKEN"] = auth_token(token_name)
819     arvados.config.settings()["ARVADOS_API_HOST"] = os.environ.get("ARVADOS_API_HOST")
820     arvados.config.settings()["ARVADOS_API_HOST_INSECURE"] = "true"
821
822 class TestCaseWithServers(unittest.TestCase):
823     """TestCase to start and stop supporting Arvados servers.
824
825     Define any of MAIN_SERVER, KEEP_SERVER, and/or KEEP_PROXY_SERVER
826     class variables as a dictionary of keyword arguments.  If you do,
827     setUpClass will start the corresponding servers by passing these
828     keyword arguments to the run, run_keep, and/or run_keep_server
829     functions, respectively.  It will also set Arvados environment
830     variables to point to these servers appropriately.  If you don't
831     run a Keep or Keep proxy server, setUpClass will set up a
832     temporary directory for Keep local storage, and set it as
833     KEEP_LOCAL_STORE.
834
835     tearDownClass will stop any servers started, and restore the
836     original environment.
837     """
838     MAIN_SERVER = None
839     WS_SERVER = None
840     KEEP_SERVER = None
841     KEEP_PROXY_SERVER = None
842     KEEP_WEB_SERVER = None
843
844     @staticmethod
845     def _restore_dict(src, dest):
846         for key in list(dest.keys()):
847             if key not in src:
848                 del dest[key]
849         dest.update(src)
850
851     @classmethod
852     def setUpClass(cls):
853         cls._orig_environ = os.environ.copy()
854         cls._orig_config = arvados.config.settings().copy()
855         cls._cleanup_funcs = []
856         os.environ.pop('ARVADOS_KEEP_SERVICES', None)
857         os.environ.pop('ARVADOS_EXTERNAL_CLIENT', None)
858         for server_kwargs, start_func, stop_func in (
859                 (cls.MAIN_SERVER, run, reset),
860                 (cls.WS_SERVER, run_ws, stop_ws),
861                 (cls.KEEP_SERVER, run_keep, stop_keep),
862                 (cls.KEEP_PROXY_SERVER, run_keep_proxy, stop_keep_proxy),
863                 (cls.KEEP_WEB_SERVER, run_keep_web, stop_keep_web)):
864             if server_kwargs is not None:
865                 start_func(**server_kwargs)
866                 cls._cleanup_funcs.append(stop_func)
867         if (cls.KEEP_SERVER is None) and (cls.KEEP_PROXY_SERVER is None):
868             cls.local_store = tempfile.mkdtemp()
869             os.environ['KEEP_LOCAL_STORE'] = cls.local_store
870             cls._cleanup_funcs.append(
871                 lambda: shutil.rmtree(cls.local_store, ignore_errors=True))
872         else:
873             os.environ.pop('KEEP_LOCAL_STORE', None)
874         arvados.config.initialize()
875
876     @classmethod
877     def tearDownClass(cls):
878         for clean_func in cls._cleanup_funcs:
879             clean_func()
880         cls._restore_dict(cls._orig_environ, os.environ)
881         cls._restore_dict(cls._orig_config, arvados.config.settings())
882
883
884 if __name__ == "__main__":
885     actions = [
886         'start', 'stop',
887         'start_ws', 'stop_ws',
888         'start_controller', 'stop_controller',
889         'start_keep', 'stop_keep',
890         'start_keep_proxy', 'stop_keep_proxy',
891         'start_keep-web', 'stop_keep-web',
892         'start_arv-git-httpd', 'stop_arv-git-httpd',
893         'start_nginx', 'stop_nginx', 'setup_config',
894     ]
895     parser = argparse.ArgumentParser()
896     parser.add_argument('action', type=str, help="one of {}".format(actions))
897     parser.add_argument('--auth', type=str, metavar='FIXTURE_NAME', help='Print authorization info for given api_client_authorizations fixture')
898     parser.add_argument('--num-keep-servers', metavar='int', type=int, default=2, help="Number of keep servers desired")
899     parser.add_argument('--keep-blob-signing', action="store_true", help="Enable blob signing for keepstore servers")
900
901     args = parser.parse_args()
902
903     if args.action not in actions:
904         print("Unrecognized action '{}'. Actions are: {}.".
905               format(args.action, actions),
906               file=sys.stderr)
907         sys.exit(1)
908     if args.action == 'start':
909         stop(force=('ARVADOS_TEST_API_HOST' not in os.environ))
910         run(leave_running_atexit=True)
911         host = os.environ['ARVADOS_API_HOST']
912         if args.auth is not None:
913             token = auth_token(args.auth)
914             print("export ARVADOS_API_TOKEN={}".format(pipes.quote(token)))
915             print("export ARVADOS_API_HOST={}".format(pipes.quote(host)))
916             print("export ARVADOS_API_HOST_INSECURE=true")
917         else:
918             print(host)
919     elif args.action == 'stop':
920         stop(force=('ARVADOS_TEST_API_HOST' not in os.environ))
921     elif args.action == 'start_ws':
922         run_ws()
923     elif args.action == 'stop_ws':
924         stop_ws()
925     elif args.action == 'start_controller':
926         run_controller()
927     elif args.action == 'stop_controller':
928         stop_controller()
929     elif args.action == 'start_keep':
930         run_keep(blob_signing=args.keep_blob_signing, num_servers=args.num_keep_servers)
931     elif args.action == 'stop_keep':
932         stop_keep(num_servers=args.num_keep_servers)
933     elif args.action == 'start_keep_proxy':
934         run_keep_proxy()
935     elif args.action == 'stop_keep_proxy':
936         stop_keep_proxy()
937     elif args.action == 'start_arv-git-httpd':
938         run_arv_git_httpd()
939     elif args.action == 'stop_arv-git-httpd':
940         stop_arv_git_httpd()
941     elif args.action == 'start_keep-web':
942         run_keep_web()
943     elif args.action == 'stop_keep-web':
944         stop_keep_web()
945     elif args.action == 'start_nginx':
946         run_nginx()
947         print("export ARVADOS_API_HOST=0.0.0.0:{}".format(external_port_from_config('Controller')))
948     elif args.action == 'stop_nginx':
949         stop_nginx()
950     elif args.action == 'setup_config':
951         setup_config()
952     else:
953         raise Exception("action recognized but not implemented!?")