3 from __future__ import print_function
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))
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']
41 TEST_TMPDIR = os.path.join(ARVADOS_DIR, 'tmp')
42 if not os.path.exists(TEST_TMPDIR):
48 def find_server_pid(PID_PATH, wait=10):
52 while (not good_pid) and (now <= timeout):
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:
67 def kill_server_pid(pidfile, wait=10, passenger_root=False):
68 # Must re-import modules in order to work during atexit
75 # First try to shut down nicely
76 restore_cwd = os.getcwd()
77 os.chdir(passenger_root)
79 'bundle', 'exec', 'passenger', 'stop', '--pid-file', pidfile])
83 with open(pidfile, 'r') as f:
84 server_pid = int(f.read())
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)
93 except EnvironmentError:
96 def find_available_port():
97 """Return an IPv4 port number that is not in use right now.
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).
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.
109 sock = socket.socket()
110 sock.bind(('0.0.0.0', 0))
111 port = sock.getsockname()[1]
115 def _wait_until_port_listens(port, timeout=10):
116 """Wait for a process to start listening on the given port.
118 If nothing listens on the port within the specified timeout (given
119 in seconds), print a warning on stderr before returning.
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.")
128 deadline = time.time() + timeout
129 while time.time() < deadline:
131 subprocess.check_output(
132 ['lsof', '-t', '-i', 'tcp:'+str(port)])
133 except subprocess.CalledProcessError:
138 "WARNING: Nothing is listening on port {} (waited {} seconds).".
139 format(port, timeout),
142 def _fifo2stderr(label):
143 """Create a fifo, and copy it to stderr, prepending label to each line.
145 Return value is the path to the new FIFO.
147 +label+ should contain only alphanumerics: it is also used as part
148 of the FIFO filename.
150 fifo = os.path.join(TEST_TMPDIR, label+'.fifo')
153 except OSError as error:
154 if error.errno != errno.ENOENT:
156 os.mkfifo(fifo, 0700)
158 ['sed', '-e', 's/^/['+label+'] /', fifo],
162 def run(leave_running_atexit=False):
163 """Ensure an API server is running, and ARVADOS_API_* env vars have
164 admin credentials for it.
166 If ARVADOS_TEST_API_HOST is set, a parent process has started a
167 test server for us to use: we just need to reset() it using the
170 If a previous call to run() started a new server process, and it
171 is still running, we just need to reset() it to fixture state and
174 If neither of those options work out, we'll really start a new
179 # Delete cached discovery document.
180 shutil.rmtree(arvados.http_cache('discovery'))
182 pid_file = os.path.join(SERVICES_SRC_DIR, 'api', SERVER_PID_PATH)
183 pid_file_ok = find_server_pid(pid_file, 0)
185 existing_api_host = os.environ.get('ARVADOS_TEST_API_HOST', my_api_host)
186 if existing_api_host and pid_file_ok:
187 if existing_api_host == my_api_host:
191 # Fall through to shutdown-and-start case.
194 # Server was provided by parent. Can't recover if it's
198 # Before trying to start up our own server, call stop() to avoid
199 # "Phusion Passenger Standalone is already running on PID 12345".
200 # (If we've gotten this far, ARVADOS_TEST_API_HOST isn't set, so
201 # we know the server is ours to kill.)
204 restore_cwd = os.getcwd()
205 api_src_dir = os.path.join(SERVICES_SRC_DIR, 'api')
206 os.chdir(api_src_dir)
208 # Either we haven't started a server of our own yet, or it has
209 # died, or we have lost our credentials, or something else is
210 # preventing us from calling reset(). Start a new one.
212 if not os.path.exists('tmp'):
215 if not os.path.exists('tmp/api'):
216 os.makedirs('tmp/api')
218 if not os.path.exists('tmp/logs'):
219 os.makedirs('tmp/logs')
221 if not os.path.exists('tmp/self-signed.pem'):
222 # We assume here that either passenger reports its listening
223 # address as https:/0.0.0.0:port/. If it reports "127.0.0.1"
224 # then the certificate won't match the host and reset() will
225 # fail certificate verification. If it reports "localhost",
226 # clients (notably Python SDK's websocket client) might
227 # resolve localhost as ::1 and then fail to connect.
228 subprocess.check_call([
229 'openssl', 'req', '-new', '-x509', '-nodes',
230 '-out', 'tmp/self-signed.pem',
231 '-keyout', 'tmp/self-signed.key',
233 '-subj', '/CN=0.0.0.0'],
236 # Install the git repository fixtures.
237 gitdir = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'git')
238 gittarball = os.path.join(SERVICES_SRC_DIR, 'api', 'test', 'test.git.tar')
239 if not os.path.isdir(gitdir):
241 subprocess.check_output(['tar', '-xC', gitdir, '-f', gittarball])
243 port = find_available_port()
244 env = os.environ.copy()
245 env['RAILS_ENV'] = 'test'
246 env['ARVADOS_WEBSOCKETS'] = 'yes'
247 env.pop('ARVADOS_TEST_API_HOST', None)
248 env.pop('ARVADOS_API_HOST', None)
249 env.pop('ARVADOS_API_HOST_INSECURE', None)
250 env.pop('ARVADOS_API_TOKEN', None)
251 start_msg = subprocess.check_output(
253 'passenger', 'start', '-d', '-p{}'.format(port),
254 '--pid-file', os.path.join(os.getcwd(), pid_file),
255 '--log-file', os.path.join(os.getcwd(), 'log/test.log'),
257 '--ssl-certificate', 'tmp/self-signed.pem',
258 '--ssl-certificate-key', 'tmp/self-signed.key'],
261 if not leave_running_atexit:
262 atexit.register(kill_server_pid, pid_file, passenger_root=api_src_dir)
264 match = re.search(r'Accessible via: https://(.*?)/', start_msg)
267 "Passenger did not report endpoint: {}".format(start_msg))
268 my_api_host = match.group(1)
269 os.environ['ARVADOS_API_HOST'] = my_api_host
271 # Make sure the server has written its pid file and started
272 # listening on its TCP port
273 find_server_pid(pid_file)
274 _wait_until_port_listens(port)
277 os.chdir(restore_cwd)
280 """Reset the test server to fixture state.
282 This resets the ARVADOS_TEST_API_HOST provided by a parent process
283 if any, otherwise the server started by run().
285 It also resets ARVADOS_* environment vars to point to the test
286 server with admin credentials.
288 existing_api_host = os.environ.get('ARVADOS_TEST_API_HOST', my_api_host)
289 token = auth_token('admin')
290 httpclient = httplib2.Http(ca_certs=os.path.join(
291 SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.pem'))
293 'https://{}/database/reset'.format(existing_api_host),
295 headers={'Authorization': 'OAuth2 {}'.format(token)})
296 os.environ['ARVADOS_API_HOST_INSECURE'] = 'true'
297 os.environ['ARVADOS_API_HOST'] = existing_api_host
298 os.environ['ARVADOS_API_TOKEN'] = token
300 def stop(force=False):
301 """Stop the API server, if one is running.
303 If force==False, kill it only if we started it ourselves. (This
304 supports the use case where a Python test suite calls run(), but
305 run() just uses the ARVADOS_TEST_API_HOST provided by the parent
306 process, and the test suite cleans up after itself by calling
307 stop(). In this case the test server provided by the parent
308 process should be left alone.)
310 If force==True, kill it even if we didn't start it
311 ourselves. (This supports the use case in __main__, where "run"
312 and "stop" happen in different processes.)
315 if force or my_api_host is not None:
316 kill_server_pid(os.path.join(SERVICES_SRC_DIR, 'api', SERVER_PID_PATH))
319 def _start_keep(n, keep_args):
320 keep0 = tempfile.mkdtemp()
321 port = find_available_port()
322 keep_cmd = ["keepstore",
323 "-volume={}".format(keep0),
324 "-listen=:{}".format(port),
325 "-pid="+_pidfile('keep{}'.format(n))]
327 for arg, val in keep_args.iteritems():
328 keep_cmd.append("{}={}".format(arg, val))
330 logf = open(_fifo2stderr('keep{}'.format(n)), 'w')
331 kp0 = subprocess.Popen(
332 keep_cmd, stdin=open('/dev/null'), stdout=logf, stderr=logf, close_fds=True)
334 with open(_pidfile('keep{}'.format(n)), 'w') as f:
335 f.write(str(kp0.pid))
337 with open("{}/keep{}.volume".format(TEST_TMPDIR, n), 'w') as f:
340 _wait_until_port_listens(port)
344 def run_keep(blob_signing_key=None, enforce_permissions=False, num_servers=2):
345 stop_keep(num_servers)
348 if not blob_signing_key:
349 blob_signing_key = 'zfhgfenhffzltr9dixws36j1yhksjoll2grmku38mi7yxd66h5j4q9w4jzanezacp8s6q0ro3hxakfye02152hncy6zml2ed0uc'
350 with open(os.path.join(TEST_TMPDIR, "keep.blob_signing_key"), "w") as f:
351 keep_args['-blob-signing-key-file'] = f.name
352 f.write(blob_signing_key)
353 if enforce_permissions:
354 keep_args['-enforce-permissions'] = 'true'
355 with open(os.path.join(TEST_TMPDIR, "keep.data-manager-token-file"), "w") as f:
356 keep_args['-data-manager-token-file'] = f.name
357 f.write(auth_token('data_manager'))
358 keep_args['-never-delete'] = 'false'
362 host=os.environ['ARVADOS_API_HOST'],
363 token=os.environ['ARVADOS_API_TOKEN'],
366 for d in api.keep_services().list(filters=[['service_type','=','disk']]).execute()['items']:
367 api.keep_services().delete(uuid=d['uuid']).execute()
368 for d in api.keep_disks().list().execute()['items']:
369 api.keep_disks().delete(uuid=d['uuid']).execute()
371 for d in range(0, num_servers):
372 port = _start_keep(d, keep_args)
373 svc = api.keep_services().create(body={'keep_service': {
374 'uuid': 'zzzzz-bi6l4-keepdisk{:07d}'.format(d),
375 'service_host': 'localhost',
376 'service_port': port,
377 'service_type': 'disk',
378 'service_ssl_flag': False,
380 api.keep_disks().create(body={
381 'keep_disk': {'keep_service_uuid': svc['uuid'] }
384 # If keepproxy is running, send SIGHUP to make it discover the new
385 # keepstore services.
386 proxypidfile = _pidfile('keepproxy')
387 if os.path.exists(proxypidfile):
388 os.kill(int(open(proxypidfile).read()), signal.SIGHUP)
391 kill_server_pid(_pidfile('keep{}'.format(n)), 0)
392 if os.path.exists("{}/keep{}.volume".format(TEST_TMPDIR, n)):
393 with open("{}/keep{}.volume".format(TEST_TMPDIR, n), 'r') as r:
394 shutil.rmtree(r.read(), True)
395 os.unlink("{}/keep{}.volume".format(TEST_TMPDIR, n))
396 if os.path.exists(os.path.join(TEST_TMPDIR, "keep.blob_signing_key")):
397 os.remove(os.path.join(TEST_TMPDIR, "keep.blob_signing_key"))
399 def stop_keep(num_servers=2):
400 for n in range(0, num_servers):
403 def run_keep_proxy():
404 if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
408 port = find_available_port()
409 env = os.environ.copy()
410 env['ARVADOS_API_TOKEN'] = auth_token('anonymous')
411 logf = open(_fifo2stderr('keepproxy'), 'w')
412 kp = subprocess.Popen(
414 '-pid='+_pidfile('keepproxy'),
415 '-listen=:{}'.format(port)],
416 env=env, stdin=open('/dev/null'), stdout=logf, stderr=logf, close_fds=True)
420 host=os.environ['ARVADOS_API_HOST'],
421 token=auth_token('admin'),
423 for d in api.keep_services().list(
424 filters=[['service_type','=','proxy']]).execute()['items']:
425 api.keep_services().delete(uuid=d['uuid']).execute()
426 api.keep_services().create(body={'keep_service': {
427 'service_host': 'localhost',
428 'service_port': port,
429 'service_type': 'proxy',
430 'service_ssl_flag': False,
432 os.environ["ARVADOS_KEEP_PROXY"] = "http://localhost:{}".format(port)
433 _setport('keepproxy', port)
434 _wait_until_port_listens(port)
436 def stop_keep_proxy():
437 if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
439 kill_server_pid(_pidfile('keepproxy'), wait=0)
441 def run_arv_git_httpd():
442 if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
446 gitdir = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'git')
447 gitport = find_available_port()
448 env = os.environ.copy()
449 env.pop('ARVADOS_API_TOKEN', None)
450 logf = open(_fifo2stderr('arv-git-httpd'), 'w')
451 agh = subprocess.Popen(
453 '-repo-root='+gitdir+'/test',
454 '-address=:'+str(gitport)],
455 env=env, stdin=open('/dev/null'), stdout=logf, stderr=logf)
456 with open(_pidfile('arv-git-httpd'), 'w') as f:
457 f.write(str(agh.pid))
458 _setport('arv-git-httpd', gitport)
459 _wait_until_port_listens(gitport)
461 def stop_arv_git_httpd():
462 if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
464 kill_server_pid(_pidfile('arv-git-httpd'), wait=0)
467 if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
471 keepwebport = find_available_port()
472 env = os.environ.copy()
473 env['ARVADOS_API_TOKEN'] = auth_token('anonymous')
474 logf = open(_fifo2stderr('keep-web'), 'w')
475 keepweb = subprocess.Popen(
478 '-attachment-only-host=localhost:'+str(keepwebport),
479 '-listen=:'+str(keepwebport)],
480 env=env, stdin=open('/dev/null'), stdout=logf, stderr=logf)
481 with open(_pidfile('keep-web'), 'w') as f:
482 f.write(str(keepweb.pid))
483 _setport('keep-web', keepwebport)
484 _wait_until_port_listens(keepwebport)
487 if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
489 kill_server_pid(_pidfile('keep-web'), wait=0)
492 if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
495 nginxconf['KEEPWEBPORT'] = _getport('keep-web')
496 nginxconf['KEEPWEBSSLPORT'] = find_available_port()
497 nginxconf['KEEPPROXYPORT'] = _getport('keepproxy')
498 nginxconf['KEEPPROXYSSLPORT'] = find_available_port()
499 nginxconf['GITPORT'] = _getport('arv-git-httpd')
500 nginxconf['GITSSLPORT'] = find_available_port()
501 nginxconf['SSLCERT'] = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.pem')
502 nginxconf['SSLKEY'] = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.key')
503 nginxconf['ACCESSLOG'] = _fifo2stderr('nginx_access_log')
505 conftemplatefile = os.path.join(MY_DIRNAME, 'nginx.conf')
506 conffile = os.path.join(TEST_TMPDIR, 'nginx.conf')
507 with open(conffile, 'w') as f:
510 lambda match: str(nginxconf.get(match.group(1))),
511 open(conftemplatefile).read()))
513 env = os.environ.copy()
514 env['PATH'] = env['PATH']+':/sbin:/usr/sbin:/usr/local/sbin'
516 nginx = subprocess.Popen(
518 '-g', 'error_log stderr info;',
519 '-g', 'pid '+_pidfile('nginx')+';',
521 env=env, stdin=open('/dev/null'), stdout=sys.stderr)
522 _setport('keep-web-ssl', nginxconf['KEEPWEBSSLPORT'])
523 _setport('keepproxy-ssl', nginxconf['KEEPPROXYSSLPORT'])
524 _setport('arv-git-httpd-ssl', nginxconf['GITSSLPORT'])
527 if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
529 kill_server_pid(_pidfile('nginx'), wait=0)
531 def _pidfile(program):
532 return os.path.join(TEST_TMPDIR, program + '.pid')
534 def _portfile(program):
535 return os.path.join(TEST_TMPDIR, program + '.port')
537 def _setport(program, port):
538 with open(_portfile(program), 'w') as f:
541 # Returns 9 if program is not up.
542 def _getport(program):
544 return int(open(_portfile(program)).read())
550 return _cached_config[key]
551 def _load(f, required=True):
552 fullpath = os.path.join(SERVICES_SRC_DIR, 'api', 'config', f)
553 if not required and not os.path.exists(fullpath):
555 return yaml.load(fullpath)
556 cdefault = _load('application.default.yml')
557 csite = _load('application.yml', required=False)
559 for section in [cdefault.get('common',{}), cdefault.get('test',{}),
560 csite.get('common',{}), csite.get('test',{})]:
561 _cached_config.update(section)
562 return _cached_config[key]
565 '''load a fixture yaml file'''
566 with open(os.path.join(SERVICES_SRC_DIR, 'api', "test", "fixtures",
570 trim_index = yaml_file.index("# Test Helper trims the rest of the file")
571 yaml_file = yaml_file[0:trim_index]
574 return yaml.load(yaml_file)
576 def auth_token(token_name):
577 return fixture("api_client_authorizations")[token_name]["api_token"]
579 def authorize_with(token_name):
580 '''token_name is the symbolic name of the token from the api_client_authorizations fixture'''
581 arvados.config.settings()["ARVADOS_API_TOKEN"] = auth_token(token_name)
582 arvados.config.settings()["ARVADOS_API_HOST"] = os.environ.get("ARVADOS_API_HOST")
583 arvados.config.settings()["ARVADOS_API_HOST_INSECURE"] = "true"
585 class TestCaseWithServers(unittest.TestCase):
586 """TestCase to start and stop supporting Arvados servers.
588 Define any of MAIN_SERVER, KEEP_SERVER, and/or KEEP_PROXY_SERVER
589 class variables as a dictionary of keyword arguments. If you do,
590 setUpClass will start the corresponding servers by passing these
591 keyword arguments to the run, run_keep, and/or run_keep_server
592 functions, respectively. It will also set Arvados environment
593 variables to point to these servers appropriately. If you don't
594 run a Keep or Keep proxy server, setUpClass will set up a
595 temporary directory for Keep local storage, and set it as
598 tearDownClass will stop any servers started, and restore the
599 original environment.
603 KEEP_PROXY_SERVER = None
604 KEEP_WEB_SERVER = None
607 def _restore_dict(src, dest):
608 for key in dest.keys():
615 cls._orig_environ = os.environ.copy()
616 cls._orig_config = arvados.config.settings().copy()
617 cls._cleanup_funcs = []
618 os.environ.pop('ARVADOS_KEEP_PROXY', None)
619 os.environ.pop('ARVADOS_EXTERNAL_CLIENT', None)
620 for server_kwargs, start_func, stop_func in (
621 (cls.MAIN_SERVER, run, reset),
622 (cls.KEEP_SERVER, run_keep, stop_keep),
623 (cls.KEEP_PROXY_SERVER, run_keep_proxy, stop_keep_proxy),
624 (cls.KEEP_WEB_SERVER, run_keep_web, stop_keep_web)):
625 if server_kwargs is not None:
626 start_func(**server_kwargs)
627 cls._cleanup_funcs.append(stop_func)
628 if (cls.KEEP_SERVER is None) and (cls.KEEP_PROXY_SERVER is None):
629 cls.local_store = tempfile.mkdtemp()
630 os.environ['KEEP_LOCAL_STORE'] = cls.local_store
631 cls._cleanup_funcs.append(
632 lambda: shutil.rmtree(cls.local_store, ignore_errors=True))
634 os.environ.pop('KEEP_LOCAL_STORE', None)
635 arvados.config.initialize()
638 def tearDownClass(cls):
639 for clean_func in cls._cleanup_funcs:
641 cls._restore_dict(cls._orig_environ, os.environ)
642 cls._restore_dict(cls._orig_config, arvados.config.settings())
645 if __name__ == "__main__":
648 'start_keep', 'stop_keep',
649 'start_keep_proxy', 'stop_keep_proxy',
650 'start_keep-web', 'stop_keep-web',
651 'start_arv-git-httpd', 'stop_arv-git-httpd',
652 'start_nginx', 'stop_nginx',
654 parser = argparse.ArgumentParser()
655 parser.add_argument('action', type=str, help="one of {}".format(actions))
656 parser.add_argument('--auth', type=str, metavar='FIXTURE_NAME', help='Print authorization info for given api_client_authorizations fixture')
657 parser.add_argument('--num-keep-servers', metavar='int', type=int, default=2, help="Number of keep servers desired")
658 parser.add_argument('--keep-enforce-permissions', action="store_true", help="Enforce keep permissions")
660 args = parser.parse_args()
662 if args.action not in actions:
663 print("Unrecognized action '{}'. Actions are: {}.".format(args.action, actions), file=sys.stderr)
665 if args.action == 'start':
666 stop(force=('ARVADOS_TEST_API_HOST' not in os.environ))
667 run(leave_running_atexit=True)
668 host = os.environ['ARVADOS_API_HOST']
669 if args.auth is not None:
670 token = auth_token(args.auth)
671 print("export ARVADOS_API_TOKEN={}".format(pipes.quote(token)))
672 print("export ARVADOS_API_HOST={}".format(pipes.quote(host)))
673 print("export ARVADOS_API_HOST_INSECURE=true")
676 elif args.action == 'stop':
677 stop(force=('ARVADOS_TEST_API_HOST' not in os.environ))
678 elif args.action == 'start_keep':
679 run_keep(enforce_permissions=args.keep_enforce_permissions, num_servers=args.num_keep_servers)
680 elif args.action == 'stop_keep':
682 elif args.action == 'start_keep_proxy':
684 elif args.action == 'stop_keep_proxy':
686 elif args.action == 'start_arv-git-httpd':
688 elif args.action == 'stop_arv-git-httpd':
690 elif args.action == 'start_keep-web':
692 elif args.action == 'stop_keep-web':
694 elif args.action == 'start_nginx':
696 elif args.action == 'stop_nginx':
699 raise Exception("action recognized but not implemented!?")