]> git.proxmox.com Git - ceph.git/blame_incremental - ceph/src/pybind/mgr/dashboard/module.py
update download target update for octopus release
[ceph.git] / ceph / src / pybind / mgr / dashboard / module.py
... / ...
CommitLineData
1# -*- coding: utf-8 -*-
2"""
3ceph dashboard mgr plugin (based on CherryPy)
4"""
5from __future__ import absolute_import
6
7import collections
8import errno
9import os
10import socket
11import tempfile
12import threading
13import time
14from uuid import uuid4
15from OpenSSL import crypto
16from mgr_module import MgrModule, MgrStandbyModule, Option, CLIWriteCommand
17from mgr_util import get_default_addr, ServerConfigException, verify_tls_files
18
19try:
20 import cherrypy
21 from cherrypy._cptools import HandlerWrapperTool
22except ImportError:
23 # To be picked up and reported by .can_run()
24 cherrypy = None
25
26from .services.sso import load_sso_db
27
28if cherrypy is not None:
29 from .cherrypy_backports import patch_cherrypy
30 patch_cherrypy(cherrypy.__version__)
31
32if 'COVERAGE_ENABLED' in os.environ:
33 import coverage
34 __cov = coverage.Coverage(config_file="{}/.coveragerc".format(os.path.dirname(__file__)),
35 data_suffix=True)
36
37 cherrypy.engine.subscribe('start', __cov.start)
38 cherrypy.engine.subscribe('after_request', __cov.save)
39 cherrypy.engine.subscribe('stop', __cov.stop)
40
41# pylint: disable=wrong-import-position
42from . import logger, mgr
43from .controllers import generate_routes, json_error_page
44from .grafana import push_local_dashboards
45from .tools import NotificationQueue, RequestLoggingTool, TaskManager, \
46 prepare_url_prefix, str_to_bool
47from .services.auth import AuthManager, AuthManagerTool, JwtManager
48from .services.sso import SSO_COMMANDS, \
49 handle_sso_command
50from .services.exception import dashboard_exception_handler
51from .settings import options_command_list, options_schema_list, \
52 handle_option_command
53
54from .plugins import PLUGIN_MANAGER
55from .plugins import feature_toggles, debug # noqa # pylint: disable=unused-import
56
57
58PLUGIN_MANAGER.hook.init()
59
60
61# cherrypy likes to sys.exit on error. don't let it take us down too!
62# pylint: disable=W0613
63def os_exit_noop(*args):
64 pass
65
66
67# pylint: disable=W0212
68os._exit = os_exit_noop
69
70
71class CherryPyConfig(object):
72 """
73 Class for common server configuration done by both active and
74 standby module, especially setting up SSL.
75 """
76
77 def __init__(self):
78 self._stopping = threading.Event()
79 self._url_prefix = ""
80
81 self.cert_tmp = None
82 self.pkey_tmp = None
83
84 def shutdown(self):
85 self._stopping.set()
86
87 @property
88 def url_prefix(self):
89 return self._url_prefix
90
91 @staticmethod
92 def update_cherrypy_config(config):
93 PLUGIN_MANAGER.hook.configure_cherrypy(config=config)
94 cherrypy.config.update(config)
95
96 # pylint: disable=too-many-branches
97 def _configure(self):
98 """
99 Configure CherryPy and initialize self.url_prefix
100
101 :returns our URI
102 """
103 server_addr = self.get_localized_module_option(
104 'server_addr', get_default_addr())
105 ssl = self.get_localized_module_option('ssl', True)
106 if not ssl:
107 server_port = self.get_localized_module_option('server_port', 8080)
108 else:
109 server_port = self.get_localized_module_option('ssl_server_port', 8443)
110
111 if server_addr is None:
112 raise ServerConfigException(
113 'no server_addr configured; '
114 'try "ceph config set mgr mgr/{}/{}/server_addr <ip>"'
115 .format(self.module_name, self.get_mgr_id()))
116 self.log.info('server: ssl=%s host=%s port=%d', 'yes' if ssl else 'no',
117 server_addr, server_port)
118
119 # Initialize custom handlers.
120 cherrypy.tools.authenticate = AuthManagerTool()
121 cherrypy.tools.plugin_hooks_filter_request = cherrypy.Tool(
122 'before_handler',
123 lambda: PLUGIN_MANAGER.hook.filter_request_before_handler(request=cherrypy.request),
124 priority=1)
125 cherrypy.tools.request_logging = RequestLoggingTool()
126 cherrypy.tools.dashboard_exception_handler = HandlerWrapperTool(dashboard_exception_handler,
127 priority=31)
128
129 # Apply the 'global' CherryPy configuration.
130 config = {
131 'engine.autoreload.on': False,
132 'server.socket_host': server_addr,
133 'server.socket_port': int(server_port),
134 'error_page.default': json_error_page,
135 'tools.request_logging.on': True,
136 'tools.gzip.on': True,
137 'tools.gzip.mime_types': [
138 # text/html and text/plain are the default types to compress
139 'text/html', 'text/plain',
140 # We also want JSON and JavaScript to be compressed
141 'application/json',
142 'application/javascript',
143 ],
144 'tools.json_in.on': True,
145 'tools.json_in.force': False,
146 'tools.plugin_hooks_filter_request.on': True,
147 }
148
149 if ssl:
150 # SSL initialization
151 cert = self.get_store("crt")
152 if cert is not None:
153 self.cert_tmp = tempfile.NamedTemporaryFile()
154 self.cert_tmp.write(cert.encode('utf-8'))
155 self.cert_tmp.flush() # cert_tmp must not be gc'ed
156 cert_fname = self.cert_tmp.name
157 else:
158 cert_fname = self.get_localized_module_option('crt_file')
159
160 pkey = self.get_store("key")
161 if pkey is not None:
162 self.pkey_tmp = tempfile.NamedTemporaryFile()
163 self.pkey_tmp.write(pkey.encode('utf-8'))
164 self.pkey_tmp.flush() # pkey_tmp must not be gc'ed
165 pkey_fname = self.pkey_tmp.name
166 else:
167 pkey_fname = self.get_localized_module_option('key_file')
168
169 verify_tls_files(cert_fname, pkey_fname)
170
171 config['server.ssl_module'] = 'builtin'
172 config['server.ssl_certificate'] = cert_fname
173 config['server.ssl_private_key'] = pkey_fname
174
175 self.update_cherrypy_config(config)
176
177 self._url_prefix = prepare_url_prefix(self.get_module_option('url_prefix',
178 default=''))
179
180 uri = "{0}://{1}:{2}{3}/".format(
181 'https' if ssl else 'http',
182 socket.getfqdn() if server_addr in ['::', '0.0.0.0'] else server_addr,
183 server_port,
184 self.url_prefix
185 )
186
187 return uri
188
189 def await_configuration(self):
190 """
191 Block until configuration is ready (i.e. all needed keys are set)
192 or self._stopping is set.
193
194 :returns URI of configured webserver
195 """
196 while not self._stopping.is_set():
197 try:
198 uri = self._configure()
199 except ServerConfigException as e:
200 self.log.info("Config not ready to serve, waiting: {0}".format(
201 e
202 ))
203 # Poll until a non-errored config is present
204 self._stopping.wait(5)
205 else:
206 self.log.info("Configured CherryPy, starting engine...")
207 return uri
208
209
210class Module(MgrModule, CherryPyConfig):
211 """
212 dashboard module entrypoint
213 """
214
215 COMMANDS = [
216 {
217 'cmd': 'dashboard set-jwt-token-ttl '
218 'name=seconds,type=CephInt',
219 'desc': 'Set the JWT token TTL in seconds',
220 'perm': 'w'
221 },
222 {
223 'cmd': 'dashboard get-jwt-token-ttl',
224 'desc': 'Get the JWT token TTL in seconds',
225 'perm': 'r'
226 },
227 {
228 "cmd": "dashboard create-self-signed-cert",
229 "desc": "Create self signed certificate",
230 "perm": "w"
231 },
232 {
233 "cmd": "dashboard grafana dashboards update",
234 "desc": "Push dashboards to Grafana",
235 "perm": "w",
236 },
237 ]
238 COMMANDS.extend(options_command_list())
239 COMMANDS.extend(SSO_COMMANDS)
240 PLUGIN_MANAGER.hook.register_commands()
241
242 MODULE_OPTIONS = [
243 Option(name='server_addr', type='str', default=get_default_addr()),
244 Option(name='server_port', type='int', default=8080),
245 Option(name='ssl_server_port', type='int', default=8443),
246 Option(name='jwt_token_ttl', type='int', default=28800),
247 Option(name='password', type='str', default=''),
248 Option(name='url_prefix', type='str', default=''),
249 Option(name='username', type='str', default=''),
250 Option(name='key_file', type='str', default=''),
251 Option(name='crt_file', type='str', default=''),
252 Option(name='ssl', type='bool', default=True),
253 Option(name='standby_behaviour', type='str', default='redirect',
254 enum_allowed=['redirect', 'error']),
255 Option(name='standby_error_status_code', type='int', default=500,
256 min=400, max=599)
257 ]
258 MODULE_OPTIONS.extend(options_schema_list())
259 for options in PLUGIN_MANAGER.hook.get_options() or []:
260 MODULE_OPTIONS.extend(options)
261
262 __pool_stats = collections.defaultdict(lambda: collections.defaultdict(
263 lambda: collections.deque(maxlen=10)))
264
265 def __init__(self, *args, **kwargs):
266 super(Module, self).__init__(*args, **kwargs)
267 CherryPyConfig.__init__(self)
268
269 mgr.init(self)
270
271 self._stopping = threading.Event()
272 self.shutdown_event = threading.Event()
273
274 self.ACCESS_CTRL_DB = None
275 self.SSO_DB = None
276
277 @classmethod
278 def can_run(cls):
279 if cherrypy is None:
280 return False, "Missing dependency: cherrypy"
281
282 if not os.path.exists(cls.get_frontend_path()):
283 return False, "Frontend assets not found: incomplete build?"
284
285 return True, ""
286
287 @classmethod
288 def get_frontend_path(cls):
289 current_dir = os.path.dirname(os.path.abspath(__file__))
290 return os.path.join(current_dir, 'frontend/dist')
291
292 def serve(self):
293 AuthManager.initialize()
294 load_sso_db()
295
296 uri = self.await_configuration()
297 if uri is None:
298 # We were shut down while waiting
299 return
300
301 # Publish the URI that others may use to access the service we're
302 # about to start serving
303 self.set_uri(uri)
304
305 mapper, parent_urls = generate_routes(self.url_prefix)
306
307 config = {}
308 for purl in parent_urls:
309 config[purl] = {
310 'request.dispatch': mapper
311 }
312
313 cherrypy.tree.mount(None, config=config)
314
315 PLUGIN_MANAGER.hook.setup()
316
317 cherrypy.engine.start()
318 NotificationQueue.start_queue()
319 TaskManager.init()
320 logger.info('Engine started.')
321 update_dashboards = str_to_bool(
322 self.get_module_option('GRAFANA_UPDATE_DASHBOARDS', 'False'))
323 if update_dashboards:
324 logger.info('Starting Grafana dashboard task')
325 TaskManager.run(
326 'grafana/dashboards/update',
327 {},
328 push_local_dashboards,
329 kwargs=dict(tries=10, sleep=60),
330 )
331 # wait for the shutdown event
332 self.shutdown_event.wait()
333 self.shutdown_event.clear()
334 NotificationQueue.stop()
335 cherrypy.engine.stop()
336 logger.info('Engine stopped')
337
338 def shutdown(self):
339 super(Module, self).shutdown()
340 CherryPyConfig.shutdown(self)
341 logger.info('Stopping engine...')
342 self.shutdown_event.set()
343
344 @CLIWriteCommand("dashboard set-ssl-certificate",
345 "name=mgr_id,type=CephString,req=false")
346 def set_ssl_certificate(self, mgr_id=None, inbuf=None):
347 if inbuf is None:
348 return -errno.EINVAL, '',\
349 'Please specify the certificate file with "-i" option'
350 if mgr_id is not None:
351 self.set_store('{}/crt'.format(mgr_id), inbuf)
352 else:
353 self.set_store('crt', inbuf)
354 return 0, 'SSL certificate updated', ''
355
356 @CLIWriteCommand("dashboard set-ssl-certificate-key",
357 "name=mgr_id,type=CephString,req=false")
358 def set_ssl_certificate_key(self, mgr_id=None, inbuf=None):
359 if inbuf is None:
360 return -errno.EINVAL, '',\
361 'Please specify the certificate key file with "-i" option'
362 if mgr_id is not None:
363 self.set_store('{}/key'.format(mgr_id), inbuf)
364 else:
365 self.set_store('key', inbuf)
366 return 0, 'SSL certificate key updated', ''
367
368 def handle_command(self, inbuf, cmd):
369 # pylint: disable=too-many-return-statements
370 res = handle_option_command(cmd)
371 if res[0] != -errno.ENOSYS:
372 return res
373 res = handle_sso_command(cmd)
374 if res[0] != -errno.ENOSYS:
375 return res
376 if cmd['prefix'] == 'dashboard set-jwt-token-ttl':
377 self.set_module_option('jwt_token_ttl', str(cmd['seconds']))
378 return 0, 'JWT token TTL updated', ''
379 if cmd['prefix'] == 'dashboard get-jwt-token-ttl':
380 ttl = self.get_module_option('jwt_token_ttl', JwtManager.JWT_TOKEN_TTL)
381 return 0, str(ttl), ''
382 if cmd['prefix'] == 'dashboard create-self-signed-cert':
383 self.create_self_signed_cert()
384 return 0, 'Self-signed certificate created', ''
385 if cmd['prefix'] == 'dashboard grafana dashboards update':
386 push_local_dashboards()
387 return 0, 'Grafana dashboards updated', ''
388
389 return (-errno.EINVAL, '', 'Command not found \'{0}\''
390 .format(cmd['prefix']))
391
392 def create_self_signed_cert(self):
393 # create a key pair
394 pkey = crypto.PKey()
395 pkey.generate_key(crypto.TYPE_RSA, 2048)
396
397 # create a self-signed cert
398 cert = crypto.X509()
399 cert.get_subject().O = "IT"
400 cert.get_subject().CN = "ceph-dashboard"
401 cert.set_serial_number(int(uuid4()))
402 cert.gmtime_adj_notBefore(0)
403 cert.gmtime_adj_notAfter(10*365*24*60*60)
404 cert.set_issuer(cert.get_subject())
405 cert.set_pubkey(pkey)
406 cert.sign(pkey, 'sha512')
407
408 cert = crypto.dump_certificate(crypto.FILETYPE_PEM, cert)
409 self.set_store('crt', cert.decode('utf-8'))
410
411 pkey = crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey)
412 self.set_store('key', pkey.decode('utf-8'))
413
414 def notify(self, notify_type, notify_id):
415 NotificationQueue.new_notification(notify_type, notify_id)
416
417 def get_updated_pool_stats(self):
418 df = self.get('df')
419 pool_stats = {p['id']: p['stats'] for p in df['pools']}
420 now = time.time()
421 for pool_id, stats in pool_stats.items():
422 for stat_name, stat_val in stats.items():
423 self.__pool_stats[pool_id][stat_name].append((now, stat_val))
424
425 return self.__pool_stats
426
427
428class StandbyModule(MgrStandbyModule, CherryPyConfig):
429 def __init__(self, *args, **kwargs):
430 super(StandbyModule, self).__init__(*args, **kwargs)
431 CherryPyConfig.__init__(self)
432 self.shutdown_event = threading.Event()
433
434 # We can set the global mgr instance to ourselves even though
435 # we're just a standby, because it's enough for logging.
436 mgr.init(self)
437
438 def serve(self):
439 uri = self.await_configuration()
440 if uri is None:
441 # We were shut down while waiting
442 return
443
444 module = self
445
446 class Root(object):
447 @cherrypy.expose
448 def default(self, *args, **kwargs):
449 if module.get_module_option('standby_behaviour', 'redirect') == 'redirect':
450 active_uri = module.get_active_uri()
451 if active_uri:
452 module.log.info("Redirecting to active '%s'", active_uri)
453 raise cherrypy.HTTPRedirect(active_uri)
454 else:
455 template = """
456 <html>
457 <!-- Note: this is only displayed when the standby
458 does not know an active URI to redirect to, otherwise
459 a simple redirect is returned instead -->
460 <head>
461 <title>Ceph</title>
462 <meta http-equiv="refresh" content="{delay}">
463 </head>
464 <body>
465 No active ceph-mgr instance is currently running
466 the dashboard. A failover may be in progress.
467 Retrying in {delay} seconds...
468 </body>
469 </html>
470 """
471 return template.format(delay=5)
472 else:
473 status = module.get_module_option('standby_error_status_code', 500)
474 raise cherrypy.HTTPError(status, message="Keep on looking")
475
476 cherrypy.tree.mount(Root(), "{}/".format(self.url_prefix), {})
477 self.log.info("Starting engine...")
478 cherrypy.engine.start()
479 self.log.info("Engine started...")
480 # Wait for shutdown event
481 self.shutdown_event.wait()
482 self.shutdown_event.clear()
483 cherrypy.engine.stop()
484 self.log.info("Engine stopped.")
485
486 def shutdown(self):
487 CherryPyConfig.shutdown(self)
488
489 self.log.info("Stopping engine...")
490 self.shutdown_event.set()
491 self.log.info("Stopped engine...")