#!/usr/bin/env python2.6 # # Copyright (C) 2012-2015 Tagged # # All rights reserved. """Python application to install or restart a given Tagged application""" import collections import ConfigParser import os import shlex import signal import subprocess import sys import time import psutil # Make sure we're running at least Python 2.6 # and not running Python 3 pyvers = sys.version_info[:2] if pyvers < (2, 6): raise RuntimeError('Python 2.6 is required to use this program') if pyvers[0] == 3: raise RuntimeError('Python 3.x is not supported at this time, please ' 'use Python 2.6+') # NOTE: The following three top-level methods are from TDS's # utils/processes.py module. Once a proper TDS 'core' package # has been created, it should be imported and these should # be removed. def run(cmd, expect_return_code=0, shell=False, **kwds): """Wrapper to run external command""" proc = start_process(cmd, shell=shell, **kwds) return wait_for_process(proc, expect_return_code=expect_return_code, **kwds) def start_process(cmd, shell=False, **kwds): """ Start a subprocess. Return a token-like object that can be used in a call to `wait_for_process` to end the process and get the results. """ if isinstance(cmd, basestring): args = shlex.split(cmd.replace('\\', '\\\\')) else: args = cmd args = map(str, args) try: start = time.time() proc = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=shell, **kwds) proc.cmd = args proc.start_time = start except OSError as e: proc.start_time = start except OSError as e: exc = subprocess.CalledProcessError(1, args) exc.stderr = 'Error using Popen: %s' % e exc.stdout = None raise exc return proc def wait_for_process(proc, expect_return_code=0, **_kwds): """ Finalize a process token and return information about the ended process. This is a blocking call if the subprocess has not yet finished. process.duration is not strictly correct -- if the process ended before the call to this function, the duration will be inflated. """ stdout, stderr = proc.communicate() end = time.time() duration = end - proc.start_time if not (expect_return_code is None or expect_return_code == proc.returncode): exc = subprocess.CalledProcessError(proc.returncode, proc.cmd) exc.stderr = stderr exc.stdout = stdout exc.duration = duration raise exc process = collections.namedtuple( 'Process', ['cmd', 'stdout', 'stderr', 'returncode', 'duration'] ) return process( cmd=proc.cmd, stdout=stdout, stderr=stderr, returncode=proc.returncode, duration=duration ) class TDSAppManagerError(Exception): pass class TDSAppManager(object): """ """ def __init__( self, app, version=None, do_restart=False, do_uninstall=False ): """Basic setup and initial checks""" self.app = app self.version = version self.restart = do_restart self.uninstall = do_uninstall self.tds_conf = '/etc/tagops/tds.conf' if sum( bool(param) for param in [version, do_restart, do_uninstall] ) > 1: raise TDSAppManagerError( 'Only one of these may be defined: ' 'package version, "restart", "uninstall"' ) if self.version: try: self.version = int(self.version) except ValueError: raise TDSAppManagerError( 'Version passed (%s) was not a number' % self.version ) self.app_check() def app_check(self): """Verify application is valid and is allowed to be on host""" try: with open(self.tds_conf) as conf_file: config = ConfigParser.SafeConfigParser() config.readfp(conf_file) except IOError as err: raise TDSAppManagerError( 'Unable to access the configuration file %s: %s' % (self.tds_conf, err) ) try: apps = config.get('applications', 'valid') except ConfigParser.NoOptionError as err: raise TDSAppManagerError( 'Failed to get configuration information: %s' % err ) valid_apps = [x.strip() for x in apps.split(',')] if self.app not in valid_apps: raise TDSAppManagerError( 'Application "%s" is not allowed on this system' % self.app ) def get_version(self): """Return current version of application on host""" vers_cmd = ['/bin/rpm', '-q', '--queryformat', '%{VERSION}', self.app] try: return run(vers_cmd).stdout except subprocess.CalledProcessError: # This failing almost certainly means the package isn't # installed, so return None return None @staticmethod def manage_services(action): """Manage defined application services with a given action""" svc_cmd = ['/usr/local/tagops/sbin/services', action] try: result = run(svc_cmd) except subprocess.CalledProcessError as err: raise TDSAppManagerError( 'Failed to run "%s": %s' % (' '.join(svc_cmd), err) ) if result.returncode: raise TDSAppManagerError( 'Action "%s" on defined services failed: %s' % (action, result.stderr) ) @staticmethod def puppet_check(): """Check for a running Puppet client process""" pids = [] for proc in psutil.process_iter(): if (proc.name == 'puppet' and 'agent' in proc.cmdline and proc.username == 'root'): pids.append(proc.pid) return pids @classmethod def stop_puppet(cls): """Stop the Puppet client process if it's running""" running = cls.puppet_check() if running: print 'Puppet process(es) running, trying to stop nicely' for idx in xrange(0, 10): for pid in running: try: os.kill(pid, signal.SIGINT) except OSError: # Process already gone, ignore pass time.sleep(1) running = cls.puppet_check() if not running: break else: print 'Puppet process(es) still running, sending kill' for pid in running: os.kill(pid, signal.SIGKILL) def verify_install(self): """Verify the installed application is the correct version""" inst_version = self.get_version() if inst_version != self.version: raise TDSAppManagerError( 'Incorrect version of "%s" installed: %s. Should be %s.' % (self.app, inst_version, self.version) ) def perform_install(self): """Install the given version of the application on the host""" # Acquire current installed version inst_version = self.get_version() # Compare versions as integers for now; this will need to change # if we move to non-integer versions if inst_version is None or int(inst_version) < int(self.version): yum_op = "install" else: yum_op = "downgrade" makecache_cmd = [ '/usr/bin/yum', '--disablerepo=*', '--enablerepo=deploy', 'makecache' ] try: run(makecache_cmd) except subprocess.CalledProcessError as err: raise TDSAppManagerError( 'Unable to run "%s": %s' % (' '.join(makecache_cmd), err) ) # Stop any running Puppet agent, then stop services before install # # NOTE: We don't disable the puppet agent because Puppet will not # touch the package on the host if a host deployment entry exists, # which will be the case here self.stop_puppet() self.manage_services('stop') install_cmd = [ '/usr/bin/yum', '-y', '--nogpgcheck', yum_op, '%s-%s-1' % (self.app, self.version) ] try: result = run(install_cmd) except subprocess.CalledProcessError as err: raise TDSAppManagerError( 'Failed to run "%s": %s' % (' '.join(install_cmd), err) ) if result.returncode: raise TDSAppManagerError( 'Unable to install application "%s" via yum: %s' % (self.app, result.stderr) ) # 'yum install/upgrade' may return 0 even with a failure # so check to be sure inst_version = self.get_version() # Currently version is a number, so handle inst_version # as such; this will change if int(inst_version) != self.version: raise TDSAppManagerError( 'Application "%s" was not installed/upgraded - ' 'Installed: "%r", Expected: "%r"' % (self.app, inst_version, self.version) ) # Start services again self.manage_services('start') def perform_restart(self): """Restart the application on the host""" self.manage_services('restart') def perform_uninstall(self): """Uninstall the application on the host""" # Stop any running Puppet agent, then stop services before uninstall # # NOTE: We don't disable the puppet agent because Puppet will not # touch the package on the host if a host deployment entry exists, # which will be the case here self.stop_puppet() self.manage_services('stop') uninstall_cmd = [ '/usr/bin/yum', '-y', '--nogpgcheck', 'remove', self.app ] try: result = run(uninstall_cmd) except subprocess.CalledProcessError as err: raise TDSAppManagerError( 'Failed to run "%s": %s' % (' '.join(uninstall_cmd), err) ) if result.returncode: raise TDSAppManagerError( 'Unable to uninstall application "%s" via yum: %s' % (self.app, result.stderr) ) # 'yum remove' almost always returns 0 even with failures inst_version = self.get_version() if inst_version is not None: raise TDSAppManagerError( 'Application "%s" was not uninstalled' % self.app ) def perform_task(self): """Perform task based on parameters passed""" if self.restart: self.perform_restart() elif self.uninstall: self.perform_uninstall() else: self.perform_install() # Methods called directly by Salt def _task(app, **kwargs): app_manager = TDSAppManager(app, **kwargs) try: app_manager.perform_task() return 'successful' except TDSAppManagerError as err: return err def install(opts): app, version = opts result = _task(app, version=version) if result == 'successful': return 'Install of app "%s", version "%s" successful' \ % (app, version) else: return 'Install of app "%s", version "%s" failed. Reason: %s' \ % (app, version, result) def restart(opts): app = opts[0] # Single element list result = _task(app, do_restart=True) if result == 'successful': return 'Restart of app "%s" successful' % app else: return 'Restart of app "%s" failed' % app def uninstall(opts): app = opts[0] # Single element list result = _task(app, do_uninstall=True) if result == 'successful': return 'Uninstall of app "%s" successful' % app else: return 'Uninstall of app "%s" failed' % app # Allow program to be run via command line def main(): """ """ if len(sys.argv) != 3: sys.exit( 'Usage: %s [|restart|uninstall]' % sys.argv[0] ) do_restart = False do_uninstall = False app, version = sys.argv[1:] if sys.argv[2] == 'restart': do_restart = True version = None elif sys.argv[2] == 'uninstall': do_uninstall = True version = None else: try: version = int(sys.argv[2]) except ValueError: sys.exit('Version passed (%s) was not a number' % sys.argv[2]) app_manager = TDSAppManager(app, version, do_restart, do_uninstall) try: app_manager.perform_task() except TDSAppManagerError as err: sys.exit(err) if __name__ == '__main__': main()