web.py 20 KB

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