[Zope-Checkins] CVS: Zope/lib/python/Zope/Startup - __init__.py:1.7.2.2 zopectl.py:1.3.2.5

Chris McDonough chrism@zope.com
Sat, 2 Aug 2003 01:48:45 -0400


Update of /cvs-repository/Zope/lib/python/Zope/Startup
In directory cvs.zope.org:/tmp/cvs-serv17848/lib/python/Zope/Startup

Modified Files:
      Tag: Zope-2_7-branch
	__init__.py zopectl.py 
Log Message:
Merge from HEAD:

Refactor start_zope function and add tests for its functionality.

Changes:

 - startup log handler now pays attention to the logging levels of
   the handlers defined within the config file and uses the "lowest"
   level to log messages to stdout during startup.

 - entirely removed warning when the starting user's umask is "too
   permissive".  it wasn't clear that it added any value under normal
   operations.

 - replaced ancient setuid code with code stolen from zdaemon that
   works the same but looks nicer.



=== Zope/lib/python/Zope/Startup/__init__.py 1.7.2.1 => 1.7.2.2 ===
--- Zope/lib/python/Zope/Startup/__init__.py:1.7.2.1	Mon Jul 21 12:37:47 2003
+++ Zope/lib/python/Zope/Startup/__init__.py	Sat Aug  2 01:48:10 2003
@@ -21,153 +21,254 @@
 
 import ZConfig
 
-def start_zope(cfg):
-    # set up our initial logging environment (log everything to stderr
-    # if we're not in debug mode).
-    import zLOG
-    import logging
-
-    # don't initialize the event logger from the environment
-    zLOG._call_initialize = 0
-
-    from zLOG.LogHandlers import StartupHandler
+started = False
 
+def start_zope(cfg):
+    """ The function called by run.py which starts a Zope appserver """
+    global started
+    if started:
+        # dont allow any code to call start_zope twice.
+        return
+
+    check_python_version()
+    starter = ZopeStarter(cfg)
+    starter.setupLocale()
     # we log events to the root logger, which is backed by a
     # "StartupHandler" log handler.  The "StartupHandler" outputs to
     # stderr but also buffers log messages.  When the "real" loggers
     # are set up, we flush accumulated messages in StartupHandler's
     # buffers to the real logger.
-    startup_handler = StartupHandler(sys.stderr)
-    formatter = zLOG.EventLogger.formatters['file']
-    startup_handler.setFormatter(formatter)
-    if not cfg.debug_mode:
-        # prevent startup messages from going to stderr if we're not
-        # in debug mode
-        if os.path.exists('/dev/null'): # unix
-            devnull = '/dev/null'
-        else: # win32
-            devnull = 'nul:'
-        startup_handler = StartupHandler(open(devnull, 'w'))
-
-    # set up our event logger temporarily with a startup handler
-    event_logger = zLOG.EventLogger.EventLogger.logger
-    event_logger.addHandler(startup_handler)
-    # set the initial logging level to INFO (this will be changed by the
-    # zconfig settings later)
-    event_logger.level = logging.INFO
-
-    # set a locale if one has been specified in the config
-    if cfg.locale:
-        do_locale(cfg.locale)
-
-    # Increase the number of threads
-    import ZServer
-    ZServer.setNumberOfThreads(cfg.zserver_threads)
-
-    # Start ZServer servers before we setuid so we can bind to low ports:
-    socket_err = (
-        'There was a problem starting a server of type "%s". '
-        'This may mean that your user does not have permission to '
-        'bind to the port which the server is trying to use or the '
-        'port may already be in use by another application. '
-        '(%s)'
-        )
-    servers = []
-    for server in cfg.servers:
-        # create the server from the server factory
-        # set up in the config
-        try:
-            servers.append(server.create())
-        except socket.error,e:
-            raise ZConfig.ConfigurationError(socket_err
-                                             % (server.servertype(),e[1]))
-    cfg.servers = servers
-
-    # do stuff that only applies to posix platforms (setuid mainly)
-    if os.name == 'posix':
-        do_posix_stuff(cfg)
-
-    # Import Zope
-    import Zope
-    Zope.startup()
-
-    # this is a bit of a white lie, since we haven't actually successfully
-    # started yet, but we're pretty close and we want this output to
-    # go to the startup logger in order to prevent the kinds of email messages 
+    starter.setupStartupHandler()
+    # Start ZServer servers before we drop privileges so we can bind to
+    # "low" ports:
+    starter.setupZServerThreads()
+    starter.setupServers()
+    # drop privileges after setting up servers
+    starter.dropPrivileges()
+    starter.makeLockFile()
+    starter.makePidFile()
+    starter.startZope()
+    starter.registerSignals()
+    # emit a "ready" message in order to prevent the kinds of emails
     # to the Zope maillist in which people claim that Zope has "frozen"
