15003: Remove NodeProfiles section from integration test config.
[arvados.git] / sdk / python / tests / run_test_server.py
1 # Copyright (C) The Arvados Authors. All rights reserved.
2 #
3 # SPDX-License-Identifier: Apache-2.0
4
5 from __future__ import print_function
6 from __future__ import division
7 from builtins import str
8 from builtins import range
9 import argparse
10 import atexit
11 import errno
12 import glob
13 import httplib2
14 import os
15 import pipes
16 import random
17 import re
18 import shutil
19 import signal
20 import socket
21 import string
22 import subprocess
23 import sys
24 import tempfile
25 import time
26 import unittest
27 import yaml
28
29 MY_DIRNAME = os.path.dirname(os.path.realpath(__file__))
30 if __name__ == '__main__' and os.path.exists(
31       os.path.join(MY_DIRNAME, '..', 'arvados', '__init__.py')):
32     # We're being launched to support another test suite.
33     # Add the Python SDK source to the library path.
34     sys.path.insert(1, os.path.dirname(MY_DIRNAME))
35
36 import arvados
37 import arvados.config
38
39 ARVADOS_DIR = os.path.realpath(os.path.join(MY_DIRNAME, '../../..'))
40 SERVICES_SRC_DIR = os.path.join(ARVADOS_DIR, 'services')
41 if 'GOPATH' in os.environ:
42     # Add all GOPATH bin dirs to PATH -- but insert them after the
43     # ruby gems bin dir, to ensure "bundle" runs the Ruby bundler
44     # command, not the golang.org/x/tools/cmd/bundle command.
45     gopaths = os.environ['GOPATH'].split(':')
46     addbins = [os.path.join(path, 'bin') for path in gopaths]
47     newbins = []
48     for path in os.environ['PATH'].split(':'):
49         newbins.append(path)
50         if os.path.exists(os.path.join(path, 'bundle')):
51             newbins += addbins
52             addbins = []
53     newbins += addbins
54     os.environ['PATH'] = ':'.join(newbins)
55
56 TEST_TMPDIR = os.path.join(ARVADOS_DIR, 'tmp')
57 if not os.path.exists(TEST_TMPDIR):
58     os.mkdir(TEST_TMPDIR)
59
60 my_api_host = None
61 _cached_config = {}
62 _cached_db_config = {}
63
64 def find_server_pid(PID_PATH, wait=10):
65     now = time.time()
66     timeout = now + wait
67     good_pid = False
68     while (not good_pid) and (now <= timeout):
69         time.sleep(0.2)
70         try:
71             with open(PID_PATH, 'r') as f:
72                 server_pid = int(f.read())
73             good_pid = (os.kill(server_pid, 0) is None)
74         except EnvironmentError:
75             good_pid = False
76         now = time.time()
77
78     if not good_pid:
79         return None
80
81     return server_pid
82
83 def kill_server_pid(pidfile, wait=10, passenger_root=False):
84     # Must re-import modules in order to work during atexit
85     import os
86     import signal
87     import subprocess
88     import time
89
90     now = time.time()
91     startTERM = now
92     deadline = now + wait
93
94     if passenger_root:
95         # First try to shut down nicely
96         restore_cwd = os.getcwd()
97         os.chdir(passenger_root)
98         subprocess.call([
99             'bundle', 'exec', 'passenger', 'stop', '--pid-file', pidfile])
100         os.chdir(restore_cwd)
101         # Use up to half of the +wait+ period waiting for "passenger
102         # stop" to work. If the process hasn't exited by then, start
103         # sending TERM signals.
104         startTERM += wait//2
105
106     server_pid = None
107     while now <= deadline and server_pid is None:
108         try:
109             with open(pidfile, 'r') as f:
110                 server_pid = int(f.read())
111         except IOError:
112             # No pidfile = nothing to kill.
113             return
114         except ValueError as error:
115             # Pidfile exists, but we can't parse it. Perhaps the
116             # server has created the file but hasn't written its PID
117             # yet?
118             print("Parse error reading pidfile {}: {}".format(pidfile, error),
119                   file=sys.stderr)
120             time.sleep(0.1)
121             now = time.time()
122
123     while now <= deadline:
124         try:
125             exited, _ = os.waitpid(server_pid, os.WNOHANG)
126             if exited > 0:
127                 _remove_pidfile(pidfile)
128                 return
129         except OSError:
130             # already exited, or isn't our child process
131             pass
132         try:
133             if now >= startTERM:
134                 os.kill(server_pid, signal.SIGTERM)
135                 print("Sent SIGTERM to {} ({})".format(server_pid, pidfile),
136                       file=sys.stderr)
137         except OSError as error:
138             if error.errno == errno.ESRCH:
139                 # Thrown by os.getpgid() or os.kill() if the process
140                 # does not exist, i.e., our work here is done.
141                 _remove_pidfile(pidfile)
142                 return
143             raise
144         time.sleep(0.1)
145         now = time.time()
146
147     print("Server PID {} ({}) did not exit, giving up after {}s".
148           format(server_pid, pidfile, wait),
149           file=sys.stderr)
150
151 def _remove_pidfile(pidfile):
152     try:
153         os.unlink(pidfile)
154     except:
155         if os.path.lexists(pidfile):
156             raise
157
158 def find_available_port():
159     """Return an IPv4 port number that is not in use right now.
160
161     We assume whoever needs to use the returned port is able to reuse
162     a recently used port without waiting for TIME_WAIT (see
163     SO_REUSEADDR / SO_REUSEPORT).
164
165     Some opportunity for races here, but it's better than choosing
166     something at random and not checking at all. If all of our servers
167     (hey Passenger) knew that listening on port 0 was a thing, the OS
168     would take care of the races, and this wouldn't be needed at all.
169     """
170
171     sock = socket.socket()
172     sock.bind(('0.0.0.0', 0))
173     port = sock.getsockname()[1]
174     sock.close()
175     return port
176
177 def _wait_until_port_listens(port, timeout=10, warn=True):
178     """Wait for a process to start listening on the given port.
179
180     If nothing listens on the port within the specified timeout (given
181     in seconds), print a warning on stderr before returning.
182     """
183     try:
184         subprocess.check_output(['which', 'lsof'])
185     except subprocess.CalledProcessError:
186         print("WARNING: No `lsof` -- cannot wait for port to listen. "+
187               "Sleeping 0.5 and hoping for the best.",
188               file=sys.stderr)
189         time.sleep(0.5)
190         return
191     deadline = time.time() + timeout
192     while time.time() < deadline:
193         try:
194             subprocess.check_output(
195                 ['lsof', '-t', '-i', 'tcp:'+str(port)])
196         except subprocess.CalledProcessError:
197             time.sleep(0.1)
198             continue
199         return True
200     if warn:
201         print(
202             "WARNING: Nothing is listening on port {} (waited {} seconds).".
203             format(port, timeout),
204             file=sys.stderr)
205     return False
206
207 def _logfilename(label):
208     """Set up a labelled log file, and return a path to write logs to.
209
210     Normally, the returned path is {tmpdir}/{label}.log.
211
212     In debug mode, logs are also written to stderr, with [label]
213     prepended to each line. The returned path is a FIFO.
214
215     +label+ should contain only alphanumerics: it is also used as part
216     of the FIFO filename.
217
218     """
219     logfilename = os.path.join(TEST_TMPDIR, label+'.log')
220     if not os.environ.get('ARVADOS_DEBUG', ''):
221         return logfilename
222     fifo = os.path.join(TEST_TMPDIR, label+'.fifo')
223     try:
224         os.remove(fifo)
225     except OSError as error:
226         if error.errno != errno.ENOENT:
227             raise
228     os.mkfifo(fifo, 0o700)
229     stdbuf = ['stdbuf', '-i0', '-oL', '-eL']
230     # open(fifo, 'r') would block waiting for someone to open the fifo
231     # for writing, so we need a separate cat process to open it for
232     # us.
233     cat = subprocess.Popen(
234         stdbuf+['cat', fifo],
235         stdin=open('/dev/null'),
236         stdout=subprocess.PIPE)
237     tee = subprocess.Popen(
238         stdbuf+['tee', '-a', logfilename],
239         stdin=cat.stdout,
240         stdout=subprocess.PIPE)
241     subprocess.Popen(
242         stdbuf+['sed', '-e', 's/^/['+label+'] /'],
243         stdin=tee.stdout,
244         stdout=sys.stderr)
245     return fifo
246
247 def run(leave_running_atexit=False):
248     """Ensure an API server is running, and ARVADOS_API_* env vars have
249     admin credentials for it.
250
251     If ARVADOS_TEST_API_HOST is set, a parent process has started a
252     test server for us to use: we just need to reset() it using the
253     admin token fixture.
254
255     If a previous call to run() started a new server process, and it
256     is still running, we just need to reset() it to fixture state and
257     return.
258
259     If neither of those options work out, we'll really start a new
260     server.
261     """
262     global my_api_host
263
264     # Delete cached discovery documents.
265     #
266     # This will clear cached docs that belong to other processes (like
267     # concurrent test suites) even if they're still running. They should
268     # be able to tolerate that.
269     for fn in glob.glob(os.path.join(
270             str(arvados.http_cache('discovery')),
271             '*,arvados,v1,rest,*')):
272         os.unlink(fn)
273
274     pid_file = _pidfile('api')
275     pid_file_ok = find_server_pid(pid_file, 0)
276
277     existing_api_host = os.environ.get('ARVADOS_TEST_API_HOST', my_api_host)
278     if existing_api_host and pid_file_ok:
279         if existing_api_host == my_api_host:
280             try:
281                 return reset()
282             except:
283                 # Fall through to shutdown-and-start case.
284                 pass
285         else:
286             # Server was provided by parent. Can't recover if it's
287             # unresettable.
288             return reset()
289
290     # Before trying to start up our own server, call stop() to avoid
291     # "Phusion Passenger Standalone is already running on PID 12345".
292     # (If we've gotten this far, ARVADOS_TEST_API_HOST isn't set, so
293     # we know the server is ours to kill.)
294     stop(force=True)
295
296     restore_cwd = os.getcwd()
297     api_src_dir = os.path.join(SERVICES_SRC_DIR, 'api')
298     os.chdir(api_src_dir)
299
300     # Either we haven't started a server of our own yet, or it has
301     # died, or we have lost our credentials, or something else is
302     # preventing us from calling reset(). Start a new one.
303
304     if not os.path.exists('tmp'):
305         os.makedirs('tmp')
306
307     if not os.path.exists('tmp/api'):
308         os.makedirs('tmp/api')
309
310     if not os.path.exists('tmp/logs'):
311         os.makedirs('tmp/logs')
312
313     # Install the git repository fixtures.
314     gitdir = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'git')
315     gittarball = os.path.join(SERVICES_SRC_DIR, 'api', 'test', 'test.git.tar')
316     if not os.path.isdir(gitdir):
317         os.makedirs(gitdir)
318     subprocess.check_output(['tar', '-xC', gitdir, '-f', gittarball])
319
320     # The nginx proxy isn't listening here yet, but we need to choose
321     # the wss:// port now so we can write the API server config file.
322     wss_port = find_available_port()
323     _setport('wss', wss_port)
324
325     port = find_available_port()
326     env = os.environ.copy()
327     env['RAILS_ENV'] = 'test'
328     env['ARVADOS_TEST_WSS_PORT'] = str(wss_port)
329     env.pop('ARVADOS_WEBSOCKETS', None)
330     env.pop('ARVADOS_TEST_API_HOST', None)
331     env.pop('ARVADOS_API_HOST', None)
332     env.pop('ARVADOS_API_HOST_INSECURE', None)
333     env.pop('ARVADOS_API_TOKEN', None)
334     start_msg = subprocess.check_output(
335         ['bundle', 'exec',
336          'passenger', 'start', '-d', '-p{}'.format(port),
337          '--pid-file', pid_file,
338          '--log-file', os.path.join(os.getcwd(), 'log/test.log'),
339          '--ssl',
340          '--ssl-certificate', 'tmp/self-signed.pem',
341          '--ssl-certificate-key', 'tmp/self-signed.key'],
342         env=env)
343
344     if not leave_running_atexit:
345         atexit.register(kill_server_pid, pid_file, passenger_root=api_src_dir)
346
347     match = re.search(r'Accessible via: https://(.*?)/', start_msg)
348     if not match:
349         raise Exception(
350             "Passenger did not report endpoint: {}".format(start_msg))
351     my_api_host = match.group(1)
352     os.environ['ARVADOS_API_HOST'] = my_api_host
353
354     # Make sure the server has written its pid file and started
355     # listening on its TCP port
356     find_server_pid(pid_file)
357     _wait_until_port_listens(port)
358
359     reset()
360     os.chdir(restore_cwd)
361
362 def reset():
363     """Reset the test server to fixture state.
364
365     This resets the ARVADOS_TEST_API_HOST provided by a parent process
366     if any, otherwise the server started by run().
367
368     It also resets ARVADOS_* environment vars to point to the test
369     server with admin credentials.
370     """
371     existing_api_host = os.environ.get('ARVADOS_TEST_API_HOST', my_api_host)
372     token = auth_token('admin')
373     httpclient = httplib2.Http(ca_certs=os.path.join(
374         SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.pem'))
375     httpclient.request(
376         'https://{}/database/reset'.format(existing_api_host),
377         'POST',
378         headers={'Authorization': 'OAuth2 {}'.format(token)})
379     os.environ['ARVADOS_API_HOST_INSECURE'] = 'true'
380     os.environ['ARVADOS_API_TOKEN'] = token
381     if _wait_until_port_listens(_getport('controller-ssl'), timeout=0.5, warn=False):
382         os.environ['ARVADOS_API_HOST'] = '0.0.0.0:'+str(_getport('controller-ssl'))
383     else:
384         os.environ['ARVADOS_API_HOST'] = existing_api_host
385
386 def stop(force=False):
387     """Stop the API server, if one is running.
388
389     If force==False, kill it only if we started it ourselves. (This
390     supports the use case where a Python test suite calls run(), but
391     run() just uses the ARVADOS_TEST_API_HOST provided by the parent
392     process, and the test suite cleans up after itself by calling
393     stop(). In this case the test server provided by the parent
394     process should be left alone.)
395
396     If force==True, kill it even if we didn't start it
397     ourselves. (This supports the use case in __main__, where "run"
398     and "stop" happen in different processes.)
399     """
400     global my_api_host
401     if force or my_api_host is not None:
402         kill_server_pid(_pidfile('api'))
403         my_api_host = None
404
405 def run_controller():
406     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
407         return
408     stop_controller()
409     rails_api_port = int(string.split(os.environ.get('ARVADOS_TEST_API_HOST', my_api_host), ':')[-1])
410     port = find_available_port()
411     conf = os.path.join(TEST_TMPDIR, 'arvados.yml')
412     with open(conf, 'w') as f:
413         f.write("""
414 Clusters:
415   zzzzz:
416     ManagementToken: e687950a23c3a9bceec28c6223a06c79
417     API:
418       RequestTimeout: 30s
419     PostgreSQL:
420       ConnectionPool: 32
421       Connection:
422         host: {dbhost}
423         dbname: {dbname}
424         user: {dbuser}
425         password: {dbpass}
426     TLS:
427       Insecure: true
428     Services:
429       Controller:
430         InternalURLs:
431           "http://localhost:{controllerport}": {{}}
432       RailsAPI:
433         InternalURLs:
434           "https://localhost:{railsport}": {{}}
435         """.format(
436             dbhost=_dbconfig('host'),
437             dbname=_dbconfig('database'),
438             dbuser=_dbconfig('username'),
439             dbpass=_dbconfig('password'),
440             controllerport=port,
441             railsport=rails_api_port,
442         ))
443     logf = open(_logfilename('controller'), 'a')
444     controller = subprocess.Popen(
445         ["arvados-server", "controller", "-config", conf],
446         stdin=open('/dev/null'), stdout=logf, stderr=logf, close_fds=True)
447     with open(_pidfile('controller'), 'w') as f:
448         f.write(str(controller.pid))
449     _wait_until_port_listens(port)
450     _setport('controller', port)
451     return port
452
453 def stop_controller():
454     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
455         return
456     kill_server_pid(_pidfile('controller'))
457
458 def run_ws():
459     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
460         return
461     stop_ws()
462     port = find_available_port()
463     conf = os.path.join(TEST_TMPDIR, 'ws.yml')
464     with open(conf, 'w') as f:
465         f.write("""
466 Client:
467   APIHost: {}
468   Insecure: true
469 Listen: :{}
470 LogLevel: {}
471 Postgres:
472   host: {}
473   dbname: {}
474   user: {}
475   password: {}
476   sslmode: require
477         """.format(os.environ['ARVADOS_API_HOST'],
478                    port,
479                    ('info' if os.environ.get('ARVADOS_DEBUG', '') in ['','0'] else 'debug'),
480                    _dbconfig('host'),
481                    _dbconfig('database'),
482                    _dbconfig('username'),
483                    _dbconfig('password')))
484     logf = open(_logfilename('ws'), 'a')
485     ws = subprocess.Popen(
486         ["ws", "-config", conf],
487         stdin=open('/dev/null'), stdout=logf, stderr=logf, close_fds=True)
488     with open(_pidfile('ws'), 'w') as f:
489         f.write(str(ws.pid))
490     _wait_until_port_listens(port)
491     _setport('ws', port)
492     return port
493
494 def stop_ws():
495     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
496         return
497     kill_server_pid(_pidfile('ws'))
498
499 def _start_keep(n, keep_args):
500     keep0 = tempfile.mkdtemp()
501     port = find_available_port()
502     keep_cmd = ["keepstore",
503                 "-volume={}".format(keep0),
504                 "-listen=:{}".format(port),
505                 "-pid="+_pidfile('keep{}'.format(n))]
506
507     for arg, val in keep_args.items():
508         keep_cmd.append("{}={}".format(arg, val))
509
510     logf = open(_logfilename('keep{}'.format(n)), 'a')
511     kp0 = subprocess.Popen(
512         keep_cmd, stdin=open('/dev/null'), stdout=logf, stderr=logf, close_fds=True)
513
514     with open(_pidfile('keep{}'.format(n)), 'w') as f:
515         f.write(str(kp0.pid))
516
517     with open("{}/keep{}.volume".format(TEST_TMPDIR, n), 'w') as f:
518         f.write(keep0)
519
520     _wait_until_port_listens(port)
521
522     return port
523
524 def run_keep(blob_signing_key=None, enforce_permissions=False, num_servers=2):
525     stop_keep(num_servers)
526
527     keep_args = {}
528     if not blob_signing_key:
529         blob_signing_key = 'zfhgfenhffzltr9dixws36j1yhksjoll2grmku38mi7yxd66h5j4q9w4jzanezacp8s6q0ro3hxakfye02152hncy6zml2ed0uc'
530     with open(os.path.join(TEST_TMPDIR, "keep.blob_signing_key"), "w") as f:
531         keep_args['-blob-signing-key-file'] = f.name
532         f.write(blob_signing_key)
533     keep_args['-enforce-permissions'] = str(enforce_permissions).lower()
534     with open(os.path.join(TEST_TMPDIR, "keep.data-manager-token-file"), "w") as f:
535         keep_args['-data-manager-token-file'] = f.name
536         f.write(auth_token('data_manager'))
537     keep_args['-never-delete'] = 'false'
538
539     api = arvados.api(
540         version='v1',
541         host=os.environ['ARVADOS_API_HOST'],
542         token=os.environ['ARVADOS_API_TOKEN'],
543         insecure=True)
544
545     for d in api.keep_services().list(filters=[['service_type','=','disk']]).execute()['items']:
546         api.keep_services().delete(uuid=d['uuid']).execute()
547     for d in api.keep_disks().list().execute()['items']:
548         api.keep_disks().delete(uuid=d['uuid']).execute()
549
550     for d in range(0, num_servers):
551         port = _start_keep(d, keep_args)
552         svc = api.keep_services().create(body={'keep_service': {
553             'uuid': 'zzzzz-bi6l4-keepdisk{:07d}'.format(d),
554             'service_host': 'localhost',
555             'service_port': port,
556             'service_type': 'disk',
557             'service_ssl_flag': False,
558         }}).execute()
559         api.keep_disks().create(body={
560             'keep_disk': {'keep_service_uuid': svc['uuid'] }
561         }).execute()
562
563     # If keepproxy and/or keep-web is running, send SIGHUP to make
564     # them discover the new keepstore services.
565     for svc in ('keepproxy', 'keep-web'):
566         pidfile = _pidfile('keepproxy')
567         if os.path.exists(pidfile):
568             try:
569                 os.kill(int(open(pidfile).read()), signal.SIGHUP)
570             except OSError:
571                 os.remove(pidfile)
572
573 def _stop_keep(n):
574     kill_server_pid(_pidfile('keep{}'.format(n)))
575     if os.path.exists("{}/keep{}.volume".format(TEST_TMPDIR, n)):
576         with open("{}/keep{}.volume".format(TEST_TMPDIR, n), 'r') as r:
577             shutil.rmtree(r.read(), True)
578         os.unlink("{}/keep{}.volume".format(TEST_TMPDIR, n))
579     if os.path.exists(os.path.join(TEST_TMPDIR, "keep.blob_signing_key")):
580         os.remove(os.path.join(TEST_TMPDIR, "keep.blob_signing_key"))
581
582 def stop_keep(num_servers=2):
583     for n in range(0, num_servers):
584         _stop_keep(n)
585
586 def run_keep_proxy():
587     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
588         os.environ["ARVADOS_KEEP_SERVICES"] = "http://localhost:{}".format(_getport('keepproxy'))
589         return
590     stop_keep_proxy()
591
592     port = find_available_port()
593     env = os.environ.copy()
594     env['ARVADOS_API_TOKEN'] = auth_token('anonymous')
595     logf = open(_logfilename('keepproxy'), 'a')
596     kp = subprocess.Popen(
597         ['keepproxy',
598          '-pid='+_pidfile('keepproxy'),
599          '-listen=:{}'.format(port)],
600         env=env, stdin=open('/dev/null'), stdout=logf, stderr=logf, close_fds=True)
601
602     api = arvados.api(
603         version='v1',
604         host=os.environ['ARVADOS_API_HOST'],
605         token=auth_token('admin'),
606         insecure=True)
607     for d in api.keep_services().list(
608             filters=[['service_type','=','proxy']]).execute()['items']:
609         api.keep_services().delete(uuid=d['uuid']).execute()
610     api.keep_services().create(body={'keep_service': {
611         'service_host': 'localhost',
612         'service_port': port,
613         'service_type': 'proxy',
614         'service_ssl_flag': False,
615     }}).execute()
616     os.environ["ARVADOS_KEEP_SERVICES"] = "http://localhost:{}".format(port)
617     _setport('keepproxy', port)
618     _wait_until_port_listens(port)
619
620 def stop_keep_proxy():
621     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
622         return
623     kill_server_pid(_pidfile('keepproxy'))
624
625 def run_arv_git_httpd():
626     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
627         return
628     stop_arv_git_httpd()
629
630     gitdir = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'git')
631     gitport = find_available_port()
632     env = os.environ.copy()
633     env.pop('ARVADOS_API_TOKEN', None)
634     logf = open(_logfilename('arv-git-httpd'), 'a')
635     agh = subprocess.Popen(
636         ['arv-git-httpd',
637          '-repo-root='+gitdir+'/test',
638          '-management-token=e687950a23c3a9bceec28c6223a06c79',
639          '-address=:'+str(gitport)],
640         env=env, stdin=open('/dev/null'), stdout=logf, stderr=logf)
641     with open(_pidfile('arv-git-httpd'), 'w') as f:
642         f.write(str(agh.pid))
643     _setport('arv-git-httpd', gitport)
644     _wait_until_port_listens(gitport)
645
646 def stop_arv_git_httpd():
647     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
648         return
649     kill_server_pid(_pidfile('arv-git-httpd'))
650
651 def run_keep_web():
652     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
653         return
654     stop_keep_web()
655
656     keepwebport = find_available_port()
657     env = os.environ.copy()
658     env['ARVADOS_API_TOKEN'] = auth_token('anonymous')
659     logf = open(_logfilename('keep-web'), 'a')
660     keepweb = subprocess.Popen(
661         ['keep-web',
662          '-allow-anonymous',
663          '-attachment-only-host=download',
664          '-management-token=e687950a23c3a9bceec28c6223a06c79',
665          '-listen=:'+str(keepwebport)],
666         env=env, stdin=open('/dev/null'), stdout=logf, stderr=logf)
667     with open(_pidfile('keep-web'), 'w') as f:
668         f.write(str(keepweb.pid))
669     _setport('keep-web', keepwebport)
670     _wait_until_port_listens(keepwebport)
671
672 def stop_keep_web():
673     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
674         return
675     kill_server_pid(_pidfile('keep-web'))
676
677 def run_nginx():
678     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
679         return
680     stop_nginx()
681     nginxconf = {}
682     nginxconf['CONTROLLERPORT'] = _getport('controller')
683     nginxconf['CONTROLLERSSLPORT'] = find_available_port()
684     nginxconf['KEEPWEBPORT'] = _getport('keep-web')
685     nginxconf['KEEPWEBDLSSLPORT'] = find_available_port()
686     nginxconf['KEEPWEBSSLPORT'] = find_available_port()
687     nginxconf['KEEPPROXYPORT'] = _getport('keepproxy')
688     nginxconf['KEEPPROXYSSLPORT'] = find_available_port()
689     nginxconf['GITPORT'] = _getport('arv-git-httpd')
690     nginxconf['GITSSLPORT'] = find_available_port()
691     nginxconf['WSPORT'] = _getport('ws')
692     nginxconf['WSSPORT'] = _getport('wss')
693     nginxconf['SSLCERT'] = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.pem')
694     nginxconf['SSLKEY'] = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.key')
695     nginxconf['ACCESSLOG'] = _logfilename('nginx_access')
696     nginxconf['ERRORLOG'] = _logfilename('nginx_error')
697     nginxconf['TMPDIR'] = TEST_TMPDIR
698
699     conftemplatefile = os.path.join(MY_DIRNAME, 'nginx.conf')
700     conffile = os.path.join(TEST_TMPDIR, 'nginx.conf')
701     with open(conffile, 'w') as f:
702         f.write(re.sub(
703             r'{{([A-Z]+)}}',
704             lambda match: str(nginxconf.get(match.group(1))),
705             open(conftemplatefile).read()))
706
707     env = os.environ.copy()
708     env['PATH'] = env['PATH']+':/sbin:/usr/sbin:/usr/local/sbin'
709
710     nginx = subprocess.Popen(
711         ['nginx',
712          '-g', 'error_log stderr info;',
713          '-g', 'pid '+_pidfile('nginx')+';',
714          '-c', conffile],
715         env=env, stdin=open('/dev/null'), stdout=sys.stderr)
716     _setport('controller-ssl', nginxconf['CONTROLLERSSLPORT'])
717     _setport('keep-web-dl-ssl', nginxconf['KEEPWEBDLSSLPORT'])
718     _setport('keep-web-ssl', nginxconf['KEEPWEBSSLPORT'])
719     _setport('keepproxy-ssl', nginxconf['KEEPPROXYSSLPORT'])
720     _setport('arv-git-httpd-ssl', nginxconf['GITSSLPORT'])
721
722 def stop_nginx():
723     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
724         return
725     kill_server_pid(_pidfile('nginx'))
726
727 def _pidfile(program):
728     return os.path.join(TEST_TMPDIR, program + '.pid')
729
730 def _portfile(program):
731     return os.path.join(TEST_TMPDIR, program + '.port')
732
733 def _setport(program, port):
734     with open(_portfile(program), 'w') as f:
735         f.write(str(port))
736
737 # Returns 9 if program is not up.
738 def _getport(program):
739     try:
740         return int(open(_portfile(program)).read())
741     except IOError:
742         return 9
743
744 def _dbconfig(key):
745     global _cached_db_config
746     if not _cached_db_config:
747         _cached_db_config = yaml.safe_load(open(os.path.join(
748             SERVICES_SRC_DIR, 'api', 'config', 'database.yml')))
749     return _cached_db_config['test'][key]
750
751 def _apiconfig(key):
752     global _cached_config
753     if _cached_config:
754         return _cached_config[key]
755     def _load(f, required=True):
756         fullpath = os.path.join(SERVICES_SRC_DIR, 'api', 'config', f)
757         if not required and not os.path.exists(fullpath):
758             return {}
759         return yaml.safe_load(fullpath)
760     cdefault = _load('application.default.yml')
761     csite = _load('application.yml', required=False)
762     _cached_config = {}
763     for section in [cdefault.get('common',{}), cdefault.get('test',{}),
764                     csite.get('common',{}), csite.get('test',{})]:
765         _cached_config.update(section)
766     return _cached_config[key]
767
768 def fixture(fix):
769     '''load a fixture yaml file'''
770     with open(os.path.join(SERVICES_SRC_DIR, 'api', "test", "fixtures",
771                            fix + ".yml")) as f:
772         yaml_file = f.read()
773         try:
774           trim_index = yaml_file.index("# Test Helper trims the rest of the file")
775           yaml_file = yaml_file[0:trim_index]
776         except ValueError:
777           pass
778         return yaml.safe_load(yaml_file)
779
780 def auth_token(token_name):
781     return fixture("api_client_authorizations")[token_name]["api_token"]
782
783 def authorize_with(token_name):
784     '''token_name is the symbolic name of the token from the api_client_authorizations fixture'''
785     arvados.config.settings()["ARVADOS_API_TOKEN"] = auth_token(token_name)
786     arvados.config.settings()["ARVADOS_API_HOST"] = os.environ.get("ARVADOS_API_HOST")
787     arvados.config.settings()["ARVADOS_API_HOST_INSECURE"] = "true"
788
789 class TestCaseWithServers(unittest.TestCase):
790     """TestCase to start and stop supporting Arvados servers.
791
792     Define any of MAIN_SERVER, KEEP_SERVER, and/or KEEP_PROXY_SERVER
793     class variables as a dictionary of keyword arguments.  If you do,
794     setUpClass will start the corresponding servers by passing these
795     keyword arguments to the run, run_keep, and/or run_keep_server
796     functions, respectively.  It will also set Arvados environment
797     variables to point to these servers appropriately.  If you don't
798     run a Keep or Keep proxy server, setUpClass will set up a
799     temporary directory for Keep local storage, and set it as
800     KEEP_LOCAL_STORE.
801
802     tearDownClass will stop any servers started, and restore the
803     original environment.
804     """
805     MAIN_SERVER = None
806     WS_SERVER = None
807     KEEP_SERVER = None
808     KEEP_PROXY_SERVER = None
809     KEEP_WEB_SERVER = None
810
811     @staticmethod
812     def _restore_dict(src, dest):
813         for key in list(dest.keys()):
814             if key not in src:
815                 del dest[key]
816         dest.update(src)
817
818     @classmethod
819     def setUpClass(cls):
820         cls._orig_environ = os.environ.copy()
821         cls._orig_config = arvados.config.settings().copy()
822         cls._cleanup_funcs = []
823         os.environ.pop('ARVADOS_KEEP_SERVICES', None)
824         os.environ.pop('ARVADOS_EXTERNAL_CLIENT', None)
825         for server_kwargs, start_func, stop_func in (
826                 (cls.MAIN_SERVER, run, reset),
827                 (cls.WS_SERVER, run_ws, stop_ws),
828                 (cls.KEEP_SERVER, run_keep, stop_keep),
829                 (cls.KEEP_PROXY_SERVER, run_keep_proxy, stop_keep_proxy),
830                 (cls.KEEP_WEB_SERVER, run_keep_web, stop_keep_web)):
831             if server_kwargs is not None:
832                 start_func(**server_kwargs)
833                 cls._cleanup_funcs.append(stop_func)
834         if (cls.KEEP_SERVER is None) and (cls.KEEP_PROXY_SERVER is None):
835             cls.local_store = tempfile.mkdtemp()
836             os.environ['KEEP_LOCAL_STORE'] = cls.local_store
837             cls._cleanup_funcs.append(
838                 lambda: shutil.rmtree(cls.local_store, ignore_errors=True))
839         else:
840             os.environ.pop('KEEP_LOCAL_STORE', None)
841         arvados.config.initialize()
842
843     @classmethod
844     def tearDownClass(cls):
845         for clean_func in cls._cleanup_funcs:
846             clean_func()
847         cls._restore_dict(cls._orig_environ, os.environ)
848         cls._restore_dict(cls._orig_config, arvados.config.settings())
849
850
851 if __name__ == "__main__":
852     actions = [
853         'start', 'stop',
854         'start_ws', 'stop_ws',
855         'start_controller', 'stop_controller',
856         'start_keep', 'stop_keep',
857         'start_keep_proxy', 'stop_keep_proxy',
858         'start_keep-web', 'stop_keep-web',
859         'start_arv-git-httpd', 'stop_arv-git-httpd',
860         'start_nginx', 'stop_nginx',
861     ]
862     parser = argparse.ArgumentParser()
863     parser.add_argument('action', type=str, help="one of {}".format(actions))
864     parser.add_argument('--auth', type=str, metavar='FIXTURE_NAME', help='Print authorization info for given api_client_authorizations fixture')
865     parser.add_argument('--num-keep-servers', metavar='int', type=int, default=2, help="Number of keep servers desired")
866     parser.add_argument('--keep-enforce-permissions', action="store_true", help="Enforce keep permissions")
867
868     args = parser.parse_args()
869
870     if args.action not in actions:
871         print("Unrecognized action '{}'. Actions are: {}.".
872               format(args.action, actions),
873               file=sys.stderr)
874         sys.exit(1)
875     if args.action == 'start':
876         stop(force=('ARVADOS_TEST_API_HOST' not in os.environ))
877         run(leave_running_atexit=True)
878         host = os.environ['ARVADOS_API_HOST']
879         if args.auth is not None:
880             token = auth_token(args.auth)
881             print("export ARVADOS_API_TOKEN={}".format(pipes.quote(token)))
882             print("export ARVADOS_API_HOST={}".format(pipes.quote(host)))
883             print("export ARVADOS_API_HOST_INSECURE=true")
884         else:
885             print(host)
886     elif args.action == 'stop':
887         stop(force=('ARVADOS_TEST_API_HOST' not in os.environ))
888     elif args.action == 'start_ws':
889         run_ws()
890     elif args.action == 'stop_ws':
891         stop_ws()
892     elif args.action == 'start_controller':
893         run_controller()
894     elif args.action == 'stop_controller':
895         stop_controller()
896     elif args.action == 'start_keep':
897         run_keep(enforce_permissions=args.keep_enforce_permissions, num_servers=args.num_keep_servers)
898     elif args.action == 'stop_keep':
899         stop_keep(num_servers=args.num_keep_servers)
900     elif args.action == 'start_keep_proxy':
901         run_keep_proxy()
902     elif args.action == 'stop_keep_proxy':
903         stop_keep_proxy()
904     elif args.action == 'start_arv-git-httpd':
905         run_arv_git_httpd()
906     elif args.action == 'stop_arv-git-httpd':
907         stop_arv_git_httpd()
908     elif args.action == 'start_keep-web':
909         run_keep_web()
910     elif args.action == 'stop_keep-web':
911         stop_keep_web()
912     elif args.action == 'start_nginx':
913         run_nginx()
914         print("export ARVADOS_API_HOST=0.0.0.0:{}".format(_getport('controller-ssl')))
915     elif args.action == 'stop_nginx':
916         stop_nginx()
917     else:
918         raise Exception("action recognized but not implemented!?")