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):
47 def find_server_pid(PID_PATH, wait=10):
51 while (not good_pid) and (now <= timeout):
54 with open(PID_PATH, 'r') as f:
55 server_pid = int(f.read())
56 good_pid = (os.kill(server_pid, 0) is None)
68 def kill_server_pid(pidfile, wait=10, passenger_root=False):
69 # Must re-import modules in order to work during atexit
76 # First try to shut down nicely
77 restore_cwd = os.getcwd()
78 os.chdir(passenger_root)
80 'bundle', 'exec', 'passenger', 'stop', '--pid-file', pidfile])
84 with open(pidfile, 'r') as f:
85 server_pid = int(f.read())
87 if not passenger_root or timeout - now < wait / 2:
88 # Half timeout has elapsed. Start sending SIGTERM
89 os.kill(server_pid, signal.SIGTERM)
90 # Raise OSError if process has disappeared
91 os.getpgid(server_pid)
99 def find_available_port():
100 """Return an IPv4 port number that is not in use right now.
102 We assume whoever needs to use the returned port is able to reuse
103 a recently used port without waiting for TIME_WAIT (see
104 SO_REUSEADDR / SO_REUSEPORT).
106 Some opportunity for races here, but it's better than choosing
107 something at random and not checking at all. If all of our servers
108 (hey Passenger) knew that listening on port 0 was a thing, the OS
109 would take care of the races, and this wouldn't be needed at all.
112 sock = socket.socket()
113 sock.bind(('0.0.0.0', 0))
114 port = sock.getsockname()[1]
118 def run(leave_running_atexit=False):
119 """Ensure an API server is running, and ARVADOS_API_* env vars have
120 admin credentials for it.
122 If ARVADOS_TEST_API_HOST is set, a parent process has started a
123 test server for us to use: we just need to reset() it using the
126 If a previous call to run() started a new server process, and it
127 is still running, we just need to reset() it to fixture state and
130 If neither of those options work out, we'll really start a new
135 # Delete cached discovery document.
136 shutil.rmtree(arvados.http_cache('discovery'))
138 pid_file = os.path.join(SERVICES_SRC_DIR, 'api', SERVER_PID_PATH)
139 pid_file_ok = find_server_pid(pid_file, 0)
141 existing_api_host = os.environ.get('ARVADOS_TEST_API_HOST', my_api_host)
142 if existing_api_host and pid_file_ok:
143 if existing_api_host == my_api_host:
147 # Fall through to shutdown-and-start case.
150 # Server was provided by parent. Can't recover if it's
154 # Before trying to start up our own server, call stop() to avoid
155 # "Phusion Passenger Standalone is already running on PID 12345".
156 # (If we've gotten this far, ARVADOS_TEST_API_HOST isn't set, so
157 # we know the server is ours to kill.)
160 restore_cwd = os.getcwd()
161 api_src_dir = os.path.join(SERVICES_SRC_DIR, 'api')
162 os.chdir(api_src_dir)
164 # Either we haven't started a server of our own yet, or it has
165 # died, or we have lost our credentials, or something else is
166 # preventing us from calling reset(). Start a new one.
168 if not os.path.exists('tmp/self-signed.pem'):
169 # We assume here that either passenger reports its listening
170 # address as https:/0.0.0.0:port/. If it reports "127.0.0.1"
171 # then the certificate won't match the host and reset() will
172 # fail certificate verification. If it reports "localhost",
173 # clients (notably Python SDK's websocket client) might
174 # resolve localhost as ::1 and then fail to connect.
175 subprocess.check_call([
176 'openssl', 'req', '-new', '-x509', '-nodes',
177 '-out', 'tmp/self-signed.pem',
178 '-keyout', 'tmp/self-signed.key',
180 '-subj', '/CN=0.0.0.0'],
183 # Install the git repository fixtures.
184 gitdir = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'git')
185 gittarball = os.path.join(SERVICES_SRC_DIR, 'api', 'test', 'test.git.tar')
186 if not os.path.isdir(gitdir):
188 subprocess.check_output(['tar', '-xC', gitdir, '-f', gittarball])
190 port = find_available_port()
191 env = os.environ.copy()
192 env['RAILS_ENV'] = 'test'
193 env['ARVADOS_WEBSOCKETS'] = 'yes'
194 env.pop('ARVADOS_TEST_API_HOST', None)
195 env.pop('ARVADOS_API_HOST', None)
196 env.pop('ARVADOS_API_HOST_INSECURE', None)
197 env.pop('ARVADOS_API_TOKEN', None)
198 start_msg = subprocess.check_output(
200 'passenger', 'start', '-d', '-p{}'.format(port),
201 '--pid-file', os.path.join(os.getcwd(), pid_file),
202 '--log-file', os.path.join(os.getcwd(), 'log/test.log'),
204 '--ssl-certificate', 'tmp/self-signed.pem',
205 '--ssl-certificate-key', 'tmp/self-signed.key'],
208 if not leave_running_atexit:
209 atexit.register(kill_server_pid, pid_file, passenger_root=api_src_dir)
211 match = re.search(r'Accessible via: https://(.*?)/', start_msg)
214 "Passenger did not report endpoint: {}".format(start_msg))
215 my_api_host = match.group(1)
216 os.environ['ARVADOS_API_HOST'] = my_api_host
218 # Make sure the server has written its pid file before continuing
219 find_server_pid(pid_file)
222 os.chdir(restore_cwd)
225 """Reset the test server to fixture state.
227 This resets the ARVADOS_TEST_API_HOST provided by a parent process
228 if any, otherwise the server started by run().
230 It also resets ARVADOS_* environment vars to point to the test
231 server with admin credentials.
233 existing_api_host = os.environ.get('ARVADOS_TEST_API_HOST', my_api_host)
234 token = auth_token('admin')
235 httpclient = httplib2.Http(ca_certs=os.path.join(
236 SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.pem'))
238 'https://{}/database/reset'.format(existing_api_host),
240 headers={'Authorization': 'OAuth2 {}'.format(token)})
241 os.environ['ARVADOS_API_HOST_INSECURE'] = 'true'
242 os.environ['ARVADOS_API_HOST'] = existing_api_host
243 os.environ['ARVADOS_API_TOKEN'] = token
245 def stop(force=False):
246 """Stop the API server, if one is running.
248 If force==False, kill it only if we started it ourselves. (This
249 supports the use case where a Python test suite calls run(), but
250 run() just uses the ARVADOS_TEST_API_HOST provided by the parent
251 process, and the test suite cleans up after itself by calling
252 stop(). In this case the test server provided by the parent
253 process should be left alone.)
255 If force==True, kill it even if we didn't start it
256 ourselves. (This supports the use case in __main__, where "run"
257 and "stop" happen in different processes.)
260 if force or my_api_host is not None:
261 kill_server_pid(os.path.join(SERVICES_SRC_DIR, 'api', SERVER_PID_PATH))
264 def _start_keep(n, keep_args):
265 keep0 = tempfile.mkdtemp()
266 port = find_available_port()
267 keep_cmd = ["keepstore",
268 "-volume={}".format(keep0),
269 "-listen=:{}".format(port),
270 "-pid="+_pidfile('keep{}'.format(n))]
272 for arg, val in keep_args.iteritems():
273 keep_cmd.append("{}={}".format(arg, val))
275 kp0 = subprocess.Popen(
276 keep_cmd, stdin=open('/dev/null'), stdout=sys.stderr)
277 with open(_pidfile('keep{}'.format(n)), 'w') as f:
278 f.write(str(kp0.pid))
280 with open("{}/keep{}.volume".format(TEST_TMPDIR, n), 'w') as f:
285 def run_keep(blob_signing_key=None, enforce_permissions=False):
290 with open(os.path.join(TEST_TMPDIR, "keep.blob_signing_key"), "w") as f:
291 keep_args['--permission-key-file'] = f.name
292 f.write(blob_signing_key)
293 if enforce_permissions:
294 keep_args['--enforce-permissions'] = 'true'
298 host=os.environ['ARVADOS_API_HOST'],
299 token=os.environ['ARVADOS_API_TOKEN'],
301 for d in api.keep_services().list().execute()['items']:
302 api.keep_services().delete(uuid=d['uuid']).execute()
303 for d in api.keep_disks().list().execute()['items']:
304 api.keep_disks().delete(uuid=d['uuid']).execute()
306 for d in range(0, 2):
307 port = _start_keep(d, keep_args)
308 svc = api.keep_services().create(body={'keep_service': {
309 'uuid': 'zzzzz-bi6l4-keepdisk{:07d}'.format(d),
310 'service_host': 'localhost',
311 'service_port': port,
312 'service_type': 'disk',
313 'service_ssl_flag': False,
315 api.keep_disks().create(body={
316 'keep_disk': {'keep_service_uuid': svc['uuid'] }
320 kill_server_pid(_pidfile('keep{}'.format(n)), 0)
321 if os.path.exists("{}/keep{}.volume".format(TEST_TMPDIR, n)):
322 with open("{}/keep{}.volume".format(TEST_TMPDIR, n), 'r') as r:
323 shutil.rmtree(r.read(), True)
324 os.unlink("{}/keep{}.volume".format(TEST_TMPDIR, n))
325 if os.path.exists(os.path.join(TEST_TMPDIR, "keep.blob_signing_key")):
326 os.remove(os.path.join(TEST_TMPDIR, "keep.blob_signing_key"))
332 def run_keep_proxy():
333 if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
337 admin_token = auth_token('admin')
338 port = find_available_port()
339 env = os.environ.copy()
340 env['ARVADOS_API_TOKEN'] = admin_token
341 kp = subprocess.Popen(
343 '-pid='+_pidfile('keepproxy'),
344 '-listen=:{}'.format(port)],
345 env=env, stdin=open('/dev/null'), stdout=sys.stderr)
349 host=os.environ['ARVADOS_API_HOST'],
352 for d in api.keep_services().list(
353 filters=[['service_type','=','proxy']]).execute()['items']:
354 api.keep_services().delete(uuid=d['uuid']).execute()
355 api.keep_services().create(body={'keep_service': {
356 'service_host': 'localhost',
357 'service_port': port,
358 'service_type': 'proxy',
359 'service_ssl_flag': False,
361 os.environ["ARVADOS_KEEP_PROXY"] = "http://localhost:{}".format(port)
362 _setport('keepproxy', port)
364 def stop_keep_proxy():
365 if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
367 kill_server_pid(_pidfile('keepproxy'), wait=0)
369 def run_arv_git_httpd():
370 if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
374 gitdir = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'git')
375 gitport = find_available_port()
376 env = os.environ.copy()
377 env.pop('ARVADOS_API_TOKEN', None)
378 agh = subprocess.Popen(
380 '-repo-root='+gitdir+'/test',
381 '-address=:'+str(gitport)],
382 env=env, stdin=open('/dev/null'), stdout=sys.stderr)
383 with open(_pidfile('arv-git-httpd'), 'w') as f:
384 f.write(str(agh.pid))
385 _setport('arv-git-httpd', gitport)
387 def stop_arv_git_httpd():
388 if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
390 kill_server_pid(_pidfile('arv-git-httpd'), wait=0)
393 if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
396 nginxconf['KEEPPROXYPORT'] = _getport('keepproxy')
397 nginxconf['KEEPPROXYSSLPORT'] = find_available_port()
398 nginxconf['GITPORT'] = _getport('arv-git-httpd')
399 nginxconf['GITSSLPORT'] = find_available_port()
400 nginxconf['SSLCERT'] = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.pem')
401 nginxconf['SSLKEY'] = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.key')
403 conftemplatefile = os.path.join(MY_DIRNAME, 'nginx.conf')
404 conffile = os.path.join(TEST_TMPDIR, 'nginx.conf')
405 with open(conffile, 'w') as f:
408 lambda match: str(nginxconf.get(match.group(1))),
409 open(conftemplatefile).read()))
411 env = os.environ.copy()
412 env['PATH'] = env['PATH']+':/sbin:/usr/sbin:/usr/local/sbin'
413 nginx = subprocess.Popen(
415 '-g', 'error_log stderr info;',
416 '-g', 'pid '+_pidfile('nginx')+';',
418 env=env, stdin=open('/dev/null'), stdout=sys.stderr)
419 _setport('keepproxy-ssl', nginxconf['KEEPPROXYSSLPORT'])
420 _setport('arv-git-httpd-ssl', nginxconf['GITSSLPORT'])
423 if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
425 kill_server_pid(_pidfile('nginx'), wait=0)
427 def _pidfile(program):
428 return os.path.join(TEST_TMPDIR, program + '.pid')
430 def _portfile(program):
431 return os.path.join(TEST_TMPDIR, program + '.port')
433 def _setport(program, port):
434 with open(_portfile(program), 'w') as f:
437 # Returns 9 if program is not up.
438 def _getport(program):
440 return int(open(_portfile(program)).read())
446 return _cached_config[key]
448 return yaml.load(os.path.join(SERVICES_SRC_DIR, 'api', 'config', f))
449 cdefault = _load('application.default.yml')
450 csite = _load('application.yml')
452 for section in [cdefault.get('common',{}), cdefault.get('test',{}),
453 csite.get('common',{}), csite.get('test',{})]:
454 _cached_config.update(section)
455 return _cached_config[key]
458 '''load a fixture yaml file'''
459 with open(os.path.join(SERVICES_SRC_DIR, 'api', "test", "fixtures",
463 trim_index = yaml_file.index("# Test Helper trims the rest of the file")
464 yaml_file = yaml_file[0:trim_index]
467 return yaml.load(yaml_file)
469 def auth_token(token_name):
470 return fixture("api_client_authorizations")[token_name]["api_token"]
472 def authorize_with(token_name):
473 '''token_name is the symbolic name of the token from the api_client_authorizations fixture'''
474 arvados.config.settings()["ARVADOS_API_TOKEN"] = auth_token(token_name)
475 arvados.config.settings()["ARVADOS_API_HOST"] = os.environ.get("ARVADOS_API_HOST")
476 arvados.config.settings()["ARVADOS_API_HOST_INSECURE"] = "true"
478 class TestCaseWithServers(unittest.TestCase):
479 """TestCase to start and stop supporting Arvados servers.
481 Define any of MAIN_SERVER, KEEP_SERVER, and/or KEEP_PROXY_SERVER
482 class variables as a dictionary of keyword arguments. If you do,
483 setUpClass will start the corresponding servers by passing these
484 keyword arguments to the run, run_keep, and/or run_keep_server
485 functions, respectively. It will also set Arvados environment
486 variables to point to these servers appropriately. If you don't
487 run a Keep or Keep proxy server, setUpClass will set up a
488 temporary directory for Keep local storage, and set it as
491 tearDownClass will stop any servers started, and restore the
492 original environment.
496 KEEP_PROXY_SERVER = None
499 def _restore_dict(src, dest):
500 for key in dest.keys():
507 cls._orig_environ = os.environ.copy()
508 cls._orig_config = arvados.config.settings().copy()
509 cls._cleanup_funcs = []
510 os.environ.pop('ARVADOS_KEEP_PROXY', None)
511 os.environ.pop('ARVADOS_EXTERNAL_CLIENT', None)
512 for server_kwargs, start_func, stop_func in (
513 (cls.MAIN_SERVER, run, reset),
514 (cls.KEEP_SERVER, run_keep, stop_keep),
515 (cls.KEEP_PROXY_SERVER, run_keep_proxy, stop_keep_proxy)):
516 if server_kwargs is not None:
517 start_func(**server_kwargs)
518 cls._cleanup_funcs.append(stop_func)
519 if (cls.KEEP_SERVER is None) and (cls.KEEP_PROXY_SERVER is None):
520 cls.local_store = tempfile.mkdtemp()
521 os.environ['KEEP_LOCAL_STORE'] = cls.local_store
522 cls._cleanup_funcs.append(
523 lambda: shutil.rmtree(cls.local_store, ignore_errors=True))
525 os.environ.pop('KEEP_LOCAL_STORE', None)
526 arvados.config.initialize()
529 def tearDownClass(cls):
530 for clean_func in cls._cleanup_funcs:
532 cls._restore_dict(cls._orig_environ, os.environ)
533 cls._restore_dict(cls._orig_config, arvados.config.settings())
536 if __name__ == "__main__":
539 'start_keep', 'stop_keep',
540 'start_keep_proxy', 'stop_keep_proxy',
541 'start_arv-git-httpd', 'stop_arv-git-httpd',
542 'start_nginx', 'stop_nginx',
544 parser = argparse.ArgumentParser()
545 parser.add_argument('action', type=str, help="one of {}".format(actions))
546 parser.add_argument('--auth', type=str, metavar='FIXTURE_NAME', help='Print authorization info for given api_client_authorizations fixture')
547 args = parser.parse_args()
549 if args.action not in actions:
550 print("Unrecognized action '{}'. Actions are: {}.".format(args.action, actions), file=sys.stderr)
552 if args.action == 'start':
553 stop(force=('ARVADOS_TEST_API_HOST' not in os.environ))
554 run(leave_running_atexit=True)
555 host = os.environ['ARVADOS_API_HOST']
556 if args.auth is not None:
557 token = auth_token(args.auth)
558 print("export ARVADOS_API_TOKEN={}".format(pipes.quote(token)))
559 print("export ARVADOS_API_HOST={}".format(pipes.quote(host)))
560 print("export ARVADOS_API_HOST_INSECURE=true")
563 elif args.action == 'stop':
564 stop(force=('ARVADOS_TEST_API_HOST' not in os.environ))
565 elif args.action == 'start_keep':
567 elif args.action == 'stop_keep':
569 elif args.action == 'start_keep_proxy':
571 elif args.action == 'stop_keep_proxy':
573 elif args.action == 'start_arv-git-httpd':
575 elif args.action == 'stop_arv-git-httpd':
577 elif args.action == 'start_nginx':
579 elif args.action == 'stop_nginx':
582 raise Exception("action recognized but not implemented!?")