PageRenderTime 51ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/src/pybind/mgr/dashboard/module.py

https://gitlab.com/unofficial-mirrors/ceph
Python | 410 lines | 363 code | 29 blank | 18 comment | 20 complexity | c0b3100847b6ec04562599fb6a212635 MD5 | raw file
  1. # -*- coding: utf-8 -*-
  2. """
  3. openATTIC mgr plugin (based on CherryPy)
  4. """
  5. from __future__ import absolute_import
  6. import errno
  7. from distutils.version import StrictVersion
  8. import os
  9. import socket
  10. import tempfile
  11. import threading
  12. from uuid import uuid4
  13. from OpenSSL import crypto
  14. from mgr_module import MgrModule, MgrStandbyModule
  15. try:
  16. from urlparse import urljoin
  17. except ImportError:
  18. from urllib.parse import urljoin
  19. try:
  20. import cherrypy
  21. from cherrypy._cptools import HandlerWrapperTool
  22. except ImportError:
  23. # To be picked up and reported by .can_run()
  24. cherrypy = None
  25. # The SSL code in CherryPy 3.5.0 is buggy. It was fixed long ago,
  26. # but 3.5.0 is still shipping in major linux distributions
  27. # (Fedora 27, Ubuntu Xenial), so we must monkey patch it to get SSL working.
  28. if cherrypy is not None:
  29. v = StrictVersion(cherrypy.__version__)
  30. # It was fixed in 3.7.0. Exact lower bound version is probably earlier,
  31. # but 3.5.0 is what this monkey patch is tested on.
  32. if v >= StrictVersion("3.5.0") and v < StrictVersion("3.7.0"):
  33. from cherrypy.wsgiserver.wsgiserver2 import HTTPConnection,\
  34. CP_fileobject
  35. def fixed_init(hc_self, server, sock, makefile=CP_fileobject):
  36. hc_self.server = server
  37. hc_self.socket = sock
  38. hc_self.rfile = makefile(sock, "rb", hc_self.rbufsize)
  39. hc_self.wfile = makefile(sock, "wb", hc_self.wbufsize)
  40. hc_self.requests_seen = 0
  41. HTTPConnection.__init__ = fixed_init
  42. if 'COVERAGE_ENABLED' in os.environ:
  43. import coverage
  44. _cov = coverage.Coverage(config_file="{}/.coveragerc".format(os.path.dirname(__file__)))
  45. _cov.start()
  46. # pylint: disable=wrong-import-position
  47. from . import logger, mgr
  48. from .controllers import generate_routes, json_error_page
  49. from .controllers.auth import Auth
  50. from .tools import SessionExpireAtBrowserCloseTool, NotificationQueue, \
  51. RequestLoggingTool, TaskManager
  52. from .services.exception import dashboard_exception_handler
  53. from .settings import options_command_list, options_schema_list, \
  54. handle_option_command
  55. # cherrypy likes to sys.exit on error. don't let it take us down too!
  56. # pylint: disable=W0613
  57. def os_exit_noop(*args):
  58. pass
  59. # pylint: disable=W0212
  60. os._exit = os_exit_noop
  61. def prepare_url_prefix(url_prefix):
  62. """
  63. return '' if no prefix, or '/prefix' without slash in the end.
  64. """
  65. url_prefix = urljoin('/', url_prefix)
  66. return url_prefix.rstrip('/')
  67. class ServerConfigException(Exception):
  68. pass
  69. class SSLCherryPyConfig(object):
  70. """
  71. Class for common server configuration done by both active and
  72. standby module, especially setting up SSL.
  73. """
  74. def __init__(self):
  75. self._stopping = threading.Event()
  76. self._url_prefix = ""
  77. self.cert_tmp = None
  78. self.pkey_tmp = None
  79. def shutdown(self):
  80. self._stopping.set()
  81. @property
  82. def url_prefix(self):
  83. return self._url_prefix
  84. def _configure(self):
  85. """
  86. Configure CherryPy and initialize self.url_prefix
  87. :returns our URI
  88. """
  89. server_addr = self.get_localized_config('server_addr', '::')
  90. server_port = self.get_localized_config('server_port', '8080')
  91. if server_addr is None:
  92. raise ServerConfigException(
  93. 'no server_addr configured; '
  94. 'try "ceph config set mgr mgr/{}/{}/server_addr <ip>"'
  95. .format(self.module_name, self.get_mgr_id()))
  96. self.log.info('server_addr: %s server_port: %s', server_addr,
  97. server_port)
  98. # Initialize custom handlers.
  99. cherrypy.tools.authenticate = cherrypy.Tool('before_handler', Auth.check_auth)
  100. cherrypy.tools.session_expire_at_browser_close = SessionExpireAtBrowserCloseTool()
  101. cherrypy.tools.request_logging = RequestLoggingTool()
  102. cherrypy.tools.dashboard_exception_handler = HandlerWrapperTool(dashboard_exception_handler,
  103. priority=31)
  104. # SSL initialization
  105. cert = self.get_store("crt")
  106. if cert is not None:
  107. self.cert_tmp = tempfile.NamedTemporaryFile()
  108. self.cert_tmp.write(cert.encode('utf-8'))
  109. self.cert_tmp.flush() # cert_tmp must not be gc'ed
  110. cert_fname = self.cert_tmp.name
  111. else:
  112. cert_fname = self.get_localized_config('crt_file')
  113. pkey = self.get_store("key")
  114. if pkey is not None:
  115. self.pkey_tmp = tempfile.NamedTemporaryFile()
  116. self.pkey_tmp.write(pkey.encode('utf-8'))
  117. self.pkey_tmp.flush() # pkey_tmp must not be gc'ed
  118. pkey_fname = self.pkey_tmp.name
  119. else:
  120. pkey_fname = self.get_localized_config('key_file')
  121. if not cert_fname or not pkey_fname:
  122. raise ServerConfigException('no certificate configured')
  123. if not os.path.isfile(cert_fname):
  124. raise ServerConfigException('certificate %s does not exist' % cert_fname)
  125. if not os.path.isfile(pkey_fname):
  126. raise ServerConfigException('private key %s does not exist' % pkey_fname)
  127. # Apply the 'global' CherryPy configuration.
  128. config = {
  129. 'engine.autoreload.on': False,
  130. 'server.socket_host': server_addr,
  131. 'server.socket_port': int(server_port),
  132. 'server.ssl_module': 'builtin',
  133. 'server.ssl_certificate': cert_fname,
  134. 'server.ssl_private_key': pkey_fname,
  135. 'error_page.default': json_error_page,
  136. 'tools.request_logging.on': True
  137. }
  138. cherrypy.config.update(config)
  139. self._url_prefix = prepare_url_prefix(self.get_config('url_prefix',
  140. default=''))
  141. uri = "https://{0}:{1}{2}/".format(
  142. socket.getfqdn() if server_addr == "::" else server_addr,
  143. server_port,
  144. self.url_prefix
  145. )
  146. return uri
  147. def await_configuration(self):
  148. """
  149. Block until configuration is ready (i.e. all needed keys are set)
  150. or self._stopping is set.
  151. :returns URI of configured webserver
  152. """
  153. while not self._stopping.is_set():
  154. try:
  155. uri = self._configure()
  156. except ServerConfigException as e:
  157. self.log.info("Config not ready to serve, waiting: {0}".format(
  158. e
  159. ))
  160. # Poll until a non-errored config is present
  161. self._stopping.wait(5)
  162. else:
  163. self.log.info("Configured CherryPy, starting engine...")
  164. return uri
  165. class Module(MgrModule, SSLCherryPyConfig):
  166. """
  167. dashboard module entrypoint
  168. """
  169. COMMANDS = [
  170. {
  171. 'cmd': 'dashboard set-login-credentials '
  172. 'name=username,type=CephString '
  173. 'name=password,type=CephString',
  174. 'desc': 'Set the login credentials',
  175. 'perm': 'w'
  176. },
  177. {
  178. 'cmd': 'dashboard set-session-expire '
  179. 'name=seconds,type=CephInt',
  180. 'desc': 'Set the session expire timeout',
  181. 'perm': 'w'
  182. },
  183. {
  184. "cmd": "dashboard create-self-signed-cert",
  185. "desc": "Create self signed certificate",
  186. "perm": "w"
  187. },
  188. ]
  189. COMMANDS.extend(options_command_list())
  190. OPTIONS = [
  191. {'name': 'server_addr'},
  192. {'name': 'server_port'},
  193. {'name': 'session-expire'},
  194. {'name': 'password'},
  195. {'name': 'url_prefix'},
  196. {'name': 'username'},
  197. {'name': 'key_file'},
  198. {'name': 'crt_file'},
  199. ]
  200. OPTIONS.extend(options_schema_list())
  201. def __init__(self, *args, **kwargs):
  202. super(Module, self).__init__(*args, **kwargs)
  203. SSLCherryPyConfig.__init__(self)
  204. mgr.init(self)
  205. self._stopping = threading.Event()
  206. @classmethod
  207. def can_run(cls):
  208. if cherrypy is None:
  209. return False, "Missing dependency: cherrypy"
  210. if not os.path.exists(cls.get_frontend_path()):
  211. return False, "Frontend assets not found: incomplete build?"
  212. return True, ""
  213. @classmethod
  214. def get_frontend_path(cls):
  215. current_dir = os.path.dirname(os.path.abspath(__file__))
  216. return os.path.join(current_dir, 'frontend/dist')
  217. def serve(self):
  218. if 'COVERAGE_ENABLED' in os.environ:
  219. _cov.start()
  220. uri = self.await_configuration()
  221. if uri is None:
  222. # We were shut down while waiting
  223. return
  224. # Publish the URI that others may use to access the service we're
  225. # about to start serving
  226. self.set_uri(uri)
  227. mapper = generate_routes(self.url_prefix)
  228. config = {
  229. '{}/'.format(self.url_prefix): {
  230. 'tools.staticdir.on': True,
  231. 'tools.staticdir.dir': self.get_frontend_path(),
  232. 'tools.staticdir.index': 'index.html'
  233. },
  234. '{}/api'.format(self.url_prefix): {'request.dispatch': mapper}
  235. }
  236. cherrypy.tree.mount(None, config=config)
  237. cherrypy.engine.start()
  238. NotificationQueue.start_queue()
  239. TaskManager.init()
  240. logger.info('Waiting for engine...')
  241. cherrypy.engine.block()
  242. if 'COVERAGE_ENABLED' in os.environ:
  243. _cov.stop()
  244. _cov.save()
  245. logger.info('Engine done')
  246. def shutdown(self):
  247. super(Module, self).shutdown()
  248. SSLCherryPyConfig.shutdown(self)
  249. logger.info('Stopping server...')
  250. NotificationQueue.stop()
  251. cherrypy.engine.exit()
  252. logger.info('Stopped server')
  253. def handle_command(self, cmd):
  254. res = handle_option_command(cmd)
  255. if res[0] != -errno.ENOSYS:
  256. return res
  257. if cmd['prefix'] == 'dashboard set-login-credentials':
  258. Auth.set_login_credentials(cmd['username'], cmd['password'])
  259. return 0, 'Username and password updated', ''
  260. elif cmd['prefix'] == 'dashboard set-session-expire':
  261. self.set_config('session-expire', str(cmd['seconds']))
  262. return 0, 'Session expiration timeout updated', ''
  263. elif cmd['prefix'] == 'dashboard create-self-signed-cert':
  264. self.create_self_signed_cert()
  265. return 0, 'Self-signed certificate created', ''
  266. return (-errno.EINVAL, '', 'Command not found \'{0}\''
  267. .format(cmd['prefix']))
  268. def create_self_signed_cert(self):
  269. # create a key pair
  270. pkey = crypto.PKey()
  271. pkey.generate_key(crypto.TYPE_RSA, 2048)
  272. # create a self-signed cert
  273. cert = crypto.X509()
  274. cert.get_subject().O = "IT"
  275. cert.get_subject().CN = "ceph-dashboard"
  276. cert.set_serial_number(int(uuid4()))
  277. cert.gmtime_adj_notBefore(0)
  278. cert.gmtime_adj_notAfter(10*365*24*60*60)
  279. cert.set_issuer(cert.get_subject())
  280. cert.set_pubkey(pkey)
  281. cert.sign(pkey, 'sha512')
  282. cert = crypto.dump_certificate(crypto.FILETYPE_PEM, cert)
  283. self.set_store('crt', cert.decode('utf-8'))
  284. pkey = crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey)
  285. self.set_store('key', pkey.decode('utf-8'))
  286. def notify(self, notify_type, notify_id):
  287. NotificationQueue.new_notification(notify_type, notify_id)
  288. class StandbyModule(MgrStandbyModule, SSLCherryPyConfig):
  289. def __init__(self, *args, **kwargs):
  290. super(StandbyModule, self).__init__(*args, **kwargs)
  291. SSLCherryPyConfig.__init__(self)
  292. # We can set the global mgr instance to ourselves even though
  293. # we're just a standby, because it's enough for logging.
  294. mgr.init(self)
  295. def serve(self):
  296. uri = self.await_configuration()
  297. if uri is None:
  298. # We were shut down while waiting
  299. return
  300. module = self
  301. class Root(object):
  302. @cherrypy.expose
  303. def index(self):
  304. active_uri = module.get_active_uri()
  305. if active_uri:
  306. module.log.info("Redirecting to active '%s'", active_uri)
  307. raise cherrypy.HTTPRedirect(active_uri)
  308. else:
  309. template = """
  310. <html>
  311. <!-- Note: this is only displayed when the standby
  312. does not know an active URI to redirect to, otherwise
  313. a simple redirect is returned instead -->
  314. <head>
  315. <title>Ceph</title>
  316. <meta http-equiv="refresh" content="{delay}">
  317. </head>
  318. <body>
  319. No active ceph-mgr instance is currently running
  320. the dashboard. A failover may be in progress.
  321. Retrying in {delay} seconds...
  322. </body>
  323. </html>
  324. """
  325. return template.format(delay=5)
  326. cherrypy.tree.mount(Root(), "{}/".format(self.url_prefix), {})
  327. self.log.info("Starting engine...")
  328. cherrypy.engine.start()
  329. self.log.info("Waiting for engine...")
  330. cherrypy.engine.wait(state=cherrypy.engine.states.STOPPED)
  331. self.log.info("Engine done.")
  332. def shutdown(self):
  333. SSLCherryPyConfig.shutdown(self)
  334. self.log.info("Stopping server...")
  335. cherrypy.engine.wait(state=cherrypy.engine.states.STARTED)
  336. cherrypy.engine.stop()
  337. self.log.info("Stopped server")