-    # after it has emitted ZServer messages ;-)
-    zLOG.LOG('Zope', zLOG.INFO, 'Ready to handle requests')
+    # after it has emitted ZServer messages.
+    starter.info('Ready to handle requests')
+    starter.removeStartupHandler()
+    starter.setupConfiguredLoggers()
+    starter.flushStartupHandlerBuffer()
 
-    if not cfg.zserver_read_only_mode:
-        # lock_file is used for the benefit of zctl-like systems, so they
-        # can tell whether Zope is already running before attempting to fire
-        # it off again.
-        #
-        # We aren't concerned about locking the file to protect against
-        # other Zope instances running from our CLIENT_HOME, we just
-        # try to lock the file to signal that zctl should not try to
-        # start Zope if *it* can't lock the file; we don't panic
-        # if we can't lock it.
-        # we need a separate lock file because on win32, locks are not
-        # advisory, otherwise we would just use the pid file
-        from Zope.Startup.misc.lock_file import lock_file
-        lock_filename = cfg.lock_filename
-        try:
-            if os.path.exists(lock_filename):
-                os.unlink(lock_filename)
-            LOCK_FILE = open(lock_filename, 'w')
-            lock_file(LOCK_FILE)
-            LOCK_FILE.write(str(os.getpid()))
-            LOCK_FILE.flush()
-        except IOError:
-            pass
-
-        # write the pid into the pidfile if possible
-        pid_filename = cfg.pid_filename
-        try:
-            if os.path.exists(pid_filename):
-                os.unlink(pid_filename)
-            f = open(pid_filename, 'w')
-            f.write(str(os.getpid()))
-            f.close()
-        except IOError:
-            pass
-        
-        # Now that we've successfully setuid'd, we can log to
-        # somewhere other than stderr.  Activate the configured logs:
-        if cfg.access is not None:
-            cfg.access()
-        if cfg.trace is not None:
-            cfg.trace()
-
-        # flush buffered startup messages to event logger
-        event_logger.removeHandler(startup_handler)
-        if cfg.eventlog is not None:
-            logger = cfg.eventlog()
-            startup_handler.flushBufferTo(logger)
+    started = True
 
-    # Start Medusa, Ye Hass!
+    # the mainloop.
     try:
+        import ZServer
         import Lifetime
         Lifetime.loop()
         sys.exit(ZServer.exit_code)
     finally:
