Merge "Factor out common code between cli utilities"
diff --git a/tests/test_stack_dump.py b/tests/test_stack_dump.py
index cc8cf8f..824e04c 100644
--- a/tests/test_stack_dump.py
+++ b/tests/test_stack_dump.py
@@ -17,7 +17,7 @@
 import signal
 import testtools
 
-import zuul.cmd.server
+import zuul.cmd
 
 
 class TestStackDump(testtools.TestCase):
@@ -29,6 +29,6 @@
     def test_stack_dump_logs(self):
         "Test that stack dumps end up in logs."
 
-        zuul.cmd.server.stack_dump_handler(signal.SIGUSR2, None)
+        zuul.cmd.stack_dump_handler(signal.SIGUSR2, None)
         self.assertIn("Thread", self.log_fixture.output)
         self.assertIn("test_stack_dump_logs", self.log_fixture.output)
diff --git a/zuul/cmd/__init__.py b/zuul/cmd/__init__.py
index e69de29..e17ad5b 100644
--- a/zuul/cmd/__init__.py
+++ b/zuul/cmd/__init__.py
@@ -0,0 +1,73 @@
+#!/usr/bin/env python
+# Copyright 2012 Hewlett-Packard Development Company, L.P.
+# Copyright 2013 OpenStack Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import ConfigParser
+import logging
+import logging.config
+import os
+import signal
+import sys
+import traceback
+
+# No zuul imports here because they pull in paramiko which must not be
+# imported until after the daemonization.
+# https://github.com/paramiko/paramiko/issues/59
+# Similar situation with gear and statsd.
+
+
+def stack_dump_handler(signum, frame):
+    signal.signal(signal.SIGUSR2, signal.SIG_IGN)
+    log_str = ""
+    for thread_id, stack_frame in sys._current_frames().items():
+        log_str += "Thread: %s\n" % thread_id
+        log_str += "".join(traceback.format_stack(stack_frame))
+    log = logging.getLogger("zuul.stack_dump")
+    log.debug(log_str)
+    signal.signal(signal.SIGUSR2, stack_dump_handler)
+
+
+class ZuulApp(object):
+
+    def __init__(self):
+        self.args = None
+        self.config = None
+
+    def _get_version(self):
+        from zuul.version import version_info as zuul_version_info
+        return "Zuul version: %s" % zuul_version_info.version_string()
+
+    def read_config(self):
+        self.config = ConfigParser.ConfigParser()
+        if self.args.config:
+            locations = [self.args.config]
+        else:
+            locations = ['/etc/zuul/zuul.conf',
+                         '~/zuul.conf']
+        for fp in locations:
+            if os.path.exists(os.path.expanduser(fp)):
+                self.config.read(os.path.expanduser(fp))
+                return
+        raise Exception("Unable to locate config file in %s" % locations)
+
+    def setup_logging(self, section, parameter):
+        if self.config.has_option(section, parameter):
+            fp = os.path.expanduser(self.config.get(section, parameter))
+            if not os.path.exists(fp):
+                raise Exception("Unable to read logging config file at %s" %
+                                fp)
+            logging.config.fileConfig(fp)
+        else:
+            logging.basicConfig(level=logging.DEBUG)
diff --git a/zuul/cmd/client.py b/zuul/cmd/client.py
index 147fade..766a4ef 100644
--- a/zuul/cmd/client.py
+++ b/zuul/cmd/client.py
@@ -16,26 +16,20 @@
 
 import argparse
 import babel.dates
-import ConfigParser
 import datetime
 import logging
-import logging.config
-import os
 import prettytable
 import sys
 import time
 
+
 import zuul.rpcclient
+import zuul.cmd
 
 
-class Client(object):
+class Client(zuul.cmd.ZuulApp):
     log = logging.getLogger("zuul.Client")
 
-    def __init__(self):
-        self.args = None
-        self.config = None
-        self.gear_server_pid = None
-
     def parse_arguments(self):
         parser = argparse.ArgumentParser(
             description='Zuul Project Gating System Client.')
