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