Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 9311d42
Showing
4 changed files
with
378 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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,)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
[Main] | ||
sd_url: http://www.example.com | ||
agent_key: keyHere |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(). | ||
""" |