├── .gitignore ├── HISTORY.rst ├── README.rst ├── mr ├── __init__.py └── laforge │ ├── __init__.py │ ├── controllerplugin.py │ └── rpcinterface.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | -------------------------------------------------------------------------------- /HISTORY.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | 0.9 - 2019-09-15 5 | ---------------- 6 | 7 | * Fix import of controllerplugin for Python 3.x. 8 | [fschulze] 9 | 10 | 11 | 0.8 - 2018-07-10 12 | ---------------- 13 | 14 | * Fix ``do_kill`` for Python 3.x. 15 | [fschulze] 16 | 17 | 18 | 0.7 - 2018-07-02 19 | ---------------- 20 | 21 | * Add Python 3.x support. 22 | [fschulze] 23 | 24 | * Add timeout to socket, so we don't hang on unavailable hosts. 25 | [fschulze] 26 | 27 | 28 | 0.6 - 2012-10-06 29 | ---------------- 30 | 31 | * Added ``supervisordown`` script and matching ``down`` method to make sure a 32 | process is stopped. 33 | [fschulze] 34 | 35 | 36 | 0.5 - 2012-05-23 37 | ---------------- 38 | 39 | * Allow setting supervisor arguments via the ``MR_LAFORGE_SUPERVISOR_ARGS`` 40 | environment variable. 41 | [witsch] 42 | 43 | * Added ``shutdown`` function to shutdown supervisord from Python code. 44 | [fschulze] 45 | 46 | 47 | 0.4 - 2012-05-09 48 | ---------------- 49 | 50 | * Added waitforports script and function to wait until a process is listening 51 | on specified ports. 52 | [fschulze, witsch] 53 | 54 | 55 | 0.3 - 2012-04-03 56 | ---------------- 57 | 58 | * Don't pass command line options to supervisor code. 59 | [fschulze] 60 | 61 | * Add supervisor_args keyword argument to ``up`` function. 62 | [fschulze, witsch (Andreas Zeidler)] 63 | 64 | 65 | 0.2 - 2011-04-20 66 | ---------------- 67 | 68 | * Added supervisord plugin with ``kill`` command to send signals to processes. 69 | [fschulze] 70 | 71 | 72 | 0.1 - 2011-04-20 73 | ---------------- 74 | 75 | * Initial release 76 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Introduction 2 | ============ 3 | 4 | **mr.laforge** is a utility and plugin for `supervisord`_. 5 | 6 | It let's you easily make sure that ``supervisord`` and specific processes 7 | controlled by it are running from within shell and Python scripts. 8 | 9 | The plugin part adds a ``kill`` command to send signals to processes. 10 | 11 | .. _`supervisord`: http://supervisord.org/ 12 | 13 | 14 | Usage 15 | ===== 16 | 17 | Installation as a script 18 | ------------------------ 19 | 20 | One way to use it, is by installing it as a script. That's also the way to use 21 | it in shell scripts. You can either just install it as an egg or you can 22 | install it in a buildout: 23 | 24 | .. code-block:: ini 25 | 26 | [mr.laforge] 27 | recipe = zc.recipe.egg 28 | eggs = mr.laforge 29 | 30 | Either way you will get a ``supervisorup``, a ``supervisordown`` and a 31 | ``waitforports`` script. 32 | 33 | Running ``supervisorup`` without arguments will check whether ``supervisord`` 34 | is running and if not will start it. You can also provide process names on the 35 | command line and those will be started if they are not already running. 36 | 37 | The ``supervisordown`` script does the same as ``supervisorup``, but makes sure 38 | the process is stopped. This is useful for scripts which initialize a 39 | development database and similar tasks. 40 | 41 | With ``waitforports`` you can check whether one or more processes are listening 42 | on the specified ports. The script has some additional arguments you can list 43 | with ``-h`` or ``--help``. 44 | 45 | You can set the ``supervisor_args`` keyword argument to set supervisor arguments 46 | for the ``supervisorup`` script like the config file location: 47 | 48 | .. code-block:: ini 49 | 50 | [mr.laforge] 51 | recipe = zc.recipe.egg 52 | eggs = mr.laforge 53 | arguments = supervisor_args=['-c', 'etc/my_supervisord.conf'] 54 | 55 | Alternatively, supervisor arguments can also be set via the 56 | ``MR_LAFORGE_SUPERVISOR_ARGS`` environment variable. 57 | 58 | 59 | Usage from a Python script 60 | -------------------------- 61 | 62 | You can use the ``up`` method in ``mr.laforge`` which similar to the 63 | ``supervisorup`` script takes process names as arguments. 64 | 65 | One example is a `zc.recipe.testrunner`_ part in a buildout like this: 66 | 67 | .. code-block:: ini 68 | 69 | [test] 70 | recipe = zc.recipe.testrunner 71 | eggs = 72 | ... 73 | mr.laforge 74 | initialization = 75 | import mr.laforge 76 | mr.laforge.up('solr-test') 77 | 78 | As you can see, you have to add the egg, so it can be imported by the 79 | initialization code added to the ``test`` script created by 80 | `zc.recipe.testrunner`_. The ``up`` call gets ``solr-test`` as an argument 81 | to make sure that the ``solr-test`` process is running for the tests. 82 | 83 | Another example is an initialization snippet in a script created by 84 | `zc.recipe.egg`_: 85 | 86 | .. code-block:: ini 87 | 88 | [paster] 89 | recipe = zc.recipe.egg 90 | eggs = 91 | ... 92 | mr.laforge 93 | dependent-scripts = true 94 | scripts = paster 95 | initialization = 96 | import mr.laforge 97 | mr.laforge.up('solr') 98 | 99 | Now everytime you run the ``paster`` script created by this, it's checked that 100 | ``supervisord`` and the ``solr`` process controlled by it are running. 101 | 102 | The ``down`` method can be used to make sure a process is stopped and is the 103 | base of the ``supervisordown`` script. It's used like the ``up`` method above. 104 | 105 | The equivalent for the ``waitforports`` script is ``mr.laforge.waitforports``. 106 | It takes a list of ports as arguments, which can be integers or strings which 107 | can also contain the host separated with a colon. You can also set the default 108 | host with the ``host`` keyword argument and the timeout value with the 109 | ``timeout`` keyword argument. Here is an example: 110 | 111 | .. code-block:: python 112 | 113 | mr.laforge.waitforports(8080, 'db-server.example.com:5432', timeout=10) 114 | 115 | .. _`zc.recipe.testrunner`: http://pypi.python.org/pypi/zc.recipe.testrunner 116 | .. _`zc.recipe.egg`: http://pypi.python.org/pypi/zc.recipe.egg 117 | 118 | There is also a ``shutdown`` function with which you can shutdown supervisord 119 | from Python code. 120 | 121 | Add as plugin to supervisord 122 | ---------------------------- 123 | 124 | To use the plugin part of mr.laforge, you have to add the following to your 125 | supervisord config: 126 | 127 | .. code-block:: ini 128 | 129 | [rpcinterface:laforge] 130 | supervisor.rpcinterface_factory = mr.laforge.rpcinterface:make_laforge_rpcinterface 131 | 132 | [ctlplugin:laforge] 133 | supervisor.ctl_factory = mr.laforge.controllerplugin:make_laforge_controllerplugin 134 | 135 | You have to make sure that mr.laforge is importable by supervisord. In a 136 | buildout you would have to add the egg to supervisor like this: 137 | 138 | .. code-block:: ini 139 | 140 | [supervisor] 141 | recipe = zc.recipe.egg 142 | eggs = 143 | supervisor 144 | mr.laforge 145 | 146 | Now you can use the ``kill`` command: 147 | 148 | .. code-block:: bash 149 | 150 | ./bin/supervisorctl kill HUP nginx 151 | -------------------------------------------------------------------------------- /mr/__init__.py: -------------------------------------------------------------------------------- 1 | __import__('pkg_resources').declare_namespace(__name__) 2 | -------------------------------------------------------------------------------- /mr/laforge/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | from supervisor.options import ClientOptions 3 | from supervisor.xmlrpc import SupervisorTransport 4 | import argparse 5 | import os 6 | import socket 7 | import subprocess 8 | import sys 9 | import time 10 | try: 11 | import xmlrpc.client as xmlrpclib 12 | except ImportError: 13 | import xmlrpclib 14 | 15 | 16 | def get_rpc(options): 17 | transport = SupervisorTransport( 18 | options.username, 19 | options.password, 20 | options.serverurl) 21 | return xmlrpclib.ServerProxy('http://127.0.0.1', transport) 22 | 23 | 24 | def find_supervisord(): 25 | process_path = os.path.dirname(os.path.abspath(sys.argv[0])) 26 | supervisord = os.path.join(process_path, 'supervisord') 27 | if os.path.exists(supervisord): 28 | return supervisord 29 | supervisord = os.path.join(os.getcwd(), 'bin', 'supervisord') 30 | if os.path.exists(supervisord): 31 | return supervisord 32 | print("Couldn't find supervisord", file=sys.stderr) 33 | sys.exit(1) 34 | 35 | 36 | def get_supervisor_args(kwargs): 37 | return kwargs.get('supervisor_args', 38 | os.environ.get('MR_LAFORGE_SUPERVISOR_ARGS', '').split()) 39 | 40 | 41 | def up(*args, **kwargs): 42 | if not args: 43 | args = sys.argv[1:] 44 | supervisor_args = get_supervisor_args(kwargs) 45 | options = ClientOptions() 46 | options.realize(args=supervisor_args) 47 | status = "init" 48 | while 1: 49 | try: 50 | rpc = get_rpc(options) 51 | rpc.supervisor.getPID() 52 | if status == 'shutdown': 53 | sys.stderr.write("\n") 54 | break 55 | except socket.error: 56 | if status == 'shutdown': 57 | sys.stderr.write("\n") 58 | sys.stderr.write("Starting supervisord\n") 59 | supervisord = find_supervisord() 60 | cmd = [supervisord] + supervisor_args 61 | retcode = subprocess.call(cmd) 62 | if retcode != 0: 63 | sys.exit(retcode) 64 | status = 'starting' 65 | except xmlrpclib.Fault as e: 66 | if e.faultString == 'SHUTDOWN_STATE': 67 | if status == 'init': 68 | sys.stderr.write("Supervisor currently shutting down ") 69 | sys.stderr.flush() 70 | status = 'shutdown' 71 | else: 72 | sys.stderr.write(".") 73 | sys.stderr.flush() 74 | time.sleep(1) 75 | if len(args): 76 | for name in args: 77 | info = rpc.supervisor.getProcessInfo(name) 78 | if info['statename'] != 'RUNNING': 79 | print("Starting %s" % name) 80 | try: 81 | rpc.supervisor.startProcess(name) 82 | except xmlrpclib.Fault as e: 83 | if e.faultCode == 60: # already started 84 | continue 85 | print(e.faultCode, e.faultString, file=sys.stderr) 86 | sys.exit(1) 87 | else: 88 | print("%s is already running" % name, file=sys.stderr) 89 | 90 | 91 | def down(*args, **kwargs): 92 | if not args: 93 | args = sys.argv[1:] 94 | supervisor_args = get_supervisor_args(kwargs) 95 | options = ClientOptions() 96 | options.realize(args=supervisor_args) 97 | status = "init" 98 | while 1: 99 | try: 100 | rpc = get_rpc(options) 101 | rpc.supervisor.getPID() 102 | if status == 'shutdown': 103 | sys.stderr.write("\n") 104 | break 105 | except socket.error: 106 | if status == 'shutdown': 107 | sys.stderr.write("\n") 108 | sys.stderr.write("Starting supervisord\n") 109 | supervisord = find_supervisord() 110 | cmd = [supervisord] + supervisor_args 111 | retcode = subprocess.call(cmd) 112 | if retcode != 0: 113 | sys.exit(retcode) 114 | status = 'starting' 115 | except xmlrpclib.Fault as e: 116 | if e.faultString == 'SHUTDOWN_STATE': 117 | if status == 'init': 118 | sys.stderr.write("Supervisor currently shutting down ") 119 | sys.stderr.flush() 120 | status = 'shutdown' 121 | else: 122 | sys.stderr.write(".") 123 | sys.stderr.flush() 124 | time.sleep(1) 125 | if len(args): 126 | for name in args: 127 | info = rpc.supervisor.getProcessInfo(name) 128 | if info['statename'] != 'STOPPED': 129 | print("Stopping %s" % name) 130 | try: 131 | rpc.supervisor.stopProcess(name) 132 | except xmlrpclib.Fault as e: 133 | # if e.faultCode == 60: # already stopped 134 | # continue 135 | print(e.faultCode, e.faultString, file=sys.stderr) 136 | sys.exit(1) 137 | else: 138 | print("%s is already stopped" % name, file=sys.stderr) 139 | 140 | 141 | def shutdown(**kwargs): 142 | supervisor_args = get_supervisor_args(kwargs) 143 | options = ClientOptions() 144 | options.realize(args=supervisor_args) 145 | try: 146 | rpc = get_rpc(options) 147 | rpc.supervisor.shutdown() 148 | print("Shutting down supervisor", file=sys.stderr) 149 | except socket.error: 150 | print("Supervisor already shut down", file=sys.stderr) 151 | except xmlrpclib.Fault as e: 152 | if e.faultString == 'SHUTDOWN_STATE': 153 | print("Supervisor already shutting down", file=sys.stderr) 154 | 155 | 156 | def waitforports(*args, **kwargs): 157 | if not args: 158 | args = sys.argv[1:] 159 | timeout = kwargs.get('timeout', 30) 160 | default_host = kwargs.get('host', 'localhost') 161 | parser = argparse.ArgumentParser() 162 | parser.add_argument('-t', '--timeout', type=int, default=timeout) 163 | parser.add_argument('-H', '--default-host', default=default_host) 164 | parser.add_argument('ports', nargs='+') 165 | args = parser.parse_args(map(str, args)) 166 | default_ip = socket.gethostbyname(args.default_host) 167 | timeout = args.timeout 168 | ports = set() 169 | for port in args.ports: 170 | if ':' in port: 171 | host, port = port.split(':') 172 | port = int(port) 173 | if not host.strip(): 174 | ip = default_ip 175 | else: 176 | ip = socket.gethostbyname(host) 177 | else: 178 | port = int(port) 179 | ip = default_ip 180 | ports.add((ip, port)) 181 | sys.stderr.write( 182 | "Waiting for %s " % ", ".join( 183 | "%s:%s" % x for x in sorted(ports))) 184 | sys.stderr.flush() 185 | while ports and timeout > 0: 186 | for ip, port in list(ports): 187 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 188 | s.settimeout(1) 189 | if s.connect_ex((ip, port)) == 0: 190 | ports.remove((ip, port)) 191 | s.close() 192 | if ports: 193 | time.sleep(1) 194 | sys.stderr.write(".") 195 | sys.stderr.flush() 196 | timeout -= 1 197 | sys.stderr.write("\n") 198 | sys.stderr.flush() 199 | if ports: 200 | sys.stderr.write( 201 | "Timeout on %s\n" % ", ".join( 202 | "%s:%s" % x for x in sorted(ports))) 203 | sys.stderr.flush() 204 | sys.exit(1) 205 | -------------------------------------------------------------------------------- /mr/laforge/controllerplugin.py: -------------------------------------------------------------------------------- 1 | from supervisor.compat import Fault 2 | from supervisor.options import split_namespec 3 | from supervisor.supervisorctl import ControllerPluginBase 4 | from supervisor.xmlrpc import Faults 5 | 6 | 7 | class LaForgeControllerPlugin(ControllerPluginBase): 8 | def __init__(self, controller): 9 | self.ctl = controller 10 | self.laforge = controller.get_server_proxy('laforge') 11 | 12 | def _killresult(self, result): 13 | name = result['name'] 14 | code = result['status'] 15 | template = '%s: ERROR (%s)' 16 | if code == Faults.BAD_NAME: 17 | return template % (name, 'no such process') 18 | elif code == Faults.BAD_ARGUMENTS: 19 | return template % (name, 'signal not defined') 20 | elif code == Faults.NOT_RUNNING: 21 | return template % (name, 'not running') 22 | elif code == Faults.SUCCESS: 23 | return '%s: signal sent' % name 24 | # assertion 25 | raise ValueError('Unknown result code %s for %s' % (code, name)) 26 | 27 | def do_kill(self, arg): 28 | if not self.ctl.upcheck(): 29 | return 30 | 31 | args = arg.strip().split() 32 | if not args: 33 | self.ctl.output("Error: kill requires a signal and process name") 34 | self.help_kill() 35 | return 36 | 37 | signal = args[0] 38 | names = args[1:] 39 | 40 | if not names: 41 | self.ctl.output("Error: kill requires a process name") 42 | self.help_start() 43 | return 44 | 45 | for name in names: 46 | group_name, process_name = split_namespec(name) 47 | if process_name is None: 48 | results = self.laforge.killProcessGroup(group_name, signal) 49 | for result in results: 50 | result = self._killresult(result) 51 | self.ctl.output(result) 52 | else: 53 | try: 54 | result = self.laforge.killProcess(name, signal) 55 | except Fault as e: 56 | error = self._killresult({'status':e.faultCode, 57 | 'name':name, 58 | 'description':e.faultString}) 59 | self.ctl.output(error) 60 | else: 61 | self.ctl.output('%s: signal sent' % name) 62 | 63 | def help_kill(self): 64 | self.ctl.output("kill \t\tSend signal to a process") 65 | self.ctl.output("kill :*\t\tSend signal to all processes in a group") 66 | self.ctl.output( 67 | "kill \tSend signal to multiple processes or groups") 68 | 69 | 70 | def make_laforge_controllerplugin(controller, **config): 71 | return LaForgeControllerPlugin(controller) 72 | -------------------------------------------------------------------------------- /mr/laforge/rpcinterface.py: -------------------------------------------------------------------------------- 1 | from supervisor.options import split_namespec 2 | from supervisor.rpcinterface import isRunning, make_allfunc 3 | from supervisor.states import RUNNING_STATES 4 | from supervisor.supervisord import SupervisorStates 5 | from supervisor.xmlrpc import Faults 6 | from supervisor.xmlrpc import RPCError 7 | import signal 8 | 9 | 10 | API_VERSION = '0.1' 11 | 12 | 13 | class LaForgeRPCInterface(object): 14 | def __init__(self, supervisord): 15 | self.supervisord = supervisord 16 | 17 | def _update(self, text): 18 | if self.supervisord.options.mood < SupervisorStates.RUNNING: 19 | raise RPCError(Faults.SHUTDOWN_STATE) 20 | 21 | def getAPIVersion(self): 22 | """ Return the version of the RPC API used by mr.laforge 23 | 24 | @return string version 25 | """ 26 | self._update('getAPIVersion') 27 | return API_VERSION 28 | 29 | def getMrLaForgeVersion(self): 30 | """ Return the version of the mr.laforge package 31 | 32 | @return string version version id 33 | """ 34 | self._update('getMrLaForgeVersion') 35 | import pkg_resources 36 | return pkg_resources.get_distribution("mr.laforge").version 37 | 38 | def _getGroupAndProcess(self, name): 39 | # get process to start from name 40 | group_name, process_name = split_namespec(name) 41 | 42 | group = self.supervisord.process_groups.get(group_name) 43 | if group is None: 44 | raise RPCError(Faults.BAD_NAME, name) 45 | 46 | if process_name is None: 47 | return group, None 48 | 49 | process = group.processes.get(process_name) 50 | if process is None: 51 | raise RPCError(Faults.BAD_NAME, name) 52 | 53 | return group, process 54 | 55 | def _getSignalFromString(self, name): 56 | try: 57 | return int(name) 58 | except ValueError: 59 | pass 60 | name = name.upper() 61 | sig = getattr(signal, name, None) 62 | if isinstance(sig, int): 63 | return sig 64 | sig = getattr(signal, "SIG%s" % name, None) 65 | if isinstance(sig, int): 66 | return sig 67 | 68 | def killProcess(self, name, signal): 69 | """ Send signal to a process 70 | 71 | @param string signal Signal identifier 72 | @param string name Process name (or 'group:name', or 'group:*') 73 | @return boolean result Always true unless error 74 | 75 | """ 76 | self._update('killProcess') 77 | group, process = self._getGroupAndProcess(name) 78 | if process is None: 79 | group_name, process_name = split_namespec(name) 80 | return self.killProcessGroup(signal, group_name) 81 | 82 | sig = self._getSignalFromString(signal) 83 | 84 | if sig is None: 85 | raise RPCError(Faults.BAD_ARGUMENTS, signal) 86 | 87 | killed = [] 88 | called = [] 89 | kill = self.supervisord.options.kill 90 | 91 | def killit(): 92 | if not called: 93 | if process.get_state() not in RUNNING_STATES: 94 | raise RPCError(Faults.NOT_RUNNING) 95 | # use a mutable for lexical scoping; see startProcess 96 | called.append(1) 97 | 98 | if not killed: 99 | kill(process.pid, sig) 100 | killed.append(1) 101 | 102 | return True 103 | 104 | killit.delay = 0.2 105 | killit.rpcinterface = self 106 | return killit # deferred 107 | 108 | 109 | def killProcessGroup(self, name, signal): 110 | """ Send signal to all processes in the group named 'name' 111 | 112 | @param string signal Signal identifier 113 | @param string name The group name 114 | @return struct result A structure containing start statuses 115 | """ 116 | self._update('killProcessGroup') 117 | 118 | group = self.supervisord.process_groups.get(name) 119 | 120 | if group is None: 121 | raise RPCError(Faults.BAD_NAME, name) 122 | 123 | processes = group.processes.values() 124 | processes.sort() 125 | processes = [ (group, process) for process in processes ] 126 | 127 | killall = make_allfunc(processes, isRunning, self.killProcess, signal=signal) 128 | 129 | killall.delay = 0.05 130 | killall.rpcinterface = self 131 | return killall # deferred 132 | 133 | 134 | def make_laforge_rpcinterface(supervisord, **config): 135 | return LaForgeRPCInterface(supervisord) 136 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup 3 | 4 | version = '0.9' 5 | 6 | here = os.path.abspath(os.path.dirname(__file__)) 7 | README = open(os.path.join(here, 'README.rst')).read() 8 | HISTORY = open(os.path.join(here, 'HISTORY.rst')).read() 9 | 10 | install_requires = [ 11 | 'setuptools', 12 | 'supervisor'] 13 | 14 | try: 15 | import argparse 16 | argparse # make pyflakes happy... 17 | except ImportError: 18 | install_requires.append('argparse') 19 | 20 | setup( 21 | name='mr.laforge', 22 | version=version, 23 | description="Plugins and utilities for supervisor", 24 | long_description=README + "\n\n" + HISTORY, 25 | keywords='', 26 | author='Florian Schulze', 27 | author_email='florian.schulze@gmx.net', 28 | url='http://github.com/fschulze/mr.laforge', 29 | license='BSD', 30 | packages=['mr', 'mr.laforge'], 31 | namespace_packages=['mr'], 32 | include_package_data=True, 33 | zip_safe=True, 34 | install_requires=install_requires, 35 | entry_points=""" 36 | [console_scripts] 37 | supervisorup = mr.laforge:up 38 | supervisordown = mr.laforge:down 39 | waitforports = mr.laforge:waitforports 40 | """ 41 | ) 42 | --------------------------------------------------------------------------------