-        if not cfg.zserver_read_only_mode:
+        starter.unlinkLockFile()
+        starter.unlinkPidFile()
+        started = False
+
+class ZopeStarter:
+    """ This is a class which starts a Zope server.  Making it a class
+    makes it easier to unit test. """
+    def __init__(self, cfg):
+        self.cfg = cfg
+        import zLOG
+        # don't initialize the event logger from the environment
+        zLOG._call_initialize = 0
+        self.event_logger = zLOG.EventLogger.EventLogger.logger
+
+    def info(self, msg):
+        import zLOG
+        zLOG.LOG('Zope', zLOG.INFO, msg)
+
+    def panic(self, msg):
+        import zLOG
+        zLOG.LOG('Zope', zLOG.PANIC, msg)
+
+    def error(self, msg):
+        import zLOG
+        zLOG.LOG('Zope', zLOG.ERROR, msg)
+
+    def registerSignals(self):
+        if os.name == 'posix':
+            from Signals import Signals
+            Signals.registerZopeSignals()
+
+    def setupStartupHandler(self):
+        # set up our initial logging environment (log everything to stderr
+        # if we're not in debug mode).
+        import zLOG
+
+        from zLOG.LogHandlers import StartupHandler
+        import logging
+
+        if self.cfg.eventlog is not None:
+            # get the lowest handler level.  This is the effective level
+            # level at which which we will spew messages to the console
+            # during startup.
+            level = self.cfg.eventlog.getLowestHandlerLevel()
+        else:
+            level = logging.INFO
+
+        self.startup_handler = StartupHandler(sys.stderr)
+        self.startup_handler.setLevel(level)
+        formatter = zLOG.EventLogger.formatters['file']
+        self.startup_handler.setFormatter(formatter)
+        if not self.cfg.debug_mode:
+            # prevent startup messages from going to stderr if we're not
+            # in debug mode
+            if os.path.exists('/dev/null'): # unix
+                devnull = '/dev/null'
+            else: # win32
+                devnull = 'nul:'
+            self.startup_handler = StartupHandler(open(devnull, 'w'))
+
+        # set up our event logger temporarily with a startup handler only
+        self.event_logger.handlers = []
+        self.event_logger.addHandler(self.startup_handler)
+        # set the initial logging level (this will be changed by the
+        # zconfig settings later)
+        self.event_logger.level = level
+
+    def setupLocale(self):
+        # set a locale if one has been specified in the config
+        if not self.cfg.locale:
+            return
+
+        # workaround to allow unicode encoding conversions in DTML
+        import codecs
+        dummy = codecs.lookup('iso-8859-1')
+
+        locale_id = self.cfg.locale
+
+        if locale_id is not None:
+            try:
+                import locale
+            except:
+                raise ZConfig.ConfigurationError(
+                    'The locale module could not be imported.\n'
+                    'To use localization options, you must ensure\n'
+                    'that the locale module is compiled into your\n'
+                    'Python installation.'
+                    )
+            try:
+                locale.setlocale(locale.LC_ALL, locale_id)
+            except:
+                raise ZConfig.ConfigurationError(
+                    'The specified locale "%s" is not supported by your'
+                    'system.\nSee your operating system documentation for '
+                    'more\ninformation on locale support.' % locale_id
+                    )
+
+    def setupZServerThreads(self):
+        # Increase the number of threads
+        import ZServer
+        ZServer.setNumberOfThreads(self.cfg.zserver_threads)
+
+    def setupServers(self):
+        socket_err = (
+            'There was a problem starting a server of type "%s". '
+            'This may mean that your user does not have permission to '
+            'bind to the port which the server is trying to use or the '
+            'port may already be in use by another application. '
+            '(%s)'
+            )
+        servers = []
+        for server in self.cfg.servers:
+            # create the server from the server factory
+            # set up in the config
+            try:
+                servers.append(server.create())
+            except socket.error,e:
+                raise ZConfig.ConfigurationError(socket_err
+                                                 % (server.servertype(),e[1]))
+        self.cfg.servers = servers
+
+    def dropPrivileges(self):
+        return dropPrivileges(self.cfg)
+
+    def removeStartupHandler(self):
+        if self.startup_handler in self.event_logger.handlers:
+            self.event_logger.removeHandler(self.startup_handler)
+
+    def setupConfiguredLoggers(self):
+        if self.cfg.zserver_read_only_mode:
+            # no log files written in read only mode
+            return
+
+        # flush buffered startup messages to event logger
+        if self.cfg.eventlog is not None:
+            self.cfg.eventlog()
+        if self.cfg.access is not None:
+            self.cfg.access()
+        if self.cfg.trace is not None:
+            self.cfg.trace()
+
+    def flushStartupHandlerBuffer(self):
+        import logging
+        logger = logging.getLogger('event')
+        self.startup_handler.flushBufferTo(logger)
+
+    def startZope(self):
+        # Import Zope
+        import Zope
+        Zope.startup()
+
+    def makeLockFile(self):
+        if not self.cfg.zserver_read_only_mode:
+            # lock_file is used for the benefit of zctl-like systems, so they
+            # can tell whether Zope is already running before attempting to
+            # fire it off again.
+            #
+            # We aren't concerned about locking the file to protect against
+            # other Zope instances running from our CLIENT_HOME, we just
+            # try to lock the file to signal that zctl should not try to
+            # start Zope if *it* can't lock the file; we don't panic
+            # if we can't lock it.
+            # we need a separate lock file because on win32, locks are not
+            # advisory, otherwise we would just use the pid file
+            from Zope.Startup.misc.lock_file import lock_file
+            lock_filename = self.cfg.lock_filename
+            try:
+                if os.path.exists(lock_filename):
+                    os.unlink(lock_filename)
+                self.lockfile = open(lock_filename, 'w')
+                lock_file(self.lockfile)
+                self.lockfile.write(str(os.getpid()))
+                self.lockfile.flush()
+            except IOError:
+                pass
+
+    def makePidFile(self):
+        if not self.cfg.zserver_read_only_mode:
+            # write the pid into the pidfile if possible
+            try:
+                if os.path.exists(self.cfg.pid_filename):
+                    os.unlink(self.cfg.pid_filename)
+                f = open(self.cfg.pid_filename, 'w')
+                f.write(str(os.getpid()))
+                f.close()
+            except IOError:
+                pass
+
+    def unlinkPidFile(self):
+        if not self.cfg.zserver_read_only_mode:
             try:
