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