serverdensity-heroku 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/bin/agent.py ADDED
@@ -0,0 +1,503 @@
1
+ #!/usr/bin/env python
2
+ '''
3
+ Server Density
4
+ www.serverdensity.com
5
+ ----
6
+ Server monitoring agent for Linux, FreeBSD and Mac OS X
7
+
8
+ Licensed under Simplified BSD License (see LICENSE)
9
+ '''
10
+
11
+ import logging
12
+
13
+ # General config
14
+ agentConfig = {}
15
+ agentConfig['logging'] = logging.DEBUG
16
+ agentConfig['checkFreq'] = 60
17
+
18
+ agentConfig['version'] = '1.11.4'
19
+
20
+ rawConfig = {}
21
+
22
+ # Check we're not using an old version of Python. Do this before anything else
23
+ # We need 2.4 above because some modules (like subprocess) were only introduced in 2.4.
24
+ import sys
25
+ if int(sys.version_info[1]) <= 3:
26
+ print 'You are using an outdated version of Python. Please update to v2.4 or above (v3 is not supported). For newer OSs, you can update Python without affecting your system install. See http://blog.boxedice.com/2010/01/19/updating-python-on-rhelcentos/ If you are running RHEl 4 / CentOS 4 then you will need to compile Python manually.'
27
+ sys.exit(1)
28
+
29
+ # Core modules
30
+ import ConfigParser
31
+ import os
32
+ import re
33
+ import sched
34
+ import time
35
+
36
+ # After the version check as this isn't available on older Python versions
37
+ # and will error before the message is shown
38
+ import subprocess
39
+
40
+ # Custom modules
41
+ from checks import checks
42
+ from daemon import Daemon
43
+
44
+ # Config handling
45
+ try:
46
+ path = os.path.realpath(__file__)
47
+ path = os.path.dirname(path)
48
+
49
+ config = ConfigParser.ConfigParser()
50
+
51
+ if os.path.exists('/etc/sd-agent/config.cfg'):
52
+ configPath = '/etc/sd-agent/config.cfg'
53
+ else:
54
+ configPath = path + '/config.cfg'
55
+
56
+ if os.access(configPath, os.R_OK) == False:
57
+ print 'Unable to read the config file at ' + configPath
58
+ print 'Agent will now quit'
59
+ sys.exit(1)
60
+
61
+ config.read(configPath)
62
+
63
+ # Core config
64
+ agentConfig['sdUrl'] = config.get('Main', 'sd_url')
65
+
66
+ if agentConfig['sdUrl'].endswith('/'):
67
+ agentConfig['sdUrl'] = agentConfig['sdUrl'][:-1]
68
+
69
+ agentConfig['agentKey'] = config.get('Main', 'agent_key')
70
+
71
+ # Tmp path
72
+ if os.path.exists('/var/log/sd-agent/'):
73
+ agentConfig['tmpDirectory'] = '/var/log/sd-agent/'
74
+ else:
75
+ agentConfig['tmpDirectory'] = '/tmp/' # default which may be overriden in the config later
76
+
77
+ agentConfig['pidfileDirectory'] = agentConfig['tmpDirectory']
78
+
79
+ # Plugin config
80
+ if config.has_option('Main', 'plugin_directory'):
81
+ agentConfig['pluginDirectory'] = config.get('Main', 'plugin_directory')
82
+
83
+ # Optional config
84
+ # Also do not need to be present in the config file (case 28326).
85
+ if config.has_option('Main', 'apache_status_url'):
86
+ agentConfig['apacheStatusUrl'] = config.get('Main', 'apache_status_url')
87
+
88
+ if config.has_option('Main', 'apache_status_user'):
89
+ agentConfig['apacheStatusUser'] = config.get('Main', 'apache_status_user')
90
+
91
+ if config.has_option('Main', 'apache_status_pass'):
92
+ agentConfig['apacheStatusPass'] = config.get('Main', 'apache_status_pass')
93
+
94
+ if config.has_option('Main', 'logging_level'):
95
+ # Maps log levels from the configuration file to Python log levels
96
+ loggingLevelMapping = {
97
+ 'debug' : logging.DEBUG,
98
+ 'info' : logging.INFO,
99
+ 'error' : logging.ERROR,
100
+ 'warn' : logging.WARN,
101
+ 'warning' : logging.WARNING,
102
+ 'critical' : logging.CRITICAL,
103
+ 'fatal' : logging.FATAL,
104
+ }
105
+
106
+ customLogging = config.get('Main', 'logging_level')
107
+
108
+ try:
109
+ agentConfig['logging'] = loggingLevelMapping[customLogging.lower()]
110
+
111
+ except KeyError, ex:
112
+ agentConfig['logging'] = logging.INFO
113
+
114
+ if config.has_option('Main', 'mongodb_server'):
115
+ agentConfig['MongoDBServer'] = config.get('Main', 'mongodb_server')
116
+
117
+ if config.has_option('Main', 'mongodb_dbstats'):
118
+ agentConfig['MongoDBDBStats'] = config.get('Main', 'mongodb_dbstats')
119
+
120
+ if config.has_option('Main', 'mongodb_replset'):
121
+ agentConfig['MongoDBReplSet'] = config.get('Main', 'mongodb_replset')
122
+
123
+ if config.has_option('Main', 'mysql_server'):
124
+ agentConfig['MySQLServer'] = config.get('Main', 'mysql_server')
125
+
126
+ if config.has_option('Main', 'mysql_user'):
127
+ agentConfig['MySQLUser'] = config.get('Main', 'mysql_user')
128
+
129
+ if config.has_option('Main', 'mysql_pass'):
130
+ agentConfig['MySQLPass'] = config.get('Main', 'mysql_pass')
131
+
132
+ if config.has_option('Main', 'mysql_port'):
133
+ agentConfig['MySQLPort'] = config.get('Main', 'mysql_port')
134
+
135
+ if config.has_option('Main', 'mysql_socket'):
136
+ agentConfig['MySQLSocket'] = config.get('Main', 'mysql_socket')
137
+
138
+ if config.has_option('Main', 'mysql_norepl'):
139
+ agentConfig['MySQLNoRepl'] = config.get('Main', 'mysql_norepl')
140
+
141
+ if config.has_option('Main', 'nginx_status_url'):
142
+ agentConfig['nginxStatusUrl'] = config.get('Main', 'nginx_status_url')
143
+
144
+ if config.has_option('Main', 'tmp_directory'):
145
+ agentConfig['tmpDirectory'] = config.get('Main', 'tmp_directory')
146
+
147
+ if config.has_option('Main', 'pidfile_directory'):
148
+ agentConfig['pidfileDirectory'] = config.get('Main', 'pidfile_directory')
149
+
150
+ if config.has_option('Main', 'rabbitmq_status_url'):
151
+ agentConfig['rabbitMQStatusUrl'] = config.get('Main', 'rabbitmq_status_url')
152
+
153
+ if config.has_option('Main', 'rabbitmq_user'):
154
+ agentConfig['rabbitMQUser'] = config.get('Main', 'rabbitmq_user')
155
+
156
+ if config.has_option('Main', 'rabbitmq_pass'):
157
+ agentConfig['rabbitMQPass'] = config.get('Main', 'rabbitmq_pass')
158
+
159
+ except ConfigParser.NoSectionError, e:
160
+ print 'Config file not found or incorrectly formatted'
161
+ print 'Agent will now quit'
162
+ sys.exit(1)
163
+
164
+ except ConfigParser.ParsingError, e:
165
+ print 'Config file not found or incorrectly formatted'
166
+ print 'Agent will now quit'
167
+ sys.exit(1)
168
+
169
+ except ConfigParser.NoOptionError, e:
170
+ print 'There are some items missing from your config file, but nothing fatal'
171
+
172
+ # Check to make sure the default config values have been changed (only core config values)
173
+ if agentConfig['sdUrl'] == 'http://example.serverdensity.com' or agentConfig['agentKey'] == 'keyHere':
174
+ print 'You have not modified config.cfg for your server'
175
+ print 'Agent will now quit'
176
+ sys.exit(1)
177
+
178
+ # Check to make sure sd_url is in correct
179
+ if re.match('http(s)?(\:\/\/)[a-zA-Z0-9_\-]+\.(serverdensity.com)', agentConfig['sdUrl']) == None:
180
+ print 'Your sd_url is incorrect. It needs to be in the form http://example.serverdensity.com (or using https)'
181
+ print 'Agent will now quit'
182
+ sys.exit(1)
183
+
184
+ # Check apache_status_url is not empty (case 27073)
185
+ if 'apacheStatusUrl' in agentConfig and agentConfig['apacheStatusUrl'] == None:
186
+ print 'You must provide a config value for apache_status_url. If you do not wish to use Apache monitoring, leave it as its default value - http://www.example.com/server-status/?auto'
187
+ print 'Agent will now quit'
188
+ sys.exit(1)
189
+
190
+ if 'nginxStatusUrl' in agentConfig and agentConfig['nginxStatusUrl'] == None:
191
+ print 'You must provide a config value for nginx_status_url. If you do not wish to use Nginx monitoring, leave it as its default value - http://www.example.com/nginx_status'
192
+ print 'Agent will now quit'
193
+ sys.exit(1)
194
+
195
+ if 'MySQLServer' in agentConfig and agentConfig['MySQLServer'] != '' and 'MySQLUser' in agentConfig and agentConfig['MySQLUser'] != '' and 'MySQLPass' in agentConfig:
196
+ try:
197
+ import MySQLdb
198
+ except ImportError:
199
+ print 'You have configured MySQL for monitoring, but the MySQLdb module is not installed. For more info, see: http://www.serverdensity.com/docs/agent/mysqlstatus/'
200
+ print 'Agent will now quit'
201
+ sys.exit(1)
202
+
203
+ if 'MongoDBServer' in agentConfig and agentConfig['MongoDBServer'] != '':
204
+ try:
205
+ import pymongo
206
+ except ImportError:
207
+ print 'You have configured MongoDB for monitoring, but the pymongo module is not installed. For more info, see: http://www.serverdensity.com/docs/agent/mongodbstatus/'
208
+ print 'Agent will now quit'
209
+ sys.exit(1)
210
+
211
+ for section in config.sections():
212
+ rawConfig[section] = {}
213
+
214
+ for option in config.options(section):
215
+ rawConfig[section][option] = config.get(section, option)
216
+
217
+ # Override the generic daemon class to run our checks
218
+ class agent(Daemon):
219
+
220
+ def run(self):
221
+ mainLogger.debug('Collecting basic system stats')
222
+
223
+ # Get some basic system stats to post back for development/testing
224
+ import platform
225
+ systemStats = {'machine': platform.machine(), 'platform': sys.platform, 'processor': platform.processor(), 'pythonV': platform.python_version(), 'cpuCores': self.cpuCores()}
226
+
227
+ if sys.platform == 'linux2':
228
+ systemStats['nixV'] = platform.dist()
229
+
230
+ elif sys.platform == 'darwin':
231
+ systemStats['macV'] = platform.mac_ver()
232
+
233
+ elif sys.platform.find('freebsd') != -1:
234
+ version = platform.uname()[2]
235
+ systemStats['fbsdV'] = ('freebsd', version, '') # no codename for FreeBSD
236
+
237
+ mainLogger.info('System: ' + str(systemStats))
238
+
239
+ mainLogger.debug('Creating checks instance')
240
+
241
+ # Checks instance
242
+ c = checks(agentConfig, rawConfig, mainLogger)
243
+
244
+ # Schedule the checks
245
+ mainLogger.info('checkFreq: %s', agentConfig['checkFreq'])
246
+ s = sched.scheduler(time.time, time.sleep)
247
+ c.doChecks(s, True, systemStats) # start immediately (case 28315)
248
+ s.run()
249
+
250
+ def cpuCores(self):
251
+ if sys.platform == 'linux2':
252
+ grep = subprocess.Popen(['grep', 'model name', '/proc/cpuinfo'], stdout=subprocess.PIPE, close_fds=True)
253
+ wc = subprocess.Popen(['wc', '-l'], stdin=grep.stdout, stdout=subprocess.PIPE, close_fds=True)
254
+ output = wc.communicate()[0]
255
+ return int(output)
256
+
257
+ if sys.platform == 'darwin':
258
+ output = subprocess.Popen(['sysctl', 'hw.ncpu'], stdout=subprocess.PIPE, close_fds=True).communicate()[0].split(': ')[1]
259
+ return int(output)
260
+
261
+ # Control of daemon
262
+ if __name__ == '__main__':
263
+
264
+ # Logging
265
+ logFile = os.path.join(agentConfig['tmpDirectory'], 'sd-agent.log')
266
+
267
+ if os.access(agentConfig['tmpDirectory'], os.W_OK) == False:
268
+ print 'Unable to write the log file at ' + logFile
269
+ print 'Agent will now quit'
270
+ sys.exit(1)
271
+
272
+ handler = logging.handlers.RotatingFileHandler(logFile, maxBytes=10485760, backupCount=5) # 10MB files
273
+ formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
274
+
275
+ handler.setFormatter(formatter)
276
+
277
+ mainLogger = logging.getLogger('main')
278
+ mainLogger.setLevel(agentConfig['logging'])
279
+ mainLogger.addHandler(handler)
280
+
281
+ mainLogger.info('--')
282
+ mainLogger.info('sd-agent %s started', agentConfig['version'])
283
+ mainLogger.info('--')
284
+
285
+ mainLogger.info('sd_url: %s', agentConfig['sdUrl'])
286
+ mainLogger.info('agent_key: %s', agentConfig['agentKey'])
287
+
288
+ argLen = len(sys.argv)
289
+
290
+ if argLen == 3 or argLen == 4: # needs to accept case when --clean is passed
291
+ if sys.argv[2] == 'init':
292
+ # This path added for newer Linux packages which run under
293
+ # a separate sd-agent user account.
294
+ if os.path.exists('/var/run/sd-agent/'):
295
+ pidFile = '/var/run/sd-agent/sd-agent.pid'
296
+ else:
297
+ pidFile = '/var/run/sd-agent.pid'
298
+
299
+ else:
300
+ pidFile = os.path.join(agentConfig['pidfileDirectory'], 'sd-agent.pid')
301
+
302
+ if os.access(agentConfig['pidfileDirectory'], os.W_OK) == False:
303
+ print 'Unable to write the PID file at ' + pidFile
304
+ print 'Agent will now quit'
305
+ sys.exit(1)
306
+
307
+ mainLogger.info('PID: %s', pidFile)
308
+
309
+ if argLen == 4 and sys.argv[3] == '--clean':
310
+ mainLogger.info('--clean')
311
+ try:
312
+ os.remove(pidFile)
313
+ except OSError:
314
+ # Did not find pid file
315
+ pass
316
+
317
+ # Daemon instance from agent class
318
+ daemon = agent(pidFile)
319
+
320
+ # Control options
321
+ if argLen == 2 or argLen == 3 or argLen == 4:
322
+ if 'start' == sys.argv[1]:
323
+ mainLogger.info('Action: start')
324
+ daemon.start()
325
+
326
+ elif 'stop' == sys.argv[1]:
327
+ mainLogger.info('Action: stop')
328
+ daemon.stop()
329
+
330
+ elif 'restart' == sys.argv[1]:
331
+ mainLogger.info('Action: restart')
332
+ daemon.restart()
333
+
334
+ elif 'foreground' == sys.argv[1]:
335
+ mainLogger.info('Action: foreground')
336
+ daemon.run()
337
+
338
+ elif 'status' == sys.argv[1]:
339
+ mainLogger.info('Action: status')
340
+
341
+ try:
342
+ pf = file(pidFile,'r')
343
+ pid = int(pf.read().strip())
344
+ pf.close()
345
+ except IOError:
346
+ pid = None
347
+ except SystemExit:
348
+ pid = None
349
+
350
+ if pid:
351
+ print 'sd-agent is running as pid %s.' % pid
352
+ else:
353
+ print 'sd-agent is not running.'
354
+
355
+ elif 'update' == sys.argv[1]:
356
+ mainLogger.info('Action: update')
357
+
358
+ if os.path.abspath(__file__) == '/usr/bin/sd-agent/agent.py':
359
+ print 'Please use the Linux package manager that was used to install the agent to update it.'
360
+ print 'e.g. yum install sd-agent or apt-get install sd-agent'
361
+ sys.exit(1)
362
+
363
+ import httplib
364
+ import platform
365
+ import urllib2
366
+
367
+ print 'Checking if there is a new version';
368
+
369
+ # Get the latest version info
370
+ try:
371
+ mainLogger.debug('Update: checking for update')
372
+
373
+ request = urllib2.urlopen('http://www.serverdensity.com/agentupdate/')
374
+ response = request.read()
375
+
376
+ except urllib2.HTTPError, e:
377
+ print 'Unable to get latest version info - HTTPError = ' + str(e)
378
+ sys.exit(1)
379
+
380
+ except urllib2.URLError, e:
381
+ print 'Unable to get latest version info - URLError = ' + str(e)
382
+ sys.exit(1)
383
+
384
+ except httplib.HTTPException, e:
385
+ print 'Unable to get latest version info - HTTPException'
386
+ sys.exit(1)
387
+
388
+ except Exception, e:
389
+ import traceback
390
+ print 'Unable to get latest version info - Exception = ' + traceback.format_exc()
391
+ sys.exit(1)
392
+
393
+ mainLogger.debug('Update: importing json/minjson')
394
+
395
+ # We need to return the data using JSON. As of Python 2.6+, there is a core JSON
396
+ # module. We have a 2.4/2.5 compatible lib included with the agent but if we're
397
+ # on 2.6 or above, we should use the core module which will be faster
398
+ pythonVersion = platform.python_version_tuple()
399
+
400
+ # Decode the JSON
401
+ if int(pythonVersion[1]) >= 6: # Don't bother checking major version since we only support v2 anyway
402
+ import json
403
+
404
+ mainLogger.debug('Update: decoding JSON (json)')
405
+
406
+ try:
407
+ updateInfo = json.loads(response)
408
+ except Exception, e:
409
+ print 'Unable to get latest version info. Try again later.'
410
+ sys.exit(1)
411
+
412
+ else:
413
+ import minjson
414
+
415
+ mainLogger.debug('Update: decoding JSON (minjson)')
416
+
417
+ try:
418
+ updateInfo = minjson.safeRead(response)
419
+ except Exception, e:
420
+ print 'Unable to get latest version info. Try again later.'
421
+ sys.exit(1)
422
+
423
+ # Do the version check
424
+ if updateInfo['version'] != agentConfig['version']:
425
+ import md5 # I know this is depreciated, but we still support Python 2.4 and hashlib is only in 2.5. Case 26918
426
+ import urllib
427
+
428
+ print 'A new version is available.'
429
+
430
+ def downloadFile(agentFile, recursed = False):
431
+ mainLogger.debug('Update: downloading ' + agentFile['name'])
432
+ print 'Downloading ' + agentFile['name']
433
+
434
+ downloadedFile = urllib.urlretrieve('http://www.serverdensity.com/downloads/sd-agent/' + agentFile['name'])
435
+
436
+ # Do md5 check to make sure the file downloaded properly
437
+ checksum = md5.new()
438
+ f = file(downloadedFile[0], 'rb')
439
+
440
+ # Although the files are small, we can't guarantee the available memory nor that there
441
+ # won't be large files in the future, so read the file in small parts (1kb at time)
442
+ while True:
443
+ part = f.read(1024)
444
+
445
+ if not part:
446
+ break # end of file
447
+
448
+ checksum.update(part)
449
+
450
+ f.close()
451
+
452
+ # Do we have a match?
453
+ if checksum.hexdigest() == agentFile['md5']:
454
+ return downloadedFile[0]
455
+
456
+ else:
457
+ # Try once more
458
+ if recursed == False:
459
+ downloadFile(agentFile, True)
460
+
461
+ else:
462
+ print agentFile['name'] + ' did not match its checksum - it is corrupted. This may be caused by network issues so please try again in a moment.'
463
+ sys.exit(1)
464
+
465
+ # Loop through the new files and call the download function
466
+ for agentFile in updateInfo['files']:
467
+ agentFile['tempFile'] = downloadFile(agentFile)
468
+
469
+ # If we got to here then everything worked out fine. However, all the files are still in temporary locations so we need to move them
470
+ # This is to stop an update breaking a working agent if the update fails halfway through
471
+ import os
472
+ import shutil # Prevents [Errno 18] Invalid cross-device link (case 26878) - http://mail.python.org/pipermail/python-list/2005-February/308026.html
473
+
474
+ for agentFile in updateInfo['files']:
475
+ mainLogger.debug('Update: updating ' + agentFile['name'])
476
+ print 'Updating ' + agentFile['name']
477
+
478
+ try:
479
+ if os.path.exists(agentFile['name']):
480
+ os.remove(agentFile['name'])
481
+
482
+ shutil.move(agentFile['tempFile'], agentFile['name'])
483
+
484
+ except OSError:
485
+ print 'An OS level error occurred. You will need to manually re-install the agent by downloading the latest version from http://www.serverdensity.com/downloads/sd-agent.tar.gz. You can copy your config.cfg to the new install'
486
+ sys.exit(1)
487
+
488
+ mainLogger.debug('Update: done')
489
+
490
+ print 'Update completed. Please restart the agent (python agent.py restart).'
491
+
492
+ else:
493
+ print 'The agent is already up to date'
494
+
495
+ else:
496
+ print 'Unknown command'
497
+ sys.exit(1)
498
+
499
+ sys.exit(0)
500
+
501
+ else:
502
+ print 'usage: %s start|stop|restart|status|update' % sys.argv[0]
503
+ sys.exit(1)