5824: Enable anonymous use of keep-web in integration tests.
[arvados.git] / sdk / python / tests / run_test_server.py
1 #!/usr/bin/env python
2
3 from __future__ import print_function
4 import argparse
5 import atexit
6 import errno
7 import httplib2
8 import os
9 import pipes
10 import random
11 import re
12 import shutil
13 import signal
14 import socket
15 import subprocess
16 import string
17 import sys
18 import tempfile
19 import time
20 import unittest
21 import yaml
22
23 MY_DIRNAME = os.path.dirname(os.path.realpath(__file__))
24 if __name__ == '__main__' and os.path.exists(
25       os.path.join(MY_DIRNAME, '..', 'arvados', '__init__.py')):
26     # We're being launched to support another test suite.
27     # Add the Python SDK source to the library path.
28     sys.path.insert(1, os.path.dirname(MY_DIRNAME))
29
30 import arvados
31 import arvados.config
32
33 ARVADOS_DIR = os.path.realpath(os.path.join(MY_DIRNAME, '../../..'))
34 SERVICES_SRC_DIR = os.path.join(ARVADOS_DIR, 'services')
35 SERVER_PID_PATH = 'tmp/pids/test-server.pid'
36 if 'GOPATH' in os.environ:
37     gopaths = os.environ['GOPATH'].split(':')
38     gobins = [os.path.join(path, 'bin') for path in gopaths]
39     os.environ['PATH'] = ':'.join(gobins) + ':' + os.environ['PATH']
40
41 TEST_TMPDIR = os.path.join(ARVADOS_DIR, 'tmp')
42 if not os.path.exists(TEST_TMPDIR):
43     os.mkdir(TEST_TMPDIR)
44
45 my_api_host = None
46 _cached_config = {}
47
48 def find_server_pid(PID_PATH, wait=10):
49     now = time.time()
50     timeout = now + wait
51     good_pid = False
52     while (not good_pid) and (now <= timeout):
53         time.sleep(0.2)
54         try:
55             with open(PID_PATH, 'r') as f:
56                 server_pid = int(f.read())
57             good_pid = (os.kill(server_pid, 0) is None)
58         except EnvironmentError:
59             good_pid = False
60         now = time.time()
61
62     if not good_pid:
63         return None
64
65     return server_pid
66
67 def kill_server_pid(pidfile, wait=10, passenger_root=False):
68     # Must re-import modules in order to work during atexit
69     import os
70     import signal
71     import subprocess
72     import time
73     try:
74         if passenger_root:
75             # First try to shut down nicely
76             restore_cwd = os.getcwd()
77             os.chdir(passenger_root)
78             subprocess.call([
79                 'bundle', 'exec', 'passenger', 'stop', '--pid-file', pidfile])
80             os.chdir(restore_cwd)
81         now = time.time()
82         timeout = now + wait
83         with open(pidfile, 'r') as f:
84             server_pid = int(f.read())
85         while now <= timeout:
86             if not passenger_root or timeout - now < wait / 2:
87                 # Half timeout has elapsed. Start sending SIGTERM
88                 os.kill(server_pid, signal.SIGTERM)
89             # Raise OSError if process has disappeared
90             os.getpgid(server_pid)
91             time.sleep(0.1)
92             now = time.time()
93     except EnvironmentError:
94         pass
95
96 def find_available_port():
97     """Return an IPv4 port number that is not in use right now.
98
99     We assume whoever needs to use the returned port is able to reuse
100     a recently used port without waiting for TIME_WAIT (see
101     SO_REUSEADDR / SO_REUSEPORT).
102
103     Some opportunity for races here, but it's better than choosing
104     something at random and not checking at all. If all of our servers
105     (hey Passenger) knew that listening on port 0 was a thing, the OS
106     would take care of the races, and this wouldn't be needed at all.
107     """
108
109     sock = socket.socket()
110     sock.bind(('0.0.0.0', 0))
111     port = sock.getsockname()[1]
112     sock.close()
113     return port
114
115 def _wait_until_port_listens(port, timeout=10):
116     """Wait for a process to start listening on the given port.
117
118     If nothing listens on the port within the specified timeout (given
119     in seconds), print a warning on stderr before returning.
120     """
121     try:
122         subprocess.check_output(['which', 'lsof'])
123     except subprocess.CalledProcessError:
124         print("WARNING: No `lsof` -- cannot wait for port to listen. "+
125               "Sleeping 0.5 and hoping for the best.")
126         time.sleep(0.5)
127         return
128     deadline = time.time() + timeout
129     while time.time() < deadline:
130         try:
131             subprocess.check_output(
132                 ['lsof', '-t', '-i', 'tcp:'+str(port)])
133         except subprocess.CalledProcessError:
134             time.sleep(0.1)
135             continue
136         return
137     print(
138         "WARNING: Nothing is listening on port {} (waited {} seconds).".
139         format(port, timeout),
140         file=sys.stderr)
141
142 def run(leave_running_atexit=False):
143     """Ensure an API server is running, and ARVADOS_API_* env vars have
144     admin credentials for it.
145
146     If ARVADOS_TEST_API_HOST is set, a parent process has started a
147     test server for us to use: we just need to reset() it using the
148     admin token fixture.
149
150     If a previous call to run() started a new server process, and it
151     is still running, we just need to reset() it to fixture state and
152     return.
153
154     If neither of those options work out, we'll really start a new
155     server.
156     """
157     global my_api_host
158
159     # Delete cached discovery document.
160     shutil.rmtree(arvados.http_cache('discovery'))
161
162     pid_file = os.path.join(SERVICES_SRC_DIR, 'api', SERVER_PID_PATH)
163     pid_file_ok = find_server_pid(pid_file, 0)
164
165     existing_api_host = os.environ.get('ARVADOS_TEST_API_HOST', my_api_host)
166     if existing_api_host and pid_file_ok:
167         if existing_api_host == my_api_host:
168             try:
169                 return reset()
170             except:
171                 # Fall through to shutdown-and-start case.
172                 pass
173         else:
174             # Server was provided by parent. Can't recover if it's
175             # unresettable.
176             return reset()
177
178     # Before trying to start up our own server, call stop() to avoid
179     # "Phusion Passenger Standalone is already running on PID 12345".
180     # (If we've gotten this far, ARVADOS_TEST_API_HOST isn't set, so
181     # we know the server is ours to kill.)
182     stop(force=True)
183
184     restore_cwd = os.getcwd()
185     api_src_dir = os.path.join(SERVICES_SRC_DIR, 'api')
186     os.chdir(api_src_dir)
187
188     # Either we haven't started a server of our own yet, or it has
189     # died, or we have lost our credentials, or something else is
190     # preventing us from calling reset(). Start a new one.
191
192     if not os.path.exists('tmp'):
193         os.makedirs('tmp')
194
195     if not os.path.exists('tmp/api'):
196         os.makedirs('tmp/api')
197
198     if not os.path.exists('tmp/logs'):
199         os.makedirs('tmp/logs')
200
201     if not os.path.exists('tmp/self-signed.pem'):
202         # We assume here that either passenger reports its listening
203         # address as https:/0.0.0.0:port/. If it reports "127.0.0.1"
204         # then the certificate won't match the host and reset() will
205         # fail certificate verification. If it reports "localhost",
206         # clients (notably Python SDK's websocket client) might
207         # resolve localhost as ::1 and then fail to connect.
208         subprocess.check_call([
209             'openssl', 'req', '-new', '-x509', '-nodes',
210             '-out', 'tmp/self-signed.pem',
211             '-keyout', 'tmp/self-signed.key',
212             '-days', '3650',
213             '-subj', '/CN=0.0.0.0'],
214         stdout=sys.stderr)
215
216     # Install the git repository fixtures.
217     gitdir = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'git')
218     gittarball = os.path.join(SERVICES_SRC_DIR, 'api', 'test', 'test.git.tar')
219     if not os.path.isdir(gitdir):
220         os.makedirs(gitdir)
221     subprocess.check_output(['tar', '-xC', gitdir, '-f', gittarball])
222
223     port = find_available_port()
224     env = os.environ.copy()
225     env['RAILS_ENV'] = 'test'
226     env['ARVADOS_WEBSOCKETS'] = 'yes'
227     env.pop('ARVADOS_TEST_API_HOST', None)
228     env.pop('ARVADOS_API_HOST', None)
229     env.pop('ARVADOS_API_HOST_INSECURE', None)
230     env.pop('ARVADOS_API_TOKEN', None)
231     start_msg = subprocess.check_output(
232         ['bundle', 'exec',
233          'passenger', 'start', '-d', '-p{}'.format(port),
234          '--pid-file', os.path.join(os.getcwd(), pid_file),
235          '--log-file', os.path.join(os.getcwd(), 'log/test.log'),
236          '--ssl',
237          '--ssl-certificate', 'tmp/self-signed.pem',
238          '--ssl-certificate-key', 'tmp/self-signed.key'],
239         env=env)
240
241     if not leave_running_atexit:
242         atexit.register(kill_server_pid, pid_file, passenger_root=api_src_dir)
243
244     match = re.search(r'Accessible via: https://(.*?)/', start_msg)
245     if not match:
246         raise Exception(
247             "Passenger did not report endpoint: {}".format(start_msg))
248     my_api_host = match.group(1)
249     os.environ['ARVADOS_API_HOST'] = my_api_host
250
251     # Make sure the server has written its pid file and started
252     # listening on its TCP port
253     find_server_pid(pid_file)
254     _wait_until_port_listens(port)
255
256     reset()
257     os.chdir(restore_cwd)
258
259 def reset():
260     """Reset the test server to fixture state.
261
262     This resets the ARVADOS_TEST_API_HOST provided by a parent process
263     if any, otherwise the server started by run().
264
265     It also resets ARVADOS_* environment vars to point to the test
266     server with admin credentials.
267     """
268     existing_api_host = os.environ.get('ARVADOS_TEST_API_HOST', my_api_host)
269     token = auth_token('admin')
270     httpclient = httplib2.Http(ca_certs=os.path.join(
271         SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.pem'))
272     httpclient.request(
273         'https://{}/database/reset'.format(existing_api_host),
274         'POST',
275         headers={'Authorization': 'OAuth2 {}'.format(token)})
276     os.environ['ARVADOS_API_HOST_INSECURE'] = 'true'
277     os.environ['ARVADOS_API_HOST'] = existing_api_host
278     os.environ['ARVADOS_API_TOKEN'] = token
279
280 def stop(force=False):
281     """Stop the API server, if one is running.
282
283     If force==False, kill it only if we started it ourselves. (This
284     supports the use case where a Python test suite calls run(), but
285     run() just uses the ARVADOS_TEST_API_HOST provided by the parent
286     process, and the test suite cleans up after itself by calling
287     stop(). In this case the test server provided by the parent
288     process should be left alone.)
289
290     If force==True, kill it even if we didn't start it
291     ourselves. (This supports the use case in __main__, where "run"
292     and "stop" happen in different processes.)
293     """
294     global my_api_host
295     if force or my_api_host is not None:
296         kill_server_pid(os.path.join(SERVICES_SRC_DIR, 'api', SERVER_PID_PATH))
297         my_api_host = None
298
299 def _start_keep(n, keep_args):
300     keep0 = tempfile.mkdtemp()
301     port = find_available_port()
302     keep_cmd = ["keepstore",
303                 "-volume={}".format(keep0),
304                 "-listen=:{}".format(port),
305                 "-pid="+_pidfile('keep{}'.format(n))]
306
307     for arg, val in keep_args.iteritems():
308         keep_cmd.append("{}={}".format(arg, val))
309
310     logf = open(os.path.join(TEST_TMPDIR, 'keep{}.log'.format(n)), 'a+')
311     kp0 = subprocess.Popen(
312         keep_cmd, stdin=open('/dev/null'), stdout=logf, stderr=logf, close_fds=True)
313     with open(_pidfile('keep{}'.format(n)), 'w') as f:
314         f.write(str(kp0.pid))
315
316     with open("{}/keep{}.volume".format(TEST_TMPDIR, n), 'w') as f:
317         f.write(keep0)
318
319     _wait_until_port_listens(port)
320
321     return port
322
323 def run_keep(blob_signing_key=None, enforce_permissions=False, num_servers=2):
324     stop_keep(num_servers)
325
326     keep_args = {}
327     if not blob_signing_key:
328         blob_signing_key = 'zfhgfenhffzltr9dixws36j1yhksjoll2grmku38mi7yxd66h5j4q9w4jzanezacp8s6q0ro3hxakfye02152hncy6zml2ed0uc'
329     with open(os.path.join(TEST_TMPDIR, "keep.blob_signing_key"), "w") as f:
330         keep_args['-blob-signing-key-file'] = f.name
331         f.write(blob_signing_key)
332     if enforce_permissions:
333         keep_args['-enforce-permissions'] = 'true'
334     with open(os.path.join(TEST_TMPDIR, "keep.data-manager-token-file"), "w") as f:
335         keep_args['-data-manager-token-file'] = f.name
336         f.write(os.environ['ARVADOS_API_TOKEN'])
337     keep_args['-never-delete'] = 'false'
338
339     api = arvados.api(
340         version='v1',
341         host=os.environ['ARVADOS_API_HOST'],
342         token=os.environ['ARVADOS_API_TOKEN'],
343         insecure=True)
344
345     for d in api.keep_services().list(filters=[['service_type','=','disk']]).execute()['items']:
346         api.keep_services().delete(uuid=d['uuid']).execute()
347     for d in api.keep_disks().list().execute()['items']:
348         api.keep_disks().delete(uuid=d['uuid']).execute()
349
350     for d in range(0, num_servers):
351         port = _start_keep(d, keep_args)
352         svc = api.keep_services().create(body={'keep_service': {
353             'uuid': 'zzzzz-bi6l4-keepdisk{:07d}'.format(d),
354             'service_host': 'localhost',
355             'service_port': port,
356             'service_type': 'disk',
357             'service_ssl_flag': False,
358         }}).execute()
359         api.keep_disks().create(body={
360             'keep_disk': {'keep_service_uuid': svc['uuid'] }
361         }).execute()
362
363     # If keepproxy is running, send SIGHUP to make it discover the new
364     # keepstore services.
365     proxypidfile = _pidfile('keepproxy')
366     if os.path.exists(proxypidfile):
367         os.kill(int(open(proxypidfile).read()), signal.SIGHUP)
368
369 def _stop_keep(n):
370     kill_server_pid(_pidfile('keep{}'.format(n)), 0)
371     if os.path.exists("{}/keep{}.volume".format(TEST_TMPDIR, n)):
372         with open("{}/keep{}.volume".format(TEST_TMPDIR, n), 'r') as r:
373             shutil.rmtree(r.read(), True)
374         os.unlink("{}/keep{}.volume".format(TEST_TMPDIR, n))
375     if os.path.exists(os.path.join(TEST_TMPDIR, "keep.blob_signing_key")):
376         os.remove(os.path.join(TEST_TMPDIR, "keep.blob_signing_key"))
377
378 def stop_keep(num_servers=2):
379     for n in range(0, num_servers):
380         _stop_keep(n)
381
382 def run_keep_proxy():
383     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
384         return
385     stop_keep_proxy()
386
387     admin_token = auth_token('admin')
388     port = find_available_port()
389     env = os.environ.copy()
390     env['ARVADOS_API_TOKEN'] = admin_token
391     kp = subprocess.Popen(
392         ['keepproxy',
393          '-pid='+_pidfile('keepproxy'),
394          '-listen=:{}'.format(port)],
395         env=env, stdin=open('/dev/null'), stdout=sys.stderr)
396
397     api = arvados.api(
398         version='v1',
399         host=os.environ['ARVADOS_API_HOST'],
400         token=admin_token,
401         insecure=True)
402     for d in api.keep_services().list(
403             filters=[['service_type','=','proxy']]).execute()['items']:
404         api.keep_services().delete(uuid=d['uuid']).execute()
405     api.keep_services().create(body={'keep_service': {
406         'service_host': 'localhost',
407         'service_port': port,
408         'service_type': 'proxy',
409         'service_ssl_flag': False,
410     }}).execute()
411     os.environ["ARVADOS_KEEP_PROXY"] = "http://localhost:{}".format(port)
412     _setport('keepproxy', port)
413     _wait_until_port_listens(port)
414
415 def stop_keep_proxy():
416     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
417         return
418     kill_server_pid(_pidfile('keepproxy'), wait=0)
419
420 def run_arv_git_httpd():
421     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
422         return
423     stop_arv_git_httpd()
424
425     gitdir = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'git')
426     gitport = find_available_port()
427     env = os.environ.copy()
428     env.pop('ARVADOS_API_TOKEN', None)
429     agh = subprocess.Popen(
430         ['arv-git-httpd',
431          '-repo-root='+gitdir+'/test',
432          '-address=:'+str(gitport)],
433         env=env, stdin=open('/dev/null'), stdout=sys.stderr)
434     with open(_pidfile('arv-git-httpd'), 'w') as f:
435         f.write(str(agh.pid))
436     _setport('arv-git-httpd', gitport)
437     _wait_until_port_listens(gitport)
438
439 def stop_arv_git_httpd():
440     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
441         return
442     kill_server_pid(_pidfile('arv-git-httpd'), wait=0)
443
444 def run_keep_web():
445     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
446         return
447     stop_keep_web()
448
449     keepwebport = find_available_port()
450     env = os.environ.copy()
451     env.pop('ARVADOS_API_TOKEN', None)
452     keepweb = subprocess.Popen(
453         ['keep-web',
454          '-anonymous-token='+fixture('api_client_authorizations')['anonymous']['api_token'],
455          '-attachment-only-host=localhost:'+str(keepwebport),
456          '-address=:'+str(keepwebport)],
457         env=env, stdin=open('/dev/null'), stdout=sys.stderr)
458     with open(_pidfile('keep-web'), 'w') as f:
459         f.write(str(keepweb.pid))
460     _setport('keep-web', keepwebport)
461     _wait_until_port_listens(keepwebport)
462
463 def stop_keep_web():
464     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
465         return
466     kill_server_pid(_pidfile('keep-web'), wait=0)
467
468 def run_nginx():
469     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
470         return
471     nginxconf = {}
472     nginxconf['KEEPWEBPORT'] = _getport('keep-web')
473     nginxconf['KEEPWEBSSLPORT'] = find_available_port()
474     nginxconf['KEEPPROXYPORT'] = _getport('keepproxy')
475     nginxconf['KEEPPROXYSSLPORT'] = find_available_port()
476     nginxconf['GITPORT'] = _getport('arv-git-httpd')
477     nginxconf['GITSSLPORT'] = find_available_port()
478     nginxconf['SSLCERT'] = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.pem')
479     nginxconf['SSLKEY'] = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.key')
480     nginxconf['ACCESSLOG'] = os.path.join(TEST_TMPDIR, 'nginx_access_log.fifo')
481
482     conftemplatefile = os.path.join(MY_DIRNAME, 'nginx.conf')
483     conffile = os.path.join(TEST_TMPDIR, 'nginx.conf')
484     with open(conffile, 'w') as f:
485         f.write(re.sub(
486             r'{{([A-Z]+)}}',
487             lambda match: str(nginxconf.get(match.group(1))),
488             open(conftemplatefile).read()))
489
490     env = os.environ.copy()
491     env['PATH'] = env['PATH']+':/sbin:/usr/sbin:/usr/local/sbin'
492
493     try:
494         os.remove(nginxconf['ACCESSLOG'])
495     except OSError as error:
496         if error.errno != errno.ENOENT:
497             raise
498
499     os.mkfifo(nginxconf['ACCESSLOG'], 0700)
500     nginx = subprocess.Popen(
501         ['nginx',
502          '-g', 'error_log stderr info;',
503          '-g', 'pid '+_pidfile('nginx')+';',
504          '-c', conffile],
505         env=env, stdin=open('/dev/null'), stdout=sys.stderr)
506     cat_access = subprocess.Popen(
507         ['cat', nginxconf['ACCESSLOG']],
508         stdout=sys.stderr)
509     _setport('keep-web-ssl', nginxconf['KEEPWEBSSLPORT'])
510     _setport('keepproxy-ssl', nginxconf['KEEPPROXYSSLPORT'])
511     _setport('arv-git-httpd-ssl', nginxconf['GITSSLPORT'])
512
513 def stop_nginx():
514     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
515         return
516     kill_server_pid(_pidfile('nginx'), wait=0)
517
518 def _pidfile(program):
519     return os.path.join(TEST_TMPDIR, program + '.pid')
520
521 def _portfile(program):
522     return os.path.join(TEST_TMPDIR, program + '.port')
523
524 def _setport(program, port):
525     with open(_portfile(program), 'w') as f:
526         f.write(str(port))
527
528 # Returns 9 if program is not up.
529 def _getport(program):
530     try:
531         return int(open(_portfile(program)).read())
532     except IOError:
533         return 9
534
535 def _apiconfig(key):
536     if _cached_config:
537         return _cached_config[key]
538     def _load(f, required=True):
539         fullpath = os.path.join(SERVICES_SRC_DIR, 'api', 'config', f)
540         if not required and not os.path.exists(fullpath):
541             return {}
542         return yaml.load(fullpath)
543     cdefault = _load('application.default.yml')
544     csite = _load('application.yml', required=False)
545     _cached_config = {}
546     for section in [cdefault.get('common',{}), cdefault.get('test',{}),
547                     csite.get('common',{}), csite.get('test',{})]:
548         _cached_config.update(section)
549     return _cached_config[key]
550
551 def fixture(fix):
552     '''load a fixture yaml file'''
553     with open(os.path.join(SERVICES_SRC_DIR, 'api', "test", "fixtures",
554                            fix + ".yml")) as f:
555         yaml_file = f.read()
556         try:
557           trim_index = yaml_file.index("# Test Helper trims the rest of the file")
558           yaml_file = yaml_file[0:trim_index]
559         except ValueError:
560           pass
561         return yaml.load(yaml_file)
562
563 def auth_token(token_name):
564     return fixture("api_client_authorizations")[token_name]["api_token"]
565
566 def authorize_with(token_name):
567     '''token_name is the symbolic name of the token from the api_client_authorizations fixture'''
568     arvados.config.settings()["ARVADOS_API_TOKEN"] = auth_token(token_name)
569     arvados.config.settings()["ARVADOS_API_HOST"] = os.environ.get("ARVADOS_API_HOST")
570     arvados.config.settings()["ARVADOS_API_HOST_INSECURE"] = "true"
571
572 class TestCaseWithServers(unittest.TestCase):
573     """TestCase to start and stop supporting Arvados servers.
574
575     Define any of MAIN_SERVER, KEEP_SERVER, and/or KEEP_PROXY_SERVER
576     class variables as a dictionary of keyword arguments.  If you do,
577     setUpClass will start the corresponding servers by passing these
578     keyword arguments to the run, run_keep, and/or run_keep_server
579     functions, respectively.  It will also set Arvados environment
580     variables to point to these servers appropriately.  If you don't
581     run a Keep or Keep proxy server, setUpClass will set up a
582     temporary directory for Keep local storage, and set it as
583     KEEP_LOCAL_STORE.
584
585     tearDownClass will stop any servers started, and restore the
586     original environment.
587     """
588     MAIN_SERVER = None
589     KEEP_SERVER = None
590     KEEP_PROXY_SERVER = None
591     KEEP_WEB_SERVER = None
592
593     @staticmethod
594     def _restore_dict(src, dest):
595         for key in dest.keys():
596             if key not in src:
597                 del dest[key]
598         dest.update(src)
599
600     @classmethod
601     def setUpClass(cls):
602         cls._orig_environ = os.environ.copy()
603         cls._orig_config = arvados.config.settings().copy()
604         cls._cleanup_funcs = []
605         os.environ.pop('ARVADOS_KEEP_PROXY', None)
606         os.environ.pop('ARVADOS_EXTERNAL_CLIENT', None)
607         for server_kwargs, start_func, stop_func in (
608                 (cls.MAIN_SERVER, run, reset),
609                 (cls.KEEP_SERVER, run_keep, stop_keep),
610                 (cls.KEEP_PROXY_SERVER, run_keep_proxy, stop_keep_proxy),
611                 (cls.KEEP_WEB_SERVER, run_keep_web, stop_keep_web)):
612             if server_kwargs is not None:
613                 start_func(**server_kwargs)
614                 cls._cleanup_funcs.append(stop_func)
615         if (cls.KEEP_SERVER is None) and (cls.KEEP_PROXY_SERVER is None):
616             cls.local_store = tempfile.mkdtemp()
617             os.environ['KEEP_LOCAL_STORE'] = cls.local_store
618             cls._cleanup_funcs.append(
619                 lambda: shutil.rmtree(cls.local_store, ignore_errors=True))
620         else:
621             os.environ.pop('KEEP_LOCAL_STORE', None)
622         arvados.config.initialize()
623
624     @classmethod
625     def tearDownClass(cls):
626         for clean_func in cls._cleanup_funcs:
627             clean_func()
628         cls._restore_dict(cls._orig_environ, os.environ)
629         cls._restore_dict(cls._orig_config, arvados.config.settings())
630
631
632 if __name__ == "__main__":
633     actions = [
634         'start', 'stop',
635         'start_keep', 'stop_keep',
636         'start_keep_proxy', 'stop_keep_proxy',
637         'start_keep-web', 'stop_keep-web',
638         'start_arv-git-httpd', 'stop_arv-git-httpd',
639         'start_nginx', 'stop_nginx',
640     ]
641     parser = argparse.ArgumentParser()
642     parser.add_argument('action', type=str, help="one of {}".format(actions))
643     parser.add_argument('--auth', type=str, metavar='FIXTURE_NAME', help='Print authorization info for given api_client_authorizations fixture')
644     parser.add_argument('--num-keep-servers', metavar='int', type=int, default=2, help="Number of keep servers desired")
645     parser.add_argument('--keep-enforce-permissions', action="store_true", help="Enforce keep permissions")
646
647     args = parser.parse_args()
648
649     if args.action not in actions:
650         print("Unrecognized action '{}'. Actions are: {}.".format(args.action, actions), file=sys.stderr)
651         sys.exit(1)
652     if args.action == 'start':
653         stop(force=('ARVADOS_TEST_API_HOST' not in os.environ))
654         run(leave_running_atexit=True)
655         host = os.environ['ARVADOS_API_HOST']
656         if args.auth is not None:
657             token = auth_token(args.auth)
658             print("export ARVADOS_API_TOKEN={}".format(pipes.quote(token)))
659             print("export ARVADOS_API_HOST={}".format(pipes.quote(host)))
660             print("export ARVADOS_API_HOST_INSECURE=true")
661         else:
662             print(host)
663     elif args.action == 'stop':
664         stop(force=('ARVADOS_TEST_API_HOST' not in os.environ))
665     elif args.action == 'start_keep':
666         run_keep(enforce_permissions=args.keep_enforce_permissions, num_servers=args.num_keep_servers)
667     elif args.action == 'stop_keep':
668         stop_keep()
669     elif args.action == 'start_keep_proxy':
670         run_keep_proxy()
671     elif args.action == 'stop_keep_proxy':
672         stop_keep_proxy()
673     elif args.action == 'start_arv-git-httpd':
674         run_arv_git_httpd()
675     elif args.action == 'stop_arv-git-httpd':
676         stop_arv_git_httpd()
677     elif args.action == 'start_keep-web':
678         run_keep_web()
679     elif args.action == 'stop_keep-web':
680         stop_keep_web()
681     elif args.action == 'start_nginx':
682         run_nginx()
683     elif args.action == 'stop_nginx':
684         stop_nginx()
685     else:
686         raise Exception("action recognized but not implemented!?")