pydirector-1.0.0/0000755000026300012320000000000010157566420014320 5ustar anthonytech00000000000000pydirector-1.0.0/doc/0000755000026300012320000000000010157566420015065 5ustar anthonytech00000000000000pydirector-1.0.0/doc/ChangeLog0000644000026300012320000004017610157564166016654 0ustar anthonytech000000000000002004-12-15 00:19 anthonybaxter * pydirector/pdnetwork.py: support twisted or asyncore transparently (but prefer twisted) 2004-12-15 00:18 anthonybaxter * confex.xml: move comment from top 2004-08-18 15:36 anthonybaxter * pydirector/pdconf.py: move import to top of method 2003-10-10 15:41 anthonybaxter * pydirector/__init__.py: new version 2003-10-09 18:53 anthonybaxter * pydir.py, pydirector/__init__.py, pydirector/micropubl.py, pydirector/pdadmin.py, pydirector/pdconf.py, pydirector/pdlogging.py, pydirector/pdmain.py, pydirector/pdmanager.py, pydirector/pdnetworkasyncore.py, pydirector/pdnetworktwisted.py, pydirector/pdschedulers.py: update copyright year 2003-10-09 18:51 anthonybaxter * README.txt: release stuff. 2003-10-09 18:50 anthonybaxter * TODO.txt: [no log message] 2003-10-09 18:38 anthonybaxter * TODO.txt: [no log message] 2003-10-09 18:35 anthonybaxter * TODO.txt: done a couple 2003-10-09 18:33 anthonybaxter * README.txt, doc/configure.txt: document '*' == all addresses. 2003-10-09 18:26 anthonybaxter * pydirector/pdconf.py: can specify '*' as hostname to listen for admin interface == all addresses. Will also add the same feature to the listen directive. 2003-10-09 18:22 anthonybaxter * doc/ChangeLog: [no log message] 2003-10-09 18:20 anthonybaxter * README.txt, doc/pythondirector.dtd, doc/schedulers.txt, pydirector/pdschedulers.py: Added 'leastconnsrr' scheduler. May end up deprecating/removing the existing leastconns scheduler, as the new one is far far better. 2003-08-18 13:24 anthonybaxter * pydirector/pdnetworktwisted.py: actually change the scheduler when requested. 2003-05-02 19:26 anthonybaxter * pydirector/pdconf.py: force backend server addresses to be ascii strings - avoids some internal processing by python. 2003-05-01 14:28 anthonybaxter * pydirector/: pdmain.py, pdnetworkasyncore.py, pdnetworktwisted.py: whitespace n18n 2003-05-01 14:26 anthonybaxter * pydirector/pdschedulers.py: debugging print left in. 2003-04-30 18:41 anthonybaxter * README.txt, TODO.txt, pydirector/pdnetworkasyncore.py, pydirector/pdnetworktwisted.py, pydirector/pdschedulers.py: first bunch of changes to allow "sticky" schedulers, where a client is sent to the same server each time (by preference). No actual scheduler or config support is there yet. 2003-04-30 18:24 anthonybaxter * pydir.py, pydirector/pdmain.py, pydirector/pdnetworktwisted.py: optional profile (hotshot) support 2003-04-30 16:41 anthonybaxter * MANIFEST.in: add dtd to package. 2003-04-30 16:40 anthonybaxter * doc/ChangeLog: [no log message] 2003-04-30 16:38 anthonybaxter * README.txt, pydirector/__init__.py: pre-release dance. 2003-04-30 16:35 anthonybaxter * pydirector/pdconf.py: damn. stupid braino. 2003-04-30 16:34 anthonybaxter * pydirector/pdconf.py: handle comments in more places. 2003-04-30 16:13 anthonybaxter * BUGS.txt, setup.py, pydirector/pdmanager.py, pydirector/pdnetworktwisted.py, pydirector/pdschedulers.py: whitespace n18n 2003-04-30 16:13 anthonybaxter * TODO.txt: [no log message] 2003-04-30 16:07 anthonybaxter * pydir.py: up the number of open files with resource.setrlimit(RLIMIT_NOFILE) 2003-04-30 16:05 anthonybaxter * pydirector/pdmanager.py: in the case where all backends are down, go into aggressive mode - automatically re-add all servers each time round the main manager loop. 2003-04-30 16:04 anthonybaxter * pydirector/pdconf.py: accept (and ignore) comments in the XML config. 2003-04-30 16:03 anthonybaxter * pydirector/pdnetworktwisted.py: use pdlogging rather than 'print'. in the case of no working backends, drop the client connection. 2003-04-30 16:02 anthonybaxter * pydirector/pdschedulers.py: handle case where no working servers were found - doneHost won't be able to mark anything as "done". 2003-04-30 16:01 anthonybaxter * pydirector/pdmain.py: don't hardcode default manager sleep granularity here as well as in pdmanager.py 2002-11-26 17:42 anthonybaxter * pydirector/__init__.py: [no log message] 2002-11-26 17:32 anthonybaxter * README.txt, pydir.py, setup.py: more pre-release cleanup. 2002-11-26 16:50 anthonybaxter * pydirector/: __init__.py, pdadmin.py, pdmain.py, pdnetworktwisted.py, pdschedulers.py: reindent.py-ified. 2002-11-26 14:53 anthonybaxter * pydirector/pdmanager.py: *sigh* missed another line ending. 2002-11-26 14:52 anthonybaxter * README.txt: 0.0.5 notes. 2002-11-26 14:45 anthonybaxter * pydirector/: __init__.py, pdlogging.py, pdmain.py, pdnetwork.py, pdnetworkasyncore.py, pdnetworktwisted.py, pdschedulers.py: merging the twisted_branch into the HEAD. 2002-11-26 14:34 anthonybaxter * pydirector/pdnetworktwisted.py: mark hosts when they're down. 2002-11-26 14:06 anthonybaxter * pydirector/pdnetworktwisted.py: twisted now works. ! :) 2002-11-26 11:21 anthonybaxter * pydirector/: pdlogging.py, pdschedulers.py: logging cleanup. 2002-11-25 18:03 anthonybaxter * pydirector/: pdmain.py, pdnetwork.py, pdnetworktwisted.py: Basic code works - have to handle failure cases and interacting better with the scheduler next. 2002-11-25 17:32 anthonybaxter * pydirector/: pdmain.py, pdnetworkasyncore.py, pdnetworktwisted.py: checkpoint to save work 2002-11-25 17:32 anthonybaxter * pydirector/pdnetworktwisted.py: file pdnetworktwisted.py was initially added on branch twisted_branch. 2002-11-25 13:30 anthonybaxter * pydirector/: __init__.py, pdnetwork.py: pdnetwork now loads either twisted or asyncore versions. 2002-11-25 13:29 anthonybaxter * pydirector/pdnetworkasyncore.py: file pdnetworkasyncore.py was initially added on branch twisted_branch. 2002-11-25 13:29 anthonybaxter * pydirector/: pdnetwork.py, pdnetworkasyncore.py: renamed 2002-11-25 13:27 anthonybaxter * pydirector/: pdmain.py, pdnetwork.py: moved all asyncore stuff into pdnetwork.py, in preparation for twisted version. 2002-07-23 14:25 anthonybaxter * pydirector/pdnetwork.py (tags: twisted_root): even more defensiveness in the Listener. 2002-07-23 11:43 anthonybaxter * pydirector/: pdlogging.py (tags: twisted_root), pdmanager.py (tags: twisted_root), pdnetwork.py, pdschedulers.py (tags: twisted_root): more logging, with datestamps! 2002-07-17 17:35 anthonybaxter * TODO.txt: [no log message] 2002-07-17 15:16 anthonybaxter * pydirector/: pdadmin.py, pdconf.py, pdmain.py, pdmanager.py, pdschedulers.py, pdsortlist.py: first cut at refactoring the scheduler. 2002-07-17 15:16 anthonybaxter * pydirector/pdsortlist.py: file pdsortlist.py was initially added on branch scheduler-refactor-branch. 2002-07-17 14:53 anthonybaxter * pydirector/pdadmin.py (tags: twisted_root, scheduler-refactor-root): SO_REUSEADDR re-added. oops. 2002-07-08 11:46 anthonybaxter * TODO.txt: [no log message] 2002-07-08 10:53 anthonybaxter * README.txt, pydirector/__init__.py (tags: twisted_root, scheduler-refactor-root) (utags: release_0_0_4): 0.0.4 release 2002-07-08 10:52 anthonybaxter * TODO.txt, doc/ChangeLog (utags: release_0_0_4): [no log message] 2002-07-08 10:41 anthonybaxter * README.txt, TODO.txt, pydir.py (tags: release_0_0_4), pydirector/pdadmin.py (tags: release_0_0_4), pydirector/pdconf.py (tags: twisted_root, scheduler-refactor-root, release_0_0_4), pydirector/pdmain.py (tags: twisted_root, scheduler-refactor-root, release_0_0_4), pydirector/pdmanager.py (tags: scheduler-refactor-root, release_0_0_4): support multiple listen directives for a service. 2002-07-08 10:37 anthonybaxter * TODO.txt: async C module notes, upgrade ideas 2002-07-08 10:36 anthonybaxter * BUGS.txt (tags: release_0_0_4): [no log message] 2002-07-08 10:32 anthonybaxter * doc/: configure.txt, pythondirector.dtd, xmlreference.txt (utags: release_0_0_4): can now specify more than one 'listen' directive. update documentation. 2002-07-04 18:23 anthonybaxter * confex.xml (tags: release_0_0_4), doc/configure.txt, doc/pythondirector.dtd, doc/webapi.txt (tags: release_0_0_4), doc/xmlreference.txt: document the -> change. 2002-07-04 18:22 anthonybaxter * README.txt, doc/ChangeLog, TODO.txt: [no log message] 2002-07-04 14:09 anthonybaxter * pydirector/: pdadmin.py, pdconf.py: renamed 'client' to 'host' in config all config objects now support getFoo(name), getFoos(), and getFooNames() (where they have children) 2002-07-03 21:24 anthonybaxter * TODO.txt: [no log message] 2002-07-03 21:16 anthonybaxter * .cvsignore (tags: release_0_0_4), doc/ChangeLog, TODO.txt: [no log message] 2002-07-03 21:15 anthonybaxter * pydirector/: pdadmin.py, pdconf.py: implemented addUser/delUser. hooked in basic SSL support (from M2Crypto) for the admin web server. Not entirely complete yet, but sort-of-works. 2002-07-03 19:25 anthonybaxter * doc/webapi.txt: add needed delGroup command 2002-07-03 19:25 anthonybaxter * BUGS.txt, TODO.txt: [no log message] 2002-07-03 19:18 anthonybaxter * doc/ChangeLog: [no log message] 2002-07-03 19:17 anthonybaxter * pydirector/pdconf.py: der. fixed stupid typo. 2002-07-03 19:17 anthonybaxter * pydirector/: __init__.py, compareconf.py (tags: twisted_root, scheduler-refactor-root, release_0_0_4), micropubl.py (tags: twisted_root, scheduler-refactor-root, release_0_0_4), pdadmin.py, pdconf.py, pdlogging.py (tags: scheduler-refactor-root, release_0_0_4), pdmain.py, pdmanager.py, pdnetwork.py (tags: scheduler-refactor-root, release_0_0_4), pdschedulers.py (tags: scheduler-refactor-root, release_0_0_4): whitespace normalisation 2002-07-03 19:16 anthonybaxter * pydirector/pdconf.py: allow pdconfig.PDConfig to take either filename or string containing XML. 2002-07-03 19:15 anthonybaxter * pydirector/compareconf.py: module to compare an old and new config, and supply api calls to bring the old up to the new. 2002-07-03 18:15 anthonybaxter * TODO.txt: [no log message] 2002-07-03 18:11 anthonybaxter * pydirector/pdadmin.py: added Refresh and Auto-refresh to /running. 2002-07-03 17:56 anthonybaxter * pydirector/pdnetwork.py: missing newline on log_info() log messages. 2002-07-03 17:52 anthonybaxter * pydirector/pdschedulers.py: handle "no alive hosts" explicitly in schedulers. 2002-07-03 17:39 anthonybaxter * pydirector/: pdadmin.py, pdschedulers.py: added totals to innards of scheduler and to the running interface. 2002-07-03 17:39 anthonybaxter * BUGS.txt: notes about bugs I spot 2002-07-03 15:29 anthonybaxter * TODO.txt: https admin interface. 2002-07-03 15:28 anthonybaxter * pydirector/pdlogging.py: oops. flush() the log entry. 2002-07-03 15:24 anthonybaxter * setup.py (tags: release_0_0_4, release_0_0_3): missed a 0.0.3 checkin 2002-07-03 15:24 anthonybaxter * TODO.txt: more bits. 2002-07-03 11:35 anthonybaxter * doc/: README.txt (tags: release_0_0_4), reference.txt, xmlreference.txt: added docs/README.txt renamed reference to xmlreference.txt 2002-07-03 11:28 anthonybaxter * doc/webapi.txt: complete web api doco. 2002-07-03 10:49 anthonybaxter * doc/: configure.txt, pythondirector.dtd, reference.txt, schedulers.txt (tags: release_0_0_4): documentation. whoda thunk it. 2002-07-02 17:03 anthonybaxter * pydirector/__init__.py (tags: release_0_0_3): onwards to 0.0.3! 2002-07-02 17:03 anthonybaxter * README.txt (tags: release_0_0_3): on to 0.0.3! 2002-07-02 16:56 anthonybaxter * doc/ChangeLog (tags: release_0_0_3): [no log message] 2002-07-02 16:55 anthonybaxter * TODO.txt (tags: release_0_0_3): delHost implemented. 2002-07-02 16:55 anthonybaxter * pydirector/pdadmin.py (tags: release_0_0_3): added delHost support. 2002-07-02 16:55 anthonybaxter * pydirector/pdschedulers.py (tags: release_0_0_3): delHost takes an argument 'activegroup'. If true, you can't delete the last host in the scheduler. 2002-07-02 16:39 anthonybaxter * confex.xml (tags: release_0_0_3): wee. more sample stuff. 2002-07-02 16:29 anthonybaxter * TODO.txt: 0.1 plans. 2002-07-02 15:59 anthonybaxter * README.txt: more API. 2002-07-02 15:58 anthonybaxter * TODO.txt: complete the API list. 2002-07-02 15:58 anthonybaxter * pydirector/pdadmin.py: added method running.xml running.txt is more sane. running.xml and config.xml are now access 'Read' 2002-07-02 15:57 anthonybaxter * pydirector/pdconf.py (tags: release_0_0_3): add PDAdmin.getUsers() to retrieve full user list. 2002-07-01 18:52 anthonybaxter * TODO.txt, confex.xml, pydirector/pdadmin.py, pydirector/pdconf.py, pydirector/pdlogging.py (tags: release_0_0_3), pydirector/pdnetwork.py (tags: release_0_0_3): added a bunch of new logging support to the code. logging now all goes to a single central file. many many logging entries need to be written. 2002-07-01 16:52 anthonybaxter * TODO.txt: more todo 2002-07-01 16:51 anthonybaxter * pydirector/pdnetwork.py: socket.gaierror is a 2.2-ism. Handle it's absence for 2.1. Todo: what does 'unknown host' raise under 2.1. 2002-07-01 15:51 anthonybaxter * .cvsignore (tags: release_0_0_3): more setup.py ignore 2002-07-01 15:51 anthonybaxter * doc/ChangeLog: [no log message] 2002-07-01 15:51 anthonybaxter * MANIFEST.in (tags: release_0_0_4, release_0_0_3): added ChangeLog to dist. 2002-07-01 15:49 anthonybaxter * TODO.txt: more notes to self. 2002-07-01 15:47 anthonybaxter * .cvsignore: setup.py stuff 2002-07-01 15:47 anthonybaxter * MANIFEST.in: added setup.py input file. 2002-07-01 15:47 anthonybaxter * pydirector/: __init__.py, pdadmin.py: moved version to package top. 2002-07-01 15:44 anthonybaxter * doc/: ChangeLog.preSF, OldChangelog.txt (tags: release_0_0_4, release_0_0_3): renamed 2002-07-01 15:40 anthonybaxter * setup.py: simple setup.py 2002-07-01 15:34 anthonybaxter * pydir.py (tags: release_0_0_3): python2.1 compat fixes. 2002-07-01 15:33 anthonybaxter * pydirector/: __init__.py, micropubl.py (tags: release_0_0_3), pdadmin.py, pdconf.py, pdmain.py (tags: release_0_0_3), pdnetwork.py: Py2.1 compatibility fixes. 2002-07-01 15:33 anthonybaxter * pydirector/pdschedulers.py: renamed getHosts to getHostNames. Made py2.1 compat. 2002-07-01 15:30 anthonybaxter * pydirector/pdmanager.py (tags: release_0_0_3): made the pdmanager bad host re-add work again (woops!) 2002-07-01 15:05 anthonybaxter * pydir.py, pydirector/pdmain.py: broke main driver object off into seperate pdmain module. driver is now trivial. 2002-07-01 14:59 anthonybaxter * TODO.txt, micropubl.py, pdadmin.py, pdconf.py, pdmanager.py, pdnetwork.py, pdschedulers.py, pydir.py, pydirector/.cvsignore (tags: twisted_root, scheduler-refactor-root, release_0_0_4, release_0_0_3), pydirector/__init__.py, pydirector/micropubl.py, pydirector/pdadmin.py, pydirector/pdconf.py, pydirector/pdmanager.py, pydirector/pdnetwork.py, pydirector/pdschedulers.py: Packaged up the code. Everything but 'pydir.py' is now in the pydirector package. 2002-07-01 13:38 anthonybaxter * confex.xml: add *(documentation). 2002-07-01 13:28 anthonybaxter * TODO.txt: should there be a shutdown mode in the web interface? 2002-07-01 13:22 anthonybaxter * micropubl.py, pdadmin.py, pdconf.py, pdmanager.py, doc/balance.fig (tags: release_0_0_4, release_0_0_3): big refactoring of the web interface. using a tiny publisher mixin (babybobo, or micropublisher) to extract args and do that sort of magic. not completely happy with it for now, but it's Good Enough for now. 2002-06-25 14:00 anthonybaxter * TODO.txt, pdadmin.py, pdschedulers.py, pydir.py: addHost works. added a whole lotta CSS guff. running is now pretty with form bits and stuff. 2002-06-21 16:39 anthonybaxter * TODO.txt: note adding sticky schedulers. 2002-06-21 14:18 anthonybaxter * confex.xml: made the username more .. sample-ish. 2002-06-21 13:56 anthonybaxter * .cvsignore: sssh. 2002-06-21 13:54 anthonybaxter * README.txt, TODO.txt, confex.xml, pdadmin.py, pdconf.py, pdmanager.py, pdnetwork.py, pdschedulers.py, pydir.py, doc/ChangeLog.preSF, doc/LICENSE.txt (tags: release_0_0_4, release_0_0_3) (utags: initial_sf_checkin): Initial SF checkin. 2002-06-21 13:54 anthonybaxter * README.txt, TODO.txt, confex.xml, pdadmin.py, pdconf.py, pdmanager.py, pdnetwork.py, pdschedulers.py, pydir.py, doc/ChangeLog.preSF, doc/LICENSE.txt: Initial revision pydirector-1.0.0/doc/LICENSE.txt0000644000026300012320000000215307504521545016712 0ustar anthonytech00000000000000Copyright (c) 2002 ekit.com Inc (http://www.ekit-inc.com/) and Anthony Baxter Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. pydirector-1.0.0/doc/OldChangelog.txt0000644000026300012320000001056307507766117020173 0ustar anthonytech00000000000000This changelog is for pre-sourceforge changes to the code. Can't be bothered doing the tarball upload to get the full CVS history onto SF. 2002-06-20 11:31 anthony * TODO.txt, pdadmin.py, pdnetwork.py: added running.txt to interface. 2002-06-19 16:14 anthony * pdadmin.py: slight goof in the display of bad hosts. 2002-06-19 00:59 anthony * pdadmin.py: stylesheet new bits. 2002-06-19 00:46 anthony * TODO.txt, pdadmin.py, pdmanager.py, pdnetwork.py, pdschedulers.py: /running now subsumes the 'status' page 2002-06-19 00:08 anthony * pdschedulers.py, pydir.py: re-use the schedulers, don't keep recreating them. 2002-06-18 23:56 anthony * pdadmin.py: added message box. 2002-06-18 23:44 anthony * README.txt, pdadmin.py: cleaned up interface of config. made redirection work. 2002-06-18 22:55 anthony * confex.xml, pdadmin.py, pdconf.py, pdmanager.py, pdschedulers.py, pydir.py: renamed the various ld* and LD* to pd* and PD* 2002-06-18 22:48 anthony * README.txt, TODO.txt, confex.xml, pdadmin.py, pdconf.py, pdschedulers.py: added LDHost object. Hosts now have a name and an ip. 2002-06-18 21:56 anthony * confex.xml, pdadmin.py, pydir.py: enableGroup works from the webpage through to the backend. Yay. 2002-06-18 21:22 anthony * pdadmin.py, pdconf.py: add getGroups() to ldconf.LDService. enable new /config call in admin interface. 2002-06-18 21:11 anthony * ld.conf: old conf file. byebye. 2002-06-18 20:57 anthony * TODO.txt, pdadmin.py: renamed config.txt to config.xml 2002-06-18 20:44 anthony * pdconf.py, pdschedulers.py, pydir.py: refactor services and groups into dictionaries. clean up various code that used them. 2002-06-18 14:50 anthony * TODO.txt, pdadmin.py, pdconf.py, pdmanager.py, pdnetwork.py, pdschedulers.py, pydir.py: whitespace normalisation. 2002-06-18 14:48 anthony * pdschedulers.py: BaseScheduler.newHost can take ('host',port) or 'host:port' whitespace normalisation. 2002-06-18 13:25 anthony * pdmanager.py: oops. missed one spot in the change of director.listeners from list to dictionary. 2002-06-18 13:22 anthony * .cvsignore: [no log message] 2002-06-18 13:21 anthony * schedulers.py: renamed to ldschedulers.py 2002-06-18 13:21 anthony * pydir.py: split network bits into seperate module. pychecker cleanups. refactored the PythonDirector.__init__ method. added recreateScheduler to rebuild the scheduler for a listener. only use a single schedule manager now to manage all schedulers. 2002-06-18 13:19 anthony * pdnetwork.py: split network bits (Listener, Sender, Receiver) into seperate module. pychecker cleanups. 2002-06-18 13:18 anthony * pdmanager.py, pdschedulers.py: split manager stuff into seperate module. renamed schedulers to ldschedulers. pychecker cleanups. 2002-06-18 13:13 anthony * pdconf.py: pychecker fix. 2002-06-18 12:27 anthony * pdadmin.py: print the service name pychecker cleanups 2002-06-17 18:56 anthony * README.txt: note about webadmin 2002-06-17 18:54 anthony * pdadmin.py: index.html, status, status.txt, config.txt, config all done. access control implemented. 2002-06-17 18:54 anthony * TODO.txt: sketching out API. 2002-06-17 18:50 anthony * pydir.py: only start admin if requested. 2002-06-17 18:50 anthony * pdconf.py: handle the section (and users) save the config DOM for display purposes. 2002-06-17 14:15 anthony * README.txt, TODO.txt, conf.py, confex.xml, pdadmin.py, pdconf.py, pydir.py, schedulers.py: scheduler is now a property of 'group', not service. first bits of web admin are there - not hooked into the xml config yet, tho. leastconns is there, and works. added some docs. 2002-06-13 19:15 anthony * .cvsignore, conf.py, confex.xml, ld.conf, ld.py, pydir.py, schedulers.py: new config, scheduler stuff. 2002-06-13 19:14 anthony * ld.py: schedulers, config to seperate modules. 2002-06-13 19:14 anthony * ld.py: more changes. 2002-06-11 18:44 anthony * ld.py: handle connect-time errors (such as host unknown). refactor out bits from the __init__ method. 2002-06-11 18:26 anthony * ld.py: made the sender call receiver.close_when_done() - magic methods, aaaah. 2002-06-11 17:58 anthony * ld.conf: sample config file. 2002-06-11 17:58 anthony * ld.py: current working set. 2002-06-07 16:52 anthony * ld.py: Initial revision 2002-06-07 16:52 anthony * ld.py: initial. pydirector-1.0.0/doc/README.txt0000644000026300012320000000126607510452377016574 0ustar anthonytech00000000000000Documentation for Python director --------------------------------- The docs directory contains the following files: configure.txt - Basic configuration guide to the python director schedulers.txt - Information about the different schedulers available in python director. xmlreference.txt - a reference guide for the XML config file format. webapi.txt - documentation of all web API calls. pythondirector.dtd - XML DTD for the python director config file XML. balance.fig - xfig image of "what is a load balancer" LICENSE.txt - license for python director. ChangeLog - current GNU-style Changelog. OldChangelog.txt - Changelog prior to first SF import. pydirector-1.0.0/doc/balance.fig0000644000026300012320000000307107507745410017145 0ustar anthonytech00000000000000#FIG 3.2 Landscape Center Inches Letter 100.00 Single -2 1200 2 6 1275 3150 2625 3900 2 4 0 1 0 7 50 0 -1 0.000 0 0 7 0 0 5 2625 3900 2625 3150 1275 3150 1275 3900 2625 3900 4 0 0 50 0 18 12 0.0000 4 135 660 1620 3592 CLIENT\001 -6 6 7125 1425 8475 2175 2 4 0 1 0 7 50 0 -1 0.000 0 0 7 0 0 5 8475 2175 8475 1425 7125 1425 7125 2175 8475 2175 4 0 0 50 0 18 12 0.0000 4 135 765 7417 1867 SERVER\001 -6 6 7125 3375 8475 4125 2 4 0 1 0 7 50 0 -1 0.000 0 0 7 0 0 5 8475 4125 8475 3375 7125 3375 7125 4125 8475 4125 4 0 0 50 0 18 12 0.0000 4 135 765 7417 3817 SERVER\001 -6 6 7125 4425 8475 5175 2 4 0 1 0 7 50 0 -1 0.000 0 0 7 0 0 5 8475 5175 8475 4425 7125 4425 7125 5175 8475 5175 4 0 0 50 0 18 12 0.0000 4 135 765 7417 4867 SERVER\001 -6 6 7125 2400 8475 3150 2 4 0 1 0 7 50 0 -1 0.000 0 0 7 0 0 5 8475 3150 8475 2400 7125 2400 7125 3150 8475 3150 4 0 0 50 0 18 12 0.0000 4 135 765 7417 2842 SERVER\001 -6 6 3750 2850 5625 4275 6 4200 3375 5175 3750 4 0 0 50 0 18 12 0.0000 4 135 780 4297 3525 PYTHON\001 4 0 0 50 0 18 12 0.0000 4 135 975 4200 3750 DIRECTOR\001 -6 2 4 0 2 0 7 50 0 -1 0.000 0 0 7 0 0 5 5550 4200 5550 2925 3825 2925 3825 4200 5550 4200 -6 2 1 0 2 0 7 50 0 -1 0.000 0 0 -1 1 0 2 1 1 2.00 120.00 240.00 2625 3450 3825 3450 2 1 1 2 0 7 50 0 -1 6.000 0 0 -1 1 0 2 1 1 2.00 120.00 240.00 5550 3150 7125 1875 2 1 1 2 0 7 50 0 -1 6.000 0 0 -1 1 0 2 1 1 2.00 120.00 240.00 5550 3375 7125 2775 2 1 1 2 0 7 50 0 -1 6.000 0 0 -1 1 0 2 1 1 2.00 120.00 240.00 5550 3600 7125 3675 2 1 1 2 0 7 50 0 -1 6.000 0 0 -1 1 0 2 1 1 2.00 120.00 240.00 5550 3825 7125 4800 pydirector-1.0.0/doc/configure.txt0000644000026300012320000000515407741215362017615 0ustar anthonytech00000000000000Configuration of Python Director -------------------------------- A load balancer sits between client systems and a number of server systems. Client TCP connections connect to the loadbalancer, and it in turn distributes the connections to the various backend servers. A Python Director config consists of a number of XML directives. One of the simplest possible configs would be This configuration would produce a server listening on all addresses (0.0.0.0), port 80, and would alternate hits to the backend servers 192.168.10.1 and 192.168.10.2. The configuration file is defined completely in doc/reference.txt or in pythondirector.dtd for the XML-savvy. A configuration consists of one or more 'service', plus an optional 'admin' and 'logging' section. The 'service' sections describe a service to be handled. Inside this section is a listen directive that specifies the port and address the service will accept connections on. The meat of the service section is one or more 'group' sections. These are a group of backend servers and the description of how to distribute hits between them. Only one group can be active at a time. You'd use multiple groups, for instance, to specify a set of main hosts, and a set of backup hosts to use when the main hosts are undergoing maintenance. You don't need to specify more than one group, but it is useful. Finally, there should be an 'enable' directive that specifies which group starts off enabled. The admin section controls the administrative web interface. If you don't have an admin section, there will be no admin web interface started - as a result, you'll have to re-start the python director to make any changes. The admin directive takes a listen argument to specify what address to listen for connections. Use either '*' or '0.0.0.0' to mean 'listen on all addresses'. Inside the admin section are one or more 'user' entries. These have attributes 'name' (the login name), 'password' (the unix crypt password, see documentation for python module 'crypt'), and 'access'. Access can be 'full' or 'readonly'. A readonly user can view the current state, but cannot make changes to the system. The final section in the config is also optional, and specifies a log file target. If this section is not specified, the python director will write log entries to stderr. pydirector-1.0.0/doc/pythondirector.dtd0000644000026300012320000000205207741215153020635 0ustar anthonytech00000000000000 pydirector-1.0.0/doc/schedulers.txt0000644000026300012320000000267407741214210017770 0ustar anthonytech00000000000000Python director schedulers -------------------------- The scheduler controls how client hits are distributed amongst the backend servers. They're typically a very small piece of code - see the file pdschedulers.py in the package for the current schedulers. Current schedulers ================== random For each hit, randomly choose from available servers roundrobin Distribute hits in a round-robin fashion; so for three servers A, B, C hits would go A B C A B C A B C ... leastconns Send the hit to the backend server with the least number of current open connections. Assuming that the server holds the TCP connections open while processing, this will often lead to a quite effective load balancing, as the faster servers will process and finish connections faster than the slower servers. leastconnsrr Send the hit to the backend server with the least number of current connections. If multiple machines have the same number of open connections, send to the least recently used. Future work =========== The following are not new scheduler types, but options for the existing schedulers. sticky Prefer the same server address for a given client address. Useful when the backend servers have some form of per-client caching. weighted Explicitly provide weighting for different servers. E.g. if the default weighting is 1, then a server with a weighting of 0.5 will only receive half as many hits as the default. pydirector-1.0.0/doc/webapi.txt0000644000026300012320000000643007511003025017063 0ustar anthonytech00000000000000This is a list of all current and planned web calls. Ones marked with 'unimplemented' in the text are, well, unimplemented. Ones marked with [maybe] are tentative and may not be implemented. Information/Configuration ------------------------- - running current configuration and status of the PD (HTML) this page is the GUI for interactively editing the pydir config. - running.txt simple text view of the current running config. suitable for simple reading by programs - config.xml - initial boot-time configuration of the PD (xml) - running.xml current running config of the PD (xml) - [maybe] config / config.txt ? is there a use for text view of boot time config? not really. unimplemented. - [maybe] config vs. running a display of what's different between boot time and current config. unimplemented. - [maybe] reload config, making changes some sort of smart reload that painlessly reloads the config and switches in the various new bits without interruption. will probably do this as a seperate program that uses the webapi to do this, instead. unimplemented. Host management --------------- - addHost?service=NNN&group=NNN&name=NNNN&ip=NNN:n add a new host to the group of a service - delHost?service=NNN&group=NNN&name=NNN remove a host from the group of a service Note that this will not let you remove all hosts from the enabled group. - delAllHosts?service=NNN&group=NNN remove all hosts from the group of a service. unimplemented Note that this will not let you remove all hosts from the enabled group. Group management ---------------- - enableGroup?service=NNN&group=NNN - switch the currently enabled group. Note that this will not affect any in-progress connections. The existing enabled group. - changeScheduler?service=NNN&group=NNN&scheduler=NNN change the scheduler of a group. This will not allow you to change the scheduer of an enabled group. unimplemented. User management --------------- - addUser?name=NNNN&password=NNNN&access=NNNN add a new user to the web admin interface. unimplemented. - delUser?name=NNNN delete a user from the web admin interface. unimplemented. Service management ------------------ - newService?service=NNN&listen=NNN add a new service. unimplemented. - newGroup?service=NNN&group=NNN&scheduler=NNN add a new group to a service. unimplemented. - delGroup?service=NNN&group=NNN remove a new group from a service. can't remove the active group. unimplemented. Miscellaneous ------------- - closeLog close and re-open the log file. unimplemented. - retryHost?service=NNN&group=NNN&host=NNN force the manager thread to re-add a host now. unimplemented. - hostAdminDown?service=NNN&group=NNN&name=NNN mark a host as 'administratively down'. This disables it, without removing it from the configuration. unimplemented. - hostAdminUp?service=NNN&group=NNN&name=NNN mark a host as 'administratively up'. This re-enables it, assuming it's been previously marked 'down'. unimplemented. - [maybe] shutdown shut down the python director. unimplemented. pydirector-1.0.0/doc/xmlreference.txt0000644000026300012320000000521407512156630020306 0ustar anthonytech00000000000000Configuration of Python Director -------------------------------- XML Reference ============= A Python Director config consists of a number of XML directives. - the top level XML node. Attributes: No attributes. Children: Must contain one or more nodes. Can contain an optional node. Can contain an optional node. Other notes: None - a load balancer service. Child of: Attributes: name - the name of this service. A text label. Children: Must contain one or more nodes. Must contain one or more nodes. Must contain one node (after the nodes) Other notes: None - listen on a network port Child of: Attributes: ip - IP address to listen on, in format host:port Children: No children. Other notes: A service can listen on multiple host:ip ports. - a group of backend servers Child of: Attributes: name - name of this group. Text label, should be unique in the service. scheduler - name of the scheduler to use for this group. Full list in doc/schedulers.txt Children: Should contain one or more nodes. Other notes: None - a backend server Child of: Attributes: name - name of this host ip - IP address to listen on, in format host:port Children: No children. Other notes: This used to be called . Should be no references to the old name left anywhere. - specifies the enabled group Child of: Attributes: group - the group name that should start up enabled Children: No children. Other notes: No reason this couldn't be optional - the first/only group would be chosen in that case. - administrative web interface Child of: Attributes: listen - IP address to run admin web interface on, in host:port format Children: Should contain one or more nodes Other notes: Should the 'listen' be a sub-node, like for service? Could you have multiple admin nodes, to listen on multiple ports with different user DBs? No reason why not... - user of administrative web interface Child of: Attributes: name - the user's login name password - the unix crypt() password of the user access - what rights the user has. One of "full" or "readonly" Children: No children. Other notes: Source users from somewhere else? - logging for python director Child of: Attributes: file - log file path Children: No children. Other notes: None pydirector-1.0.0/pydirector/0000755000026300012320000000000010157566420016504 5ustar anthonytech00000000000000pydirector-1.0.0/pydirector/__init__.py0000644000026300012320000000034210157565473020623 0ustar anthonytech00000000000000# # Copyright (c) 2002-2004 ekit.com Inc (http://www.ekit-inc.com) # and Anthony Baxter # # $Id: __init__.py,v 1.13 2004/12/14 13:31:39 anthonybaxter Exp $ # # Package init file. Version = '1.0.0' pydirector-1.0.0/pydirector/compareconf.py0000644000026300012320000001227210110565064021346 0ustar anthonytech00000000000000# # Very straightforward (brutally so) code to compare to # configurations and supply a list of web api commands to # make the first look like the second. # # This code prefers simplicity to elegance. I want something # I can read clearly # # $Id: compareconf.py,v 1.2 2002/07/03 09:17:23 anthonybaxter Exp $ # class DiffError(Exception): pass def diffXML(oldxml, newxml): from pydirector.pdconf import PDConfig ret = [] oldconf = PDConfig(xml=oldxml) newconf = PDConfig(xml=newxml) ret.extend( compareServices(oldconf, newconf) ) ret.extend( compareAdmin(oldconf, newconf) ) ret.extend( compareLogging(oldconf, newconf) ) return ret def compareServices(oldconf, newconf): ret = [] oldservices = oldconf.services.keys() oldservices.sort() newservices = newconf.services.keys() newservices.sort() if oldservices != newservices: # would we want to enable new services this way? raise DiffError, "can't handle different services list" for serviceName in oldservices: os = oldconf.services[serviceName] ns = newconf.services[serviceName] ret.extend(compareListeners(serviceName, os, ns)) ret.extend(compareGroups(serviceName, os, ns)) ret.extend(compareEnable(serviceName, os, ns)) return ret def compareGroups(serviceName, oldservice, newservice): ret = [] oldgroups = oldservice.groups.keys() oldgroups.sort() newgroups = newservice.groups.keys() newgroups.sort() if oldgroups != newgroups: # maybe change this after adding 'newGroup' to the api raise DiffError, \ "can't handle different groups list for %s"%serviceName for groupName in oldgroups: og = oldservice.groups[groupName] ng = newservice.groups[groupName] ret.extend(compareHosts(serviceName, groupName, og, ng)) if og.scheduler != ng.scheduler: ret.append(("changeScheduler", {'service' : serviceName, 'group' : groupName, 'scheduler': ng.scheduler})) return ret def compareHosts(serviceName, groupName, oldgroup, newgroup): ret = [] oldhosts = oldgroup.hosts.keys() oldhosts.sort() newhosts = newgroup.hosts.keys() newhosts.sort() allhosts = mergelists(oldhosts,newhosts) for host in allhosts: if host in oldhosts and host in newhosts: continue elif host in newhosts: newhost = newgroup.getHost(host) ret.append(("addHost", {'service' : serviceName, 'group' : groupName, 'ip' : newhost.ip, 'name' : newhost.name })) elif host in oldhosts: oldhost = oldgroup.getHost(host) ret.append(("delHost", {'service' : serviceName, 'group' : groupName, 'ip' : oldhost.ip })) else: raise DiffError, "what the hey?" return ret def mergelists(l1, l2): d = {} for i in l1+l2: d[i] = 1 l = d.keys() l.sort() return l def compareEnable(serviceName, oldservice, newservice): ret = [] # to do. or not? do we care? return ret def compareListeners(serviceName, oldservice, newservice): ret = [] # how do we handle this? no API for listener changes return ret def compareAdmin(oldconf, newconf): ret = [] if oldconf.admin is None and newconf.admin is None: # both empty return ret if oldconf.admin is None or newconf.admin is None: raise DiffError, "can't handle enabling/disabling admin" else: # we should also handle looking at the listen (and secure, # when added). needs web api commands. compareUsers(oldconf.admin.userdb, newconf.admin.userdb) return ret def compareUsers(olduserdb, newuserdb): ret = [] oldusers = olduserdb.keys() oldusers.sort() newusers = newuserdb.keys() newusers.sort() allusers = mergelists(oldusers,newusers) for user in allusers: if user in oldusers and user in newusers: ou = olduserdb.get(user) nu = newuserdb.get(user) if ou.password != nu.password or ou.access != nu.access: # user has changed! ret.append(("delUser", {'name' : user })) ret.append(("addUser", {'name' : user, 'password' : nu.password, 'access' : nu.access })) elif user in newusers: nu = newuserdb.get(user) ret.append(("addUser", {'name' : user, 'password' : nu.password, 'access' : nu.access })) elif user in oldusers: ret.append(("delUser", {'name' : user })) else: raise DiffError, "what the hey - user %s?"%user return ret def compareLogging(oldconf, newconf): ret = [] # should handle this - changing logfile location? return ret if __name__ == "__main__": import sys ret = diffXML(open(sys.argv[1]).read(), open(sys.argv[2]).read()) ret.sort() for r in ret: print r pydirector-1.0.0/pydirector/micropubl.py0000644000026300012320000000512710157565473021066 0ustar anthonytech00000000000000# # Copyright (c) 2002-2004 ekit.com Inc (http://www.ekit-inc.com) # and Anthony Baxter # # $Id: micropubl.py,v 1.5 2004/12/14 13:31:39 anthonybaxter Exp $ # import sys if sys.version_info < (2,2): class object: pass # a.k.a babybobo. A very small and limited object publisher. # where possible, it's not tied to any particular mechanism # (e.g. web) def patchArgs(argdict): " fairly simple hack. all args are lower cased " n = {} for k,v in argdict.items(): n[k.lower()] = v return n class uPublisherError(Exception): pass class NotFoundError(uPublisherError): pass class MissingArgumentError(uPublisherError): pass class UnhandledArgumentError(uPublisherError): pass class AccessDenied(uPublisherError): pass class MicroPublisher(object): """ a small object publisher """ published_prefix = "publ_" def publish(self, method, args, user): args = patchArgs(args) if not hasattr(self, '%s%s'%(self.published_prefix, method)): raise NotFoundError, "method %s not found"%method fnarg = getattr(self, '%s%s'%(self.published_prefix, method)) self.checkArgs(fnarg, args) # check that the user has correct privs if not user.checkAccess(fnarg, args): raise AccessDenied, "userobject denied access" # finally, call the method fnarg(**args) def checkArgs(self, fn, args): from inspect import getargspec arglist, vaarg, kwarg, defargs = getargspec(fn.im_func) if arglist[0] == "self": arglist = arglist[1:] arglist.reverse() if defargs: argsneeded = arglist[len(defargs):] else: argsneeded = arglist[:] #print arglist, argsneeded, defargs # first, check for missing required args missing = [] for a in argsneeded: if not args.has_key(a): missing.append(a) if missing: raise MissingArgumentError, \ "missing argument(s): %s"%(', '.join(missing)) # now, check if it can handle unknown args (if any provided) if 0: #not kwarg: provided = args.keys() for a in arglist: provided.remove(a) if provided: raise UnhandledArgumentError, \ "Additional unhandled argument(s) %s"%(', '.join(provided)) # all ok to continue. additional checks here self.checkPublisherAccess(fn, args) return def checkPublisherAccess(self, fn, args): " override in subclass if desired " return pydirector-1.0.0/pydirector/pdadmin.py0000644000026300012320000005531510157565473020512 0ustar anthonytech00000000000000# # Copyright (c) 2002-2004 ekit.com Inc (http://www.ekit-inc.com) # and Anthony Baxter # # $Id: pdadmin.py,v 1.16 2004/12/14 13:31:39 anthonybaxter Exp $ # import sys if sys.version_info < (2,2): class object: pass import threading, BaseHTTPServer, SocketServer, urlparse, re, urllib import socket, time, sys, traceback, time import micropubl from pydirector import Version, pdlogging try: from M2Crypto import SSL except ImportError: SSL = None def start(adminconf, director): AdminClass.director = director AdminClass.config = adminconf AdminClass.starttime = time.time() if adminconf.secure == 'yes' and SSL is not None: tcps = PDTCPServerSSL(adminconf.listen, AdminClass, get_ssl_context()) else: tcps = PDTCPServer(adminconf.listen, AdminClass) at = threading.Thread(target=tcps.serve_forever) at.setDaemon(1) at.start() class PDTCPServerBase: allow_reuse_address = 1 def handle_error(self, request, client_address): "overridden from SocketServer.BaseServer" nil, t, v, tbinfo = pdlogging.compact_traceback() pdlogging.log("ADMIN(Exception) %s - %s: %s %s\n"% (time.ctime(time.time()), t,v,tbinfo)) class PDTCPServer(SocketServer.ThreadingTCPServer, PDTCPServerBase): allow_reuse_address = 1 if SSL is not None: class PDTCPServerSSL(SSL.ThreadingSSLServer, PDTCPServerBase): allow_reuse_address = 1 def __init__(self, server_addr, handler, ssl_ctx): SSL.ThreadingSSLServer.__init__(self, server_addr, handler, ssl_ctx) self.server_name = server_addr[0] self.server_port = server_addr[1] def finish(self): self.request.set_shutdown(SSL.SSL_RECEIVED_SHUTDOWN | SSL.SSL_SENT_SHUTDOWN) self.request.close() def get_ssl_context(): from M2Crypto import Rand Rand.load_file('randpool.dat', -1) ctx = init_context('sslv23', 'server.pem', 'ca.pem', \ SSL.verify_none) #SSL.verify_peer | SSL.verify_fail_if_no_peer_cert) ctx.set_tmp_dh('dh1024.pem') Rand.save_file('randpool.dat') return ctx def init_context(protocol, certfile, cafile, verify, verify_depth=10): ctx=SSL.Context(protocol) ctx.load_cert(certfile) ctx.load_client_ca(cafile) ctx.load_verify_info(cafile) ctx.set_verify(verify, verify_depth) ctx.set_allow_unknown_ca(1) ctx.set_session_id_ctx('https_srv') ctx.set_info_callback() return ctx class AdminClass(BaseHTTPServer.BaseHTTPRequestHandler, micropubl.MicroPublisher): server_version = "pythondirector/%s"%Version director = None config = None starttime = None published_prefix = "pdadmin_" def getUser(self, authstr): from base64 import decodestring type,auth = authstr.split() if type.lower() != 'basic': return None auth = decodestring(auth) user,pw = auth.split(':',1) userObj = self.config.getUser(user) if not ( userObj and userObj.checkPW(pw) ): # unknown user or incorrect pw return None else: return userObj def unauth(self, why): self.send_response(401) self.send_header('WWW-Authenticate', 'basic realm="python director"') self.wfile.write("

Unauthorised

\n") def header(self, html=1, refresh=''): self.send_response(200) if html: self.send_header("Content-type", "text/html") else: self.send_header("Content-type", "text/plain") self.end_headers() if html: W = self.wfile.write W("""python director """) if refresh: W(''%refresh) W("""""") W("""
Python Director version %s, running on host %s.
"""%(self.server_version, socket.gethostname())) def footer(self, message=''): W = self.wfile.write W(""" """) if message: message = urllib.unquote(message) W("""

%s

"""%message) W("""\n\n""") def redir(self, url): self.send_response(302) self.send_header("Location", url) self.end_headers() def action_done(self, mesg): self.redir('/running?resultMessage=%s'%urllib.quote(mesg)) def do_GET(self): try: self.do_request() except: self.log_exception() def do_request(self): #print "URL",self.path h,p,u,p,q,f = urlparse.urlparse(self.path) authstr = self.headers.get('Authorization','') #print "authstr", authstr if authstr: user = self.getUser(authstr) if not (authstr and user): self.unauth(why='no valid auth') return if u == "/": u = 'index_html' args = dictify(q) if u.startswith("/"): u = u[1:] u = re.sub(r'\.', '_', u) try: self.publish(u, args, user=user) except micropubl.NotFoundError: self.send_response(404) self.send_header("Content-type", "text/html") self.end_headers() self.wfile.write("no such URL") except micropubl.AccessDenied: self.unauth('insufficient privileges') return except micropubl.uPublisherError: self.send_response(500) self.send_header("Content-type", "text/html") self.end_headers() self.wfile.write("

error:

") e,v,t = sys.exc_info() self.wfile.write("%s %s\n
"%(e,v))
            self.wfile.write("\n".join(traceback.format_tb(t)))
            self.wfile.write("
\n") def pdadmin_pydirector_css(self, Access='Read'): self.header(html=0) self.wfile.write(PYDIR_CSS) def pdadmin_index_html(self, Access='Read'): self.header(html=1) self.wfile.write("""

Python Director version %s, running on %s

Running since %s

"""%(self.server_version, socket.gethostname(), time.ctime(self.starttime))) self.footer() def pdadmin_running_xml(self, verbose=0, Access='Read'): from xml.dom.minidom import Document self.header(html=0) W = self.wfile.write conf = self.director.conf doc = Document() top = doc.createElement("pdconfig") doc.appendChild(top) for service in conf.getServices(): top.appendChild(doc.createTextNode("\n ")) serv = doc.createElement("service") serv.setAttribute('name', service.name) top.appendChild(serv) for l in service.listen: serv.appendChild(doc.createTextNode("\n ")) lobj = doc.createElement("listen") lobj.setAttribute('ip', l) serv.appendChild(lobj) groups = service.getGroups() for group in groups: serv.appendChild(doc.createTextNode("\n ")) sch = self.director.getScheduler(service.name, group.name) xg = doc.createElement("group") xg.setAttribute('name', group.name) xg.setAttribute('scheduler', sch.schedulerName) serv.appendChild(xg) stats = sch.getStats(verbose=verbose) hosts = group.getHosts() hdict = sch.getHostNames() counts = stats['open'] ahosts = counts.keys() # ahosts is now a list of active hosts # now add disabled hosts. for k in stats['bad'].keys(): ahosts.append('%s:%s'%k) ahosts.sort() for h in ahosts: xg.appendChild(doc.createTextNode("\n ")) xh = doc.createElement("host") xh.setAttribute('name', hdict[h]) xh.setAttribute('ip', h) xg.appendChild(xh) xg.appendChild(doc.createTextNode("\n ")) serv.appendChild(doc.createTextNode("\n ")) eg = service.getEnabledGroup() xeg = doc.createElement("enable") xeg.setAttribute("group", eg.name) serv.appendChild(xeg) serv.appendChild(doc.createTextNode("\n ")) top.appendChild(doc.createTextNode("\n ")) # now the admin block admin = self.director.conf.admin if admin is not None: xa = doc.createElement("admin") xa.setAttribute("listen", "%s:%s"%admin.listen) top.appendChild(xa) for user in admin.getUsers(): xa.appendChild(doc.createTextNode("\n ")) xu = doc.createElement("user") xu.setAttribute("name", user.name) xu.setAttribute("password", user.password) xu.setAttribute("access", user.access) xa.appendChild(xu) xa.appendChild(doc.createTextNode("\n ")) top.appendChild(doc.createTextNode("\n ")) # finally, the logging section (if set) logger = pdlogging.Logger if logger.logfile is not None: xl = doc.createElement("logging") xl.setAttribute("file", logger.logfile) top.appendChild(xl) # final newline top.appendChild(doc.createTextNode("\n")) # and spit out the XML self.wfile.write(doc.toxml()) def pdadmin_running_txt(self, verbose=0, Access='Read'): self.header(html=0) W = self.wfile.write conf = self.director.conf for service in conf.getServices(): eg = service.getEnabledGroup() for l in service.listen: W('service %s %s %s\n'%(service.name, l, eg.name)) groups = service.getGroups() for group in groups: sch = self.director.getScheduler(service.name, group.name) stats = sch.getStats(verbose=verbose) hosts = group.getHosts() hdict = sch.getHostNames() if group is eg: klass = 'enabled' else: klass = 'inactive' W('group %s %s\n'%(group.name, klass)) counts = stats['open'] k = counts.keys() k.sort() # k is now a list of hosts in the opencount stats for h in k: W("host %s %s "%(hdict[h], h)) if counts.has_key(h): W("%s -\n"%counts[h]) else: W("- -\n") bad = stats['bad'] for k in bad: host = '%s:%s'%k W("disabled %s %s"%(hdict[host], host)) when,what = bad[k] W(" %s -\n"%what) def pdadmin_running(self, verbose=0, refresh=0, ignore='', resultmessage='', Access='Read'): from urllib import quote self.header(html=1, refresh='/running?refresh=1&ignore=%s'%time.time()) W = self.wfile.write W('

current config

\n') W('

last update at %s

\n'%time.ctime(time.time())) W('

Refresh'%time.time()) if refresh: W('Stop auto-refresh

'%time.time()) else: W('Start auto-refresh

'%time.time()) W("

\n") conf = self.director.conf for service in conf.getServices(): W('\n'% service.name) for l in service.listen: W('\n'%l) eg = service.getEnabledGroup() groups = service.getGroups() for group in groups: sch = self.director.getScheduler(service.name, group.name) stats = sch.getStats(verbose=verbose) hdict = sch.getHostNames() if group is eg: klass = 'enabled' else: klass = 'inactive' W('') W('\n') W('''\n'''%klass) counts = stats['open'] totals = stats['totals'] k = counts.keys() k.sort() for h in k: W('\n"%(hdict[h], h)) if counts.has_key(h): oc = counts[h] else: oc = '--' if totals.has_key(h): tc = totals[h] else: tc = '--' W(""%(oc,tc)) W('') W('') bad = stats['bad'] if bad: W('''\n'''%klass) for k in bad.keys(): host = '%s:%s'%k W('\n"%(hdict[host], host)) # XXXX when,what = bad[k] W(""%what) W('') W("
Service: %s
Listening on %s
%s '%(klass, group.name)) if group is eg: W('ENABLED\n') else: W('enable\n'% (service.name, group.name)) W('') W('') W('') W(''%service.name) W(''%group.name) W('') W('') W('') W('') W('
name
ip
') W('
hosts opentotal
'%klass) W("%s%s%s%s
') a='service=%s&group=%s&ip=%s'%( quote(service.name), quote(group.name), quote(h)) W('remove host'%(a)) W('
disabled hosts whywhen
'%klass) W("%s%s%s--
") self.footer(resultmessage) def pdadmin_addHost(self, service, group, name, ip, Access='Write'): sched = self.director.getScheduler(serviceName=service, groupName=group) sched.newHost(name=name, ip=ip) # also add to conf DOM object self.action_done('Host %s(%s) added to %s / %s'%( name, ip, group, service)) self.wfile.write("OK\n") def pdadmin_delHost(self, service, group, ip, Access='Write'): sched = self.director.getScheduler(serviceName=service, groupName=group) service = self.director.conf.getService(service) eg = service.getEnabledGroup() if group == eg.name: if sched.delHost(ip=ip, activegroup=1): self.action_done('host %s deleted (from active group!)'%ip) else: self.action_done('host %s not deleted from active group'%ip) else: if sched.delHost(ip=ip): self.action_done('host %s deleted from inactive group'%ip) else: self.action_done('host %s not deleted from inactive group'%ip) self.wfile.write("OK\n") def pdadmin_delAllHosts(self, service, group, Access='Write'): self.action_done('not implemented yet') self.wfile.write("OK\n") def pdadmin_enableGroup(self, service, group, Access='Write'): self.director.enableGroup(service, group) self.action_done('Group %s enabled for service %s'%( group, service)) self.wfile.write("OK\n") def pdadmin_changeScheduler(self, service, group, scheduler, Access='Write'): self.action_done('not implemented yet') self.wfile.write("OK\n") def pdadmin_config_xml(self, Access='Read'): self.header(html=0) self.wfile.write(self.director.conf.dom.toxml()) def pdadmin_status_txt(self, verbose=0, Access='Read'): self.header(html=0) W = self.wfile.write # needs to handle multiple listeners per service! raise "Broken", "update me!" for listener in self.director.listeners.values(): sch_stats = listener.scheduler.getStats(verbose='verbose') lh,lp = listener.listening_address sn = listener.scheduler.schedulerName W("service: %s\n"%listener.name) W("listen: %s:%s %s\n"%(lh,lp, sn)) for h, c in sch_stats['open']: W("host: %s:%s %s\n"%(h[0],h[1],c)) bad = sch_stats['bad'] if bad: for b in bad: W("disabled: %s:%s\n"%b) def pdadmin_addUser(self, name, password, access, Access='Write'): if self.adminconf.getUser(name): self.action_done('user %s already exists'%name) self.wfile.write("NOT OK\n") else: self.adminconf.addUser(name, password, access) self.action_done('user %s added'%name) self.wfile.write("OK\n") def pdadmin_delUser(self, name, Access='Write'): if self.adminconf.getUser(name): self.adminconf.delUser(name) self.action_done('user %s deleted'%name) self.wfile.write("OK\n") else: self.action_done('user %s not found'%name) self.wfile.write("NOT OK\n") def pdadmin_unimplemented(self, Access='Write'): self.action_done('not implemented yet') self.wfile.write("OK\n") def log_message(self, format, *args): "overridden from BaseHTTPServer" pdlogging.log("ADMIN: %s - - [%s] %s\n" % (self.address_string(), self.log_date_time_string(), format%args)) def log_exception(self): nil, t, v, tbinfo = pdlogging.compact_traceback() pdlogging.log("ADMIN(Exception) %s - %s: %s %s\n"% (time.ctime(time.time()), t,v,tbinfo)) def dictify(q): """ takes string of form '?a=b&c=d&e=f' and returns {'a':'b', 'c':'d', 'e':'f'} """ from urllib import unquote out = {} if not q: return {} avs = q.split('&') for av in avs: #print "av", av a,v = av.split('=',1) out[unquote(a)] = unquote(v) return out def html_quote(str): return re.subn("<", "<", str)[0] PYDIR_CSS = """ body { font-family: helvetica; font-size: 10pt } a { text-decoration: none; background-color: transparent; } A:link {color: #000000 } A:visited {color: #000000} /* borrowed ideas from plone */ div.footer { font-family: courier; font-size: 8pt ; background: transparent; border-collapse: collapse; border-top-color: #88AAAA; border-top-style: solid; border-top-width: 1px; padding: 0em 0em 0.5em 2em; white-space: nowrap; color: #000033 ; } div.footer a { background: transparent; border-color: #88AAAA; border-width: 1px; border-style: none solid solid solid; color: #226666; font-weight: normal; margin-right: 0.5em; padding: 0em 2em; text-transform: lowercase; } div.footer a:hover { background: #DEE7EC; border-color: #88AAAA; border-top-color: #88AAAA; color: #436976; } div.title { font-weight: bold; color: #000033 ; background: transparent; border-color: #88AAAA; border-style: none solid solid solid ; border-width: 1px; margin: 4px; padding: 3px; white-space: nowrap; } div.deleteButton { color: red ; background: yellow; border-collapse: collapse; border-color: red; border-style: solid solid solid solid ; border-width: 1px; padding: 1px; white-space: nowrap; } div.deleteButton a { color: black; } div.deleteButton a:hover { color: red; } p.message { color: #000000 ; background-color: #eeeeee ; border: thin solid #ff0000 ; padding: 5px ; } p.bigbutton { color: #000000 ; background-color: #eebbee ; border: thin solid #cc4400 ; padding: 2px ; } a.button { color: #000000 ; background-color: #eebbee ; border: thin solid #cc4400 ; padding: 4px ; margin: 2px ; } tr.enabled { background-color: #ccdddd; color: #dd0000 } tr.inactive { background-color: #eeeeee; color: #000000 } tr.inactive td.servHeader a { background-color: #ccdddd; color: #000000 ; padding: 0px 2px 0px 2px; border-style: solid; border-width: 1px; } tr.inactive td.servHeader a:hover { background-color: #ccdddd; color: red } th { font-family: helvetica ; font-size: 10pt ; } td { font: 10px helvetica; } td.addWidget { padding: 0px; } table.addWidget { padding: 0px; } /*table.addWidget td { font: 10px helvetica; background-color: white; margin: 0em 0em 0em 0em; } */ div.widgetLabel { background-color: transparent; font-weight: bold; margin: 0em 0em 0em 0em; } input { /* Small cosmetic fix which makes input gadgets look nicer. */ font: 10px Verdana, Helvetica, Arial, sans-serif; border: 1px solid #8cacbb; color: Black; background-color: white; margin: 0em 0em 0em 0em; } input:hover { background-color: #DEE7EC ; } p.copyright { font-weight: bold; max-width: 60%; } p.license { font-weight: bold; max-width: 60%; } """ pydirector-1.0.0/pydirector/pdconf.py0000644000026300012320000001755210157565473020350 0ustar anthonytech00000000000000# # Copyright (c) 2002-2004 ekit.com Inc (http://www.ekit-inc.com) # and Anthony Baxter # # $Id: pdconf.py,v 1.18 2004/12/14 13:31:39 anthonybaxter Exp $ # import sys if sys.version_info < (2,2): class object: pass def getDefaultArgs(methodObj): import inspect arglist, vaarg, kwarg, defargs = inspect.getargspec(methodObj.im_func) arglist.reverse() defargs = list(defargs) defargs.reverse() ad = {} for a,v in zip(arglist, defargs): ad[a] = v return ad def splitHostPort(s): h,p = s.split(':') p = int(p) if h == '*': h = '' return h,p class ConfigError(Exception): pass class ServiceError(ConfigError): pass class GroupError(ServiceError): pass class PDHost(object): __slots__ = [ 'name', 'ip' ] def __init__(self, name, ip): self.name = name if type(ip) is type(u''): self.ip = ip.encode('ascii') else: self.ip = ip class PDGroup(object): __slots__ = [ 'name', 'scheduler', 'hosts' ] def __init__(self, name): self.name = name self.scheduler = None self.hosts = {} def getHost(self,name): return self.hosts[name] def getHostNamess(self): return self.hosts.keys() def getHosts(self): return self.hosts.values() def addHost(self, name, ip): self.hosts[name] = PDHost(name, ip) def delHost(self, name): del self.hosts[name] class PDService(object): __slots__ = [ 'listen', 'groups', 'enabledgroup', 'name' ] def __init__(self, name): self.name = name self.groups = {} self.listen = [] self.enabledgroup = None def loadGroup(self, groupobj): groupName = groupobj.getAttribute('name') newgroup = PDGroup(groupName) newgroup.scheduler = groupobj.getAttribute('scheduler') cc = 0 for host in groupobj.childNodes: if host.nodeName in ("#text", "#comment"): continue if host.nodeName != u'host': raise ConfigError, \ "expected 'host', got '%s'"%host.nodeName name = host.getAttribute('name') if not name: name = 'host.%s'%cc newgroup.addHost(name, host.getAttribute('ip')) cc += 1 self.groups[groupName] = newgroup def getGroup(self, groupName): return self.groups.get(groupName) def getGroups(self): return self.groups.values() def getGroupNames(self): return self.groups.keys() def getEnabledGroup(self): return self.groups.get(self.enabledgroup) def checkSanity(self): if not self.name: raise ServiceError, "no name set" if not self.listen: raise ServiceError, "no listen address set" if not self.groups: raise ServiceError, "no host groups" if not self.enabledgroup: raise ServiceError, "no group enabled" if not self.groups.get(self.enabledgroup): raise GroupError, \ "enabled group '%s' not defined"%self.enabledgroup for group in self.groups.values(): if not group.name: raise GroupError, "no group name set" if not group.scheduler: raise GroupError, \ "no scheduler set for %s"%group.name if not group.hosts: raise GroupError, \ "no hosts set for %s"%group.name class PDAdminUser(object): __slots__ = [ 'name', 'password', 'access' ] def checkPW(self, password): from crypt import crypt if crypt(password, self.password[:2]) == self.password: return 1 else: return 0 def checkAccess(self, methodObj, argdict): from inspect import getargspec a = getDefaultArgs(methodObj) required = a.get('Access', 'NoAccess') if required == "Read" and self.access in ('full', 'readonly'): return 1 elif required == "Write" and self.access == 'full': return 1 else: return 0 class PDAdmin(object): __slots__ = [ 'listen', 'userdb', 'secure' ] def __init__(self): self.listen = None self.secure = None self.userdb = {} def addUser(self, name, password, access): u = PDAdminUser() u.name = name u.password = password u.access = access self.userdb[name] = u def delUser(self, name): if self.userdb.has_key(name): del self.userdb[name] return 1 else: return 0 def loadUser(self, userobj): name = userobj.getAttribute('name') password = userobj.getAttribute('password') access = userobj.getAttribute('access') self.addUser(name, password, access) def getUser(self, name): return self.userdb.get(name) def getUsers(self): return self.userdb.values() def getUserNames(self): return self.userdb.keys() class PDConfig(object): __slots__ = [ 'services', 'admin', 'dom' ] def __init__(self, filename=None, xml=None): import pdlogging self.services = {} self.admin = None dom = self._loadDOM(filename, xml) if dom.nodeName != 'pdconfig': raise ConfigError, "expected top level 'pdconfig', got '%s'"%( dom.nodeName) for item in dom.childNodes: if item.nodeName in ("#text", "#comment"): continue if item.nodeName not in ( u'service', u'admin', u'logging' ): raise ConfigError, \ "expected 'service' or 'admin', got '%s'"%item.nodeName if item.nodeName == u'service': self.loadService(item) elif item.nodeName == u'admin': if self.admin is None: self.loadAdmin(item) else: raise ConfigError, "only one 'admin' block allowed" elif item.nodeName == u'logging': pdlogging.initlog(item.getAttribute('file')) def _loadDOM(self, filename, xml): from xml.dom.minidom import parseString if filename is not None: xml = open(filename).read() elif xml is None: raise ConfigError, "need filename or xml" self.dom = parseString(xml) return self.dom.childNodes[0] def loadAdmin(self, admin): adminServer = PDAdmin() adminServer.listen = splitHostPort(admin.getAttribute('listen')) if admin.hasAttribute('secure'): adminServer.secure = admin.getAttribute('secure') for user in admin.childNodes: if user.nodeName in ("#text", "#comment"): continue if user.nodeName == u'user': adminServer.loadUser(user) else: raise ConfigError, "only expect to see users in admin block" self.admin = adminServer def getService(self, serviceName): return self.services.get(serviceName) def getServices(self): return self.services.values() def getServiceNames(self): return self.services.keys() def loadService(self, service): serviceName = service.getAttribute('name') newService = PDService(serviceName) for c in service.childNodes: if c.nodeName in ("#text", "#comment"): continue if c.nodeName == u'listen': newService.listen.append(c.getAttribute('ip')) elif c.nodeName == u'group': newService.loadGroup(c) elif c.nodeName == u'enable': newService.enabledgroup = c.getAttribute('group') elif c.nodeName == "#comment": continue else: raise ConfigError, "unknown node '%s'"%c.nodeName newService.checkSanity() self.services[serviceName] = newService if __name__ == "__main__": import sys PDConfig(sys.argv[1]) pydirector-1.0.0/pydirector/pdlogging.py0000644000026300012320000000271110157565473021040 0ustar anthonytech00000000000000# # Copyright (c) 2002-2004 ekit.com Inc (http://www.ekit-inc.com) # and Anthony Baxter # # $Id: pdlogging.py,v 1.7 2004/12/14 13:31:39 anthonybaxter Exp $ # Logger=None from asyncore import compact_traceback import sys, time # look at replacing this later class _LoggerClass: def __init__(self, logfile=None): self.logfile = logfile self.fp = None self.reopen() def reopen(self): if self.logfile is not None and self.fp is not None: del self.fp if self.logfile is None: self.fp = sys.stderr else: self.fp = open(self.logfile, 'a') def log(self, message, datestamp=0): if datestamp: self.fp.write("%s %s"%(self.log_date_time_string(),message)) else: self.fp.write(message) self.fp.flush() def log_date_time_string(self): """Return the current time formatted for logging.""" now = time.time() year, month, day, hh, mm, ss, x, y, z = time.localtime(now) s = "%02d/%02d/%04d %02d:%02d:%02d" % ( day, month, year, hh, mm, ss) return s def initlog(filename): global Logger Logger = _LoggerClass(filename) def log(message, datestamp=0): global Logger if Logger is None: Logger = _LoggerClass() Logger.log(message, datestamp) def reload(): global Logger if Logger is None: Logger = _LoggerClass() Logger.reload() pydirector-1.0.0/pydirector/pdmain.py0000644000026300012320000000617310157565473020344 0ustar anthonytech00000000000000# # Copyright (c) 2002-2004 ekit.com Inc (http://www.ekit-inc.com) # and Anthony Baxter # # $Id: pdmain.py,v 1.11 2004/12/14 13:31:39 anthonybaxter Exp $ # import sys if sys.version_info < (2,2): class object: pass class PythonDirector(object): def __init__(self, config): from pydirector import pdconf self.listeners = {} self.schedulers = {} self.manager = None self.conf = pdconf.PDConfig(config) self.createManager() self.createListeners() def start(self, profile=0): import sys from pydirector import pdadmin from pdnetwork import mainloop if self.conf.admin is not None: pdadmin.start(adminconf=self.conf.admin, director=self) self.manager.start() try: if profile: import hotshot print "creating profiling log" prof = hotshot.Profile("pydir.prof") try: prof.runcall(mainloop) finally: print "closing profile log" prof.close() else: mainloop(timeout=4) except KeyboardInterrupt: sys.exit(0) def createManager(self): from pydirector import pdmanager import threading manager = pdmanager.SchedulerManager(self) mt = threading.Thread(target=manager.mainloop) mt.setDaemon(1) self.manager = mt def createSchedulers(self, service): from pydirector import pdschedulers for group in service.getGroups(): s = pdschedulers.createScheduler(group) self.schedulers[(service.name,group.name)] = s def getScheduler(self, serviceName, groupName): return self.schedulers[(serviceName,groupName)] def createListeners(self): from pydirector import pdnetwork, pdconf for service in self.conf.getServices(): self.createSchedulers(service) eg = service.getEnabledGroup() scheduler = self.getScheduler(service.name, eg.name) # handle multiple listeners for a service self.listeners[service.name] = [] for lobj in service.listen: l = pdnetwork.Listener(service.name, pdconf.splitHostPort(lobj), scheduler) self.listeners[service.name].append(l) def enableGroup(self, serviceName, groupName): serviceConf = self.conf.getService(serviceName) group = serviceConf.getGroup(groupName) if group: serviceConf.enabledgroup = groupName self.switchScheduler(serviceName) def switchScheduler(self, serviceName): """ switch the scheduler for a listener. this is needed, e.g. if we change the active group """ serviceConf = self.conf.getService(serviceName) eg = serviceConf.getEnabledGroup() scheduler = self.getScheduler(serviceName, eg.name) for listener in self.listeners[serviceName]: listener.setScheduler(scheduler) pydirector-1.0.0/pydirector/pdmanager.py0000644000026300012320000000360510157565473021027 0ustar anthonytech00000000000000# # Copyright (c) 2002-2004 ekit.com Inc (http://www.ekit-inc.com) # and Anthony Baxter # # $Id: pdmanager.py,v 1.10 2004/12/14 13:31:39 anthonybaxter Exp $ # import sys if sys.version_info < (2,2): class object: pass import pdconf, pdlogging class SchedulerManager(object): """ This object sits in a seperate thread and manages the scheduler. It's responsible for reconfiguration, checking dead hosts to see if they've come back, that sort of thing. """ def __init__(self, director, sleeptime=15, checktime=120): self.director = director self.sleeptime = sleeptime self.checktime = checktime def mainloop(self): import time print "manager sleeptime is %s"%(self.sleeptime) while 1: time.sleep(self.sleeptime) for listeners in self.director.listeners.values(): # since all listeners for a service share a scheduler, # we only need to check the first listener. listener = listeners[0] scheduler = listener.scheduler #print scheduler.showStats(verbose=0) self.checkBadHosts(scheduler) def checkBadHosts(self, scheduler): import time forcecheck=0 badhosts = scheduler.badhosts hosts = badhosts.keys() if not len(scheduler.hosts): # All servers are down! Go into a more aggressive mode for # checking. forcecheck=1 for bh in hosts: now = time.time() when,what = badhosts[bh] if forcecheck or (now > when + self.checktime): pdlogging.log("re-adding %s automatically\n"%str(bh), datestamp=1) name = scheduler.getHostNames()[bh] del badhosts[bh] scheduler.newHost(bh, name) pydirector-1.0.0/pydirector/pdnetwork.py0000644000026300012320000000031110157564105021064 0ustar anthonytech00000000000000import pdlogging try: import twisted from pdnetworktwisted import * except ImportError: pdlogging.log("no twisted available - falling back to asyncore") from pdnetworkasyncore import * pydirector-1.0.0/pydirector/pdnetworkasyncore.py0000644000026300012320000001533510157565473022655 0ustar anthonytech00000000000000# # Copyright (c) 2002-2004 ekit.com Inc (http://www.ekit-inc.com) # and Anthony Baxter # # Networking core - asyncore version # # $Id: pdnetworkasyncore.py,v 1.6 2004/12/14 13:31:39 anthonybaxter Exp $ # import asyncore, asynchat, socket, sys, errno import pdlogging #asyncore.DEBUG = 1 class Listener(asyncore.dispatcher): """ This object sits and waits for incoming connections on a port. When it receives a connection, it creates a Receiver to accept data to the port, and also passes the scheduler across to the Receiver.""" def __init__(self, name, (bindhost, bindport), scheduler): asyncore.dispatcher.__init__(self) self.scheduler = scheduler self.create_socket(socket.AF_INET, socket.SOCK_STREAM) self.set_reuse_addr() self.listening_address =(bindhost,bindport) self.name = name self.bind((bindhost,bindport)) self.listen(10) def setScheduler(self, scheduler): self.scheduler = scheduler def handle_accept(self): # don't accept if no backends available! who = self.accept() #print "got connection from", who try: # it's critical that the listener is not killed by # exceptions from the Receivers or Senders r = Receiver(self, who, self.scheduler) r.go() except: nil, t, v, tbinfo = pdlogging.compact_traceback() pdlogging.log('Listener: %s %s %s\n'% (str(t), str(v), str(tbinfo)), datestamp=1) class Receiver(asynchat.async_chat): """ A Receiver is created when a new inbound connection from a client is detected. It sets itself up to get the client data, then creates a Sender object to handle the server side of the connection. It passes the Scheduler across to the Sender - this is called to ask which end server to use """ def __init__(self, listener, (conn, addr), scheduler): asynchat.async_chat.__init__(self, conn) self.set_terminator(None) self.listener = listener self.id = id(self) self.client_addr = addr self.sender = Sender(self, scheduler) self.sender.id = self.id self.scheduler = scheduler self.buffer = '' self.serverOk = 0 self.cacheBuffer = '' #print "receiver %x init"%id(self) def go(self): self.sender.go() def retry_connection(self): self.sender = Sender(self, self.scheduler) self.sender.id = self.id self.buffer = self.cacheBuffer self.cacheBuffer = '' self.maybe_push_data() def collect_incoming_data(self, data): self.buffer = self.buffer + data self.maybe_push_data() def maybe_push_data(self, force=0): if self.buffer: data = self.buffer self.buffer = '' self.sender.push(data) # if this server's known to be answering... if self.serverOk: self.cacheBuffer = '' else: self.cacheBuffer += data def handle_close(self): """the connection to the client is done. no point in the sender continuing, as the client connection's toast""" #print "receiver %x/sender %x close"%(id(self), id(self.sender)) self.maybe_push_data(force=1) try: # There's a race condition here, sometimes we lose. but we # don't actually care. self.sender.close() self.scheduler.doneHost(self) except: pass self.close() class Sender(asynchat.async_chat): """A sender handles the proxy<->server side of the conversation. When created, it gets passed a Scheduler object. It asks the scheduler object to find out the eventual host to connect.""" def __init__(self, receiver, scheduler): asynchat.async_chat.__init__(self) self.receiver = receiver self.set_terminator(None) self.scheduler = scheduler self.buffer = '' def go(self): self.do_connect() def do_connect(self): self.create_socket(socket.AF_INET, socket.SOCK_STREAM) dest = self.scheduler.getHost(self.receiver, self.receiver.client_addr) if dest: self.dest = dest try: self.connect(self.dest) except: self.handle_error() else: self.log_info("NO WORKING HOSTS!\n", 'fatal') self.close() def handle_connect(self): pass def dead_server(self, retry=0, reason=""): # The server we connected to is dead. Let the scheduler know, # then, if required, get the receiver to retry the connection. # (this will get a new Sender instance, connected to a new dest. print "marking server %s as dead (%s)"%(self.dest, reason) self.scheduler.deadHost(self.receiver, reason) if retry: r = self.receiver del self.receiver r.retry_connection() self.close() def handle_error (self): e,v,t = sys.exc_info() what = '' if e is socket.error: if v[0] == errno.ECONNREFUSED: # server is down :( self.dead_server(retry=1, reason="conn_refused") return elif v[0] == errno.ECONNABORTED: self.close() return else: what = " (%s)"%errno.errorcode[v[0]] elif hasattr(socket, 'gaierror') and e is socket.gaierror: if v[0] == -2: self.dead_server(retry=1, reason="unknown_name") return nil, t, v, tbinfo = pdlogging.compact_traceback() # sometimes user repr method will crash. try: self_repr = repr (self) except: self_repr = '<__repr__ (self) failed for object at %0x>' % id(self) self.log_info ( 'sender python exception, closing channel %s (%s:%s%s %s)' % ( self_repr, t, v, what, tbinfo ), 'error' ) self.close() def collect_incoming_data(self, data): self.receiver.push(data) def handle_close(self): #print "receiver %x close_when_done"%id(self.receiver) self.receiver.close_when_done() self.scheduler.doneHost(self.receiver) self.close() def log(self, message): pdlogging.log(message) def log_info (self, message, type='info'): pdlogging.log('%s: %s\n' % (type, message), datestamp=1) def mainloop(timeout): " wrapper around the I/O library's main loop " asyncore.loop(timeout = 4) pydirector-1.0.0/pydirector/pdnetworktwisted.py0000644000026300012320000001654310157565473022517 0ustar anthonytech00000000000000# # Copyright (c) 2002-2004 ekit.com Inc (http://www.ekit-inc.com) # and Anthony Baxter # # Networking core - twisted version (http://www.twistedmatrix.com) # # $Id: pdnetworktwisted.py,v 1.11 2004/12/14 13:31:39 anthonybaxter Exp $ # from twisted.internet.protocol import ServerFactory, ClientFactory, Protocol from twisted.internet import reactor import twisted.internet import pdlogging class Listener: """ Listener object. Listens at a given host/port for connections. Creates a receiver to collect data from client, and a sender to connect to the eventual destination host. Public API: method __init__(self, name, (bindhost, bindport), scheduler) attribute .scheduler: read/write - a PDScheduler attribute .listening_address: read - a tuple of (host,port) """ def __init__(self, name, (bindhost, bindport), scheduler): self.name = name self.listening_address = (bindhost, bindport) self.rfactory = ReceiverFactory((bindhost,bindport), scheduler) self.setScheduler(scheduler) reactor.listenTCP(bindport, self.rfactory, interface=bindhost) def setScheduler(self, scheduler): self.scheduler = scheduler self.rfactory.setScheduler(scheduler) class Sender(Protocol): """ A Sender object connects to the remote final server, and passes data back and forth. Unlike the receiver, it's not necessary to buffer up data, since the client _must_ be connected (if it's not, toss the data) """ receiver = None def setReceiver(self, receiver): self.receiver = receiver def connectionLost(self, reason): """ the server is done, and has closed the connection. write out any remaining data, and close the socket. """ if self.receiver is not None: if reason.type is twisted.internet.error.ConnectionDone: return elif reason.type is twisted.internet.error.ConnectionLost: pass else: #print id(self),"connection to server lost:",reason pass self.receiver.transport.loseConnection() def dataReceived(self, data): #print "client data", len(data) if self.receiver is None: pdlogging.log("client got data, no receiver, tho\n", datestamp=1) else: self.receiver.transport.write(data) def connectionMade(self): """ we've connected to the destination server. tell the other end it's ok to send any buffered data from the client. """ #print "client connection",self.factory if self.receiver.receiverOk: self.receiver.setSender(self) else: # the receiver's already given up at this point and gone # home. _if_ the receiver got data from the client, we # must send it on - the client thinks that it's successfully # sent it, so we should honour that. We don't need to worry # about the response from the server itself. data = self.receiver.getBuffer() if data: self.transport.write(data) self.transport.loseConnection() self.setReceiver(None) class SenderFactory(ClientFactory): "create a Sender when needed. The sender connects to the remote host" protocol = Sender noisy = 0 def setReceiver(self, receiver): #print "senderfactory.setreceiver", receiver self.receiver = receiver def buildProtocol(self, *args, **kw): # over-ride the base class method, because we want to connect # the objects together. protObj = ClientFactory.buildProtocol(self, *args, **kw) protObj.setReceiver(self.receiver) return protObj def clientConnectionFailed(self, connector, reason): #print "bzzt. we failed,", connector, reason # this would hang up the inbound. We don't want that. #self.receiver.transport.loseConnection() self.receiver.factory.scheduler.deadHost(self, reason) next = self.receiver.factory.scheduler.getHost(self, self.receiver.client_addr) if next: pdlogging.log("retrying with %s\n"%repr(next), datestamp=1) host, port = next reactor.connectTCP(host, port, self) else: # No working servers!? pdlogging.log("no working servers, manager -> aggressive\n", datestamp=1) self.receiver.transport.loseConnection() def stopFactory(self): self.receiver.factory.scheduler.doneHost(self) class Receiver(Protocol): "Listener bit for clients connecting to the director" sender = None buffer = '' receiverOk = 0 def connectionMade(self): "This is invoked when a client connects to the director" self.receiverOk = 1 self.client_addr = self.transport.client sender = SenderFactory() sender.setReceiver(self) dest = self.factory.scheduler.getHost(sender, self.client_addr) if dest: host, port = dest sender = reactor.connectTCP(host, port, sender) else: #print "(still) no working servers!" self.transport.loseConnection() def setSender(self, sender): "the sender side of the proxy is connected" self.sender = sender if self.buffer: self.sender.transport.write(self.buffer) self.buffer = '' def connectionLost(self, reason): """ the client has hung up/disconnected. send the rest of the data through before disconnecting. Let the client know that it can just discard the data. """ # damn. XXX TODO. If the client connects, sends, then disconnects, # before the end server has connected, we have data loss - the client # thinks it's connected and sent the data, but it won't have. damn. if self.sender: # according to the interface docstring, this sends all pending # data before closing the connection. self.sender.setReceiver(None) self.sender.transport.loseConnection() self.receiverOk = 0 else: # there's a race condition here - we could be in the process of # setting up the director->server connection. This then comes in # after this, and you end up with a hosed receiver that's hanging # around. self.receiverOk = 0 def getBuffer(self): "return any buffered data" return self.buffer def dataReceived(self, data): "received data from the client. either send it on, or save it" if self.sender is not None: self.sender.transport.write(data) else: self.buffer += data class ReceiverFactory(ServerFactory): "Factory for the listener bit of the pydirector" protocol = Receiver noisy = 0 def __init__(self, (bindhost, bindport), scheduler): self.bindhost = bindhost self.bindport = bindport self.scheduler = scheduler def setScheduler(self, scheduler): self.scheduler = scheduler def mainloop(timeout=5): " run the main loop " #print "running mainloop" reactor.run() pydirector-1.0.0/pydirector/pdschedulers.py0000644000026300012320000001446210157565473021561 0ustar anthonytech00000000000000# # Copyright (c) 2002-2004 ekit.com Inc (http://www.ekit-inc.com) # and Anthony Baxter # # $Id: pdschedulers.py,v 1.16 2004/12/14 13:31:39 anthonybaxter Exp $ # import sys, time if sys.version_info < (2,2): class object: pass import pdconf, pdlogging def createScheduler(groupConfig): schedulerName = groupConfig.scheduler if schedulerName == "random": return RandomScheduler(groupConfig) elif schedulerName == "leastconns": return LeastConnsScheduler(groupConfig) elif schedulerName == "roundrobin": return RoundRobinScheduler(groupConfig) elif schedulerName == "leastconnsrr": return LeastConnsRRScheduler(groupConfig) else: raise ValueError, "Unknown scheduler type `%s'"%schedulerName class BaseScheduler: schedulerName = "base" def __init__(self, groupConfig): self.hosts = [] self.hostnames = {} self.badhosts = {} self.open = {} self.openconns = {} self.totalconns = {} self.lastclose = {} self.loadConfig(groupConfig) def loadConfig(self, groupConfig): self.group = groupConfig hosts = self.group.getHosts() for host in hosts: self.newHost(host.ip, host.name) #print self.hosts def getStats(self, verbose=0): out = {} out['open'] = {} out['totals'] = {} hc = self.openconns.items() hc.sort() for h,c in hc: out['open']['%s:%s'%h] = c hc = self.totalconns.items() hc.sort() for h,c in hc: out['totals']['%s:%s'%h] = c bh = self.badhosts out['bad'] = bh return out def showStats(self, verbose=1): out = [] out.append( "%d open connections"%len(self.open.keys()) ) hc = self.openconns.items() hc.sort() out = out + [str(x) for x in hc] if verbose: oh = [x[1] for x in self.open.values()] oh.sort() out = out + [str(x) for x in oh] return "\n".join(out) def getHost(self, s_id, client_addr=None): from time import time host = self.nextHost(client_addr) if host: cur = self.openconns.get(host) self.open[s_id] = (time(),host) self.openconns[host] = cur+1 return host else: return None def getHostNames(self): return self.hostnames def doneHost(self, s_id): try: t,host = self.open[s_id] except KeyError: #print "Couldn't find %s in %s"%(repr(s_id), repr(self.open.keys())) return del self.open[s_id] cur = self.openconns.get(host) if cur is not None: self.openconns[host] = cur - 1 self.totalconns[host] += 1 self.lastclose[host] = time.time() def newHost(self, ip, name): if type(ip) is not type(()): ip = pdconf.splitHostPort(ip) self.hosts.append(ip) self.hostnames[ip] = name self.hostnames['%s:%d'%ip] = name self.openconns[ip] = 0 self.totalconns[ip] = 0 def delHost(self, ip=None, name=None, activegroup=0): "remove a host" if ip is not None: if type(ip) is not type(()): ip = pdconf.splitHostPort(ip) elif name is not None: for ip in self.hostnames.keys(): if self.hostnames[ip] == name: break raise ValueError, "No host named %s"%(name) else: raise ValueError, "Neither ip nor name supplied" if activegroup and len(self.hosts) == 1: return 0 if ip in self.hosts: self.hosts.remove(ip) del self.hostnames[ip] del self.openconns[ip] del self.totalconns[ip] elif self.badhosts.has_key(ip): del self.badhosts[ip] else: raise ValueError, "Couldn't find host" return 1 def deadHost(self, s_id, reason=''): from time import time t,host = self.open[s_id] if host in self.hosts: pdlogging.log("marking host %s down (%s)\n"%(str(host), reason), datestamp=1) self.hosts.remove(host) if self.openconns.has_key(host): del self.openconns[host] if self.totalconns.has_key(host): del self.totalconns[host] self.badhosts[host] = (time(), reason) # make sure we also mark this session as done. self.doneHost(s_id) def nextHost(self): raise NotImplementedError class RandomScheduler(BaseScheduler): schedulerName = "random" def nextHost(self, client_addr): import random if self.hosts: pick = random.choice(self.hosts) return pick else: return None class RoundRobinScheduler(BaseScheduler): schedulerName = "roundrobin" counter = 0 def nextHost(self, client_addr): if not self.hosts: return None if self.counter >= len(self.hosts): self.counter = 0 if self.hosts: d = self.hosts[self.counter] self.counter += 1 return d class LeastConnsScheduler(BaseScheduler): """ This scheduler passes the connection to the destination with the least number of current open connections. This is a very cheap and quite accurate method of load balancing. """ schedulerName = "leastconns" counter = 0 def nextHost(self, client_addr): if not self.openconns.keys(): return None hosts = [ (x[1],x[0]) for x in self.openconns.items() ] hosts.sort() return hosts[0][1] class LeastConnsRRScheduler(BaseScheduler): """ The basic LeastConnsScheduler has a problem - it sorts by open connections, then by hostname. So hostnames that are earlier in the alphabet get many many more hits. This is suboptimal. """ schedulerName = "leastconnsrr" counter = 0 def nextHost(self, client_addr): if not self.openconns.keys(): return None hosts = [ (x[1], self.lastclose.get(x[0],0), x[0]) for x in self.openconns.items() ] hosts.sort() return hosts[0][2] pydirector-1.0.0/BUGS.txt0000644000026300012320000000000007653663644015624 0ustar anthonytech00000000000000pydirector-1.0.0/README.txt0000644000026300012320000001433410157565063016025 0ustar anthonytech00000000000000README for pythondirector 1.0.0 This is a pure python TCP load balancer. It takes inbound TCP connections and connects them to one of a number of backend servers. Project home: http://pythondirector.sourceforge.net/ Contact email: Anthony Baxter ---------------------------------------------------------------------- Features: - by default, uses the Twisted framework for async I/O, but can also use asyncore. - async i/o based, so much less overhead than fork/thread based balancers - Multiple scheduling algorithms (random, round robin, leastconns, leastconns-least-recently-used) - If a server fails to answer, it's removed from the pool - the client that failed to connect gets transparently failed over to a new host. - xml based configuration file - seperate management thread that periodically re-adds failed hosts if they've come back up. - optional builtin webserver for admin ---------------------------------------------------------------------- Performance: - On my notebook, load balancing an apache on the same local ethernet (serving a static 18K text file) gets 155 connections per second and 2850 kbytes/s throughput (apachebench -n 2000 -c 10). Connecting directly to the apache gets 180 conns/sec and 3400kbytes/s. So unless you're serving really really stupidly high hit rates it's unlikely to be pythondirector causing you difficulties. (Note that 155 connections/sec is 13 million hits per day...) - Running purely over the loopback interface to a local apache seems to max out at around 350 conns/second. ---------------------------------------------------------------------- API (web based): See doc/webapi.txt for a full list of web api commands ---------------------------------------------------------------------- Twisted vs. asyncore Pythondirector will use either twisted or asyncore for it's networking - it prefers twisted. The twisted implementation is much, much faster, but does require an additional package - see http://www.twistedmatrix.com for the software. I've also seen "weird failures" from asyncore with some sort of nasty race condition. ---------------------------------------------------------------------- Changes from 0.0.7 to 1.0.0 - Very few, mostly this is to update the project to 'stable' status. - The networking code now uses twisted if available, and falls back to asyncore. Changes from 0.0.6 to 0.0.7 - You can specify a hostname of '*' to the listen directive for both the scheduler and the administrative interface to mean 'listen on all interfaces'. Considerably more obvious than '0.0.0.0'. Thanks to Andrew Sydelko for the idea. - New "leastconnsrr" scheduler - this is leastconns, with a roundrobin as well. Previously, leastconns would keep the list of hosts sorted, which often meant one system got beaten up pretty badly. - Twisted backend group selection works again. - The client address is now passed to the scheduler's getHost() method. This allows the creation of "sticky" schedulers, where a client is (by preference) sent to the same backend server. The factory function for schedulers will change to allow things like "roundrobin,sticky". Changes from 0.0.5 to 0.0.6: - fixed an error in the (hopefully rare) case where all backend servers are down. - the main script uses resource.setrlimit() to boost the number of open filedescriptors (solaris has stupidly low defaults) - when all backend servers are down, the manager thread goes into a much more aggressive mode re-adding them. - handle comments in the config file Changes from 0.0.4 to 0.0.5: - bunch of bugfixes to the logging - re-implemented the networking code using the 'twisted' framework (a simple loopback test: asyncore based pydir: Requests per second: 107.72 Transfer rate: 2462.69 kb/s received twisted based pydir: Requests per second: 197.90 Transfer rate: 4519.69 kb/s received (5 way, 2000 fetches) ) Changes from 0.0.3 to 0.0.4: - can now specify more than one listener for a service - 'client' in the config XML is now 'host' - fixed a bug in leastconns and roundrobin scheduler if all backends were unavailable. - whole lotta documentation added. - running display in web api now shows count of total connections - running display now has refresh and auto-refresh - compareconf module - takes a running config and a new config and emits the web api commands needed to make the running config match the new config - first cut at enabling https for web interface (needs m2crypto) Changes from 0.0.2 to 0.0.3: - delHost hooked up - running.xml added - XML dump of current config - centralised logging - the various things that write logfile entries need to be made consistent, and a lot of additional logging needs to be added. - Python2.1 compatibility fix: no socket.gaierror exception on 2.1 Changes from 0.0.1 to 0.0.2: - refactored web publishing (babybobo) - package-ised and distutil-ised the code ---------------------------------------------------------------------- This software is covered by the following license: Copyright (c) 2002-2004 ekit.com Inc (http://www.ekit-inc.com/) and Anthony Baxter Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. pydirector-1.0.0/TODO.txt0000644000026300012320000000464610157565432015642 0ustar anthonytech00000000000000Things that could be added: logging cleanup scheduler stats structures cleanup adminUp/adminDown host 'weighted' hosts more doco write a client that compares config xml and running xml, and updates the latter to match the former. mostly there now, see compareconf Figure out a way to restart/upgrade pydir. Idea: close down listener, start new code immediately. Let old connections on old pydir finish (run to completion) before exiting. Replace crappy manager thread and admin thread with twisted-based implementations. Caching of DNS lookups dump out curBadHosts/histBadHosts/open make the connection checker work sticky connections. keep a cache of 'client_ip':'backend_ip'. manager needs to be able to call "scheduler.cleanup()" to allow the cache to be manageable. Admin interface bits left to do: Information: [maybe] config / config.txt ? [maybe] config vs. running [maybe] reload config, making changes Host mgmt: delAllHosts?service=NNN&group=NNN Group mgmt: changeScheduler?service=NNN&group=NNN&scheduler=NNN Service mgmt: newService?service=NNN&listen=NNN newGroup?service=NNN&group=NNN&scheduler=NNN delGroup?service=NNN&group=NNN other closeLog retryHost?service=NNN&group=NNN&client=NNN hostAdminDown hostAdminUp [maybe] shutdown? multiple admins for a pydir (e.g. one http, one https) make enable optional logging options? Document the various return results from the text mode interfaces - eg running.txt additional schedulers. better testing stuff in SchedulerManager, rather than just dropping the dead entry back into the mix. In the case of "it's down", this is harmless, but nonetheless. Store away more info about the down hosts - when, how long til next test, &c. expires on the CSS. seriously refactor the CSS and also the pdadmin_running HTML. Nasty nasty. pid file? specify users not in the config.xml file, but somewhere else. what does 'host unknown' raise under 2.1? Clean up all the logging. Logging now goes to a single place, but in inconsistent format. Still, it's better than it was. Track the count of faults. If M2Crypto is installed, allow a https-based admin interface. Sort-of there now, use the undocumented 'secure="yes"' flag to the admin directive. Additional idea: connection count scheduler. could be used to track licensing of a service, or limit it. pydirector-1.0.0/confex.xml0000644000026300012320000000234410157564007016326 0ustar anthonytech00000000000000 pydirector-1.0.0/loadprof.py0000644000026300012320000000043607654435431016510 0ustar anthonytech00000000000000 import hotshot.stats print "loading stats..." stats =hotshot.stats.load("pydir.prof") print "...done" stats.strip_dirs() stats.sort_stats('time') import sys stdout = sys.stdout sys.stdout = open("profile.dump", "w") stats.print_stats() stats.print_callers() sys.stdout = stdout pydirector-1.0.0/pydir++.py0000755000026300012320000000203007654446342016155 0ustar anthonytech00000000000000#!/usr/bin/env python # main driver script for pythondirector #from twisted.internet import default #default.install() from twisted.internet import pollreactor pollreactor.install() #from twisted.internet import cReactor #cReactor.install() #from twisted.internet import gtkreactor #gtkreactor.install() #from qt import QApplication #app=QApplication([]) #from twisted.internet import qtreactor #qtreactor.install() import sys, resource def versionCheck(): if not (hasattr(sys, 'version_info') and sys.version_info > (2,1)): raise RuntimeError, "PythonDirector needs Python2.1 or greater" def main(): from pydirector.pdmain import PythonDirector resource.setrlimit(resource.RLIMIT_NOFILE, (1024, 1024)) config = sys.argv[1] pd = PythonDirector(config) pd.start(profile=1) if __name__ == "__main__": versionCheck() main() # # Copyright (c) 2002 ekit.com Inc (http://www.ekit-inc.com) # and Anthony Baxter # # $Id: pydir.py,v 1.9 2003/04/30 08:24:35 anthonybaxter Exp $ # pydirector-1.0.0/pydir.py0000644000026300012320000000126710157565473016036 0ustar anthonytech00000000000000#!/usr/bin/env python # main driver script for pythondirector import sys, resource def versionCheck(): if not (hasattr(sys, 'version_info') and sys.version_info > (2,1)): raise RuntimeError, "PythonDirector needs Python2.1 or greater" def main(): from pydirector.pdmain import PythonDirector resource.setrlimit(resource.RLIMIT_NOFILE, (1024, 1024)) config = sys.argv[1] pd = PythonDirector(config) pd.start(profile=0) if __name__ == "__main__": versionCheck() main() # # Copyright (c) 2002-2004 ekit.com Inc (http://www.ekit-inc.com) # and Anthony Baxter # # $Id: pydir.py,v 1.11 2004/12/14 13:31:39 anthonybaxter Exp $ # pydirector-1.0.0/setup.py0000644000026300012320000000224010157565307016033 0ustar anthonytech00000000000000 from distutils.core import setup from pydirector import Version # patch distutils if it can't cope with the "classifiers" keyword. # this just makes it ignore it. import sys if sys.version < '2.2.3': from distutils.dist import DistributionMetadata DistributionMetadata.classifiers = None setup( name = "pydirector", version = Version, description = "Python Director - TCP load balancer.", author = "Anthony Baxter", author_email = "anthony@interlink.com.au", url = 'http://sourceforge.net/projects/pythondirector/', packages = ['pydirector'], scripts = ['pydir.py'], classifiers = [ 'Development Status :: 5 - Production/Stable', 'Environment :: Web Environment', 'Environment :: No Input/Output (Daemon)', 'License :: OSI Approved :: Python Software Foundation License', 'Operating System :: POSIX', 'Operating System :: MacOS :: MacOS X', 'Operating System :: Microsoft', 'Programming Language :: Python', 'Intended Audience :: System Administrators', 'Intended Audience :: Developers', 'Topic :: Internet', 'Topic :: System :: Networking', ] ) pydirector-1.0.0/PKG-INFO0000644000026300012320000000151510157566420015417 0ustar anthonytech00000000000000Metadata-Version: 1.0 Name: pydirector Version: 1.0.0 Summary: Python Director - TCP load balancer. Home-page: http://sourceforge.net/projects/pythondirector/ Author: Anthony Baxter Author-email: anthony@interlink.com.au License: UNKNOWN Description: UNKNOWN Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Web Environment Classifier: Environment :: No Input/Output (Daemon) Classifier: License :: OSI Approved :: Python Software Foundation License Classifier: Operating System :: POSIX Classifier: Operating System :: MacOS :: MacOS X Classifier: Operating System :: Microsoft Classifier: Programming Language :: Python Classifier: Intended Audience :: System Administrators Classifier: Intended Audience :: Developers Classifier: Topic :: Internet Classifier: Topic :: System :: Networking