NewsBlur/vendor/mms-agent/agent.py

337 lines
12 KiB
Python
Raw Normal View History

2013-03-29 11:04:46 -07:00
"""
(C) Copyright 2011, 10gen
This is a label on a mattress. Do not modify this file!
"""
# App
import settings as _settings
import logConfig
# Python
2020-06-17 03:56:44 -04:00
import sys, socket, time, os, hmac, urllib.request, urllib.error, urllib.parse, threading, subprocess, traceback
2013-03-29 11:04:46 -07:00
try:
import hashlib
except ImportError:
sys.exit( 'ERROR - you must have hashlib installed - see README for more info' )
_logger = logConfig.initLogger()
socket.setdefaulttimeout( _settings.socket_timeout )
_pymongoVersion = None
_processPid = os.getpid()
# Try and reduce the stack size.
try:
threading.stack_size( 409600 )
except:
pass
if _settings.mms_key == '@API_KEY@':
sys.exit( 'ERROR - you must set your @API_KEY@ - see https://mms.10gen.com/settings' )
if _settings.secret_key == '@SECRET_KEY@':
sys.exit( 'ERROR - you must set your @SECRET_KEY@ - see https://mms.10gen.com/settings' )
if sys.version_info < ( 2, 4 ):
sys.exit( 'ERROR - old Python - the MMS agent requires Python 2.4 or higher' )
# Make sure pymongo is installed
try:
import pymongo
import bson
except ImportError:
sys.exit( 'ERROR - pymongo not installed - see: http://api.mongodb.org/python/ - run: easy_install pymongo' )
# Check the version of pymongo.
pyv = pymongo.version
if "partition" in dir( pyv ):
pyv = pyv.partition( "+" )[0]
_pymongoVersion = pyv
2020-06-17 03:56:44 -04:00
if list(map( int, pyv.split('.') )) < [ 1, 9]:
2013-03-29 11:04:46 -07:00
sys.exit( 'ERROR - The MMS agent requires pymongo 1.9 or higher: easy_install -U pymongo' )
if _settings.useSslForAllConnections:
2020-06-17 03:56:44 -04:00
if list(map( int, pyv.split('.') )) < [ 2, 1, 1]:
2013-03-29 11:04:46 -07:00
sys.exit( 'ERROR - The MMS agent requires pymongo 2.1.1 or higher to use SSL: easy_install -U pymongo' )
_pymongoVersion = pymongo.version
class AgentProcessContainer( object ):
""" Store the handle and lock to the agent process. """
def __init__( self ):
""" Init the lock and init process to none """
self.lock = threading.Lock()
self.agent = None
def pingAgentProcess( self ):
""" Ping the agent process """
try:
self.lock.acquire()
if self.agent is None or self.agent.poll() is not None:
return
self.agent.stdin.write( 'hello\n' )
self.agent.stdin.flush()
finally:
self.lock.release()
def stopAgentProcess( self ):
""" Send the stop message to the agent process """
try:
self.lock.acquire()
if self.agent is None or self.agent.poll() is not None:
return
self.agent.stdin.write( 'seeya\n' )
self.agent.stdin.flush()
time.sleep( 1 )
self.agent = None
finally:
self.lock.release()
class AgentShutdownListenerThread( threading.Thread ):
""" Disabled by default. When enabled listens for shutdown messages. """
def __init__( self, loggerObj, settingsObj ):
""" Initialize the object """
self.logger = loggerObj
self.settings = settingsObj
threading.Thread.__init__( self )
def run( self ):
""" Listen for the shutdown messages """
try:
sock = socket.socket( socket.AF_INET, socket.SOCK_DGRAM )
sock.bind( ( self.settings.shutdownAgentBindAddr, self.settings.shutdownAgentBindPort ) )
2020-06-17 03:56:44 -04:00
except Exception as e:
2013-03-29 11:04:46 -07:00
self.logger.error( traceback.format_exc( e ) )
self.logger.info( 'Shutdown listener bound to address %s on port %d' % ( self.settings.shutdownAgentBindAddr, self.settings.shutdownAgentBindPort ) )
while True:
try:
time.sleep( 5 )
data, addr = sock.recvfrom( 1024 )
if data != self.settings.shutdownAgentBindChallenge:
self.logger.error( 'received bad shutdown message from: %s' % addr[0] )
else:
self.logger.info( 'received valid shutdown message from: %s - exiting' % addr[0] )
os._exit( 0 )
except:
pass
class AgentProcessMonitorThread( threading.Thread ):
""" Make sure the agent process is running """
def __init__( self, logger, agentDir, processContainerObj ):
""" Initialize the object """
self.logger = logger
self.agentDir = agentDir
self.processContainer = processContainerObj
threading.Thread.__init__( self )
def _launchAgentProcess( self ):
""" Execute the agent process and keep a handle to it. """
return subprocess.Popen( [ sys.executable, os.path.join( sys.path[0], 'agentProcess.py' ), str( _processPid ) ], stdin=subprocess.PIPE, stdout=subprocess.PIPE )
def run( self ):
""" If the agent process is not alive, start the process """
while True:
try:
time.sleep( 5 )
self._monitorProcess()
2020-06-17 03:56:44 -04:00
except Exception as e:
2013-03-29 11:04:46 -07:00
self.logger.error( traceback.format_exc( e ) )
def _monitorProcess( self ):
""" Monitor the child process """
self.processContainer.lock.acquire()
try:
try:
if self.processContainer.agent is None or self.processContainer.agent.poll() is not None:
self.processContainer.agent = self._launchAgentProcess()
2020-06-17 03:56:44 -04:00
except Exception as e:
2013-03-29 11:04:46 -07:00
self.logger.error( traceback.format_exc( e ) )
finally:
self.processContainer.lock.release()
class AgentUpdateThread( threading.Thread ):
""" Check to see if updates are available - if so download and restart agent process """
def __init__( self, logger, agentDir, settingsObj, processContainerObj ):
""" Initialize the object """
self.logger = logger
self.agentDir = agentDir
self.settings = settingsObj
self.processContainer = processContainerObj
threading.Thread.__init__( self )
def run( self ):
""" Update the agent if possible """
while True:
try:
time.sleep( 300 )
self._checkForUpdate()
2020-06-17 03:56:44 -04:00
except Exception as e:
2013-03-29 11:04:46 -07:00
self.logger.error( 'Problem with upgrade check: ' + traceback.format_exc( e ) )
def _checkForUpdate( self ):
""" Update the agent if possible """
2020-06-17 03:56:44 -04:00
res = urllib.request.urlopen( self.settings.version_url % { 'key' : self.settings.mms_key } )
2013-03-29 11:04:46 -07:00
resBson = None
try:
resBson = bson.decode_all( res.read() )
finally:
if res is not None:
res.close()
res = None
if len(resBson) != 1:
return
versionResponse = resBson[0]
if 'status' not in versionResponse or versionResponse['status'] != 'ok':
return
if 'agentVersion' not in versionResponse or 'authCode' not in versionResponse:
return
remoteAgentVersion = versionResponse['agentVersion']
authCode = versionResponse['authCode']
if authCode != hmac.new( self.settings.secret_key, remoteAgentVersion, digestmod=hashlib.sha1 ).hexdigest():
self.logger.error( 'Invalid auth code - please confirm your secret key (defined on Settings page) is correct and hmac is properly installed - http://mms.10gen.com/help/' )
return
if self._shouldUpgradeAgent( self.settings.settingsAgentVersion, remoteAgentVersion ):
self._upgradeAgent( remoteAgentVersion )
def _shouldUpgradeAgent( self, localVersion, remoteVersion ):
""" Returns true if the agent should upgrade itself. """
try:
for l, r in zip( localVersion.split('.'), remoteVersion.split('.') ):
if int( l ) < int( r ):
return True
if int( l ) > int( r ):
return False
2020-06-17 03:56:44 -04:00
except Exception:
2013-03-29 11:04:46 -07:00
self.logger.error( "Upgrade problem with versions - local: '%s' - remote: '%s'" % ( localVersion, remoteVersion ) )
return False
def _upgradeAgent( self, newAgentVersion ):
""" Pull down the files, verify and then stop the current process """
2020-06-17 03:56:44 -04:00
res = urllib.request.urlopen( self.settings.upgrade_url % { 'key' : self.settings.mms_key } )
2013-03-29 11:04:46 -07:00
resBson = None
try:
resBson = bson.decode_all( res.read() )
finally:
if res is not None:
res.close()
res = None
if len(resBson) != 1:
return
upgradeResponse = resBson[0]
if 'status' not in upgradeResponse or upgradeResponse['status'] != 'ok' or 'files' not in upgradeResponse:
return
# Verify the auth codes for all files and names first.
for fileInfo in upgradeResponse['files']:
if fileInfo['fileAuthCode'] != hmac.new( self.settings.secret_key, fileInfo['file'], digestmod=hashlib.sha1 ).hexdigest():
self.logger.error( 'Invalid file auth code for upgrade - cancelling' )
return
if fileInfo['fileNameAuthCode'] != hmac.new( self.settings.secret_key, fileInfo['fileName'], digestmod=hashlib.sha1 ).hexdigest():
self.logger.error( 'Invalid file name auth code for upgrade - cancelling' )
return
# Write the files.
for fileInfo in upgradeResponse['files']:
fileContent = fileInfo['file']
fileName = fileInfo['fileName']
# If the user has a global username/password defined, make sure it is set in the new settings.py file.
if fileName == 'settings.py' and getattr( self.settings, 'globalAuthUsername', None ) is not None and getattr( self.settings, 'globalAuthPassword', None ) is not None:
fileContent = fileContent.replace( 'globalAuthPassword = None', 'globalAuthPassword=%r' % self.settings.globalAuthPassword )
fileContent = fileContent.replace( 'globalAuthUsername = None', 'globalAuthUsername=%r' % self.settings.globalAuthUsername )
fileSystemName = os.path.join( self.agentDir, fileName )
newFile = open( fileSystemName, 'w' )
try:
newFile.write( fileContent )
finally:
if newFile is not None:
newFile.close()
# Stop the current agent process
try:
self.processContainer.stopAgentProcess()
self.settings.settingsAgentVersion = newAgentVersion
self.logger.info( 'Agent upgraded to version: ' + newAgentVersion + ' - there is up to a five minute timeout before data will be sent again' )
2020-06-17 03:56:44 -04:00
except Exception as e:
2013-03-29 11:04:46 -07:00
self.logger.error( 'Problem restarting agent process: ' + traceback.format_exc( e ) )
#
# Run the process monitor and update threads.
#
if __name__ == "__main__":
try:
_logger.info( 'Starting agent parent process - version: %s' % ( _settings.settingsAgentVersion ) )
_logger.info( 'Note: If you have hundreds or thousands of databases, disable dbstats on the settings page before running the MMS agent.' )
processContainer = AgentProcessContainer()
# Star the agent monitor thread.
monitorThread = AgentProcessMonitorThread( _logger, sys.path[0], processContainer )
monitorThread.setName( 'AgentProcessMonitorThread' )
monitorThread.setDaemon( True )
monitorThread.start()
# If enabled, start the shutdown listener thread (disabled by default).
if _settings.shutdownAgentBindAddr is not None:
shutdownListenerThread = AgentShutdownListenerThread( _logger, _settings )
shutdownListenerThread.setName( 'AgentShutdownListenerThread' )
shutdownListenerThread.setDaemon( True )
shutdownListenerThread.start()
if _settings.autoUpdateEnabled:
updateThread = AgentUpdateThread( _logger, sys.path[0], _settings, processContainer )
updateThread.setName( 'AgentUpdateThread' )
updateThread.setDaemon( True )
updateThread.start()
_logger.info( 'Started agent parent process - version: %s' % ( _settings.settingsAgentVersion ) )
# The parent process will let the child process know it's alive.
while True:
try:
time.sleep( 2 )
processContainer.pingAgentProcess()
2020-06-17 03:56:44 -04:00
except Exception as exc:
2013-03-29 11:04:46 -07:00
_logger.error( traceback.format_exc( exc ) )
except KeyboardInterrupt:
processContainer.stopAgentProcess()
2020-06-17 03:56:44 -04:00
except Exception as ex:
2013-03-29 11:04:46 -07:00
_logger.error( traceback.format_exc( ex ) )