[Supervisor-checkins] r813 - in superlance: . trunk trunk/superlance
Chris McDonough
chrism at agendaless.com
Thu Sep 18 18:55:52 EDT 2008
Author: Chris McDonough <chrism at agendaless.com>
Date: Thu Sep 18 18:55:51 2008
New Revision: 813
Log:
Superlance.
Added:
superlance/
superlance/trunk/
superlance/trunk/CHANGES.txt
superlance/trunk/README.txt
superlance/trunk/ez_setup.py
superlance/trunk/setup.py
superlance/trunk/superlance/
superlance/trunk/superlance/__init__.py
superlance/trunk/superlance/httpok.py
superlance/trunk/superlance/tests.py
superlance/trunk/superlance/timeoutconn.py
Added: superlance/trunk/CHANGES.txt
==============================================================================
--- (empty file)
+++ superlance/trunk/CHANGES.txt Thu Sep 18 18:55:51 2008
@@ -0,0 +1,3 @@
+0.1
+
+ Initial release
Added: superlance/trunk/README.txt
==============================================================================
--- (empty file)
+++ superlance/trunk/README.txt Thu Sep 18 18:55:51 2008
@@ -0,0 +1,14 @@
+superlance plugins for supervisor
+=================================
+
+Superlance is a package of plugin utilities for monitoring and
+controlling processes that run under `supervisor
+<http://supervisord.org>`_.
+
+Currently, it provides only one script named ``httpok``. This script
+can be used as a supervisor event listener (subscribed to TICK events)
+which will restart a "hung" HTTP server process, which is defined as a
+process in the RUNNING state which does not respond in an appropriate
+or timely manner to an HTTP GET request.
+
+
Added: superlance/trunk/ez_setup.py
==============================================================================
--- (empty file)
+++ superlance/trunk/ez_setup.py Thu Sep 18 18:55:51 2008
@@ -0,0 +1,272 @@
+#!python
+"""Bootstrap setuptools installation
+
+If you want to use setuptools in your package's setup.py, just include this
+file in the same directory with it, and add this to the top of your setup.py::
+
+ from ez_setup import use_setuptools
+ use_setuptools()
+
+If you want to require a specific version of setuptools, set a download
+mirror, or use an alternate download directory, you can do so by supplying
+the appropriate options to ``use_setuptools()``.
+
+This file can also be run as a script to install or upgrade setuptools.
+"""
+import sys
+DEFAULT_VERSION = "0.6c8"
+DEFAULT_URL = "http://pypi.python.org/packages/%s/s/setuptools/" % sys.version[:3]
+
+md5_data = {
+ 'setuptools-0.6b1-py2.3.egg': '8822caf901250d848b996b7f25c6e6ca',
+ 'setuptools-0.6b1-py2.4.egg': 'b79a8a403e4502fbb85ee3f1941735cb',
+ 'setuptools-0.6b2-py2.3.egg': '5657759d8a6d8fc44070a9d07272d99b',
+ 'setuptools-0.6b2-py2.4.egg': '4996a8d169d2be661fa32a6e52e4f82a',
+ 'setuptools-0.6b3-py2.3.egg': 'bb31c0fc7399a63579975cad9f5a0618',
+ 'setuptools-0.6b3-py2.4.egg': '38a8c6b3d6ecd22247f179f7da669fac',
+ 'setuptools-0.6b4-py2.3.egg': '62045a24ed4e1ebc77fe039aa4e6f7e5',
+ 'setuptools-0.6b4-py2.4.egg': '4cb2a185d228dacffb2d17f103b3b1c4',
+ 'setuptools-0.6c1-py2.3.egg': 'b3f2b5539d65cb7f74ad79127f1a908c',
+ 'setuptools-0.6c1-py2.4.egg': 'b45adeda0667d2d2ffe14009364f2a4b',
+ 'setuptools-0.6c2-py2.3.egg': 'f0064bf6aa2b7d0f3ba0b43f20817c27',
+ 'setuptools-0.6c2-py2.4.egg': '616192eec35f47e8ea16cd6a122b7277',
+ 'setuptools-0.6c3-py2.3.egg': 'f181fa125dfe85a259c9cd6f1d7b78fa',
+ 'setuptools-0.6c3-py2.4.egg': 'e0ed74682c998bfb73bf803a50e7b71e',
+ 'setuptools-0.6c3-py2.5.egg': 'abef16fdd61955514841c7c6bd98965e',
+ 'setuptools-0.6c4-py2.3.egg': 'b0b9131acab32022bfac7f44c5d7971f',
+ 'setuptools-0.6c4-py2.4.egg': '2a1f9656d4fbf3c97bf946c0a124e6e2',
+ 'setuptools-0.6c4-py2.5.egg': '8f5a052e32cdb9c72bcf4b5526f28afc',
+ 'setuptools-0.6c5-py2.3.egg': 'ee9fd80965da04f2f3e6b3576e9d8167',
+ 'setuptools-0.6c5-py2.4.egg': 'afe2adf1c01701ee841761f5bcd8aa64',
+ 'setuptools-0.6c5-py2.5.egg': 'a8d3f61494ccaa8714dfed37bccd3d5d',
+ 'setuptools-0.6c6-py2.3.egg': '35686b78116a668847237b69d549ec20',
+ 'setuptools-0.6c6-py2.4.egg': '3c56af57be3225019260a644430065ab',
+ 'setuptools-0.6c6-py2.5.egg': 'b2f8a7520709a5b34f80946de5f02f53',
+ 'setuptools-0.6c7-py2.3.egg': '209fdf9adc3a615e5115b725658e13e2',
+ 'setuptools-0.6c7-py2.4.egg': '5a8f954807d46a0fb67cf1f26c55a82e',
+ 'setuptools-0.6c7-py2.5.egg': '45d2ad28f9750e7434111fde831e8372',
+ 'setuptools-0.6c8-py2.3.egg': '50759d29b349db8cfd807ba8303f1902',
+ 'setuptools-0.6c8-py2.4.egg': 'cba38d74f7d483c06e9daa6070cce6de',
+ 'setuptools-0.6c8-py2.5.egg': '1721747ee329dc150590a58b3e1ac95b',
+}
+
+import sys, os
+
+def _validate_md5(egg_name, data):
+ if egg_name in md5_data:
+ from md5 import md5
+ digest = md5(data).hexdigest()
+ if digest != md5_data[egg_name]:
+ print >>sys.stderr, (
+ "md5 validation of %s failed! (Possible download problem?)"
+ % egg_name
+ )
+ sys.exit(2)
+ return data
+
+
+def use_setuptools(
+ version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
+ download_delay=15
+):
+ """Automatically find/download setuptools and make it available on sys.path
+
+ `version` should be a valid setuptools version number that is available
+ as an egg for download under the `download_base` URL (which should end with
+ a '/'). `to_dir` is the directory where setuptools will be downloaded, if
+ it is not already available. If `download_delay` is specified, it should
+ be the number of seconds that will be paused before initiating a download,
+ should one be required. If an older version of setuptools is installed,
+ this routine will print a message to ``sys.stderr`` and raise SystemExit in
+ an attempt to abort the calling script.
+ """
+ was_imported = 'pkg_resources' in sys.modules or 'setuptools' in sys.modules
+ def do_download():
+ egg = download_setuptools(version, download_base, to_dir, download_delay)
+ sys.path.insert(0, egg)
+ import setuptools; setuptools.bootstrap_install_from = egg
+ try:
+ import pkg_resources
+ except ImportError:
+ return do_download()
+ try:
+ pkg_resources.require("setuptools>="+version); return
+ except pkg_resources.VersionConflict, e:
+ if was_imported:
+ print >>sys.stderr, (
+ "The required version of setuptools (>=%s) is not available, and\n"
+ "can't be installed while this script is running. Please install\n"
+ " a more recent version first, using 'easy_install -U setuptools'."
+ "\n\n(Currently using %r)"
+ ) % (version, e.args[0])
+ sys.exit(2)
+ else:
+ del pkg_resources, sys.modules['pkg_resources'] # reload ok
+ return do_download()
+ except pkg_resources.DistributionNotFound:
+ return do_download()
+
+def download_setuptools(
+ version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
+ delay = 15
+):
+ """Download setuptools from a specified location and return its filename
+
+ `version` should be a valid setuptools version number that is available
+ as an egg for download under the `download_base` URL (which should end
+ with a '/'). `to_dir` is the directory where the egg will be downloaded.
+ `delay` is the number of seconds to pause before an actual download attempt.
+ """
+ import urllib2, shutil
+ egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3])
+ url = download_base + egg_name
+ saveto = os.path.join(to_dir, egg_name)
+ src = dst = None
+ if not os.path.exists(saveto): # Avoid repeated downloads
+ try:
+ from distutils import log
+ if delay:
+ log.warn("""
+---------------------------------------------------------------------------
+This script requires setuptools version %s to run (even to display
+help). I will attempt to download it for you (from
+%s), but
+you may need to enable firewall access for this script first.
+I will start the download in %d seconds.
+
+(Note: if this machine does not have network access, please obtain the file
+
+ %s
+
+and place it in this directory before rerunning this script.)
+---------------------------------------------------------------------------""",
+ version, download_base, delay, url
+ ); from time import sleep; sleep(delay)
+ log.warn("Downloading %s", url)
+ src = urllib2.urlopen(url)
+ # Read/write all in one block, so we don't create a corrupt file
+ # if the download is interrupted.
+ data = _validate_md5(egg_name, src.read())
+ dst = open(saveto,"wb"); dst.write(data)
+ finally:
+ if src: src.close()
+ if dst: dst.close()
+ return os.path.realpath(saveto)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+def main(argv, version=DEFAULT_VERSION):
+ """Install or upgrade setuptools and EasyInstall"""
+ try:
+ import setuptools
+ except ImportError:
+ egg = None
+ try:
+ egg = download_setuptools(version, delay=0)
+ sys.path.insert(0,egg)
+ from setuptools.command.easy_install import main
+ return main(list(argv)+[egg]) # we're done here
+ finally:
+ if egg and os.path.exists(egg):
+ os.unlink(egg)
+ else:
+ if setuptools.__version__ == '0.0.1':
+ print >>sys.stderr, (
+ "You have an obsolete version of setuptools installed. Please\n"
+ "remove it from your system entirely before rerunning this script."
+ )
+ sys.exit(2)
+
+ req = "setuptools>="+version
+ import pkg_resources
+ try:
+ pkg_resources.require(req)
+ except pkg_resources.VersionConflict:
+ try:
+ from setuptools.command.easy_install import main
+ except ImportError:
+ from easy_install import main
+ main(list(argv)+[download_setuptools(delay=0)])
+ sys.exit(0) # try to force an exit
+ else:
+ if argv:
+ from setuptools.command.easy_install import main
+ main(argv)
+ else:
+ print "Setuptools version",version,"or greater has been installed."
+ print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)'
+
+def update_md5(filenames):
+ """Update our built-in md5 registry"""
+
+ import re
+ from md5 import md5
+
+ for name in filenames:
+ base = os.path.basename(name)
+ f = open(name,'rb')
+ md5_data[base] = md5(f.read()).hexdigest()
+ f.close()
+
+ data = [" %r: %r,\n" % it for it in md5_data.items()]
+ data.sort()
+ repl = "".join(data)
+
+ import inspect
+ srcfile = inspect.getsourcefile(sys.modules[__name__])
+ f = open(srcfile, 'rb'); src = f.read(); f.close()
+
+ match = re.search("\nmd5_data = {\n([^}]+)}", src)
+ if not match:
+ print >>sys.stderr, "Internal error!"
+ sys.exit(2)
+
+ src = src[:match.start(1)] + repl + src[match.end(1):]
+ f = open(srcfile,'w')
+ f.write(src)
+ f.close()
+
+
+if __name__=='__main__':
+ if len(sys.argv)>2 and sys.argv[1]=='--md5update':
+ update_md5(sys.argv[2:])
+ else:
+ main(sys.argv[1:])
+
+
+
+
+
Added: superlance/trunk/setup.py
==============================================================================
--- (empty file)
+++ superlance/trunk/setup.py Thu Sep 18 18:55:51 2008
@@ -0,0 +1,52 @@
+import os
+
+from ez_setup import use_setuptools
+use_setuptools()
+
+from setuptools import setup, find_packages
+
+here = os.path.abspath(os.path.dirname(__file__))
+try:
+ README = open(os.path.join(here, 'README.txt')).read()
+except (IOError, OSError):
+ README = ''
+try:
+ CHANGES = open(os.path.join(here, 'CHANGES.txt')).read()
+except (IOError, OSError):
+ CHANGES = ''
+
+setup(name='superlance',
+ version='0.1',
+ description='superlance plugins for supervisord',
+ long_description=README + '\n\n' + CHANGES,
+ classifiers=[
+ "Development Status :: 3 - Alpha",
+ 'Environment :: No Input/Output (Daemon)',
+ 'Intended Audience :: System Administrators',
+ 'Natural Language :: English',
+ 'Operating System :: POSIX',
+ 'Topic :: System :: Boot',
+ 'Topic :: System :: Monitoring',
+ 'Topic :: System :: Systems Administration',
+ ],
+ author='Chris McDonough',
+ author_email='chrism at plope.com',
+ url='http://supervisord.org',
+ keywords = 'supervisor monitoring',
+ packages = find_packages(),
+ include_package_data=True,
+ zip_safe=False,
+ install_requires=[
+ 'setuptools',
+ 'supervisor',
+ ],
+ tests_require=[
+ 'supervisor',
+ ],
+ test_suite="superlance",
+ entry_points = """\
+ [console_scripts]
+ httpok = superlance.httpok:main
+ """
+ )
+
Added: superlance/trunk/superlance/__init__.py
==============================================================================
--- (empty file)
+++ superlance/trunk/superlance/__init__.py Thu Sep 18 18:55:51 2008
@@ -0,0 +1 @@
+# superlance package
Added: superlance/trunk/superlance/httpok.py
==============================================================================
--- (empty file)
+++ superlance/trunk/superlance/httpok.py Thu Sep 18 18:55:51 2008
@@ -0,0 +1,323 @@
+#!/usr/bin/env python -u
+##############################################################################
+#
+# Copyright (c) 2007 Agendaless Consulting and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the BSD-like license at
+# http://www.repoze.org/LICENSE.txt. A copy of the license should accompany
+# this distribution. THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL
+# EXPRESS OR IMPLIED WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO,
+# THE IMPLIED WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND
+# FITNESS FOR A PARTICULAR PURPOSE
+#
+##############################################################################
+
+# A event listener meant to be subscribed to TICK_60 (or TICK_5)
+# events, which restarts processes that are children of
+# supervisord based on the response from an HTTP port.
+
+# A supervisor config snippet that tells supervisor to use this script
+# as a listener is below.
+#
+# [eventlistener:httpok]
+# command=python -u /bin/httpok http://localhost:8080/tasty/service
+# events=TICK_60
+
+doc = """\
+httpok.py [-p processname] [-a] [-t timeout] [-c status_code] [-b inbody]
+ [-m mail_address] [-s sendmail] URL
+
+Options:
+
+-p -- specify a supervisor process_name. Restart the supervisor
+ process named 'process_name' if it's in the RUNNING state when
+ the URL returns an unexpected result or times out. If this
+ process is part of a group, it can be specified using the
+ 'process_name:group_name' syntax.
+
+-a -- Restart any child of the supervisord under in the RUNNING state
+ if the URL returns an unexpected result or times out. Overrides
+ any -p parameters passed in the same httpok process
+ invocation.
+
+-t -- The number of seconds that httpok should wait for a response
+ before timing out. If this timeout is exceeded, httpok will
+ attempt to restart processes in the RUNNING state specified by
+ -p or -a. This defaults to 10 seconds.
+
+-c -- specify an expected HTTP status code from a GET request to the
+ URL. If this status code is not the status code provided by the
+ response, httpok will attempt to restart processes in the
+ RUNNING state specified by -p or -a. This defaults to the
+ string"200".
+
+-b -- specify a string which should be present in the body resulting
+ from the GET request. If this string is not present in the
+ response, the processes in the RUNNING state specified by -p
+ or -a will be restarted. The default is to ignore the
+ body.
+
+-s -- the sendmail command to use to send email
+ (e.g. "/usr/sbin/sendmail -t -i"). Must be a command which accepts
+ header and message data on stdin and sends mail.
+ Default is "/usr/sbin/sendmail -t -i".
+
+-m -- specify an email address. The script will send mail to this
+ address when httpok attempts to restart processes. If no email
+ address is specified, email will not be sent.
+
+URL -- The URL to which to issue a GET request.
+
+The -p option may be specified more than once, allowing for
+specification of multiple processes. Specifying -a overrides any
+selection of -p.
+
+A sample invocation:
+
+httpok.py -p program1 -p group1:program2 http://localhost:8080/tasty
+
+"""
+
+import os
+import sys
+import socket
+import time
+import urlparse
+import xmlrpclib
+
+from supervisor import childutils
+from supervisor.states import ProcessStates
+from supervisor.options import make_namespec
+
+import timeoutconn
+
+def usage():
+ print doc
+ sys.exit(255)
+
+class HTTPOk:
+ connclass = None
+ def __init__(self, rpc, programs, any, url, timeout, status, inbody,
+ email, sendmail):
+ self.rpc = rpc
+ self.programs = programs
+ self.any = any
+ self.url = url
+ self.timeout = timeout
+ self.status = status
+ self.inbody = inbody
+ self.email = email
+ self.sendmail = sendmail
+ self.stdin = sys.stdin
+ self.stdout = sys.stdout
+ self.stderr = sys.stderr
+
+ def runforever(self, test=False):
+ parsed = urlparse.urlsplit(self.url)
+ scheme = parsed[0].lower()
+ hostport = parsed[1]
+ path = parsed[2]
+ params = parsed[3]
+ query = parsed[4]
+
+ if self.connclass:
+ ConnClass = self.connclass
+ elif scheme == 'http':
+ ConnClass = timeoutconn.TimeoutHTTPConnection
+ elif scheme == 'https':
+ ConnClass = timeoutconn.TimeoutHTTPSConnection
+ else:
+ raise ValueError('Bad scheme %s' % scheme)
+
+ while 1:
+ # we explicitly use self.stdin, self.stdout, and self.stderr
+ # instead of sys.* so we can unit test this code
+ headers, payload = childutils.listener.wait(self.stdin, self.stdout)
+
+ if not headers['eventname'].startswith('TICK'):
+ # do nothing with non-TICK events
+ childutils.listener.ok(self.stdout)
+ if test:
+ break
+ continue
+
+ conn = ConnClass(hostport)
+
+ if query:
+ path += '?' + query
+
+ act = False
+
+ try:
+ conn.request('GET', path)
+ res = conn.getresponse()
+ body = res.read()
+ status = res.status
+ msg = 'status contacting %s: %s %s' % (self.url,
+ res.status, res.reason)
+ except Exception, why:
+ body = ''
+ status = None
+ msg = 'error contacting %s:\n\n %s' % (self.url, why)
+
+ self.stderr.flush()
+
+ if str(status) != str(self.status):
+ subject = 'httpok for %s: bad status returned' % self.url
+ self.act(subject, msg)
+ elif self.inbody and self.inbody not in res.body:
+ act = True
+ subject = 'httpok for %s: bad body returned' % self.url
+ self.act(subject, msg)
+
+ childutils.listener.ok(self.stdout)
+ if test:
+ break
+
+ def act(self, subject, msg):
+ messages = [msg]
+
+ def write(msg):
+ self.stderr.write('%s\n' % msg)
+ messages.append(msg)
+
+ specs = self.rpc.supervisor.getAllProcessInfo()
+ waiting = list(self.programs)
+
+ if self.any:
+ write('Restarting all running processes')
+ for spec in specs:
+ name = spec['name']
+ group = spec['group']
+ self.restart(spec, write)
+ namespec = make_namespec(name, group)
+ if name in waiting:
+ waiting.remove(name)
+ if namespec in waiting:
+ waiting.remove(namespec)
+ else:
+ write('Restarting selected processes %s' % self.programs)
+ for spec in specs:
+ name = spec['name']
+ group = spec['group']
+ namespec = make_namespec(name, group)
+ if (name in self.programs) or (namespec in self.programs):
+ self.restart(spec, write)
+ if name in waiting:
+ waiting.remove(name)
+ if namespec in waiting:
+ waiting.remove(namespec)
+
+ if waiting:
+ write(
+ 'Programs not restarted because they did not exist: %s' %
+ waiting)
+
+ if self.email:
+ now = time.asctime()
+ message = '\n'.join(messages)
+ self.mail(self.email, subject, message)
+
+ def mail(self, email, subject, msg):
+ body = 'To: %s\n' % self.email
+ body += 'Subject: %s\n' % subject
+ body += '\n'
+ body += msg
+ m = os.popen(self.sendmail, 'w')
+ m.write(body)
+ m.close()
+ self.mailed = body
+
+ def restart(self, spec, write):
+ namespec = make_namespec(spec['group'], spec['name'])
+ if spec['state'] is ProcessStates.RUNNING:
+ write('%s is in RUNNING state, restarting' % namespec)
+ try:
+ self.rpc.supervisor.stopProcess(namespec)
+ except xmlrpclib.Fault, what:
+ write('Failed to stop process %s: %s' % (
+ namespec, what))
+
+ try:
+ self.rpc.supervisor.startProcess(namespec)
+ except xmlrpclib.Fault, what:
+ write('Failed to start process %s: %s' % (
+ namespec, what))
+ else:
+ write('%s restarted' % namespec)
+
+ else:
+ write('%s not in RUNNING state, NOT restarting' % namespec)
+
+
+def main(argv=sys.argv):
+ import getopt
+ short_args="hp:g:at:c:b:s:m:"
+ long_args=[
+ "help",
+ "program=",
+ "group=",
+ "any",
+ "timeout=",
+ "code=",
+ "body=",
+ "sendmail_program=",
+ "email=",
+ ]
+ arguments = argv[1:]
+ try:
+ opts, args = getopt.getopt(arguments, short_args, long_args)
+ except:
+ usage()
+
+ if not args:
+ usage()
+ if len(args) > 1:
+ usage()
+
+ programs = []
+ any = False
+ sendmail = '/usr/sbin/sendmail -t -i'
+ email = None
+ timeout = 10
+ status = '200'
+ inbody = None
+
+ for option, value in opts:
+
+ if option in ('-h', '--help'):
+ usage()
+
+ if option in ('-p', '--program'):
+ programs.append(value)
+
+ if option in ('-a', '--any'):
+ any = True
+
+ if option in ('-s', '--sendmail_program'):
+ sendmail = value
+
+ if option in ('-m', '--email'):
+ email = value
+
+ if option in ('-t', '--timeout'):
+ timeout = int(value)
+
+ if option in ('-c', '--code'):
+ status = value
+
+ if option in ('-b', '--body'):
+ inbody = value
+
+ url = arguments[-1]
+ rpc = childutils.getRPCInterface(os.environ)
+ prog = HTTPOk(rpc, programs, any, url, timeout, status, inbody, email,
+ sendmail)
+ prog.runforever()
+
+if __name__ == '__main__':
+ main()
+
+
+
Added: superlance/trunk/superlance/tests.py
==============================================================================
--- (empty file)
+++ superlance/trunk/superlance/tests.py Thu Sep 18 18:55:51 2008
@@ -0,0 +1,260 @@
+import sys
+import unittest
+from StringIO import StringIO
+
+class HTTPOkTests(unittest.TestCase):
+ def _getTargetClass(self):
+ from superlance.httpok import HTTPOk
+ return HTTPOk
+
+ def _makeOne(self, *opts):
+ return self._getTargetClass()(*opts)
+
+ def _makeOnePopulated(self, programs, any, response=None, exc=None):
+ if response is None:
+ response = DummyResponse()
+ rpc = DummyRPCServer()
+ sendmail = 'cat - > /dev/null'
+ email = 'chrism at plope.com'
+ url = 'http://foo/bar'
+ timeout = 10
+ status = '200'
+ inbody = None
+ prog = self._makeOne(rpc, programs, any, url, timeout, status,
+ inbody, email, sendmail)
+ prog.stdin = StringIO()
+ prog.stdout = StringIO()
+ prog.stderr = StringIO()
+ prog.connclass = make_connection(response, exc=exc)
+ return prog
+
+ def test_runforever_notatick(self):
+ programs = {'foo':0, 'bar':0, 'baz_01':0 }
+ groups = {}
+ any = None
+ prog = self._makeOnePopulated(programs, any)
+ prog.stdin.write('eventname:NOTATICK len:0\n')
+ prog.stdin.seek(0)
+ prog.runforever(test=True)
+ self.assertEqual(prog.stderr.getvalue(), '')
+
+ def test_runforever_error_on_request_some(self):
+ programs = ['foo', 'bar', 'baz_01', 'notexisting']
+ any = None
+ prog = self._makeOnePopulated(programs, any, exc=True)
+ prog.stdin.write('eventname:TICK len:0\n')
+ prog.stdin.seek(0)
+ prog.runforever(test=True)
+ lines = prog.stderr.getvalue().split('\n')
+ self.assertEqual(len(lines), 7)
+ self.assertEqual(lines[0],
+ ("Restarting selected processes ['foo', 'bar', "
+ "'baz_01', 'notexisting']")
+ )
+ self.assertEqual(lines[1], 'foo is in RUNNING state, restarting')
+ self.assertEqual(lines[2], 'foo restarted')
+ self.assertEqual(lines[3], 'bar not in RUNNING state, NOT restarting')
+ self.assertEqual(lines[4],
+ 'baz:baz_01 not in RUNNING state, NOT restarting')
+ self.assertEqual(lines[5],
+ "Programs not restarted because they did not exist: ['notexisting']")
+ mailed = prog.mailed.split('\n')
+ self.assertEqual(len(mailed), 12)
+ self.assertEqual(mailed[0], 'To: chrism at plope.com')
+ self.assertEqual(mailed[1],
+ 'Subject: httpok for http://foo/bar: bad status returned')
+
+ def test_runforever_error_on_request_any(self):
+ programs = []
+ any = True
+ prog = self._makeOnePopulated(programs, any, exc=True)
+ prog.stdin.write('eventname:TICK len:0\n')
+ prog.stdin.seek(0)
+ prog.runforever(test=True)
+ lines = prog.stderr.getvalue().split('\n')
+ self.assertEqual(len(lines), 6)
+ self.assertEqual(lines[0], 'Restarting all running processes')
+ self.assertEqual(lines[1], 'foo is in RUNNING state, restarting')
+ self.assertEqual(lines[2], 'foo restarted')
+ self.assertEqual(lines[3], 'bar not in RUNNING state, NOT restarting')
+ self.assertEqual(lines[4],
+ 'baz:baz_01 not in RUNNING state, NOT restarting')
+ mailed = prog.mailed.split('\n')
+ self.assertEqual(len(mailed), 11)
+ self.assertEqual(mailed[0], 'To: chrism at plope.com')
+ self.assertEqual(mailed[1],
+ 'Subject: httpok for http://foo/bar: bad status returned')
+
+ def test_runforever_error_on_process_stop(self):
+ programs = ['FAILED']
+ any = False
+ prog = self._makeOnePopulated(programs, any, exc=True)
+ prog.rpc.supervisor.all_process_info = _FAIL
+ prog.stdin.write('eventname:TICK len:0\n')
+ prog.stdin.seek(0)
+ prog.runforever(test=True)
+ lines = prog.stderr.getvalue().split('\n')
+ self.assertEqual(len(lines), 5)
+ self.assertEqual(lines[0], "Restarting selected processes ['FAILED']")
+ self.assertEqual(lines[1], 'foo:FAILED is in RUNNING state, restarting')
+ self.assertEqual(lines[2],
+ "Failed to stop process foo:FAILED: <Fault 30: 'FAILED'>")
+ self.assertEqual(lines[3], 'foo:FAILED restarted')
+ mailed = prog.mailed.split('\n')
+ self.assertEqual(len(mailed), 10)
+ self.assertEqual(mailed[0], 'To: chrism at plope.com')
+ self.assertEqual(mailed[1],
+ 'Subject: httpok for http://foo/bar: bad status returned')
+
+ def test_runforever_error_on_process_start(self):
+ programs = ['SPAWN_ERROR']
+ any = False
+ prog = self._makeOnePopulated(programs, any, exc=True)
+ prog.rpc.supervisor.all_process_info = _FAIL
+ prog.stdin.write('eventname:TICK len:0\n')
+ prog.stdin.seek(0)
+ prog.runforever(test=True)
+ lines = prog.stderr.getvalue().split('\n')
+ self.assertEqual(len(lines), 4)
+ self.assertEqual(lines[0],
+ "Restarting selected processes ['SPAWN_ERROR']")
+ self.assertEqual(lines[1],
+ 'foo:SPAWN_ERROR is in RUNNING state, restarting')
+ self.assertEqual(lines[2],
+ "Failed to start process foo:SPAWN_ERROR: <Fault 50: 'SPAWN_ERROR'>")
+ mailed = prog.mailed.split('\n')
+ self.assertEqual(len(mailed), 9)
+ self.assertEqual(mailed[0], 'To: chrism at plope.com')
+ self.assertEqual(mailed[1],
+ 'Subject: httpok for http://foo/bar: bad status returned')
+
+def make_connection(response, exc=None):
+ class TestConnection:
+ def __init__(self, hostport):
+ self.hostport = hostport
+
+ def request(self, method, path):
+ if exc:
+ raise ValueError('foo')
+ self.method = method
+ self.path = path
+
+ def getresponse(self):
+ return response
+
+ return TestConnection
+
+class DummyResponse:
+ status = 200
+ reason = 'OK'
+ body = 'OK'
+ def read(self):
+ return self.body
+
+class DummyRPCServer:
+ def __init__(self):
+ self.supervisor = DummySupervisorRPCNamespace()
+ self.system = DummySystemRPCNamespace()
+
+class DummySystemRPCNamespace:
+ pass
+
+import time
+from supervisor.process import ProcessStates
+
+_NOW = time.time()
+
+_FAIL = [ {
+ 'name':'FAILED',
+ 'group':'foo',
+ 'pid':11,
+ 'state':ProcessStates.RUNNING,
+ 'statename':'RUNNING',
+ 'start':_NOW - 100,
+ 'stop':0,
+ 'spawnerr':'',
+ 'now':_NOW,
+ 'description':'foo description',
+ },
+{
+ 'name':'SPAWN_ERROR',
+ 'group':'foo',
+ 'pid':11,
+ 'state':ProcessStates.RUNNING,
+ 'statename':'RUNNING',
+ 'start':_NOW - 100,
+ 'stop':0,
+ 'spawnerr':'',
+ 'now':_NOW,
+ 'description':'foo description',
+ },]
+
+class DummySupervisorRPCNamespace:
+ _restartable = True
+ _restarted = False
+ _shutdown = False
+ _readlog_error = False
+
+
+ all_process_info = [
+ {
+ 'name':'foo',
+ 'group':'foo',
+ 'pid':11,
+ 'state':ProcessStates.RUNNING,
+ 'statename':'RUNNING',
+ 'start':_NOW - 100,
+ 'stop':0,
+ 'spawnerr':'',
+ 'now':_NOW,
+ 'description':'foo description',
+ },
+ {
+ 'name':'bar',
+ 'group':'bar',
+ 'pid':12,
+ 'state':ProcessStates.FATAL,
+ 'statename':'FATAL',
+ 'start':_NOW - 100,
+ 'stop':_NOW - 50,
+ 'spawnerr':'screwed',
+ 'now':_NOW,
+ 'description':'bar description',
+ },
+ {
+ 'name':'baz_01',
+ 'group':'baz',
+ 'pid':12,
+ 'state':ProcessStates.STOPPED,
+ 'statename':'STOPPED',
+ 'start':_NOW - 100,
+ 'stop':_NOW - 25,
+ 'spawnerr':'',
+ 'now':_NOW,
+ 'description':'baz description',
+ },
+ ]
+
+ def getAllProcessInfo(self):
+ return self.all_process_info
+
+ def startProcess(self, name):
+ from supervisor import xmlrpc
+ from xmlrpclib import Fault
+ if name.endswith('SPAWN_ERROR'):
+ raise Fault(xmlrpc.Faults.SPAWN_ERROR, 'SPAWN_ERROR')
+ return True
+
+ def stopProcess(self, name):
+ from supervisor import xmlrpc
+ from xmlrpclib import Fault
+ if name.endswith('FAILED'):
+ raise Fault(xmlrpc.Faults.FAILED, 'FAILED')
+ return True
+
+
+def test_suite():
+ return unittest.findTestCases(sys.modules[__name__])
+
+if __name__ == '__main__':
+ unittest.main(defaultTest='test_suite')
Added: superlance/trunk/superlance/timeoutconn.py
==============================================================================
--- (empty file)
+++ superlance/trunk/superlance/timeoutconn.py Thu Sep 18 18:55:51 2008
@@ -0,0 +1,42 @@
+import httplib
+import socket
+
+class TimeoutHTTPConnection(httplib.HTTPConnection):
+ """A customised HTTPConnection allowing a per-connection
+ timeout, specified at construction."""
+ timeout = None
+
+ def connect(self):
+ """Override HTTPConnection.connect to connect to
+ host/port specified in __init__."""
+
+ msg = "getaddrinfo returns an empty list"
+ for res in socket.getaddrinfo(self.host, self.port,
+ 0, socket.SOCK_STREAM):
+ af, socktype, proto, canonname, sa = res
+ try:
+ self.sock = socket.socket(af, socktype, proto)
+ if self.timeout: # this is the new bit
+ self.sock.settimeout(self.timeout)
+ self.sock.connect(sa)
+ except socket.error, msg:
+ if self.sock:
+ self.sock.close()
+ self.sock = None
+ continue
+ break
+ if not self.sock:
+ raise socket.error, msg
+
+class TimeoutHTTPSConnection(httplib.HTTPSConnection):
+ timeout = None
+
+ def connect(self):
+ "Connect to a host on a given (SSL) port."
+
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ if self.timeout:
+ self.sock.settimeout(self.timeout)
+ sock.connect((self.host, self.port))
+ ssl = socket.ssl(sock, self.key_file, self.cert_file)
+ self.sock = httplib.FakeSocket(sock, ssl)
More information about the Supervisor-checkins
mailing list