20 MY_DIRNAME = os.path.dirname(os.path.realpath(__file__))
21 if __name__ == '__main__' and os.path.exists(
22 os.path.join(MY_DIRNAME, '..', 'arvados', '__init__.py')):
23 # We're being launched to support another test suite.
24 # Add the Python SDK source to the library path.
25 sys.path.insert(1, os.path.dirname(MY_DIRNAME))
30 ARVADOS_DIR = os.path.realpath(os.path.join(MY_DIRNAME, '../../..'))
31 SERVICES_SRC_DIR = os.path.join(ARVADOS_DIR, 'services')
32 SERVER_PID_PATH = 'tmp/pids/test-server.pid'
33 if 'GOPATH' in os.environ:
34 gopaths = os.environ['GOPATH'].split(':')
35 gobins = [os.path.join(path, 'bin') for path in gopaths]
36 os.environ['PATH'] = ':'.join(gobins) + ':' + os.environ['PATH']
38 TEST_TMPDIR = os.path.join(ARVADOS_DIR, 'tmp')
39 if not os.path.exists(TEST_TMPDIR):
44 def find_server_pid(PID_PATH, wait=10):
48 while (not good_pid) and (now <= timeout):
51 with open(PID_PATH, 'r') as f:
52 server_pid = int(f.read())
53 good_pid = (os.kill(server_pid, 0) is None)
65 def kill_server_pid(pidfile, wait=10, passenger_root=False):
66 # Must re-import modules in order to work during atexit
73 # First try to shut down nicely
74 restore_cwd = os.getcwd()
75 os.chdir(passenger_root)
77 'bundle', 'exec', 'passenger', 'stop', '--pid-file', pidfile])
81 with open(pidfile, 'r') as f:
82 server_pid = int(f.read())
84 if not passenger_root or timeout - now < wait / 2:
85 # Half timeout has elapsed. Start sending SIGTERM
86 os.kill(server_pid, signal.SIGTERM)
87 # Raise OSError if process has disappeared
88 os.getpgid(server_pid)
96 def find_available_port():
97 """Return a port number that is not in use right now.
99 Some opportunity for races here, but it's better than choosing
100 something at random and not checking at all. If all of our servers
101 (hey Passenger) knew that listening on port 0 was a thing, the OS
102 would take care of the races, and this wouldn't be needed at all.
106 port = random.randint(20000, 40000)
107 port_hex = ':%04x ' % port
109 with open('/proc/net/tcp', 'r') as f:
111 if 0 <= string.find(line, port_hex):
115 # This isn't going so well. Just use the random port.
121 def run(leave_running_atexit=False):
122 """Ensure an API server is running, and ARVADOS_API_* env vars have
123 admin credentials for it.
125 If ARVADOS_TEST_API_HOST is set, a parent process has started a
126 test server for us to use: we just need to reset() it using the
129 If a previous call to run() started a new server process, and it
130 is still running, we just need to reset() it to fixture state and
133 If neither of those options work out, we'll really start a new
138 # Delete cached discovery document.
139 shutil.rmtree(arvados.http_cache('discovery'))
141 pid_file = os.path.join(SERVICES_SRC_DIR, 'api', SERVER_PID_PATH)
142 pid_file_ok = find_server_pid(pid_file, 0)
144 existing_api_host = os.environ.get('ARVADOS_TEST_API_HOST', my_api_host)
145 if existing_api_host and pid_file_ok:
146 if existing_api_host == my_api_host:
150 # Fall through to shutdown-and-start case.
153 # Server was provided by parent. Can't recover if it's
157 # Before trying to start up our own server, call stop() to avoid
158 # "Phusion Passenger Standalone is already running on PID 12345".
159 # (If we've gotten this far, ARVADOS_TEST_API_HOST isn't set, so
160 # we know the server is ours to kill.)
163 restore_cwd = os.getcwd()
164 api_src_dir = os.path.join(SERVICES_SRC_DIR, 'api')
165 os.chdir(api_src_dir)
167 # Either we haven't started a server of our own yet, or it has
168 # died, or we have lost our credentials, or something else is
169 # preventing us from calling reset(). Start a new one.
171 if not os.path.exists('tmp/self-signed.pem'):
172 # We assume here that either passenger reports its listening
173 # address as https:/0.0.0.0:port/. If it reports "127.0.0.1"
174 # then the certificate won't match the host and reset() will
175 # fail certificate verification. If it reports "localhost",
176 # clients (notably Python SDK's websocket client) might
177 # resolve localhost as ::1 and then fail to connect.
178 subprocess.check_call([
179 'openssl', 'req', '-new', '-x509', '-nodes',
180 '-out', 'tmp/self-signed.pem',
181 '-keyout', 'tmp/self-signed.key',
183 '-subj', '/CN=0.0.0.0'])
185 port = find_available_port()
186 env = os.environ.copy()
187 env['RAILS_ENV'] = 'test'
188 env['ARVADOS_WEBSOCKETS'] = 'yes'
189 env.pop('ARVADOS_TEST_API_HOST', None)
190 env.pop('ARVADOS_API_HOST', None)
191 env.pop('ARVADOS_API_HOST_INSECURE', None)
192 env.pop('ARVADOS_API_TOKEN', None)
193 start_msg = subprocess.check_output(
195 'passenger', 'start', '-d', '-p{}'.format(port),
196 '--pid-file', os.path.join(os.getcwd(), pid_file),
197 '--log-file', os.path.join(os.getcwd(), 'log/test.log'),
199 '--ssl-certificate', 'tmp/self-signed.pem',
200 '--ssl-certificate-key', 'tmp/self-signed.key'],
203 if not leave_running_atexit:
204 atexit.register(kill_server_pid, pid_file, passenger_root=api_src_dir)
206 match = re.search(r'Accessible via: https://(.*?)/', start_msg)
209 "Passenger did not report endpoint: {}".format(start_msg))
210 my_api_host = match.group(1)
211 os.environ['ARVADOS_API_HOST'] = my_api_host
213 # Make sure the server has written its pid file before continuing
214 find_server_pid(pid_file)
217 os.chdir(restore_cwd)
220 """Reset the test server to fixture state.
222 This resets the ARVADOS_TEST_API_HOST provided by a parent process
223 if any, otherwise the server started by run().
225 It also resets ARVADOS_* environment vars to point to the test
226 server with admin credentials.
228 existing_api_host = os.environ.get('ARVADOS_TEST_API_HOST', my_api_host)
229 token = auth_token('admin')
230 httpclient = httplib2.Http(ca_certs=os.path.join(
231 SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.pem'))
233 'https://{}/database/reset'.format(existing_api_host),
235 headers={'Authorization': 'OAuth2 {}'.format(token)})
236 os.environ['ARVADOS_API_HOST_INSECURE'] = 'true'
237 os.environ['ARVADOS_API_HOST'] = existing_api_host
238 os.environ['ARVADOS_API_TOKEN'] = token
240 def stop(force=False):
241 """Stop the API server, if one is running.
243 If force==False, kill it only if we started it ourselves. (This
244 supports the use case where a Python test suite calls run(), but
245 run() just uses the ARVADOS_TEST_API_HOST provided by the parent
246 process, and the test suite cleans up after itself by calling
247 stop(). In this case the test server provided by the parent
248 process should be left alone.)
250 If force==True, kill it even if we didn't start it
251 ourselves. (This supports the use case in __main__, where "run"
252 and "stop" happen in different processes.)
255 if force or my_api_host is not None:
256 kill_server_pid(os.path.join(SERVICES_SRC_DIR, 'api', SERVER_PID_PATH))
259 def _start_keep(n, keep_args):
260 keep0 = tempfile.mkdtemp()
261 port = find_available_port()
262 keep_cmd = ["keepstore",
263 "-volumes={}".format(keep0),
264 "-listen=:{}".format(port),
265 "-pid={}".format("{}/keep{}.pid".format(TEST_TMPDIR, n))]
267 for arg, val in keep_args.iteritems():
268 keep_cmd.append("{}={}".format(arg, val))
270 kp0 = subprocess.Popen(keep_cmd)
271 with open("{}/keep{}.pid".format(TEST_TMPDIR, n), 'w') as f:
272 f.write(str(kp0.pid))
274 with open("{}/keep{}.volume".format(TEST_TMPDIR, n), 'w') as f:
279 def run_keep(blob_signing_key=None, enforce_permissions=False):
284 with open(os.path.join(TEST_TMPDIR, "keep.blob_signing_key"), "w") as f:
285 keep_args['--permission-key-file'] = f.name
286 f.write(blob_signing_key)
287 if enforce_permissions:
288 keep_args['--enforce-permissions'] = 'true'
292 host=os.environ['ARVADOS_API_HOST'],
293 token=os.environ['ARVADOS_API_TOKEN'],
295 for d in api.keep_services().list().execute()['items']:
296 api.keep_services().delete(uuid=d['uuid']).execute()
297 for d in api.keep_disks().list().execute()['items']:
298 api.keep_disks().delete(uuid=d['uuid']).execute()
300 for d in range(0, 2):
301 port = _start_keep(d, keep_args)
302 svc = api.keep_services().create(body={'keep_service': {
303 'uuid': 'zzzzz-bi6l4-keepdisk{:07d}'.format(d),
304 'service_host': 'localhost',
305 'service_port': port,
306 'service_type': 'disk',
307 'service_ssl_flag': False,
309 api.keep_disks().create(body={
310 'keep_disk': {'keep_service_uuid': svc['uuid'] }
314 kill_server_pid("{}/keep{}.pid".format(TEST_TMPDIR, n), 0)
315 if os.path.exists("{}/keep{}.volume".format(TEST_TMPDIR, n)):
316 with open("{}/keep{}.volume".format(TEST_TMPDIR, n), 'r') as r:
317 shutil.rmtree(r.read(), True)
318 os.unlink("{}/keep{}.volume".format(TEST_TMPDIR, n))
319 if os.path.exists(os.path.join(TEST_TMPDIR, "keep.blob_signing_key")):
320 os.remove(os.path.join(TEST_TMPDIR, "keep.blob_signing_key"))
326 def run_keep_proxy():
329 admin_token = auth_token('admin')
330 port = find_available_port()
331 env = os.environ.copy()
332 env['ARVADOS_API_TOKEN'] = admin_token
333 kp = subprocess.Popen(
335 '-pid={}/keepproxy.pid'.format(TEST_TMPDIR),
336 '-listen=:{}'.format(port)],
341 host=os.environ['ARVADOS_API_HOST'],
344 for d in api.keep_services().list(
345 filters=[['service_type','=','proxy']]).execute()['items']:
346 api.keep_services().delete(uuid=d['uuid']).execute()
347 api.keep_services().create(body={'keep_service': {
348 'service_host': 'localhost',
349 'service_port': port,
350 'service_type': 'proxy',
351 'service_ssl_flag': False,
353 os.environ["ARVADOS_KEEP_PROXY"] = "http://localhost:{}".format(port)
355 def stop_keep_proxy():
356 kill_server_pid(os.path.join(TEST_TMPDIR, "keepproxy.pid"), 0)
359 '''load a fixture yaml file'''
360 with open(os.path.join(SERVICES_SRC_DIR, 'api', "test", "fixtures",
364 trim_index = yaml_file.index("# Test Helper trims the rest of the file")
365 yaml_file = yaml_file[0:trim_index]
368 return yaml.load(yaml_file)
370 def auth_token(token_name):
371 return fixture("api_client_authorizations")[token_name]["api_token"]
373 def authorize_with(token_name):
374 '''token_name is the symbolic name of the token from the api_client_authorizations fixture'''
375 arvados.config.settings()["ARVADOS_API_TOKEN"] = auth_token(token_name)
376 arvados.config.settings()["ARVADOS_API_HOST"] = os.environ.get("ARVADOS_API_HOST")
377 arvados.config.settings()["ARVADOS_API_HOST_INSECURE"] = "true"
379 class TestCaseWithServers(unittest.TestCase):
380 """TestCase to start and stop supporting Arvados servers.
382 Define any of MAIN_SERVER, KEEP_SERVER, and/or KEEP_PROXY_SERVER
383 class variables as a dictionary of keyword arguments. If you do,
384 setUpClass will start the corresponding servers by passing these
385 keyword arguments to the run, run_keep, and/or run_keep_server
386 functions, respectively. It will also set Arvados environment
387 variables to point to these servers appropriately. If you don't
388 run a Keep or Keep proxy server, setUpClass will set up a
389 temporary directory for Keep local storage, and set it as
392 tearDownClass will stop any servers started, and restore the
393 original environment.
397 KEEP_PROXY_SERVER = None
400 def _restore_dict(src, dest):
401 for key in dest.keys():
408 cls._orig_environ = os.environ.copy()
409 cls._orig_config = arvados.config.settings().copy()
410 cls._cleanup_funcs = []
411 os.environ.pop('ARVADOS_KEEP_PROXY', None)
412 os.environ.pop('ARVADOS_EXTERNAL_CLIENT', None)
413 for server_kwargs, start_func, stop_func in (
414 (cls.MAIN_SERVER, run, reset),
415 (cls.KEEP_SERVER, run_keep, stop_keep),
416 (cls.KEEP_PROXY_SERVER, run_keep_proxy, stop_keep_proxy)):
417 if server_kwargs is not None:
418 start_func(**server_kwargs)
419 cls._cleanup_funcs.append(stop_func)
420 if (cls.KEEP_SERVER is None) and (cls.KEEP_PROXY_SERVER is None):
421 cls.local_store = tempfile.mkdtemp()
422 os.environ['KEEP_LOCAL_STORE'] = cls.local_store
423 cls._cleanup_funcs.append(
424 lambda: shutil.rmtree(cls.local_store, ignore_errors=True))
426 os.environ.pop('KEEP_LOCAL_STORE', None)
427 arvados.config.initialize()
430 def tearDownClass(cls):
431 for clean_func in cls._cleanup_funcs:
433 cls._restore_dict(cls._orig_environ, os.environ)
434 cls._restore_dict(cls._orig_config, arvados.config.settings())
437 if __name__ == "__main__":
438 actions = ['start', 'stop',
439 'start_keep', 'stop_keep',
440 'start_keep_proxy', 'stop_keep_proxy']
441 parser = argparse.ArgumentParser()
442 parser.add_argument('action', type=str, help="one of {}".format(actions))
443 parser.add_argument('--auth', type=str, metavar='FIXTURE_NAME', help='Print authorization info for given api_client_authorizations fixture')
444 args = parser.parse_args()
446 if args.action == 'start':
447 stop(force=('ARVADOS_TEST_API_HOST' not in os.environ))
448 run(leave_running_atexit=True)
449 host = os.environ['ARVADOS_API_HOST']
450 if args.auth is not None:
451 token = auth_token(args.auth)
452 print("export ARVADOS_API_TOKEN={}".format(pipes.quote(token)))
453 print("export ARVADOS_API_HOST={}".format(pipes.quote(host)))
454 print("export ARVADOS_API_HOST_INSECURE=true")
457 elif args.action == 'stop':
458 stop(force=('ARVADOS_TEST_API_HOST' not in os.environ))
459 elif args.action == 'start_keep':
461 elif args.action == 'stop_keep':
463 elif args.action == 'start_keep_proxy':
465 elif args.action == 'stop_keep_proxy':
468 print("Unrecognized action '{}'. Actions are: {}.".format(args.action, actions))