Skip to content

Commit

Permalink
First commit
Browse files Browse the repository at this point in the history
  • Loading branch information
davidmytton committed Mar 1, 2009
0 parents commit 9311d42
Show file tree
Hide file tree
Showing 4 changed files with 378 additions and 0 deletions.
85 changes: 85 additions & 0 deletions agent.py
@@ -0,0 +1,85 @@
'''
Server Density
www.serverdensity.com
----
A web based server resource monitoring application
(C) Boxed Ice 2009 all rights reserved
'''

# General config
DEBUG_MODE = 0
CHECK_FREQUENCY = 60

VERSION = '1.0.0b1'

# Core modules
import ConfigParser
import logging
import sched
import time
import sys

# Custom modules
from checks import checks
from daemon import Daemon

# Config handling
try:
config = ConfigParser.ConfigParser()
config.read('config.cfg')
SD_URL = config.get('Main', 'sd_url')
AGENT_KEY = config.get('Main', 'agent_key')
except ConfigParser.NoSectionError, e:
print 'Config file not found or incorrectly formatted'
quit()
except ConfigParser.ParsingError, e:
print 'Config file not found or incorrectly formatted'
quit()

# Override the generic daemon class to run our checks
class agent(Daemon):

def run(self):
agentLogger = logging.getLogger('agent')
agentLogger.debug('Creating checks instance')

# Checks instance
c = checks(SD_URL, AGENT_KEY, CHECK_FREQUENCY)

# Schedule the checks
agentLogger.debug('Scheduling checks every ' + str(CHECK_FREQUENCY) + ' seconds')
s = sched.scheduler(time.time, time.sleep)
s.enter(CHECK_FREQUENCY, 1, c.doChecks, (s,))
s.run()

