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