]> git.proxmox.com Git - ceph.git/blame - ceph/src/pybind/mgr/dashboard/module.py
update ceph source to reef 18.2.1
[ceph.git] / ceph / src / pybind / mgr / dashboard / module.py
CommitLineData
11fdf7f2 1# -*- coding: utf-8 -*-
31f18b77 2"""
11fdf7f2 3ceph dashboard mgr plugin (based on CherryPy)
31f18b77 4"""
31f18b77 5import collections
11fdf7f2 6import errno
9f95a23c 7import logging
31f18b77 8import os
39ae355f 9import socket
f91f0fd5
TL
10import ssl
11import sys
11fdf7f2
TL
12import tempfile
13import threading
14import time
18d92ca7 15from typing import TYPE_CHECKING, Optional
39ae355f 16from urllib.parse import urlparse
f6b5b4d7 17
18d92ca7
TL
18if TYPE_CHECKING:
19 if sys.version_info >= (3, 8):
20 from typing import Literal
21 else:
22 from typing_extensions import Literal
23
33c7a0ef
TL
24from mgr_module import CLIReadCommand, CLIWriteCommand, HandleCommandResult, \
25 MgrModule, MgrStandbyModule, NotifyType, Option, _get_localized_key
522d829b
TL
26from mgr_util import ServerConfigException, build_url, \
27 create_self_signed_cert, get_default_addr, verify_tls_files
f67539c2
TL
28
29from . import mgr
a4b75251 30from .controllers import Router, json_error_page
f67539c2
TL
31from .grafana import push_local_dashboards
32from .services.auth import AuthManager, AuthManagerTool, JwtManager
33from .services.exception import dashboard_exception_handler
522d829b 34from .services.rgw_client import configure_rgw_credentials
f67539c2 35from .services.sso import SSO_COMMANDS, handle_sso_command
aee94f69 36from .settings import Settings, handle_option_command, options_command_list, options_schema_list
f67539c2
TL
37from .tools import NotificationQueue, RequestLoggingTool, TaskManager, \
38 prepare_url_prefix, str_to_bool
11fdf7f2
TL
39
40try:
41 import cherrypy
42 from cherrypy._cptools import HandlerWrapperTool
43except ImportError:
44 # To be picked up and reported by .can_run()
45 cherrypy = None
46
47from .services.sso import load_sso_db
48
11fdf7f2 49if cherrypy is not None:
92f5a8d4
TL
50 from .cherrypy_backports import patch_cherrypy
51 patch_cherrypy(cherrypy.__version__)
a8e16298 52
11fdf7f2 53# pylint: disable=wrong-import-position
522d829b 54from .plugins import PLUGIN_MANAGER, debug, feature_toggles, motd # isort:skip # noqa E501 # pylint: disable=unused-import
92f5a8d4
TL
55
56PLUGIN_MANAGER.hook.init()
3efd9988
FG
57
58
11fdf7f2
TL
59# cherrypy likes to sys.exit on error. don't let it take us down too!
60# pylint: disable=W0613
61def os_exit_noop(*args):
62 pass
3efd9988 63
b32b8144 64
11fdf7f2
TL
65# pylint: disable=W0212
66os._exit = os_exit_noop
b32b8144 67
3efd9988 68
9f95a23c
TL
69logger = logging.getLogger(__name__)
70
71
11fdf7f2
TL
72class CherryPyConfig(object):
73 """
74 Class for common server configuration done by both active and
75 standby module, especially setting up SSL.
76 """
81eedcae 77
11fdf7f2
TL
78 def __init__(self):
79 self._stopping = threading.Event()
80 self._url_prefix = ""
3efd9988 81
11fdf7f2
TL
82 self.cert_tmp = None
83 self.pkey_tmp = None
3efd9988
FG
84
85 def shutdown(self):
11fdf7f2 86 self._stopping.set()
3efd9988 87
31f18b77 88 @property
11fdf7f2
TL
89 def url_prefix(self):
90 return self._url_prefix
31f18b77 91
92f5a8d4
TL
92 @staticmethod
93 def update_cherrypy_config(config):
94 PLUGIN_MANAGER.hook.configure_cherrypy(config=config)
95 cherrypy.config.update(config)
96
81eedcae 97 # pylint: disable=too-many-branches
11fdf7f2 98 def _configure(self):
31f18b77 99 """
11fdf7f2
TL
100 Configure CherryPy and initialize self.url_prefix
101
102 :returns our URI
31f18b77 103 """
9f95a23c 104 server_addr = self.get_localized_module_option( # type: ignore
494da23a 105 'server_addr', get_default_addr())
f91f0fd5
TL
106 use_ssl = self.get_localized_module_option('ssl', True) # type: ignore
107 if not use_ssl:
9f95a23c 108 server_port = self.get_localized_module_option('server_port', 8080) # type: ignore
31f18b77 109 else:
9f95a23c 110 server_port = self.get_localized_module_option('ssl_server_port', 8443) # type: ignore
31f18b77 111
11fdf7f2
TL
112 if server_addr is None:
113 raise ServerConfigException(
114 'no server_addr configured; '
115 'try "ceph config set mgr mgr/{}/{}/server_addr <ip>"'
9f95a23c 116 .format(self.module_name, self.get_mgr_id())) # type: ignore
f91f0fd5 117 self.log.info('server: ssl=%s host=%s port=%d', 'yes' if use_ssl else 'no', # type: ignore
11fdf7f2
TL
118 server_addr, server_port)
119
120 # Initialize custom handlers.
121 cherrypy.tools.authenticate = AuthManagerTool()
39ae355f 122 self.configure_cors()
92f5a8d4 123 cherrypy.tools.plugin_hooks_filter_request = cherrypy.Tool(
11fdf7f2
TL
124 'before_handler',
125 lambda: PLUGIN_MANAGER.hook.filter_request_before_handler(request=cherrypy.request),
92f5a8d4 126 priority=1)
11fdf7f2
TL
127 cherrypy.tools.request_logging = RequestLoggingTool()
128 cherrypy.tools.dashboard_exception_handler = HandlerWrapperTool(dashboard_exception_handler,
129 priority=31)
130
9f95a23c
TL
131 cherrypy.log.access_log.propagate = False
132 cherrypy.log.error_log.propagate = False
133
11fdf7f2
TL
134 # Apply the 'global' CherryPy configuration.
135 config = {
136 'engine.autoreload.on': False,
137 'server.socket_host': server_addr,
138 'server.socket_port': int(server_port),
139 'error_page.default': json_error_page,
140 'tools.request_logging.on': True,
141 'tools.gzip.on': True,
142 'tools.gzip.mime_types': [
143 # text/html and text/plain are the default types to compress
144 'text/html', 'text/plain',
145 # We also want JSON and JavaScript to be compressed
146 'application/json',
f67539c2 147 'application/*+json',
11fdf7f2
TL
148 'application/javascript',
149 ],
150 'tools.json_in.on': True,
f91f0fd5 151 'tools.json_in.force': True,
92f5a8d4 152 'tools.plugin_hooks_filter_request.on': True,
31f18b77
FG
153 }
154
f91f0fd5 155 if use_ssl:
11fdf7f2 156 # SSL initialization
f67539c2 157 cert = self.get_localized_store("crt") # type: ignore
11fdf7f2
TL
158 if cert is not None:
159 self.cert_tmp = tempfile.NamedTemporaryFile()
160 self.cert_tmp.write(cert.encode('utf-8'))
161 self.cert_tmp.flush() # cert_tmp must not be gc'ed
162 cert_fname = self.cert_tmp.name
b32b8144 163 else:
9f95a23c 164 cert_fname = self.get_localized_module_option('crt_file') # type: ignore
11fdf7f2 165
f67539c2 166 pkey = self.get_localized_store("key") # type: ignore
11fdf7f2
TL
167 if pkey is not None:
168 self.pkey_tmp = tempfile.NamedTemporaryFile()
169 self.pkey_tmp.write(pkey.encode('utf-8'))
170 self.pkey_tmp.flush() # pkey_tmp must not be gc'ed
171 pkey_fname = self.pkey_tmp.name
172 else:
9f95a23c 173 pkey_fname = self.get_localized_module_option('key_file') # type: ignore
c07f9fc5 174
eafe8130 175 verify_tls_files(cert_fname, pkey_fname)
81eedcae 176
f91f0fd5
TL
177 # Create custom SSL context to disable TLS 1.0 and 1.1.
178 context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
179 context.load_cert_chain(cert_fname, pkey_fname)
180 if sys.version_info >= (3, 7):
aee94f69
TL
181 if Settings.UNSAFE_TLS_v1_2:
182 context.minimum_version = ssl.TLSVersion.TLSv1_2
183 else:
184 context.minimum_version = ssl.TLSVersion.TLSv1_3
f91f0fd5 185 else:
aee94f69
TL
186 if Settings.UNSAFE_TLS_v1_2:
187 context.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1
188 else:
189 context.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 | ssl.OP_NO_TLSv1_2
f91f0fd5 190
11fdf7f2
TL
191 config['server.ssl_module'] = 'builtin'
192 config['server.ssl_certificate'] = cert_fname
193 config['server.ssl_private_key'] = pkey_fname
f91f0fd5 194 config['server.ssl_context'] = context
31f18b77 195
92f5a8d4 196 self.update_cherrypy_config(config)
31f18b77 197
9f95a23c
TL
198 self._url_prefix = prepare_url_prefix(self.get_module_option( # type: ignore
199 'url_prefix', default=''))
31f18b77 200
a4b75251
TL
201 if server_addr in ['::', '0.0.0.0']:
202 server_addr = self.get_mgr_ip() # type: ignore
522d829b
TL
203 base_url = build_url(
204 scheme='https' if use_ssl else 'http',
205 host=server_addr,
206 port=server_port,
11fdf7f2 207 )
522d829b 208 uri = f'{base_url}{self.url_prefix}/'
11fdf7f2 209 return uri
1adf2230 210
11fdf7f2
TL
211 def await_configuration(self):
212 """
213 Block until configuration is ready (i.e. all needed keys are set)
214 or self._stopping is set.
31f18b77 215
11fdf7f2
TL
216 :returns URI of configured webserver
217 """
218 while not self._stopping.is_set():
219 try:
220 uri = self._configure()
221 except ServerConfigException as e:
9f95a23c
TL
222 self.log.info( # type: ignore
223 "Config not ready to serve, waiting: {0}".format(e)
224 )
11fdf7f2
TL
225 # Poll until a non-errored config is present
226 self._stopping.wait(5)
227 else:
9f95a23c 228 self.log.info("Configured CherryPy, starting engine...") # type: ignore
11fdf7f2 229 return uri
31f18b77 230
39ae355f
TL
231 def configure_cors(self):
232 """
233 Allow CORS requests if the cross_origin_url option is set.
234 """
235 cross_origin_url = mgr.get_localized_module_option('cross_origin_url', '')
236 if cross_origin_url:
237 cherrypy.tools.CORS = cherrypy.Tool('before_handler', self.cors_tool)
238 config = {
239 'tools.CORS.on': True,
240 }
241 self.update_cherrypy_config(config)
242
243 def cors_tool(self):
244 '''
245 Handle both simple and complex CORS requests
246
247 Add CORS headers to each response. If the request is a CORS preflight
248 request swap out the default handler with a simple, single-purpose handler
249 that verifies the request and provides a valid CORS response.
250 '''
251 req_head = cherrypy.request.headers
252 resp_head = cherrypy.response.headers
253
254 # Always set response headers necessary for 'simple' CORS.
255 req_header_cross_origin_url = req_head.get('Access-Control-Allow-Origin')
256 cross_origin_urls = mgr.get_localized_module_option('cross_origin_url', '')
257 cross_origin_url_list = [url.strip() for url in cross_origin_urls.split(',')]
258 if req_header_cross_origin_url in cross_origin_url_list:
259 resp_head['Access-Control-Allow-Origin'] = req_header_cross_origin_url
260 resp_head['Access-Control-Expose-Headers'] = 'GET, POST'
261 resp_head['Access-Control-Allow-Credentials'] = 'true'
262
263 # Non-simple CORS preflight request; short-circuit the normal handler.
264 if cherrypy.request.method == 'OPTIONS':
265 req_header_origin_url = req_head.get('Origin')
266 if req_header_origin_url in cross_origin_url_list:
267 resp_head['Access-Control-Allow-Origin'] = req_header_origin_url
268 ac_method = req_head.get('Access-Control-Request-Method', None)
269
aee94f69 270 allowed_methods = ['GET', 'POST', 'PUT']
39ae355f
TL
271 allowed_headers = [
272 'Content-Type',
273 'Authorization',
274 'Accept',
275 'Access-Control-Allow-Origin'
276 ]
277
278 if ac_method and ac_method in allowed_methods:
279 resp_head['Access-Control-Allow-Methods'] = ', '.join(allowed_methods)
280 resp_head['Access-Control-Allow-Headers'] = ', '.join(allowed_headers)
281
282 resp_head['Connection'] = 'keep-alive'
283 resp_head['Access-Control-Max-Age'] = '3600'
284
285 # CORS requests should short-circuit the other tools.
286 cherrypy.response.body = ''.encode('utf8')
287 cherrypy.response.status = 200
288 cherrypy.serving.request.handler = None
289
290 # Needed to avoid the auth_tool check.
291 if cherrypy.request.config.get('tools.sessions.on', False):
292 cherrypy.session['token'] = True
293 return True
294
31f18b77 295
18d92ca7
TL
296if TYPE_CHECKING:
297 SslConfigKey = Literal['crt', 'key']
298
299
11fdf7f2
TL
300class Module(MgrModule, CherryPyConfig):
301 """
302 dashboard module entrypoint
303 """
31f18b77 304
11fdf7f2
TL
305 COMMANDS = [
306 {
307 'cmd': 'dashboard set-jwt-token-ttl '
308 'name=seconds,type=CephInt',
309 'desc': 'Set the JWT token TTL in seconds',
310 'perm': 'w'
311 },
312 {
313 'cmd': 'dashboard get-jwt-token-ttl',
314 'desc': 'Get the JWT token TTL in seconds',
315 'perm': 'r'
316 },
317 {
318 "cmd": "dashboard create-self-signed-cert",
319 "desc": "Create self signed certificate",
320 "perm": "w"
321 },
81eedcae
TL
322 {
323 "cmd": "dashboard grafana dashboards update",
324 "desc": "Push dashboards to Grafana",
325 "perm": "w",
326 },
11fdf7f2
TL
327 ]
328 COMMANDS.extend(options_command_list())
329 COMMANDS.extend(SSO_COMMANDS)
330 PLUGIN_MANAGER.hook.register_commands()
331
332 MODULE_OPTIONS = [
494da23a 333 Option(name='server_addr', type='str', default=get_default_addr()),
11fdf7f2
TL
334 Option(name='server_port', type='int', default=8080),
335 Option(name='ssl_server_port', type='int', default=8443),
336 Option(name='jwt_token_ttl', type='int', default=28800),
11fdf7f2 337 Option(name='url_prefix', type='str', default=''),
11fdf7f2
TL
338 Option(name='key_file', type='str', default=''),
339 Option(name='crt_file', type='str', default=''),
eafe8130
TL
340 Option(name='ssl', type='bool', default=True),
341 Option(name='standby_behaviour', type='str', default='redirect',
342 enum_allowed=['redirect', 'error']),
343 Option(name='standby_error_status_code', type='int', default=500,
39ae355f
TL
344 min=400, max=599),
345 Option(name='redirect_resolve_ip_addr', type='bool', default=False),
346 Option(name='cross_origin_url', type='str', default=''),
11fdf7f2
TL
347 ]
348 MODULE_OPTIONS.extend(options_schema_list())
349 for options in PLUGIN_MANAGER.hook.get_options() or []:
350 MODULE_OPTIONS.extend(options)
351
20effc67
TL
352 NOTIFY_TYPES = [NotifyType.clog]
353
11fdf7f2 354 __pool_stats = collections.defaultdict(lambda: collections.defaultdict(
9f95a23c 355 lambda: collections.deque(maxlen=10))) # type: dict
31f18b77 356
11fdf7f2
TL
357 def __init__(self, *args, **kwargs):
358 super(Module, self).__init__(*args, **kwargs)
359 CherryPyConfig.__init__(self)
31f18b77 360
11fdf7f2 361 mgr.init(self)
31f18b77 362
11fdf7f2
TL
363 self._stopping = threading.Event()
364 self.shutdown_event = threading.Event()
11fdf7f2
TL
365 self.ACCESS_CTRL_DB = None
366 self.SSO_DB = None
adb31ebb 367 self.health_checks = {}
31f18b77 368
11fdf7f2
TL
369 @classmethod
370 def can_run(cls):
371 if cherrypy is None:
372 return False, "Missing dependency: cherrypy"
31f18b77 373
11fdf7f2 374 if not os.path.exists(cls.get_frontend_path()):
f67539c2
TL
375 return False, ("Frontend assets not found at '{}': incomplete build?"
376 .format(cls.get_frontend_path()))
31f18b77 377
11fdf7f2 378 return True, ""
31f18b77 379
11fdf7f2
TL
380 @classmethod
381 def get_frontend_path(cls):
382 current_dir = os.path.dirname(os.path.abspath(__file__))
20effc67
TL
383 path = os.path.join(current_dir, 'frontend/dist')
384 if os.path.exists(path):
385 return path
386 else:
387 path = os.path.join(current_dir,
388 '../../../../build',
389 'src/pybind/mgr/dashboard',
390 'frontend/dist')
391 return os.path.abspath(path)
c07f9fc5 392
11fdf7f2 393 def serve(self):
f6b5b4d7
TL
394
395 if 'COVERAGE_ENABLED' in os.environ:
396 import coverage
397 __cov = coverage.Coverage(config_file="{}/.coveragerc"
398 .format(os.path.dirname(__file__)),
399 data_suffix=True)
400 __cov.start()
401 cherrypy.engine.subscribe('after_request', __cov.save)
402 cherrypy.engine.subscribe('stop', __cov.stop)
403
11fdf7f2
TL
404 AuthManager.initialize()
405 load_sso_db()
31f18b77 406
11fdf7f2
TL
407 uri = self.await_configuration()
408 if uri is None:
409 # We were shut down while waiting
410 return
3efd9988
FG
411
412 # Publish the URI that others may use to access the service we're
413 # about to start serving
11fdf7f2 414 self.set_uri(uri)
c07f9fc5 415
a4b75251 416 mapper, parent_urls = Router.generate_routes(self.url_prefix)
c07f9fc5 417
eafe8130 418 config = {}
11fdf7f2
TL
419 for purl in parent_urls:
420 config[purl] = {
421 'request.dispatch': mapper
422 }
92f5a8d4 423
11fdf7f2 424 cherrypy.tree.mount(None, config=config)
c07f9fc5 425
11fdf7f2 426 PLUGIN_MANAGER.hook.setup()
c07f9fc5 427
11fdf7f2
TL
428 cherrypy.engine.start()
429 NotificationQueue.start_queue()
430 TaskManager.init()
431 logger.info('Engine started.')
81eedcae
TL
432 update_dashboards = str_to_bool(
433 self.get_module_option('GRAFANA_UPDATE_DASHBOARDS', 'False'))
434 if update_dashboards:
435 logger.info('Starting Grafana dashboard task')
436 TaskManager.run(
437 'grafana/dashboards/update',
438 {},
439 push_local_dashboards,
440 kwargs=dict(tries=10, sleep=60),
441 )
11fdf7f2
TL
442 # wait for the shutdown event
443 self.shutdown_event.wait()
444 self.shutdown_event.clear()
445 NotificationQueue.stop()
446 cherrypy.engine.stop()
447 logger.info('Engine stopped')
c07f9fc5 448
11fdf7f2
TL
449 def shutdown(self):
450 super(Module, self).shutdown()
451 CherryPyConfig.shutdown(self)
452 logger.info('Stopping engine...')
453 self.shutdown_event.set()
454
18d92ca7
TL
455 def _set_ssl_item(self, item_label: str, item_key: 'SslConfigKey' = 'crt',
456 mgr_id: Optional[str] = None, inbuf: Optional[str] = None):
494da23a 457 if inbuf is None:
18d92ca7
TL
458 return -errno.EINVAL, '', f'Please specify the {item_label} with "-i" option'
459
494da23a 460 if mgr_id is not None:
18d92ca7 461 self.set_store(_get_localized_key(mgr_id, item_key), inbuf)
494da23a 462 else:
18d92ca7
TL
463 self.set_store(item_key, inbuf)
464 return 0, f'SSL {item_label} updated', ''
465
466 @CLIWriteCommand("dashboard set-ssl-certificate")
467 def set_ssl_certificate(self, mgr_id: Optional[str] = None, inbuf: Optional[str] = None):
468 return self._set_ssl_item('certificate', 'crt', mgr_id, inbuf)
494da23a 469
f67539c2 470 @CLIWriteCommand("dashboard set-ssl-certificate-key")
18d92ca7
TL
471 def set_ssl_certificate_key(self, mgr_id: Optional[str] = None, inbuf: Optional[str] = None):
472 return self._set_ssl_item('certificate key', 'key', mgr_id, inbuf)
473
474 @CLIWriteCommand("dashboard create-self-signed-cert")
475 def set_mgr_created_self_signed_cert(self):
476 cert, pkey = create_self_signed_cert('IT', 'ceph-dashboard')
477 result = HandleCommandResult(*self.set_ssl_certificate(inbuf=cert))
478 if result.retval != 0:
479 return result
480
481 result = HandleCommandResult(*self.set_ssl_certificate_key(inbuf=pkey))
482 if result.retval != 0:
483 return result
484 return 0, 'Self-signed certificate created', ''
494da23a 485
522d829b
TL
486 @CLIWriteCommand("dashboard set-rgw-credentials")
487 def set_rgw_credentials(self):
488 try:
489 configure_rgw_credentials()
490 except Exception as error:
491 return -errno.EINVAL, '', str(error)
492
493 return 0, 'RGW credentials configured', ''
494
33c7a0ef 495 @CLIWriteCommand("dashboard set-login-banner")
2a845540
TL
496 def set_login_banner(self, inbuf: str):
497 '''
498 Set the custom login banner read from -i <file>
499 '''
33c7a0ef
TL
500 item_label = 'login banner file'
501 if inbuf is None:
502 return HandleCommandResult(
503 -errno.EINVAL,
504 stderr=f'Please specify the {item_label} with "-i" option'
505 )
2a845540 506 mgr.set_store('custom_login_banner', inbuf)
33c7a0ef
TL
507 return HandleCommandResult(stdout=f'{item_label} added')
508
509 @CLIReadCommand("dashboard get-login-banner")
510 def get_login_banner(self):
2a845540
TL
511 '''
512 Get the custom login banner text
513 '''
514 banner_text = mgr.get_store('custom_login_banner')
33c7a0ef
TL
515 if banner_text is None:
516 return HandleCommandResult(stdout='No login banner set')
517 else:
518 return HandleCommandResult(stdout=banner_text)
519
520 @CLIWriteCommand("dashboard unset-login-banner")
521 def unset_login_banner(self):
2a845540
TL
522 '''
523 Unset the custom login banner
524 '''
525 mgr.set_store('custom_login_banner', None)
33c7a0ef
TL
526 return HandleCommandResult(stdout='Login banner removed')
527
11fdf7f2
TL
528 def handle_command(self, inbuf, cmd):
529 # pylint: disable=too-many-return-statements
cd265ab1 530 res = handle_option_command(cmd, inbuf)
11fdf7f2
TL
531 if res[0] != -errno.ENOSYS:
532 return res
533 res = handle_sso_command(cmd)
534 if res[0] != -errno.ENOSYS:
535 return res
536 if cmd['prefix'] == 'dashboard set-jwt-token-ttl':
537 self.set_module_option('jwt_token_ttl', str(cmd['seconds']))
538 return 0, 'JWT token TTL updated', ''
539 if cmd['prefix'] == 'dashboard get-jwt-token-ttl':
540 ttl = self.get_module_option('jwt_token_ttl', JwtManager.JWT_TOKEN_TTL)
541 return 0, str(ttl), ''
81eedcae
TL
542 if cmd['prefix'] == 'dashboard grafana dashboards update':
543 push_local_dashboards()
544 return 0, 'Grafana dashboards updated', ''
11fdf7f2
TL
545
546 return (-errno.EINVAL, '', 'Command not found \'{0}\''
547 .format(cmd['prefix']))
548
20effc67
TL
549 def notify(self, notify_type: NotifyType, notify_id):
550 NotificationQueue.new_notification(str(notify_type), notify_id)
11fdf7f2
TL
551
552 def get_updated_pool_stats(self):
553 df = self.get('df')
554 pool_stats = {p['id']: p['stats'] for p in df['pools']}
555 now = time.time()
556 for pool_id, stats in pool_stats.items():
557 for stat_name, stat_val in stats.items():
558 self.__pool_stats[pool_id][stat_name].append((now, stat_val))
c07f9fc5 559
11fdf7f2 560 return self.__pool_stats
c07f9fc5 561
adb31ebb
TL
562 def config_notify(self):
563 """
564 This method is called whenever one of our config options is changed.
565 """
566 PLUGIN_MANAGER.hook.config_notify()
567
568 def refresh_health_checks(self):
569 self.set_health_checks(self.health_checks)
570
c07f9fc5 571
11fdf7f2
TL
572class StandbyModule(MgrStandbyModule, CherryPyConfig):
573 def __init__(self, *args, **kwargs):
574 super(StandbyModule, self).__init__(*args, **kwargs)
575 CherryPyConfig.__init__(self)
576 self.shutdown_event = threading.Event()
c07f9fc5 577
11fdf7f2
TL
578 # We can set the global mgr instance to ourselves even though
579 # we're just a standby, because it's enough for logging.
580 mgr.init(self)
c07f9fc5 581
11fdf7f2
TL
582 def serve(self):
583 uri = self.await_configuration()
584 if uri is None:
585 # We were shut down while waiting
586 return
c07f9fc5 587
11fdf7f2 588 module = self
c07f9fc5 589
11fdf7f2 590 class Root(object):
c07f9fc5 591 @cherrypy.expose
92f5a8d4 592 def default(self, *args, **kwargs):
eafe8130
TL
593 if module.get_module_option('standby_behaviour', 'redirect') == 'redirect':
594 active_uri = module.get_active_uri()
2a845540
TL
595
596 if cherrypy.request.path_info.startswith('/api/prometheus_receiver'):
597 module.log.debug("Suppressed redirecting alert to active '%s'",
598 active_uri)
599 cherrypy.response.status = 204
600 return None
601
eafe8130 602 if active_uri:
39ae355f
TL
603 if module.get_module_option('redirect_resolve_ip_addr'):
604 p_result = urlparse(active_uri)
605 hostname = str(p_result.hostname)
606 fqdn_netloc = p_result.netloc.replace(
607 hostname, socket.getfqdn(hostname))
608 active_uri = p_result._replace(netloc=fqdn_netloc).geturl()
609
eafe8130
TL
610 module.log.info("Redirecting to active '%s'", active_uri)
611 raise cherrypy.HTTPRedirect(active_uri)
612 else:
613 template = """
614 <html>
615 <!-- Note: this is only displayed when the standby
616 does not know an active URI to redirect to, otherwise
617 a simple redirect is returned instead -->
618 <head>
619 <title>Ceph</title>
620 <meta http-equiv="refresh" content="{delay}">
621 </head>
622 <body>
623 No active ceph-mgr instance is currently running
624 the dashboard. A failover may be in progress.
625 Retrying in {delay} seconds...
626 </body>
627 </html>
628 """
629 return template.format(delay=5)
11fdf7f2 630 else:
eafe8130
TL
631 status = module.get_module_option('standby_error_status_code', 500)
632 raise cherrypy.HTTPError(status, message="Keep on looking")
11fdf7f2
TL
633
634 cherrypy.tree.mount(Root(), "{}/".format(self.url_prefix), {})
635 self.log.info("Starting engine...")
31f18b77 636 cherrypy.engine.start()
11fdf7f2
TL
637 self.log.info("Engine started...")
638 # Wait for shutdown event
639 self.shutdown_event.wait()
640 self.shutdown_event.clear()
641 cherrypy.engine.stop()
642 self.log.info("Engine stopped.")
643
644 def shutdown(self):
645 CherryPyConfig.shutdown(self)
646
647 self.log.info("Stopping engine...")
648 self.shutdown_event.set()
649 self.log.info("Stopped engine...")