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