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 os.environ['ARVADOS_API_TOKEN'] = auth_token('admin')
142 os.environ['ARVADOS_API_HOST_INSECURE'] = 'true'
144 pid_file = os.path.join(SERVICES_SRC_DIR, 'api', SERVER_PID_PATH)
145 pid_file_ok = find_server_pid(pid_file, 0)
147 existing_api_host = os.environ.get('ARVADOS_TEST_API_HOST', my_api_host)
148 if existing_api_host and pid_file_ok:
150 os.environ['ARVADOS_API_HOST'] = existing_api_host
156 restore_cwd = os.getcwd()
157 api_src_dir = os.path.join(SERVICES_SRC_DIR, 'api')
158 os.chdir(api_src_dir)
160 # Either we haven't started a server of our own yet, or it has
161 # died, or we have lost our credentials, or something else is
162 # preventing us from calling reset(). Start a new one.
164 if not os.path.exists('tmp/self-signed.pem'):
165 # We assume here that either passenger reports its listening
166 # address as https:/0.0.0.0:port/. If it reports "127.0.0.1"
167 # then the certificate won't match the host and reset() will
168 # fail certificate verification. If it reports "localhost",
169 # clients (notably Python SDK's websocket client) might
170 # resolve localhost as ::1 and then fail to connect.
171 subprocess.check_call([
172 'openssl', 'req', '-new', '-x509', '-nodes',
173 '-out', 'tmp/self-signed.pem',
174 '-keyout', 'tmp/self-signed.key',
176 '-subj', '/CN=0.0.0.0'])
178 port = find_available_port()
179 env = os.environ.copy()
180 env['RAILS_ENV'] = 'test'
181 env['ARVADOS_WEBSOCKETS'] = 'yes'
182 env.pop('ARVADOS_TEST_API_HOST', None)
183 env.pop('ARVADOS_API_HOST', None)
184 env.pop('ARVADOS_API_HOST_INSECURE', None)
185 env.pop('ARVADOS_API_TOKEN', None)
186 start_msg = subprocess.check_output(
188 'passenger', 'start', '-d', '-p{}'.format(port),
189 '--pid-file', os.path.join(os.getcwd(), pid_file),
190 '--log-file', os.path.join(os.getcwd(), 'log/test.log'),
192 '--ssl-certificate', 'tmp/self-signed.pem',
193 '--ssl-certificate-key', 'tmp/self-signed.key'],
196 if not leave_running_atexit:
197 atexit.register(kill_server_pid, pid_file, passenger_root=api_src_dir)
199 match = re.search(r'Accessible via: https://(.*?)/', start_msg)
202 "Passenger did not report endpoint: {}".format(start_msg))
203 my_api_host = match.group(1)
204 os.environ['ARVADOS_API_HOST'] = my_api_host
206 # Make sure the server has written its pid file before continuing
207 find_server_pid(pid_file)
210 os.chdir(restore_cwd)
213 """Reset the test server to fixture state.
215 This resets the ARVADOS_TEST_API_HOST provided by a parent process
216 if any, otherwise the server started by run().
218 existing_api_host = os.environ.get('ARVADOS_TEST_API_HOST', my_api_host)
219 token = auth_token('admin')
220 httpclient = httplib2.Http(ca_certs=os.path.join(
221 SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.pem'))
223 'https://{}/database/reset'.format(existing_api_host),
225 headers={'Authorization': 'OAuth2 {}'.format(token)})
227 def stop(force=False):
228 """Stop the API server, if one is running.
230 If force==False, kill it only if we started it ourselves. (This
231 supports the use case where a Python test suite calls run(), but
232 run() just uses the ARVADOS_TEST_API_HOST provided by the parent
233 process, and the test suite cleans up after itself by calling
234 stop(). In this case the test server provided by the parent
235 process should be left alone.)
237 If force==True, kill it even if we didn't start it
238 ourselves. (This supports the use case in __main__, where "run"
239 and "stop" happen in different processes.)
242 if force or my_api_host is not None:
243 kill_server_pid(os.path.join(SERVICES_SRC_DIR, 'api', SERVER_PID_PATH))
246 def _start_keep(n, keep_args):
247 keep0 = tempfile.mkdtemp()
248 port = find_available_port()
249 keep_cmd = ["keepstore",
250 "-volumes={}".format(keep0),
251 "-listen=:{}".format(port),
252 "-pid={}".format("{}/keep{}.pid".format(TEST_TMPDIR, n))]
254 for arg, val in keep_args.iteritems():
255 keep_cmd.append("{}={}".format(arg, val))
257 kp0 = subprocess.Popen(keep_cmd)
258 with open("{}/keep{}.pid".format(TEST_TMPDIR, n), 'w') as f:
259 f.write(str(kp0.pid))
261 with open("{}/keep{}.volume".format(TEST_TMPDIR, n), 'w') as f:
266 def run_keep(blob_signing_key=None, enforce_permissions=False):
271 with open(os.path.join(TEST_TMPDIR, "keep.blob_signing_key"), "w") as f:
272 keep_args['--permission-key-file'] = f.name
273 f.write(blob_signing_key)
274 if enforce_permissions:
275 keep_args['--enforce-permissions'] = 'true'
279 host=os.environ['ARVADOS_API_HOST'],
280 token=os.environ['ARVADOS_API_TOKEN'],
282 for d in api.keep_services().list().execute()['items']:
283 api.keep_services().delete(uuid=d['uuid']).execute()
284 for d in api.keep_disks().list().execute()['items']:
285 api.keep_disks().delete(uuid=d['uuid']).execute()
287 for d in range(0, 2):
288 port = _start_keep(d, keep_args)
289 svc = api.keep_services().create(body={'keep_service': {
290 'uuid': 'zzzzz-bi6l4-keepdisk{:07d}'.format(d),
291 'service_host': 'localhost',
292 'service_port': port,
293 'service_type': 'disk',
294 'service_ssl_flag': False,
296 api.keep_disks().create(body={
297 'keep_disk': {'keep_service_uuid': svc['uuid'] }
301 kill_server_pid("{}/keep{}.pid".format(TEST_TMPDIR, n), 0)
302 if os.path.exists("{}/keep{}.volume".format(TEST_TMPDIR, n)):
303 with open("{}/keep{}.volume".format(TEST_TMPDIR, n), 'r') as r:
304 shutil.rmtree(r.read(), True)
305 os.unlink("{}/keep{}.volume".format(TEST_TMPDIR, n))
306 if os.path.exists(os.path.join(TEST_TMPDIR, "keep.blob_signing_key")):
307 os.remove(os.path.join(TEST_TMPDIR, "keep.blob_signing_key"))
313 def run_keep_proxy():
316 admin_token = auth_token('admin')
317 port = find_available_port()
318 env = os.environ.copy()
319 env['ARVADOS_API_TOKEN'] = admin_token
320 kp = subprocess.Popen(
322 '-pid={}/keepproxy.pid'.format(TEST_TMPDIR),
323 '-listen=:{}'.format(port)],
328 host=os.environ['ARVADOS_API_HOST'],
331 for d in api.keep_services().list(
332 filters=[['service_type','=','proxy']]).execute()['items']:
333 api.keep_services().delete(uuid=d['uuid']).execute()
334 api.keep_services().create(body={'keep_service': {
335 'service_host': 'localhost',
336 'service_port': port,
337 'service_type': 'proxy',
338 'service_ssl_flag': False,
340 os.environ["ARVADOS_KEEP_PROXY"] = "http://localhost:{}".format(port)
342 def stop_keep_proxy():
343 kill_server_pid(os.path.join(TEST_TMPDIR, "keepproxy.pid"), 0)
346 '''load a fixture yaml file'''
347 with open(os.path.join(SERVICES_SRC_DIR, 'api', "test", "fixtures",
351 trim_index = yaml_file.index("# Test Helper trims the rest of the file")
352 yaml_file = yaml_file[0:trim_index]
355 return yaml.load(yaml_file)
357 def auth_token(token_name):
358 return fixture("api_client_authorizations")[token_name]["api_token"]
360 def authorize_with(token_name):
361 '''token_name is the symbolic name of the token from the api_client_authorizations fixture'''
362 arvados.config.settings()["ARVADOS_API_TOKEN"] = auth_token(token_name)
363 arvados.config.settings()["ARVADOS_API_HOST"] = os.environ.get("ARVADOS_API_HOST")
364 arvados.config.settings()["ARVADOS_API_HOST_INSECURE"] = "true"
366 class TestCaseWithServers(unittest.TestCase):
367 """TestCase to start and stop supporting Arvados servers.
369 Define any of MAIN_SERVER, KEEP_SERVER, and/or KEEP_PROXY_SERVER
370 class variables as a dictionary of keyword arguments. If you do,
371 setUpClass will start the corresponding servers by passing these
372 keyword arguments to the run, run_keep, and/or run_keep_server
373 functions, respectively. It will also set Arvados environment
374 variables to point to these servers appropriately. If you don't
375 run a Keep or Keep proxy server, setUpClass will set up a
376 temporary directory for Keep local storage, and set it as
379 tearDownClass will stop any servers started, and restore the
380 original environment.
384 KEEP_PROXY_SERVER = None
387 def _restore_dict(src, dest):
388 for key in dest.keys():
395 cls._orig_environ = os.environ.copy()
396 cls._orig_config = arvados.config.settings().copy()
397 cls._cleanup_funcs = []
398 os.environ.pop('ARVADOS_KEEP_PROXY', None)
399 os.environ.pop('ARVADOS_EXTERNAL_CLIENT', None)
400 for server_kwargs, start_func, stop_func in (
401 (cls.MAIN_SERVER, run, reset),
402 (cls.KEEP_SERVER, run_keep, stop_keep),
403 (cls.KEEP_PROXY_SERVER, run_keep_proxy, stop_keep_proxy)):
404 if server_kwargs is not None:
405 start_func(**server_kwargs)
406 cls._cleanup_funcs.append(stop_func)
407 if (cls.KEEP_SERVER is None) and (cls.KEEP_PROXY_SERVER is None):
408 cls.local_store = tempfile.mkdtemp()
409 os.environ['KEEP_LOCAL_STORE'] = cls.local_store
410 cls._cleanup_funcs.append(
411 lambda: shutil.rmtree(cls.local_store, ignore_errors=True))
413 os.environ.pop('KEEP_LOCAL_STORE', None)
414 arvados.config.initialize()
417 def tearDownClass(cls):
418 for clean_func in cls._cleanup_funcs:
420 cls._restore_dict(cls._orig_environ, os.environ)
421 cls._restore_dict(cls._orig_config, arvados.config.settings())
424 if __name__ == "__main__":
425 actions = ['start', 'stop',
426 'start_keep', 'stop_keep',
427 'start_keep_proxy', 'stop_keep_proxy']
428 parser = argparse.ArgumentParser()
429 parser.add_argument('action', type=str, help="one of {}".format(actions))
430 parser.add_argument('--auth', type=str, metavar='FIXTURE_NAME', help='Print authorization info for given api_client_authorizations fixture')
431 args = parser.parse_args()
433 if args.action == 'start':
434 stop(force=('ARVADOS_TEST_API_HOST' not in os.environ))
435 run(leave_running_atexit=True)
436 host = os.environ['ARVADOS_API_HOST']
437 if args.auth is not None:
438 token = auth_token(args.auth)
439 print("export ARVADOS_API_TOKEN={}".format(pipes.quote(token)))
440 print("export ARVADOS_API_HOST={}".format(pipes.quote(host)))
441 print("export ARVADOS_API_HOST_INSECURE=true")
444 elif args.action == 'stop':
445 stop(force=('ARVADOS_TEST_API_HOST' not in os.environ))
446 elif args.action == 'start_keep':
448 elif args.action == 'stop_keep':
450 elif args.action == 'start_keep_proxy':
452 elif args.action == 'stop_keep_proxy':
455 print("Unrecognized action '{}'. Actions are: {}.".format(args.action, actions))