web.py 23 KB

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