5898: Wait for servers to start *listening* before running integration tests against...
[arvados.git] / sdk / python / tests / run_test_server.py
1 #!/usr/bin/env python
2
3 from __future__ import print_function
4 import argparse
5 import atexit
6 import httplib2
7 import os
8 import pipes
9 import random
10 import re
11 import shutil
12 import signal
13 import socket
14 import subprocess
15 import string
16 import sys
17 import tempfile
18 import time
19 import unittest
20 import yaml
21
22 MY_DIRNAME = os.path.dirname(os.path.realpath(__file__))
23 if __name__ == '__main__' and os.path.exists(
24       os.path.join(MY_DIRNAME, '..', 'arvados', '__init__.py')):
25     # We're being launched to support another test suite.
26     # Add the Python SDK source to the library path.
27     sys.path.insert(1, os.path.dirname(MY_DIRNAME))
28
29 import arvados
30 import arvados.config
31
32 ARVADOS_DIR = os.path.realpath(os.path.join(MY_DIRNAME, '../../..'))
33 SERVICES_SRC_DIR = os.path.join(ARVADOS_DIR, 'services')
34 SERVER_PID_PATH = 'tmp/pids/test-server.pid'
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']
39
40 TEST_TMPDIR = os.path.join(ARVADOS_DIR, 'tmp')
41 if not os.path.exists(TEST_TMPDIR):
42     os.mkdir(TEST_TMPDIR)
43
44 my_api_host = None
45 _cached_config = {}
46
47 def find_server_pid(PID_PATH, wait=10):
48     now = time.time()
49     timeout = now + wait
50     good_pid = False
51     while (not good_pid) and (now <= timeout):
52         time.sleep(0.2)
53         try:
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 IOError:
58             good_pid = False
59         except OSError:
60             good_pid = False
61         now = time.time()
62
63     if not good_pid:
64         return None
65
66     return server_pid
67
68 def kill_server_pid(pidfile, wait=10, passenger_root=False):
69     # Must re-import modules in order to work during atexit
70     import os
71     import signal
72     import subprocess
73     import time
74     try:
75         if passenger_root:
76             # First try to shut down nicely
77             restore_cwd = os.getcwd()
78             os.chdir(passenger_root)
79             subprocess.call([
80                 'bundle', 'exec', 'passenger', 'stop', '--pid-file', pidfile])
81             os.chdir(restore_cwd)
82         now = time.time()
83         timeout = now + wait
84         with open(pidfile, 'r') as f:
85             server_pid = int(f.read())
86         while now <= timeout:
87             if not passenger_root or timeout - now < wait / 2:
88                 # Half timeout has elapsed. Start sending SIGTERM
89                 os.kill(server_pid, signal.SIGTERM)
90             # Raise OSError if process has disappeared
91             os.getpgid(server_pid)
92             time.sleep(0.1)
93             now = time.time()
94     except IOError:
95         pass
96     except OSError:
97         pass
98
99 def find_available_port():
100     """Return an IPv4 port number that is not in use right now.
101
102     We assume whoever needs to use the returned port is able to reuse
103     a recently used port without waiting for TIME_WAIT (see
104     SO_REUSEADDR / SO_REUSEPORT).
105
106     Some opportunity for races here, but it's better than choosing
107     something at random and not checking at all. If all of our servers
108     (hey Passenger) knew that listening on port 0 was a thing, the OS
109     would take care of the races, and this wouldn't be needed at all.
110     """
111
112     sock = socket.socket()
113     sock.bind(('0.0.0.0', 0))
114     port = sock.getsockname()[1]
115     sock.close()
116     return port
117
118 def _wait_until_port_listens(port, timeout=10):
119     """Wait for a process to start listening on the given port.
120
121     If nothing listens on the port within the specified timeout (given
122     in seconds), print a warning on stderr before returning.
123     """
124     try:
125         subprocess.check_output(['fuser', '-l'])
126     except subprocess.CalledProcessError:
127         print("WARNING: No `fuser` -- cannot wait for port to listen. "+
128               "Sleeping 0.5 and hoping for the best.")
129         time.sleep(0.5)
130         return
131     deadline = time.time() + timeout
132     while time.time() < deadline:
133         try:
134             fuser_says = subprocess.check_output(['fuser', str(port)+'/tcp'])
135         except subprocess.CalledProcessError:
136             time.sleep(0.1)
137             continue
138         return
139     print(
140         "WARNING: Nothing is listening on port {} (waited {} seconds).".
141         format(port, timeout),
142         file=sys.stderr)
143
144 def run(leave_running_atexit=False):
145     """Ensure an API server is running, and ARVADOS_API_* env vars have
146     admin credentials for it.
147
148     If ARVADOS_TEST_API_HOST is set, a parent process has started a
149     test server for us to use: we just need to reset() it using the
150     admin token fixture.
151
152     If a previous call to run() started a new server process, and it
153     is still running, we just need to reset() it to fixture state and
154     return.
155
156     If neither of those options work out, we'll really start a new
157     server.
158     """
159     global my_api_host
160
161     # Delete cached discovery document.
162     shutil.rmtree(arvados.http_cache('discovery'))
163
164     pid_file = os.path.join(SERVICES_SRC_DIR, 'api', SERVER_PID_PATH)
165     pid_file_ok = find_server_pid(pid_file, 0)
166
167     existing_api_host = os.environ.get('ARVADOS_TEST_API_HOST', my_api_host)
168     if existing_api_host and pid_file_ok:
169         if existing_api_host == my_api_host:
170             try:
171                 return reset()
172             except:
173                 # Fall through to shutdown-and-start case.
174                 pass
175         else:
176             # Server was provided by parent. Can't recover if it's
177             # unresettable.
178             return reset()
179
180     # Before trying to start up our own server, call stop() to avoid
181     # "Phusion Passenger Standalone is already running on PID 12345".
182     # (If we've gotten this far, ARVADOS_TEST_API_HOST isn't set, so
183     # we know the server is ours to kill.)
184     stop(force=True)
185
186     restore_cwd = os.getcwd()
187     api_src_dir = os.path.join(SERVICES_SRC_DIR, 'api')
188     os.chdir(api_src_dir)
189
190     # Either we haven't started a server of our own yet, or it has
191     # died, or we have lost our credentials, or something else is
192     # preventing us from calling reset(). Start a new one.
193
194     if not os.path.exists('tmp'):
195         os.makedirs('tmp')
196
197     if not os.path.exists('tmp/api'):
198         os.makedirs('tmp/api')
199
200     if not os.path.exists('tmp/logs'):
201         os.makedirs('tmp/logs')
202
203     if not os.path.exists('tmp/self-signed.pem'):
204         # We assume here that either passenger reports its listening
205         # address as https:/0.0.0.0:port/. If it reports "127.0.0.1"
206         # then the certificate won't match the host and reset() will
207         # fail certificate verification. If it reports "localhost",
208         # clients (notably Python SDK's websocket client) might
209         # resolve localhost as ::1 and then fail to connect.
210         subprocess.check_call([
211             'openssl', 'req', '-new', '-x509', '-nodes',
212             '-out', 'tmp/self-signed.pem',
213             '-keyout', 'tmp/self-signed.key',
214             '-days', '3650',
215             '-subj', '/CN=0.0.0.0'],
216         stdout=sys.stderr)
217
218     # Install the git repository fixtures.
219     gitdir = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'git')
220     gittarball = os.path.join(SERVICES_SRC_DIR, 'api', 'test', 'test.git.tar')
221     if not os.path.isdir(gitdir):
222         os.makedirs(gitdir)
223     subprocess.check_output(['tar', '-xC', gitdir, '-f', gittarball])
224
225     port = find_available_port()
226     env = os.environ.copy()
227     env['RAILS_ENV'] = 'test'
228     env['ARVADOS_WEBSOCKETS'] = 'yes'
229     env.pop('ARVADOS_TEST_API_HOST', None)
230     env.pop('ARVADOS_API_HOST', None)
231     env.pop('ARVADOS_API_HOST_INSECURE', None)
232     env.pop('ARVADOS_API_TOKEN', None)
233     start_msg = subprocess.check_output(
234         ['bundle', 'exec',
235          'passenger', 'start', '-d', '-p{}'.format(port),
236          '--pid-file', os.path.join(os.getcwd(), pid_file),
237          '--log-file', os.path.join(os.getcwd(), 'log/test.log'),
238          '--ssl',
239          '--ssl-certificate', 'tmp/self-signed.pem',
240          '--ssl-certificate-key', 'tmp/self-signed.key'],
241         env=env)
242
243     if not leave_running_atexit:
244         atexit.register(kill_server_pid, pid_file, passenger_root=api_src_dir)
245
246     match = re.search(r'Accessible via: https://(.*?)/', start_msg)
247     if not match:
248         raise Exception(
249             "Passenger did not report endpoint: {}".format(start_msg))
250     my_api_host = match.group(1)
251     os.environ['ARVADOS_API_HOST'] = my_api_host
252
253     # Make sure the server has written its pid file and started
254     # listening on its TCP port
255     find_server_pid(pid_file)
256     _wait_until_port_listens(port)
257
258     reset()
259     os.chdir(restore_cwd)
260
261 def reset():
262     """Reset the test server to fixture state.
263
264     This resets the ARVADOS_TEST_API_HOST provided by a parent process
265     if any, otherwise the server started by run().
266
267     It also resets ARVADOS_* environment vars to point to the test
268     server with admin credentials.
269     """
270     existing_api_host = os.environ.get('ARVADOS_TEST_API_HOST', my_api_host)
271     token = auth_token('admin')
272     httpclient = httplib2.Http(ca_certs=os.path.join(
273         SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.pem'))
274     httpclient.request(
275         'https://{}/database/reset'.format(existing_api_host),
276         'POST',
277         headers={'Authorization': 'OAuth2 {}'.format(token)})
278     os.environ['ARVADOS_API_HOST_INSECURE'] = 'true'
279     os.environ['ARVADOS_API_HOST'] = existing_api_host
280     os.environ['ARVADOS_API_TOKEN'] = token
281
282 def stop(force=False):
283     """Stop the API server, if one is running.
284
285     If force==False, kill it only if we started it ourselves. (This
286     supports the use case where a Python test suite calls run(), but
287     run() just uses the ARVADOS_TEST_API_HOST provided by the parent
288     process, and the test suite cleans up after itself by calling
289     stop(). In this case the test server provided by the parent
290     process should be left alone.)
291
292     If force==True, kill it even if we didn't start it
293     ourselves. (This supports the use case in __main__, where "run"
294     and "stop" happen in different processes.)
295     """
296     global my_api_host
297     if force or my_api_host is not None:
298         kill_server_pid(os.path.join(SERVICES_SRC_DIR, 'api', SERVER_PID_PATH))
299         my_api_host = None
300
301 def _start_keep(n, keep_args):
302     keep0 = tempfile.mkdtemp()
303     port = find_available_port()
304     keep_cmd = ["keepstore",
305                 "-volume={}".format(keep0),
306                 "-listen=:{}".format(port),
307                 "-pid="+_pidfile('keep{}'.format(n))]
308
309     for arg, val in keep_args.iteritems():
310         keep_cmd.append("{}={}".format(arg, val))
311
312     kp0 = subprocess.Popen(
313         keep_cmd, stdin=open('/dev/null'), stdout=sys.stderr)
314     with open(_pidfile('keep{}'.format(n)), 'w') as f:
315         f.write(str(kp0.pid))
316
317     with open("{}/keep{}.volume".format(TEST_TMPDIR, n), 'w') as f:
318         f.write(keep0)
319
320     _wait_until_port_listens(port)
321
322     return port
323
324 def run_keep(blob_signing_key=None, enforce_permissions=False):
325     stop_keep()
326
327     keep_args = {}
328     if blob_signing_key:
329         with open(os.path.join(TEST_TMPDIR, "keep.blob_signing_key"), "w") as f:
330             keep_args['--permission-key-file'] = f.name
331             f.write(blob_signing_key)
332     if enforce_permissions:
333         keep_args['--enforce-permissions'] = 'true'
334
335     api = arvados.api(
336         version='v1',
337         host=os.environ['ARVADOS_API_HOST'],
338         token=os.environ['ARVADOS_API_TOKEN'],
339         insecure=True)
340     for d in api.keep_services().list().execute()['items']:
341         api.keep_services().delete(uuid=d['uuid']).execute()
342     for d in api.keep_disks().list().execute()['items']:
343         api.keep_disks().delete(uuid=d['uuid']).execute()
344
345     for d in range(0, 2):
346         port = _start_keep(d, keep_args)
347         svc = api.keep_services().create(body={'keep_service': {
348             'uuid': 'zzzzz-bi6l4-keepdisk{:07d}'.format(d),
349             'service_host': 'localhost',
350             'service_port': port,
351             'service_type': 'disk',
352             'service_ssl_flag': False,
353         }}).execute()
354         api.keep_disks().create(body={
355             'keep_disk': {'keep_service_uuid': svc['uuid'] }
356         }).execute()
357
358 def _stop_keep(n):
359     kill_server_pid(_pidfile('keep{}'.format(n)), 0)
360     if os.path.exists("{}/keep{}.volume".format(TEST_TMPDIR, n)):
361         with open("{}/keep{}.volume".format(TEST_TMPDIR, n), 'r') as r:
362             shutil.rmtree(r.read(), True)
363         os.unlink("{}/keep{}.volume".format(TEST_TMPDIR, n))
364     if os.path.exists(os.path.join(TEST_TMPDIR, "keep.blob_signing_key")):
365         os.remove(os.path.join(TEST_TMPDIR, "keep.blob_signing_key"))
366
367 def stop_keep():
368     _stop_keep(0)
369     _stop_keep(1)
370
371 def run_keep_proxy():
372     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
373         return
374     stop_keep_proxy()
375
376     admin_token = auth_token('admin')
377     port = find_available_port()
378     env = os.environ.copy()
379     env['ARVADOS_API_TOKEN'] = admin_token
380     kp = subprocess.Popen(
381         ['keepproxy',
382          '-pid='+_pidfile('keepproxy'),
383          '-listen=:{}'.format(port)],
384         env=env, stdin=open('/dev/null'), stdout=sys.stderr)
385
386     api = arvados.api(
387         version='v1',
388         host=os.environ['ARVADOS_API_HOST'],
389         token=admin_token,
390         insecure=True)
391     for d in api.keep_services().list(
392             filters=[['service_type','=','proxy']]).execute()['items']:
393         api.keep_services().delete(uuid=d['uuid']).execute()
394     api.keep_services().create(body={'keep_service': {
395         'service_host': 'localhost',
396         'service_port': port,
397         'service_type': 'proxy',
398         'service_ssl_flag': False,
399     }}).execute()
400     os.environ["ARVADOS_KEEP_PROXY"] = "http://localhost:{}".format(port)
401     _setport('keepproxy', port)
402     _wait_until_port_listens(port)
403
404 def stop_keep_proxy():
405     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
406         return
407     kill_server_pid(_pidfile('keepproxy'), wait=0)
408
409 def run_arv_git_httpd():
410     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
411         return
412     stop_arv_git_httpd()
413
414     gitdir = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'git')
415     gitport = find_available_port()
416     env = os.environ.copy()
417     env.pop('ARVADOS_API_TOKEN', None)
418     agh = subprocess.Popen(
419         ['arv-git-httpd',
420          '-repo-root='+gitdir+'/test',
421          '-address=:'+str(gitport)],
422         env=env, stdin=open('/dev/null'), stdout=sys.stderr)
423     with open(_pidfile('arv-git-httpd'), 'w') as f:
424         f.write(str(agh.pid))
425     _setport('arv-git-httpd', gitport)
426     _wait_until_port_listens(gitport)
427
428 def stop_arv_git_httpd():
429     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
430         return
431     kill_server_pid(_pidfile('arv-git-httpd'), wait=0)
432
433 def run_nginx():
434     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
435         return
436     nginxconf = {}
437     nginxconf['KEEPPROXYPORT'] = _getport('keepproxy')
438     nginxconf['KEEPPROXYSSLPORT'] = find_available_port()
439     nginxconf['GITPORT'] = _getport('arv-git-httpd')
440     nginxconf['GITSSLPORT'] = find_available_port()
441     nginxconf['SSLCERT'] = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.pem')
442     nginxconf['SSLKEY'] = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.key')
443
444     conftemplatefile = os.path.join(MY_DIRNAME, 'nginx.conf')
445     conffile = os.path.join(TEST_TMPDIR, 'nginx.conf')
446     with open(conffile, 'w') as f:
447         f.write(re.sub(
448             r'{{([A-Z]+)}}',
449             lambda match: str(nginxconf.get(match.group(1))),
450             open(conftemplatefile).read()))
451
452     env = os.environ.copy()
453     env['PATH'] = env['PATH']+':/sbin:/usr/sbin:/usr/local/sbin'
454     nginx = subprocess.Popen(
455         ['nginx',
456          '-g', 'error_log stderr info;',
457          '-g', 'pid '+_pidfile('nginx')+';',
458          '-c', conffile],
459         env=env, stdin=open('/dev/null'), stdout=sys.stderr)
460     _setport('keepproxy-ssl', nginxconf['KEEPPROXYSSLPORT'])
461     _setport('arv-git-httpd-ssl', nginxconf['GITSSLPORT'])
462
463 def stop_nginx():
464     if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
465         return
466     kill_server_pid(_pidfile('nginx'), wait=0)
467
468 def _pidfile(program):
469     return os.path.join(TEST_TMPDIR, program + '.pid')
470
471 def _portfile(program):
472     return os.path.join(TEST_TMPDIR, program + '.port')
473
474 def _setport(program, port):
475     with open(_portfile(program), 'w') as f:
476         f.write(str(port))
477
478 # Returns 9 if program is not up.
479 def _getport(program):
480     try:
481         return int(open(_portfile(program)).read())
482     except IOError:
483         return 9
484
485 def _apiconfig(key):
486     if _cached_config:
487         return _cached_config[key]
488     def _load(f, required=True):
489         fullpath = os.path.join(SERVICES_SRC_DIR, 'api', 'config', f)
490         if not required and not os.path.exists(fullpath):
491             return {}
492         return yaml.load(fullpath)
493     cdefault = _load('application.default.yml')
494     csite = _load('application.yml', required=False)
495     _cached_config = {}
496     for section in [cdefault.get('common',{}), cdefault.get('test',{}),
497                     csite.get('common',{}), csite.get('test',{})]:
498         _cached_config.update(section)
499     return _cached_config[key]
500
501 def fixture(fix):
502     '''load a fixture yaml file'''
503     with open(os.path.join(SERVICES_SRC_DIR, 'api', "test", "fixtures",
504                            fix + ".yml")) as f:
505         yaml_file = f.read()
506         try:
507           trim_index = yaml_file.index("# Test Helper trims the rest of the file")
508           yaml_file = yaml_file[0:trim_index]
509         except ValueError:
510           pass
511         return yaml.load(yaml_file)
512
513 def auth_token(token_name):
514     return fixture("api_client_authorizations")[token_name]["api_token"]
515
516 def authorize_with(token_name):
517     '''token_name is the symbolic name of the token from the api_client_authorizations fixture'''
518     arvados.config.settings()["ARVADOS_API_TOKEN"] = auth_token(token_name)
519     arvados.config.settings()["ARVADOS_API_HOST"] = os.environ.get("ARVADOS_API_HOST")
520     arvados.config.settings()["ARVADOS_API_HOST_INSECURE"] = "true"
521
522 class TestCaseWithServers(unittest.TestCase):
523     """TestCase to start and stop supporting Arvados servers.
524
525     Define any of MAIN_SERVER, KEEP_SERVER, and/or KEEP_PROXY_SERVER
526     class variables as a dictionary of keyword arguments.  If you do,
527     setUpClass will start the corresponding servers by passing these
528     keyword arguments to the run, run_keep, and/or run_keep_server
529     functions, respectively.  It will also set Arvados environment
530     variables to point to these servers appropriately.  If you don't
531     run a Keep or Keep proxy server, setUpClass will set up a
532     temporary directory for Keep local storage, and set it as
533     KEEP_LOCAL_STORE.
534
535     tearDownClass will stop any servers started, and restore the
536     original environment.
537     """
538     MAIN_SERVER = None
539     KEEP_SERVER = None
540     KEEP_PROXY_SERVER = None
541
542     @staticmethod
543     def _restore_dict(src, dest):
544         for key in dest.keys():
545             if key not in src:
546                 del dest[key]
547         dest.update(src)
548
549     @classmethod
550     def setUpClass(cls):
551         cls._orig_environ = os.environ.copy()
552         cls._orig_config = arvados.config.settings().copy()
553         cls._cleanup_funcs = []
554         os.environ.pop('ARVADOS_KEEP_PROXY', None)
555         os.environ.pop('ARVADOS_EXTERNAL_CLIENT', None)
556         for server_kwargs, start_func, stop_func in (
557                 (cls.MAIN_SERVER, run, reset),
558                 (cls.KEEP_SERVER, run_keep, stop_keep),
559                 (cls.KEEP_PROXY_SERVER, run_keep_proxy, stop_keep_proxy)):
560             if server_kwargs is not None:
561                 start_func(**server_kwargs)
562                 cls._cleanup_funcs.append(stop_func)
563         if (cls.KEEP_SERVER is None) and (cls.KEEP_PROXY_SERVER is None):
564             cls.local_store = tempfile.mkdtemp()
565             os.environ['KEEP_LOCAL_STORE'] = cls.local_store
566             cls._cleanup_funcs.append(
567                 lambda: shutil.rmtree(cls.local_store, ignore_errors=True))
568         else:
569             os.environ.pop('KEEP_LOCAL_STORE', None)
570         arvados.config.initialize()
571
572     @classmethod
573     def tearDownClass(cls):
574         for clean_func in cls._cleanup_funcs:
575             clean_func()
576         cls._restore_dict(cls._orig_environ, os.environ)
577         cls._restore_dict(cls._orig_config, arvados.config.settings())
578
579
580 if __name__ == "__main__":
581     actions = [
582         'start', 'stop',
583         'start_keep', 'stop_keep',
584         'start_keep_proxy', 'stop_keep_proxy',
585         'start_arv-git-httpd', 'stop_arv-git-httpd',
586         'start_nginx', 'stop_nginx',
587     ]
588     parser = argparse.ArgumentParser()
589     parser.add_argument('action', type=str, help="one of {}".format(actions))
590     parser.add_argument('--auth', type=str, metavar='FIXTURE_NAME', help='Print authorization info for given api_client_authorizations fixture')
591     args = parser.parse_args()
592
593     if args.action not in actions:
594         print("Unrecognized action '{}'. Actions are: {}.".format(args.action, actions), file=sys.stderr)
595         sys.exit(1)
596     if args.action == 'start':
597         stop(force=('ARVADOS_TEST_API_HOST' not in os.environ))
598         run(leave_running_atexit=True)
599         host = os.environ['ARVADOS_API_HOST']
600         if args.auth is not None:
601             token = auth_token(args.auth)
602             print("export ARVADOS_API_TOKEN={}".format(pipes.quote(token)))
603             print("export ARVADOS_API_HOST={}".format(pipes.quote(host)))
604             print("export ARVADOS_API_HOST_INSECURE=true")
605         else:
606             print(host)
607     elif args.action == 'stop':
608         stop(force=('ARVADOS_TEST_API_HOST' not in os.environ))
609     elif args.action == 'start_keep':
610         run_keep()
611     elif args.action == 'stop_keep':
612         stop_keep()
613     elif args.action == 'start_keep_proxy':
614         run_keep_proxy()
615     elif args.action == 'stop_keep_proxy':
616         stop_keep_proxy()
617     elif args.action == 'start_arv-git-httpd':
618         run_arv_git_httpd()
619     elif args.action == 'stop_arv-git-httpd':
620         stop_arv_git_httpd()
621     elif args.action == 'start_nginx':
622         run_nginx()
623     elif args.action == 'stop_nginx':
624         stop_nginx()
625     else:
626         raise Exception("action recognized but not implemented!?")