spacepaste

  1.  
  2. #!/usr/bin/env python2.6
  3. #
  4. # Copyright (C) 2012-2015 Tagged
  5. #
  6. # All rights reserved.
  7. """Python application to install or restart a given Tagged application"""
  8. import collections
  9. import ConfigParser
  10. import os
  11. import shlex
  12. import signal
  13. import subprocess
  14. import sys
  15. import time
  16. import psutil
  17. # Make sure we're running at least Python 2.6
  18. # and not running Python 3
  19. pyvers = sys.version_info[:2]
  20. if pyvers < (2, 6):
  21. raise RuntimeError('Python 2.6 is required to use this program')
  22. if pyvers[0] == 3:
  23. raise RuntimeError('Python 3.x is not supported at this time, please '
  24. 'use Python 2.6+')
  25. # NOTE: The following three top-level methods are from TDS's
  26. # utils/processes.py module. Once a proper TDS 'core' package
  27. # has been created, it should be imported and these should
  28. # be removed.
  29. def run(cmd, expect_return_code=0, shell=False, **kwds):
  30. """Wrapper to run external command"""
  31. proc = start_process(cmd, shell=shell, **kwds)
  32. return wait_for_process(proc, expect_return_code=expect_return_code,
  33. **kwds)
  34. def start_process(cmd, shell=False, **kwds):
  35. """
  36. Start a subprocess.
  37. Return a token-like object that can be used in a call to
  38. `wait_for_process` to end the process and get the results.
  39. """
  40. if isinstance(cmd, basestring):
  41. args = shlex.split(cmd.replace('\\', '\\\\'))
  42. else:
  43. args = cmd
  44. args = map(str, args)
  45. try:
  46. start = time.time()
  47. proc = subprocess.Popen(args, stdout=subprocess.PIPE,
  48. stderr=subprocess.PIPE, shell=shell, **kwds)
  49. proc.cmd = args
  50. proc.start_time = start
  51. except OSError as e:
  52. proc.start_time = start
  53. except OSError as e:
  54. exc = subprocess.CalledProcessError(1, args)
  55. exc.stderr = 'Error using Popen: %s' % e
  56. exc.stdout = None
  57. raise exc
  58. return proc
  59. def wait_for_process(proc, expect_return_code=0, **_kwds):
  60. """
  61. Finalize a process token and return information about the ended process.
  62. This is a blocking call if the subprocess has not yet finished.
  63. process.duration is not strictly correct -- if the process ended
  64. before the call to this function, the duration will be inflated.
  65. """
  66. stdout, stderr = proc.communicate()
  67. end = time.time()
  68. duration = end - proc.start_time
  69. if not (expect_return_code is None or
  70. expect_return_code == proc.returncode):
  71. exc = subprocess.CalledProcessError(proc.returncode, proc.cmd)
  72. exc.stderr = stderr
  73. exc.stdout = stdout
  74. exc.duration = duration
  75. raise exc
  76. process = collections.namedtuple(
  77. 'Process',
  78. ['cmd', 'stdout', 'stderr', 'returncode', 'duration']
  79. )
  80. return process(
  81. cmd=proc.cmd,
  82. stdout=stdout,
  83. stderr=stderr,
  84. returncode=proc.returncode,
  85. duration=duration
  86. )
  87. class TDSAppManagerError(Exception):
  88. pass
  89. class TDSAppManager(object):
  90. """
  91. """
  92. def __init__(
  93. self, app, version=None, do_restart=False, do_uninstall=False
  94. ):
  95. """Basic setup and initial checks"""
  96. self.app = app
  97. self.version = version
  98. self.restart = do_restart
  99. self.uninstall = do_uninstall
  100. self.tds_conf = '/etc/tagops/tds.conf'
  101. if sum(
  102. bool(param) for param in [version, do_restart, do_uninstall]
  103. ) > 1:
  104. raise TDSAppManagerError(
  105. 'Only one of these may be defined: '
  106. 'package version, "restart", "uninstall"'
  107. )
  108. if self.version:
  109. try:
  110. self.version = int(self.version)
  111. except ValueError:
  112. raise TDSAppManagerError(
  113. 'Version passed (%s) was not a number' % self.version
  114. )
  115. self.app_check()
  116. def app_check(self):
  117. """Verify application is valid and is allowed to be on host"""
  118. try:
  119. with open(self.tds_conf) as conf_file:
  120. config = ConfigParser.SafeConfigParser()
  121. config.readfp(conf_file)
  122. except IOError as err:
  123. raise TDSAppManagerError(
  124. 'Unable to access the configuration file %s: %s'
  125. % (self.tds_conf, err)
  126. )
  127. try:
  128. apps = config.get('applications', 'valid')
  129. except ConfigParser.NoOptionError as err:
  130. raise TDSAppManagerError(
  131. 'Failed to get configuration information: %s' % err
  132. )
  133. valid_apps = [x.strip() for x in apps.split(',')]
  134. if self.app not in valid_apps:
  135. raise TDSAppManagerError(
  136. 'Application "%s" is not allowed on this system' % self.app
  137. )
  138. def get_version(self):
  139. """Return current version of application on host"""
  140. vers_cmd = ['/bin/rpm', '-q', '--queryformat', '%{VERSION}', self.app]
  141. try:
  142. return run(vers_cmd).stdout
  143. except subprocess.CalledProcessError:
  144. # This failing almost certainly means the package isn't
  145. # installed, so return None
  146. return None
  147. @staticmethod
  148. def manage_services(action):
  149. """Manage defined application services with a given action"""
  150. svc_cmd = ['/usr/local/tagops/sbin/services', action]
  151. try:
  152. result = run(svc_cmd)
  153. except subprocess.CalledProcessError as err:
  154. raise TDSAppManagerError(
  155. 'Failed to run "%s": %s' % (' '.join(svc_cmd), err)
  156. )
  157. if result.returncode:
  158. raise TDSAppManagerError(
  159. 'Action "%s" on defined services failed: %s'
  160. % (action, result.stderr)
  161. )
  162. @staticmethod
  163. def puppet_check():
  164. """Check for a running Puppet client process"""
  165. pids = []
  166. for proc in psutil.process_iter():
  167. if (proc.name == 'puppet' and 'agent' in proc.cmdline
  168. and proc.username == 'root'):
  169. pids.append(proc.pid)
  170. return pids
  171. @classmethod
  172. def stop_puppet(cls):
  173. """Stop the Puppet client process if it's running"""
  174. running = cls.puppet_check()
  175. if running:
  176. print 'Puppet process(es) running, trying to stop nicely'
  177. for idx in xrange(0, 10):
  178. for pid in running:
  179. try:
  180. os.kill(pid, signal.SIGINT)
  181. except OSError:
  182. # Process already gone, ignore
  183. pass
  184. time.sleep(1)
  185. running = cls.puppet_check()
  186. if not running:
  187. break
  188. else:
  189. print 'Puppet process(es) still running, sending kill'
  190. for pid in running:
  191. os.kill(pid, signal.SIGKILL)
  192. def verify_install(self):
  193. """Verify the installed application is the correct version"""
  194. inst_version = self.get_version()
  195. if inst_version != self.version:
  196. raise TDSAppManagerError(
  197. 'Incorrect version of "%s" installed: %s. Should be %s.'
  198. % (self.app, inst_version, self.version)
  199. )
  200. def perform_install(self):
  201. """Install the given version of the application on the host"""
  202. # Acquire current installed version
  203. inst_version = self.get_version()
  204. # Compare versions as integers for now; this will need to change
  205. # if we move to non-integer versions
  206. if inst_version is None or int(inst_version) < int(self.version):
  207. yum_op = "install"
  208. else:
  209. yum_op = "downgrade"
  210. makecache_cmd = [
  211. '/usr/bin/yum', '--disablerepo=*', '--enablerepo=deploy',
  212. 'makecache'
  213. ]
  214. try:
  215. run(makecache_cmd)
  216. except subprocess.CalledProcessError as err:
  217. raise TDSAppManagerError(
  218. 'Unable to run "%s": %s' % (' '.join(makecache_cmd), err)
  219. )
  220. # Stop any running Puppet agent, then stop services before install
  221. #
  222. # NOTE: We don't disable the puppet agent because Puppet will not
  223. # touch the package on the host if a host deployment entry exists,
  224. # which will be the case here
  225. self.stop_puppet()
  226. self.manage_services('stop')
  227. install_cmd = [
  228. '/usr/bin/yum', '-y', '--nogpgcheck', yum_op,
  229. '%s-%s-1' % (self.app, self.version)
  230. ]
  231. try:
  232. result = run(install_cmd)
  233. except subprocess.CalledProcessError as err:
  234. raise TDSAppManagerError(
  235. 'Failed to run "%s": %s' % (' '.join(install_cmd), err)
  236. )
  237. if result.returncode:
  238. raise TDSAppManagerError(
  239. 'Unable to install application "%s" via yum: %s'
  240. % (self.app, result.stderr)
  241. )
  242. # 'yum install/upgrade' may return 0 even with a failure
  243. # so check to be sure
  244. inst_version = self.get_version()
  245. # Currently version is a number, so handle inst_version
  246. # as such; this will change
  247. if int(inst_version) != self.version:
  248. raise TDSAppManagerError(
  249. 'Application "%s" was not installed/upgraded - '
  250. 'Installed: "%r", Expected: "%r"'
  251. % (self.app, inst_version, self.version)
  252. )
  253. # Start services again
  254. self.manage_services('start')
  255. def perform_restart(self):
  256. """Restart the application on the host"""
  257. self.manage_services('restart')
  258. def perform_uninstall(self):
  259. """Uninstall the application on the host"""
  260. # Stop any running Puppet agent, then stop services before uninstall
  261. #
  262. # NOTE: We don't disable the puppet agent because Puppet will not
  263. # touch the package on the host if a host deployment entry exists,
  264. # which will be the case here
  265. self.stop_puppet()
  266. self.manage_services('stop')
  267. uninstall_cmd = [
  268. '/usr/bin/yum', '-y', '--nogpgcheck', 'remove', self.app
  269. ]
  270. try:
  271. result = run(uninstall_cmd)
  272. except subprocess.CalledProcessError as err:
  273. raise TDSAppManagerError(
  274. 'Failed to run "%s": %s' % (' '.join(uninstall_cmd), err)
  275. )
  276. if result.returncode:
  277. raise TDSAppManagerError(
  278. 'Unable to uninstall application "%s" via yum: %s'
  279. % (self.app, result.stderr)
  280. )
  281. # 'yum remove' almost always returns 0 even with failures
  282. inst_version = self.get_version()
  283. if inst_version is not None:
  284. raise TDSAppManagerError(
  285. 'Application "%s" was not uninstalled' % self.app
  286. )
  287. def perform_task(self):
  288. """Perform task based on parameters passed"""
  289. if self.restart:
  290. self.perform_restart()
  291. elif self.uninstall:
  292. self.perform_uninstall()
  293. else:
  294. self.perform_install()
  295. # Methods called directly by Salt
  296. def _task(app, **kwargs):
  297. app_manager = TDSAppManager(app, **kwargs)
  298. try:
  299. app_manager.perform_task()
  300. return 'successful'
  301. except TDSAppManagerError as err:
  302. return err
  303. def install(opts):
  304. app, version = opts
  305. result = _task(app, version=version)
  306. if result == 'successful':
  307. return 'Install of app "%s", version "%s" successful' \
  308. % (app, version)
  309. else:
  310. return 'Install of app "%s", version "%s" failed. Reason: %s' \
  311. % (app, version, result)
  312. def restart(opts):
  313. app = opts[0] # Single element list
  314. result = _task(app, do_restart=True)
  315. if result == 'successful':
  316. return 'Restart of app "%s" successful' % app
  317. else:
  318. return 'Restart of app "%s" failed' % app
  319. def uninstall(opts):
  320. app = opts[0] # Single element list
  321. result = _task(app, do_uninstall=True)
  322. if result == 'successful':
  323. return 'Uninstall of app "%s" successful' % app
  324. else:
  325. return 'Uninstall of app "%s" failed' % app
  326. # Allow program to be run via command line
  327. def main():
  328. """ """
  329. if len(sys.argv) != 3:
  330. sys.exit(
  331. 'Usage: %s <application> [<version>|restart|uninstall]'
  332. % sys.argv[0]
  333. )
  334. do_restart = False
  335. do_uninstall = False
  336. app, version = sys.argv[1:]
  337. if sys.argv[2] == 'restart':
  338. do_restart = True
  339. version = None
  340. elif sys.argv[2] == 'uninstall':
  341. do_uninstall = True
  342. version = None
  343. else:
  344. try:
  345. version = int(sys.argv[2])
  346. except ValueError:
  347. sys.exit('Version passed (%s) was not a number' % sys.argv[2])
  348. app_manager = TDSAppManager(app, version, do_restart, do_uninstall)
  349. try:
  350. app_manager.perform_task()
  351. except TDSAppManagerError as err:
  352. sys.exit(err)
  353. if __name__ == '__main__':
  354. main()
  355.