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 _wait_until_port_listens(port, timeout=10):
119 """Wait for a process to start listening on the given port.
121 If nothing listens on the port within the specified timeout (given
122 in seconds), print a warning on stderr before returning.
125 subprocess.check_output(['fuser', '-l'])
126 except subprocess.CalledProcessError:
127 print("WARNING: No `fuser` -- cannot wait for port to listen. "+
128 "Sleeping 0.5 and hoping for the best.")
131 deadline = time.time() + timeout
132 while time.time() < deadline:
134 fuser_says = subprocess.check_output(['fuser', str(port)+'/tcp'])
135 except subprocess.CalledProcessError:
140 "WARNING: Nothing is listening on port {} (waited {} seconds).".
141 format(port, timeout),
144 def run(leave_running_atexit=False):
145 """Ensure an API server is running, and ARVADOS_API_* env vars have
146 admin credentials for it.
148 If ARVADOS_TEST_API_HOST is set, a parent process has started a
149 test server for us to use: we just need to reset() it using the
152 If a previous call to run() started a new server process, and it
153 is still running, we just need to reset() it to fixture state and
156 If neither of those options work out, we'll really start a new
161 # Delete cached discovery document.
162 shutil.rmtree(arvados.http_cache('discovery'))
164 pid_file = os.path.join(SERVICES_SRC_DIR, 'api', SERVER_PID_PATH)
165 pid_file_ok = find_server_pid(pid_file, 0)
167 existing_api_host = os.environ.get('ARVADOS_TEST_API_HOST', my_api_host)
168 if existing_api_host and pid_file_ok:
169 if existing_api_host == my_api_host:
173 # Fall through to shutdown-and-start case.
176 # Server was provided by parent. Can't recover if it's
180 # Before trying to start up our own server, call stop() to avoid
181 # "Phusion Passenger Standalone is already running on PID 12345".
182 # (If we've gotten this far, ARVADOS_TEST_API_HOST isn't set, so
183 # we know the server is ours to kill.)
186 restore_cwd = os.getcwd()
187 api_src_dir = os.path.join(SERVICES_SRC_DIR, 'api')
188 os.chdir(api_src_dir)
190 # Either we haven't started a server of our own yet, or it has
191 # died, or we have lost our credentials, or something else is
192 # preventing us from calling reset(). Start a new one.
194 if not os.path.exists('tmp'):
197 if not os.path.exists('tmp/api'):
198 os.makedirs('tmp/api')
200 if not os.path.exists('tmp/logs'):
201 os.makedirs('tmp/logs')
203 if not os.path.exists('tmp/self-signed.pem'):
204 # We assume here that either passenger reports its listening
205 # address as https:/0.0.0.0:port/. If it reports "127.0.0.1"
206 # then the certificate won't match the host and reset() will
207 # fail certificate verification. If it reports "localhost",
208 # clients (notably Python SDK's websocket client) might
209 # resolve localhost as ::1 and then fail to connect.
210 subprocess.check_call([
211 'openssl', 'req', '-new', '-x509', '-nodes',
212 '-out', 'tmp/self-signed.pem',
213 '-keyout', 'tmp/self-signed.key',
215 '-subj', '/CN=0.0.0.0'],
218 # Install the git repository fixtures.
219 gitdir = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'git')
220 gittarball = os.path.join(SERVICES_SRC_DIR, 'api', 'test', 'test.git.tar')
221 if not os.path.isdir(gitdir):
223 subprocess.check_output(['tar', '-xC', gitdir, '-f', gittarball])
225 port = find_available_port()
226 env = os.environ.copy()
227 env['RAILS_ENV'] = 'test'
228 env['ARVADOS_WEBSOCKETS'] = 'yes'
229 env.pop('ARVADOS_TEST_API_HOST', None)
230 env.pop('ARVADOS_API_HOST', None)
231 env.pop('ARVADOS_API_HOST_INSECURE', None)
232 env.pop('ARVADOS_API_TOKEN', None)
233 start_msg = subprocess.check_output(
235 'passenger', 'start', '-d', '-p{}'.format(port),
236 '--pid-file', os.path.join(os.getcwd(), pid_file),
237 '--log-file', os.path.join(os.getcwd(), 'log/test.log'),
239 '--ssl-certificate', 'tmp/self-signed.pem',
240 '--ssl-certificate-key', 'tmp/self-signed.key'],
243 if not leave_running_atexit:
244 atexit.register(kill_server_pid, pid_file, passenger_root=api_src_dir)
246 match = re.search(r'Accessible via: https://(.*?)/', start_msg)
249 "Passenger did not report endpoint: {}".format(start_msg))
250 my_api_host = match.group(1)
251 os.environ['ARVADOS_API_HOST'] = my_api_host
253 # Make sure the server has written its pid file and started
254 # listening on its TCP port
255 find_server_pid(pid_file)
256 _wait_until_port_listens(port)
259 os.chdir(restore_cwd)
262 """Reset the test server to fixture state.
264 This resets the ARVADOS_TEST_API_HOST provided by a parent process
265 if any, otherwise the server started by run().
267 It also resets ARVADOS_* environment vars to point to the test
268 server with admin credentials.
270 existing_api_host = os.environ.get('ARVADOS_TEST_API_HOST', my_api_host)
271 token = auth_token('admin')
272 httpclient = httplib2.Http(ca_certs=os.path.join(
273 SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.pem'))
275 'https://{}/database/reset'.format(existing_api_host),
277 headers={'Authorization': 'OAuth2 {}'.format(token)})
278 os.environ['ARVADOS_API_HOST_INSECURE'] = 'true'
279 os.environ['ARVADOS_API_HOST'] = existing_api_host
280 os.environ['ARVADOS_API_TOKEN'] = token
282 def stop(force=False):
283 """Stop the API server, if one is running.
285 If force==False, kill it only if we started it ourselves. (This
286 supports the use case where a Python test suite calls run(), but
287 run() just uses the ARVADOS_TEST_API_HOST provided by the parent
288 process, and the test suite cleans up after itself by calling
289 stop(). In this case the test server provided by the parent
290 process should be left alone.)
292 If force==True, kill it even if we didn't start it
293 ourselves. (This supports the use case in __main__, where "run"
294 and "stop" happen in different processes.)
297 if force or my_api_host is not None:
298 kill_server_pid(os.path.join(SERVICES_SRC_DIR, 'api', SERVER_PID_PATH))
301 def _start_keep(n, keep_args):
302 keep0 = tempfile.mkdtemp()
303 port = find_available_port()
304 keep_cmd = ["keepstore",
305 "-volume={}".format(keep0),
306 "-listen=:{}".format(port),
307 "-pid="+_pidfile('keep{}'.format(n))]
309 for arg, val in keep_args.iteritems():
310 keep_cmd.append("{}={}".format(arg, val))
312 kp0 = subprocess.Popen(
313 keep_cmd, stdin=open('/dev/null'), stdout=sys.stderr)
314 with open(_pidfile('keep{}'.format(n)), 'w') as f:
315 f.write(str(kp0.pid))
317 with open("{}/keep{}.volume".format(TEST_TMPDIR, n), 'w') as f:
320 _wait_until_port_listens(port)
324 def run_keep(blob_signing_key=None, enforce_permissions=False):
329 with open(os.path.join(TEST_TMPDIR, "keep.blob_signing_key"), "w") as f:
330 keep_args['--permission-key-file'] = f.name
331 f.write(blob_signing_key)
332 if enforce_permissions:
333 keep_args['--enforce-permissions'] = 'true'
337 host=os.environ['ARVADOS_API_HOST'],
338 token=os.environ['ARVADOS_API_TOKEN'],
340 for d in api.keep_services().list().execute()['items']:
341 api.keep_services().delete(uuid=d['uuid']).execute()
342 for d in api.keep_disks().list().execute()['items']:
343 api.keep_disks().delete(uuid=d['uuid']).execute()
345 for d in range(0, 2):
346 port = _start_keep(d, keep_args)
347 svc = api.keep_services().create(body={'keep_service': {
348 'uuid': 'zzzzz-bi6l4-keepdisk{:07d}'.format(d),
349 'service_host': 'localhost',
350 'service_port': port,
351 'service_type': 'disk',
352 'service_ssl_flag': False,
354 api.keep_disks().create(body={
355 'keep_disk': {'keep_service_uuid': svc['uuid'] }
359 kill_server_pid(_pidfile('keep{}'.format(n)), 0)
360 if os.path.exists("{}/keep{}.volume".format(TEST_TMPDIR, n)):
361 with open("{}/keep{}.volume".format(TEST_TMPDIR, n), 'r') as r:
362 shutil.rmtree(r.read(), True)
363 os.unlink("{}/keep{}.volume".format(TEST_TMPDIR, n))
364 if os.path.exists(os.path.join(TEST_TMPDIR, "keep.blob_signing_key")):
365 os.remove(os.path.join(TEST_TMPDIR, "keep.blob_signing_key"))
371 def run_keep_proxy():
372 if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
376 admin_token = auth_token('admin')
377 port = find_available_port()
378 env = os.environ.copy()
379 env['ARVADOS_API_TOKEN'] = admin_token
380 kp = subprocess.Popen(
382 '-pid='+_pidfile('keepproxy'),
383 '-listen=:{}'.format(port)],
384 env=env, stdin=open('/dev/null'), stdout=sys.stderr)
388 host=os.environ['ARVADOS_API_HOST'],
391 for d in api.keep_services().list(
392 filters=[['service_type','=','proxy']]).execute()['items']:
393 api.keep_services().delete(uuid=d['uuid']).execute()
394 api.keep_services().create(body={'keep_service': {
395 'service_host': 'localhost',
396 'service_port': port,
397 'service_type': 'proxy',
398 'service_ssl_flag': False,
400 os.environ["ARVADOS_KEEP_PROXY"] = "http://localhost:{}".format(port)
401 _setport('keepproxy', port)
402 _wait_until_port_listens(port)
404 def stop_keep_proxy():
405 if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
407 kill_server_pid(_pidfile('keepproxy'), wait=0)
409 def run_arv_git_httpd():
410 if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
414 gitdir = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'git')
415 gitport = find_available_port()
416 env = os.environ.copy()
417 env.pop('ARVADOS_API_TOKEN', None)
418 agh = subprocess.Popen(
420 '-repo-root='+gitdir+'/test',
421 '-address=:'+str(gitport)],
422 env=env, stdin=open('/dev/null'), stdout=sys.stderr)
423 with open(_pidfile('arv-git-httpd'), 'w') as f:
424 f.write(str(agh.pid))
425 _setport('arv-git-httpd', gitport)
426 _wait_until_port_listens(gitport)
428 def stop_arv_git_httpd():
429 if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
431 kill_server_pid(_pidfile('arv-git-httpd'), wait=0)
434 if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
437 nginxconf['KEEPPROXYPORT'] = _getport('keepproxy')
438 nginxconf['KEEPPROXYSSLPORT'] = find_available_port()
439 nginxconf['GITPORT'] = _getport('arv-git-httpd')
440 nginxconf['GITSSLPORT'] = find_available_port()
441 nginxconf['SSLCERT'] = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.pem')
442 nginxconf['SSLKEY'] = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.key')
444 conftemplatefile = os.path.join(MY_DIRNAME, 'nginx.conf')
445 conffile = os.path.join(TEST_TMPDIR, 'nginx.conf')
446 with open(conffile, 'w') as f:
449 lambda match: str(nginxconf.get(match.group(1))),
450 open(conftemplatefile).read()))
452 env = os.environ.copy()
453 env['PATH'] = env['PATH']+':/sbin:/usr/sbin:/usr/local/sbin'
454 nginx = subprocess.Popen(
456 '-g', 'error_log stderr info;',
457 '-g', 'pid '+_pidfile('nginx')+';',
459 env=env, stdin=open('/dev/null'), stdout=sys.stderr)
460 _setport('keepproxy-ssl', nginxconf['KEEPPROXYSSLPORT'])
461 _setport('arv-git-httpd-ssl', nginxconf['GITSSLPORT'])
464 if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
466 kill_server_pid(_pidfile('nginx'), wait=0)
468 def _pidfile(program):
469 return os.path.join(TEST_TMPDIR, program + '.pid')
471 def _portfile(program):
472 return os.path.join(TEST_TMPDIR, program + '.port')
474 def _setport(program, port):
475 with open(_portfile(program), 'w') as f:
478 # Returns 9 if program is not up.
479 def _getport(program):
481 return int(open(_portfile(program)).read())
487 return _cached_config[key]
488 def _load(f, required=True):
489 fullpath = os.path.join(SERVICES_SRC_DIR, 'api', 'config', f)
490 if not required and not os.path.exists(fullpath):
492 return yaml.load(fullpath)
493 cdefault = _load('application.default.yml')
494 csite = _load('application.yml', required=False)
496 for section in [cdefault.get('common',{}), cdefault.get('test',{}),
497 csite.get('common',{}), csite.get('test',{})]:
498 _cached_config.update(section)
499 return _cached_config[key]
502 '''load a fixture yaml file'''
503 with open(os.path.join(SERVICES_SRC_DIR, 'api', "test", "fixtures",
507 trim_index = yaml_file.index("# Test Helper trims the rest of the file")
508 yaml_file = yaml_file[0:trim_index]
511 return yaml.load(yaml_file)
513 def auth_token(token_name):
514 return fixture("api_client_authorizations")[token_name]["api_token"]
516 def authorize_with(token_name):
517 '''token_name is the symbolic name of the token from the api_client_authorizations fixture'''
518 arvados.config.settings()["ARVADOS_API_TOKEN"] = auth_token(token_name)
519 arvados.config.settings()["ARVADOS_API_HOST"] = os.environ.get("ARVADOS_API_HOST")
520 arvados.config.settings()["ARVADOS_API_HOST_INSECURE"] = "true"
522 class TestCaseWithServers(unittest.TestCase):
523 """TestCase to start and stop supporting Arvados servers.
525 Define any of MAIN_SERVER, KEEP_SERVER, and/or KEEP_PROXY_SERVER
526 class variables as a dictionary of keyword arguments. If you do,
527 setUpClass will start the corresponding servers by passing these
528 keyword arguments to the run, run_keep, and/or run_keep_server
529 functions, respectively. It will also set Arvados environment
530 variables to point to these servers appropriately. If you don't
531 run a Keep or Keep proxy server, setUpClass will set up a
532 temporary directory for Keep local storage, and set it as
535 tearDownClass will stop any servers started, and restore the
536 original environment.
540 KEEP_PROXY_SERVER = None
543 def _restore_dict(src, dest):
544 for key in dest.keys():
551 cls._orig_environ = os.environ.copy()
552 cls._orig_config = arvados.config.settings().copy()
553 cls._cleanup_funcs = []
554 os.environ.pop('ARVADOS_KEEP_PROXY', None)
555 os.environ.pop('ARVADOS_EXTERNAL_CLIENT', None)
556 for server_kwargs, start_func, stop_func in (
557 (cls.MAIN_SERVER, run, reset),
558 (cls.KEEP_SERVER, run_keep, stop_keep),
559 (cls.KEEP_PROXY_SERVER, run_keep_proxy, stop_keep_proxy)):
560 if server_kwargs is not None:
561 start_func(**server_kwargs)
562 cls._cleanup_funcs.append(stop_func)
563 if (cls.KEEP_SERVER is None) and (cls.KEEP_PROXY_SERVER is None):
564 cls.local_store = tempfile.mkdtemp()
565 os.environ['KEEP_LOCAL_STORE'] = cls.local_store
566 cls._cleanup_funcs.append(
567 lambda: shutil.rmtree(cls.local_store, ignore_errors=True))
569 os.environ.pop('KEEP_LOCAL_STORE', None)
570 arvados.config.initialize()
573 def tearDownClass(cls):
574 for clean_func in cls._cleanup_funcs:
576 cls._restore_dict(cls._orig_environ, os.environ)
577 cls._restore_dict(cls._orig_config, arvados.config.settings())
580 if __name__ == "__main__":
583 'start_keep', 'stop_keep',
584 'start_keep_proxy', 'stop_keep_proxy',
585 'start_arv-git-httpd', 'stop_arv-git-httpd',
586 'start_nginx', 'stop_nginx',
588 parser = argparse.ArgumentParser()
589 parser.add_argument('action', type=str, help="one of {}".format(actions))
590 parser.add_argument('--auth', type=str, metavar='FIXTURE_NAME', help='Print authorization info for given api_client_authorizations fixture')
591 args = parser.parse_args()
593 if args.action not in actions:
594 print("Unrecognized action '{}'. Actions are: {}.".format(args.action, actions), file=sys.stderr)
596 if args.action == 'start':
597 stop(force=('ARVADOS_TEST_API_HOST' not in os.environ))
598 run(leave_running_atexit=True)
599 host = os.environ['ARVADOS_API_HOST']
600 if args.auth is not None:
601 token = auth_token(args.auth)
602 print("export ARVADOS_API_TOKEN={}".format(pipes.quote(token)))
603 print("export ARVADOS_API_HOST={}".format(pipes.quote(host)))
604 print("export ARVADOS_API_HOST_INSECURE=true")
607 elif args.action == 'stop':
608 stop(force=('ARVADOS_TEST_API_HOST' not in os.environ))
609 elif args.action == 'start_keep':
611 elif args.action == 'stop_keep':
613 elif args.action == 'start_keep_proxy':
615 elif args.action == 'stop_keep_proxy':
617 elif args.action == 'start_arv-git-httpd':
619 elif args.action == 'stop_arv-git-httpd':
621 elif args.action == 'start_nginx':
623 elif args.action == 'stop_nginx':
626 raise Exception("action recognized but not implemented!?")