rediscli.py 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. import logging
  2. import shlex
  3. import warnings
  4. from flask import request
  5. from jinja2 import Markup
  6. from flask_admin.base import BaseView, expose
  7. from flask_admin.babel import gettext
  8. from flask_admin._compat import VER
  9. # Set up logger
  10. log = logging.getLogger("flask-admin.redis")
  11. class CommandError(Exception):
  12. """
  13. RedisCli error exception.
  14. """
  15. pass
  16. class TextWrapper(str):
  17. """
  18. Small text wrapper for result formatter to distinguish between
  19. different string types.
  20. """
  21. pass
  22. class RedisCli(BaseView):
  23. """
  24. Simple redis console.
  25. To use it, simply pass `Redis` connection object to the constructor.
  26. """
  27. shlex_check = True
  28. """
  29. shlex from stdlib does not work with unicode on 2.7.2 and lower.
  30. If you want to suppress warning, set this attribute to False.
  31. """
  32. remapped_commands = {
  33. 'del': 'delete'
  34. }
  35. """
  36. List of redis remapped commands.
  37. """
  38. excluded_commands = set(('pubsub', 'set_response_callback', 'from_url'))
  39. """
  40. List of excluded commands.
  41. """
  42. def __init__(self, redis,
  43. name=None, category=None, endpoint=None, url=None):
  44. """
  45. Constructor.
  46. :param redis:
  47. Redis connection
  48. :param name:
  49. View name. If not provided, will use the model class name
  50. :param category:
  51. View category
  52. :param endpoint:
  53. Base endpoint. If not provided, will use the model name + 'view'.
  54. For example if model name was 'User', endpoint will be
  55. 'userview'
  56. :param url:
  57. Base URL. If not provided, will use endpoint as a URL.
  58. """
  59. super(RedisCli, self).__init__(name, category, endpoint, url)
  60. self.redis = redis
  61. self.commands = {}
  62. self._inspect_commands()
  63. self._contribute_commands()
  64. if self.shlex_check and VER < (2, 7, 3):
  65. warnings.warn('Warning: rediscli uses shlex library and it does '
  66. 'not work with unicode until Python 2.7.3. To '
  67. 'remove this warning, upgrade to Python 2.7.3 or '
  68. 'suppress it by setting shlex_check attribute '
  69. 'to False.')
  70. def _inspect_commands(self):
  71. """
  72. Inspect connection object and extract command names.
  73. """
  74. for name in dir(self.redis):
  75. if not name.startswith('_'):
  76. attr = getattr(self.redis, name)
  77. if callable(attr) and name not in self.excluded_commands:
  78. doc = (getattr(attr, '__doc__', '') or '').strip()
  79. self.commands[name] = (attr, doc)
  80. for new, old in self.remapped_commands.items():
  81. self.commands[new] = self.commands[old]
  82. def _contribute_commands(self):
  83. """
  84. Contribute custom commands.
  85. """
  86. self.commands['help'] = (self._cmd_help, 'Help!')
  87. def _execute_command(self, name, args):
  88. """
  89. Execute single command.
  90. :param name:
  91. Command name
  92. :param args:
  93. Command arguments
  94. """
  95. # Do some remapping
  96. new_cmd = self.remapped_commands.get(name)
  97. if new_cmd:
  98. name = new_cmd
  99. # Execute command
  100. if name not in self.commands:
  101. return self._error(gettext('Cli: Invalid command.'))
  102. handler, _ = self.commands[name]
  103. return self._result(handler(*args))
  104. def _parse_cmd(self, cmd):
  105. """
  106. Parse command by using shlex module.
  107. :param cmd:
  108. Command to parse
  109. """
  110. if VER < (2, 7, 3):
  111. # shlex can't work with unicode until 2.7.3
  112. return tuple(x.decode('utf-8') for x in shlex.split(cmd.encode('utf-8')))
  113. return tuple(shlex.split(cmd))
  114. def _error(self, msg):
  115. """
  116. Format error message as HTTP response.
  117. :param msg:
  118. Message to format
  119. """
  120. return Markup('<div class="error">%s</div>' % msg)
  121. def _result(self, result):
  122. """
  123. Format result message as HTTP response.
  124. :param msg:
  125. Result to format.
  126. """
  127. return self.render('admin/rediscli/response.html',
  128. type_name=lambda d: type(d).__name__,
  129. result=result)
  130. # Commands
  131. def _cmd_help(self, *args):
  132. """
  133. Help command implementation.
  134. """
  135. if not args:
  136. help = 'Usage: help <command>.\nList of supported commands: '
  137. help += ', '.join(n for n in sorted(self.commands))
  138. return TextWrapper(help)
  139. cmd = args[0]
  140. if cmd not in self.commands:
  141. raise CommandError('Invalid command.')
  142. help = self.commands[cmd][1]
  143. if not help:
  144. return TextWrapper('Command does not have any help.')
  145. return TextWrapper(help)
  146. # Views
  147. @expose('/')
  148. def console_view(self):
  149. """
  150. Console view.
  151. """
  152. return self.render('admin/rediscli/console.html')
  153. @expose('/run/', methods=('POST',))
  154. def execute_view(self):
  155. """
  156. AJAX API.
  157. """
  158. try:
  159. cmd = request.form.get('cmd').lower()
  160. if not cmd:
  161. return self._error('Cli: Empty command.')
  162. parts = self._parse_cmd(cmd)
  163. if not parts:
  164. return self._error('Cli: Failed to parse command.')
  165. return self._execute_command(parts[0], parts[1:])
  166. except CommandError as err:
  167. return self._error('Cli: %s' % err)
  168. except Exception as ex:
  169. log.exception(ex)
  170. return self._error('Cli: %s' % ex)