def serve(self): server_addr = self.get_localized_module_option( 'server_addr', get_default_addr()) server_port = self.get_localized_module_option( 'server_port', DEFAULT_PORT) self.log.info("server_addr: %s server_port: %s" % (server_addr, server_port)) cherrypy.config.update({ 'server.socket_host': server_addr, 'server.socket_port': int(server_port), 'engine.autoreload.on': False }) module = self class Root(object): @cherrypy.expose def index(self): active_uri = module.get_active_uri() return '''<!DOCTYPE html> <html> <head><title>Ceph Exporter</title></head> <body> <h1>Ceph Exporter</h1> <p><a href='{}metrics'>Metrics</a></p> </body> </html>'''.format(active_uri) @cherrypy.expose def metrics(self): cherrypy.response.headers['Content-Type'] = 'text/plain' return '' cherrypy.tree.mount(Root(), '/', {}) self.log.info('Starting engine...') cherrypy.engine.start() self.log.info('Engine started.') # Wait for shutdown event self.shutdown_event.wait() self.shutdown_event.clear() cherrypy.engine.stop() self.log.info('Engine stopped.')
def serve(self): class Root(object): # collapse everything to '/' def _cp_dispatch(self, vpath): cherrypy.request.path = '' return self @cherrypy.expose def index(self): return '''<!DOCTYPE html> <html> <head><title>Ceph Exporter</title></head> <body> <h1>Ceph Exporter</h1> <p><a href='/metrics'>Metrics</a></p> </body> </html>''' @cherrypy.expose def metrics(self): # Lock the function execution assert isinstance(_global_instance, Module) with _global_instance.collect_lock: return self._metrics(_global_instance) @staticmethod def _metrics(instance): # type: (Module) -> Any # Return cached data if available if not instance.collect_cache: raise cherrypy.HTTPError(503, 'No cached data available yet') def respond(): assert isinstance(instance, Module) cherrypy.response.headers['Content-Type'] = 'text/plain' return instance.collect_cache if instance.collect_time < instance.scrape_interval: # Respond if cache isn't stale return respond() if instance.stale_cache_strategy == instance.STALE_CACHE_RETURN: # Respond even if cache is stale instance.log.info( 'Gathering data took {:.2f} seconds, metrics are stale for {:.2f} seconds, ' 'returning metrics from stale cache.'.format( instance.collect_time, instance.collect_time - instance.scrape_interval)) return respond() if instance.stale_cache_strategy == instance.STALE_CACHE_FAIL: # Fail if cache is stale msg = ( 'Gathering data took {:.2f} seconds, metrics are stale for {:.2f} seconds, ' 'returning "service unavailable".'.format( instance.collect_time, instance.collect_time - instance.scrape_interval, )) instance.log.error(msg) raise cherrypy.HTTPError(503, msg) # Make the cache timeout for collecting configurable self.scrape_interval = float( self.get_localized_module_option('scrape_interval', 15.0)) self.stale_cache_strategy = self.get_localized_module_option( 'stale_cache_strategy', 'log') if self.stale_cache_strategy not in [ self.STALE_CACHE_FAIL, self.STALE_CACHE_RETURN ]: self.stale_cache_strategy = self.STALE_CACHE_FAIL server_addr = self.get_localized_module_option('server_addr', get_default_addr()) server_port = self.get_localized_module_option('server_port', DEFAULT_PORT) self.log.info("server_addr: %s server_port: %s" % (server_addr, server_port)) # Publish the URI that others may use to access the service we're # about to start serving self.set_uri('http://{0}:{1}/'.format( socket.getfqdn() if server_addr in ['::', '0.0.0.0'] else server_addr, server_port)) cherrypy.config.update({ 'server.socket_host': server_addr, 'server.socket_port': int(server_port), 'engine.autoreload.on': False }) cherrypy.tree.mount(Root(), "/") self.log.info('Starting engine...') cherrypy.engine.start() self.log.info('Engine started.') # wait for the shutdown event self.shutdown_event.wait() self.shutdown_event.clear() cherrypy.engine.stop() self.log.info('Engine stopped.') self.shutdown_rbd_stats()
def serve(self): class Root(object): # collapse everything to '/' def _cp_dispatch(self, vpath): cherrypy.request.path = '' return self @cherrypy.expose def index(self): return '''<!DOCTYPE html> <html> <head><title>Ceph Exporter</title></head> <body> <h1>Ceph Exporter</h1> <p><a href='/metrics'>Metrics</a></p> </body> </html>''' @cherrypy.expose def metrics(self): instance = global_instance() # Lock the function execution try: instance.collect_lock.acquire() return self._metrics(instance) finally: instance.collect_lock.release() @staticmethod def _metrics(instance): # Return cached data if available and collected before the # cache times out if instance.collect_cache and time.time() - instance.collect_time < instance.collect_timeout: cherrypy.response.headers['Content-Type'] = 'text/plain' return instance.collect_cache if instance.have_mon_connection(): instance.collect_cache = None instance.collect_time = time.time() instance.collect_cache = instance.collect() cherrypy.response.headers['Content-Type'] = 'text/plain' return instance.collect_cache else: raise cherrypy.HTTPError(503, 'No MON connection') # Make the cache timeout for collecting configurable self.collect_timeout = float(self.get_localized_module_option( 'scrape_interval', 5.0)) server_addr = self.get_localized_module_option( 'server_addr', get_default_addr()) server_port = self.get_localized_module_option( 'server_port', DEFAULT_PORT) self.log.info( "server_addr: %s server_port: %s" % (server_addr, server_port) ) # Publish the URI that others may use to access the service we're # about to start serving self.set_uri('http://{0}:{1}/'.format( socket.getfqdn() if server_addr in ['::', '0.0.0.0'] else server_addr, server_port )) cherrypy.config.update({ 'server.socket_host': server_addr, 'server.socket_port': int(server_port), 'engine.autoreload.on': False }) cherrypy.tree.mount(Root(), "/") self.log.info('Starting engine...') cherrypy.engine.start() self.log.info('Engine started.') # wait for the shutdown event self.shutdown_event.wait() self.shutdown_event.clear() cherrypy.engine.stop() self.log.info('Engine stopped.') self.shutdown_rbd_stats()
def _configure(self): """ Configure CherryPy and initialize self.url_prefix :returns our URI """ server_addr = self.get_localized_module_option( # type: ignore 'server_addr', get_default_addr()) ssl = self.get_localized_module_option('ssl', True) # type: ignore if not ssl: server_port = self.get_localized_module_option( 'server_port', 8080) # type: ignore else: server_port = self.get_localized_module_option( 'ssl_server_port', 8443) # type: ignore if server_addr is None: raise ServerConfigException( 'no server_addr configured; ' 'try "ceph config set mgr mgr/{}/{}/server_addr <ip>"'.format( self.module_name, self.get_mgr_id())) # type: ignore self.log.info( 'server: ssl=%s host=%s port=%d', 'yes' if ssl else 'no', # type: ignore server_addr, server_port) # Initialize custom handlers. cherrypy.tools.authenticate = AuthManagerTool() cherrypy.tools.plugin_hooks_filter_request = cherrypy.Tool( 'before_handler', lambda: PLUGIN_MANAGER.hook.filter_request_before_handler( request=cherrypy.request), priority=1) cherrypy.tools.request_logging = RequestLoggingTool() cherrypy.tools.dashboard_exception_handler = HandlerWrapperTool( dashboard_exception_handler, priority=31) cherrypy.log.access_log.propagate = False cherrypy.log.error_log.propagate = False # Apply the 'global' CherryPy configuration. config = { 'engine.autoreload.on': False, 'server.socket_host': server_addr, 'server.socket_port': int(server_port), 'error_page.default': json_error_page, 'tools.request_logging.on': True, 'tools.gzip.on': True, 'tools.gzip.mime_types': [ # text/html and text/plain are the default types to compress 'text/html', 'text/plain', # We also want JSON and JavaScript to be compressed 'application/json', 'application/javascript', ], 'tools.json_in.on': True, 'tools.json_in.force': False, 'tools.plugin_hooks_filter_request.on': True, } if ssl: # SSL initialization cert = self.get_store("crt") # type: ignore if cert is not None: self.cert_tmp = tempfile.NamedTemporaryFile() self.cert_tmp.write(cert.encode('utf-8')) self.cert_tmp.flush() # cert_tmp must not be gc'ed cert_fname = self.cert_tmp.name else: cert_fname = self.get_localized_module_option( 'crt_file') # type: ignore pkey = self.get_store("key") # type: ignore if pkey is not None: self.pkey_tmp = tempfile.NamedTemporaryFile() self.pkey_tmp.write(pkey.encode('utf-8')) self.pkey_tmp.flush() # pkey_tmp must not be gc'ed pkey_fname = self.pkey_tmp.name else: pkey_fname = self.get_localized_module_option( 'key_file') # type: ignore verify_tls_files(cert_fname, pkey_fname) config['server.ssl_module'] = 'builtin' config['server.ssl_certificate'] = cert_fname config['server.ssl_private_key'] = pkey_fname self.update_cherrypy_config(config) self._url_prefix = prepare_url_prefix( self.get_module_option( # type: ignore 'url_prefix', default='')) uri = "{0}://{1}:{2}{3}/".format( 'https' if ssl else 'http', socket.getfqdn() if server_addr in ['::', '0.0.0.0'] else server_addr, server_port, self.url_prefix) return uri
class Module(MgrModule, CherryPyConfig): """ dashboard module entrypoint """ COMMANDS = [ { 'cmd': 'dashboard set-jwt-token-ttl ' 'name=seconds,type=CephInt', 'desc': 'Set the JWT token TTL in seconds', 'perm': 'w' }, { 'cmd': 'dashboard get-jwt-token-ttl', 'desc': 'Get the JWT token TTL in seconds', 'perm': 'r' }, { "cmd": "dashboard create-self-signed-cert", "desc": "Create self signed certificate", "perm": "w" }, { "cmd": "dashboard grafana dashboards update", "desc": "Push dashboards to Grafana", "perm": "w", }, ] COMMANDS.extend(options_command_list()) COMMANDS.extend(SSO_COMMANDS) PLUGIN_MANAGER.hook.register_commands() MODULE_OPTIONS = [ Option(name='server_addr', type='str', default=get_default_addr()), Option(name='server_port', type='int', default=8080), Option(name='ssl_server_port', type='int', default=8443), Option(name='jwt_token_ttl', type='int', default=28800), Option(name='password', type='str', default=''), Option(name='url_prefix', type='str', default=''), Option(name='username', type='str', default=''), Option(name='key_file', type='str', default=''), Option(name='crt_file', type='str', default=''), Option(name='ssl', type='bool', default=True), Option(name='standby_behaviour', type='str', default='redirect', enum_allowed=['redirect', 'error']), Option(name='standby_error_status_code', type='int', default=500, min=400, max=599) ] MODULE_OPTIONS.extend(options_schema_list()) for options in PLUGIN_MANAGER.hook.get_options() or []: MODULE_OPTIONS.extend(options) __pool_stats = collections.defaultdict(lambda: collections.defaultdict( lambda: collections.deque(maxlen=10))) # type: dict def __init__(self, *args, **kwargs): super(Module, self).__init__(*args, **kwargs) CherryPyConfig.__init__(self) mgr.init(self) self._stopping = threading.Event() self.shutdown_event = threading.Event() self.ACCESS_CTRL_DB = None self.SSO_DB = None @classmethod def can_run(cls): if cherrypy is None: return False, "Missing dependency: cherrypy" if not os.path.exists(cls.get_frontend_path()): return False, "Frontend assets not found: incomplete build?" return True, "" @classmethod def get_frontend_path(cls): current_dir = os.path.dirname(os.path.abspath(__file__)) return os.path.join(current_dir, 'frontend/dist') def serve(self): AuthManager.initialize() load_sso_db() uri = self.await_configuration() if uri is None: # We were shut down while waiting return # Publish the URI that others may use to access the service we're # about to start serving self.set_uri(uri) mapper, parent_urls = generate_routes(self.url_prefix) config = {} for purl in parent_urls: config[purl] = {'request.dispatch': mapper} cherrypy.tree.mount(None, config=config) PLUGIN_MANAGER.hook.setup() cherrypy.engine.start() NotificationQueue.start_queue() TaskManager.init() logger.info('Engine started.') update_dashboards = str_to_bool( self.get_module_option('GRAFANA_UPDATE_DASHBOARDS', 'False')) if update_dashboards: logger.info('Starting Grafana dashboard task') TaskManager.run( 'grafana/dashboards/update', {}, push_local_dashboards, kwargs=dict(tries=10, sleep=60), ) # wait for the shutdown event self.shutdown_event.wait() self.shutdown_event.clear() NotificationQueue.stop() cherrypy.engine.stop() logger.info('Engine stopped') def shutdown(self): super(Module, self).shutdown() CherryPyConfig.shutdown(self) logger.info('Stopping engine...') self.shutdown_event.set() @CLIWriteCommand("dashboard set-ssl-certificate", "name=mgr_id,type=CephString,req=false") def set_ssl_certificate(self, mgr_id=None, inbuf=None): if inbuf is None: return -errno.EINVAL, '',\ 'Please specify the certificate file with "-i" option' if mgr_id is not None: self.set_store('{}/crt'.format(mgr_id), inbuf) else: self.set_store('crt', inbuf) return 0, 'SSL certificate updated', '' @CLIWriteCommand("dashboard set-ssl-certificate-key", "name=mgr_id,type=CephString,req=false") def set_ssl_certificate_key(self, mgr_id=None, inbuf=None): if inbuf is None: return -errno.EINVAL, '',\ 'Please specify the certificate key file with "-i" option' if mgr_id is not None: self.set_store('{}/key'.format(mgr_id), inbuf) else: self.set_store('key', inbuf) return 0, 'SSL certificate key updated', '' def handle_command(self, inbuf, cmd): # pylint: disable=too-many-return-statements res = handle_option_command(cmd) if res[0] != -errno.ENOSYS: return res res = handle_sso_command(cmd) if res[0] != -errno.ENOSYS: return res if cmd['prefix'] == 'dashboard set-jwt-token-ttl': self.set_module_option('jwt_token_ttl', str(cmd['seconds'])) return 0, 'JWT token TTL updated', '' if cmd['prefix'] == 'dashboard get-jwt-token-ttl': ttl = self.get_module_option('jwt_token_ttl', JwtManager.JWT_TOKEN_TTL) return 0, str(ttl), '' if cmd['prefix'] == 'dashboard create-self-signed-cert': self.create_self_signed_cert() return 0, 'Self-signed certificate created', '' if cmd['prefix'] == 'dashboard grafana dashboards update': push_local_dashboards() return 0, 'Grafana dashboards updated', '' return (-errno.EINVAL, '', 'Command not found \'{0}\''.format(cmd['prefix'])) def create_self_signed_cert(self): cert, pkey = create_self_signed_cert('IT', 'ceph-dashboard') self.set_store('crt', cert) self.set_store('key', pkey) def notify(self, notify_type, notify_id): NotificationQueue.new_notification(notify_type, notify_id) def get_updated_pool_stats(self): df = self.get('df') pool_stats = {p['id']: p['stats'] for p in df['pools']} now = time.time() for pool_id, stats in pool_stats.items(): for stat_name, stat_val in stats.items(): self.__pool_stats[pool_id][stat_name].append((now, stat_val)) return self.__pool_stats
class Module(MgrModule, CherryPyConfig): """ dashboard module entrypoint """ COMMANDS = [ { 'cmd': 'dashboard set-jwt-token-ttl ' 'name=seconds,type=CephInt', 'desc': 'Set the JWT token TTL in seconds', 'perm': 'w' }, { 'cmd': 'dashboard get-jwt-token-ttl', 'desc': 'Get the JWT token TTL in seconds', 'perm': 'r' }, { "cmd": "dashboard create-self-signed-cert", "desc": "Create self signed certificate", "perm": "w" }, { "cmd": "dashboard grafana dashboards update", "desc": "Push dashboards to Grafana", "perm": "w", }, ] COMMANDS.extend(options_command_list()) COMMANDS.extend(SSO_COMMANDS) PLUGIN_MANAGER.hook.register_commands() MODULE_OPTIONS = [ Option(name='server_addr', type='str', default=get_default_addr()), Option(name='server_port', type='int', default=8080), Option(name='ssl_server_port', type='int', default=8443), Option(name='jwt_token_ttl', type='int', default=28800), Option(name='url_prefix', type='str', default=''), Option(name='key_file', type='str', default=''), Option(name='crt_file', type='str', default=''), Option(name='ssl', type='bool', default=True), Option(name='standby_behaviour', type='str', default='redirect', enum_allowed=['redirect', 'error']), Option(name='standby_error_status_code', type='int', default=500, min=400, max=599) ] MODULE_OPTIONS.extend(options_schema_list()) for options in PLUGIN_MANAGER.hook.get_options() or []: MODULE_OPTIONS.extend(options) __pool_stats = collections.defaultdict(lambda: collections.defaultdict( lambda: collections.deque(maxlen=10))) # type: dict def __init__(self, *args, **kwargs): super(Module, self).__init__(*args, **kwargs) CherryPyConfig.__init__(self) mgr.init(self) self._stopping = threading.Event() self.shutdown_event = threading.Event() self.ACCESS_CTRL_DB = None self.SSO_DB = None self.health_checks = {} @classmethod def can_run(cls): if cherrypy is None: return False, "Missing dependency: cherrypy" if not os.path.exists(cls.get_frontend_path()): return False, ( "Frontend assets not found at '{}': incomplete build?".format( cls.get_frontend_path())) return True, "" @classmethod def get_frontend_path(cls): current_dir = os.path.dirname(os.path.abspath(__file__)) path = os.path.join(current_dir, 'frontend/dist') if os.path.exists(path): return path else: path = os.path.join(current_dir, '../../../../build', 'src/pybind/mgr/dashboard', 'frontend/dist') return os.path.abspath(path) def serve(self): if 'COVERAGE_ENABLED' in os.environ: import coverage __cov = coverage.Coverage(config_file="{}/.coveragerc".format( os.path.dirname(__file__)), data_suffix=True) __cov.start() cherrypy.engine.subscribe('after_request', __cov.save) cherrypy.engine.subscribe('stop', __cov.stop) AuthManager.initialize() load_sso_db() uri = self.await_configuration() if uri is None: # We were shut down while waiting return # Publish the URI that others may use to access the service we're # about to start serving self.set_uri(uri) mapper, parent_urls = generate_routes(self.url_prefix) config = {} for purl in parent_urls: config[purl] = {'request.dispatch': mapper} cherrypy.tree.mount(None, config=config) PLUGIN_MANAGER.hook.setup() cherrypy.engine.start() NotificationQueue.start_queue() TaskManager.init() logger.info('Engine started.') update_dashboards = str_to_bool( self.get_module_option('GRAFANA_UPDATE_DASHBOARDS', 'False')) if update_dashboards: logger.info('Starting Grafana dashboard task') TaskManager.run( 'grafana/dashboards/update', {}, push_local_dashboards, kwargs=dict(tries=10, sleep=60), ) # wait for the shutdown event self.shutdown_event.wait() self.shutdown_event.clear() NotificationQueue.stop() cherrypy.engine.stop() logger.info('Engine stopped') def shutdown(self): super(Module, self).shutdown() CherryPyConfig.shutdown(self) logger.info('Stopping engine...') self.shutdown_event.set() def _set_ssl_item(self, item_label: str, item_key: 'SslConfigKey' = 'crt', mgr_id: Optional[str] = None, inbuf: Optional[str] = None): if inbuf is None: return -errno.EINVAL, '', f'Please specify the {item_label} with "-i" option' if mgr_id is not None: self.set_store(_get_localized_key(mgr_id, item_key), inbuf) else: self.set_store(item_key, inbuf) return 0, f'SSL {item_label} updated', '' @CLIWriteCommand("dashboard set-ssl-certificate") def set_ssl_certificate(self, mgr_id: Optional[str] = None, inbuf: Optional[str] = None): return self._set_ssl_item('certificate', 'crt', mgr_id, inbuf) @CLIWriteCommand("dashboard set-ssl-certificate-key") def set_ssl_certificate_key(self, mgr_id: Optional[str] = None, inbuf: Optional[str] = None): return self._set_ssl_item('certificate key', 'key', mgr_id, inbuf) @CLIWriteCommand("dashboard create-self-signed-cert") def set_mgr_created_self_signed_cert(self): cert, pkey = create_self_signed_cert('IT', 'ceph-dashboard') result = HandleCommandResult(*self.set_ssl_certificate(inbuf=cert)) if result.retval != 0: return result result = HandleCommandResult(*self.set_ssl_certificate_key(inbuf=pkey)) if result.retval != 0: return result return 0, 'Self-signed certificate created', '' @CLICommand("dashboard get issue") def get_issues_cli(self, issue_number: int): try: issue_number = int(issue_number) except TypeError: return -errno.EINVAL, '', f'Invalid issue number {issue_number}' tracker_client = CephTrackerClient() try: response = tracker_client.get_issues(issue_number) except RequestException as error: if error.status_code == 404: return -errno.EINVAL, '', f'Issue {issue_number} not found' else: return -errno.EREMOTEIO, '', f'Error: {str(error)}' return 0, str(response), '' @CLICommand("dashboard create issue") def report_issues_cli(self, project: str, tracker: str, subject: str, description: str): ''' Create an issue in the Ceph Issue tracker Syntax: ceph dashboard create issue <project> <bug|feature> <subject> <description> ''' try: feedback = Feedback(Feedback.Project[project].value, Feedback.TrackerType[tracker].value, subject, description) except KeyError: return -errno.EINVAL, '', 'Invalid arguments' tracker_client = CephTrackerClient() try: response = tracker_client.create_issue(feedback) except RequestException as error: if error.status_code == 401: return -errno.EINVAL, '', 'Invalid API Key' else: return -errno.EINVAL, '', f'Error: {str(error)}' except Exception: return -errno.EINVAL, '', 'Ceph Tracker API key not set' return 0, str(response), '' @CLIWriteCommand("dashboard set-rgw-credentials") def set_rgw_credentials(self): try: configure_rgw_credentials() except Exception as error: return -errno.EINVAL, '', str(error) return 0, 'RGW credentials configured', '' def handle_command(self, inbuf, cmd): # pylint: disable=too-many-return-statements res = handle_option_command(cmd, inbuf) if res[0] != -errno.ENOSYS: return res res = handle_sso_command(cmd) if res[0] != -errno.ENOSYS: return res if cmd['prefix'] == 'dashboard set-jwt-token-ttl': self.set_module_option('jwt_token_ttl', str(cmd['seconds'])) return 0, 'JWT token TTL updated', '' if cmd['prefix'] == 'dashboard get-jwt-token-ttl': ttl = self.get_module_option('jwt_token_ttl', JwtManager.JWT_TOKEN_TTL) return 0, str(ttl), '' if cmd['prefix'] == 'dashboard grafana dashboards update': push_local_dashboards() return 0, 'Grafana dashboards updated', '' return (-errno.EINVAL, '', 'Command not found \'{0}\''.format(cmd['prefix'])) def notify(self, notify_type, notify_id): NotificationQueue.new_notification(notify_type, notify_id) def get_updated_pool_stats(self): df = self.get('df') pool_stats = {p['id']: p['stats'] for p in df['pools']} now = time.time() for pool_id, stats in pool_stats.items(): for stat_name, stat_val in stats.items(): self.__pool_stats[pool_id][stat_name].append((now, stat_val)) return self.__pool_stats def config_notify(self): """ This method is called whenever one of our config options is changed. """ PLUGIN_MANAGER.hook.config_notify() def refresh_health_checks(self): self.set_health_checks(self.health_checks)
def _configure(self): """ Configure CherryPy and initialize self.url_prefix :returns our URI """ server_addr = self.get_localized_module_option('server_addr', get_default_addr()) ssl = self.get_localized_module_option('ssl', True) if not ssl: server_port = self.get_localized_module_option('server_port', 8080) else: server_port = self.get_localized_module_option( 'ssl_server_port', 8443) if server_addr is None: raise ServerConfigException( 'no server_addr configured; ' 'try "ceph config set mgr mgr/{}/{}/server_addr <ip>"'.format( self.module_name, self.get_mgr_id())) self.log.info('server: ssl=%s host=%s port=%d', 'yes' if ssl else 'no', server_addr, server_port) # Initialize custom handlers. cherrypy.tools.authenticate = AuthManagerTool() cherrypy.tools.plugin_hooks = cherrypy.Tool( 'before_handler', lambda: PLUGIN_MANAGER.hook.filter_request_before_handler( request=cherrypy.request), priority=10) cherrypy.tools.request_logging = RequestLoggingTool() cherrypy.tools.dashboard_exception_handler = HandlerWrapperTool( dashboard_exception_handler, priority=31) # Apply the 'global' CherryPy configuration. config = { 'engine.autoreload.on': False, 'server.socket_host': server_addr, 'server.socket_port': int(server_port), 'error_page.default': json_error_page, 'tools.request_logging.on': True, 'tools.gzip.on': True, 'tools.gzip.mime_types': [ # text/html and text/plain are the default types to compress 'text/html', 'text/plain', # We also want JSON and JavaScript to be compressed 'application/json', 'application/javascript', ], 'tools.json_in.on': True, 'tools.json_in.force': False, 'tools.plugin_hooks.on': True, } if ssl: # SSL initialization cert = self.get_store("crt") if cert is not None: self.cert_tmp = tempfile.NamedTemporaryFile() self.cert_tmp.write(cert.encode('utf-8')) self.cert_tmp.flush() # cert_tmp must not be gc'ed cert_fname = self.cert_tmp.name else: cert_fname = self.get_localized_module_option('crt_file') pkey = self.get_store("key") if pkey is not None: self.pkey_tmp = tempfile.NamedTemporaryFile() self.pkey_tmp.write(pkey.encode('utf-8')) self.pkey_tmp.flush() # pkey_tmp must not be gc'ed pkey_fname = self.pkey_tmp.name else: pkey_fname = self.get_localized_module_option('key_file') if not cert_fname or not pkey_fname: raise ServerConfigException('no certificate configured') if not os.path.isfile(cert_fname): raise ServerConfigException('certificate %s does not exist' % cert_fname) if not os.path.isfile(pkey_fname): raise ServerConfigException('private key %s does not exist' % pkey_fname) # Do some validations to the private key and certificate: # - Check the type and format # - Check the certificate expiration date # - Check the consistency of the private key # - Check that the private key and certificate match up try: with open(cert_fname) as f: x509 = crypto.load_certificate(crypto.FILETYPE_PEM, f.read()) if x509.has_expired(): self.log.warning( 'Certificate {} has been expired'.format( cert_fname)) except (ValueError, crypto.Error) as e: raise ServerConfigException( 'Invalid certificate {}: {}'.format(cert_fname, str(e))) try: with open(pkey_fname) as f: pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, f.read()) pkey.check() except (ValueError, crypto.Error) as e: raise ServerConfigException( 'Invalid private key {}: {}'.format(pkey_fname, str(e))) try: context = SSL.Context(SSL.TLSv1_METHOD) context.use_certificate_file(cert_fname, crypto.FILETYPE_PEM) context.use_privatekey_file(pkey_fname, crypto.FILETYPE_PEM) context.check_privatekey() except crypto.Error as e: self.log.warning( 'Private key {} and certificate {} do not match up: {}'. format(pkey_fname, cert_fname, str(e))) config['server.ssl_module'] = 'builtin' config['server.ssl_certificate'] = cert_fname config['server.ssl_private_key'] = pkey_fname cherrypy.config.update(config) self._url_prefix = prepare_url_prefix( self.get_module_option('url_prefix', default='')) uri = "{0}://{1}:{2}{3}/".format( 'https' if ssl else 'http', socket.getfqdn() if server_addr == "::" else server_addr, server_port, self.url_prefix) return uri
class Module(MgrModule, CherryPyConfig): """ dashboard module entrypoint """ COMMANDS = [ { 'cmd': 'dashboard set-jwt-token-ttl ' 'name=seconds,type=CephInt', 'desc': 'Set the JWT token TTL in seconds', 'perm': 'w' }, { 'cmd': 'dashboard get-jwt-token-ttl', 'desc': 'Get the JWT token TTL in seconds', 'perm': 'r' }, { "cmd": "dashboard create-self-signed-cert", "desc": "Create self signed certificate", "perm": "w" }, { "cmd": "dashboard grafana dashboards update", "desc": "Push dashboards to Grafana", "perm": "w", }, ] COMMANDS.extend(options_command_list()) COMMANDS.extend(SSO_COMMANDS) PLUGIN_MANAGER.hook.register_commands() MODULE_OPTIONS = [ Option(name='server_addr', type='str', default=get_default_addr()), Option(name='server_port', type='int', default=8080), Option(name='ssl_server_port', type='int', default=8443), Option(name='jwt_token_ttl', type='int', default=28800), Option(name='password', type='str', default=''), Option(name='url_prefix', type='str', default=''), Option(name='username', type='str', default=''), Option(name='key_file', type='str', default=''), Option(name='crt_file', type='str', default=''), Option(name='ssl', type='bool', default=True) ] MODULE_OPTIONS.extend(options_schema_list()) for options in PLUGIN_MANAGER.hook.get_options() or []: MODULE_OPTIONS.extend(options) __pool_stats = collections.defaultdict( lambda: collections.defaultdict(lambda: collections.deque(maxlen=10))) def __init__(self, *args, **kwargs): super(Module, self).__init__(*args, **kwargs) CherryPyConfig.__init__(self) mgr.init(self) self._stopping = threading.Event() self.shutdown_event = threading.Event() self.ACCESS_CTRL_DB = None self.SSO_DB = None @classmethod def can_run(cls): if cherrypy is None: return False, "Missing dependency: cherrypy" if not os.path.exists(cls.get_frontend_path()): return False, "Frontend assets not found: incomplete build?" return True, "" @classmethod def get_frontend_path(cls): current_dir = os.path.dirname(os.path.abspath(__file__)) return os.path.join(current_dir, 'frontend/dist') def serve(self): AuthManager.initialize() load_sso_db() uri = self.await_configuration() if uri is None: # We were shut down while waiting return # Publish the URI that others may use to access the service we're # about to start serving self.set_uri(uri) mapper, parent_urls = generate_routes(self.url_prefix) config = { self.url_prefix or '/': { 'tools.staticdir.on': True, 'tools.staticdir.dir': self.get_frontend_path(), 'tools.staticdir.index': 'index.html' } } for purl in parent_urls: config[purl] = {'request.dispatch': mapper} cherrypy.tree.mount(None, config=config) PLUGIN_MANAGER.hook.setup() cherrypy.engine.start() NotificationQueue.start_queue() TaskManager.init() logger.info('Engine started.') update_dashboards = str_to_bool( self.get_module_option('GRAFANA_UPDATE_DASHBOARDS', 'False')) if update_dashboards: logger.info('Starting Grafana dashboard task') TaskManager.run( 'grafana/dashboards/update', {}, push_local_dashboards, kwargs=dict(tries=10, sleep=60), ) # wait for the shutdown event self.shutdown_event.wait() self.shutdown_event.clear() NotificationQueue.stop() cherrypy.engine.stop() logger.info('Engine stopped') def shutdown(self): super(Module, self).shutdown() CherryPyConfig.shutdown(self) logger.info('Stopping engine...') self.shutdown_event.set() def handle_command(self, inbuf, cmd): # pylint: disable=too-many-return-statements res = handle_option_command(cmd) if res[0] != -errno.ENOSYS: return res res = handle_sso_command(cmd) if res[0] != -errno.ENOSYS: return res if cmd['prefix'] == 'dashboard set-jwt-token-ttl': self.set_module_option('jwt_token_ttl', str(cmd['seconds'])) return 0, 'JWT token TTL updated', '' if cmd['prefix'] == 'dashboard get-jwt-token-ttl': ttl = self.get_module_option('jwt_token_ttl', JwtManager.JWT_TOKEN_TTL) return 0, str(ttl), '' if cmd['prefix'] == 'dashboard create-self-signed-cert': self.create_self_signed_cert() return 0, 'Self-signed certificate created', '' if cmd['prefix'] == 'dashboard grafana dashboards update': push_local_dashboards() return 0, 'Grafana dashboards updated', '' return (-errno.EINVAL, '', 'Command not found \'{0}\''.format(cmd['prefix'])) def create_self_signed_cert(self): # create a key pair pkey = crypto.PKey() pkey.generate_key(crypto.TYPE_RSA, 2048) # create a self-signed cert cert = crypto.X509() cert.get_subject().O = "IT" cert.get_subject().CN = "ceph-dashboard" cert.set_serial_number(int(uuid4())) cert.gmtime_adj_notBefore(0) cert.gmtime_adj_notAfter(10 * 365 * 24 * 60 * 60) cert.set_issuer(cert.get_subject()) cert.set_pubkey(pkey) cert.sign(pkey, 'sha512') cert = crypto.dump_certificate(crypto.FILETYPE_PEM, cert) self.set_store('crt', cert.decode('utf-8')) pkey = crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey) self.set_store('key', pkey.decode('utf-8')) def notify(self, notify_type, notify_id): NotificationQueue.new_notification(notify_type, notify_id) def get_updated_pool_stats(self): df = self.get('df') pool_stats = {p['id']: p['stats'] for p in df['pools']} now = time.time() for pool_id, stats in pool_stats.items(): for stat_name, stat_val in stats.items(): self.__pool_stats[pool_id][stat_name].append((now, stat_val)) return self.__pool_stats