web.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623
  1. import os
  2. import re
  3. import cgi
  4. import time
  5. import traceback
  6. import urllib
  7. import datetime
  8. from supervisor.medusa import producers
  9. from supervisor.medusa.http_server import http_date
  10. from supervisor.medusa.http_server import get_header
  11. from supervisor.medusa.xmlrpc_handler import collector
  12. import meld3
  13. from supervisor.process import ProcessStates
  14. from supervisor.http import NOT_DONE_YET
  15. from supervisor.options import VERSION
  16. from supervisor.options import make_namespec
  17. from supervisor.options import split_namespec
  18. from supervisor.xmlrpc import SystemNamespaceRPCInterface
  19. from supervisor.xmlrpc import RootRPCInterface
  20. from supervisor.xmlrpc import Faults
  21. from supervisor.xmlrpc import RPCError
  22. from supervisor.rpcinterface import SupervisorNamespaceRPCInterface
  23. class DeferredWebProducer:
  24. """ A medusa producer that implements a deferred callback; requires
  25. a subclass of asynchat.async_chat that handles NOT_DONE_YET sentinel """
  26. CONNECTION = re.compile ('Connection: (.*)', re.IGNORECASE)
  27. def __init__(self, request, callback):
  28. self.callback = callback
  29. self.request = request
  30. self.finished = False
  31. self.delay = float(callback.delay)
  32. def more(self):
  33. if self.finished:
  34. return ''
  35. try:
  36. response = self.callback()
  37. if response is NOT_DONE_YET:
  38. return NOT_DONE_YET
  39. self.finished = True
  40. return self.sendresponse(response)
  41. except:
  42. tb = traceback.format_exc()
  43. # this should go to the main supervisor log file
  44. self.request.channel.server.logger.log('Web interface error', tb)
  45. self.finished = True
  46. self.request.error(500)
  47. def sendresponse(self, response):
  48. headers = response.get('headers', {})
  49. for header in headers:
  50. self.request[header] = headers[header]
  51. if not self.request.has_key('Content-Type'):
  52. self.request['Content-Type'] = 'text/plain'
  53. if headers.get('Location'):
  54. self.request['Content-Length'] = 0
  55. self.request.error(301)
  56. return
  57. body = response.get('body', '')
  58. self.request['Content-Length'] = len(body)
  59. self.request.push(body)
  60. connection = get_header(self.CONNECTION, self.request.header)
  61. close_it = 0
  62. wrap_in_chunking = 0
  63. if self.request.version == '1.0':
  64. if connection == 'keep-alive':
  65. if not self.request.has_key('Content-Length'):
  66. close_it = 1
  67. else:
  68. self.request['Connection'] = 'Keep-Alive'
  69. else:
  70. close_it = 1
  71. elif self.request.version == '1.1':
  72. if connection == 'close':
  73. close_it = 1
  74. elif not self.request.has_key ('Content-Length'):
  75. if self.request.has_key ('Transfer-Encoding'):
  76. if not self.request['Transfer-Encoding'] == 'chunked':
  77. close_it = 1
  78. elif self.request.use_chunked:
  79. self.request['Transfer-Encoding'] = 'chunked'
  80. wrap_in_chunking = 1
  81. else:
  82. close_it = 1
  83. elif self.request.version is None:
  84. close_it = 1
  85. outgoing_header = producers.simple_producer (
  86. self.request.build_reply_header())
  87. if close_it:
  88. self.request['Connection'] = 'close'
  89. if wrap_in_chunking:
  90. outgoing_producer = producers.chunked_producer (
  91. producers.composite_producer (self.request.outgoing)
  92. )
  93. # prepend the header
  94. outgoing_producer = producers.composite_producer(
  95. [outgoing_header, outgoing_producer]
  96. )
  97. else:
  98. # prepend the header
  99. self.request.outgoing.insert(0, outgoing_header)
  100. outgoing_producer = producers.composite_producer (
  101. self.request.outgoing)
  102. # apply a few final transformations to the output
  103. self.request.channel.push_with_producer (
  104. # globbing gives us large packets
  105. producers.globbing_producer (
  106. # hooking lets us log the number of bytes sent
  107. producers.hooked_producer (
  108. outgoing_producer,
  109. self.request.log
  110. )
  111. )
  112. )
  113. self.request.channel.current_request = None
  114. if close_it:
  115. self.request.channel.close_when_done()
  116. class ViewContext:
  117. def __init__(self, **kw):
  118. self.__dict__.update(kw)
  119. class MeldView:
  120. content_type = 'text/html'
  121. delay = .5
  122. def __init__(self, context):
  123. self.context = context
  124. template = self.context.template
  125. if not os.path.isabs(template):
  126. here = os.path.abspath(os.path.dirname(__file__))
  127. template = os.path.join(here, template)
  128. self.root = meld3.parse_xml(template)
  129. self.callback = None
  130. def __call__(self):
  131. body = self.render()
  132. if body is NOT_DONE_YET:
  133. return NOT_DONE_YET
  134. response = self.context.response
  135. headers = response['headers']
  136. headers['Content-Type'] = self.content_type
  137. headers['Pragma'] = 'no-cache'
  138. headers['Cache-Control'] = 'no-cache'
  139. headers['Expires'] = http_date.build_http_date(0)
  140. response['body'] = body
  141. return response
  142. def clone(self):
  143. return self.root.clone()
  144. class TailView(MeldView):
  145. def render(self):
  146. supervisord = self.context.supervisord
  147. form = self.context.form
  148. if not 'processname' in form:
  149. tail = 'No process name found'
  150. processname = None
  151. else:
  152. processname = form['processname']
  153. offset = 0
  154. limit = form.get('limit', '1024')
  155. if limit.isdigit():
  156. limit = min(-1024, int(limit) * -1)
  157. else:
  158. limit = -1024
  159. if not processname:
  160. tail = 'No process name found'
  161. else:
  162. rpcinterface = SupervisorNamespaceRPCInterface(supervisord)
  163. try:
  164. tail = rpcinterface.readProcessStdoutLog(processname,
  165. limit, offset)
  166. except RPCError, e:
  167. if e.code == Faults.NO_FILE:
  168. tail = 'No file for %s' % processname
  169. else:
  170. tail = 'ERROR: unexpected rpc fault [%d] %s' % (
  171. e.code, e.text)
  172. root = self.clone()
  173. title = root.findmeld('title')
  174. title.content('Supervisor tail of process %s' % processname)
  175. tailbody = root.findmeld('tailbody')
  176. tailbody.content(tail)
  177. refresh_anchor = root.findmeld('refresh_anchor')
  178. if processname is not None:
  179. refresh_anchor.attributes(
  180. href='tail.html?processname=%s&limit=%s' % (
  181. urllib.quote(processname), urllib.quote(str(abs(limit)))
  182. )
  183. )
  184. else:
  185. refresh_anchor.deparent()
  186. return root.write_xhtmlstring()
  187. class StatusView(MeldView):
  188. def actions_for_process(self, process):
  189. state = process.get_state()
  190. processname = urllib.quote(make_namespec(process.group.config.name,
  191. process.config.name))
  192. start = {
  193. 'name':'Start',
  194. 'href':'index.html?processname=%s&action=start' % processname,
  195. 'target':None,
  196. }
  197. restart = {
  198. 'name':'Restart',
  199. 'href':'index.html?processname=%s&action=restart' % processname,
  200. 'target':None,
  201. }
  202. stop = {
  203. 'name':'Stop',
  204. 'href':'index.html?processname=%s&action=stop' % processname,
  205. 'target':None,
  206. }
  207. clearlog = {
  208. 'name':'Clear Log',
  209. 'href':'index.html?processname=%s&action=clearlog' % processname,
  210. 'target':None,
  211. }
  212. tailf = {
  213. 'name':'Tail -f',
  214. 'href':'logtail/%s' % processname,
  215. 'target':'_blank'
  216. }
  217. if state == ProcessStates.RUNNING:
  218. actions = [restart, stop, clearlog, tailf]
  219. elif state in (ProcessStates.STOPPED, ProcessStates.EXITED,
  220. ProcessStates.FATAL):
  221. actions = [start, None, clearlog, tailf]
  222. else:
  223. actions = [None, None, clearlog, tailf]
  224. return actions
  225. def css_class_for_state(self, state):
  226. if state == ProcessStates.RUNNING:
  227. return 'statusrunning'
  228. elif state in (ProcessStates.FATAL, ProcessStates.BACKOFF):
  229. return 'statuserror'
  230. else:
  231. return 'statusnominal'
  232. def make_callback(self, namespec, action):
  233. supervisord = self.context.supervisord
  234. # the rpc interface code is already written to deal properly in a
  235. # deferred world, so just use it
  236. main = ('supervisor', SupervisorNamespaceRPCInterface(supervisord))
  237. system = ('system', SystemNamespaceRPCInterface([main]))
  238. rpcinterface = RootRPCInterface([main, system])
  239. if action:
  240. if action == 'refresh':
  241. def donothing():
  242. message = 'Page refreshed at %s' % time.ctime()
  243. return message
  244. donothing.delay = 0.05
  245. return donothing
  246. elif action == 'stopall':
  247. callback = rpcinterface.supervisor.stopAllProcesses()
  248. def stopall():
  249. if callback() is NOT_DONE_YET:
  250. return NOT_DONE_YET
  251. else:
  252. return 'All stopped at %s' % time.ctime()
  253. stopall.delay = 0.05
  254. return stopall
  255. elif action == 'restartall':
  256. callback = rpcinterface.system.multicall(
  257. [ {'methodName':'supervisor.stopAllProcesses'},
  258. {'methodName':'supervisor.startAllProcesses'} ] )
  259. def restartall():
  260. result = callback()
  261. if result is NOT_DONE_YET:
  262. return NOT_DONE_YET
  263. return 'All restarted at %s' % time.ctime()
  264. restartall.delay = 0.05
  265. return restartall
  266. elif namespec:
  267. def wrong():
  268. return 'No such process named %s' % namespec
  269. wrong.delay = 0.05
  270. group_name, process_name = split_namespec(namespec)
  271. group = supervisord.process_groups.get(group_name)
  272. if group is None:
  273. return wrong
  274. process = group.processes.get(process_name)
  275. if process is None:
  276. return wrong
  277. elif action == 'stop':
  278. try:
  279. callback = rpcinterface.supervisor.stopProcess(
  280. namespec)
  281. except RPCError, e:
  282. def stoperr():
  283. return 'unexpected rpc fault [%d] %s' % (
  284. e.code, e.text)
  285. stoperr.delay = 0.05
  286. return stoperr
  287. def stopprocess():
  288. try:
  289. result = callback()
  290. except RPCError, e:
  291. return 'unexpected rpc fault [%d] %s' % (
  292. e.code, e.text)
  293. if result is NOT_DONE_YET:
  294. return NOT_DONE_YET
  295. return 'Process %s stopped' % namespec
  296. stopprocess.delay = 0.05
  297. return stopprocess
  298. elif action == 'restart':
  299. callback = rpcinterface.system.multicall(
  300. [ {'methodName':'supervisor.stopProcess',
  301. 'params': [namespec]},
  302. {'methodName':'supervisor.startProcess',
  303. 'params': [namespec]},
  304. ]
  305. )
  306. def restartprocess():
  307. result = callback()
  308. if result is NOT_DONE_YET:
  309. return NOT_DONE_YET
  310. return 'Process %s restarted' % namespec
  311. restartprocess.delay = 0.05
  312. return restartprocess
  313. elif action == 'start':
  314. try:
  315. callback = rpcinterface.supervisor.startProcess(
  316. namespec)
  317. except RPCError, e:
  318. if e.code == Faults.NO_FILE:
  319. msg = 'no such file'
  320. elif e.code == Faults.NOT_EXECUTABLE:
  321. msg = 'file not executable'
  322. elif e.code == Faults.ALREADY_STARTED:
  323. msg = 'already started'
  324. elif e.code == Faults.SPAWN_ERROR:
  325. msg = 'spawn error'
  326. elif e.code == Faults.ABNORMAL_TERMINATION:
  327. msg = 'abnormal termination'
  328. else:
  329. msg = 'unexpected rpc fault [%d] %s' % (
  330. e.code, e.text)
  331. def starterr():
  332. return 'ERROR: Process %s: %s' % (namespec, msg)
  333. starterr.delay = 0.05
  334. return starterr
  335. def startprocess():
  336. try:
  337. result = callback()
  338. except RPCError, e:
  339. if e.code == Faults.SPAWN_ERROR:
  340. msg = 'spawn error'
  341. elif e.code == Faults.ABNORMAL_TERMINATION:
  342. msg = 'abnormal termination'
  343. else:
  344. msg = 'unexpected rpc fault [%d] %s' % (
  345. e.code, e.text)
  346. return 'ERROR: Process %s: %s' % (namespec, msg)
  347. if result is NOT_DONE_YET:
  348. return NOT_DONE_YET
  349. return 'Process %s started' % namespec
  350. startprocess.delay = 0.05
  351. return startprocess
  352. elif action == 'clearlog':
  353. callback = rpcinterface.supervisor.clearProcessLogs(
  354. namespec)
  355. def clearlog():
  356. return 'Log for %s cleared' % namespec
  357. clearlog.delay = 0.05
  358. return clearlog
  359. raise ValueError(action)
  360. def render(self):
  361. form = self.context.form
  362. response = self.context.response
  363. processname = form.get('processname')
  364. action = form.get('action')
  365. message = form.get('message')
  366. if action:
  367. if not self.callback:
  368. self.callback = self.make_callback(processname, action)
  369. return NOT_DONE_YET
  370. else:
  371. message = self.callback()
  372. if message is NOT_DONE_YET:
  373. return NOT_DONE_YET
  374. if message is not None:
  375. server_url = form['SERVER_URL']
  376. location = server_url + '?message=%s' % urllib.quote(
  377. message)
  378. response['headers']['Location'] = location
  379. supervisord = self.context.supervisord
  380. rpcinterface = RootRPCInterface(
  381. [('supervisor',
  382. SupervisorNamespaceRPCInterface(supervisord))]
  383. )
  384. processnames = []
  385. groups = supervisord.process_groups.values()
  386. for group in groups:
  387. gprocnames = group.processes.keys()
  388. for gprocname in gprocnames:
  389. processnames.append((group.config.name, gprocname))
  390. processnames.sort()
  391. data = []
  392. for groupname, processname in processnames:
  393. actions = self.actions_for_process(
  394. supervisord.process_groups[groupname].processes[processname])
  395. sent_name = make_namespec(groupname, processname)
  396. info = rpcinterface.supervisor.getProcessInfo(sent_name)
  397. data.append({
  398. 'status':info['statename'],
  399. 'name':processname,
  400. 'group':groupname,
  401. 'actions':actions,
  402. 'state':info['state'],
  403. 'description':info['description'],
  404. })
  405. root = self.clone()
  406. if message is not None:
  407. statusarea = root.findmeld('statusmessage')
  408. statusarea.attrib['class'] = 'status_msg'
  409. statusarea.content(message)
  410. if data:
  411. iterator = root.findmeld('tr').repeat(data)
  412. shaded_tr = False
  413. for tr_element, item in iterator:
  414. status_text = tr_element.findmeld('status_text')
  415. status_text.content(item['status'].lower())
  416. status_text.attrib['class'] = self.css_class_for_state(
  417. item['state'])
  418. info_text = tr_element.findmeld('info_text')
  419. info_text.content(item['description'])
  420. anchor = tr_element.findmeld('name_anchor')
  421. processname = make_namespec(item['group'], item['name'])
  422. anchor.attributes(href='tail.html?processname=%s' %
  423. urllib.quote(processname))
  424. anchor.content(processname)
  425. actions = item['actions']
  426. actionitem_td = tr_element.findmeld('actionitem_td')
  427. for li_element, actionitem in actionitem_td.repeat(actions):
  428. anchor = li_element.findmeld('actionitem_anchor')
  429. if actionitem is None:
  430. anchor.attrib['class'] = 'hidden'
  431. else:
  432. anchor.attributes(href=actionitem['href'],
  433. name=actionitem['name'])
  434. anchor.content(actionitem['name'])
  435. if actionitem['target']:
  436. anchor.attributes(target=actionitem['target'])
  437. if shaded_tr:
  438. tr_element.attrib['class'] = 'shade'
  439. shaded_tr = not shaded_tr
  440. else:
  441. table = root.findmeld('statustable')
  442. table.replace('No programs to manage')
  443. root.findmeld('supervisor_version').content(VERSION)
  444. copyright_year = str(datetime.date.today().year)
  445. root.findmeld('copyright_date').content(copyright_year)
  446. return root.write_xhtmlstring()
  447. class OKView:
  448. delay = 0
  449. def __init__(self, context):
  450. self.context = context
  451. def __call__(self):
  452. return {'body':'OK'}
  453. VIEWS = {
  454. 'index.html': {
  455. 'template':'ui/status.html',
  456. 'view':StatusView
  457. },
  458. 'tail.html': {
  459. 'template':'ui/tail.html',
  460. 'view':TailView,
  461. },
  462. 'ok.html': {
  463. 'template':None,
  464. 'view':OKView,
  465. },
  466. }
  467. class supervisor_ui_handler:
  468. IDENT = 'Supervisor Web UI HTTP Request Handler'
  469. def __init__(self, supervisord):
  470. self.supervisord = supervisord
  471. def match(self, request):
  472. if request.command not in ('POST', 'GET'):
  473. return False
  474. path, params, query, fragment = request.split_uri()
  475. while path.startswith('/'):
  476. path = path[1:]
  477. if not path:
  478. path = 'index.html'
  479. for viewname in VIEWS.keys():
  480. if viewname == path:
  481. return True
  482. def handle_request(self, request):
  483. if request.command == 'POST':
  484. request.collector = collector(self, request)
  485. else:
  486. self.continue_request('', request)
  487. def continue_request (self, data, request):
  488. form = {}
  489. cgi_env = request.cgi_environment()
  490. form.update(cgi_env)
  491. if not form.has_key('QUERY_STRING'):
  492. form['QUERY_STRING'] = ''
  493. query = form['QUERY_STRING']
  494. # we only handle x-www-form-urlencoded values from POSTs
  495. form_urlencoded = cgi.parse_qsl(data)
  496. query_data = cgi.parse_qs(query)
  497. for k, v in query_data.items():
  498. # ignore dupes
  499. form[k] = v[0]
  500. for k, v in form_urlencoded:
  501. # ignore dupes
  502. form[k] = v
  503. form['SERVER_URL'] = request.get_server_url()
  504. path = form['PATH_INFO']
  505. # strip off all leading slashes
  506. while path and path[0] == '/':
  507. path = path[1:]
  508. if not path:
  509. path = 'index.html'
  510. viewinfo = VIEWS.get(path)
  511. if viewinfo is None:
  512. # this should never happen if our match method works
  513. return
  514. response = {}
  515. response['headers'] = {}
  516. viewclass = viewinfo['view']
  517. viewtemplate = viewinfo['template']
  518. context = ViewContext(template=viewtemplate,
  519. request = request,
  520. form = form,
  521. response = response,
  522. supervisord=self.supervisord)
  523. view = viewclass(context)
  524. pushproducer = request.channel.push_with_producer
  525. pushproducer(DeferredWebProducer(request, view))