[Oval-commits] r330 - in trunk/oval-server: . oval/dsa2oval oval/server
Pavel Vinogradov
blaze-guest at alioth.debian.org
Sun Feb 10 22:15:50 UTC 2008
Author: blaze-guest
Date: 2008-02-10 22:15:49 +0000 (Sun, 10 Feb 2008)
New Revision: 330
Added:
trunk/oval-server/daemon.py
Modified:
trunk/oval-server/oval-server.py
trunk/oval-server/oval/dsa2oval/convertor.py
trunk/oval-server/oval/server/OvalHttpServer.py
trunk/oval-server/server.conf
Log:
Updates in daemon mode
Added: trunk/oval-server/daemon.py
===================================================================
--- trunk/oval-server/daemon.py (rev 0)
+++ trunk/oval-server/daemon.py 2008-02-10 22:15:49 UTC (rev 330)
@@ -0,0 +1,387 @@
+"""Daemon base class
+
+Provides a framework for daemonizing a process. Features:
+
+ - reads the command line
+
+ - reads a configuration file
+
+ - configures logging
+
+ - calls root-level setup code
+
+ - drops privileges
+
+ - calls user-level setup code
+
+ - detaches from the controlling terminal
+
+ - checks and writes a pidfile
+
+
+Example daemon:
+
+import daemon
+import logging
+import time
+
+class HelloDaemon(daemon.Daemon):
+ default_conf = '/etc/hellodaemon.conf'
+ section = 'hello'
+
+ def run(self):
+ while True:
+ logging.info('The daemon says hello')
+ time.sleep(1)
+
+if __name__ == '__main__':
+ HelloDaemon().main()
+
+
+Example hellodaemon.conf:
+
+[hello]
+uid =
+gid =
+pidfile = ./hellodaemon.pid
+logfile = ./hellodaemon.log
+loglevel = info
+
+"""
+
+import ConfigParser
+import errno
+import grp
+import logging
+import optparse
+import os
+import pwd
+import signal
+import sys
+import time
+
+
+class Daemon(object):
+ """Daemon base class"""
+
+ default_conf = '' # override this
+ section = 'daemon' # override this
+
+ def setup_root(self):
+ """Override to perform setup tasks with root privileges.
+
+ When this is called, logging has been initialized, but the
+ terminal has not been detached and the pid of the long-running
+ process is not yet known.
+ """
+
+ def setup_user(self):
+ """Override to perform setup tasks with user privileges.
+
+ Like setup_root, the terminal is still attached and the pid is
+ temporary. However, the process has dropped root privileges.
+ """
+
+ def run(self):
+ """Override.
+
+ The terminal has been detached at this point.
+ """
+
+ def main(self):
+ """Read the command line and either start or stop the daemon"""
+ self.parse_options()
+ action = self.options.action
+ self.read_basic_config()
+ if action == 'start':
+ self.start()
+ elif action == 'stop':
+ self.stop()
+ else:
+ raise ValueError(action)
+
+ def parse_options(self):
+ """Parse the command line"""
+ p = optparse.OptionParser()
+ p.add_option('--start', dest='action',
+ action='store_const', const='start', default='start',
+ help='Start the daemon (the default action)')
+ p.add_option('-s', '--stop', dest='action',
+ action='store_const', const='stop', default='start',
+ help='Stop the daemon')
+ p.add_option('-c', dest='config_filename',
+ action='store', default=self.default_conf,
+ help='Specify alternate configuration file name. By default: /etc/oval/server.conf')
+ p.add_option('-n', '--nodaemon', dest='daemonize',
+ action='store_false', default=True,
+ help='Run in the foreground')
+ self.options, self.args = p.parse_args()
+ if not os.path.exists(self.options.config_filename):
+ p.error('configuration file not found: %s'
+ % self.options.config_filename)
+
+ def read_basic_config(self):
+ """Read basic options from the daemon config file"""
+ self.config_filename = self.options.config_filename
+ cp = ConfigParser.ConfigParser()
+ cp.read([self.config_filename])
+ self.config_parser = cp
+
+ try:
+ self.uid, self.gid = get_uid_gid(cp, self.section)
+ except ValueError, e:
+ sys.exit(str(e))
+
+ self.pidfile = cp.get(self.section, 'pidfile')
+ self.logfile = cp.get(self.section, 'logfile')
+ self.loglevel = cp.get(self.section, 'loglevel')
+
+ def on_sigterm(self, signalnum, frame):
+ """Handle segterm by treating as a keyboard interrupt"""
+ raise KeyboardInterrupt('SIGTERM')
+
+ def add_signal_handlers(self):
+ """Register the sigterm handler"""
+ signal.signal(signal.SIGTERM, self.on_sigterm)
+
+ def start(self):
+ """Initialize and run the daemon"""
+ # The order of the steps below is chosen carefully.
+ # - don't proceed if another instance is already running.
+ self.check_pid()
+ # - start handling signals
+ self.add_signal_handlers()
+ # - create log file and pid file directories if they don't exist
+ self.prepare_dirs()
+
+ # - start_logging must come after check_pid so that two
+ # processes don't write to the same log file, but before
+ # setup_root so that work done with root privileges can be
+ # logged.
+ self.start_logging()
+ try:
+ # - set up with root privileges
+ self.setup_root()
+ # - drop privileges
+ self.set_uid()
+ # - check_pid_writable must come after set_uid in order to
+ # detect whether the daemon user can write to the pidfile
+ self.check_pid_writable()
+ # - set up with user privileges before daemonizing, so that
+ # startup failures can appear on the console
+ self.setup_user()
+
+ # - daemonize
+ if self.options.daemonize:
+ daemonize()
+ except:
+ logging.exception("failed to start due to an exception")
+ raise
+
+ # - write_pid must come after daemonizing since the pid of the
+ # long running process is known only after daemonizing
+ self.write_pid()
+ try:
+ logging.info("started")
+ try:
+ self.run()
+ except (KeyboardInterrupt, SystemExit):
+ pass
+ except:
+ logging.exception("stopping with an exception")
+ raise
+ finally:
+ self.remove_pid()
+ logging.info("stopped")
+
+ def stop(self):
+ """Stop the running process"""
+ if self.pidfile and os.path.exists(self.pidfile):
+ pid = int(open(self.pidfile).read())
+ os.kill(pid, signal.SIGTERM)
+ # wait for a moment to see if the process dies
+ for n in range(10):
+ time.sleep(0.25)
+ try:
+ # poll the process state
+ os.kill(pid, 0)
+ except OSError, why:
+ if why[0] == errno.ESRCH:
+ # process has died
+ break
+ else:
+ raise
+ else:
+ sys.exit("pid %d did not die" % pid)
+ #os.kill(pid, signal.SIGKILL)
+ else:
+ sys.exit("not running")
+
+ def prepare_dirs(self):
+ """Ensure the log and pid file directories exist and are writable"""
+ for fn in (self.pidfile, self.logfile):
+ if not fn:
+ continue
+ parent = os.path.dirname(fn)
+ if not os.path.exists(parent):
+ os.makedirs(parent)
+ self.chown(parent)
+
+ def set_uid(self):
+ """Drop root privileges"""
+ if self.gid:
+ try:
+ os.setgid(self.gid)
+ except OSError, (code, message):
+ sys.exit("can't setgid(%d): %s, %s" %
+ (self.gid, code, message))
+ if self.uid:
+ try:
+ os.setuid(self.uid)
+ except OSError, (code, message):
+ sys.exit("can't setuid(%d): %s, %s" %
+ (self.uid, code, message))
+
+ def chown(self, fn):
+ """Change the ownership of a file to match the daemon uid/gid"""
+ if self.uid or self.gid:
+ uid = self.uid
+ if not uid:
+ uid = os.stat(fn).st_uid
+ gid = self.gid
+ if not gid:
+ gid = os.stat(fn).st_gid
+ try:
+ os.chown(fn, uid, gid)
+ except OSError, (code, message):
+ sys.exit("can't chown(%s, %d, %d): %s, %s" %
+ (repr(fn), uid, gid, code, message))
+
+ def start_logging(self):
+ """Configure the logging module"""
+ try:
+ level = int(self.loglevel)
+ except ValueError:
+ level = int(logging.getLevelName(self.loglevel.upper()))
+
+ handlers = []
+ if self.logfile:
+ handlers.append(logging.FileHandler(self.logfile))
+ self.chown(self.logfile)
+ if not self.options.daemonize:
+ # also log to stderr
+ handlers.append(logging.StreamHandler())
+
+ log = logging.getLogger()
+ log.setLevel(level)
+ for h in handlers:
+ h.setFormatter(logging.Formatter(
+ "%(asctime)s %(threadName)s %(levelname)s %(message)s"))
+ log.addHandler(h)
+
+ def check_pid(self):
+ """Check the pid file.
+
+ Stop using sys.exit() if another instance is already running.
+ If the pid file exists but no other instance is running,
+ delete the pid file.
+ """
+ if not self.pidfile:
+ return
+ # based on twisted/scripts/twistd.py
+ if os.path.exists(self.pidfile):
+ try:
+ pid = int(open(self.pidfile).read().strip())
+ except ValueError:
+ msg = 'pidfile %s contains a non-integer value' % self.pidfile
+ sys.exit(msg)
+ try:
+ os.kill(pid, 0)
+ except OSError, (code, text):
+ if code == errno.ESRCH:
+ # The pid doesn't exist, so remove the stale pidfile.
+ os.remove(self.pidfile)
+ else:
+ msg = ("failed to check status of process %s "
+ "from pidfile %s: %s" % (pid, self.pidfile, text))
+ sys.exit(msg)
+ else:
+ msg = ('another instance seems to be running (pid %s), '
+ 'exiting' % pid)
+ sys.exit(msg)
+
+ def check_pid_writable(self):
+ """Verify the user has access to write to the pid file.
+
+ Note that the eventual process ID isn't known until after
+ daemonize(), so it's not possible to write the PID here.
+ """
+ if not self.pidfile:
+ return
+ if os.path.exists(self.pidfile):
+ check = self.pidfile
+ else:
+ check = os.path.dirname(self.pidfile)
+ if not os.access(check, os.W_OK):
+ msg = 'unable to write to pidfile %s' % self.pidfile
+ sys.exit(msg)
+
+ def write_pid(self):
+ """Write to the pid file"""
+ if self.pidfile:
+ open(self.pidfile, 'wb').write(str(os.getpid()))
+
+ def remove_pid(self):
+ """Delete the pid file"""
+ if self.pidfile and os.path.exists(self.pidfile):
+ os.remove(self.pidfile)
+
+
+def get_uid_gid(cp, section):
+ """Get a numeric uid/gid from a configuration file.
+
+ May return an empty uid and gid.
+ """
+ uid = cp.get(section, 'uid')
+ if uid:
+ try:
+ int(uid)
+ except ValueError:
+ # convert user name to uid
+ try:
+ uid = pwd.getpwnam(uid)[2]
+ except KeyError:
+ raise ValueError("user is not in password database: %s" % uid)
+
+ gid = cp.get(section, 'gid')
+ if gid:
+ try:
+ int(gid)
+ except ValueError:
+ # convert group name to gid
+ try:
+ gid = grp.getgrnam(gid)[2]
+ except KeyError:
+ raise ValueError("group is not in group database: %s" % gid)
+
+ return uid, gid
+
+
+def daemonize():
+ """Detach from the terminal and continue as a daemon"""
+ # swiped from twisted/scripts/twistd.py
+ # See http://www.erlenstar.demon.co.uk/unix/faq_toc.html#TOC16
+ if os.fork(): # launch child and...
+ os._exit(0) # kill off parent
+ os.setsid()
+ if os.fork(): # launch child and...
+ os._exit(0) # kill off parent again.
+ os.umask(077)
+ null=os.open('/dev/null', os.O_RDWR)
+ for i in range(3):
+ try:
+ os.dup2(null, i)
+ except OSError, e:
+ if e.errno != errno.EBADF:
+ raise
+ os.close(null)
Modified: trunk/oval-server/oval/dsa2oval/convertor.py
===================================================================
--- trunk/oval-server/oval/dsa2oval/convertor.py 2008-01-27 06:26:24 UTC (rev 329)
+++ trunk/oval-server/oval/dsa2oval/convertor.py 2008-02-10 22:15:49 UTC (rev 330)
@@ -86,5 +86,27 @@
file = open (path, 'w')
xml.dom.ext.PrettyPrint (generator.createOVALDefinitions(self.dsaref), file)
file.close()
+
+ self.saveOvalDefMD5 (path)
else:
xml.dom.ext.PrettyPrint (generator.createOVALDefinitions(self.dsaref))
+
+ def saveOvalDefMD5 (self, path):
+ """ Generate MD5 hash file for OVAL definition set
+
+ @type path: C(string)
+ @param path: path to OVAL definitions set
+ """
+ import md5
+
+ file = open (path, 'r')
+ data = file.read ();
+ file.close ()
+
+ hash = md5.new(data).digest()
+
+ (path, ext) = os.path.splitext (path)
+ path = '.'.join(path, '.md5')
+ file = open (path, 'w')
+ file.write (hash)
+ file.close ()
Modified: trunk/oval-server/oval/server/OvalHttpServer.py
===================================================================
--- trunk/oval-server/oval/server/OvalHttpServer.py 2008-01-27 06:26:24 UTC (rev 329)
+++ trunk/oval-server/oval/server/OvalHttpServer.py 2008-02-10 22:15:49 UTC (rev 330)
@@ -42,12 +42,15 @@
agentIP = self.client_address[0]
if db.getAgentInfo(agentIP):
try:
+ import md5
f = open (self.server.workdir+os.sep+agentIP+'.xml')
+ data = f.read()
+ f.close()
self.send_response(200)
self.send_header('Content-type', 'text/xml')
+ self.send_header('Content-hash', md5.new(data).digest())
self.end_headers()
- self.wfile.write(f.read())
- f.close()
+ self.wfile.write(data)
return
except IOError:
self.send_error(404, 'File Not Found: %s.xml' % agentIP)
Modified: trunk/oval-server/oval-server.py
===================================================================
--- trunk/oval-server/oval-server.py 2008-01-27 06:26:24 UTC (rev 329)
+++ trunk/oval-server/oval-server.py 2008-02-10 22:15:49 UTC (rev 330)
@@ -6,9 +6,10 @@
"""Start OVAL server program."""
+import daemon
from ConfigParser import SafeConfigParser
from threading import Thread
-import os, logging, sys, time, getopt
+import os, logging, sys, time
import traceback, exceptions
sys.path = ['/usr/share/oval-server'] + sys.path
from oval.dsa2oval import convertor
@@ -22,21 +23,10 @@
class configNotFoundError (Exception):
pass
-logLevels = {'CRITICAL' : 50, 'ERROR' : 40, 'WARNING' : 30, 'INFO' : 20, 'DEBUG' : 10, 'NOTSET' : 0}
-threadStatus = {'INACTIVE' : 10, 'RUN' : 20, 'WORK' : 30, 'STOP' : 40}
-
-def usage (prog = 'oval-server.py'):
- """Print information about script flags and options"""
-
- print """usage: python %s [-h] [-c <config>]
-\t-h\tthis help
-\t-c\tpath to config file (by default /etc/oval/server.conf""" % prog
-
-
class serverThread(Thread):
- """ serverThread - thread which handle cleant requests to server.
+ """ serverThread - thread which handle client requests to server.
- Server requests from clients, particular server type depends
+ Serve requests from clients, particular server type depends
on configuration file, but currently inplement only OvalHttpServer.
"""
@@ -164,21 +154,6 @@
logfilename = self.config.get('general', 'log_file')
self.log_level = self.config.get('general', 'log_level')
self.outfilename = os.path.join(logdirname, logfilename)
-
- # Create the root handler (removing any others)
- hdlr = logging.FileHandler(self.outfilename, 'a')
- hdlr.setFormatter(logging.Formatter('%(asctime)s %(threadName)s %(name)s %(levelname)s %(message)s'))
- for h in logging.root.handlers:
- logging.root.removeHandler(h)
- logging.root.addHandler(hdlr)
-
- if logLevels.has_key(self.log_level):
- logging.root.setLevel(logLevels[self.log_level])
- else:
- logging.root.setLevel(logging.WARNING)
- self.logger.warning('Wrong value of log_level key in config. Use WARNING as default.')
-
- self.logger.info('Logging begins')
except IOError, e:
sys.stderr.write("Can't create logger handler: " + str(e) + "\n")
@@ -227,36 +202,24 @@
self.logger.info('Shutting down')
logging.shutdown()
+class ovalDaemon(daemon.Daemon):
+ #By default we search for config file in global etc directory
+ default_conf = '/etc/oval/server.conf'
+ section = 'daemon'
+ def run(self):
+ logging.info('Debian Oval Server 0.1.')
+
+ #Creat server instance and run it
+ try:
+ main = mainThread(self.config_filename)
+ main.run ()
+ except configNotFoundError, e:
+ sys.stderr.write (str(e))
+ except Exception, e:
+ sys.stderr.write('Unhandled error during execution: %s : %s.\n' % (e.__class__, str(e)))
+ traceback.print_exc()
+
if __name__ == "__main__":
- #Parse command line options.
- #By default we search for config file in global etc directory
- opts = {'-c' : '/etc/oval/server.conf'}
-
- try:
- opt, args = getopt.getopt (sys.argv[1:], 'hc:')
- except getopt.GetoptError:
- usage (sys.argv[0])
- sys.exit(1)
-
- for key, value in opt:
- opts[key] = value
+ ovalDaemon().main()
- if opts.has_key ('-h'):
- usage(sys.argv[0])
- sys.exit(0)
-
- #Crate server instance and run it
- try:
- main = mainThread(opts['-c'])
- main.run ()
- except configNotFoundError, e:
- sys.stderr.write (str(e))
- except KeyboardInterrupt, e:
- time.sleep(20)
- #os.system ('kill -HUP %d' % os.getpid())
- except exceptions.SystemExit, e:
- raise e
- except Exception, e:
- sys.stderr.write('Unhandled error during execution: %s : %s.\n' % (e.__class__, str(e)))
- traceback.print_exc()
Modified: trunk/oval-server/server.conf
===================================================================
--- trunk/oval-server/server.conf 2008-01-27 06:26:24 UTC (rev 329)
+++ trunk/oval-server/server.conf 2008-02-10 22:15:49 UTC (rev 330)
@@ -6,6 +6,13 @@
dsa_storage = /var/lib/oval-server/dsa
db = /var/lib/oval-server/oval-server.db
+[daemon]
+uid =
+gid =
+pidfile = ./daemon.pid
+logfile = ./daemon.log
+loglevel = debug
+
[server]
type = http
ip = 127.0.0.1
More information about the Oval-commits
mailing list