@@ -89,24 +83,8 @@
 
         self.args = parser.parse_args()
 
-    def _get_version(self):
-        from zuul.version import version_info as zuul_version_info
-        return "Zuul version: %s" % zuul_version_info.version_string()
-
-    def read_config(self):
-        self.config = ConfigParser.ConfigParser()
-        if self.args.config:
-            locations = [self.args.config]
-        else:
-            locations = ['/etc/zuul/zuul.conf',
-                         '~/zuul.conf']
-        for fp in locations:
-            if os.path.exists(os.path.expanduser(fp)):
-                self.config.read(os.path.expanduser(fp))
-                return
-        raise Exception("Unable to locate config file in %s" % locations)
-
     def setup_logging(self):
+        """Client logging does not rely on conf file"""
         if self.args.verbose:
             logging.basicConfig(level=logging.DEBUG)
 
diff --git a/zuul/cmd/merger.py b/zuul/cmd/merger.py
index edf8da9..dc3484a 100644
--- a/zuul/cmd/merger.py
+++ b/zuul/cmd/merger.py
@@ -15,7 +15,6 @@
 # under the License.
 
 import argparse
-import ConfigParser
 import daemon
 import extras
 
@@ -23,12 +22,11 @@
 # instead it depends on lockfile-0.9.1 which uses pidfile.
 pid_file_module = extras.try_imports(['daemon.pidlockfile', 'daemon.pidfile'])
 
-import logging
-import logging.config
 import os
 import sys
 import signal
-import traceback
+
+import zuul.cmd
 
 # No zuul imports here because they pull in paramiko which must not be
 # imported until after the daemonization.
@@ -36,21 +34,7 @@
 # Similar situation with gear and statsd.
 
 
-def stack_dump_handler(signum, frame):
-    signal.signal(signal.SIGUSR2, signal.SIG_IGN)
-    log_str = ""
-    for thread_id, stack_frame in sys._current_frames().items():
-        log_str += "Thread: %s\n" % thread_id
-        log_str += "".join(traceback.format_stack(stack_frame))
-    log = logging.getLogger("zuul.stack_dump")
-    log.debug(log_str)
-    signal.signal(signal.SIGUSR2, stack_dump_handler)
-
-
-class Merger(object):
-    def __init__(self):
-        self.args = None
-        self.config = None
+class Merger(zuul.cmd.ZuulApp):
 
     def parse_arguments(self):
         parser = argparse.ArgumentParser(description='Zuul merge worker.')
@@ -58,33 +42,11 @@
                             help='specify the config file')
         parser.add_argument('-d', dest='nodaemon', action='store_true',
                             help='do not run as a daemon')
