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'):
171 if not os.path.exists('tmp/api'):
172 os.makedirs('tmp/api')
174 if not os.path.exists('tmp/logs'):
175 os.makedirs('tmp/logs')
177 if not os.path.exists('tmp/self-signed.pem'):
178 # We assume here that either passenger reports its listening
179 # address as https:/0.0.0.0:port/. If it reports "127.0.0.1"
180 # then the certificate won't match the host and reset() will
181 # fail certificate verification. If it reports "localhost",
182 # clients (notably Python SDK's websocket client) might
183 # resolve localhost as ::1 and then fail to connect.
184 subprocess.check_call([
185 'openssl', 'req', '-new', '-x509', '-nodes',
186 '-out', 'tmp/self-signed.pem',
187 '-keyout', 'tmp/self-signed.key',
189 '-subj', '/CN=0.0.0.0'],
192 # Install the git repository fixtures.
193 gitdir = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'git')
194 gittarball = os.path.join(SERVICES_SRC_DIR, 'api', 'test', 'test.git.tar')
195 if not os.path.isdir(gitdir):
197 subprocess.check_output(['tar', '-xC', gitdir, '-f', gittarball])
199 port = find_available_port()
200 env = os.environ.copy()
201 env['RAILS_ENV'] = 'test'
202 env['ARVADOS_WEBSOCKETS'] = 'yes'
203 env.pop('ARVADOS_TEST_API_HOST', None)
204 env.pop('ARVADOS_API_HOST', None)
205 env.pop('ARVADOS_API_HOST_INSECURE', None)
206 env.pop('ARVADOS_API_TOKEN', None)
207 start_msg = subprocess.check_output(
209 'passenger', 'start', '-d', '-p{}'.format(port),
210 '--pid-file', os.path.join(os.getcwd(), pid_file),
211 '--log-file', os.path.join(os.getcwd(), 'log/test.log'),
213 '--ssl-certificate', 'tmp/self-signed.pem',
214 '--ssl-certificate-key', 'tmp/self-signed.key'],
217 if not leave_running_atexit:
218 atexit.register(kill_server_pid, pid_file, passenger_root=api_src_dir)
220 match = re.search(r'Accessible via: https://(.*?)/', start_msg)
223 "Passenger did not report endpoint: {}".format(start_msg))
224 my_api_host = match.group(1)
225 os.environ['ARVADOS_API_HOST'] = my_api_host
227 # Make sure the server has written its pid file before continuing
228 find_server_pid(pid_file)
231 os.chdir(restore_cwd)
234 """Reset the test server to fixture state.
236 This resets the ARVADOS_TEST_API_HOST provided by a parent process
237 if any, otherwise the server started by run().
239 It also resets ARVADOS_* environment vars to point to the test
240 server with admin credentials.
242 existing_api_host = os.environ.get('ARVADOS_TEST_API_HOST', my_api_host)
243 token = auth_token('admin')
244 httpclient = httplib2.Http(ca_certs=os.path.join(
245 SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.pem'))
247 'https://{}/database/reset'.format(existing_api_host),
249 headers={'Authorization': 'OAuth2 {}'.format(token)})
250 os.environ['ARVADOS_API_HOST_INSECURE'] = 'true'
251 os.environ['ARVADOS_API_HOST'] = existing_api_host
252 os.environ['ARVADOS_API_TOKEN'] = token
254 def stop(force=False):
255 """Stop the API server, if one is running.
257 If force==False, kill it only if we started it ourselves. (This
258 supports the use case where a Python test suite calls run(), but
259 run() just uses the ARVADOS_TEST_API_HOST provided by the parent
260 process, and the test suite cleans up after itself by calling
261 stop(). In this case the test server provided by the parent
262 process should be left alone.)
264 If force==True, kill it even if we didn't start it
265 ourselves. (This supports the use case in __main__, where "run"
266 and "stop" happen in different processes.)
269 if force or my_api_host is not None:
270 kill_server_pid(os.path.join(SERVICES_SRC_DIR, 'api', SERVER_PID_PATH))
273 def _start_keep(n, keep_args):
274 keep0 = tempfile.mkdtemp()
275 port = find_available_port()
276 keep_cmd = ["keepstore",
277 "-volume={}".format(keep0),
278 "-listen=:{}".format(port),
279 "-pid="+_pidfile('keep{}'.format(n))]
281 for arg, val in keep_args.iteritems():
282 keep_cmd.append("{}={}".format(arg, val))
284 kp0 = subprocess.Popen(
285 keep_cmd, stdin=open('/dev/null'), stdout=sys.stderr)
286 with open(_pidfile('keep{}'.format(n)), 'w') as f:
287 f.write(str(kp0.pid))
289 with open("{}/keep{}.volume".format(TEST_TMPDIR, n), 'w') as f:
294 def run_keep(blob_signing_key=None, enforce_permissions=False):
299 with open(os.path.join(TEST_TMPDIR, "keep.blob_signing_key"), "w") as f:
300 keep_args['--permission-key-file'] = f.name
301 f.write(blob_signing_key)
302 if enforce_permissions:
303 keep_args['--enforce-permissions'] = 'true'
307 host=os.environ['ARVADOS_API_HOST'],
308 token=os.environ['ARVADOS_API_TOKEN'],
310 for d in api.keep_services().list().execute()['items']:
311 api.keep_services().delete(uuid=d['uuid']).execute()
312 for d in api.keep_disks().list().execute()['items']:
313 api.keep_disks().delete(uuid=d['uuid']).execute()
315 for d in range(0, 2):
316 port = _start_keep(d, keep_args)
317 svc = api.keep_services().create(body={'keep_service': {
318 'uuid': 'zzzzz-bi6l4-keepdisk{:07d}'.format(d),
319 'service_host': 'localhost',
320 'service_port': port,
321 'service_type': 'disk',
322 'service_ssl_flag': False,
324 api.keep_disks().create(body={
325 'keep_disk': {'keep_service_uuid': svc['uuid'] }
329 kill_server_pid(_pidfile('keep{}'.format(n)), 0)
330 if os.path.exists("{}/keep{}.volume".format(TEST_TMPDIR, n)):
331 with open("{}/keep{}.volume".format(TEST_TMPDIR, n), 'r') as r:
332 shutil.rmtree(r.read(), True)
333 os.unlink("{}/keep{}.volume".format(TEST_TMPDIR, n))
334 if os.path.exists(os.path.join(TEST_TMPDIR, "keep.blob_signing_key")):
335 os.remove(os.path.join(TEST_TMPDIR, "keep.blob_signing_key"))
341 def run_keep_proxy():
342 if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
346 admin_token = auth_token('admin')
347 port = find_available_port()
348 env = os.environ.copy()
349 env['ARVADOS_API_TOKEN'] = admin_token
350 kp = subprocess.Popen(
352 '-pid='+_pidfile('keepproxy'),
353 '-listen=:{}'.format(port)],
354 env=env, stdin=open('/dev/null'), stdout=sys.stderr)
358 host=os.environ['ARVADOS_API_HOST'],
361 for d in api.keep_services().list(
362 filters=[['service_type','=','proxy']]).execute()['items']:
363 api.keep_services().delete(uuid=d['uuid']).execute()
364 api.keep_services().create(body={'keep_service': {
365 'service_host': 'localhost',
366 'service_port': port,
367 'service_type': 'proxy',
368 'service_ssl_flag': False,
370 os.environ["ARVADOS_KEEP_PROXY"] = "http://localhost:{}".format(port)
371 _setport('keepproxy', port)
373 def stop_keep_proxy():
374 if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
376 kill_server_pid(_pidfile('keepproxy'), wait=0)
378 def run_arv_git_httpd():
379 if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
383 gitdir = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'git')
384 gitport = find_available_port()
385 env = os.environ.copy()
386 env.pop('ARVADOS_API_TOKEN', None)
387 agh = subprocess.Popen(
389 '-repo-root='+gitdir+'/test',
390 '-address=:'+str(gitport)],
391 env=env, stdin=open('/dev/null'), stdout=sys.stderr)
392 with open(_pidfile('arv-git-httpd'), 'w') as f:
393 f.write(str(agh.pid))
394 _setport('arv-git-httpd', gitport)
396 def stop_arv_git_httpd():
397 if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
399 kill_server_pid(_pidfile('arv-git-httpd'), wait=0)
402 if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
405 nginxconf['KEEPPROXYPORT'] = _getport('keepproxy')
406 nginxconf['KEEPPROXYSSLPORT'] = find_available_port()
407 nginxconf['GITPORT'] = _getport('arv-git-httpd')
408 nginxconf['GITSSLPORT'] = find_available_port()
409 nginxconf['SSLCERT'] = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.pem')
410 nginxconf['SSLKEY'] = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.key')
412 conftemplatefile = os.path.join(MY_DIRNAME, 'nginx.conf')
413 conffile = os.path.join(TEST_TMPDIR, 'nginx.conf')
414 with open(conffile, 'w') as f:
417 lambda match: str(nginxconf.get(match.group(1))),
418 open(conftemplatefile).read()))
420 env = os.environ.copy()
421 env['PATH'] = env['PATH']+':/sbin:/usr/sbin:/usr/local/sbin'
422 nginx = subprocess.Popen(
424 '-g', 'error_log stderr info;',
425 '-g', 'pid '+_pidfile('nginx')+';',
427 env=env, stdin=open('/dev/null'), stdout=sys.stderr)
428 _setport('keepproxy-ssl', nginxconf['KEEPPROXYSSLPORT'])
429 _setport('arv-git-httpd-ssl', nginxconf['GITSSLPORT'])
432 if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
434 kill_server_pid(_pidfile('nginx'), wait=0)
436 def _pidfile(program):
437 return os.path.join(TEST_TMPDIR, program + '.pid')
439 def _portfile(program):
440 return os.path.join(TEST_TMPDIR, program + '.port')
442 def _setport(program, port):
443 with open(_portfile(program), 'w') as f:
446 # Returns 9 if program is not up.
447 def _getport(program):
449 return int(open(_portfile(program)).read())
455 return _cached_config[key]
457 return yaml.load(os.path.join(SERVICES_SRC_DIR, 'api', 'config', f))
458 cdefault = _load('application.default.yml')
459 csite = _load('application.yml')
461 for section in [cdefault.get('common',{}), cdefault.get('test',{}),
462 csite.get('common',{}), csite.get('test',{})]:
463 _cached_config.update(section)
464 return _cached_config[key]
467 '''load a fixture yaml file'''
468 with open(os.path.join(SERVICES_SRC_DIR, 'api', "test", "fixtures",
472 trim_index = yaml_file.index("# Test Helper trims the rest of the file")
473 yaml_file = yaml_file[0:trim_index]
476 return yaml.load(yaml_file)
478 def auth_token(token_name):
479 return fixture("api_client_authorizations")[token_name]["api_token"]
481 def authorize_with(token_name):
482 '''token_name is the symbolic name of the token from the api_client_authorizations fixture'''
483 arvados.config.settings()["ARVADOS_API_TOKEN"] = auth_token(token_name)
484 arvados.config.settings()["ARVADOS_API_HOST"] = os.environ.get("ARVADOS_API_HOST")
485 arvados.config.settings()["ARVADOS_API_HOST_INSECURE"] = "true"
487 class TestCaseWithServers(unittest.TestCase):
488 """TestCase to start and stop supporting Arvados servers.
490 Define any of MAIN_SERVER, KEEP_SERVER, and/or KEEP_PROXY_SERVER
491 class variables as a dictionary of keyword arguments. If you do,
492 setUpClass will start the corresponding servers by passing these
493 keyword arguments to the run, run_keep, and/or run_keep_server
494 functions, respectively. It will also set Arvados environment
495 variables to point to these servers appropriately. If you don't
496 run a Keep or Keep proxy server, setUpClass will set up a
497 temporary directory for Keep local storage, and set it as
500 tearDownClass will stop any servers started, and restore the
501 original environment.
505 KEEP_PROXY_SERVER = None
508 def _restore_dict(src, dest):
509 for key in dest.keys():
516 cls._orig_environ = os.environ.copy()
517 cls._orig_config = arvados.config.settings().copy()
518 cls._cleanup_funcs = []
519 os.environ.pop('ARVADOS_KEEP_PROXY', None)
520 os.environ.pop('ARVADOS_EXTERNAL_CLIENT', None)
521 for server_kwargs, start_func, stop_func in (
522 (cls.MAIN_SERVER, run, reset),
523 (cls.KEEP_SERVER, run_keep, stop_keep),
524 (cls.KEEP_PROXY_SERVER, run_keep_proxy, stop_keep_proxy)):
525 if server_kwargs is not None:
526 start_func(**server_kwargs)
527 cls._cleanup_funcs.append(stop_func)
528 if (cls.KEEP_SERVER is None) and (cls.KEEP_PROXY_SERVER is None):
529 cls.local_store = tempfile.mkdtemp()
530 os.environ['KEEP_LOCAL_STORE'] = cls.local_store
531 cls._cleanup_funcs.append(
532 lambda: shutil.rmtree(cls.local_store, ignore_errors=True))
534 os.environ.pop('KEEP_LOCAL_STORE', None)
535 arvados.config.initialize()
538 def tearDownClass(cls):
539 for clean_func in cls._cleanup_funcs:
541 cls._restore_dict(cls._orig_environ, os.environ)
542 cls._restore_dict(cls._orig_config, arvados.config.settings())
545 if __name__ == "__main__":
548 'start_keep', 'stop_keep',
549 'start_keep_proxy', 'stop_keep_proxy',
550 'start_arv-git-httpd', 'stop_arv-git-httpd',
551 'start_nginx', 'stop_nginx',
553 parser = argparse.ArgumentParser()
554 parser.add_argument('action', type=str, help="one of {}".format(actions))
555 parser.add_argument('--auth', type=str, metavar='FIXTURE_NAME', help='Print authorization info for given api_client_authorizations fixture')
556 args = parser.parse_args()
558 if args.action not in actions:
559 print("Unrecognized action '{}'. Actions are: {}.".format(args.action, actions), file=sys.stderr)
561 if args.action == 'start':
562 stop(force=('ARVADOS_TEST_API_HOST' not in os.environ))
563 run(leave_running_atexit=True)
564 host = os.environ['ARVADOS_API_HOST']
565 if args.auth is not None:
566 token = auth_token(args.auth)
567 print("export ARVADOS_API_TOKEN={}".format(pipes.quote(token)))
568 print("export ARVADOS_API_HOST={}".format(pipes.quote(host)))
569 print("export ARVADOS_API_HOST_INSECURE=true")
572 elif args.action == 'stop':
573 stop(force=('ARVADOS_TEST_API_HOST' not in os.environ))
574 elif args.action == 'start_keep':
576 elif args.action == 'stop_keep':
578 elif args.action == 'start_keep_proxy':
580 elif args.action == 'stop_keep_proxy':
582 elif args.action == 'start_arv-git-httpd':
584 elif args.action == 'stop_arv-git-httpd':
586 elif args.action == 'start_nginx':
588 elif args.action == 'stop_nginx':
591 raise Exception("action recognized but not implemented!?")