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