3 from __future__ import print_function
22 MY_DIRNAME = os.path.dirname(os.path.realpath(__file__))
23 if __name__ == '__main__' and os.path.exists(
24 os.path.join(MY_DIRNAME, '..', 'arvados', '__init__.py')):
25 # We're being launched to support another test suite.
26 # Add the Python SDK source to the library path.
27 sys.path.insert(1, os.path.dirname(MY_DIRNAME))
32 ARVADOS_DIR = os.path.realpath(os.path.join(MY_DIRNAME, '../../..'))
33 SERVICES_SRC_DIR = os.path.join(ARVADOS_DIR, 'services')
34 SERVER_PID_PATH = 'tmp/pids/test-server.pid'
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']
40 TEST_TMPDIR = os.path.join(ARVADOS_DIR, 'tmp')
41 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)
69 def kill_server_pid(pidfile, wait=10, passenger_root=False):
70 # Must re-import modules in order to work during atexit
77 # First try to shut down nicely
78 restore_cwd = os.getcwd()
79 os.chdir(passenger_root)
81 'bundle', 'exec', 'passenger', 'stop', '--pid-file', pidfile])
85 with open(pidfile, 'r') as f:
86 server_pid = int(f.read())
88 if not passenger_root or timeout - now < wait / 2:
89 # Half timeout has elapsed. Start sending SIGTERM
90 os.kill(server_pid, signal.SIGTERM)
91 # Raise OSError if process has disappeared
92 os.getpgid(server_pid)
100 def find_available_port():
101 """Return an IPv4 port number that is not in use right now.
103 We assume whoever needs to use the returned port is able to reuse
104 a recently used port without waiting for TIME_WAIT (see
105 SO_REUSEADDR / SO_REUSEPORT).
107 Some opportunity for races here, but it's better than choosing
108 something at random and not checking at all. If all of our servers
109 (hey Passenger) knew that listening on port 0 was a thing, the OS
110 would take care of the races, and this wouldn't be needed at all.
113 sock = socket.socket()
114 sock.bind(('0.0.0.0', 0))
115 port = sock.getsockname()[1]
119 def _wait_until_port_listens(port, timeout=10):
120 """Wait for a process to start listening on the given port.
122 If nothing listens on the port within the specified timeout (given
123 in seconds), print a warning on stderr before returning.
126 subprocess.check_output(['which', 'lsof'])
127 except subprocess.CalledProcessError:
128 print("WARNING: No `lsof` -- cannot wait for port to listen. "+
129 "Sleeping 0.5 and hoping for the best.")
132 deadline = time.time() + timeout
133 while time.time() < deadline:
135 subprocess.check_output(
136 ['lsof', '-t', '-i', 'tcp:'+str(port)])
137 except subprocess.CalledProcessError:
142 "WARNING: Nothing is listening on port {} (waited {} seconds).".
143 format(port, timeout),
146 def run(leave_running_atexit=False):
147 """Ensure an API server is running, and ARVADOS_API_* env vars have
148 admin credentials for it.
150 If ARVADOS_TEST_API_HOST is set, a parent process has started a
151 test server for us to use: we just need to reset() it using the
154 If a previous call to run() started a new server process, and it
155 is still running, we just need to reset() it to fixture state and
158 If neither of those options work out, we'll really start a new
163 # Delete cached discovery document.
164 shutil.rmtree(arvados.http_cache('discovery'))
166 pid_file = os.path.join(SERVICES_SRC_DIR, 'api', SERVER_PID_PATH)
167 pid_file_ok = find_server_pid(pid_file, 0)
169 existing_api_host = os.environ.get('ARVADOS_TEST_API_HOST', my_api_host)
170 if existing_api_host and pid_file_ok:
171 if existing_api_host == my_api_host:
175 # Fall through to shutdown-and-start case.
178 # Server was provided by parent. Can't recover if it's
182 # Before trying to start up our own server, call stop() to avoid
183 # "Phusion Passenger Standalone is already running on PID 12345".
184 # (If we've gotten this far, ARVADOS_TEST_API_HOST isn't set, so
185 # we know the server is ours to kill.)
188 restore_cwd = os.getcwd()
189 api_src_dir = os.path.join(SERVICES_SRC_DIR, 'api')
190 os.chdir(api_src_dir)
192 # Either we haven't started a server of our own yet, or it has
193 # died, or we have lost our credentials, or something else is
194 # preventing us from calling reset(). Start a new one.
196 if not os.path.exists('tmp'):
199 if not os.path.exists('tmp/api'):
200 os.makedirs('tmp/api')
202 if not os.path.exists('tmp/logs'):
203 os.makedirs('tmp/logs')
205 if not os.path.exists('tmp/self-signed.pem'):
206 # We assume here that either passenger reports its listening
207 # address as https:/0.0.0.0:port/. If it reports "127.0.0.1"
208 # then the certificate won't match the host and reset() will
209 # fail certificate verification. If it reports "localhost",
210 # clients (notably Python SDK's websocket client) might
211 # resolve localhost as ::1 and then fail to connect.
212 subprocess.check_call([
213 'openssl', 'req', '-new', '-x509', '-nodes',
214 '-out', 'tmp/self-signed.pem',
215 '-keyout', 'tmp/self-signed.key',
217 '-subj', '/CN=0.0.0.0'],
220 # Install the git repository fixtures.
221 gitdir = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'git')
222 gittarball = os.path.join(SERVICES_SRC_DIR, 'api', 'test', 'test.git.tar')
223 if not os.path.isdir(gitdir):
225 subprocess.check_output(['tar', '-xC', gitdir, '-f', gittarball])
227 port = find_available_port()
228 env = os.environ.copy()
229 env['RAILS_ENV'] = 'test'
230 env['ARVADOS_WEBSOCKETS'] = 'yes'
231 env.pop('ARVADOS_TEST_API_HOST', None)
232 env.pop('ARVADOS_API_HOST', None)
233 env.pop('ARVADOS_API_HOST_INSECURE', None)
234 env.pop('ARVADOS_API_TOKEN', None)
235 start_msg = subprocess.check_output(
237 'passenger', 'start', '-d', '-p{}'.format(port),
238 '--pid-file', os.path.join(os.getcwd(), pid_file),
239 '--log-file', os.path.join(os.getcwd(), 'log/test.log'),
241 '--ssl-certificate', 'tmp/self-signed.pem',
242 '--ssl-certificate-key', 'tmp/self-signed.key'],
245 if not leave_running_atexit:
246 atexit.register(kill_server_pid, pid_file, passenger_root=api_src_dir)
248 match = re.search(r'Accessible via: https://(.*?)/', start_msg)
251 "Passenger did not report endpoint: {}".format(start_msg))
252 my_api_host = match.group(1)
253 os.environ['ARVADOS_API_HOST'] = my_api_host
255 # Make sure the server has written its pid file and started
256 # listening on its TCP port
257 find_server_pid(pid_file)
258 _wait_until_port_listens(port)
261 os.chdir(restore_cwd)
264 """Reset the test server to fixture state.
266 This resets the ARVADOS_TEST_API_HOST provided by a parent process
267 if any, otherwise the server started by run().
269 It also resets ARVADOS_* environment vars to point to the test
270 server with admin credentials.
272 existing_api_host = os.environ.get('ARVADOS_TEST_API_HOST', my_api_host)
273 token = auth_token('admin')
274 httpclient = httplib2.Http(ca_certs=os.path.join(
275 SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.pem'))
277 'https://{}/database/reset'.format(existing_api_host),
279 headers={'Authorization': 'OAuth2 {}'.format(token)})
280 os.environ['ARVADOS_API_HOST_INSECURE'] = 'true'
281 os.environ['ARVADOS_API_HOST'] = existing_api_host
282 os.environ['ARVADOS_API_TOKEN'] = token
284 def stop(force=False):
285 """Stop the API server, if one is running.
287 If force==False, kill it only if we started it ourselves. (This
288 supports the use case where a Python test suite calls run(), but
289 run() just uses the ARVADOS_TEST_API_HOST provided by the parent
290 process, and the test suite cleans up after itself by calling
291 stop(). In this case the test server provided by the parent
292 process should be left alone.)
294 If force==True, kill it even if we didn't start it
295 ourselves. (This supports the use case in __main__, where "run"
296 and "stop" happen in different processes.)
299 if force or my_api_host is not None:
300 kill_server_pid(os.path.join(SERVICES_SRC_DIR, 'api', SERVER_PID_PATH))
303 def _start_keep(n, keep_args):
304 keep0 = tempfile.mkdtemp()
305 port = find_available_port()
306 keep_cmd = ["keepstore",
307 "-volume={}".format(keep0),
308 "-listen=:{}".format(port),
309 "-pid="+_pidfile('keep{}'.format(n))]
311 for arg, val in keep_args.iteritems():
312 keep_cmd.append("{}={}".format(arg, val))
314 logf = open(os.path.join(TEST_TMPDIR, 'keep{}.log'.format(n)), 'a+')
315 kp0 = subprocess.Popen(
316 keep_cmd, stdin=open('/dev/null'), stdout=logf, stderr=logf, close_fds=True)
317 with open(_pidfile('keep{}'.format(n)), 'w') as f:
318 f.write(str(kp0.pid))
320 with open("{}/keep{}.volume".format(TEST_TMPDIR, n), 'w') as f:
323 _wait_until_port_listens(port)
327 def run_keep(blob_signing_key=None, enforce_permissions=False):
328 if keep_existing is None:
332 if not blob_signing_key:
333 blob_signing_key = 'zfhgfenhffzltr9dixws36j1yhksjoll2grmku38mi7yxd66h5j4q9w4jzanezacp8s6q0ro3hxakfye02152hncy6zml2ed0uc'
334 with open(os.path.join(TEST_TMPDIR, "keep.blob_signing_key"), "w") as f:
335 keep_args['-blob-signing-key-file'] = f.name
336 f.write(blob_signing_key)
337 if enforce_permissions:
338 keep_args['-enforce-permissions'] = 'true'
339 with open(os.path.join(TEST_TMPDIR, "keep.data-manager-token-file"), "w") as f:
340 keep_args['-data-manager-token-file'] = f.name
341 f.write(os.environ['ARVADOS_API_TOKEN'])
342 keep_args['-never-delete'] = 'false'
346 host=os.environ['ARVADOS_API_HOST'],
347 token=os.environ['ARVADOS_API_TOKEN'],
350 for d in api.keep_services().list().execute()['items']:
351 api.keep_services().delete(uuid=d['uuid']).execute()
352 for d in api.keep_disks().list().execute()['items']:
353 api.keep_disks().delete(uuid=d['uuid']).execute()
357 if keep_existing is not None:
360 for d in range(start_index, end_index):
361 port = _start_keep(d, keep_args)
362 svc = api.keep_services().create(body={'keep_service': {
363 'uuid': 'zzzzz-bi6l4-keepdisk{:07d}'.format(d),
364 'service_host': 'localhost',
365 'service_port': port,
366 'service_type': 'disk',
367 'service_ssl_flag': False,
369 api.keep_disks().create(body={
370 'keep_disk': {'keep_service_uuid': svc['uuid'] }
374 kill_server_pid(_pidfile('keep{}'.format(n)), 0)
375 if os.path.exists("{}/keep{}.volume".format(TEST_TMPDIR, n)):
376 with open("{}/keep{}.volume".format(TEST_TMPDIR, n), 'r') as r:
377 shutil.rmtree(r.read(), True)
378 os.unlink("{}/keep{}.volume".format(TEST_TMPDIR, n))
379 if os.path.exists(os.path.join(TEST_TMPDIR, "keep.blob_signing_key")):
380 os.remove(os.path.join(TEST_TMPDIR, "keep.blob_signing_key"))
385 # We may have created an additional keep servers when keep_existing is used
388 def run_keep_proxy():
389 if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
393 admin_token = auth_token('admin')
394 port = find_available_port()
395 env = os.environ.copy()
396 env['ARVADOS_API_TOKEN'] = admin_token
397 kp = subprocess.Popen(
399 '-pid='+_pidfile('keepproxy'),
400 '-listen=:{}'.format(port)],
401 env=env, stdin=open('/dev/null'), stdout=sys.stderr)
405 host=os.environ['ARVADOS_API_HOST'],
408 for d in api.keep_services().list(
409 filters=[['service_type','=','proxy']]).execute()['items']:
410 api.keep_services().delete(uuid=d['uuid']).execute()
411 api.keep_services().create(body={'keep_service': {
412 'service_host': 'localhost',
413 'service_port': port,
414 'service_type': 'proxy',
415 'service_ssl_flag': False,
417 os.environ["ARVADOS_KEEP_PROXY"] = "http://localhost:{}".format(port)
418 _setport('keepproxy', port)
419 _wait_until_port_listens(port)
421 def stop_keep_proxy():
422 if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
424 kill_server_pid(_pidfile('keepproxy'), wait=0)
426 def run_arv_git_httpd():
427 if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
431 gitdir = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'git')
432 gitport = find_available_port()
433 env = os.environ.copy()
434 env.pop('ARVADOS_API_TOKEN', None)
435 agh = subprocess.Popen(
437 '-repo-root='+gitdir+'/test',
438 '-address=:'+str(gitport)],
439 env=env, stdin=open('/dev/null'), stdout=sys.stderr)
440 with open(_pidfile('arv-git-httpd'), 'w') as f:
441 f.write(str(agh.pid))
442 _setport('arv-git-httpd', gitport)
443 _wait_until_port_listens(gitport)
445 def stop_arv_git_httpd():
446 if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
448 kill_server_pid(_pidfile('arv-git-httpd'), wait=0)
451 if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
454 nginxconf['KEEPPROXYPORT'] = _getport('keepproxy')
455 nginxconf['KEEPPROXYSSLPORT'] = find_available_port()
456 nginxconf['GITPORT'] = _getport('arv-git-httpd')
457 nginxconf['GITSSLPORT'] = find_available_port()
458 nginxconf['SSLCERT'] = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.pem')
459 nginxconf['SSLKEY'] = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.key')
461 conftemplatefile = os.path.join(MY_DIRNAME, 'nginx.conf')
462 conffile = os.path.join(TEST_TMPDIR, 'nginx.conf')
463 with open(conffile, 'w') as f:
466 lambda match: str(nginxconf.get(match.group(1))),
467 open(conftemplatefile).read()))
469 env = os.environ.copy()
470 env['PATH'] = env['PATH']+':/sbin:/usr/sbin:/usr/local/sbin'
471 nginx = subprocess.Popen(
473 '-g', 'error_log stderr info;',
474 '-g', 'pid '+_pidfile('nginx')+';',
476 env=env, stdin=open('/dev/null'), stdout=sys.stderr)
477 _setport('keepproxy-ssl', nginxconf['KEEPPROXYSSLPORT'])
478 _setport('arv-git-httpd-ssl', nginxconf['GITSSLPORT'])
481 if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
483 kill_server_pid(_pidfile('nginx'), wait=0)
485 def _pidfile(program):
486 return os.path.join(TEST_TMPDIR, program + '.pid')
488 def _portfile(program):
489 return os.path.join(TEST_TMPDIR, program + '.port')
491 def _setport(program, port):
492 with open(_portfile(program), 'w') as f:
495 # Returns 9 if program is not up.
496 def _getport(program):
498 return int(open(_portfile(program)).read())
504 return _cached_config[key]
505 def _load(f, required=True):
506 fullpath = os.path.join(SERVICES_SRC_DIR, 'api', 'config', f)
507 if not required and not os.path.exists(fullpath):
509 return yaml.load(fullpath)
510 cdefault = _load('application.default.yml')
511 csite = _load('application.yml', required=False)
513 for section in [cdefault.get('common',{}), cdefault.get('test',{}),
514 csite.get('common',{}), csite.get('test',{})]:
515 _cached_config.update(section)
516 return _cached_config[key]
519 '''load a fixture yaml file'''
520 with open(os.path.join(SERVICES_SRC_DIR, 'api', "test", "fixtures",
524 trim_index = yaml_file.index("# Test Helper trims the rest of the file")
525 yaml_file = yaml_file[0:trim_index]
528 return yaml.load(yaml_file)
530 def auth_token(token_name):
531 return fixture("api_client_authorizations")[token_name]["api_token"]
533 def authorize_with(token_name):
534 '''token_name is the symbolic name of the token from the api_client_authorizations fixture'''
535 arvados.config.settings()["ARVADOS_API_TOKEN"] = auth_token(token_name)
536 arvados.config.settings()["ARVADOS_API_HOST"] = os.environ.get("ARVADOS_API_HOST")
537 arvados.config.settings()["ARVADOS_API_HOST_INSECURE"] = "true"
539 class TestCaseWithServers(unittest.TestCase):
540 """TestCase to start and stop supporting Arvados servers.
542 Define any of MAIN_SERVER, KEEP_SERVER, and/or KEEP_PROXY_SERVER
543 class variables as a dictionary of keyword arguments. If you do,
544 setUpClass will start the corresponding servers by passing these
545 keyword arguments to the run, run_keep, and/or run_keep_server
546 functions, respectively. It will also set Arvados environment
547 variables to point to these servers appropriately. If you don't
548 run a Keep or Keep proxy server, setUpClass will set up a
549 temporary directory for Keep local storage, and set it as
552 tearDownClass will stop any servers started, and restore the
553 original environment.
557 KEEP_PROXY_SERVER = None
560 def _restore_dict(src, dest):
561 for key in dest.keys():
568 cls._orig_environ = os.environ.copy()
569 cls._orig_config = arvados.config.settings().copy()
570 cls._cleanup_funcs = []
571 os.environ.pop('ARVADOS_KEEP_PROXY', None)
572 os.environ.pop('ARVADOS_EXTERNAL_CLIENT', None)
573 for server_kwargs, start_func, stop_func in (
574 (cls.MAIN_SERVER, run, reset),
575 (cls.KEEP_SERVER, run_keep, stop_keep),
576 (cls.KEEP_PROXY_SERVER, run_keep_proxy, stop_keep_proxy)):
577 if server_kwargs is not None:
578 start_func(**server_kwargs)
579 cls._cleanup_funcs.append(stop_func)
580 if (cls.KEEP_SERVER is None) and (cls.KEEP_PROXY_SERVER is None):
581 cls.local_store = tempfile.mkdtemp()
582 os.environ['KEEP_LOCAL_STORE'] = cls.local_store
583 cls._cleanup_funcs.append(
584 lambda: shutil.rmtree(cls.local_store, ignore_errors=True))
586 os.environ.pop('KEEP_LOCAL_STORE', None)
587 arvados.config.initialize()
590 def tearDownClass(cls):
591 for clean_func in cls._cleanup_funcs:
593 cls._restore_dict(cls._orig_environ, os.environ)
594 cls._restore_dict(cls._orig_config, arvados.config.settings())
597 if __name__ == "__main__":
600 'start_keep', 'stop_keep',
601 'start_keep_proxy', 'stop_keep_proxy',
602 'start_arv-git-httpd', 'stop_arv-git-httpd',
603 'start_nginx', 'stop_nginx',
605 parser = argparse.ArgumentParser()
606 parser.add_argument('action', type=str, help="one of {}".format(actions))
607 parser.add_argument('--auth', type=str, metavar='FIXTURE_NAME', help='Print authorization info for given api_client_authorizations fixture')
608 parser.add_argument('--keep_existing', type=str, help="Used to add additional keep servers, without terminating existing servers")
609 args = parser.parse_args()
611 keep_existing = args.keep_existing
613 if args.action not in actions:
614 print("Unrecognized action '{}'. Actions are: {}.".format(args.action, actions), file=sys.stderr)
616 if args.action == 'start':
617 stop(force=('ARVADOS_TEST_API_HOST' not in os.environ))
618 run(leave_running_atexit=True)
619 host = os.environ['ARVADOS_API_HOST']
620 if args.auth is not None:
621 token = auth_token(args.auth)
622 print("export ARVADOS_API_TOKEN={}".format(pipes.quote(token)))
623 print("export ARVADOS_API_HOST={}".format(pipes.quote(host)))
624 print("export ARVADOS_API_HOST_INSECURE=true")
627 elif args.action == 'stop':
628 stop(force=('ARVADOS_TEST_API_HOST' not in os.environ))
629 elif args.action == 'start_keep':
631 elif args.action == 'stop_keep':
633 elif args.action == 'start_keep_proxy':
635 elif args.action == 'stop_keep_proxy':
637 elif args.action == 'start_arv-git-httpd':
639 elif args.action == 'stop_arv-git-httpd':
641 elif args.action == 'start_nginx':
643 elif args.action == 'stop_nginx':
646 raise Exception("action recognized but not implemented!?")