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]
456 def _load(f, required=True):
457 fullpath = os.path.join(SERVICES_SRC_DIR, 'api', 'config', f)
458 if not required and not os.path.exists(fullpath):
460 return yaml.load(fullpath)
461 cdefault = _load('application.default.yml')
462 csite = _load('application.yml', required=False)
464 for section in [cdefault.get('common',{}), cdefault.get('test',{}),
465 csite.get('common',{}), csite.get('test',{})]:
466 _cached_config.update(section)
467 return _cached_config[key]
470 '''load a fixture yaml file'''
471 with open(os.path.join(SERVICES_SRC_DIR, 'api', "test", "fixtures",
475 trim_index = yaml_file.index("# Test Helper trims the rest of the file")
476 yaml_file = yaml_file[0:trim_index]
479 return yaml.load(yaml_file)
481 def auth_token(token_name):
482 return fixture("api_client_authorizations")[token_name]["api_token"]
484 def authorize_with(token_name):
485 '''token_name is the symbolic name of the token from the api_client_authorizations fixture'''
486 arvados.config.settings()["ARVADOS_API_TOKEN"] = auth_token(token_name)
487 arvados.config.settings()["ARVADOS_API_HOST"] = os.environ.get("ARVADOS_API_HOST")
488 arvados.config.settings()["ARVADOS_API_HOST_INSECURE"] = "true"
490 class TestCaseWithServers(unittest.TestCase):
491 """TestCase to start and stop supporting Arvados servers.
493 Define any of MAIN_SERVER, KEEP_SERVER, and/or KEEP_PROXY_SERVER
494 class variables as a dictionary of keyword arguments. If you do,
495 setUpClass will start the corresponding servers by passing these
496 keyword arguments to the run, run_keep, and/or run_keep_server
497 functions, respectively. It will also set Arvados environment
498 variables to point to these servers appropriately. If you don't
499 run a Keep or Keep proxy server, setUpClass will set up a
500 temporary directory for Keep local storage, and set it as
503 tearDownClass will stop any servers started, and restore the
504 original environment.
508 KEEP_PROXY_SERVER = None
511 def _restore_dict(src, dest):
512 for key in dest.keys():
519 cls._orig_environ = os.environ.copy()
520 cls._orig_config = arvados.config.settings().copy()
521 cls._cleanup_funcs = []
522 os.environ.pop('ARVADOS_KEEP_PROXY', None)
523 os.environ.pop('ARVADOS_EXTERNAL_CLIENT', None)
524 for server_kwargs, start_func, stop_func in (
525 (cls.MAIN_SERVER, run, reset),
526 (cls.KEEP_SERVER, run_keep, stop_keep),
527 (cls.KEEP_PROXY_SERVER, run_keep_proxy, stop_keep_proxy)):
528 if server_kwargs is not None:
529 start_func(**server_kwargs)
530 cls._cleanup_funcs.append(stop_func)
531 if (cls.KEEP_SERVER is None) and (cls.KEEP_PROXY_SERVER is None):
532 cls.local_store = tempfile.mkdtemp()
533 os.environ['KEEP_LOCAL_STORE'] = cls.local_store
534 cls._cleanup_funcs.append(
535 lambda: shutil.rmtree(cls.local_store, ignore_errors=True))
537 os.environ.pop('KEEP_LOCAL_STORE', None)
538 arvados.config.initialize()
541 def tearDownClass(cls):
542 for clean_func in cls._cleanup_funcs:
544 cls._restore_dict(cls._orig_environ, os.environ)
545 cls._restore_dict(cls._orig_config, arvados.config.settings())
548 if __name__ == "__main__":
551 'start_keep', 'stop_keep',
552 'start_keep_proxy', 'stop_keep_proxy',
553 'start_arv-git-httpd', 'stop_arv-git-httpd',
554 'start_nginx', 'stop_nginx',
556 parser = argparse.ArgumentParser()
557 parser.add_argument('action', type=str, help="one of {}".format(actions))
558 parser.add_argument('--auth', type=str, metavar='FIXTURE_NAME', help='Print authorization info for given api_client_authorizations fixture')
559 args = parser.parse_args()
561 if args.action not in actions:
562 print("Unrecognized action '{}'. Actions are: {}.".format(args.action, actions), file=sys.stderr)
564 if args.action == 'start':
565 stop(force=('ARVADOS_TEST_API_HOST' not in os.environ))
566 run(leave_running_atexit=True)
567 host = os.environ['ARVADOS_API_HOST']
568 if args.auth is not None:
569 token = auth_token(args.auth)
570 print("export ARVADOS_API_TOKEN={}".format(pipes.quote(token)))
571 print("export ARVADOS_API_HOST={}".format(pipes.quote(host)))
572 print("export ARVADOS_API_HOST_INSECURE=true")
575 elif args.action == 'stop':
576 stop(force=('ARVADOS_TEST_API_HOST' not in os.environ))
577 elif args.action == 'start_keep':
579 elif args.action == 'stop_keep':
581 elif args.action == 'start_keep_proxy':
583 elif args.action == 'stop_keep_proxy':
585 elif args.action == 'start_arv-git-httpd':
587 elif args.action == 'stop_arv-git-httpd':
589 elif args.action == 'start_nginx':
591 elif args.action == 'stop_nginx':
594 raise Exception("action recognized but not implemented!?")