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