web.py 20 KB

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