13804: Update comments for comments for "consecutive_idle_count"
[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 _fifo2stderr(label):
206     """Create a fifo, and copy it to stderr, prepending label to each line.
207
208     Return value is the path to the new FIFO.
209
210     +label+ should contain only alphanumerics: it is also used as part
211     of the FIFO filename.
212     """
213     fifo = os.path.join(TEST_TMPDIR, label+'.fifo')
214     try:
215         os.remove(fifo)
216     except OSError as error:
217         if error.errno != errno.ENOENT:
218             raise
219     os.mkfifo(fifo, 0o700)
220     subprocess.Popen(
221         ['stdbuf', '-i0', '-oL', '-eL', 'sed', '-e', 's/^/['+label+'] /', fifo],
222         stdout=sys.stderr)
223     return fifo
224
225 def run(leave_running_atexit=False):
226     """Ensure an API server is running, and ARVADOS_API_* env vars have
227     admin credentials for it.
228
229     If ARVADOS_TEST_API_HOST is set, a parent process has started a
230     test server for us to use: we just need to reset() it using the
231     admin token fixture.
232
233     If a previous call to run() started a new server process, and it
234     is still running, we just need to reset() it to fixture state and
235     return.
236
237     If neither of those options work out, we'll really start a new
238     server.
239     """
240     global my_api_host
241
242     # Delete cached discovery documents.
243     #
244     # This will clear cached docs that belong to other processes (like
245     # concurrent test suites) even if they're still running. They should
246     # be able to tolerate that.
247     for fn in glob.glob(os.path.join(
248             str(arvados.http_cache('discovery')),
249             '*,arvados,v1,rest,*')):
250         os.unlink(fn)
251
252     pid_file = _pidfile('api')
253     pid_file_ok = find_server_pid(pid_file, 0)
254
255     existing_api_host = os.environ.get('ARVADOS_TEST_API_HOST', my_api_host)
256     if existing_api_host and pid_file_ok:
257         if existing_api_host == my_api_host:
258             try:
259                 return reset()
260             except:
261                 # Fall through to shutdown-and-start case.
262                 pass
263         else:
264             # Server was provided by parent. Can't recover if it's
265             # unresettable.
266             return reset()
267
268     # Before trying to start up our own server, call stop() to avoid
269     # "Phusion Passenger Standalone is already running on PID 12345".
270     # (If we've gotten this far, ARVADOS_TEST_API_HOST isn't set, so
271     # we know the server is ours to kill.)
272     stop(force=True)
273
274     restore_cwd = os.getcwd()
275     api_src_dir = os.path.join(SERVICES_SRC_DIR, 'api')
276     os.chdir(api_src_dir)
277
278     # Either we haven't started a server of our own yet, or it has
279     # died, or we have lost our credentials, or something else is
280     # preventing us from calling reset(). Start a new one.
281
282     if not os.path.exists('tmp'):
283         os.makedirs('tmp')
284
285     if not os.path.exists('tmp/api'):
286         os.makedirs('tmp/api')
287
288     if not os.path.exists('tmp/logs'):
289         os.makedirs('tmp/logs')
290
291     # Install the git repository fixtures.
292     gitdir = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'git')
293     gittarball = os.path.join(SERVICES_SRC_DIR, 'api', 'test', 'test.git.tar')
294     if not os.path.isdir(gitdir):
295         os.makedirs(gitdir)
296     subprocess.check_output(['tar', '-xC', gitdir, '-f', gittarball])
297
298     # The nginx proxy isn't listening here yet, but we need to choose
299     # the wss:// port now so we can write the API server config file.
300     wss_port = find_available_port()
301     _setport('wss', wss_port)
302
303     port = find_available_port()
304     env = os.environ.copy()
305     env['RAILS_ENV'] = 'test'
306     env['ARVADOS_TEST_WSS_PORT'] = str(wss_port)
307     env.pop('ARVADOS_WEBSOCKETS', None)
308     env.pop('ARVADOS_TEST_API_HOST', None)
309     env.pop('ARVADOS_API_HOST', None)
310     env.pop('ARVADOS_API_HOST_INSECURE', None)
311     env.pop('ARVADOS_API_TOKEN', None)
312     start_msg = subprocess.check_output(
313         ['bundle', 'exec',
314          'passenger', 'start', '-d', '-p{}'.format(port),
315          '--pid-file', pid_file,
316          '--log-file', os.path.join(os.getcwd(), 'log/test.log'),
317          '--ssl',
318          '--ssl-certificate', 'tmp/self-signed.pem',
319          '--ssl-certificate-key', 'tmp/self-signed.key'],
320         env=env)
321
322     if not leave_running_atexit:
323         atexit.register(kill_server_pid, pid_file, passenger_root=api_src_dir)
324
325     match = re.search(r'Accessible via: https://(.*?)/', start_msg)
326     if not match:
327         raise Exception(
328             "Passenger did not report endpoint: {}".format(start_msg))
329     my_api_host = match.group(1)
330     os.environ['ARVADOS_API_HOST'] = my_api_host
331
332     # Make sure the server has written its pid file and started
333     # listening on its TCP port
334     find_server_pid(pid_file)
335     _wait_until_port_listens(port)
336
337     reset()
338     os.chdir(restore_cwd)
339
340 def reset():
341     """Reset the test server to fixture state.
342
343     This resets the ARVADOS_TEST_API_HOST provided by a parent process
344     if any, otherwise the server started by run().
345
346     It also resets ARVADOS_* environment vars to point to the test
347     server with admin credentials.
348     """
349     existing_api_host = os.environ.get('ARVADOS_TEST_API_HOST', my_api_host)
350     token = auth_token('admin')
351     httpclient = httplib2.Http(ca_certs=os.path.join(
352         SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.pem'))
353     httpclient.request(
354         'https://{}/database/reset'.format(existing_api_host),
355         'POST',
356         headers={'Authorization': 'OAuth2 {}'.format(token)})
357     os.environ['ARVADOS_API_HOST_INSECURE'] = 'true'
358     os.environ['ARVADOS_API_HOST'] = existing_api_host
359     os.environ['ARVADOS_API_TOKEN'] = token
360
361 def stop(force=False):
362     """Stop the API server, if one is running.
363
364     If force==False, kill it only if we started it ourselves. (This
365     supports the use case where a Python test suite calls run(), but
366     run() just uses the ARVADOS_TEST_API_HOST provided by the parent
367     process, and the test suite cleans up after itself by calling
368     stop(). In this case the test server provided by the parent
369     process should be left alone.)
370
371     If force==True, kill it even if we didn't start it
372     ourselves. (This supports the use case in __main__, where "run"
373     and "stop" happen in different processes.)
374     """
375     global my_api_host
376     if force or my_api_host is not None:
377         kill_server_pid(_pidfile('api'))
378         my_api_host = None
379
380 def run_ws():
381     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
382         return
383     stop_ws()
384     port = find_available_port()
385     conf = os.path.join(TEST_TMPDIR, 'ws.yml')
386     with open(conf, 'w') as f:
387         f.write("""
388 Client:
389   APIHost: {}
390   Insecure: true
391 Listen: :{}
392 LogLevel: {}
393 Postgres:
394   host: {}
395   dbname: {}
396   user: {}
397   password: {}
398   sslmode: require
399         """.format(os.environ['ARVADOS_API_HOST'],
400                    port,
401                    ('info' if os.environ.get('ARVADOS_DEBUG', '') in ['','0'] else 'debug'),
402                    _dbconfig('host'),
403                    _dbconfig('database'),
404                    _dbconfig('username'),
405                    _dbconfig('password')))
406     logf = open(_fifo2stderr('ws'), 'w')
407     ws = subprocess.Popen(
408         ["ws", "-config", conf],
409         stdin=open('/dev/null'), stdout=logf, stderr=logf, close_fds=True)
410     with open(_pidfile('ws'), 'w') as f:
411         f.write(str(ws.pid))
412     _wait_until_port_listens(port)
413     _setport('ws', port)
414     return port
415
416 def stop_ws():
417     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
418         return
419     kill_server_pid(_pidfile('ws'))
420
421 def _start_keep(n, keep_args):
422     keep0 = tempfile.mkdtemp()
423     port = find_available_port()
424     keep_cmd = ["keepstore",
425                 "-volume={}".format(keep0),
426                 "-listen=:{}".format(port),
427                 "-pid="+_pidfile('keep{}'.format(n))]
428
429     for arg, val in keep_args.items():
430         keep_cmd.append("{}={}".format(arg, val))
431
432     logf = open(_fifo2stderr('keep{}'.format(n)), 'w')
433     kp0 = subprocess.Popen(
434         keep_cmd, stdin=open('/dev/null'), stdout=logf, stderr=logf, close_fds=True)
435
436     with open(_pidfile('keep{}'.format(n)), 'w') as f:
437         f.write(str(kp0.pid))
438
439     with open("{}/keep{}.volume".format(TEST_TMPDIR, n), 'w') as f:
440         f.write(keep0)
441
442     _wait_until_port_listens(port)
443
444     return port
445
446 def run_keep(blob_signing_key=None, enforce_permissions=False, num_servers=2):
447     stop_keep(num_servers)
448
449     keep_args = {}
450     if not blob_signing_key:
451         blob_signing_key = 'zfhgfenhffzltr9dixws36j1yhksjoll2grmku38mi7yxd66h5j4q9w4jzanezacp8s6q0ro3hxakfye02152hncy6zml2ed0uc'
452     with open(os.path.join(TEST_TMPDIR, "keep.blob_signing_key"), "w") as f:
453         keep_args['-blob-signing-key-file'] = f.name
454         f.write(blob_signing_key)
455     keep_args['-enforce-permissions'] = str(enforce_permissions).lower()
456     with open(os.path.join(TEST_TMPDIR, "keep.data-manager-token-file"), "w") as f:
457         keep_args['-data-manager-token-file'] = f.name
458         f.write(auth_token('data_manager'))
459     keep_args['-never-delete'] = 'false'
460
461     api = arvados.api(
462         version='v1',
463         host=os.environ['ARVADOS_API_HOST'],
464         token=os.environ['ARVADOS_API_TOKEN'],
465         insecure=True)
466
467     for d in api.keep_services().list(filters=[['service_type','=','disk']]).execute()['items']:
468         api.keep_services().delete(uuid=d['uuid']).execute()
469     for d in api.keep_disks().list().execute()['items']:
470         api.keep_disks().delete(uuid=d['uuid']).execute()
471
472     for d in range(0, num_servers):
473         port = _start_keep(d, keep_args)
474         svc = api.keep_services().create(body={'keep_service': {
475             'uuid': 'zzzzz-bi6l4-keepdisk{:07d}'.format(d),
476             'service_host': 'localhost',
477             'service_port': port,
478             'service_type': 'disk',
479             'service_ssl_flag': False,
480         }}).execute()
481         api.keep_disks().create(body={
482             'keep_disk': {'keep_service_uuid': svc['uuid'] }
483         }).execute()
484
485     # If keepproxy and/or keep-web is running, send SIGHUP to make
486     # them discover the new keepstore services.
487     for svc in ('keepproxy', 'keep-web'):
488         pidfile = _pidfile('keepproxy')
489         if os.path.exists(pidfile):
490             try:
491                 os.kill(int(open(pidfile).read()), signal.SIGHUP)
492             except OSError:
493                 os.remove(pidfile)
494
495 def _stop_keep(n):
496     kill_server_pid(_pidfile('keep{}'.format(n)))
497     if os.path.exists("{}/keep{}.volume".format(TEST_TMPDIR, n)):
498         with open("{}/keep{}.volume".format(TEST_TMPDIR, n), 'r') as r:
499             shutil.rmtree(r.read(), True)
500         os.unlink("{}/keep{}.volume".format(TEST_TMPDIR, n))
501     if os.path.exists(os.path.join(TEST_TMPDIR, "keep.blob_signing_key")):
502         os.remove(os.path.join(TEST_TMPDIR, "keep.blob_signing_key"))
503
504 def stop_keep(num_servers=2):
505     for n in range(0, num_servers):
506         _stop_keep(n)
507
508 def run_keep_proxy():
509     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
510         return
511     stop_keep_proxy()
512
513     port = find_available_port()
514     env = os.environ.copy()
515     env['ARVADOS_API_TOKEN'] = auth_token('anonymous')
516     logf = open(_fifo2stderr('keepproxy'), 'w')
517     kp = subprocess.Popen(
518         ['keepproxy',
519          '-pid='+_pidfile('keepproxy'),
520          '-listen=:{}'.format(port)],
521         env=env, stdin=open('/dev/null'), stdout=logf, stderr=logf, close_fds=True)
522
523     api = arvados.api(
524         version='v1',
525         host=os.environ['ARVADOS_API_HOST'],
526         token=auth_token('admin'),
527         insecure=True)
528     for d in api.keep_services().list(
529             filters=[['service_type','=','proxy']]).execute()['items']:
530         api.keep_services().delete(uuid=d['uuid']).execute()
531     api.keep_services().create(body={'keep_service': {
532         'service_host': 'localhost',
533         'service_port': port,
534         'service_type': 'proxy',
535         'service_ssl_flag': False,
536     }}).execute()
537     os.environ["ARVADOS_KEEP_SERVICES"] = "http://localhost:{}".format(port)
538     _setport('keepproxy', port)
539     _wait_until_port_listens(port)
540
541 def stop_keep_proxy():
542     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
543         return
544     kill_server_pid(_pidfile('keepproxy'))
545
546 def run_arv_git_httpd():
547     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
548         return
549     stop_arv_git_httpd()
550
551     gitdir = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'git')
552     gitport = find_available_port()
553     env = os.environ.copy()
554     env.pop('ARVADOS_API_TOKEN', None)
555     logf = open(_fifo2stderr('arv-git-httpd'), 'w')
556     agh = subprocess.Popen(
557         ['arv-git-httpd',
558          '-repo-root='+gitdir+'/test',
559          '-address=:'+str(gitport)],
560         env=env, stdin=open('/dev/null'), stdout=logf, stderr=logf)
561     with open(_pidfile('arv-git-httpd'), 'w') as f:
562         f.write(str(agh.pid))
563     _setport('arv-git-httpd', gitport)
564     _wait_until_port_listens(gitport)
565
566 def stop_arv_git_httpd():
567     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
568         return
569     kill_server_pid(_pidfile('arv-git-httpd'))
570
571 def run_keep_web():
572     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
573         return
574     stop_keep_web()
575
576     keepwebport = find_available_port()
577     env = os.environ.copy()
578     env['ARVADOS_API_TOKEN'] = auth_token('anonymous')
579     logf = open(_fifo2stderr('keep-web'), 'w')
580     keepweb = subprocess.Popen(
581         ['keep-web',
582          '-allow-anonymous',
583          '-attachment-only-host=download:'+str(keepwebport),
584          '-listen=:'+str(keepwebport)],
585         env=env, stdin=open('/dev/null'), stdout=logf, stderr=logf)
586     with open(_pidfile('keep-web'), 'w') as f:
587         f.write(str(keepweb.pid))
588     _setport('keep-web', keepwebport)
589     _wait_until_port_listens(keepwebport)
590
591 def stop_keep_web():
592     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
593         return
594     kill_server_pid(_pidfile('keep-web'))
595
596 def run_nginx():
597     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
598         return
599     stop_nginx()
600     nginxconf = {}
601     nginxconf['KEEPWEBPORT'] = _getport('keep-web')
602     nginxconf['KEEPWEBDLSSLPORT'] = find_available_port()
603     nginxconf['KEEPWEBSSLPORT'] = find_available_port()
604     nginxconf['KEEPPROXYPORT'] = _getport('keepproxy')
605     nginxconf['KEEPPROXYSSLPORT'] = find_available_port()
606     nginxconf['GITPORT'] = _getport('arv-git-httpd')
607     nginxconf['GITSSLPORT'] = find_available_port()
608     nginxconf['WSPORT'] = _getport('ws')
609     nginxconf['WSSPORT'] = _getport('wss')
610     nginxconf['SSLCERT'] = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.pem')
611     nginxconf['SSLKEY'] = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.key')
612     nginxconf['ACCESSLOG'] = _fifo2stderr('nginx_access_log')
613
614     conftemplatefile = os.path.join(MY_DIRNAME, 'nginx.conf')
615     conffile = os.path.join(TEST_TMPDIR, 'nginx.conf')
616     with open(conffile, 'w') as f:
617         f.write(re.sub(
618             r'{{([A-Z]+)}}',
619             lambda match: str(nginxconf.get(match.group(1))),
620             open(conftemplatefile).read()))
621
622     env = os.environ.copy()
623     env['PATH'] = env['PATH']+':/sbin:/usr/sbin:/usr/local/sbin'
624
625     nginx = subprocess.Popen(
626         ['nginx',
627          '-g', 'error_log stderr info;',
628          '-g', 'pid '+_pidfile('nginx')+';',
629          '-c', conffile],
630         env=env, stdin=open('/dev/null'), stdout=sys.stderr)
631     _setport('keep-web-dl-ssl', nginxconf['KEEPWEBDLSSLPORT'])
632     _setport('keep-web-ssl', nginxconf['KEEPWEBSSLPORT'])
633     _setport('keepproxy-ssl', nginxconf['KEEPPROXYSSLPORT'])
634     _setport('arv-git-httpd-ssl', nginxconf['GITSSLPORT'])
635
636 def stop_nginx():
637     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
638         return
639     kill_server_pid(_pidfile('nginx'))
640
641 def _pidfile(program):
642     return os.path.join(TEST_TMPDIR, program + '.pid')
643
644 def _portfile(program):
645     return os.path.join(TEST_TMPDIR, program + '.port')
646
647 def _setport(program, port):
648     with open(_portfile(program), 'w') as f:
649         f.write(str(port))
650
651 # Returns 9 if program is not up.
652 def _getport(program):
653     try:
654         return int(open(_portfile(program)).read())
655     except IOError:
656         return 9
657
658 def _dbconfig(key):
659     global _cached_db_config
660     if not _cached_db_config:
661         _cached_db_config = yaml.load(open(os.path.join(
662             SERVICES_SRC_DIR, 'api', 'config', 'database.yml')))
663     return _cached_db_config['test'][key]
664
665 def _apiconfig(key):
666     global _cached_config
667     if _cached_config:
668         return _cached_config[key]
669     def _load(f, required=True):
670         fullpath = os.path.join(SERVICES_SRC_DIR, 'api', 'config', f)
671         if not required and not os.path.exists(fullpath):
672             return {}
673         return yaml.load(fullpath)
674     cdefault = _load('application.default.yml')
675     csite = _load('application.yml', required=False)
676     _cached_config = {}
677     for section in [cdefault.get('common',{}), cdefault.get('test',{}),
678                     csite.get('common',{}), csite.get('test',{})]:
679         _cached_config.update(section)
680     return _cached_config[key]
681
682 def fixture(fix):
683     '''load a fixture yaml file'''
684     with open(os.path.join(SERVICES_SRC_DIR, 'api', "test", "fixtures",
685                            fix + ".yml")) as f:
686         yaml_file = f.read()
687         try:
688           trim_index = yaml_file.index("# Test Helper trims the rest of the file")
689           yaml_file = yaml_file[0:trim_index]
690         except ValueError:
691           pass
692         return yaml.load(yaml_file)
693
694 def auth_token(token_name):
695     return fixture("api_client_authorizations")[token_name]["api_token"]
696
697 def authorize_with(token_name):
698     '''token_name is the symbolic name of the token from the api_client_authorizations fixture'''
699     arvados.config.settings()["ARVADOS_API_TOKEN"] = auth_token(token_name)
700     arvados.config.settings()["ARVADOS_API_HOST"] = os.environ.get("ARVADOS_API_HOST")
701     arvados.config.settings()["ARVADOS_API_HOST_INSECURE"] = "true"
702
703 class TestCaseWithServers(unittest.TestCase):
704     """TestCase to start and stop supporting Arvados servers.
705
706     Define any of MAIN_SERVER, KEEP_SERVER, and/or KEEP_PROXY_SERVER
707     class variables as a dictionary of keyword arguments.  If you do,
708     setUpClass will start the corresponding servers by passing these
709     keyword arguments to the run, run_keep, and/or run_keep_server
710     functions, respectively.  It will also set Arvados environment
711     variables to point to these servers appropriately.  If you don't
712     run a Keep or Keep proxy server, setUpClass will set up a
713     temporary directory for Keep local storage, and set it as
714     KEEP_LOCAL_STORE.
715
716     tearDownClass will stop any servers started, and restore the
717     original environment.
718     """
719     MAIN_SERVER = None
720     WS_SERVER = None
721     KEEP_SERVER = None
722     KEEP_PROXY_SERVER = None
723     KEEP_WEB_SERVER = None
724
725     @staticmethod
726     def _restore_dict(src, dest):
727         for key in list(dest.keys()):
728             if key not in src:
729                 del dest[key]
730         dest.update(src)
731
732     @classmethod
733     def setUpClass(cls):
734         cls._orig_environ = os.environ.copy()
735         cls._orig_config = arvados.config.settings().copy()
736         cls._cleanup_funcs = []
737         os.environ.pop('ARVADOS_KEEP_SERVICES', None)
738         os.environ.pop('ARVADOS_EXTERNAL_CLIENT', None)
739         for server_kwargs, start_func, stop_func in (
740                 (cls.MAIN_SERVER, run, reset),
741                 (cls.WS_SERVER, run_ws, stop_ws),
742                 (cls.KEEP_SERVER, run_keep, stop_keep),
743                 (cls.KEEP_PROXY_SERVER, run_keep_proxy, stop_keep_proxy),
744                 (cls.KEEP_WEB_SERVER, run_keep_web, stop_keep_web)):
745             if server_kwargs is not None:
746                 start_func(**server_kwargs)
747                 cls._cleanup_funcs.append(stop_func)
748         if (cls.KEEP_SERVER is None) and (cls.KEEP_PROXY_SERVER is None):
749             cls.local_store = tempfile.mkdtemp()
750             os.environ['KEEP_LOCAL_STORE'] = cls.local_store
751             cls._cleanup_funcs.append(
752                 lambda: shutil.rmtree(cls.local_store, ignore_errors=True))
753         else:
754             os.environ.pop('KEEP_LOCAL_STORE', None)
755         arvados.config.initialize()
756
757     @classmethod
758     def tearDownClass(cls):
759         for clean_func in cls._cleanup_funcs:
760             clean_func()
761         cls._restore_dict(cls._orig_environ, os.environ)
762         cls._restore_dict(cls._orig_config, arvados.config.settings())
763
764
765 if __name__ == "__main__":
766     actions = [
767         'start', 'stop',
768         'start_ws', 'stop_ws',
769         'start_keep', 'stop_keep',
770         'start_keep_proxy', 'stop_keep_proxy',
771         'start_keep-web', 'stop_keep-web',
772         'start_arv-git-httpd', 'stop_arv-git-httpd',
773         'start_nginx', 'stop_nginx',
774     ]
775     parser = argparse.ArgumentParser()
776     parser.add_argument('action', type=str, help="one of {}".format(actions))
777     parser.add_argument('--auth', type=str, metavar='FIXTURE_NAME', help='Print authorization info for given api_client_authorizations fixture')
778     parser.add_argument('--num-keep-servers', metavar='int', type=int, default=2, help="Number of keep servers desired")
779     parser.add_argument('--keep-enforce-permissions', action="store_true", help="Enforce keep permissions")
780
781     args = parser.parse_args()
782
783     if args.action not in actions:
784         print("Unrecognized action '{}'. Actions are: {}.".
785               format(args.action, actions),
786               file=sys.stderr)
787         sys.exit(1)
788     if args.action == 'start':
789         stop(force=('ARVADOS_TEST_API_HOST' not in os.environ))
790         run(leave_running_atexit=True)
791         host = os.environ['ARVADOS_API_HOST']
792         if args.auth is not None:
793             token = auth_token(args.auth)
794             print("export ARVADOS_API_TOKEN={}".format(pipes.quote(token)))
795             print("export ARVADOS_API_HOST={}".format(pipes.quote(host)))
796             print("export ARVADOS_API_HOST_INSECURE=true")
797         else:
798             print(host)
799     elif args.action == 'stop':
800         stop(force=('ARVADOS_TEST_API_HOST' not in os.environ))
801     elif args.action == 'start_ws':
802         run_ws()
803     elif args.action == 'stop_ws':
804         stop_ws()
805     elif args.action == 'start_keep':
806         run_keep(enforce_permissions=args.keep_enforce_permissions, num_servers=args.num_keep_servers)
807     elif args.action == 'stop_keep':
808         stop_keep(num_servers=args.num_keep_servers)
809     elif args.action == 'start_keep_proxy':
810         run_keep_proxy()
811     elif args.action == 'stop_keep_proxy':
812         stop_keep_proxy()
813     elif args.action == 'start_arv-git-httpd':
814         run_arv_git_httpd()
815     elif args.action == 'stop_arv-git-httpd':
816         stop_arv_git_httpd()
817     elif args.action == 'start_keep-web':
818         run_keep_web()
819     elif args.action == 'stop_keep-web':
820         stop_keep_web()
821     elif args.action == 'start_nginx':
822         run_nginx()
823     elif args.action == 'stop_nginx':
824         stop_nginx()
825     else:
826         raise Exception("action recognized but not implemented!?")