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):
425 os.kill(int(open(proxypidfile).read()), signal.SIGHUP)
428 kill_server_pid(_pidfile('keep{}'.format(n)))
429 if os.path.exists("{}/keep{}.volume".format(TEST_TMPDIR, n)):
430 with open("{}/keep{}.volume".format(TEST_TMPDIR, n), 'r') as r:
431 shutil.rmtree(r.read(), True)
432 os.unlink("{}/keep{}.volume".format(TEST_TMPDIR, n))
433 if os.path.exists(os.path.join(TEST_TMPDIR, "keep.blob_signing_key")):
434 os.remove(os.path.join(TEST_TMPDIR, "keep.blob_signing_key"))
436 def stop_keep(num_servers=2):
437 for n in range(0, num_servers):
440 def run_keep_proxy():
441 if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
445 port = find_available_port()
446 env = os.environ.copy()
447 env['ARVADOS_API_TOKEN'] = auth_token('anonymous')
448 logf = open(_fifo2stderr('keepproxy'), 'w')
449 kp = subprocess.Popen(
451 '-pid='+_pidfile('keepproxy'),
452 '-listen=:{}'.format(port)],
453 env=env, stdin=open('/dev/null'), stdout=logf, stderr=logf, close_fds=True)
457 host=os.environ['ARVADOS_API_HOST'],
458 token=auth_token('admin'),
460 for d in api.keep_services().list(
461 filters=[['service_type','=','proxy']]).execute()['items']:
462 api.keep_services().delete(uuid=d['uuid']).execute()
463 api.keep_services().create(body={'keep_service': {
464 'service_host': 'localhost',
465 'service_port': port,
466 'service_type': 'proxy',
467 'service_ssl_flag': False,
469 os.environ["ARVADOS_KEEP_PROXY"] = "http://localhost:{}".format(port)
470 _setport('keepproxy', port)
471 _wait_until_port_listens(port)
473 def stop_keep_proxy():
474 if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
476 kill_server_pid(_pidfile('keepproxy'))
478 def run_arv_git_httpd():
479 if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
483 gitdir = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'git')
484 gitport = find_available_port()
485 env = os.environ.copy()
486 env.pop('ARVADOS_API_TOKEN', None)
487 logf = open(_fifo2stderr('arv-git-httpd'), 'w')
488 agh = subprocess.Popen(
490 '-repo-root='+gitdir+'/test',
491 '-address=:'+str(gitport)],
492 env=env, stdin=open('/dev/null'), stdout=logf, stderr=logf)
493 with open(_pidfile('arv-git-httpd'), 'w') as f:
494 f.write(str(agh.pid))
495 _setport('arv-git-httpd', gitport)
496 _wait_until_port_listens(gitport)
498 def stop_arv_git_httpd():
499 if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
501 kill_server_pid(_pidfile('arv-git-httpd'))
504 if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
508 keepwebport = find_available_port()
509 env = os.environ.copy()
510 env['ARVADOS_API_TOKEN'] = auth_token('anonymous')
511 logf = open(_fifo2stderr('keep-web'), 'w')
512 keepweb = subprocess.Popen(
515 '-attachment-only-host=download:'+str(keepwebport),
516 '-listen=:'+str(keepwebport)],
517 env=env, stdin=open('/dev/null'), stdout=logf, stderr=logf)
518 with open(_pidfile('keep-web'), 'w') as f:
519 f.write(str(keepweb.pid))
520 _setport('keep-web', keepwebport)
521 _wait_until_port_listens(keepwebport)
524 if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
526 kill_server_pid(_pidfile('keep-web'))
529 if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
532 nginxconf['KEEPWEBPORT'] = _getport('keep-web')
533 nginxconf['KEEPWEBDLSSLPORT'] = find_available_port()
534 nginxconf['KEEPWEBSSLPORT'] = find_available_port()
535 nginxconf['KEEPPROXYPORT'] = _getport('keepproxy')
536 nginxconf['KEEPPROXYSSLPORT'] = find_available_port()
537 nginxconf['GITPORT'] = _getport('arv-git-httpd')
538 nginxconf['GITSSLPORT'] = find_available_port()
539 nginxconf['SSLCERT'] = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.pem')
540 nginxconf['SSLKEY'] = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.key')
541 nginxconf['ACCESSLOG'] = _fifo2stderr('nginx_access_log')
543 conftemplatefile = os.path.join(MY_DIRNAME, 'nginx.conf')
544 conffile = os.path.join(TEST_TMPDIR, 'nginx.conf')
545 with open(conffile, 'w') as f:
548 lambda match: str(nginxconf.get(match.group(1))),
549 open(conftemplatefile).read()))
551 env = os.environ.copy()
552 env['PATH'] = env['PATH']+':/sbin:/usr/sbin:/usr/local/sbin'
554 nginx = subprocess.Popen(
556 '-g', 'error_log stderr info;',
557 '-g', 'pid '+_pidfile('nginx')+';',
559 env=env, stdin=open('/dev/null'), stdout=sys.stderr)
560 _setport('keep-web-dl-ssl', nginxconf['KEEPWEBDLSSLPORT'])
561 _setport('keep-web-ssl', nginxconf['KEEPWEBSSLPORT'])
562 _setport('keepproxy-ssl', nginxconf['KEEPPROXYSSLPORT'])
563 _setport('arv-git-httpd-ssl', nginxconf['GITSSLPORT'])
566 if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
568 kill_server_pid(_pidfile('nginx'))
570 def _pidfile(program):
571 return os.path.join(TEST_TMPDIR, program + '.pid')
573 def _portfile(program):
574 return os.path.join(TEST_TMPDIR, program + '.port')
576 def _setport(program, port):
577 with open(_portfile(program), 'w') as f:
580 # Returns 9 if program is not up.
581 def _getport(program):
583 return int(open(_portfile(program)).read())
589 return _cached_config[key]
590 def _load(f, required=True):
591 fullpath = os.path.join(SERVICES_SRC_DIR, 'api', 'config', f)
592 if not required and not os.path.exists(fullpath):
594 return yaml.load(fullpath)
595 cdefault = _load('application.default.yml')
596 csite = _load('application.yml', required=False)
598 for section in [cdefault.get('common',{}), cdefault.get('test',{}),
599 csite.get('common',{}), csite.get('test',{})]:
600 _cached_config.update(section)
601 return _cached_config[key]
604 '''load a fixture yaml file'''
605 with open(os.path.join(SERVICES_SRC_DIR, 'api', "test", "fixtures",
609 trim_index = yaml_file.index("# Test Helper trims the rest of the file")
610 yaml_file = yaml_file[0:trim_index]
613 return yaml.load(yaml_file)
615 def auth_token(token_name):
616 return fixture("api_client_authorizations")[token_name]["api_token"]
618 def authorize_with(token_name):
619 '''token_name is the symbolic name of the token from the api_client_authorizations fixture'''
620 arvados.config.settings()["ARVADOS_API_TOKEN"] = auth_token(token_name)
621 arvados.config.settings()["ARVADOS_API_HOST"] = os.environ.get("ARVADOS_API_HOST")
622 arvados.config.settings()["ARVADOS_API_HOST_INSECURE"] = "true"
624 class TestCaseWithServers(unittest.TestCase):
625 """TestCase to start and stop supporting Arvados servers.
627 Define any of MAIN_SERVER, KEEP_SERVER, and/or KEEP_PROXY_SERVER
628 class variables as a dictionary of keyword arguments. If you do,
629 setUpClass will start the corresponding servers by passing these
630 keyword arguments to the run, run_keep, and/or run_keep_server
631 functions, respectively. It will also set Arvados environment
632 variables to point to these servers appropriately. If you don't
633 run a Keep or Keep proxy server, setUpClass will set up a
634 temporary directory for Keep local storage, and set it as
637 tearDownClass will stop any servers started, and restore the
638 original environment.
642 KEEP_PROXY_SERVER = None
643 KEEP_WEB_SERVER = None
646 def _restore_dict(src, dest):
647 for key in dest.keys():
654 cls._orig_environ = os.environ.copy()
655 cls._orig_config = arvados.config.settings().copy()
656 cls._cleanup_funcs = []
657 os.environ.pop('ARVADOS_KEEP_PROXY', None)
658 os.environ.pop('ARVADOS_EXTERNAL_CLIENT', None)
659 for server_kwargs, start_func, stop_func in (
660 (cls.MAIN_SERVER, run, reset),
661 (cls.KEEP_SERVER, run_keep, stop_keep),
662 (cls.KEEP_PROXY_SERVER, run_keep_proxy, stop_keep_proxy),
663 (cls.KEEP_WEB_SERVER, run_keep_web, stop_keep_web)):
664 if server_kwargs is not None:
665 start_func(**server_kwargs)
666 cls._cleanup_funcs.append(stop_func)
667 if (cls.KEEP_SERVER is None) and (cls.KEEP_PROXY_SERVER is None):
668 cls.local_store = tempfile.mkdtemp()
669 os.environ['KEEP_LOCAL_STORE'] = cls.local_store
670 cls._cleanup_funcs.append(
671 lambda: shutil.rmtree(cls.local_store, ignore_errors=True))
673 os.environ.pop('KEEP_LOCAL_STORE', None)
674 arvados.config.initialize()
677 def tearDownClass(cls):
678 for clean_func in cls._cleanup_funcs:
680 cls._restore_dict(cls._orig_environ, os.environ)
681 cls._restore_dict(cls._orig_config, arvados.config.settings())
684 if __name__ == "__main__":
687 'start_keep', 'stop_keep',
688 'start_keep_proxy', 'stop_keep_proxy',
689 'start_keep-web', 'stop_keep-web',
690 'start_arv-git-httpd', 'stop_arv-git-httpd',
691 'start_nginx', 'stop_nginx',
693 parser = argparse.ArgumentParser()
694 parser.add_argument('action', type=str, help="one of {}".format(actions))
695 parser.add_argument('--auth', type=str, metavar='FIXTURE_NAME', help='Print authorization info for given api_client_authorizations fixture')
696 parser.add_argument('--num-keep-servers', metavar='int', type=int, default=2, help="Number of keep servers desired")
697 parser.add_argument('--keep-enforce-permissions', action="store_true", help="Enforce keep permissions")
699 args = parser.parse_args()
701 if args.action not in actions:
702 print("Unrecognized action '{}'. Actions are: {}.".
703 format(args.action, actions),
706 if args.action == 'start':
707 stop(force=('ARVADOS_TEST_API_HOST' not in os.environ))
708 run(leave_running_atexit=True)
709 host = os.environ['ARVADOS_API_HOST']
710 if args.auth is not None:
711 token = auth_token(args.auth)
712 print("export ARVADOS_API_TOKEN={}".format(pipes.quote(token)))
713 print("export ARVADOS_API_HOST={}".format(pipes.quote(host)))
714 print("export ARVADOS_API_HOST_INSECURE=true")
717 elif args.action == 'stop':
718 stop(force=('ARVADOS_TEST_API_HOST' not in os.environ))
719 elif args.action == 'start_keep':
720 run_keep(enforce_permissions=args.keep_enforce_permissions, num_servers=args.num_keep_servers)
721 elif args.action == 'stop_keep':
722 stop_keep(num_servers=args.num_keep_servers)
723 elif args.action == 'start_keep_proxy':
725 elif args.action == 'stop_keep_proxy':
727 elif args.action == 'start_arv-git-httpd':
729 elif args.action == 'stop_arv-git-httpd':
731 elif args.action == 'start_keep-web':
733 elif args.action == 'stop_keep-web':
735 elif args.action == 'start_nginx':
737 elif args.action == 'stop_nginx':
740 raise Exception("action recognized but not implemented!?")