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