-                os.unlink(pid_filename)
+                os.unlink(self.cfg.pid_filename)
             except OSError:
                 pass
+
+    def unlinkLockFile(self):
+        if not self.cfg.zserver_read_only_mode:
             try:
-                LOCK_FILE.close()
-                os.unlink(lock_filename)
+                self.lockfile.close()
+                os.unlink(self.cfg.lock_filename)
             except OSError:
                 pass
 
-def _warn_nobody():
-    import zLOG
-    zLOG.LOG("Zope", zLOG.INFO, ("Running Zope as 'nobody' can compromise "
-                                 "your Zope files; consider using a "
-                                 "dedicated user account for Zope"))
 
 def check_python_version():
     # check for Python version
@@ -184,94 +285,61 @@
                    (python_version, optimum_version))
             sys.stderr.write(err)
 
-def do_posix_stuff(cfg):
+def dropPrivileges(cfg):
+    # Drop root privileges if we have them and we're on a posix platform.
+    # This needs to be a function so it may be used outside of Zope
+    # appserver startup (e.g. from zopectl debug)
+    if os.name != 'posix':
+        return
+
+    if os.getuid() != 0:
+        return
+
     import zLOG
-    import zdaemon
     import pwd
-    from Signals import Signals
-    Signals.registerZopeSignals()
 
-    # Warn if we were started as nobody.
-    if os.getuid():
-        if pwd.getpwuid(os.getuid())[0] == 'nobody':
-            _warn_nobody()
-
-    # Drop root privileges if we have them, and do some sanity checking
-    # to make sure we're not starting with an obviously insecure setup.
-    if os.getuid() == 0:
-        UID  = cfg.effective_user
-        if UID == None:
-            msg = ('A user was not specified to setuid to; fix this to '
-                   'start as root (change the effective_user directive '
-                   'in zope.conf)')
-            zLOG.LOG('Zope', zLOG.PANIC, msg)
-            raise ZConfig.ConfigurationError(msg)
-        # stuff about client home faults removed (real effective user
-        # support now)
-        try:
-            UID = int(UID)
-        except (TypeError, ValueError):
-            pass
-        gid = None
-        if isinstance(UID, str):
-            uid = pwd.getpwnam(UID)[2]
-            gid = pwd.getpwnam(UID)[3]
-        elif isinstance(UID, int):
-            uid = pwd.getpwuid(UID)[2]
-            gid = pwd.getpwuid(UID)[3]
-            UID = pwd.getpwuid(UID)[0]
-        else:
-            zLOG.LOG("Zope", zLOG.ERROR, ("Can't find UID %s" % UID))
-            raise ZConfig.ConfigurationError('Cant find UID %s' % UID)
-        if UID == 'nobody':
-            _warn_nobody()
-        if gid is not None:
-            try:
-                import initgroups
-                initgroups.initgroups(UID, gid)
-                os.setgid(gid)
-            except OSError:
-                zLOG.LOG("Zope", zLOG.INFO,
-                         'Could not set group id of effective user',
-                         error=sys.exc_info())
-        os.setuid(uid)
-        zLOG.LOG("Zope", zLOG.INFO,
-                 'Set effective user to "%s"' % UID)
+    effective_user  = cfg.effective_user
+    if effective_user is None:
+        msg = ('A user was not specified to setuid to; fix this to '
+               'start as root (change the effective-user directive '
+               'in zope.conf)')
+        zLOG.LOG('Zope', zLOG.PANIC, msg)
+        raise ZConfig.ConfigurationError(msg)
 
