supervisorctl.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654
  1. #!/usr/bin/env python -u
  2. ##############################################################################
  3. #
  4. # Copyright (c) 2007 Agendaless Consulting and Contributors.
  5. # All Rights Reserved.
  6. #
  7. # This software is subject to the provisions of the Zope Public License,
  8. # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
  9. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
  10. # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  11. # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
  12. # FOR A PARTICULAR PURPOSE.
  13. #
  14. ##############################################################################
  15. """supervisorctl -- control applications run by supervisord from the cmd line.
  16. Usage: python supervisorctl.py [-c file] [-h] [action [arguments]]
  17. Options:
  18. -c/--configuration -- configuration file path (default /etc/supervisor.conf)
  19. -h/--help -- print usage message and exit
  20. -i/--interactive -- start an interactive shell after executing commands
  21. -s/--serverurl URL -- URL on which supervisord server is listening
  22. (default "http://localhost:9001").
  23. -u/--username -- username to use for authentication with server
  24. -p/--password -- password to use for authentication with server
  25. action [arguments] -- see below
  26. Actions are commands like "tail" or "stop". If -i is specified or no action is
  27. specified on the command line, a"shell" interpreting actions typed
  28. interactively is started. Use the action "help" to find out about available
  29. actions.
  30. """
  31. import cmd
  32. import sys
  33. import getpass
  34. import xmlrpclib
  35. import socket
  36. import asyncore
  37. import errno
  38. import urlparse
  39. from supervisor.options import ClientOptions
  40. from supervisor import xmlrpc
  41. class Controller(cmd.Cmd):
  42. def __init__(self, options, completekey='tab', stdin=None, stdout=None):
  43. self.options = options
  44. self.prompt = self.options.prompt + '> '
  45. cmd.Cmd.__init__(self, completekey, stdin, stdout)
  46. def emptyline(self):
  47. # We don't want a blank line to repeat the last command.
  48. return
  49. def onecmd(self, line):
  50. """ Override the onecmd method to catch and print all exceptions
  51. """
  52. origline = line
  53. cmd, arg, line = self.parseline(line)
  54. if not line:
  55. return self.emptyline()
  56. if cmd is None:
  57. return self.default(line)
  58. self.lastcmd = line
  59. if cmd == '':
  60. return self.default(line)
  61. else:
  62. try:
  63. func = getattr(self, 'do_' + cmd)
  64. except AttributeError:
  65. return self.default(line)
  66. try:
  67. try:
  68. return func(arg)
  69. except xmlrpclib.ProtocolError, e:
  70. if e.errcode == 401:
  71. if self.options.interactive:
  72. self._output('Server requires authentication')
  73. username = raw_input('Username:')
  74. password = getpass.getpass(prompt='Password:')
  75. self._output('')
  76. self.options.username = username
  77. self.options.password = password
  78. return self.onecmd(origline)
  79. else:
  80. self.options.usage('Server requires authentication')
  81. else:
  82. raise
  83. except SystemExit:
  84. raise
  85. except Exception, e:
  86. (file, fun, line), t, v, tbinfo = asyncore.compact_traceback()
  87. error = 'error: %s, %s: file: %s line: %s' % (t, v, file, line)
  88. self._output(error)
  89. def _output(self, stuff):
  90. if stuff is not None:
  91. self.stdout.write(stuff + '\n')
  92. def _makeNamespace(self, namespace):
  93. proxy = self.options.getServerProxy()
  94. namespace = getattr(proxy, namespace)
  95. return namespace
  96. def _get_supervisor(self):
  97. supervisor = self._makeNamespace('supervisor')
  98. return supervisor
  99. def _upcheck(self):
  100. try:
  101. supervisor = self._get_supervisor()
  102. api = supervisor.getVersion() # deprecated
  103. from supervisor import rpcinterface
  104. if api != rpcinterface.API_VERSION:
  105. self._output(
  106. 'Sorry, this version of supervisorctl expects to '
  107. 'talk to a server with API version %s, but the '
  108. 'remote version is %s.' % (rpcinterface.API_VERSION, api))
  109. return False
  110. except xmlrpclib.Fault, e:
  111. if e.faultCode == xmlrpc.Faults.UNKNOWN_METHOD:
  112. self._output(
  113. 'Sorry, supervisord responded but did not recognize '
  114. 'the supervisor namespace commands that supervisorctl '
  115. 'uses to control it. Please check that the '
  116. '[rpcinterface:supervisor] section is enabled in the '
  117. 'configuration file (see sample.conf).')
  118. return False
  119. raise
  120. except socket.error, why:
  121. if why[0] == errno.ECONNREFUSED:
  122. self._output('%s refused connection' % self.options.serverurl)
  123. return False
  124. raise
  125. return True
  126. def help_help(self):
  127. self._output("help\t\tPrint a list of available actions.")
  128. self._output("help <action>\tPrint help for <action>.")
  129. def do_EOF(self, arg):
  130. self._output('')
  131. return 1
  132. def help_EOF(self):
  133. self._output("To quit, type ^D or use the quit command.")
  134. def _tailf(self, path):
  135. if not self._upcheck():
  136. return
  137. self._output('==> Press Ctrl-C to exit <==')
  138. username = self.options.username
  139. password = self.options.password
  140. try:
  141. # Python's urllib2 (at least as of Python 2.4.2) isn't up
  142. # to this task; it doesn't actually implement a proper
  143. # HTTP/1.1 client that deals with chunked responses (it
  144. # always sends a Connection: close header). We use a
  145. # homegrown client based on asyncore instead. This makes
  146. # me sad.
  147. import http_client
  148. listener = http_client.Listener()
  149. handler = http_client.HTTPHandler(listener, username, password)
  150. handler.get(self.options.serverurl, path)
  151. asyncore.loop()
  152. except KeyboardInterrupt:
  153. handler.close()
  154. self._output('')
  155. return
  156. def do_tail(self, arg):
  157. if not self._upcheck():
  158. return
  159. args = arg.strip().split()
  160. if len(args) < 1:
  161. self._output('Error: too few arguments')
  162. self.help_tail()
  163. return
  164. elif len(args) > 3:
  165. self._output('Error: too many arguments')
  166. self.help_tail()
  167. return
  168. modifier = None
  169. if args[0].startswith('-'):
  170. modifier = args.pop(0)
  171. if len(args) == 1:
  172. processname = args[-1]
  173. channel = 'stdout'
  174. else:
  175. processname = args[0]
  176. channel = args[-1].lower()
  177. if channel not in ('stderr', 'stdout'):
  178. self._output('Error: bad channel %r' % channel)
  179. return
  180. bytes = 1600
  181. if modifier is not None:
  182. what = modifier[1:]
  183. if what == 'f':
  184. bytes = None
  185. else:
  186. try:
  187. bytes = int(what)
  188. except:
  189. self._output('Error: bad argument %s' % modifier)
  190. return
  191. supervisor = self._get_supervisor()
  192. if bytes is None:
  193. return self._tailf('/logtail/%s/%s' % (processname, channel))
  194. else:
  195. try:
  196. if channel is 'stdout':
  197. output = supervisor.readProcessStdoutLog(processname,
  198. -bytes, 0)
  199. else: # if channel is 'stderr'
  200. output = supervisor.readProcessStderrLog(processname,
  201. -bytes, 0)
  202. except xmlrpclib.Fault, e:
  203. template = '%s: ERROR (%s)'
  204. if e.faultCode == xmlrpc.Faults.NO_FILE:
  205. self._output(template % (processname, 'no log file'))
  206. elif e.faultCode == xmlrpc.Faults.FAILED:
  207. self._output(template % (processname,
  208. 'unknown error reading log'))
  209. elif e.faultCode == xmlrpc.Faults.BAD_NAME:
  210. self._output(template % (processname,
  211. 'no such process name'))
  212. else:
  213. self._output(output)
  214. def help_tail(self):
  215. self._output(
  216. "tail [-f] <processname> [stdin|stdout] (default stdout)"
  217. "Ex:\n"
  218. "tail -f <processname>\tContinuous tail of named process stdout\n"
  219. "\t\t\tCtrl-C to exit.\n"
  220. "tail -100 <processname>\tlast 100 *bytes* of process stdout\n"
  221. "tail <processname> stderr\tlast 1600 *bytes* of process stderr\n"
  222. )
  223. def do_maintail(self, arg):
  224. if not self._upcheck():
  225. return
  226. args = arg.strip().split()
  227. if len(args) > 1:
  228. self._output('Error: too many arguments')
  229. self.help_maintail()
  230. return
  231. elif len(args) == 1:
  232. if args[0].startswith('-'):
  233. what = args[0][1:]
  234. if what == 'f':
  235. path = '/mainlogtail'
  236. return self._tailf(path)
  237. try:
  238. what = int(what)
  239. except:
  240. self._output('Error: bad argument %s' % args[0])
  241. return
  242. else:
  243. bytes = what
  244. else:
  245. self._output('Error: bad argument %s' % args[0])
  246. else:
  247. bytes = 1600
  248. supervisor = self._get_supervisor()
  249. try:
  250. output = supervisor.readLog(-bytes, 0)
  251. except xmlrpclib.Fault, e:
  252. template = '%s: ERROR (%s)'
  253. if e.faultCode == xmlrpc.Faults.NO_FILE:
  254. self._output(template % ('supervisord', 'no log file'))
  255. elif e.faultCode == xmlrpc.Faults.FAILED:
  256. self._output(template % ('supervisord',
  257. 'unknown error reading log'))
  258. else:
  259. self._output(output)
  260. def help_maintail(self):
  261. self._output(
  262. "maintail -f \tContinuous tail of supervisor main log file,\n"
  263. "\t\t\tCtrl-C to exit.\n"
  264. "maintail -100\tlast 100 *bytes* of supervisord main log file\n"
  265. "maintail\tlast 1600 *bytes* of supervisor main log file\n"
  266. )
  267. def do_quit(self, arg):
  268. sys.exit(0)
  269. def help_quit(self):
  270. self._output("quit\tExit the supervisor shell.")
  271. do_exit = do_quit
  272. def help_exit(self):
  273. self._output("exit\tExit the supervisor shell.")
  274. def _procrepr(self, info):
  275. template = '%(name)-32s %(state)-10s %(desc)s'
  276. if info['name'] == info['group']:
  277. name = info['name']
  278. else:
  279. name = '%s:%s' % (info['group'], info['name'])
  280. return template % {'name':name, 'state':info['statename'],
  281. 'desc':info['description']}
  282. def do_status(self, arg):
  283. if not self._upcheck():
  284. return
  285. supervisor = self._get_supervisor()
  286. processnames = arg.strip().split()
  287. if processnames:
  288. for processname in processnames:
  289. try:
  290. info = supervisor.getProcessInfo(processname)
  291. except xmlrpclib.Fault, e:
  292. if e.faultCode == xmlrpc.Faults.BAD_NAME:
  293. self._output('No such process %s' % processname)
  294. else:
  295. raise
  296. continue
  297. self._output(self._procrepr(info))
  298. else:
  299. for info in supervisor.getAllProcessInfo():
  300. self._output(self._procrepr(info))
  301. def help_status(self):
  302. self._output("status\t\t\tGet all process status info.")
  303. self._output("status <name>\t\tGet status on a single process by name.")
  304. self._output("status <name> <name>\tGet status on multiple named "
  305. "processes.")
  306. def _startresult(self, code, processname, default=None):
  307. template = '%s: ERROR (%s)'
  308. if code == xmlrpc.Faults.BAD_NAME:
  309. return template % (processname,'no such process')
  310. elif code == xmlrpc.Faults.ALREADY_STARTED:
  311. return template % (processname,'already started')
  312. elif code == xmlrpc.Faults.SPAWN_ERROR:
  313. return template % (processname, 'spawn error')
  314. elif code == xmlrpc.Faults.ABNORMAL_TERMINATION:
  315. return template % (processname, 'abnormal termination')
  316. elif code == xmlrpc.Faults.SUCCESS:
  317. return '%s: started' % processname
  318. return default
  319. def do_start(self, arg):
  320. if not self._upcheck():
  321. return
  322. processnames = arg.strip().split()
  323. supervisor = self._get_supervisor()
  324. if not processnames:
  325. self._output("Error: start requires a process name")
  326. self.help_start()
  327. return
  328. if 'all' in processnames:
  329. results = supervisor.startAllProcesses()
  330. for result in results:
  331. name = result['name']
  332. code = result['status']
  333. result = self._startresult(code, name)
  334. if result is None:
  335. # assertion
  336. raise ValueError('Unknown result code %s for %s' %
  337. (code, name))
  338. else:
  339. self._output(result)
  340. else:
  341. for processname in processnames:
  342. try:
  343. result = supervisor.startProcess(processname)
  344. except xmlrpclib.Fault, e:
  345. error = self._startresult(e.faultCode, processname)
  346. if error is not None:
  347. self._output(error)
  348. else:
  349. raise
  350. else:
  351. if result == True:
  352. self._output('%s: started' % processname)
  353. else:
  354. raise # assertion
  355. def help_start(self):
  356. self._output("start <processname>\t\t\tStart a process.")
  357. self._output("start <processname> <processname>\tStart multiple "
  358. "processes")
  359. self._output("start all\t\t\t\tStart all processes")
  360. self._output(" When all processes are started, they are started "
  361. "in")
  362. self._output(" priority order (see config file)")
  363. def _stopresult(self, code, processname, fault_string=None):
  364. template = '%s: ERROR (%s)'
  365. if code == xmlrpc.Faults.BAD_NAME:
  366. return template % (processname, 'no such process')
  367. elif code == xmlrpc.Faults.NOT_RUNNING:
  368. return template % (processname, 'not running')
  369. elif code == xmlrpc.Faults.SUCCESS:
  370. return '%s: stopped' % processname
  371. elif code == xmlrpc.Faults.FAILED:
  372. return fault_string
  373. return None
  374. def do_stop(self, arg):
  375. if not self._upcheck():
  376. return
  377. processnames = arg.strip().split()
  378. supervisor = self._get_supervisor()
  379. if not processnames:
  380. self._output('Error: stop requires a process name')
  381. self.help_stop()
  382. return
  383. if 'all' in processnames:
  384. results = supervisor.stopAllProcesses()
  385. for result in results:
  386. name = result['name']
  387. code = result['status']
  388. fault_string = result['description']
  389. result = self._stopresult(code, name, fault_string)
  390. if result is None:
  391. # assertion
  392. raise ValueError('Unknown result code %s for %s' %
  393. (code, name))
  394. else:
  395. self._output(result)
  396. else:
  397. for processname in processnames:
  398. try:
  399. result = supervisor.stopProcess(processname)
  400. except xmlrpclib.Fault, e:
  401. error = self._stopresult(e.faultCode, processname,
  402. e.faultString)
  403. if error is not None:
  404. self._output(error)
  405. else:
  406. raise
  407. else:
  408. if result == True:
  409. self._output('%s: stopped' % processname)
  410. else:
  411. raise # assertion
  412. def help_stop(self):
  413. self._output("stop <processname>\t\t\tStop a process.")
  414. self._output("stop <processname> <processname>\tStop multiple "
  415. "processes")
  416. self._output("stop all\t\t\t\tStop all processes")
  417. self._output(" When all processes are stopped, they are stopped "
  418. "in")
  419. self._output(" reverse priority order (see config file)")
  420. def do_restart(self, arg):
  421. if not self._upcheck():
  422. return
  423. processnames = arg.strip().split()
  424. if not processnames:
  425. self._output('Error: restart requires a process name')
  426. self.help_restart()
  427. return
  428. self.do_stop(arg)
  429. self.do_start(arg)
  430. def help_restart(self):
  431. self._output("restart <processname>\t\t\tRestart a process.")
  432. self._output("restart <processname> <processname>\tRestart multiple "
  433. "processes")
  434. self._output("restart all\t\t\t\tRestart all processes")
  435. self._output(" When all processes are restarted, they are "
  436. "started in")
  437. self._output(" priority order (see config file)")
  438. def do_shutdown(self, arg):
  439. if self.options.interactive:
  440. yesno = raw_input('Really shut the remote supervisord process '
  441. 'down y/N? ')
  442. really = yesno.lower().startswith('y')
  443. else:
  444. really = 1
  445. if really:
  446. supervisor = self._get_supervisor()
  447. try:
  448. supervisor.shutdown()
  449. except xmlrpclib.Fault, e:
  450. if e.faultCode == xmlrpc.Faults.SHUTDOWN_STATE:
  451. self._output('ERROR: already shutting down')
  452. else:
  453. self._output('Shut down')
  454. def help_shutdown(self):
  455. self._output("shutdown \t\tShut the remote supervisord down.")
  456. def do_reload(self, arg):
  457. if self.options.interactive:
  458. yesno = raw_input('Really restart the remote supervisord process '
  459. 'y/N? ')
  460. really = yesno.lower().startswith('y')
  461. else:
  462. really = 1
  463. if really:
  464. supervisor = self._get_supervisor()
  465. try:
  466. supervisor.restart()
  467. except xmlrpclib.Fault, e:
  468. if e.faultCode == xmlrpc.Faults.SHUTDOWN_STATE:
  469. self._output('ERROR: already shutting down')
  470. else:
  471. self._output('Restarted supervisord')
  472. def help_reload(self):
  473. self._output("reload \t\tRestart the remote supervisord.")
  474. def _clearresult(self, code, processname, default=None):
  475. template = '%s: ERROR (%s)'
  476. if code == xmlrpc.Faults.BAD_NAME:
  477. return template % (processname, 'no such process')
  478. elif code == xmlrpc.Faults.FAILED:
  479. return template % (processname, 'failed')
  480. elif code == xmlrpc.Faults.SUCCESS:
  481. return '%s: cleared' % processname
  482. return default
  483. def do_clear(self, arg):
  484. if not self._upcheck():
  485. return
  486. processnames = arg.strip().split()
  487. if not processnames:
  488. self._output('Error: clear requires a process name')
  489. self.help_clear()
  490. return
  491. supervisor = self._get_supervisor()
  492. if 'all' in processnames:
  493. results = supervisor.clearAllProcessLogs()
  494. for result in results:
  495. name = result['name']
  496. code = result['status']
  497. result = self._clearresult(code, name)
  498. if result is None:
  499. # assertion
  500. raise ValueError('Unknown result code %s for %s' %
  501. (code, name))
  502. else:
  503. self._output(result)
  504. else:
  505. for processname in processnames:
  506. try:
  507. result = supervisor.clearProcessLogs(processname)
  508. except xmlrpclib.Fault, e:
  509. error = self._clearresult(e.faultCode, processname)
  510. if error is not None:
  511. self._output(error)
  512. else:
  513. raise
  514. else:
  515. if result == True:
  516. self._output('%s: cleared' % processname)
  517. else:
  518. raise # assertion
  519. def help_clear(self):
  520. self._output("clear <processname>\t\t\tClear a process' log files.")
  521. self._output("clear <processname> <processname>\tclear multiple "
  522. "process' log files")
  523. self._output("clear all\t\t\t\tClear all process' log files")
  524. def do_open(self, arg):
  525. url = arg.strip()
  526. parts = urlparse.urlparse(url)
  527. if parts[0] not in ('unix', 'http'):
  528. self._output('ERROR: url must be http:// or unix://')
  529. return
  530. self.options.serverurl = url
  531. self.do_status('')
  532. def help_open(self):
  533. self._output("open <url>\t\t\tConnect to a remote supervisord process.")
  534. self._output("\t\t\t(for UNIX domain socket, use unix:///socket/path)")
  535. def do_version(self, arg):
  536. if not self._upcheck():
  537. return
  538. supervisor = self._get_supervisor()
  539. self._output(supervisor.getSupervisorVersion())
  540. def help_version(self):
  541. self._output("version\t\t\tShow the version of the remote supervisord ")
  542. self._output("\t\t\tprocess")
  543. def main(args=None, options=None):
  544. if options is None:
  545. options = ClientOptions()
  546. options.realize(args, doc=__doc__)
  547. c = Controller(options)
  548. if options.args:
  549. c.onecmd(" ".join(options.args))
  550. if options.interactive:
  551. try:
  552. import readline
  553. except ImportError:
  554. pass
  555. try:
  556. c.onecmd('status')
  557. c.cmdloop()
  558. except KeyboardInterrupt:
  559. c._output('')
  560. pass
  561. if __name__ == "__main__":
  562. main()