# Control of daemon
if __name__ == '__main__':
# Logging
if DEBUG_MODE:
logging.basicConfig(filename='/tmp/sd-agent.log', filemode='w', level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')

mainLogger = logging.getLogger('main')
mainLogger.debug('Agent called')

# Daemon instance from agent class
daemon = agent('/tmp/sd-agent.pid')

# Control options
if len(sys.argv) == 2:
if 'start' == sys.argv[1]:
mainLogger.debug('Start daemon')
daemon.start()
elif 'stop' == sys.argv[1]:
mainLogger.debug('Stop daemon')
daemon.stop()
elif 'restart' == sys.argv[1]:
mainLogger.debug('Restart daemon')
daemon.restart()
else:
print 'Unknown command'
sys.exit(2)
sys.exit(0)
else:
print 'usage: %s start|stop|restart' % sys.argv[0]
sys.exit(2)
130 changes: 130 additions & 0 deletions checks.py
@@ -0,0 +1,130 @@
'''
Server Density
www.serverdensity.com
----
A web based server resource monitoring application
(C) Boxed Ice 2009 all rights reserved
'''

# Core modules
import logging
import logging.handlers
import re
import subprocess
import sys
import urllib
import urllib2

class checks:

def __init__(self, SD_URL, AGENT_KEY, CHECK_FREQUENCY):
self.SD_URL = SD_URL
self.AGENT_KEY = AGENT_KEY
self.CHECK_FREQUENCY = CHECK_FREQUENCY

def getDf(self):
# CURRENTLY UNUSED

# Get output from df
df = subprocess.Popen(['df'], stdout=subprocess.PIPE).communicate()[0]

# Split out each volume
volumes = df.split('\n')

# Remove first (headings) and last (blank)
volumes.pop()
volumes.pop(0)

# Loop through each volue and split out parts
for volume in volumes:
parts = re.findall(r'[a-zA-Z0-9_/]+', volume)

def getLoadAvrgs(self):
self.checksLogger.debug('Getting loadAvrgs')

# Get output from uptime
uptime = subprocess.Popen(['uptime'], stdout=subprocess.PIPE).communicate()[0]

# Split out the 3 load average values (we actually only use the 5 min average)
loadAvrgs = re.findall(r'([0-9]\.\d+)', uptime)

self.checksLogger.debug('Got loadAvrgs - ' + uptime)

return loadAvrgs # We only use loadAvrgs[0] but may use more in the future, so return all

def getMemoryUsage(self):
# See http://stackoverflow.com/questions/446209/possible-values-from-sys-platform/446210#446210 for possible
# sys.platform values
if sys.platform == 'linux2':
free = subprocess.Popen(['free', '-m'], stdout=subprocess.PIPE).communicate()[0]

lines = free.split('\n')
physParts = re.findall(r'([0-9]+)', lines[1])
swapParts = re.findall(r'([0-9]+)', lines[3])

return {'physUsed' : physParts[2], 'physFree' : physParts[3], 'swapUsed' : swapParts[2], 'swapFree' : swapParts[3]}
elif sys.platform == 'darwin':
top = subprocess.Popen(['top', '-l 1'], stdout=subprocess.PIPE).communicate()[0]
sysctl = subprocess.Popen(['sysctl', 'vm.swapusage'], stdout=subprocess.PIPE).communicate()[0]

# Deal with top
lines = top.split('\n')
physParts = re.findall(r'([0-9]\d+)', lines[5])

# Deal with sysctl
swapParts = re.findall(r'([0-9]+\.\d+)', sysctl)

return {'physUsed' : physParts[3], 'physFree' : physParts[4], 'swapUsed' : swapParts[1], 'swapFree' : swapParts[2]}
else:
return false

def getProcessCount(self):
self.checksLogger.debug('Getting process count')

# Get output from ps
ps = subprocess.Popen(['ps', 'aux'], stdout=subprocess.PIPE).communicate()[0]

# Split out each process
processes = ps.split('\n')

# Loop through each process and increment count
i = 0

for process in processes:
i = i + 1

self.checksLogger.debug('Got process count - ' + str(i))

return i

def doPostBack(self, postBackData):
self.checksLogger.debug('Doing postback')

# Build the request handler
request = urllib2.Request(self.SD_URL + '/postback/', postBackData, { 'User-Agent' : 'Server Density Agent' })

# Do the request, log any errors
try:
response = urllib2.urlopen(request)
except urllib2.HTTPError, e:
self.checksLogger.error('Unable to postback - HTTPError = ' + str(e.code))
except urllib2.URLError, e:
self.checksLogger.error('Unable to postback - URLError = ' + e.reason)

self.checksLogger.debug('Posted back')

def doChecks(self, sc):
self.checksLogger = logging.getLogger('checks')

# Do the checks
loadAvrgs = self.getLoadAvrgs()
processes = self.getProcessCount()
memory = self.getMemoryUsage()

# Post back the data
postBackData = urllib.urlencode({'agentKey' : self.AGENT_KEY, 'loadAvrg' : loadAvrgs[0], 'processCount' : processes, 'memPhysUsed' : memory['physUsed'], 'memPhysFree' : memory['physFree'], 'memSwapUsed' : memory['swapUsed'], 'memSwapFree' : memory['swapFree']})
self.doPostBack(postBackData)

self.checksLogger.debug('Rescheduling checks')
sc.enter(self.CHECK_FREQUENCY, 1, self.doChecks, (sc,))
3 changes: 3 additions & 0 deletions config.cfg
@@ -0,0 +1,3 @@
[Main]
sd_url: http://www.example.com
agent_key: keyHere
160 changes: 160 additions & 0 deletions daemon.py
@@ -0,0 +1,160 @@
'''
***
Modified generic daemon class
***
Author: http://www.jejik.com/articles/2007/02/a_simple_unix_linux_daemon_in_python/
www.boxedice.com
License: http://creativecommons.org/licenses/by-sa/3.0/
Changes: 23rd Jan 2009 (David Mytton <david@boxedice.com>)
- Replaced hard coded '/dev/null in __init__ with os.devnull
- Added OS check to conditionally remove code that doesn't work on OS X
- Added output to console on completion
- Tidied up formatting
'''

# Core modules
import atexit
import os
import sys
import time

from signal import SIGTERM

class Daemon:
"""
A generic daemon class.
Usage: subclass the Daemon class and override the run() method
"""
def __init__(self, pidfile, stdin=os.devnull, stdout=os.devnull, stderr=os.devnull):
self.stdin = stdin
self.stdout = stdout
self.stderr = stderr
self.pidfile = pidfile

def daemonize(self):
"""
Do the UNIX double-fork magic, see Stevens' "Advanced
Programming in the UNIX Environment" for details (ISBN 0201563177)
http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16
"""
try:
pid = os.fork()
if pid > 0:
# Exit first parent
sys.exit(0)
except OSError, e:
sys.stderr.write("fork #1 failed: %d (%s)\n" % (e.errno, e.strerror))
sys.exit(1)

# Decouple from parent environment
os.chdir("/")
os.setsid()
os.umask(0)

# Do second fork
try:
pid = os.fork()
if pid > 0:
# Exit from second parent
sys.exit(0)
except OSError, e:
sys.stderr.write("fork #2 failed: %d (%s)\n" % (e.errno, e.strerror))
sys.exit(1)

if sys.platform != 'darwin': # This block breaks on OS X
# Redirect standard file descriptors
sys.stdout.flush()
sys.stderr.flush()
si = file(self.stdin, 'r')
so = file(self.stdout, 'a+')
se = file(self.stderr, 'a+', 0)
os.dup2(si.fileno(), sys.stdin.fileno())
os.dup2(so.fileno(), sys.stdout.fileno())
os.dup2(se.fileno(), sys.stderr.fileno())

print "Started"

# Write pidfile
atexit.register(self.delpid) # Make sure pid file is removed if we quit
pid = str(os.getpid())
file(self.pidfile,'w+').write("%s\n" % pid)

def delpid(self):
os.remove(self.pidfile)

def start(self):
"""
Start the daemon
"""

print "Starting..."

# Check for a pidfile to see if the daemon already runs
try:
pf = file(self.pidfile,'r')
pid = int(pf.read().strip())
pf.close()
except IOError:
pid = None

if pid:
message = "pidfile %s already exists. Is it already running?\n"
sys.stderr.write(message % self.pidfile)
sys.exit(1)

# Start the daemon
self.daemonize()
self.run()

def stop(self):
"""
Stop the daemon
"""

print "Stopping..."

# Get the pid from the pidfile
try:
pf = file(self.pidfile,'r')
pid = int(pf.read().strip())
pf.close()
except IOError:
pid = None

if not pid:
message = "pidfile %s does not exist. Not running?\n"
sys.stderr.write(message % self.pidfile)
return # Not an error in a restart

# Try killing the daemon process
try:
while 1:
os.kill(pid, SIGTERM)
time.sleep(0.1)
except OSError, err:
err = str(err)
if err.find("No such process") > 0:
if os.path.exists(self.pidfile):
os.remove(self.pidfile)
else:
print str(err)
sys.exit(1)

print "Stopped"

def restart(self):
"""
Restart the daemon
"""
self.stop()
self.start()

def run(self):
"""
You should override this method when you subclass Daemon. It will be called after the process has been
daemonized by start() or restart().
"""

0 comments on commit 9311d42

Please sign in to comment.