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