소스 검색

- Added the ability to start, stop, and restart process groups to
supervisorctl. To start a group, use "start groupname:*". To
start multiple groups, use "start groupname1:* groupname2:*".
Equivalent commands work for "stop" and "restart". You can mix and
match short processnames, fullly-specified group:process names,
and groupsplats on the same line for any of these commands.

Chris McDonough 18 년 전
부모
커밋
2822c1a1c9

+ 7 - 0
CHANGES.txt

@@ -136,6 +136,13 @@ Next Release
     used to parse XML-RPC request bodies.  cElementTree was added as
     an "extras_require" option in setup.py.
 
+  - Added the ability to start, stop, and restart process groups to
+    supervisorctl.  To start a group, use "start groupname:*".  To
+    start multiple groups, use "start groupname1:* groupname2:*".
+    Equivalent commands work for "stop" and "restart". You can mix and
+    match short processnames, fullly-specified group:process names,
+    and groupsplats on the same line for any of these commands.
+
 3.0a2
 
   - Fixed the README.txt example for defining the supervisor RPC

+ 2 - 0
src/supervisor/options.py

@@ -1581,6 +1581,8 @@ def split_namespec(namespec):
     if len(names) == 2:
         # group and and process name differ
         group_name, process_name = names
+        if not process_name or process_name == '*':
+            process_name = None
     else:
         # group name is same as process name
         group_name, process_name = namespec, namespec

+ 11 - 1
src/supervisor/rpcinterface.py

@@ -177,6 +177,9 @@ class SupervisorNamespaceRPCInterface:
         if group is None:
             raise RPCError(Faults.BAD_NAME, name)
 
+        if process_name is None:
+            return group, None
+
         process = group.processes.get(process_name)
         if process is None:
             raise RPCError(Faults.BAD_NAME, name)
@@ -186,13 +189,16 @@ class SupervisorNamespaceRPCInterface:
     def startProcess(self, name, wait=True):
         """ Start a process
 
-        @param string name Process name (or 'group:name')
+        @param string name Process name (or 'group:name', or 'group:*')
         @param boolean wait Wait for process to be fully started
         @return boolean result     Always true unless error
 
         """
         self._update('startProcess')
         group, process = self._getGroupAndProcess(name)
+        if process is None:
+            group_name, process_name = split_namespec(name)
+            return self.startProcessGroup(group_name, wait)
 
         # test filespec, don't bother trying to spawn if we know it will
         # eventually fail
@@ -298,6 +304,10 @@ class SupervisorNamespaceRPCInterface:
 
         group, process = self._getGroupAndProcess(name)
 
+        if process is None:
+            group_name, process_name = split_namespec(name)
+            return self.stopProcessGroup(group_name)
+
         stopped = []
         called  = []
 

+ 126 - 147
src/supervisor/supervisorctl.py

@@ -43,8 +43,10 @@ import errno
 import urlparse
 
 from supervisor.options import ClientOptions
+from supervisor.options import split_namespec
 from supervisor import xmlrpc
 
+
 class Controller(cmd.Cmd):
 
     def __init__(self, options, completekey='tab', stdin=None, stdout=None):
@@ -139,15 +141,15 @@ class Controller(cmd.Cmd):
         return True
 
     def help_help(self):
-        self._output("help\t\tPrint a list of available actions.")
-        self._output("help <action>\tPrint help for <action>.")
+        self._output("help\t\tPrint a list of available actions")
+        self._output("help <action>\tPrint help for <action>")
 
     def do_EOF(self, arg):
         self._output('')
         return 1
 
     def help_EOF(self):
-        self._output("To quit, type ^D or use the quit command.")
+        self._output("To quit, type ^D or use the quit command")
 
     def _tailf(self, path):
         if not self._upcheck():
@@ -196,10 +198,10 @@ class Controller(cmd.Cmd):
             modifier = args.pop(0)
 
         if len(args) == 1:
-            processname = args[-1]
+            name = args[-1]
             channel = 'stdout'
         else:
-            processname = args[0]
+            name = args[0]
             channel = args[-1].lower()
             if channel not in ('stderr', 'stdout'):
                 self._output('Error: bad channel %r' % channel)
@@ -221,37 +223,37 @@ class Controller(cmd.Cmd):
         supervisor = self._get_supervisor()
 
         if bytes is None:
