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 enforce_permissions = False
49 def find_server_pid(PID_PATH, wait=10):
53 while (not good_pid) and (now <= timeout):
56 with open(PID_PATH, 'r') as f:
57 server_pid = int(f.read())
58 good_pid = (os.kill(server_pid, 0) is None)
70 def kill_server_pid(pidfile, wait=10, passenger_root=False):
71 # Must re-import modules in order to work during atexit
78 # First try to shut down nicely
79 restore_cwd = os.getcwd()
80 os.chdir(passenger_root)
82 'bundle', 'exec', 'passenger', 'stop', '--pid-file', pidfile])
86 with open(pidfile, 'r') as f:
87 server_pid = int(f.read())
89 if not passenger_root or timeout - now < wait / 2:
90 # Half timeout has elapsed. Start sending SIGTERM
91 os.kill(server_pid, signal.SIGTERM)
92 # Raise OSError if process has disappeared
93 os.getpgid(server_pid)
101 def find_available_port():
102 """Return an IPv4 port number that is not in use right now.
104 We assume whoever needs to use the returned port is able to reuse
105 a recently used port without waiting for TIME_WAIT (see
106 SO_REUSEADDR / SO_REUSEPORT).
108 Some opportunity for races here, but it's better than choosing
109 something at random and not checking at all. If all of our servers
110 (hey Passenger) knew that listening on port 0 was a thing, the OS
111 would take care of the races, and this wouldn't be needed at all.
114 sock = socket.socket()
115 sock.bind(('0.0.0.0', 0))
116 port = sock.getsockname()[1]
120 def _wait_until_port_listens(port, timeout=10):
121 """Wait for a process to start listening on the given port.
123 If nothing listens on the port within the specified timeout (given
124 in seconds), print a warning on stderr before returning.
127 subprocess.check_output(['which', 'lsof'])
128 except subprocess.CalledProcessError:
129 print("WARNING: No `lsof` -- cannot wait for port to listen. "+
130 "Sleeping 0.5 and hoping for the best.")
133 deadline = time.time() + timeout
134 while time.time() < deadline:
136 subprocess.check_output(
137 ['lsof', '-t', '-i', 'tcp:'+str(port)])
138 except subprocess.CalledProcessError:
143 "WARNING: Nothing is listening on port {} (waited {} seconds).".
144 format(port, timeout),
147 def run(leave_running_atexit=False):
148 """Ensure an API server is running, and ARVADOS_API_* env vars have
149 admin credentials for it.
151 If ARVADOS_TEST_API_HOST is set, a parent process has started a
152 test server for us to use: we just need to reset() it using the
155 If a previous call to run() started a new server process, and it
156 is still running, we just need to reset() it to fixture state and
159 If neither of those options work out, we'll really start a new
164 # Delete cached discovery document.
165 shutil.rmtree(arvados.http_cache('discovery'))
167 pid_file = os.path.join(SERVICES_SRC_DIR, 'api', SERVER_PID_PATH)
168 pid_file_ok = find_server_pid(pid_file, 0)
170 existing_api_host = os.environ.get('ARVADOS_TEST_API_HOST', my_api_host)
171 if existing_api_host and pid_file_ok:
172 if existing_api_host == my_api_host:
176 # Fall through to shutdown-and-start case.
179 # Server was provided by parent. Can't recover if it's
183 # Before trying to start up our own server, call stop() to avoid
184 # "Phusion Passenger Standalone is already running on PID 12345".
185 # (If we've gotten this far, ARVADOS_TEST_API_HOST isn't set, so
186 # we know the server is ours to kill.)
189 restore_cwd = os.getcwd()
190 api_src_dir = os.path.join(SERVICES_SRC_DIR, 'api')
191 os.chdir(api_src_dir)
193 # Either we haven't started a server of our own yet, or it has
194 # died, or we have lost our credentials, or something else is
195 # preventing us from calling reset(). Start a new one.
197 if not os.path.exists('tmp'):
200 if not os.path.exists('tmp/api'):
201 os.makedirs('tmp/api')
203 if not os.path.exists('tmp/logs'):
204 os.makedirs('tmp/logs')
206 if not os.path.exists('tmp/self-signed.pem'):
207 # We assume here that either passenger reports its listening
208 # address as https:/0.0.0.0:port/. If it reports "127.0.0.1"
209 # then the certificate won't match the host and reset() will
210 # fail certificate verification. If it reports "localhost",
211 # clients (notably Python SDK's websocket client) might
212 # resolve localhost as ::1 and then fail to connect.
213 subprocess.check_call([
214 'openssl', 'req', '-new', '-x509', '-nodes',
215 '-out', 'tmp/self-signed.pem',
216 '-keyout', 'tmp/self-signed.key',
218 '-subj', '/CN=0.0.0.0'],
221 # Install the git repository fixtures.
222 gitdir = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'git')
223 gittarball = os.path.join(SERVICES_SRC_DIR, 'api', 'test', 'test.git.tar')
224 if not os.path.isdir(gitdir):
226 subprocess.check_output(['tar', '-xC', gitdir, '-f', gittarball])
228 port = find_available_port()
229 env = os.environ.copy()
230 env['RAILS_ENV'] = 'test'
231 env['ARVADOS_WEBSOCKETS'] = 'yes'
232 env.pop('ARVADOS_TEST_API_HOST', None)
233 env.pop('ARVADOS_API_HOST', None)
234 env.pop('ARVADOS_API_HOST_INSECURE', None)
235 env.pop('ARVADOS_API_TOKEN', None)
236 start_msg = subprocess.check_output(
238 'passenger', 'start', '-d', '-p{}'.format(port),
239 '--pid-file', os.path.join(os.getcwd(), pid_file),
240 '--log-file', os.path.join(os.getcwd(), 'log/test.log'),
242 '--ssl-certificate', 'tmp/self-signed.pem',
243 '--ssl-certificate-key', 'tmp/self-signed.key'],
246 if not leave_running_atexit:
247 atexit.register(kill_server_pid, pid_file, passenger_root=api_src_dir)
249 match = re.search(r'Accessible via: https://(.*?)/', start_msg)
252 "Passenger did not report endpoint: {}".format(start_msg))
253 my_api_host = match.group(1)
254 os.environ['ARVADOS_API_HOST'] = my_api_host
256 # Make sure the server has written its pid file and started
257 # listening on its TCP port
258 find_server_pid(pid_file)
259 _wait_until_port_listens(port)
262 os.chdir(restore_cwd)
265 """Reset the test server to fixture state.
267 This resets the ARVADOS_TEST_API_HOST provided by a parent process
268 if any, otherwise the server started by run().
270 It also resets ARVADOS_* environment vars to point to the test
271 server with admin credentials.
273 existing_api_host = os.environ.get('ARVADOS_TEST_API_HOST', my_api_host)
274 token = auth_token('admin')
275 httpclient = httplib2.Http(ca_certs=os.path.join(
276 SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.pem'))
278 'https://{}/database/reset'.format(existing_api_host),
280 headers={'Authorization': 'OAuth2 {}'.format(token)})
281 os.environ['ARVADOS_API_HOST_INSECURE'] = 'true'
282 os.environ['ARVADOS_API_HOST'] = existing_api_host
283 os.environ['ARVADOS_API_TOKEN'] = token
285 def stop(force=False):
286 """Stop the API server, if one is running.
288 If force==False, kill it only if we started it ourselves. (This
289 supports the use case where a Python test suite calls run(), but
290 run() just uses the ARVADOS_TEST_API_HOST provided by the parent
291 process, and the test suite cleans up after itself by calling
292 stop(). In this case the test server provided by the parent
293 process should be left alone.)
295 If force==True, kill it even if we didn't start it
296 ourselves. (This supports the use case in __main__, where "run"
297 and "stop" happen in different processes.)
300 if force or my_api_host is not None:
301 kill_server_pid(os.path.join(SERVICES_SRC_DIR, 'api', SERVER_PID_PATH))
304 def _start_keep(n, keep_args):
305 keep0 = tempfile.mkdtemp()
306 port = find_available_port()
307 keep_cmd = ["keepstore",
308 "-volume={}".format(keep0),
309 "-listen=:{}".format(port),
310 "-pid="+_pidfile('keep{}'.format(n))]
312 for arg, val in keep_args.iteritems():
313 keep_cmd.append("{}={}".format(arg, val))
315 logf = open(os.path.join(TEST_TMPDIR, 'keep{}.log'.format(n)), 'a+')
316 kp0 = subprocess.Popen(
317 keep_cmd, stdin=open('/dev/null'), stdout=logf, stderr=logf, close_fds=True)
318 with open(_pidfile('keep{}'.format(n)), 'w') as f:
319 f.write(str(kp0.pid))
321 with open("{}/keep{}.volume".format(TEST_TMPDIR, n), 'w') as f:
324 _wait_until_port_listens(port)
328 def run_keep(blob_signing_key=None):
329 if not keep_existing:
333 if not blob_signing_key:
334 blob_signing_key = 'zfhgfenhffzltr9dixws36j1yhksjoll2grmku38mi7yxd66h5j4q9w4jzanezacp8s6q0ro3hxakfye02152hncy6zml2ed0uc'
335 with open(os.path.join(TEST_TMPDIR, "keep.blob_signing_key"), "w") as f:
336 keep_args['-blob-signing-key-file'] = f.name
337 f.write(blob_signing_key)
338 if enforce_permissions:
339 keep_args['-enforce-permissions'] = 'true'
340 with open(os.path.join(TEST_TMPDIR, "keep.data-manager-token-file"), "w") as f:
341 keep_args['-data-manager-token-file'] = f.name
342 f.write(os.environ['ARVADOS_API_TOKEN'])
343 keep_args['-never-delete'] = 'false'
347 host=os.environ['ARVADOS_API_HOST'],
348 token=os.environ['ARVADOS_API_TOKEN'],
351 for d in api.keep_services().list().execute()['items']:
352 api.keep_services().delete(uuid=d['uuid']).execute()
353 for d in api.keep_disks().list().execute()['items']:
354 api.keep_disks().delete(uuid=d['uuid']).execute()
361 for d in range(start_index, end_index):
362 port = _start_keep(d, keep_args)
363 svc = api.keep_services().create(body={'keep_service': {
364 'uuid': 'zzzzz-bi6l4-keepdisk{:07d}'.format(d),
365 'service_host': 'localhost',
366 'service_port': port,
367 'service_type': 'disk',
368 'service_ssl_flag': False,
370 api.keep_disks().create(body={
371 'keep_disk': {'keep_service_uuid': svc['uuid'] }
375 kill_server_pid(_pidfile('keep{}'.format(n)), 0)
376 if os.path.exists("{}/keep{}.volume".format(TEST_TMPDIR, n)):
377 with open("{}/keep{}.volume".format(TEST_TMPDIR, n), 'r') as r:
378 shutil.rmtree(r.read(), True)
379 os.unlink("{}/keep{}.volume".format(TEST_TMPDIR, n))
380 if os.path.exists(os.path.join(TEST_TMPDIR, "keep.blob_signing_key")):
381 os.remove(os.path.join(TEST_TMPDIR, "keep.blob_signing_key"))
386 # We may have created an additional keep servers when keep_existing is used
389 def run_keep_proxy():
390 if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
394 admin_token = auth_token('admin')
395 port = find_available_port()
396 env = os.environ.copy()
397 env['ARVADOS_API_TOKEN'] = admin_token
398 kp = subprocess.Popen(
400 '-pid='+_pidfile('keepproxy'),
401 '-listen=:{}'.format(port)],
402 env=env, stdin=open('/dev/null'), stdout=sys.stderr)
406 host=os.environ['ARVADOS_API_HOST'],
409 for d in api.keep_services().list(
410 filters=[['service_type','=','proxy']]).execute()['items']:
411 api.keep_services().delete(uuid=d['uuid']).execute()
412 api.keep_services().create(body={'keep_service': {
413 'service_host': 'localhost',
414 'service_port': port,
415 'service_type': 'proxy',
416 'service_ssl_flag': False,
418 os.environ["ARVADOS_KEEP_PROXY"] = "http://localhost:{}".format(port)
419 _setport('keepproxy', port)
420 _wait_until_port_listens(port)
422 def stop_keep_proxy():
423 if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
425 kill_server_pid(_pidfile('keepproxy'), wait=0)
427 def run_arv_git_httpd():
428 if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
432 gitdir = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'git')
433 gitport = find_available_port()
434 env = os.environ.copy()
435 env.pop('ARVADOS_API_TOKEN', None)
436 agh = subprocess.Popen(
438 '-repo-root='+gitdir+'/test',
439 '-address=:'+str(gitport)],
440 env=env, stdin=open('/dev/null'), stdout=sys.stderr)
441 with open(_pidfile('arv-git-httpd'), 'w') as f:
442 f.write(str(agh.pid))
443 _setport('arv-git-httpd', gitport)
444 _wait_until_port_listens(gitport)
446 def stop_arv_git_httpd():
447 if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
449 kill_server_pid(_pidfile('arv-git-httpd'), wait=0)
452 if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
455 nginxconf['KEEPPROXYPORT'] = _getport('keepproxy')
456 nginxconf['KEEPPROXYSSLPORT'] = find_available_port()
457 nginxconf['GITPORT'] = _getport('arv-git-httpd')
458 nginxconf['GITSSLPORT'] = find_available_port()
459 nginxconf['SSLCERT'] = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.pem')
460 nginxconf['SSLKEY'] = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.key')
462 conftemplatefile = os.path.join(MY_DIRNAME, 'nginx.conf')
463 conffile = os.path.join(TEST_TMPDIR, 'nginx.conf')
464 with open(conffile, 'w') as f:
467 lambda match: str(nginxconf.get(match.group(1))),
468 open(conftemplatefile).read()))
470 env = os.environ.copy()
471 env['PATH'] = env['PATH']+':/sbin:/usr/sbin:/usr/local/sbin'
472 nginx = subprocess.Popen(
474 '-g', 'error_log stderr info;',
475 '-g', 'pid '+_pidfile('nginx')+';',
477 env=env, stdin=open('/dev/null'), stdout=sys.stderr)
478 _setport('keepproxy-ssl', nginxconf['KEEPPROXYSSLPORT'])
479 _setport('arv-git-httpd-ssl', nginxconf['GITSSLPORT'])
482 if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
484 kill_server_pid(_pidfile('nginx'), wait=0)
486 def _pidfile(program):
487 return os.path.join(TEST_TMPDIR, program + '.pid')
489 def _portfile(program):
490 return os.path.join(TEST_TMPDIR, program + '.port')
492 def _setport(program, port):
493 with open(_portfile(program), 'w') as f:
496 # Returns 9 if program is not up.
497 def _getport(program):
499 return int(open(_portfile(program)).read())
505 return _cached_config[key]
506 def _load(f, required=True):
507 fullpath = os.path.join(SERVICES_SRC_DIR, 'api', 'config', f)
508 if not required and not os.path.exists(fullpath):
510 return yaml.load(fullpath)
511 cdefault = _load('application.default.yml')
512 csite = _load('application.yml', required=False)
514 for section in [cdefault.get('common',{}), cdefault.get('test',{}),
515 csite.get('common',{}), csite.get('test',{})]:
516 _cached_config.update(section)
517 return _cached_config[key]
520 '''load a fixture yaml file'''
521 with open(os.path.join(SERVICES_SRC_DIR, 'api', "test", "fixtures",
525 trim_index = yaml_file.index("# Test Helper trims the rest of the file")
526 yaml_file = yaml_file[0:trim_index]
529 return yaml.load(yaml_file)
531 def auth_token(token_name):
532 return fixture("api_client_authorizations")[token_name]["api_token"]
534 def authorize_with(token_name):
535 '''token_name is the symbolic name of the token from the api_client_authorizations fixture'''
536 arvados.config.settings()["ARVADOS_API_TOKEN"] = auth_token(token_name)
537 arvados.config.settings()["ARVADOS_API_HOST"] = os.environ.get("ARVADOS_API_HOST")
538 arvados.config.settings()["ARVADOS_API_HOST_INSECURE"] = "true"
540 class TestCaseWithServers(unittest.TestCase):
541 """TestCase to start and stop supporting Arvados servers.
543 Define any of MAIN_SERVER, KEEP_SERVER, and/or KEEP_PROXY_SERVER
544 class variables as a dictionary of keyword arguments. If you do,
545 setUpClass will start the corresponding servers by passing these
546 keyword arguments to the run, run_keep, and/or run_keep_server
547 functions, respectively. It will also set Arvados environment
548 variables to point to these servers appropriately. If you don't
549 run a Keep or Keep proxy server, setUpClass will set up a
550 temporary directory for Keep local storage, and set it as
553 tearDownClass will stop any servers started, and restore the
554 original environment.
558 KEEP_PROXY_SERVER = None
561 def _restore_dict(src, dest):
562 for key in dest.keys():
569 cls._orig_environ = os.environ.copy()
570 cls._orig_config = arvados.config.settings().copy()
571 cls._cleanup_funcs = []
572 os.environ.pop('ARVADOS_KEEP_PROXY', None)
573 os.environ.pop('ARVADOS_EXTERNAL_CLIENT', None)
574 for server_kwargs, start_func, stop_func in (
575 (cls.MAIN_SERVER, run, reset),
576 (cls.KEEP_SERVER, run_keep, stop_keep),
577 (cls.KEEP_PROXY_SERVER, run_keep_proxy, stop_keep_proxy)):
578 if server_kwargs is not None:
579 start_func(**server_kwargs)
580 cls._cleanup_funcs.append(stop_func)
581 if (cls.KEEP_SERVER is None) and (cls.KEEP_PROXY_SERVER is None):
582 cls.local_store = tempfile.mkdtemp()
583 os.environ['KEEP_LOCAL_STORE'] = cls.local_store
584 cls._cleanup_funcs.append(
585 lambda: shutil.rmtree(cls.local_store, ignore_errors=True))
587 os.environ.pop('KEEP_LOCAL_STORE', None)
588 arvados.config.initialize()
591 def tearDownClass(cls):
592 for clean_func in cls._cleanup_funcs:
594 cls._restore_dict(cls._orig_environ, os.environ)
595 cls._restore_dict(cls._orig_config, arvados.config.settings())
598 if __name__ == "__main__":
601 'start_keep', 'stop_keep',
602 'start_keep_proxy', 'stop_keep_proxy',
603 'start_arv-git-httpd', 'stop_arv-git-httpd',
604 'start_nginx', 'stop_nginx',
606 parser = argparse.ArgumentParser()
607 parser.add_argument('action', type=str, help="one of {}".format(actions))
608 parser.add_argument('--auth', type=str, metavar='FIXTURE_NAME', help='Print authorization info for given api_client_authorizations fixture')
609 parser.add_argument('--keep-existing', type=str, help="Used to add additional keep servers, without terminating existing servers")
610 parser.add_argument('--keep-enforce-permissions', type=str, help="Enforce keep permissions")
612 args = parser.parse_args()
614 if args.keep_existing == 'true':
616 if args.keep_enforce_permissions == 'true':
617 enforce_permissions = True
619 if args.action not in actions:
620 print("Unrecognized action '{}'. Actions are: {}.".format(args.action, actions), file=sys.stderr)
622 if args.action == 'start':
623 stop(force=('ARVADOS_TEST_API_HOST' not in os.environ))
624 run(leave_running_atexit=True)
625 host = os.environ['ARVADOS_API_HOST']
626 if args.auth is not None:
627 token = auth_token(args.auth)
628 print("export ARVADOS_API_TOKEN={}".format(pipes.quote(token)))
629 print("export ARVADOS_API_HOST={}".format(pipes.quote(host)))
630 print("export ARVADOS_API_HOST_INSECURE=true")
633 elif args.action == 'stop':
634 stop(force=('ARVADOS_TEST_API_HOST' not in os.environ))
635 elif args.action == 'start_keep':
637 elif args.action == 'stop_keep':
639 elif args.action == 'start_keep_proxy':
641 elif args.action == 'stop_keep_proxy':
643 elif args.action == 'start_arv-git-httpd':
645 elif args.action == 'stop_arv-git-httpd':
647 elif args.action == 'start_nginx':
649 elif args.action == 'stop_nginx':
652 raise Exception("action recognized but not implemented!?")