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