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))
105 while now <= deadline:
107 exited, _ = os.waitpid(server_pid, os.WNOHANG)
111 # already exited, or isn't our child process
115 os.kill(server_pid, signal.SIGTERM)
116 print("Sent SIGTERM to {} ({})".format(server_pid, pidfile))
117 except OSError as error:
118 if error.errno == errno.ESRCH:
119 # Thrown by os.getpgid() or os.kill() if the process
120 # does not exist, i.e., our work here is done.
126 print("Server PID {} ({}) did not exit, giving up after {}s".
127 format(server_pid, pidfile, wait))
129 def find_available_port():
130 """Return an IPv4 port number that is not in use right now.
132 We assume whoever needs to use the returned port is able to reuse
133 a recently used port without waiting for TIME_WAIT (see
134 SO_REUSEADDR / SO_REUSEPORT).
136 Some opportunity for races here, but it's better than choosing
137 something at random and not checking at all. If all of our servers
138 (hey Passenger) knew that listening on port 0 was a thing, the OS
139 would take care of the races, and this wouldn't be needed at all.
142 sock = socket.socket()
143 sock.bind(('0.0.0.0', 0))
144 port = sock.getsockname()[1]
148 def _wait_until_port_listens(port, timeout=10):
149 """Wait for a process to start listening on the given port.
151 If nothing listens on the port within the specified timeout (given
152 in seconds), print a warning on stderr before returning.
155 subprocess.check_output(['which', 'lsof'])
156 except subprocess.CalledProcessError:
157 print("WARNING: No `lsof` -- cannot wait for port to listen. "+
158 "Sleeping 0.5 and hoping for the best.")
161 deadline = time.time() + timeout
162 while time.time() < deadline:
164 subprocess.check_output(
165 ['lsof', '-t', '-i', 'tcp:'+str(port)])
166 except subprocess.CalledProcessError:
171 "WARNING: Nothing is listening on port {} (waited {} seconds).".
172 format(port, timeout),
175 def _fifo2stderr(label):
176 """Create a fifo, and copy it to stderr, prepending label to each line.
178 Return value is the path to the new FIFO.
180 +label+ should contain only alphanumerics: it is also used as part
181 of the FIFO filename.
183 fifo = os.path.join(TEST_TMPDIR, label+'.fifo')
186 except OSError as error:
187 if error.errno != errno.ENOENT:
189 os.mkfifo(fifo, 0700)
191 ['sed', '-e', 's/^/['+label+'] /', fifo],
195 def run(leave_running_atexit=False):
196 """Ensure an API server is running, and ARVADOS_API_* env vars have
197 admin credentials for it.
199 If ARVADOS_TEST_API_HOST is set, a parent process has started a
200 test server for us to use: we just need to reset() it using the
203 If a previous call to run() started a new server process, and it
204 is still running, we just need to reset() it to fixture state and
207 If neither of those options work out, we'll really start a new
212 # Delete cached discovery document.
213 shutil.rmtree(arvados.http_cache('discovery'))
215 pid_file = _pidfile('api')
216 pid_file_ok = find_server_pid(pid_file, 0)
218 existing_api_host = os.environ.get('ARVADOS_TEST_API_HOST', my_api_host)
219 if existing_api_host and pid_file_ok:
220 if existing_api_host == my_api_host:
224 # Fall through to shutdown-and-start case.
227 # Server was provided by parent. Can't recover if it's
231 # Before trying to start up our own server, call stop() to avoid
232 # "Phusion Passenger Standalone is already running on PID 12345".
233 # (If we've gotten this far, ARVADOS_TEST_API_HOST isn't set, so
234 # we know the server is ours to kill.)
237 restore_cwd = os.getcwd()
238 api_src_dir = os.path.join(SERVICES_SRC_DIR, 'api')
239 os.chdir(api_src_dir)
241 # Either we haven't started a server of our own yet, or it has
242 # died, or we have lost our credentials, or something else is
243 # preventing us from calling reset(). Start a new one.
245 if not os.path.exists('tmp'):
248 if not os.path.exists('tmp/api'):
249 os.makedirs('tmp/api')
251 if not os.path.exists('tmp/logs'):
252 os.makedirs('tmp/logs')
254 if not os.path.exists('tmp/self-signed.pem'):
255 # We assume here that either passenger reports its listening
256 # address as https:/0.0.0.0:port/. If it reports "127.0.0.1"
257 # then the certificate won't match the host and reset() will
258 # fail certificate verification. If it reports "localhost",
259 # clients (notably Python SDK's websocket client) might
260 # resolve localhost as ::1 and then fail to connect.
261 subprocess.check_call([
262 'openssl', 'req', '-new', '-x509', '-nodes',
263 '-out', 'tmp/self-signed.pem',
264 '-keyout', 'tmp/self-signed.key',
266 '-subj', '/CN=0.0.0.0'],
269 # Install the git repository fixtures.
270 gitdir = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'git')
271 gittarball = os.path.join(SERVICES_SRC_DIR, 'api', 'test', 'test.git.tar')
272 if not os.path.isdir(gitdir):
274 subprocess.check_output(['tar', '-xC', gitdir, '-f', gittarball])
276 port = find_available_port()
277 env = os.environ.copy()
278 env['RAILS_ENV'] = 'test'
279 env['ARVADOS_WEBSOCKETS'] = 'yes'
280 env.pop('ARVADOS_TEST_API_HOST', None)
281 env.pop('ARVADOS_API_HOST', None)
282 env.pop('ARVADOS_API_HOST_INSECURE', None)
283 env.pop('ARVADOS_API_TOKEN', None)
284 start_msg = subprocess.check_output(
286 'passenger', 'start', '-d', '-p{}'.format(port),
287 '--pid-file', pid_file,
288 '--log-file', os.path.join(os.getcwd(), 'log/test.log'),
290 '--ssl-certificate', 'tmp/self-signed.pem',
291 '--ssl-certificate-key', 'tmp/self-signed.key'],
294 if not leave_running_atexit:
295 atexit.register(kill_server_pid, pid_file, passenger_root=api_src_dir)
297 match = re.search(r'Accessible via: https://(.*?)/', start_msg)
300 "Passenger did not report endpoint: {}".format(start_msg))
301 my_api_host = match.group(1)
302 os.environ['ARVADOS_API_HOST'] = my_api_host
304 # Make sure the server has written its pid file and started
305 # listening on its TCP port
306 find_server_pid(pid_file)
307 _wait_until_port_listens(port)
310 os.chdir(restore_cwd)
313 """Reset the test server to fixture state.
315 This resets the ARVADOS_TEST_API_HOST provided by a parent process
316 if any, otherwise the server started by run().
318 It also resets ARVADOS_* environment vars to point to the test
319 server with admin credentials.
321 existing_api_host = os.environ.get('ARVADOS_TEST_API_HOST', my_api_host)
322 token = auth_token('admin')
323 httpclient = httplib2.Http(ca_certs=os.path.join(
324 SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.pem'))
326 'https://{}/database/reset'.format(existing_api_host),
328 headers={'Authorization': 'OAuth2 {}'.format(token)})
329 os.environ['ARVADOS_API_HOST_INSECURE'] = 'true'
330 os.environ['ARVADOS_API_HOST'] = existing_api_host
331 os.environ['ARVADOS_API_TOKEN'] = token
333 def stop(force=False):
334 """Stop the API server, if one is running.
336 If force==False, kill it only if we started it ourselves. (This
337 supports the use case where a Python test suite calls run(), but
338 run() just uses the ARVADOS_TEST_API_HOST provided by the parent
339 process, and the test suite cleans up after itself by calling
340 stop(). In this case the test server provided by the parent
341 process should be left alone.)
343 If force==True, kill it even if we didn't start it
344 ourselves. (This supports the use case in __main__, where "run"
345 and "stop" happen in different processes.)
348 if force or my_api_host is not None:
349 kill_server_pid(_pidfile('api'))
352 def _start_keep(n, keep_args):
353 keep0 = tempfile.mkdtemp()
354 port = find_available_port()
355 keep_cmd = ["keepstore",
356 "-volume={}".format(keep0),
357 "-listen=:{}".format(port),
358 "-pid="+_pidfile('keep{}'.format(n))]
360 for arg, val in keep_args.iteritems():
361 keep_cmd.append("{}={}".format(arg, val))
363 logf = open(_fifo2stderr('keep{}'.format(n)), 'w')
364 kp0 = subprocess.Popen(
365 keep_cmd, stdin=open('/dev/null'), stdout=logf, stderr=logf, close_fds=True)
367 with open(_pidfile('keep{}'.format(n)), 'w') as f:
368 f.write(str(kp0.pid))
370 with open("{}/keep{}.volume".format(TEST_TMPDIR, n), 'w') as f:
373 _wait_until_port_listens(port)
377 def run_keep(blob_signing_key=None, enforce_permissions=False, num_servers=2):
378 stop_keep(num_servers)
381 if not blob_signing_key:
382 blob_signing_key = 'zfhgfenhffzltr9dixws36j1yhksjoll2grmku38mi7yxd66h5j4q9w4jzanezacp8s6q0ro3hxakfye02152hncy6zml2ed0uc'
383 with open(os.path.join(TEST_TMPDIR, "keep.blob_signing_key"), "w") as f:
384 keep_args['-blob-signing-key-file'] = f.name
385 f.write(blob_signing_key)
386 if enforce_permissions:
387 keep_args['-enforce-permissions'] = 'true'
388 with open(os.path.join(TEST_TMPDIR, "keep.data-manager-token-file"), "w") as f:
389 keep_args['-data-manager-token-file'] = f.name
390 f.write(auth_token('data_manager'))
391 keep_args['-never-delete'] = 'false'
395 host=os.environ['ARVADOS_API_HOST'],
396 token=os.environ['ARVADOS_API_TOKEN'],
399 for d in api.keep_services().list(filters=[['service_type','=','disk']]).execute()['items']:
400 api.keep_services().delete(uuid=d['uuid']).execute()
401 for d in api.keep_disks().list().execute()['items']:
402 api.keep_disks().delete(uuid=d['uuid']).execute()
404 for d in range(0, num_servers):
405 port = _start_keep(d, keep_args)
406 svc = api.keep_services().create(body={'keep_service': {
407 'uuid': 'zzzzz-bi6l4-keepdisk{:07d}'.format(d),
408 'service_host': 'localhost',
409 'service_port': port,
410 'service_type': 'disk',
411 'service_ssl_flag': False,
413 api.keep_disks().create(body={
414 'keep_disk': {'keep_service_uuid': svc['uuid'] }
417 # If keepproxy is running, send SIGHUP to make it discover the new
418 # keepstore services.
419 proxypidfile = _pidfile('keepproxy')
420 if os.path.exists(proxypidfile):
421 os.kill(int(open(proxypidfile).read()), signal.SIGHUP)
424 kill_server_pid(_pidfile('keep{}'.format(n)))
425 if os.path.exists("{}/keep{}.volume".format(TEST_TMPDIR, n)):
426 with open("{}/keep{}.volume".format(TEST_TMPDIR, n), 'r') as r:
427 shutil.rmtree(r.read(), True)
428 os.unlink("{}/keep{}.volume".format(TEST_TMPDIR, n))
429 if os.path.exists(os.path.join(TEST_TMPDIR, "keep.blob_signing_key")):
430 os.remove(os.path.join(TEST_TMPDIR, "keep.blob_signing_key"))
432 def stop_keep(num_servers=2):
433 for n in range(0, num_servers):
436 def run_keep_proxy():
437 if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
441 port = find_available_port()
442 env = os.environ.copy()
443 env['ARVADOS_API_TOKEN'] = auth_token('anonymous')
444 logf = open(_fifo2stderr('keepproxy'), 'w')
445 kp = subprocess.Popen(
447 '-pid='+_pidfile('keepproxy'),
448 '-listen=:{}'.format(port)],
449 env=env, stdin=open('/dev/null'), stdout=logf, stderr=logf, close_fds=True)
453 host=os.environ['ARVADOS_API_HOST'],
454 token=auth_token('admin'),
456 for d in api.keep_services().list(
457 filters=[['service_type','=','proxy']]).execute()['items']:
458 api.keep_services().delete(uuid=d['uuid']).execute()
459 api.keep_services().create(body={'keep_service': {
460 'service_host': 'localhost',
461 'service_port': port,
462 'service_type': 'proxy',
463 'service_ssl_flag': False,
465 os.environ["ARVADOS_KEEP_PROXY"] = "http://localhost:{}".format(port)
466 _setport('keepproxy', port)
467 _wait_until_port_listens(port)
469 def stop_keep_proxy():
470 if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
472 kill_server_pid(_pidfile('keepproxy'))
474 def run_arv_git_httpd():
475 if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
479 gitdir = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'git')
480 gitport = find_available_port()
481 env = os.environ.copy()
482 env.pop('ARVADOS_API_TOKEN', None)
483 logf = open(_fifo2stderr('arv-git-httpd'), 'w')
484 agh = subprocess.Popen(
486 '-repo-root='+gitdir+'/test',
487 '-address=:'+str(gitport)],
488 env=env, stdin=open('/dev/null'), stdout=logf, stderr=logf)
489 with open(_pidfile('arv-git-httpd'), 'w') as f:
490 f.write(str(agh.pid))
491 _setport('arv-git-httpd', gitport)
492 _wait_until_port_listens(gitport)
494 def stop_arv_git_httpd():
495 if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
497 kill_server_pid(_pidfile('arv-git-httpd'))
500 if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
504 keepwebport = find_available_port()
505 env = os.environ.copy()
506 env['ARVADOS_API_TOKEN'] = auth_token('anonymous')
507 logf = open(_fifo2stderr('keep-web'), 'w')
508 keepweb = subprocess.Popen(
511 '-attachment-only-host=download:'+str(keepwebport),
512 '-listen=:'+str(keepwebport)],
513 env=env, stdin=open('/dev/null'), stdout=logf, stderr=logf)
514 with open(_pidfile('keep-web'), 'w') as f:
515 f.write(str(keepweb.pid))
516 _setport('keep-web', keepwebport)
517 _wait_until_port_listens(keepwebport)
520 if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
522 kill_server_pid(_pidfile('keep-web'))
525 if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
528 nginxconf['KEEPWEBPORT'] = _getport('keep-web')
529 nginxconf['KEEPWEBDLSSLPORT'] = find_available_port()
530 nginxconf['KEEPWEBSSLPORT'] = find_available_port()
531 nginxconf['KEEPPROXYPORT'] = _getport('keepproxy')
532 nginxconf['KEEPPROXYSSLPORT'] = find_available_port()
533 nginxconf['GITPORT'] = _getport('arv-git-httpd')
534 nginxconf['GITSSLPORT'] = find_available_port()
535 nginxconf['SSLCERT'] = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.pem')
536 nginxconf['SSLKEY'] = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.key')
537 nginxconf['ACCESSLOG'] = _fifo2stderr('nginx_access_log')
539 conftemplatefile = os.path.join(MY_DIRNAME, 'nginx.conf')
540 conffile = os.path.join(TEST_TMPDIR, 'nginx.conf')
541 with open(conffile, 'w') as f:
544 lambda match: str(nginxconf.get(match.group(1))),
545 open(conftemplatefile).read()))
547 env = os.environ.copy()
548 env['PATH'] = env['PATH']+':/sbin:/usr/sbin:/usr/local/sbin'
550 nginx = subprocess.Popen(
552 '-g', 'error_log stderr info;',
553 '-g', 'pid '+_pidfile('nginx')+';',
555 env=env, stdin=open('/dev/null'), stdout=sys.stderr)
556 _setport('keep-web-dl-ssl', nginxconf['KEEPWEBDLSSLPORT'])
557 _setport('keep-web-ssl', nginxconf['KEEPWEBSSLPORT'])
558 _setport('keepproxy-ssl', nginxconf['KEEPPROXYSSLPORT'])
559 _setport('arv-git-httpd-ssl', nginxconf['GITSSLPORT'])
562 if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
564 kill_server_pid(_pidfile('nginx'))
566 def _pidfile(program):
567 return os.path.join(TEST_TMPDIR, program + '.pid')
569 def _portfile(program):
570 return os.path.join(TEST_TMPDIR, program + '.port')
572 def _setport(program, port):
573 with open(_portfile(program), 'w') as f:
576 # Returns 9 if program is not up.
577 def _getport(program):
579 return int(open(_portfile(program)).read())
585 return _cached_config[key]
586 def _load(f, required=True):
587 fullpath = os.path.join(SERVICES_SRC_DIR, 'api', 'config', f)
588 if not required and not os.path.exists(fullpath):
590 return yaml.load(fullpath)
591 cdefault = _load('application.default.yml')
592 csite = _load('application.yml', required=False)
594 for section in [cdefault.get('common',{}), cdefault.get('test',{}),
595 csite.get('common',{}), csite.get('test',{})]:
596 _cached_config.update(section)
597 return _cached_config[key]
600 '''load a fixture yaml file'''
601 with open(os.path.join(SERVICES_SRC_DIR, 'api', "test", "fixtures",
605 trim_index = yaml_file.index("# Test Helper trims the rest of the file")
606 yaml_file = yaml_file[0:trim_index]
609 return yaml.load(yaml_file)
611 def auth_token(token_name):
612 return fixture("api_client_authorizations")[token_name]["api_token"]
614 def authorize_with(token_name):
615 '''token_name is the symbolic name of the token from the api_client_authorizations fixture'''
616 arvados.config.settings()["ARVADOS_API_TOKEN"] = auth_token(token_name)
617 arvados.config.settings()["ARVADOS_API_HOST"] = os.environ.get("ARVADOS_API_HOST")
618 arvados.config.settings()["ARVADOS_API_HOST_INSECURE"] = "true"
620 class TestCaseWithServers(unittest.TestCase):
621 """TestCase to start and stop supporting Arvados servers.
623 Define any of MAIN_SERVER, KEEP_SERVER, and/or KEEP_PROXY_SERVER
624 class variables as a dictionary of keyword arguments. If you do,
625 setUpClass will start the corresponding servers by passing these
626 keyword arguments to the run, run_keep, and/or run_keep_server
627 functions, respectively. It will also set Arvados environment
628 variables to point to these servers appropriately. If you don't
629 run a Keep or Keep proxy server, setUpClass will set up a
630 temporary directory for Keep local storage, and set it as
633 tearDownClass will stop any servers started, and restore the
634 original environment.
638 KEEP_PROXY_SERVER = None
639 KEEP_WEB_SERVER = None
642 def _restore_dict(src, dest):
643 for key in dest.keys():
650 cls._orig_environ = os.environ.copy()
651 cls._orig_config = arvados.config.settings().copy()
652 cls._cleanup_funcs = []
653 os.environ.pop('ARVADOS_KEEP_PROXY', None)
654 os.environ.pop('ARVADOS_EXTERNAL_CLIENT', None)
655 for server_kwargs, start_func, stop_func in (
656 (cls.MAIN_SERVER, run, reset),
657 (cls.KEEP_SERVER, run_keep, stop_keep),
658 (cls.KEEP_PROXY_SERVER, run_keep_proxy, stop_keep_proxy),
659 (cls.KEEP_WEB_SERVER, run_keep_web, stop_keep_web)):
660 if server_kwargs is not None:
661 start_func(**server_kwargs)
662 cls._cleanup_funcs.append(stop_func)
663 if (cls.KEEP_SERVER is None) and (cls.KEEP_PROXY_SERVER is None):
664 cls.local_store = tempfile.mkdtemp()
665 os.environ['KEEP_LOCAL_STORE'] = cls.local_store
666 cls._cleanup_funcs.append(
667 lambda: shutil.rmtree(cls.local_store, ignore_errors=True))
669 os.environ.pop('KEEP_LOCAL_STORE', None)
670 arvados.config.initialize()
673 def tearDownClass(cls):
674 for clean_func in cls._cleanup_funcs:
676 cls._restore_dict(cls._orig_environ, os.environ)
677 cls._restore_dict(cls._orig_config, arvados.config.settings())
680 if __name__ == "__main__":
683 'start_keep', 'stop_keep',
684 'start_keep_proxy', 'stop_keep_proxy',
685 'start_keep-web', 'stop_keep-web',
686 'start_arv-git-httpd', 'stop_arv-git-httpd',
687 'start_nginx', 'stop_nginx',
689 parser = argparse.ArgumentParser()
690 parser.add_argument('action', type=str, help="one of {}".format(actions))
691 parser.add_argument('--auth', type=str, metavar='FIXTURE_NAME', help='Print authorization info for given api_client_authorizations fixture')
692 parser.add_argument('--num-keep-servers', metavar='int', type=int, default=2, help="Number of keep servers desired")
693 parser.add_argument('--keep-enforce-permissions', action="store_true", help="Enforce keep permissions")
695 args = parser.parse_args()
697 if args.action not in actions:
698 print("Unrecognized action '{}'. Actions are: {}.".format(args.action, actions), file=sys.stderr)
700 if args.action == 'start':
701 stop(force=('ARVADOS_TEST_API_HOST' not in os.environ))
702 run(leave_running_atexit=True)
703 host = os.environ['ARVADOS_API_HOST']
704 if args.auth is not None:
705 token = auth_token(args.auth)
706 print("export ARVADOS_API_TOKEN={}".format(pipes.quote(token)))
707 print("export ARVADOS_API_HOST={}".format(pipes.quote(host)))
708 print("export ARVADOS_API_HOST_INSECURE=true")
711 elif args.action == 'stop':
712 stop(force=('ARVADOS_TEST_API_HOST' not in os.environ))
713 elif args.action == 'start_keep':
714 run_keep(enforce_permissions=args.keep_enforce_permissions, num_servers=args.num_keep_servers)
715 elif args.action == 'stop_keep':
716 stop_keep(num_servers=args.num_keep_servers)
717 elif args.action == 'start_keep_proxy':
719 elif args.action == 'stop_keep_proxy':
721 elif args.action == 'start_arv-git-httpd':
723 elif args.action == 'stop_arv-git-httpd':
725 elif args.action == 'start_keep-web':
727 elif args.action == 'stop_keep-web':
729 elif args.action == 'start_nginx':
731 elif args.action == 'stop_nginx':
734 raise Exception("action recognized but not implemented!?")