-    if not cfg.debug_mode:
-        # umask is silly, blame POSIX.  We have to set it to get its value.
-        current_umask = os.umask(0)
-        os.umask(current_umask)
-        if current_umask != 077:
-            current_umask = '%03o' % current_umask
-            zLOG.LOG("Zope", zLOG.INFO, (
-                'Your umask of %s may be too permissive; for the security of '
-                'your Zope data, it is recommended you use 077' % current_umask
-                ))
-
-
-def do_locale(locale_id):
-    # workaround to allow unicode encoding conversions in DTML
-    import codecs
-    dummy = codecs.lookup('iso-8859-1')
-
-    if locale_id is not None:
+    try:
+        uid = int(effective_user)
+    except ValueError:
         try:
-            import locale
-        except:
-            raise ZConfig.ConfigurationError(
-                'The locale module could not be imported.\n'
-                'To use localization options, you must ensure\n'
-                'that the locale module is compiled into your\n'
-                'Python installation.'
-                )
+            pwrec = pwd.getpwnam(effective_user)
+        except KeyError:
+            msg = "Can't find username %r" % effective_user
+            zLOG.LOG("Zope", zLOG.ERROR, msg)
+            raise ZConfig.ConfigurationError(msg)
+        uid = pwrec[2]
+    else:
         try:
-            locale.setlocale(locale.LC_ALL, locale_id)
-        except:
-            raise ZConfig.ConfigurationError(
-                'The specified locale "%s" is not supported by your system.\n'
-                'See your operating system documentation for more\n'
-                'information on locale support.' % locale_id
-                )
+            pwrec = pwd.getpwuid(uid)
+        except KeyError:
+            msg = "Can't find uid %r" % uid
+            zLOG.LOG("Zope", zLOG.ERROR, msg)
+            raise ZConfig.ConfigurationError(msg)
+    gid = pwrec[3]
+
+    if uid == 0:
+        msg = 'Cannot start Zope with the effective user as the root user'
+        zLOG.LOG('Zope', zLOG.INFO, msg)
+        raise ZConfig.ConfigurationError(msg)
 
+    try:
+        import initgroups
+        initgroups.initgroups(effective_user, gid)
+        os.setgid(gid)
+    except OSError:
+        zLOG.LOG("Zope", zLOG.INFO,
+                 'Could not set group id of effective user',
+                 error=sys.exc_info())
 
+    os.setuid(uid)
+    zLOG.LOG("Zope", zLOG.INFO,
+             'Set effective user to "%s"' % effective_user)
+    return 1 # for unit testing purposes 


=== Zope/lib/python/Zope/Startup/zopectl.py 1.3.2.4 => 1.3.2.5 ===
--- Zope/lib/python/Zope/Startup/zopectl.py:1.3.2.4	Sun Jul 27 20:11:14 2003
+++ Zope/lib/python/Zope/Startup/zopectl.py	Sat Aug  2 01:48:10 2003
@@ -142,8 +142,8 @@
                     'opts.realize(); '
                     'h.handleConfig(opts.configroot,opts.confighandlers);'
                     'config.setConfiguration(opts.configroot); '
-                    'from Zope.Startup import do_posix_stuff; '
-                    'do_posix_stuff(opts.configroot); '%
+                    'from Zope.Startup import dropPrivileges; '
+                    'dropPrivileges(opts.configroot); '%
                     (python, self.options.configfile)
                     )
         return cmdline + more + '\"'