3 from __future__ import print_function
23 MY_DIRNAME = os.path.dirname(os.path.realpath(__file__))
24 if __name__ == '__main__' and os.path.exists(
25 os.path.join(MY_DIRNAME, '..', 'arvados', '__init__.py')):
26 # We're being launched to support another test suite.
27 # Add the Python SDK source to the library path.
28 sys.path.insert(1, os.path.dirname(MY_DIRNAME))
33 ARVADOS_DIR = os.path.realpath(os.path.join(MY_DIRNAME, '../../..'))
34 SERVICES_SRC_DIR = os.path.join(ARVADOS_DIR, 'services')
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)
57 except EnvironmentError:
66 def kill_server_pid(pidfile, wait=10, passenger_root=False):
67 # 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])
84 # Use up to half of the +wait+ period waiting for "passenger
85 # stop" to work. If the process hasn't exited by then, start
86 # sending TERM signals.
90 while now <= deadline and server_pid is None:
92 with open(pidfile, 'r') as f:
93 server_pid = int(f.read())
95 # No pidfile = nothing to kill.
97 except ValueError as error:
98 # Pidfile exists, but we can't parse it. Perhaps the
99 # server has created the file but hasn't written its PID
101 print("Parse error reading pidfile {}: {}".format(pidfile, error),
106 while now <= deadline:
108 exited, _ = os.waitpid(server_pid, os.WNOHANG)
112 # already exited, or isn't our child process
116 os.kill(server_pid, signal.SIGTERM)
117 print("Sent SIGTERM to {} ({})".format(server_pid, pidfile),
119 except OSError as error:
120 if error.errno == errno.ESRCH:
121 # Thrown by os.getpgid() or os.kill() if the process
122 # does not exist, i.e., our work here is done.
128 print("Server PID {} ({}) did not exit, giving up after {}s".
129 format(server_pid, pidfile, wait),
132 def find_available_port():
133 """Return an IPv4 port number that is not in use right now.
135 We assume whoever needs to use the returned port is able to reuse
136 a recently used port without waiting for TIME_WAIT (see
137 SO_REUSEADDR / SO_REUSEPORT).
139 Some opportunity for races here, but it's better than choosing
140 something at random and not checking at all. If all of our servers
141 (hey Passenger) knew that listening on port 0 was a thing, the OS
142 would take care of the races, and this wouldn't be needed at all.
145 sock = socket.socket()
146 sock.bind(('0.0.0.0', 0))
147 port = sock.getsockname()[1]
151 def _wait_until_port_listens(port, timeout=10):
152 """Wait for a process to start listening on the given port.
154 If nothing listens on the port within the specified timeout (given
155 in seconds), print a warning on stderr before returning.
158 subprocess.check_output(['which', 'lsof'])
159 except subprocess.CalledProcessError:
160 print("WARNING: No `lsof` -- cannot wait for port to listen. "+
161 "Sleeping 0.5 and hoping for the best.",
165 deadline = time.time() + timeout
166 while time.time() < deadline:
168 subprocess.check_output(
169 ['lsof', '-t', '-i', 'tcp:'+str(port)])
170 except subprocess.CalledProcessError:
175 "WARNING: Nothing is listening on port {} (waited {} seconds).".
176 format(port, timeout),
179 def _fifo2stderr(label):
180 """Create a fifo, and copy it to stderr, prepending label to each line.
182 Return value is the path to the new FIFO.
184 +label+ should contain only alphanumerics: it is also used as part
185 of the FIFO filename.
187 fifo = os.path.join(TEST_TMPDIR, label+'.fifo')
190 except OSError as error:
191 if error.errno != errno.ENOENT:
193 os.mkfifo(fifo, 0700)
195 ['sed', '-e', 's/^/['+label+'] /', fifo],
199 def run(leave_running_atexit=False):
200 """Ensure an API server is running, and ARVADOS_API_* env vars have
201 admin credentials for it.
203 If ARVADOS_TEST_API_HOST is set, a parent process has started a
204 test server for us to use: we just need to reset() it using the
207 If a previous call to run() started a new server process, and it
208 is still running, we just need to reset() it to fixture state and
211 If neither of those options work out, we'll really start a new
216 # Delete cached discovery document.
217 shutil.rmtree(arvados.http_cache('discovery'))
219 pid_file = _pidfile('api')
220 pid_file_ok = find_server_pid(pid_file, 0)
222 existing_api_host = os.environ.get('ARVADOS_TEST_API_HOST', my_api_host)
223 if existing_api_host and pid_file_ok:
224 if existing_api_host == my_api_host:
228 # Fall through to shutdown-and-start case.
231 # Server was provided by parent. Can't recover if it's
235 # Before trying to start up our own server, call stop() to avoid
236 # "Phusion Passenger Standalone is already running on PID 12345".
237 # (If we've gotten this far, ARVADOS_TEST_API_HOST isn't set, so
238 # we know the server is ours to kill.)
241 restore_cwd = os.getcwd()
242 api_src_dir = os.path.join(SERVICES_SRC_DIR, 'api')
243 os.chdir(api_src_dir)
245 # Either we haven't started a server of our own yet, or it has
246 # died, or we have lost our credentials, or something else is
247 # preventing us from calling reset(). Start a new one.
249 if not os.path.exists('tmp'):
252 if not os.path.exists('tmp/api'):
253 os.makedirs('tmp/api')
255 if not os.path.exists('tmp/logs'):
256 os.makedirs('tmp/logs')
258 if not os.path.exists('tmp/self-signed.pem'):
259 # We assume here that either passenger reports its listening
260 # address as https:/0.0.0.0:port/. If it reports "127.0.0.1"
261 # then the certificate won't match the host and reset() will
262 # fail certificate verification. If it reports "localhost",
263 # clients (notably Python SDK's websocket client) might
264 # resolve localhost as ::1 and then fail to connect.
265 subprocess.check_call([
266 'openssl', 'req', '-new', '-x509', '-nodes',
267 '-out', 'tmp/self-signed.pem',
268 '-keyout', 'tmp/self-signed.key',
270 '-subj', '/CN=0.0.0.0'],
273 # Install the git repository fixtures.
274 gitdir = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'git')
275 gittarball = os.path.join(SERVICES_SRC_DIR, 'api', 'test', 'test.git.tar')
276 if not os.path.isdir(gitdir):
278 subprocess.check_output(['tar', '-xC', gitdir, '-f', gittarball])
280 port = find_available_port()
281 env = os.environ.copy()
282 env['RAILS_ENV'] = 'test'
283 env['ARVADOS_WEBSOCKETS'] = 'yes'
284 env.pop('ARVADOS_TEST_API_HOST', None)
285 env.pop('ARVADOS_API_HOST', None)
286 env.pop('ARVADOS_API_HOST_INSECURE', None)
287 env.pop('ARVADOS_API_TOKEN', None)
288 start_msg = subprocess.check_output(
290 'passenger', 'start', '-d', '-p{}'.format(port),
291 '--pid-file', pid_file,
292 '--log-file', os.path.join(os.getcwd(), 'log/test.log'),
294 '--ssl-certificate', 'tmp/self-signed.pem',
295 '--ssl-certificate-key', 'tmp/self-signed.key'],
298 if not leave_running_atexit:
299 atexit.register(kill_server_pid, pid_file, passenger_root=api_src_dir)
301 match = re.search(r'Accessible via: https://(.*?)/', start_msg)
304 "Passenger did not report endpoint: {}".format(start_msg))
305 my_api_host = match.group(1)
306 os.environ['ARVADOS_API_HOST'] = my_api_host
308 # Make sure the server has written its pid file and started
309 # listening on its TCP port
310 find_server_pid(pid_file)
311 _wait_until_port_listens(port)
314 os.chdir(restore_cwd)
317 """Reset the test server to fixture state.
319 This resets the ARVADOS_TEST_API_HOST provided by a parent process
320 if any, otherwise the server started by run().
322 It also resets ARVADOS_* environment vars to point to the test
323 server with admin credentials.
325 existing_api_host = os.environ.get('ARVADOS_TEST_API_HOST', my_api_host)
326 token = auth_token('admin')
327 httpclient = httplib2.Http(ca_certs=os.path.join(
328 SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.pem'))
330 'https://{}/database/reset'.format(existing_api_host),
332 headers={'Authorization': 'OAuth2 {}'.format(token)})
333 os.environ['ARVADOS_API_HOST_INSECURE'] = 'true'
334 os.environ['ARVADOS_API_HOST'] = existing_api_host
335 os.environ['ARVADOS_API_TOKEN'] = token
337 def stop(force=False):
338 """Stop the API server, if one is running.
340 If force==False, kill it only if we started it ourselves. (This
341 supports the use case where a Python test suite calls run(), but
342 run() just uses the ARVADOS_TEST_API_HOST provided by the parent
343 process, and the test suite cleans up after itself by calling
344 stop(). In this case the test server provided by the parent
345 process should be left alone.)
347 If force==True, kill it even if we didn't start it
348 ourselves. (This supports the use case in __main__, where "run"
349 and "stop" happen in different processes.)
352 if force or my_api_host is not None:
353 kill_server_pid(_pidfile('api'))
356 def _start_keep(n, keep_args):
357 keep0 = tempfile.mkdtemp()
358 port = find_available_port()
359 keep_cmd = ["keepstore",
360 "-volume={}".format(keep0),
361 "-listen=:{}".format(port),
362 "-pid="+_pidfile('keep{}'.format(n))]
364 for arg, val in keep_args.iteritems():
365 keep_cmd.append("{}={}".format(arg, val))
367 logf = open(_fifo2stderr('keep{}'.format(n)), 'w')
368 kp0 = subprocess.Popen(
369 keep_cmd, stdin=open('/dev/null'), stdout=logf, stderr=logf, close_fds=True)
371 with open(_pidfile('keep{}'.format(n)), 'w') as f:
372 f.write(str(kp0.pid))
374 with open("{}/keep{}.volume".format(TEST_TMPDIR, n), 'w') as f:
377 _wait_until_port_listens(port)
381 def run_keep(blob_signing_key=None, enforce_permissions=False, num_servers=2):
382 stop_keep(num_servers)
385 if not blob_signing_key:
386 blob_signing_key = 'zfhgfenhffzltr9dixws36j1yhksjoll2grmku38mi7yxd66h5j4q9w4jzanezacp8s6q0ro3hxakfye02152hncy6zml2ed0uc'
387 with open(os.path.join(TEST_TMPDIR, "keep.blob_signing_key"), "w") as f:
388 keep_args['-blob-signing-key-file'] = f.name
389 f.write(blob_signing_key)
390 if enforce_permissions:
391 keep_args['-enforce-permissions'] = 'true'
392 with open(os.path.join(TEST_TMPDIR, "keep.data-manager-token-file"), "w") as f:
393 keep_args['-data-manager-token-file'] = f.name
394 f.write(auth_token('data_manager'))
395 keep_args['-never-delete'] = 'false'
399 host=os.environ['ARVADOS_API_HOST'],
400 token=os.environ['ARVADOS_API_TOKEN'],
403 for d in api.keep_services().list(filters=[['service_type','=','disk']]).execute()['items']:
404 api.keep_services().delete(uuid=d['uuid']).execute()
405 for d in api.keep_disks().list().execute()['items']:
406 api.keep_disks().delete(uuid=d['uuid']).execute()
408 for d in range(0, num_servers):
409 port = _start_keep(d, keep_args)
410 svc = api.keep_services().create(body={'keep_service': {
411 'uuid': 'zzzzz-bi6l4-keepdisk{:07d}'.format(d),
412 'service_host': 'localhost',
413 'service_port': port,
414 'service_type': 'disk',
415 'service_ssl_flag': False,
417 api.keep_disks().create(body={
418 'keep_disk': {'keep_service_uuid': svc['uuid'] }
421 # If keepproxy is running, send SIGHUP to make it discover the new
422 # keepstore services.
423 proxypidfile = _pidfile('keepproxy')
424 if os.path.exists(proxypidfile):
426 os.kill(int(open(proxypidfile).read()), signal.SIGHUP)
428 os.remove(proxypidfile)
431 kill_server_pid(_pidfile('keep{}'.format(n)))
432 if os.path.exists("{}/keep{}.volume".format(TEST_TMPDIR, n)):
433 with open("{}/keep{}.volume".format(TEST_TMPDIR, n), 'r') as r:
434 shutil.rmtree(r.read(), True)
435 os.unlink("{}/keep{}.volume".format(TEST_TMPDIR, n))
436 if os.path.exists(os.path.join(TEST_TMPDIR, "keep.blob_signing_key")):
437 os.remove(os.path.join(TEST_TMPDIR, "keep.blob_signing_key"))
439 def stop_keep(num_servers=2):
440 for n in range(0, num_servers):
443 def run_keep_proxy():
444 if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
448 port = find_available_port()
449 env = os.environ.copy()
450 env['ARVADOS_API_TOKEN'] = auth_token('anonymous')
451 logf = open(_fifo2stderr('keepproxy'), 'w')
452 kp = subprocess.Popen(
454 '-pid='+_pidfile('keepproxy'),
455 '-listen=:{}'.format(port)],
456 env=env, stdin=open('/dev/null'), stdout=logf, stderr=logf, close_fds=True)
460 host=os.environ['ARVADOS_API_HOST'],
461 token=auth_token('admin'),
463 for d in api.keep_services().list(
464 filters=[['service_type','=','proxy']]).execute()['items']:
465 api.keep_services().delete(uuid=d['uuid']).execute()
466 api.keep_services().create(body={'keep_service': {
467 'service_host': 'localhost',
468 'service_port': port,
469 'service_type': 'proxy',
470 'service_ssl_flag': False,
472 os.environ["ARVADOS_KEEP_PROXY"] = "http://localhost:{}".format(port)
473 _setport('keepproxy', port)
474 _wait_until_port_listens(port)
476 def stop_keep_proxy():
477 if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
479 kill_server_pid(_pidfile('keepproxy'))
481 def run_arv_git_httpd():
482 if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
486 gitdir = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'git')
487 gitport = find_available_port()
488 env = os.environ.copy()
489 env.pop('ARVADOS_API_TOKEN', None)
490 logf = open(_fifo2stderr('arv-git-httpd'), 'w')
491 agh = subprocess.Popen(
493 '-repo-root='+gitdir+'/test',
494 '-address=:'+str(gitport)],
495 env=env, stdin=open('/dev/null'), stdout=logf, stderr=logf)
496 with open(_pidfile('arv-git-httpd'), 'w') as f:
497 f.write(str(agh.pid))
498 _setport('arv-git-httpd', gitport)
499 _wait_until_port_listens(gitport)
501 def stop_arv_git_httpd():
502 if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
504 kill_server_pid(_pidfile('arv-git-httpd'))
507 if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
511 keepwebport = find_available_port()
512 env = os.environ.copy()
513 env['ARVADOS_API_TOKEN'] = auth_token('anonymous')
514 logf = open(_fifo2stderr('keep-web'), 'w')
515 keepweb = subprocess.Popen(
518 '-attachment-only-host=download:'+str(keepwebport),
519 '-listen=:'+str(keepwebport)],
520 env=env, stdin=open('/dev/null'), stdout=logf, stderr=logf)
521 with open(_pidfile('keep-web'), 'w') as f:
522 f.write(str(keepweb.pid))
523 _setport('keep-web', keepwebport)
524 _wait_until_port_listens(keepwebport)
527 if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
529 kill_server_pid(_pidfile('keep-web'))
532 if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
535 nginxconf['KEEPWEBPORT'] = _getport('keep-web')
536 nginxconf['KEEPWEBDLSSLPORT'] = find_available_port()
537 nginxconf['KEEPWEBSSLPORT'] = find_available_port()
538 nginxconf['KEEPPROXYPORT'] = _getport('keepproxy')
539 nginxconf['KEEPPROXYSSLPORT'] = find_available_port()
540 nginxconf['GITPORT'] = _getport('arv-git-httpd')
541 nginxconf['GITSSLPORT'] = find_available_port()
542 nginxconf['SSLCERT'] = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.pem')
543 nginxconf['SSLKEY'] = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.key')
544 nginxconf['ACCESSLOG'] = _fifo2stderr('nginx_access_log')
546 conftemplatefile = os.path.join(MY_DIRNAME, 'nginx.conf')
547 conffile = os.path.join(TEST_TMPDIR, 'nginx.conf')
548 with open(conffile, 'w') as f:
551 lambda match: str(nginxconf.get(match.group(1))),
552 open(conftemplatefile).read()))
554 env = os.environ.copy()
555 env['PATH'] = env['PATH']+':/sbin:/usr/sbin:/usr/local/sbin'
557 nginx = subprocess.Popen(
559 '-g', 'error_log stderr info;',
560 '-g', 'pid '+_pidfile('nginx')+';',
562 env=env, stdin=open('/dev/null'), stdout=sys.stderr)
563 _setport('keep-web-dl-ssl', nginxconf['KEEPWEBDLSSLPORT'])
564 _setport('keep-web-ssl', nginxconf['KEEPWEBSSLPORT'])
565 _setport('keepproxy-ssl', nginxconf['KEEPPROXYSSLPORT'])
566 _setport('arv-git-httpd-ssl', nginxconf['GITSSLPORT'])
569 if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
571 kill_server_pid(_pidfile('nginx'))
573 def _pidfile(program):
574 return os.path.join(TEST_TMPDIR, program + '.pid')
576 def _portfile(program):
577 return os.path.join(TEST_TMPDIR, program + '.port')
579 def _setport(program, port):
580 with open(_portfile(program), 'w') as f:
583 # Returns 9 if program is not up.
584 def _getport(program):
586 return int(open(_portfile(program)).read())
592 return _cached_config[key]
593 def _load(f, required=True):
594 fullpath = os.path.join(SERVICES_SRC_DIR, 'api', 'config', f)
595 if not required and not os.path.exists(fullpath):
597 return yaml.load(fullpath)
598 cdefault = _load('application.default.yml')
599 csite = _load('application.yml', required=False)
601 for section in [cdefault.get('common',{}), cdefault.get('test',{}),
602 csite.get('common',{}), csite.get('test',{})]:
603 _cached_config.update(section)
604 return _cached_config[key]
607 '''load a fixture yaml file'''
608 with open(os.path.join(SERVICES_SRC_DIR, 'api', "test", "fixtures",
612 trim_index = yaml_file.index("# Test Helper trims the rest of the file")
613 yaml_file = yaml_file[0:trim_index]
616 return yaml.load(yaml_file)
618 def auth_token(token_name):
619 return fixture("api_client_authorizations")[token_name]["api_token"]
621 def authorize_with(token_name):
622 '''token_name is the symbolic name of the token from the api_client_authorizations fixture'''
623 arvados.config.settings()["ARVADOS_API_TOKEN"] = auth_token(token_name)
624 arvados.config.settings()["ARVADOS_API_HOST"] = os.environ.get("ARVADOS_API_HOST")
625 arvados.config.settings()["ARVADOS_API_HOST_INSECURE"] = "true"
627 class TestCaseWithServers(unittest.TestCase):
628 """TestCase to start and stop supporting Arvados servers.
630 Define any of MAIN_SERVER, KEEP_SERVER, and/or KEEP_PROXY_SERVER
631 class variables as a dictionary of keyword arguments. If you do,
632 setUpClass will start the corresponding servers by passing these
633 keyword arguments to the run, run_keep, and/or run_keep_server
634 functions, respectively. It will also set Arvados environment
635 variables to point to these servers appropriately. If you don't
636 run a Keep or Keep proxy server, setUpClass will set up a
637 temporary directory for Keep local storage, and set it as
640 tearDownClass will stop any servers started, and restore the
641 original environment.
645 KEEP_PROXY_SERVER = None
646 KEEP_WEB_SERVER = None
649 def _restore_dict(src, dest):
650 for key in dest.keys():
657 cls._orig_environ = os.environ.copy()
658 cls._orig_config = arvados.config.settings().copy()
659 cls._cleanup_funcs = []
660 os.environ.pop('ARVADOS_KEEP_PROXY', None)
661 os.environ.pop('ARVADOS_EXTERNAL_CLIENT', None)
662 for server_kwargs, start_func, stop_func in (
663 (cls.MAIN_SERVER, run, reset),
664 (cls.KEEP_SERVER, run_keep, stop_keep),
665 (cls.KEEP_PROXY_SERVER, run_keep_proxy, stop_keep_proxy),
666 (cls.KEEP_WEB_SERVER, run_keep_web, stop_keep_web)):
667 if server_kwargs is not None:
668 start_func(**server_kwargs)
669 cls._cleanup_funcs.append(stop_func)
670 if (cls.KEEP_SERVER is None) and (cls.KEEP_PROXY_SERVER is None):
671 cls.local_store = tempfile.mkdtemp()
672 os.environ['KEEP_LOCAL_STORE'] = cls.local_store
673 cls._cleanup_funcs.append(
674 lambda: shutil.rmtree(cls.local_store, ignore_errors=True))
676 os.environ.pop('KEEP_LOCAL_STORE', None)
677 arvados.config.initialize()
680 def tearDownClass(cls):
681 for clean_func in cls._cleanup_funcs:
683 cls._restore_dict(cls._orig_environ, os.environ)
684 cls._restore_dict(cls._orig_config, arvados.config.settings())
687 if __name__ == "__main__":
690 'start_keep', 'stop_keep',
691 'start_keep_proxy', 'stop_keep_proxy',
692 'start_keep-web', 'stop_keep-web',
693 'start_arv-git-httpd', 'stop_arv-git-httpd',
694 'start_nginx', 'stop_nginx',
696 parser = argparse.ArgumentParser()
697 parser.add_argument('action', type=str, help="one of {}".format(actions))
698 parser.add_argument('--auth', type=str, metavar='FIXTURE_NAME', help='Print authorization info for given api_client_authorizations fixture')
699 parser.add_argument('--num-keep-servers', metavar='int', type=int, default=2, help="Number of keep servers desired")
700 parser.add_argument('--keep-enforce-permissions', action="store_true", help="Enforce keep permissions")
702 args = parser.parse_args()
704 if args.action not in actions:
705 print("Unrecognized action '{}'. Actions are: {}.".
706 format(args.action, actions),
709 if args.action == 'start':
710 stop(force=('ARVADOS_TEST_API_HOST' not in os.environ))
711 run(leave_running_atexit=True)
712 host = os.environ['ARVADOS_API_HOST']
713 if args.auth is not None:
714 token = auth_token(args.auth)
715 print("export ARVADOS_API_TOKEN={}".format(pipes.quote(token)))
716 print("export ARVADOS_API_HOST={}".format(pipes.quote(host)))
717 print("export ARVADOS_API_HOST_INSECURE=true")
720 elif args.action == 'stop':
721 stop(force=('ARVADOS_TEST_API_HOST' not in os.environ))
722 elif args.action == 'start_keep':
723 run_keep(enforce_permissions=args.keep_enforce_permissions, num_servers=args.num_keep_servers)
724 elif args.action == 'stop_keep':
725 stop_keep(num_servers=args.num_keep_servers)
726 elif args.action == 'start_keep_proxy':
728 elif args.action == 'stop_keep_proxy':
730 elif args.action == 'start_arv-git-httpd':
732 elif args.action == 'stop_arv-git-httpd':
734 elif args.action == 'start_keep-web':
736 elif args.action == 'stop_keep-web':
738 elif args.action == 'start_nginx':
740 elif args.action == 'stop_nginx':
743 raise Exception("action recognized but not implemented!?")