-        parser.add_argument('--version', dest='version', action='store_true',
+        parser.add_argument('--version', dest='version', action='version',
+                            version=self._get_version(),
                             help='show zuul version')
         self.args = parser.parse_args()
 
-    def read_config(self):
-        self.config = ConfigParser.ConfigParser()
-        if self.args.config:
-            locations = [self.args.config]
-        else:
-            locations = ['/etc/zuul/zuul.conf',
-                         '~/zuul.conf']
-        for fp in locations:
-            if os.path.exists(os.path.expanduser(fp)):
-                self.config.read(os.path.expanduser(fp))
-                return
-        raise Exception("Unable to locate config file in %s" % locations)
-
-    def setup_logging(self, section, parameter):
-        if self.config.has_option(section, parameter):
-            fp = os.path.expanduser(self.config.get(section, parameter))
-            if not os.path.exists(fp):
-                raise Exception("Unable to read logging config file at %s" %
-                                fp)
-            logging.config.fileConfig(fp)
-        else:
-            logging.basicConfig(level=logging.DEBUG)
-
     def exit_handler(self, signum, frame):
         signal.signal(signal.SIGUSR1, signal.SIG_IGN)
         self.merger.stop()
@@ -100,7 +62,7 @@
         self.merger.start()
 
         signal.signal(signal.SIGUSR1, self.exit_handler)
-        signal.signal(signal.SIGUSR2, stack_dump_handler)
+        signal.signal(signal.SIGUSR2, zuul.cmd.stack_dump_handler)
         while True:
             try:
                 signal.pause()
@@ -113,11 +75,6 @@
     server = Merger()
     server.parse_arguments()
 
-    if server.args.version:
-        from zuul.version import version_info as zuul_version_info
-        print "Zuul version: %s" % zuul_version_info.version_string()
-        sys.exit(0)
-
     server.read_config()
 
     if server.config.has_option('zuul', 'state_dir'):
diff --git a/zuul/cmd/server.py b/zuul/cmd/server.py
index 8caa1fd..06ea780 100755
--- a/zuul/cmd/server.py
+++ b/zuul/cmd/server.py
@@ -15,7 +15,6 @@
 # under the License.
 
 import argparse
-import ConfigParser
 import daemon
 import extras
 
@@ -24,11 +23,11 @@
 pid_file_module = extras.try_imports(['daemon.pidlockfile', 'daemon.pidfile'])
 
 import logging
-import logging.config
 import os
 import sys
 import signal
-import traceback
+
+import zuul.cmd
 
 # No zuul imports here because they pull in paramiko which must not be
 # imported until after the daemonization.
@@ -36,21 +35,9 @@
 # Similar situation with gear and statsd.
 
 
-def stack_dump_handler(signum, frame):
-    signal.signal(signal.SIGUSR2, signal.SIG_IGN)
-    log_str = ""
-    for thread_id, stack_frame in sys._current_frames().items():
-        log_str += "Thread: %s\n" % thread_id
-        log_str += "".join(traceback.format_stack(stack_frame))
-    log = logging.getLogger("zuul.stack_dump")
-    log.debug(log_str)
-    signal.signal(signal.SIGUSR2, stack_dump_handler)
-
-
-class Server(object):
+class Server(zuul.cmd.ZuulApp):
     def __init__(self):
-        self.args = None
-        self.config = None
+        super(Server, self).__init__()
         self.gear_server_pid = None
 
     def parse_arguments(self):
@@ -71,33 +58,6 @@
                             help='show zuul version')
         self.args = parser.parse_args()
 
-    def _get_version(self):
-        from zuul.version import version_info as zuul_version_info
-        return "Zuul version: %s" % zuul_version_info.version_string()
-
-    def read_config(self):
-        self.config = ConfigParser.ConfigParser()
-        if self.args.config:
-            locations = [self.args.config]
-        else:
-            locations = ['/etc/zuul/zuul.conf',
-                         '~/zuul.conf']
-        for fp in locations:
-            if os.path.exists(os.path.expanduser(fp)):
-                self.config.read(os.path.expanduser(fp))
-                return
-        raise Exception("Unable to locate config file in %s" % locations)
-
-    def setup_logging(self, section, parameter):
-        if self.config.has_option(section, parameter):
-            fp = os.path.expanduser(self.config.get(section, parameter))
-            if not os.path.exists(fp):
-                raise Exception("Unable to read logging config file at %s" %
-                                fp)
-            logging.config.fileConfig(fp)
-        else:
-            logging.basicConfig(level=logging.DEBUG)
-
     def reconfigure_handler(self, signum, frame):
         signal.signal(signal.SIGHUP, signal.SIG_IGN)
         self.read_config()
@@ -235,7 +195,7 @@
 
         signal.signal(signal.SIGHUP, self.reconfigure_handler)
         signal.signal(signal.SIGUSR1, self.exit_handler)
-        signal.signal(signal.SIGUSR2, stack_dump_handler)
+        signal.signal(signal.SIGUSR2, zuul.cmd.stack_dump_handler)
         signal.signal(signal.SIGTERM, self.term_handler)
         while True:
             try: