소스 검색

- We now allow supervisor to listen on both a UNIX domain socket and
an inet socket instead of making them mutually exclusive. As a
result, the options "http_port", "http_username", "http_password",
"sockchmod" and "sockchown" are no longer part of the
'[supervisord]' section configuration. These have been supplanted by
two other sections: '[unix_http_server]' and '[inet_http_server'].
You'll need to insert one or the other (depending on whether you
want to listen on a UNIX domain socket or a TCP socket respectively)
or both into your supervisord.conf file. These sections have their
own options (where applicable) for port, username, password, chmod,
and chown. See README.txt for more information about these sections.

- All supervisord command-line options related to "http_port",
"http_username", "http_password", "sockchmod" and "sockchown" have
been removed (see above point for rationale).

- The option that *used* to be 'sockchown' within the '[supervisord]'
section (and is now named 'chown' within the '[unix_http_server]'
section) used to accept a dot-separated user.group value. The
separator now must be a colon ":", e.g. "user:group". Unices allow
for dots in usernames, so this change is a bugfix.

Chris McDonough 17 년 전
부모
커밋
154648df6a

+ 69 - 28
README.txt

@@ -225,8 +225,8 @@ Components
     supervisorctl may be accessed via a browser if you start
     supervisord against an internet socket.  Visit the server URL
     (e.g. http://localhost:9001/) to view and control process status
-    through the web interface after changing the configuration file's
-    'http_port' parameter appropriately.
+    through the web interface after activating the configuration
+    file's '[inet_http_server]' section.
 
   XML-RPC Interface
 
@@ -264,26 +264,60 @@ Components
     "Configuration File ['rpcinterface:x] Section Settings" in this
     file.
 
-Configuration File '[supervisord]' Section Settings
+Configuration File '[unix_http_server]' Section Settings
 
   The supervisord.conf log file contains a section named
-  '[supervisord]' in which global settings for the supervisord process
-  should be inserted.  These are:
-
-  'http_port' -- Either a TCP host:port value or (e.g. 127.0.0.1:9001)
-  or a path to a UNIX domain socket (e.g. /tmp/supervisord.sock) on
-  which supervisor will listen for HTTP/XML-RPC requests.
-  Supervisorctl itself uses XML-RPC to communicate with supervisord
-  over this port.  To listen on all interfaces in the machine, use
-  ":9001".
-
-  'sockchmod' -- Change the UNIX permission mode bits of the http_port
-  UNIX domain socket to this value (ignored if using a TCP socket).
+  '[unix_http_server]' under which configuration parameters for an
+  HTTP server that listens on a UNIX domain socket should be inserted.
+  If the configuration file has no '[unix_http_server]' section, a
+  UNIX domain socket HTTP server will not be started.  The
+  configuration values are:
+
+  'file' -- a path to a UNIX domain socket
+  (e.g. /tmp/supervisord.sock) on which supervisor will listen for
+  HTTP/XML-RPC requests.  Supervisorctl itself uses XML-RPC to
+  communicate with supervisord over this port.
+
+  'sockchmod' -- Change the UNIX permission mode bits of the UNIX
+  domain socket to this value
   Default: 0700.
 
   'sockchown' -- Change the user and group of the socket file to this
-  value.  May be a username (e.g. chrism) or a username and group
-  separated by a dot (e.g. chrism.wheel) Default: do not change.
+  value.  May be a UNIX username (e.g. chrism) or a UNIX username and
+  group separated by a colon (e.g. chrism:wheel) Default: do not
+  change.
+
+  'username' -- the username required for authentication to this
+  HTTP server.  Default: none.
+
+  'password' -- the password required for authentication to this
+  HTTP server.  Default: none.
+
+Configuration File '[inet_http_server]' Section Settings
+
+  The supervisord.conf log file contains a section named
+  '[inet_http_server]' under which configuration parameters for an
+  HTTP server that listens on a TCP port should be inserted.  If the
+  configuration file has no '[inet_http_server]' section, a TCP socket
+  HTTP server will not be started.  The configuration values are:
+
+  'port' -- A TCP host:port value or (e.g. 127.0.0.1:9001) on which
+  supervisor will listen for HTTP/XML-RPC requests.  Supervisorctl
+  itself may use XML-RPC to communicate with supervisord over this
+  port.  To listen on all interfaces in the machine, use ":9001" or
+  "*:9001".
+
+  'username' -- the username required for authentication to this
+  HTTP server.  Default: none.
+
+  'password' -- the password required for authentication to this
+  HTTP server.  Default: none.
+
+Configuration File '[supervisord]' Section Settings
+
+  The supervisord.conf log file contains a section named
+  '[supervisord]' in which global settings for the supervisord process
+  should be inserted.  These are:
 
   'umask' -- The umask of the supervisord process.  Default: 022.
 
@@ -322,12 +356,6 @@ Configuration File '[supervisord]' Section Settings
   'nocleanup' -- prevent supervisord from clearing any existing "AUTO"
   log files at startup time.  Default: false.
 
-  'http_username' -- the username required for authentication to our
-  HTTP server.  Default: none.
-
-  'http_password' -- the password required for authentication to our
-  HTTP server.  Default: none.
-
   'childlogdir' -- the directory used for AUTO log files.  Default:
   value of Python's tempfile.get_tempdir().
 
@@ -351,7 +379,7 @@ Configuration File '[supervisord]' Section Settings
   program's "environment" configuration stanza.  See "Subprocess
   Environment" below.
 
-  'identifier' -- The identifier for this supervisor server, used by
+  'identifier' -- The identifier for this supervisor process, used by
   the RPC interface.  Default: 'supervisor'.
 
 Configuration File '[supervisorctl]' Section Settings
@@ -364,12 +392,14 @@ Configuration File '[supervisorctl]' Section Settings
   "unix:///absolute/path/to/file.sock".
 
   'username' -- The username to pass to the supervisord server for use
-  in authentication (should be same as 'http_username' in supervisord
-  config).  Optional.
+  in authentication.  This should be same as 'username' from the
+  supervisord server configuration for the port or UNIX domain socket
+  you're attempting to access. Optional.
 
   'password' -- The password to pass to the supervisord server for use
-  in authentication (should be the same as 'http_password' in
-  supervisord config).  Optional.
+  in authentication. This should be same as 'password' from the
+  supervisord server configuration for the port or UNIX domain socket
+  you're attempting to access. Optional.
 
   'prompt' -- String used as supervisorctl prompt.  Default: supervisor.
 
@@ -403,6 +433,7 @@ Configuration File '[program:x]' Section Settings
     stderr_logfile_backups=10
     stderr_capture_maxbytes=1MB
     environment=A=1,B=2
+    serverurl=AUTO
 
   '[program:foo]' -- the section header, required for each program.
   'programname' is a descriptive name (arbitrary) used to describe the
@@ -559,6 +590,16 @@ Configuration File '[program:x]' Section Settings
   'umask' -- an octal number (e.g. 002, 022) representing the umask of
   the process.  Default: no special umask (inherit supervisor's).
 
+  'serverurl' -- the URL passed in the environment to the subprocess
+  process as 'SUPERVISOR_SERVER_URL' (see supervisor.childutils) to
+  allow the subprocess to easily communicate with the internal
+  supervisord HTTP server.  If provided, it should have the same
+  syntax and structure as the '[supervisorctl]' section option of the
+  same name.  If this is set to AUTO, or is unset, supervisor will
+  automatically construct a server URL, giving preference to a server
+  that listens on UNIX domain sockets over one that listens on an
+  internet socket.  Default: AUTO.
+
   Note that a '[program:x]' section actually represents a "homogeneous
   process group" to supervisor (new in 3.0).  The members of the group
   are defined by the combination of the 'numprocs and 'process_name'

+ 21 - 0
UPGRADING.txt

@@ -29,3 +29,24 @@ Upgrading from supervisor 2 to supervisor 3
   'unexpected' (it used to be 'true', which meant restart
   unconditionally).
 
+- We now allow supervisor to listen on both a UNIX domain socket and
+  an inet socket instead of making them mutually exclusive.  As a
+  result, the options "http_port", "http_username", "http_password",
+  "sockchmod" and "sockchown" are no longer part of the
+  '[supervisord]' section configuration. These have been supplanted by
+  two other sections: '[unix_http_server]' and '[inet_http_server'].
+  You'll need to insert one or the other (depending on whether you
+  want to listen on a UNIX domain socket or a TCP socket respectively)
+  or both into your supervisord.conf file.  These sections have their
+  own options (where applicable) for port, username, password, chmod,
+  and chown.  See README.txt for more information about these sections.
+
+- All supervisord command-line options related to "http_port",
+  "http_username", "http_password", "sockchmod" and "sockchown" have
+  been removed (see above point for rationale).
+
+- The option that *used* to be 'sockchown' within the '[supervisord]'
+  section (and is now named 'chown' within the '[unix_http_server]'
+  section) used to accept a dot-separated user.group value.  The
+  separator now must be a colon ":", e.g. "user:group".  Unices allow
+  for dots in usernames, so this change is a bugfix.

+ 15 - 7
sample.conf

@@ -1,11 +1,18 @@
 ; Sample supervisor config file.
 
+[unix_http_server]
+file=/tmp/supervisor.sock   ; (the path to the socket file)
+;chmod=0700                 ; sockef file mode (default 0700)
+;chown=nobody:nogroup       ; socket file uid:gid owner
+;username=user              ; (default is no username (open server))
+;password=123               ; (default is no password (open server))
+
+;[inet_http_server]         ; inet (TCP) server disabled by default
+;port=127.0.0.1:9001        ; (ip_address:port specifier, *:port for all iface)
+;username=user              ; (default is no username (open server))
+;password=123               ; (default is no password (open server))
+
 [supervisord]
-http_port=/tmp/supervisor.sock ; (default is to run a UNIX domain socket server)
-;http_port=127.0.0.1:9001  ; (alternately, ip_address:port specifies AF_INET)
-;sockchmod=0700              ; AF_UNIX socketmode (AF_INET ignore, default 0700)
-;sockchown=nobody.nogroup     ; AF_UNIX socket uid.gid owner (AF_INET ignores)
-;umask=022                   ; (process file creation umask;default 022)
 logfile=/tmp/supervisord.log ; (main log file;default $CWD/supervisord.log)
 logfile_maxbytes=50MB       ; (max main logfile bytes b4 rotation;default 50MB)
 logfile_backups=10          ; (num of main logfile rotation backups;default 10)
@@ -14,12 +21,11 @@ pidfile=/tmp/supervisord.pid ; (supervisord pidfile;default supervisord.pid)
 nodaemon=false              ; (start in foreground if true;default false)
 minfds=1024                 ; (min. avail startup file descriptors;default 1024)
 minprocs=200                ; (min. avail process descriptors;default 200)
+;umask=022                  ; (process file creation umask;default 022)
 ;user=chrism                 ; (default is current user, required if root)
 ;identifier=supervisor       ; (supervisord identifier, default is 'supervisor')
 ;directory=/tmp              ; (default is not to cd during start)
 ;nocleanup=true              ; (don't clean up tempfiles at start;default false)
-;http_username=user          ; (default is no username (open system))
-;http_password=123           ; (default is no password (open system))
 ;childlogdir=/tmp            ; ('AUTO' child log dir, default $TEMP)
 ;environment=KEY=value       ; (key value pairs to add to environment)
 ;strip_ansi=false            ; (strip ansi escape codes in logs; def. false)
@@ -66,6 +72,7 @@ serverurl=unix:///tmp/supervisor.sock ; use a unix:// URL  for a unix socket
 ;stderr_logfile_backups=10     ; # of stderr logfile backups (default 10)
 ;stderr_capture_maxbytes=1MB   ; number of bytes in 'capturemode' (default 0)
 ;environment=A=1,B=2           ; process environment additions (def no adds)
+;serverurl=AUTO                ; override serverurl computation (childutils)
 
 ; The below sample eventlistener section shows all possible
 ; eventlistener subsection values, create one or more 'real'
@@ -97,6 +104,7 @@ serverurl=unix:///tmp/supervisor.sock ; use a unix:// URL  for a unix socket
 ;stderr_logfile_maxbytes=1MB   ; max # logfile bytes b4 rotation (default 50MB)
 ;stderr_logfile_backups        ; # of stderr logfile backups (default 10)
 ;environment=A=1,B=2           ; process environment additions
+;serverurl=AUTO                ; override serverurl computation (childutils)
 
 ; The below sample group section shows all possible group values,
 ; create one or more 'real' group: sections to create "heterogeneous"

+ 7 - 8
src/supervisor/datatypes.py

@@ -115,17 +115,16 @@ def inet_address(s):
     port = None
     if ":" in s:
         host, s = s.split(":", 1)
-        if s:
-            port = port_number(s)
+        if not s:
+            raise ValueError("no port number specified in %r" % s)
+        port = port_number(s)
         host = host.lower()
     else:
         try:
             port = port_number(s)
         except ValueError:
-            if len(s.split()) != 1:
-                raise ValueError("not a valid host name: " + repr(s))
-            host = s.lower()
-    if not host:
+            raise ValueError("not a valid port number: %r " %s)
+    if not host or host == '*':
         host = DEFAULT_HOST
     return host, port
 
@@ -140,9 +139,9 @@ class SocketAddress:
             self.family = socket.AF_INET
             self.address = inet_address(s)
 
-def dot_separated_user_group(arg):
+def colon_separated_user_group(arg):
     try:
-        result = arg.split('.', 1)
+        result = arg.split(':', 1)
         if len(result) == 1:
             username = result[0]
             uid = name_to_uid(username)

+ 65 - 61
src/supervisor/http.py

@@ -760,13 +760,8 @@ class mainlogtail_handler:
 
         request.done()
 
-def make_http_server(options, supervisord):
-    if not options.http_port:
-        return
-
-    username = options.http_username
-    password = options.http_password
-
+def make_http_servers(options, supervisord):
+    servers = []
     class LogWrapper:
         def log(self, msg):
             if msg.endswith('\n'):
@@ -774,57 +769,66 @@ def make_http_server(options, supervisord):
             options.logger.trace(msg)
     wrapper = LogWrapper()
 
-    family = options.http_port.family
-    
-    if family == socket.AF_INET:
-        host, port = options.http_port.address
-        hs = supervisor_af_inet_http_server(host, port, logger_object=wrapper)
-    elif family == socket.AF_UNIX:
-        socketname = options.http_port.address
-        sockchmod = options.sockchmod
-        sockchown = options.sockchown
-        hs = supervisor_af_unix_http_server(socketname, sockchmod, sockchown,
-                                            logger_object=wrapper)
-    else:
-        raise ValueError('Cannot determine socket type %r' % family)
-
-    from xmlrpc import supervisor_xmlrpc_handler
-    from xmlrpc import SystemNamespaceRPCInterface
-    from web import supervisor_ui_handler
-    rpcfactories = options.configroot.supervisord.rpcinterface_factories
-
-    subinterfaces = []
-    for name, factory, d in rpcfactories:
-        try:
-            inst = factory(supervisord, **d)
-        except:
-            import traceback; traceback.print_exc()
-            raise ValueError('Could not make %s rpc interface' % name)
-        subinterfaces.append((name, inst))
-        options.logger.info('RPC interface %r initialized' % name)
-        
-    subinterfaces.append(('system', SystemNamespaceRPCInterface(subinterfaces)))
-    xmlrpchandler = supervisor_xmlrpc_handler(supervisord, subinterfaces)
-    tailhandler = logtail_handler(supervisord)
-    maintailhandler = mainlogtail_handler(supervisord)
-    here = os.path.abspath(os.path.dirname(__file__))
-    templatedir = os.path.join(here, 'ui')
-    filesystem = filesys.os_filesystem(templatedir)
-    uihandler = supervisor_ui_handler(filesystem, supervisord)
-
-    if username:
-        # wrap the xmlrpc handler and tailhandler in an authentication handler
-        users = {username:password}
-        from medusa.auth_handler import auth_handler
-        xmlrpchandler = auth_handler(users, xmlrpchandler)
-        tailhandler = auth_handler(users, tailhandler)
-        maintailhandler = auth_handler(users, maintailhandler)
-        uihandler = auth_handler(users, uihandler)
-    else:
-        options.logger.critical('Running without any HTTP authentication '
-                                'checking')
-    hs.install_handler(maintailhandler)
-    hs.install_handler(tailhandler)
-    hs.install_handler(uihandler) # second-to-last for speed
-    hs.install_handler(xmlrpchandler) # last for speed (first checked)
-    return hs
+    for config in options.server_configs:
+        family = config['family']
+
+        if family == socket.AF_INET:
+            host, port = config['host'], config['port']
+            hs = supervisor_af_inet_http_server(host, port,
+                                                logger_object=wrapper)
+        elif family == socket.AF_UNIX:
+            socketname = config['file']
+            sockchmod = config['chmod']
+            sockchown = config['chown']
+            hs = supervisor_af_unix_http_server(socketname,sockchmod, sockchown,
+                                                logger_object=wrapper)
+        else:
+            raise ValueError('Cannot determine socket type %r' % family)
+
+        from xmlrpc import supervisor_xmlrpc_handler
+        from xmlrpc import SystemNamespaceRPCInterface
+        from web import supervisor_ui_handler
+
+        subinterfaces = []
+        for name, factory, d in options.rpcinterface_factories:
+            try:
+                inst = factory(supervisord, **d)
+            except:
+                import traceback; traceback.print_exc()
+                raise ValueError('Could not make %s rpc interface' % name)
+            subinterfaces.append((name, inst))
+            options.logger.info('RPC interface %r initialized' % name)
+
+        subinterfaces.append(('system',
+                              SystemNamespaceRPCInterface(subinterfaces)))
+        xmlrpchandler = supervisor_xmlrpc_handler(supervisord, subinterfaces)
+        tailhandler = logtail_handler(supervisord)
+        maintailhandler = mainlogtail_handler(supervisord)
+        here = os.path.abspath(os.path.dirname(__file__))
+        templatedir = os.path.join(here, 'ui')
+        filesystem = filesys.os_filesystem(templatedir)
+        uihandler = supervisor_ui_handler(filesystem, supervisord)
+
+        username = config['username']
+        password = config['password']
+
+        if username:
+            # wrap the xmlrpc handler and tailhandler in an authentication
+            # handler
+            users = {username:password}
+            from medusa.auth_handler import auth_handler
+            xmlrpchandler = auth_handler(users, xmlrpchandler)
+            tailhandler = auth_handler(users, tailhandler)
+            maintailhandler = auth_handler(users, maintailhandler)
+            uihandler = auth_handler(users, uihandler)
+        else:
+            options.logger.critical(
+                'Server %r running without any HTTP '
+                'authentication checking' % config['section'])
+        hs.install_handler(maintailhandler)
+        hs.install_handler(tailhandler)
+        hs.install_handler(uihandler) # second-to-last for speed
+        hs.install_handler(xmlrpchandler) # last for speed (first checked)
+        servers.append((config, hs))
+
+    return servers

+ 124 - 83
src/supervisor/options.py

@@ -50,8 +50,8 @@ from supervisor.datatypes import list_of_strings
 from supervisor.datatypes import octal_type
 from supervisor.datatypes import existing_directory
 from supervisor.datatypes import logging_level
-from supervisor.datatypes import dot_separated_user_group
-from supervisor.datatypes import SocketAddress
+from supervisor.datatypes import colon_separated_user_group
+from supervisor.datatypes import inet_address
 from supervisor.datatypes import url
 from supervisor.datatypes import Automatic
 from supervisor.datatypes import auto_restart
@@ -333,8 +333,8 @@ class ServerOptions(Options):
     nodaemon = None
     signal = None
     environment = None
-    httpserver = None
-    unlink_socketfile = True
+    httpservers = ()
+    unlink_socketfiles = True
     mood = states.SupervisorStates.RUNNING
     
     ANSI_ESCAPE_BEGIN = '\x1b['
@@ -368,12 +368,6 @@ class ServerOptions(Options):
                  existing_dirpath, default="supervisor")
         self.add("childlogdir", "supervisord.childlogdir", "q:", "childlogdir=",
                  existing_directory, default=tempfile.gettempdir())
-        self.add("http_port", "supervisord.http_port", "w:", "http_port=",
-                 SocketAddress, default=None)
-        self.add("http_username", "supervisord.http_username", "g:",
-                 "http_username=", str, default=None)
-        self.add("http_password", "supervisord.http_password", "r:",
-                 "http_password=", str, default=None)
         self.add("minfds", "supervisord.minfds",
                  "a:", "minfds=", int, default=1024)
         self.add("minprocs", "supervisord.minprocs",
@@ -384,10 +378,6 @@ class ServerOptions(Options):
                  "t", "strip_ansi", flag=1, default=0)
         self.add("profile_options", "supervisord.profile_options",
                  "", "profile_options=", profile_options, default=None)
-        self.add("sockchmod", "supervisord.sockchmod", "p:", "socket-mode=",
-                 octal_type, default=0700)
-        self.add("sockchown", "supervisord.sockchown", "o:", "socket-owner=",
-                 dot_separated_user_group)
         self.add("environment", "supervisord.environment", "b:", "environment=",
                  dict_of_key_value_pairs)
         self.pidhistory = {}
@@ -441,22 +431,35 @@ class ServerOptions(Options):
             self.pidfile = os.path.abspath(self.pidfile)
 
         self.process_group_configs = section.process_group_configs
+        self.rpcinterface_factories = section.rpcinterface_factories
 
-        if not self.sockchown:
-            self.sockchown = section.sockchown
+        self.serverurl = None
 
-        self.identifier = section.identifier
+        self.server_configs = sconfigs = section.server_configs
 
-        if section.http_port is None:
-            self.serverurl = None
+        # we need to set a fallback serverurl that process.spawn can use
 
-        else:
-            if section.http_port.family == socket.AF_INET:
-                host, port = section.http_port.address
+        # prefer a unix domain socket
+        for config in [ config for config in sconfigs if
+                        config['family'] is socket.AF_UNIX ]:
+            path = config['file']
+            self.serverurl = 'unix://%s' % path
+            break
+
+        # fall back to an inet socket
+        if self.serverurl is None:
+            for config in [ config for config in sconfigs if
+                            config['family'] is socket.AF_INET]:
+                host = config['host']
+                port = config['port']
+                if not host:
+                    host = 'localhost'
                 self.serverurl = 'http://%s:%s' % (host, port)
-            else:
-                # domain socket
-                self.serverurl = 'unix://%s' % section.http_port.address
+
+        # self.serverurl may still be None if no servers at all are
+        # configured in the config file
+
+        self.identifier = section.identifier
 
     def convert_sockchown(self, sockchown):
         # Convert chown stuff to uid/gid
@@ -508,50 +511,13 @@ class ServerOptions(Options):
 
         tempdir = tempfile.gettempdir()
         section.childlogdir = existing_directory(get('childlogdir', tempdir))
-
-        http_port = get('http_port', None)
-        if http_port is None:
-            section.http_port = None
-        else:
-            section.http_port = SocketAddress(http_port)
-
-        http_password = get('http_password', None)
-        http_username = get('http_username', None)
-        if http_password or http_username:
-            if http_password is None:
-                raise ValueError('Must specify http_password if '
-                                 'http_username is specified')
-            if http_username is None:
-                raise ValueError('Must specify http_username if '
-                                 'http_password is specified')
-        section.http_password = http_password
-        section.http_username = http_username
-
         section.nocleanup = boolean(get('nocleanup', 'false'))
-
         section.strip_ansi = boolean(get('strip_ansi', 'false'))
-        
-        sockchown = get('sockchown', None)
-        if sockchown is None:
-            section.sockchown = (-1, -1)
-        else:
-            try:
-                section.sockchown = dot_separated_user_group(sockchown)
-            except ValueError:
-                raise ValueError('Invalid sockchown value %s' % sockchown)
-
-        sockchmod = get('sockchmod', None)
-        if sockchmod is None:
-            section.sockchmod = 0700
-        else:
-            try:
-                section.sockchmod = octal_type(sockchmod)
-            except (TypeError, ValueError):
-                raise ValueError('Invalid sockchmod value %s' % sockchmod)
 
         section.environment = dict_of_key_value_pairs(get('environment', ''))
         section.process_group_configs = self.process_groups_from_parser(parser)
         section.rpcinterface_factories = self.rpcinterfaces_from_parser(parser)
+        section.server_configs = self.server_configs_from_parser(parser)
         section.profile_options = None
         return section
 
@@ -654,6 +620,9 @@ class ServerOptions(Options):
         stdout_cmaxbytes = byte_size(get(section,'stdout_capture_maxbytes','0'))
         stderr_cmaxbytes = byte_size(get(section,'stderr_capture_maxbytes','0'))
         directory = get(section, 'directory', None)
+        serverurl = get(section, 'serverurl', None)
+        if serverurl and serverurl.strip().upper() == 'AUTO':
+            serverurl = None
 
         umask = get(section, 'umask', None)
         if umask is not None:
@@ -733,7 +702,8 @@ class ServerOptions(Options):
                 stopwaitsecs=stopwaitsecs,
                 exitcodes=exitcodes,
                 redirect_stderr=redirect_stderr,
-                environment=environment)
+                environment=environment,
+                serverurl=serverurl)
 
             programs.append(pconfig)
 
@@ -747,9 +717,7 @@ class ServerOptions(Options):
         for section in parser.sections():
             if not section.startswith('rpcinterface:'):
                 continue
-            options = parser.options(section)
             name = section.split(':', 1)[1]
-            realoptions = []
             factory_spec = parser.saneget(section, factory_key, None)
             if factory_spec is None:
                 raise ValueError('section [%s] does not specify a %s'  %
@@ -765,6 +733,80 @@ class ServerOptions(Options):
 
         return factories
 
+    def _parse_servernames(self, parser, stype):
+        options = []
+        for section in parser.sections():
+            if section.startswith(stype):
+                parts = section.split(':', 1)
+                if len(parts) > 1:
+                    name = parts[1]
+                else:
+                    name = None # default sentinel
+                options.append((name, section))
+        return options
+
+    def _parse_username_and_password(self, parser, section):
+        get = parser.saneget
+        username = get(section, 'username', None)
+        password = get(section, 'password', None)
+        if username is None and password is not None:
+            raise ValueError(
+                'Must specify username if password is specified in [%s]'
+                % section)
+        return {'username':username, 'password':password}
+
+    def server_configs_from_parser(self, parser):
+        configs = []
+        inet_serverdefs = self._parse_servernames(parser, 'inet_http_server')
+        for name, section in inet_serverdefs:
+            config = {}
+            get = parser.saneget
+            config.update(self._parse_username_and_password(parser, section))
+            config['name'] = name
+            config['family'] = socket.AF_INET
+            port = get(section, 'port', None)
+            if port is None:
+                raise ValueError('section [%s] has no port value' % section)
+            host, port = inet_address(port)
+            config['host'] = host
+            config['port'] = port
+            config['section'] = section
+            configs.append(config)
+
+        unix_serverdefs = self._parse_servernames(parser, 'unix_http_server')
+        for name, section in unix_serverdefs:
+            config = {}
+            get = parser.saneget
+            sfile = get(section, 'file', None)
+            if sfile is None:
+                raise ValueError('section [%s] has no file value' % section)
+            config['name'] = name
+            config['family'] = socket.AF_UNIX
+            config['file'] = sfile.strip()
+            config.update(self._parse_username_and_password(parser, section))
+            chown = get(section, 'chown', None)
+            if chown is not None:
+                try:
+                    chown = colon_separated_user_group(chown)
+                except ValueError:
+                    raise ValueError('Invalid sockchown value %s' % chown)
+            else:
+                chown = (-1, -1)
+            config['chown'] = chown
+            chmod = get(section, 'chmod', None)
+            if chmod is not None:
+                try:
+                    chmod = octal_type(chmod)
+                except (TypeError, ValueError):
+                    raise ValueError('Invalid chmod value %s' % chmod)
+            else:
+                chmod = 0700
+            config['chmod'] = chmod
+            config['section'] = section
+            configs.append(config)
+
+        return configs
+
     def import_spec(self, spec):
         return pkg_resources.EntryPoint.parse("x="+spec).load(False)
 
@@ -833,15 +875,14 @@ class ServerOptions(Options):
                 
     def cleanup(self):
         try:
-            if self.http_port is not None:
-                if self.http_port.family == socket.AF_UNIX:
-                    if self.httpserver is not None:
-                        if self.unlink_socketfile:
-                            socketname = self.http_port.address
-                            try:
-                                os.unlink(socketname)
-                            except OSError:
-                                pass
+            for config, server in self.httpservers:
+                if config['family'] == socket.AF_UNIX:
+                    if self.unlink_socketfiles:
+                        socketname = config['file']
+                        try:
+                            os.unlink(socketname)
+                        except OSError:
+                            pass
         except OSError:
             pass
         try:
@@ -860,19 +901,18 @@ class ServerOptions(Options):
     def sigreceiver(self, sig, frame):
         self.signal = sig
 
-    def openhttpserver(self, supervisord):
-        from http import make_http_server
+    def openhttpservers(self, supervisord):
+        from supervisor.http import make_http_servers
         try:
-            self.httpserver = make_http_server(self, supervisord)
+            self.httpservers = make_http_servers(self, supervisord)
         except socket.error, why:
             if why[0] == errno.EADDRINUSE:
-                port = str(self.http_port.address)
                 self.usage('Another program is already listening on '
-                           'the port that our HTTP server is '
-                           'configured to use (%s).  Shut this program '
+                           'a port that one of our HTTP servers is '
+                           'configured to use.  Shut this program '
                            'down first before starting supervisord. ' %
                            port)
-            self.unlink_socketfile = False
+            self.unlink_socketfiles = False
         except ValueError, why:
             self.usage(why[0])
 
@@ -1332,7 +1372,7 @@ class ProcessConfig(Config):
                  stderr_logfile, stderr_capture_maxbytes,
                  stderr_logfile_backups, stderr_logfile_maxbytes,
                  stopsignal, stopwaitsecs, exitcodes, redirect_stderr,
-                 environment=None):
+                 environment=None, serverurl=None):
         self.options = options
         self.name = name
         self.command = command
@@ -1357,6 +1397,7 @@ class ProcessConfig(Config):
         self.exitcodes = exitcodes
         self.redirect_stderr = redirect_stderr
         self.environment = environment
+        self.serverurl = serverurl
 
     def create_autochildlogs(self):
         # temporary logfiles which are erased at start time

+ 3 - 1
src/supervisor/process.py

@@ -276,7 +276,9 @@ class Subprocess:
                 options.write(2, "(%s)\n" % msg)
             env = os.environ.copy()
             env['SUPERVISOR_ENABLED'] = '1'
-            serverurl = self.config.options.serverurl
+            serverurl = self.config.serverurl
+            if serverurl is None: # unset
+                serverurl = self.config.options.serverurl # might still be None
             if serverurl:
                 env['SUPERVISOR_SERVER_URL'] = serverurl
             env['SUPERVISOR_PROCESS_NAME'] = self.config.name

+ 1 - 4
src/supervisor/supervisord.py

@@ -32,9 +32,6 @@ Options:
 -q/--childlogdir DIRECTORY -- the log directory for child process logs
 -k/--nocleanup --  prevent the process from performing cleanup (removal of
                    old automatic child log files) at startup.
--w/--http_port SOCKET -- the host/port that the HTTP server should listen on
--g/--http_username STR -- the username for HTTP auth
--r/--http_password STR -- the password for HTTP auth
 -a/--minfds NUM -- the minimum number of file descriptors for start success
 -t/--strip_ansi -- strip ansi escape codes from process output
 --minprocs NUM  -- the minimum number of processes available for start success
@@ -104,7 +101,7 @@ class Supervisor:
                 name = config.name
                 self.process_groups[name] = config.make_group()
             self.options.process_environment()
-            self.options.openhttpserver(self)
+            self.options.openhttpservers(self)
             self.options.setsignals()
             if not self.options.nodaemon:
                 self.options.daemonize()

+ 5 - 4
src/supervisor/tests/base.py

@@ -29,7 +29,7 @@ class DummyOptions:
         self.fds_cleaned_up = False
         self.rlimit_set = False
         self.setuid_called = False
-        self.httpserver_opened = False
+        self.httpservers_opened = False
         self.signals_set = False
         self.daemonized = False
         self.make_logger_messages = None
@@ -90,8 +90,8 @@ class DummyOptions:
         self.setuid_called = True
         return 'setuid_called'
 
-    def openhttpserver(self, supervisord):
-        self.httpserver_opened = True
+    def openhttpservers(self, supervisord):
+        self.httpservers_opened = True
 
     def daemonize(self):
         self.daemonized = True
@@ -422,7 +422,7 @@ class DummyPConfig:
                  stderr_logfile_backups=0, stderr_logfile_maxbytes=0,
                  redirect_stderr=False,
                  stopsignal=None, stopwaitsecs=10,
-                 exitcodes=(0,2), environment=None):
+                 exitcodes=(0,2), environment=None, serverurl=None):
         self.options = options
         self.name = name
         self.command = command
@@ -451,6 +451,7 @@ class DummyPConfig:
         self.directory = directory
         self.umask = umask
         self.autochildlogs_created = False
+        self.serverurl = serverurl
 
     def create_autochildlogs(self):
         self.autochildlogs_created = True

+ 25 - 17
src/supervisor/tests/test_options.py

@@ -23,10 +23,12 @@ class ServerOptionsTests(unittest.TestCase):
         return self._getTargetClass()()
         
     def test_options(self):
-        s = lstrip("""[supervisord]
-        http_port=127.0.0.1:8999
-        http_username=chrism
-        http_password=foo   
+        s = lstrip("""[inet_http_server]
+        port=127.0.0.1:8999
+        username=chrism
+        password=foo
+
+        [supervisord]
         directory=%(tempdir)s
         backofflimit=10
         user=root
@@ -98,10 +100,12 @@ class ServerOptionsTests(unittest.TestCase):
         self.assertEqual(options.nodaemon, True)
         self.assertEqual(options.identifier, 'fleeb')
         self.assertEqual(options.childlogdir, tempfile.gettempdir())
-        self.assertEqual(options.http_port.family, socket.AF_INET)
-        self.assertEqual(options.http_port.address, ('127.0.0.1', 8999))
-        self.assertEqual(options.http_username, 'chrism')
-        self.assertEqual(options.http_password, 'foo')
+        self.assertEqual(len(options.server_configs), 1)
+        self.assertEqual(options.server_configs[0]['family'], socket.AF_INET)
+        self.assertEqual(options.server_configs[0]['host'], '127.0.0.1')
+        self.assertEqual(options.server_configs[0]['port'], 8999)
+        self.assertEqual(options.server_configs[0]['username'], 'chrism')
+        self.assertEqual(options.server_configs[0]['password'], 'foo')
         self.assertEqual(options.nocleanup, True)
         self.assertEqual(options.minfds, 2048)
         self.assertEqual(options.minprocs, 300)
@@ -219,10 +223,14 @@ class ServerOptionsTests(unittest.TestCase):
         self.assertEqual(instance.passwdfile, None)
         self.assertEqual(instance.identifier, 'fleeb')
         self.assertEqual(instance.childlogdir, tempfile.gettempdir())
-        self.assertEqual(instance.http_port.family, socket.AF_INET)
-        self.assertEqual(instance.http_port.address, ('127.0.0.1', 8999))
-        self.assertEqual(instance.http_username, 'chrism')
-        self.assertEqual(instance.http_password, 'foo')
+
+        self.assertEqual(len(instance.server_configs), 1)
+        self.assertEqual(instance.server_configs[0]['family'], socket.AF_INET)
+        self.assertEqual(instance.server_configs[0]['host'], '127.0.0.1')
+        self.assertEqual(instance.server_configs[0]['port'], 8999)
+        self.assertEqual(instance.server_configs[0]['username'], 'chrism')
+        self.assertEqual(instance.server_configs[0]['password'], 'foo')
+
         self.assertEqual(instance.nocleanup, True)
         self.assertEqual(instance.minfds, 2048)
         self.assertEqual(instance.minprocs, 300)
@@ -267,8 +275,8 @@ class ServerOptionsTests(unittest.TestCase):
             address = fn
         class Server:
             pass
-        instance.http_port = Port()
-        instance.httpserver = Server()
+        instance.httpservers = [({'family':socket.AF_UNIX, 'file':fn},
+                                 Server())]
         instance.pidfile = ''
         instance.cleanup()
         self.failIf(os.path.exists(fn))
@@ -285,10 +293,10 @@ class ServerOptionsTests(unittest.TestCase):
                 address = fn
             class Server:
                 pass
-            instance.http_port = Port()
-            instance.httpserver = Server()
+            instance.httpservers = [({'family':socket.AF_UNIX, 'file':fn},
+                                     Server())]
             instance.pidfile = ''
-            instance.unlink_socketfile = False
+            instance.unlink_socketfiles = False
             instance.cleanup()
             self.failUnless(os.path.exists(fn))
         finally:

+ 3 - 5
src/supervisor/tests/test_supervisord.py

@@ -24,11 +24,10 @@ class EntryPointTests(unittest.TestCase):
         old_stdout = sys.stdout
         try:
             tempdir = tempfile.mkdtemp()
-            sock = os.path.join(tempdir, 'sock')
             log = os.path.join(tempdir, 'log')
             pid = os.path.join(tempdir, 'pid')
             sys.stdout = new_stdout
-            main(args=['-c', conf, '-w', sock, '-l', log, '-j', pid, '-n'],
+            main(args=['-c', conf, '-l', log, '-j', pid, '-n'],
                  test=True)
         finally:
             sys.stdout = old_stdout
@@ -46,11 +45,10 @@ class EntryPointTests(unittest.TestCase):
         old_stdout = sys.stdout
         try:
             tempdir = tempfile.mkdtemp()
-            sock = os.path.join(tempdir, 'sock')
             log = os.path.join(tempdir, 'log')
             pid = os.path.join(tempdir, 'pid')
             sys.stdout = new_stdout
-            main(args=['-c', conf, '-w', sock, '-l', log, '-j', pid, '-n',
+            main(args=['-c', conf, '-l', log, '-j', pid, '-n',
                        '--profile_options=cumulative,calls'], test=True)
         finally:
             sys.stdout = old_stdout
@@ -90,7 +88,7 @@ class SupervisordTests(unittest.TestCase):
         self.assertEqual(supervisord.process_groups['foo'].config.options,
                          options)
         self.assertEqual(options.environment_processed, True)
-        self.assertEqual(options.httpserver_opened, True)
+        self.assertEqual(options.httpservers_opened, True)
         self.assertEqual(options.signals_set, True)
         self.assertEqual(options.daemonized, True)
         self.assertEqual(options.pidfile_written, True)