-            return self._tailf('/logtail/%s/%s' % (processname, channel))
+            return self._tailf('/logtail/%s/%s' % (name, channel))
 
         else:
             try:
                 if channel is 'stdout':
-                    output = supervisor.readProcessStdoutLog(processname,
+                    output = supervisor.readProcessStdoutLog(name,
                                                              -bytes, 0)
                 else: # if channel is 'stderr'
-                    output = supervisor.readProcessStderrLog(processname,
+                    output = supervisor.readProcessStderrLog(name,
                                                              -bytes, 0)
             except xmlrpclib.Fault, e:
                 template = '%s: ERROR (%s)'
                 if e.faultCode == xmlrpc.Faults.NO_FILE:
-                    self._output(template % (processname, 'no log file'))
+                    self._output(template % (name, 'no log file'))
                 elif e.faultCode == xmlrpc.Faults.FAILED:
-                    self._output(template % (processname,
+                    self._output(template % (name,
                                              'unknown error reading log'))
                 elif e.faultCode == xmlrpc.Faults.BAD_NAME:
-                    self._output(template % (processname,
+                    self._output(template % (name,
                                              'no such process name'))
             else:
                 self._output(output)
 
     def help_tail(self):
         self._output(
-            "tail [-f] <processname> [stdout|stderr] (default stdout)\n"
+            "tail [-f] <name> [stdout|stderr] (default stdout)\n"
             "Ex:\n"
-            "tail -f <processname>\tContinuous tail of named process stdout\n"
+            "tail -f <name>\t\tContinuous tail of named process stdout\n"
             "\t\t\tCtrl-C to exit.\n"
-            "tail -100 <processname>\tlast 100 *bytes* of process stdout\n"
-            "tail <processname> stderr\tlast 1600 *bytes* of process stderr\n"
+            "tail -100 <name>\tlast 100 *bytes* of process stdout\n"
+            "tail <name> stderr\tlast 1600 *bytes* of process stderr"
             )
 
     def do_maintail(self, arg):
@@ -300,8 +302,8 @@ class Controller(cmd.Cmd):
 
     def help_maintail(self):
         self._output(
-            "maintail -f \tContinuous tail of supervisor main log file,\n"
-            "\t\t\tCtrl-C to exit.\n"
+            "maintail -f \tContinuous tail of supervisor main log file"
+            " (Ctrl-C to exit)\n"
             "maintail -100\tlast 100 *bytes* of supervisord main log file\n"
             "maintail\tlast 1600 *bytes* of supervisor main log file\n"
             )
@@ -333,15 +335,15 @@ class Controller(cmd.Cmd):
         
         supervisor = self._get_supervisor()
 
-        processnames = arg.strip().split()
+        names = arg.strip().split()
 
-        if processnames:
-            for processname in processnames:
+        if names:
+            for name in names:
                 try:
-                    info = supervisor.getProcessInfo(processname)
+                    info = supervisor.getProcessInfo(name)
                 except xmlrpclib.Fault, e:
                     if e.faultCode == xmlrpc.Faults.BAD_NAME:
-                        self._output('No such process %s' % processname)
+                        self._output('No such process %s' % name)
                     else:
                         raise
                     continue
@@ -356,143 +358,132 @@ class Controller(cmd.Cmd):
         self._output("status <name> <name>\tGet status on multiple named "
                      "processes.")
 
-    def _startresult(self, code, processname, default=None):
+    def _startresult(self, result):
+        name = result['name']
+        code = result['status']
         template = '%s: ERROR (%s)'
         if code == xmlrpc.Faults.BAD_NAME:
-            return template % (processname,'no such process')
+            return template % (name,'no such process')
         elif code == xmlrpc.Faults.ALREADY_STARTED:
-            return template % (processname,'already started')
+            return template % (name,'already started')
         elif code == xmlrpc.Faults.SPAWN_ERROR:
-            return template % (processname, 'spawn error')
+            return template % (name, 'spawn error')
         elif code == xmlrpc.Faults.ABNORMAL_TERMINATION:
-            return template % (processname, 'abnormal termination')
+            return template % (name, 'abnormal termination')
         elif code == xmlrpc.Faults.SUCCESS:
-            return '%s: started' % processname
-        
-        return default
+            return '%s: started' % name
+        # assertion
+        raise ValueError('Unknown result code %s for %s' % (code, name))
 
     def do_start(self, arg):
         if not self._upcheck():
             return
 
-        processnames = arg.strip().split()
+        names = arg.strip().split()
         supervisor = self._get_supervisor()
 
-        if not processnames:
+        if not names:
             self._output("Error: start requires a process name")
             self.help_start()
             return
 
-        if 'all' in processnames:
+        if 'all' in names:
             results = supervisor.startAllProcesses()
             for result in results:
-                name = result['name']
-                code = result['status']
-                result = self._startresult(code, name)
-                if result is None:
-                    # assertion
-                    raise ValueError('Unknown result code %s for %s' %
-                                     (code, name))
-                else:
-                    self._output(result)
+                result = self._startresult(result)
+                self._output(result)
                 
         else:
-            for processname in processnames:
-                try:
-                    result = supervisor.startProcess(processname)
-                except xmlrpclib.Fault, e:
-                    error = self._startresult(e.faultCode, processname)
-                    if error is not None:
-                        self._output(error)
-                    else:
-                        raise
+            for name in names:
+                group_name, process_name = split_namespec(name)
+                if process_name is None:
+                    results = supervisor.startProcessGroup(group_name)
+                    for result in results:
+                        result = self._startresult(result)
+                        self._output(result)
                 else:
-                    if result == True:
-                        self._output('%s: started' % processname)
+                    try:
+                        result = supervisor.startProcess(name)
+                    except xmlrpclib.Fault, e:
+                        error = self._startresult({'status':e.faultCode,
+                                                   'name':name,
+                                                   'description':e.faultString})
+                        self._output(error)
                     else:
-                        raise # assertion
+                        self._output('%s: started' % name)
 
     def help_start(self):
-        self._output("start <processname>\t\t\tStart a process.")
-        self._output("start <processname> <processname>\tStart multiple "
-                     "processes")
-        self._output("start all\t\t\t\tStart all processes")
-        self._output("  When all processes are started, they are started "
-                     "in")
-        self._output("  priority order (see config file)")
-
-    def _stopresult(self, code, processname, fault_string=None):
+        self._output("start <name>\t\tStart a process")
+        self._output("start <gname>:*\t\tStart all processes in a group")
+        self._output("start <name> <name>\tStart multiple processes or groups")
+        self._output("start all\t\tStart all processes")
+
+    def _stopresult(self, result):
+        name = result['name']
+        code = result['status']
+        fault_string = result['description']
         template = '%s: ERROR (%s)'
         if code == xmlrpc.Faults.BAD_NAME:
-            return template % (processname, 'no such process')
+            return template % (name, 'no such process')
         elif code == xmlrpc.Faults.NOT_RUNNING:
-            return template % (processname, 'not running')
+            return template % (name, 'not running')
         elif code == xmlrpc.Faults.SUCCESS:
-            return '%s: stopped' % processname
+            return '%s: stopped' % name
         elif code == xmlrpc.Faults.FAILED:
             return fault_string
-        return None
+        # assertion
+        raise ValueError('Unknown result code %s for %s' % (code, name))
 
     def do_stop(self, arg):
         if not self._upcheck():
             return
 
-        processnames = arg.strip().split()
+        names = arg.strip().split()
         supervisor = self._get_supervisor()
 
-        if not processnames:
+        if not names:
             self._output('Error: stop requires a process name')
             self.help_stop()
             return
 
-        if 'all' in processnames:
+        if 'all' in names:
             results = supervisor.stopAllProcesses()
             for result in results:
-                name = result['name']
-                code = result['status']
-                fault_string = result['description']
-                result = self._stopresult(code, name, fault_string)
-                if result is None:
-                    # assertion
-                    raise ValueError('Unknown result code %s for %s' %
-                                     (code, name))
-                else:
-                    self._output(result)
+                result = self._stopresult(result)
+                self._output(result)
 
         else:
-
-            for processname in processnames:
-                try:
-                    result = supervisor.stopProcess(processname)
-                except xmlrpclib.Fault, e:
-                    error = self._stopresult(e.faultCode, processname,
-                                             e.faultString)
-                    if error is not None:
-                        self._output(error)
-                    else:
-                        raise
+            for name in names:
+                group_name, process_name = split_namespec(name)
+                if process_name is None:
+                    results = supervisor.stopProcessGroup(group_name)
+                    for result in results:
+                        result = self._stopresult(result)
+                        self._output(result)
                 else:
-                    if result == True:
-                        self._output('%s: stopped' % processname)
+                    try:
+                        result = supervisor.stopProcess(name)
+                    except xmlrpclib.Fault, e:
+                        error = self._stopresult({'status':e.faultCode,
+                                                  'name':name,
+                                                  'description':e.faultString})
+                        self._output(error)
                     else:
-                        raise # assertion
+                        self._output('%s: stopped' % name)
 
     def help_stop(self):
-        self._output("stop <processname>\t\t\tStop a process.")
-        self._output("stop <processname> <processname>\tStop multiple "
-                     "processes")
-        self._output("stop all\t\t\t\tStop all processes")
-        self._output("  When all processes are stopped, they are stopped "
-                     "in")
-        self._output("  reverse priority order (see config file)")
+        self._output("stop <name>\t\tStop a process")
+        self._output("stop <gname>:*\t\tStop all processes in a group")
+        self._output("stop <name> <name>\tStop multiple processes or groups")
+        self._output("stop all\t\tStop all processes")
 
     def do_restart(self, arg):
         if not self._upcheck():
             return
 
-        processnames = arg.strip().split()
+        names = arg.strip().split()
 
-        if not processnames:
+        if not names:
             self._output('Error: restart requires a process name')
             self.help_restart()
             return
@@ -501,13 +492,11 @@ class Controller(cmd.Cmd):
         self.do_start(arg)
 
     def help_restart(self):
-        self._output("restart <processname>\t\t\tRestart a process.")
-        self._output("restart <processname> <processname>\tRestart multiple "
-                     "processes")
-        self._output("restart all\t\t\t\tRestart all processes")
-        self._output("  When all processes are restarted, they are "
-                     "started in")
-        self._output("  priority order (see config file)")
+        self._output("restart <name>\t\tRestart a process")
+        self._output("restart <gname>:*\tRestart all processes in a group")
+        self._output("restart <name> <name>\tRestart multiple processes or "
+                     "groups")
+        self._output("restart all\t\tRestart all processes")
 
     def do_shutdown(self, arg):
         if self.options.interactive:
@@ -527,7 +516,7 @@ class Controller(cmd.Cmd):
                 self._output('Shut down')
 
     def help_shutdown(self):
-        self._output("shutdown \t\tShut the remote supervisord down.")
+        self._output("shutdown \tShut the remote supervisord down.")
 
     def do_reload(self, arg):
         if self.options.interactive:
@@ -549,64 +538,54 @@ class Controller(cmd.Cmd):
     def help_reload(self):
         self._output("reload \t\tRestart the remote supervisord.")
 
-    def _clearresult(self, code, processname, default=None):
+    def _clearresult(self, result):
+        name = result['name']
+        code = result['status']
         template = '%s: ERROR (%s)'
         if code == xmlrpc.Faults.BAD_NAME:
-            return template % (processname, 'no such process')
+            return template % (name, 'no such process')
         elif code == xmlrpc.Faults.FAILED:
-            return template % (processname, 'failed')
+            return template % (name, 'failed')
         elif code == xmlrpc.Faults.SUCCESS:
-            return '%s: cleared' % processname
-        return default
+            return '%s: cleared' % name
+        raise ValueError('Unknown result code %s for %s' % (code, name))
 
     def do_clear(self, arg):
         if not self._upcheck():
             return
 
-        processnames = arg.strip().split()
+        names = arg.strip().split()
 
-        if not processnames:
+        if not names:
             self._output('Error: clear requires a process name')
             self.help_clear()
             return
 
         supervisor = self._get_supervisor()
 
-        if 'all' in processnames:
+        if 'all' in names:
             results = supervisor.clearAllProcessLogs()
             for result in results:
-                name = result['name']
-                code = result['status']
-                result = self._clearresult(code, name)
-                if result is None:
-                    # assertion
-                    raise ValueError('Unknown result code %s for %s' %
-                                     (code, name))
-                else:
-                    self._output(result)
+                result = self._clearresult(result)
+                self._output(result)
 
         else:
 
-            for processname in processnames:
+            for name in names:
                 try:
-                    result = supervisor.clearProcessLogs(processname)
+                    result = supervisor.clearProcessLogs(name)
                 except xmlrpclib.Fault, e:
-                    error = self._clearresult(e.faultCode, processname)
-                    if error is not None:
-                        self._output(error)
-                    else:
-                        raise
+                    error = self._clearresult({'status':e.faultCode,
+                                               'name':name,
+                                               'description':e.faultString})
+                    self._output(error)
                 else:
-                    if result == True:
-                        self._output('%s: cleared' % processname)
-                    else:
-                        raise # assertion
+                    self._output('%s: cleared' % name)
 
     def help_clear(self):
-        self._output("clear <processname>\t\t\tClear a process' log files.")
-        self._output("clear <processname> <processname>\tclear multiple "
-                     "process' log files")
-        self._output("clear all\t\t\t\tClear all process' log files")
+        self._output("clear <name>\t\tClear a process' log files.")
+        self._output("clear <name> <name>\tClear multiple process' log files")
+        self._output("clear all\t\tClear all process' log files")
 
     def do_open(self, arg):
         url = arg.strip()
@@ -618,8 +597,8 @@ class Controller(cmd.Cmd):
         self.do_status('')
 
     def help_open(self):
-        self._output("open <url>\t\t\tConnect to a remote supervisord process.")
-        self._output("\t\t\t(for UNIX domain socket, use unix:///socket/path)")
+        self._output("open <url>\tConnect to a remote supervisord process.")
+        self._output("\t\t(for UNIX domain socket, use unix:///socket/path)")
 
     def do_version(self, arg):
         if not self._upcheck():
@@ -628,8 +607,8 @@ class Controller(cmd.Cmd):
         self._output(supervisor.getSupervisorVersion())
 
     def help_version(self):
-        self._output("version\t\t\tShow the version of the remote supervisord ")
-        self._output("\t\t\tprocess")
+        self._output("version\t\t\tShow the version of the remote supervisord "
+                     "process")
 
 def main(args=None, options=None):
     if options is None:

+ 22 - 0
src/supervisor/tests/base.py

@@ -647,6 +647,17 @@ class DummySupervisorRPCNamespace:
             raise Fault(xmlrpc.Faults.SPAWN_ERROR, 'SPAWN_ERROR')
         return True
 
+    def startProcessGroup(self, name):
+        from supervisor import xmlrpc
+        return [
+            {'name':'foo_00', 'group':'foo',
+             'status': xmlrpc.Faults.SUCCESS,
+             'description': 'OK'},
+            {'name':'foo_01', 'group':'foo',
+             'status':xmlrpc.Faults.SUCCESS,
+             'description': 'OK'},
+            ]
+
     def startAllProcesses(self):
         from supervisor import xmlrpc
         return [
@@ -661,6 +672,17 @@ class DummySupervisorRPCNamespace:
              'description':'SPAWN_ERROR'}
             ]
 
+    def stopProcessGroup(self, name):
+        from supervisor import xmlrpc
+        return [
+            {'name':'foo_00', 'group':'foo',
+             'status': xmlrpc.Faults.SUCCESS,
+             'description': 'OK'},
+            {'name':'foo_01', 'group':'foo',
+             'status':xmlrpc.Faults.SUCCESS,
+             'description': 'OK'},
+            ]
+
     def stopProcess(self, name):
         from supervisor import xmlrpc
         from xmlrpclib import Fault

+ 15 - 1
src/supervisor/tests/test_options.py

@@ -777,7 +777,21 @@ class ProcessGroupConfigTests(unittest.TestCase):
         group = instance.make_group()
         from supervisor.process import ProcessGroup
         self.assertEqual(group.__class__, ProcessGroup)
-            
+
+class UtilFunctionsTests(unittest.TestCase):
+    def test_make_namespec(self):
+        from supervisor.options import make_namespec
+        self.assertEquals(make_namespec('group', 'process'), 'group:process')
+        self.assertEquals(make_namespec('process', 'process'), 'process')
+        
+    def test_split_namespec(self):
+        from supervisor.options import split_namespec
+        s = split_namespec
+        self.assertEquals(s('process:group'), ('process', 'group'))
+        self.assertEquals(s('process'), ('process', 'process'))
+        self.assertEquals(s('group:'), ('group', None))
+        self.assertEquals(s('group:*'), ('group', None))
+
 def test_suite():
     return unittest.findTestCases(sys.modules[__name__])
 

+ 32 - 0
src/supervisor/tests/test_rpcinterfaces.py

@@ -368,6 +368,22 @@ class SupervisorNamespaceXMLRPCInterfaceTests(TestBase):
         from supervisor import xmlrpc
         self._assertRPCError(xmlrpc.Faults.ABNORMAL_TERMINATION, callback)
 
+    def test_startProcess_splat_calls_startProcessGroup(self):
+        from supervisor import http
+        options = DummyOptions()
+        pconfig1 = DummyPConfig(options, 'process1', __file__, autostart=False,
+                               startsecs=.01)
+        pconfig2 = DummyPConfig(options, 'process2', __file__, priority=2,
+                                startsecs=.01)
+        supervisord = PopulatedDummySupervisor(options, 'foo',
+                                               pconfig1, pconfig2)
+        from supervisor.process import ProcessStates
+        supervisord.set_procattr('process1', 'state', ProcessStates.STOPPED)
+        supervisord.set_procattr('process2', 'state', ProcessStates.STOPPED)
+        interface = self._makeOne(supervisord)
+        callback = interface.startProcess('foo:*')
+        self.assertEqual(interface.update_text, 'startProcessGroup')
+
     def test_startProcessGroup(self):
         options = DummyOptions()
         pconfig1 = DummyPConfig(options, 'process1', __file__, priority=1,
@@ -613,6 +629,22 @@ class SupervisorNamespaceXMLRPCInterfaceTests(TestBase):
         self._assertRPCError(xmlrpc.Faults.BAD_NAME,
                              interface.stopProcessGroup, 'foo')
 
+    def test_stopProcess_splat_calls_stopProcessGroup(self):
+        from supervisor import http
+        options = DummyOptions()
+        pconfig1 = DummyPConfig(options, 'process1', __file__, autostart=False,
+                               startsecs=.01)
+        pconfig2 = DummyPConfig(options, 'process2', __file__, priority=2,
+                                startsecs=.01)
+        supervisord = PopulatedDummySupervisor(options, 'foo',
+                                               pconfig1, pconfig2)
+        from supervisor.process import ProcessStates
+        supervisord.set_procattr('process1', 'state', ProcessStates.STOPPED)
+        supervisord.set_procattr('process2', 'state', ProcessStates.STOPPED)
+        interface = self._makeOne(supervisord)
+        callback = interface.stopProcess('foo:*')
+        self.assertEqual(interface.update_text, 'stopProcessGroup')
+
     def test_stopAllProcesses(self):
         options = DummyOptions()
         pconfig1 = DummyPConfig(options, 'process1', '/bin/foo')

+ 18 - 0
src/supervisor/tests/test_supervisorctl.py

@@ -234,6 +234,15 @@ class ControllerTests(unittest.TestCase):
         self.assertEqual(controller.stdout.getvalue(),
                          'foo: started\nbar: started\n')
 
+    def test_start_group(self):
+        options = DummyClientOptions()
+        controller = self._makeOne(options)
+        controller.stdout = StringIO()
+        result = controller.do_start('foo:')
+        self.assertEqual(result, None)
+        self.assertEqual(controller.stdout.getvalue(),
+                         'foo_00: started\nfoo_01: started\n')
+
     def test_start_all(self):
         options = DummyClientOptions()
         controller = self._makeOne(options)
@@ -297,6 +306,15 @@ class ControllerTests(unittest.TestCase):
         self.assertEqual(controller.stdout.getvalue(),
                          'foo: stopped\nbar: stopped\n')
 
+    def test_stop_group(self):
+        options = DummyClientOptions()
+        controller = self._makeOne(options)
+        controller.stdout = StringIO()
+        result = controller.do_stop('foo:')
+        self.assertEqual(result, None)
+        self.assertEqual(controller.stdout.getvalue(),
+                         'foo_00: stopped\nfoo_01: stopped\n')
+
     def test_stop_all(self):
         options = DummyClientOptions()
         controller = self._makeOne(options)