├── .github └── workflows │ └── release-actions.yml ├── .gitignore ├── .travis.yml ├── ChangeLog ├── Dockerfile ├── LICENSE ├── README.md ├── app ├── __init__.py ├── base.py ├── dashboard │ ├── __init__.py │ └── views.py ├── graphite │ ├── __init__.py │ └── views.py ├── influx │ ├── __init__.py │ └── views.py ├── static │ ├── css │ │ ├── bootstrap.min.slate.css │ │ └── ceph.dash.css │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.svg │ │ ├── glyphicons-halflings-regular.ttf │ │ └── glyphicons-halflings-regular.woff │ ├── img │ │ ├── favicon.ico │ │ └── icon.png │ └── js │ │ ├── bootstrap.min.js │ │ ├── ceph.dash.js │ │ ├── dx.chartjs.js │ │ ├── globalize.min.js │ │ ├── jquery-2.1.4.min.js │ │ ├── jquery.flot.byte.js │ │ ├── jquery.flot.js │ │ ├── jquery.flot.time.js │ │ └── jquery.flot.tooltip.js └── templates │ └── status.html ├── ceph-dash.py ├── config.graphite.json ├── config.influxdb.json ├── config.json ├── contrib ├── apache │ └── cephdash ├── docker │ └── entrypoint.sh ├── nginx │ ├── cephdash.conf │ ├── uwsgi.ini │ └── uwsgi_params ├── rook │ ├── ceph-dash-ingress.yaml │ ├── ceph-dash-service.yaml │ └── ceph-dash.yaml └── wsgi │ └── cephdash.wsgi ├── debian ├── ceph-dash.install ├── changelog ├── compat ├── control ├── copyright ├── rules └── source │ └── format ├── requirements.txt ├── screenshots ├── ceph-dash-content-warning.png ├── ceph-dash-graphite.png ├── ceph-dash-influxdb.png ├── ceph-dash-popover.png └── ceph-dash.png ├── test-requirements.txt ├── tests ├── __init__.py ├── test_config.py └── test_dashboard.py └── tox.ini /.github/workflows/release-actions.yml: -------------------------------------------------------------------------------- 1 | name: Publish Docker image 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | push_to_registry: 9 | name: Push Docker image to Docker Hub 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Check out the repo 13 | uses: actions/checkout@v2 14 | 15 | - name: Log in to Docker Hub 16 | uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 17 | with: 18 | username: ${{ secrets.DOCKER_USERNAME }} 19 | password: ${{ secrets.DOCKER_PASSWORD }} 20 | 21 | - name: Extract metadata (tags, labels) for Docker 22 | id: meta 23 | uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 24 | with: 25 | images: crapworks/ceph-dash 26 | 27 | - name: Build and push Docker image 28 | uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc 29 | with: 30 | context: . 31 | push: true 32 | tags: ${{ steps.meta.outputs.tags }} 33 | labels: ${{ steps.meta.outputs.labels }} 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .*.swp 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | install: 3 | - pip install tox 4 | script: 5 | - tox 6 | env: 7 | - TOXENV=flake8 8 | - TOXENV=py27 9 | -------------------------------------------------------------------------------- /ChangeLog: -------------------------------------------------------------------------------- 1 | 2015-09-24 Christian Eichelmann 2 | * ceph-dash (code): refactoring flask structure 3 | * ceph-dash (code): use of blueprints insteatd of method views 4 | * ceph-dash (code): added proxy endpoints for influxdb, graphite 5 | * ceph-dash (code): added support for influxdb graphing 6 | 7 | 2015-02-26 Christian Eichelmann 8 | * ceph-dash (js): show warning icon if showing old data 9 | * ceph-dash (js): general cleanup of javascript code 10 | 11 | 2014-11-05 Christian Eichelmann 12 | * ceph-dash (js): show popover with details for unhealthy osds (hover) 13 | * ceph-dash (api): if osds are unhealthy, retrieve additional information 14 | * ceph-dash (code): small refactorings 15 | 16 | 2014-09-10 Christian Eichelmann 17 | * ceph-dash (js/css): general cleanup 18 | * ceph-dash (js/css): sections are now collapsable 19 | 20 | 2014-09-11 Christian Eichelmann 21 | * ceph-dash (js): added flot library 22 | * ceph-dash (js/css): added code to show graphite graphs in ceph-dash 23 | * ceph-dash (config): added sample config for graphite integration 24 | * ceph-dash (template): added optional metrics section for flot 25 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM centos:7 2 | MAINTAINER Christian Eichelmann "christian@crapworks.de" 3 | 4 | ENV CEPH_VERSION mimic 5 | 6 | # Install Ceph 7 | RUN rpm --import 'https://download.ceph.com/keys/release.asc' 8 | RUN rpm -Uvh http://download.ceph.com/rpm-${CEPH_VERSION}/el7/noarch/ceph-release-1-1.el7.noarch.rpm 9 | RUN yum install -y epel-release && yum clean all 10 | RUN yum install -y ceph python27 python-pip && yum clean all 11 | 12 | COPY . /cephdash 13 | WORKDIR /cephdash 14 | RUN pip install -r requirements.txt 15 | 16 | ENTRYPOINT ["/cephdash/contrib/docker/entrypoint.sh"] 17 | CMD ["ceph-dash.py"] 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Christian Eichelmann 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ceph-dash - a free ceph dashboard / monitoring api 2 | ================================================== 3 | 4 | - [ceph-dash - a free ceph dashboard / monitoring api](#user-content-ceph-dash---a-free-ceph-dashboard--monitoring-api) 5 | - [Newest Feature](#user-content-newest-feature) 6 | - [Rook on Kubernetes](#user-content-rook-on-kubernetes) 7 | - [Docker Container](#user-content-docker-container) 8 | - [InfluxDB support](#user-content-influxdb-support) 9 | - [Old content warning](#user-content-old-content-warning) 10 | - [Unhealthy OSD popover](#user-content-unhealthy-osd-popover) 11 | - [Quickstart](#user-content-quickstart) 12 | - [Dashboard](#user-content-dashboard) 13 | - [REST Api](#user-content-rest-api) 14 | - [Nagios Check](#user-content-nagios-check) 15 | - [Deployment](#user-content-deployment) 16 | - [Pictures!!](#user-content-pictures) 17 | - [Graphite Integration](#user-content-graphite-integration) 18 | - [Configuration](#user-content-configuration) 19 | - [Example](#user-content-example) 20 | - [FAQ](#user-content-faq) 21 | 22 | 23 | This is a small and clean approach of providing the [Ceph](http://ceph.com) overall cluster health status via a restful json api as well as via a (hopefully) fancy web gui. There are no dependencies to the existing ```ceph-rest-api```. This wsgi application talks to the cluster directly via librados. 24 | 25 | You can find a blog entry regarding monitoring a Ceph cluster with ceph-dash on [Crapworks](http://crapworks.de/blog/2015/01/05/ceph-monitoring/). 26 | 27 | [Here](http://de.slideshare.net/Inktank_Ceph/07-ceph-days-sf2015-paul-evans-static) you can find a presentation from Paul Evans, taken from the Ceph Day in San Francisco (March 12, 2015) where he is comparing several Ceph-GUIs, including ceph-dash. 28 | 29 | Newest Feature 30 | -------------- 31 | 32 | ### Rook on Kubernetes 33 | 34 | I recently played around with [Rook](https://rook.io/) on [Kubernetes](https://kubernetes.io/). This was so far my fastest Ceph cluster to setup. Since Rook provides some secrets and config maps already, I made the docker container for Ceph-dash compatible with their format. I also added Kubernetes deployment files in the contrib folder (tested on [GKE](https://cloud.google.com/kubernetes-engine), but should work on any Kubernetes cluster). No need to configure anything, they should just work out of the box. 35 | 36 | ### Docker container 37 | 38 | Since everybody recently seems to be hyped as hell about the container stuff, I've decided that I can contribute to that with a ready-to-use docker container. Available at [Docker Hub](https://hub.docker.com/r/crapworks/ceph-dash/) you can pull the ceph-dash container and configure it via the following environment variables: 39 | 40 | * Required: $CEPHMONS (comma separated list of ceph monitor ip addresses) 41 | * Required: $KEYRING (full keyring that you want to use to connect to your ceph cluster) 42 | * Optional: $NAME (name of the key you want to use) 43 | * Optional: $ID (id of the key you want to use) 44 | 45 | **Example** 46 | 47 | ```bash 48 | docker run -p 5000:5000 -e CEPHMONS='10.0.2.15,10.0.2.16' -e KEYRING="$(sudo cat /etc/ceph/keyring)" crapworks/ceph-dash:latest 49 | ``` 50 | 51 | ### InfluxDB support 52 | 53 | I've refactored the code quite a bit to make use of [Blueprints](http://flask.pocoo.org/docs/0.10/blueprints/) instead of [Method Views](http://flask.pocoo.org/docs/0.10/views/). The structure of the code has changed, but I was keeping everything backwards compatible to all your deployments should still work with the current version. This is for now not a release, because I want to see if there is some negative feedback on this. And here are two new things you can cheer about! 54 | 55 | #### Graphing Proxies 56 | 57 | Your browser does not talk directly to [Graphite](graphite.wikidot.com) directly anymore! It uses the ```/graphite``` endpoint which already provides flot-formated json output. Ceph-dash will establish a connection to Graphite and gather all relevant data. This should prevent Cross-Domain issues and in case of [InfluxDB](https://influxdb.com), also hides the database password. Due to it's generic nature, it should be easy to add more graphing backends if needed. 58 | 59 | #### InfluxDB support 60 | 61 | Ceph-dash now supports also [InfluxDB](https://influxdb.com) as a graphing backend besides [Graphite](graphite.wikidot.com). You need client and server version ```> 0.9``` since the api broke with that release and is not backwards compatible. If you do not have the InfluxDB python module installed, Ceph-dash will *NOT* enable the InfluxDB proxy and will not load any configured InfluxDB resources. So please be sure to have the latest InfluxDB python module installed if you want to use InfluxDB as a backend. 62 | You can find a sample configuration file called ```config.influxdb.json``` in the root folder, which should explain how to use it. Please understand that I can't give you support for you InfluxDB setup, because this would definitely exceed the scope of Ceph-Dash. 63 | 64 | ![screenshot04](https://github.com/crapworks/ceph-dash/raw/master/screenshots/ceph-dash-influxdb.png) 65 | 66 | ### Old content warning 67 | 68 | If an AJAX call to the underlying ceph-dash API isn't answered within 3 seconds, a silent timeout is happening. The dashboard will still show the old data. I wanted to give the user a hint if something is wrong with the api or the ceph cluster, so I've added a little warning icon that tells you if the data shown in ceph-dash is getting to old. Reasons for that can be an slow or unresponsive cluster (some error handling is happening - a monitor failover for example). 69 | 70 | ![screenshot03](https://github.com/crapworks/ceph-dash/raw/master/screenshots/ceph-dash-content-warning.png) 71 | 72 | ### Unhealthy OSD popover 73 | 74 | The current release features a popover, which becomes available if there are any unhealthy osds in the cluster. If the count for Unhealthy osds is not 0, hovering over the field with the number of unhealthy osds will show a popover with additional information about those osds (including the name, the state and the host that contains this osd). To do this, ceph-dash has to issue an additional command to the cluster. This additional request will only be triggered if the first command shows any unhealthy osds! 75 | 76 | ![screenshot03](https://github.com/crapworks/ceph-dash/raw/master/screenshots/ceph-dash-popover.png) 77 | 78 | I also did some minor code refactoring to clean everything up a bit. 79 | 80 | Quickstart 81 | ---------- 82 | 83 | 1. clone this repository 84 | 2. place it on one of your ceph monitor nodes 85 | 3. run ```ceph-dash.py``` 86 | 4. point your browser to http://ceph-monitor:5000/ 87 | 5. enjoy! 88 | 89 | Dashboard 90 | --------- 91 | 92 | If you hit the address via a browser, you see the web frontend, that will inform you on a single page about all important things of your ceph cluster. 93 | 94 | REST Api 95 | -------- 96 | 97 | If you access the address via commandline tools or programming languages, use ```content-type: application/json``` and you will get all the information as a json output (which is actually the json formatted output of ```ceph status --format=json```. 98 | 99 | Anyways, this is not a wrapper around the ceph binary, it uses the python bindings of librados. 100 | 101 | This api can be requested by, for example, a nagios check, to check your overall cluster health. This brings the advantage of querying this information without running local checks on your monitor nodes, just by accessing a read only http api. 102 | 103 | Nagios Check 104 | ------------ 105 | 106 | A Nagios check that uses ceph-dash for monitoring your ceph cluster status is available [here](https://github.com/Crapworks/check_ceph_dash) 107 | 108 | Deployment 109 | ---------- 110 | 111 | You may want to deploy this wsgi application into a real webserver like apache or nginx. For convenience, I've put the wsgi file and a sample apache vhost config inside of the ```contrib``` folder, 112 | 113 | You can edit the config.json file to configure how to talk to the Ceph cluster. 114 | 115 | - `ceph_config` is the location of /etc/ceph/ceph.conf 116 | - `keyring` points to a keyring to use to authenticate with the cluster 117 | - `client_id` or `client_name` is used to specify the name to use with the keyring 118 | 119 | Pictures!! 120 | ---------- 121 | 122 | In case anyone wants to see what to expect, here you go: 123 | 124 | ![screenshot01](https://github.com/crapworks/ceph-dash/raw/master/screenshots/ceph-dash.png) 125 | 126 | Graphite Integration 127 | -------------------- 128 | 129 | I've integrated the flot graphing library to make it possible to show some graphs from [Graphite](graphite.wikidot.com) in ceph-dash. First of all: ceph-dash does **NOT** put any data into graphite! You have to do it yourself. We are using our [Icinga](https://www.icinga.org/) monitoring to push performance metrics to graphite. The graphs shown in the example were created by the above mentioned [Nagios check for ceph-dash](https://github.com/Crapworks/check_ceph_dash). 130 | 131 | If you do not have a graphite section in your ```config.json``` the Metrics section will not appear in ceph-dash. 132 | 133 | ### Configuration 134 | 135 | There is a sample configuration file called ```config.graphite.json```. Everything in there should be quite self-explanatory. If not, feel free to open an issue on github! 136 | 137 | ### Example 138 | 139 | Here you can see an example where one graph shows the bytes read/write per second, and another one shows the IOPS during the last two hours: 140 | 141 | ![screenshot01](https://github.com/crapworks/ceph-dash/raw/master/screenshots/ceph-dash-graphite.png) 142 | 143 | FAQ 144 | --- 145 | 146 | ### How can I change the port number 147 | 148 | The development server of Ceph-dash runs by default on port 5000. If you can't use this port since it is already used by another application, you can change it by opening `ceph-dash.py` and change the line 149 | ```python 150 | app.run(host='0.0.0.0', debug=True) 151 | ``` 152 | to 153 | ```python 154 | app.run(host='0.0.0.0', port=6666, debug=True) 155 | ``` 156 | Please keep in mind that the development server should not be used in a production environment. Ceph-dash should be deployed into a proper webserver like Apache or Nginx. 157 | 158 | ### Running ceph-dash behind a reverse proxy 159 | 160 | Since Version 1.2 ceph-dash is able to run behind a reverse proxy that rewrites the path where ceph-dash resides correctly. If you are using nginx, you need to use a config like this: 161 | 162 | ``` 163 | server { 164 | location /foobar { 165 | proxy_pass http://127.0.0.1:5000; 166 | proxy_set_header Host $host; 167 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 168 | proxy_set_header X-Scheme $scheme; 169 | proxy_set_header X-Script-Name /foobar; 170 | } 171 | } 172 | 173 | ``` 174 | 175 | See also: https://github.com/wilbertom/flask-reverse-proxy, which is where I got the code for doing this. 176 | 177 | ### Problems with NginX and uwsgi 178 | 179 | See [this issue](/../../issues/35) for a detailed explanation how to fix errors with NginX and uwsgi (Thanks to [@Lighiche](https://github.com/Lighiche)) 180 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import json 6 | 7 | from os.path import dirname 8 | from os.path import join 9 | from flask import Flask 10 | 11 | from app.dashboard.views import DashboardResource 12 | 13 | app = Flask(__name__) 14 | app.template_folder = join(dirname(__file__), 'templates') 15 | app.static_folder = join(dirname(__file__), 'static') 16 | 17 | 18 | class FlaskReverseProxied(object): 19 | """ 20 | Flask reverse proxy extension ripped of from: 21 | https://github.com/wilbertom/flask-reverse-proxy/ 22 | """ 23 | def __init__(self, app=None): 24 | self.app = None 25 | if app is not None: 26 | self.init_app(app) 27 | 28 | def init_app(self, app): 29 | self.app = app 30 | self.app.wsgi_app = ReverseProxied(self.app.wsgi_app) 31 | return self 32 | 33 | 34 | class ReverseProxied(object): 35 | """ 36 | Wrap the application in this middleware and configure the 37 | front-end server to add these headers, to let you quietly bind 38 | this to a URL other than / and to an HTTP scheme that is 39 | different than what is used locally. 40 | In nginx: 41 | location /prefix { 42 | proxy_pass http://192.168.0.1:5001; 43 | proxy_set_header Host $host; 44 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 45 | proxy_set_header X-Scheme $scheme; 46 | proxy_set_header X-Script-Name /prefix; 47 | } 48 | :param app: the WSGI application 49 | """ 50 | def __init__(self, app): 51 | self.app = app 52 | 53 | def __call__(self, environ, start_response): 54 | script_name = environ.get('HTTP_X_SCRIPT_NAME', '') 55 | if script_name: 56 | environ['SCRIPT_NAME'] = script_name 57 | path_info = environ.get('PATH_INFO', '') 58 | if path_info and path_info.startswith(script_name): 59 | environ['PATH_INFO'] = path_info[len(script_name):] 60 | server = environ.get('HTTP_X_FORWARDED_SERVER_CUSTOM', 61 | environ.get('HTTP_X_FORWARDED_SERVER', '')) 62 | if server: 63 | environ['HTTP_HOST'] = server 64 | 65 | scheme = environ.get('HTTP_X_SCHEME', '') 66 | 67 | if scheme: 68 | environ['wsgi.url_scheme'] = scheme 69 | 70 | return self.app(environ, start_response) 71 | 72 | 73 | class UserConfig(dict): 74 | """ loads the json configuration file """ 75 | 76 | def _string_decode_hook(self, data): 77 | rv = {} 78 | for key, value in data.items(): 79 | try: 80 | if isinstance(key, unicode): 81 | key = key.encode('utf-8') 82 | if isinstance(value, unicode): 83 | value = value.encode('utf-8') 84 | # unicode does not exist if python3 is used 85 | # ignore and don't do encoding, it is not 86 | # needed in python3 87 | except NameError: 88 | pass 89 | rv[key] = value 90 | return rv 91 | 92 | def __init__(self): 93 | dict.__init__(self) 94 | configfile = join(dirname(dirname(__file__)), 'config.json') 95 | configfile = os.environ.get('CEPHDASH_CONFIGFILE', configfile) 96 | self.update(json.load(open(configfile), object_hook=self._string_decode_hook)) 97 | 98 | if os.environ.get('CEPHDASH_CEPHCONFIG', False): 99 | self['ceph_config'] = os.environ['CEPHDASH_CEPHCONFIG'] 100 | if os.environ.get('CEPHDASH_KEYRING', False): 101 | self['keyring'] = os.environ['CEPHDASH_KEYRING'] 102 | if os.environ.get('CEPHDASH_ID', False): 103 | self['client_id'] = os.environ['CEPHDASH_ID'] 104 | if os.environ.get('CEPHDASH_NAME', False): 105 | self['client_name'] = os.environ['CEPHDASH_NAME'] 106 | 107 | 108 | app.config['USER_CONFIG'] = UserConfig() 109 | 110 | # only load influxdb endpoint if module is available 111 | try: 112 | import influxdb 113 | assert influxdb 114 | except ImportError: 115 | # remove influxdb config because we can't use it 116 | if 'influxdb' in app.config['USER_CONFIG']: 117 | del app.config['USER_CONFIG']['influxdb'] 118 | 119 | # log something so the user knows what's up 120 | # TODO: make logging work! 121 | app.logger.warning('No influxdb module found, disabling influxdb support') 122 | else: 123 | # only load endpoint if user wants to use influxdb 124 | if 'influxdb' in app.config['USER_CONFIG']: 125 | from app.influx.views import InfluxResource 126 | app.register_blueprint(InfluxResource.as_blueprint()) 127 | 128 | # only load endpoint if user wants to use graphite 129 | if 'graphite' in app.config['USER_CONFIG']: 130 | from app.graphite.views import GraphiteResource 131 | app.register_blueprint(GraphiteResource.as_blueprint()) 132 | 133 | # load dashboard and graphite endpoint 134 | app.register_blueprint(DashboardResource.as_blueprint()) 135 | 136 | # enable reverse proxy support 137 | proxied = FlaskReverseProxied(app) 138 | -------------------------------------------------------------------------------- /app/base.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | from flask.views import MethodView 3 | 4 | 5 | class ApiResource(MethodView): 6 | endpoint = None 7 | url_prefix = None 8 | url_rules = {} 9 | 10 | @classmethod 11 | def as_blueprint(cls, name=None): 12 | name = name or cls.endpoint 13 | bp = Blueprint(name, cls.__module__, url_prefix=cls.url_prefix) 14 | for endpoint, options in cls.url_rules.items(): 15 | url_rule = options.get('rule', '') 16 | defaults = options.get('defaults', {}) 17 | bp.add_url_rule(url_rule, defaults=defaults, view_func=cls.as_view(endpoint)) 18 | return bp 19 | -------------------------------------------------------------------------------- /app/dashboard/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crapworks/ceph-dash/00d354beeb9ae92ac75d789c598621048a550a96/app/dashboard/__init__.py -------------------------------------------------------------------------------- /app/dashboard/views.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | 4 | import json 5 | 6 | from flask import request 7 | from flask import render_template 8 | from flask import abort 9 | from flask import jsonify 10 | from flask import current_app 11 | from flask.views import MethodView 12 | 13 | from rados import Rados 14 | from rados import Error as RadosError 15 | 16 | from app.base import ApiResource 17 | 18 | 19 | class CephClusterProperties(dict): 20 | """ 21 | Validate ceph cluster connection properties 22 | """ 23 | 24 | def __init__(self, config): 25 | dict.__init__(self) 26 | 27 | self['conffile'] = config['ceph_config'] 28 | self['conf'] = dict() 29 | 30 | if 'keyring' in config: 31 | self['conf']['keyring'] = config['keyring'] 32 | if 'client_id' in config and 'client_name' in config: 33 | raise RadosError("Can't supply both client_id and client_name") 34 | if 'client_id' in config: 35 | self['rados_id'] = config['client_id'] 36 | if 'client_name' in config: 37 | self['name'] = config['client_name'] 38 | 39 | 40 | class CephClusterCommand(dict): 41 | """ 42 | Issue a ceph command on the given cluster and provide the returned json 43 | """ 44 | 45 | def __init__(self, cluster, **kwargs): 46 | dict.__init__(self) 47 | ret, buf, err = cluster.mon_command(json.dumps(kwargs), b'', timeout=5) 48 | if ret != 0: 49 | self['err'] = err 50 | else: 51 | self.update(json.loads(buf)) 52 | 53 | 54 | def find_host_for_osd(osd, osd_status): 55 | """ find host for a given osd """ 56 | 57 | for obj in osd_status['nodes']: 58 | if obj['type'] == 'host': 59 | if osd in obj['children']: 60 | return obj['name'] 61 | 62 | return 'unknown' 63 | 64 | 65 | def get_unhealthy_osd_details(osd_status): 66 | """ get all unhealthy osds from osd status """ 67 | 68 | unhealthy_osds = list() 69 | 70 | for obj in osd_status['nodes']: 71 | if obj['type'] == 'osd': 72 | # if OSD does not exists (DNE in osd tree) skip this entry 73 | if obj['exists'] == 0: 74 | continue 75 | if obj['status'] == 'down' or obj['reweight'] == 0.0: 76 | # It is possible to have one host in more than one branch in the tree. 77 | # Add each unhealthy OSD only once in the list 78 | if obj['status'] == 'down': 79 | status = 'down' 80 | else: 81 | status = 'out' 82 | entry = { 83 | 'name': obj['name'], 84 | 'status': status, 85 | 'host': find_host_for_osd(obj['id'], osd_status) 86 | } 87 | if entry not in unhealthy_osds: 88 | unhealthy_osds.append(entry) 89 | 90 | return unhealthy_osds 91 | 92 | 93 | class DashboardResource(ApiResource): 94 | """ 95 | Endpoint that shows overall cluster status 96 | """ 97 | 98 | endpoint = 'dashboard' 99 | url_prefix = '/' 100 | url_rules = { 101 | 'index': { 102 | 'rule': '/', 103 | } 104 | } 105 | 106 | def __init__(self): 107 | MethodView.__init__(self) 108 | self.config = current_app.config['USER_CONFIG'] 109 | self.clusterprop = CephClusterProperties(self.config) 110 | 111 | def get(self): 112 | with Rados(**self.clusterprop) as cluster: 113 | cluster_status = CephClusterCommand(cluster, prefix='status', format='json') 114 | if 'err' in cluster_status: 115 | abort(500, cluster_status['err']) 116 | 117 | # ceph >= 15.2.5 118 | if 'osdmap' not in cluster_status['osdmap']: 119 | # osdmap has been converted to depth-1 dict 120 | cluster_status['osdmap']['osdmap'] = cluster_status['osdmap'].copy() 121 | monitor_status = CephClusterCommand(cluster, prefix='quorum_status', format='json') 122 | cluster_status['monmap'] = monitor_status['monmap'] 123 | 124 | # check for unhealthy osds and get additional osd infos from cluster 125 | total_osds = cluster_status['osdmap']['osdmap']['num_osds'] 126 | in_osds = cluster_status['osdmap']['osdmap']['num_up_osds'] 127 | up_osds = cluster_status['osdmap']['osdmap']['num_in_osds'] 128 | 129 | if up_osds < total_osds or in_osds < total_osds: 130 | osd_status = CephClusterCommand(cluster, prefix='osd tree', format='json') 131 | if 'err' in osd_status: 132 | abort(500, osd_status['err']) 133 | 134 | # find unhealthy osds in osd tree 135 | cluster_status['osdmap']['details'] = get_unhealthy_osd_details(osd_status) 136 | 137 | if request.mimetype == 'application/json': 138 | return jsonify(cluster_status) 139 | else: 140 | return render_template('status.html', data=cluster_status, config=self.config) 141 | -------------------------------------------------------------------------------- /app/graphite/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crapworks/ceph-dash/00d354beeb9ae92ac75d789c598621048a550a96/app/graphite/__init__.py -------------------------------------------------------------------------------- /app/graphite/views.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | 4 | import ssl 5 | import json 6 | 7 | from flask import jsonify 8 | from flask import current_app 9 | 10 | # import urlopen from urllib.request / Python3 11 | # and fallback to urlopen from urllib2 / Python2 12 | try: 13 | from urllib.request import urlopen 14 | except ImportError: 15 | from urllib2 import urlopen 16 | 17 | from app.base import ApiResource 18 | 19 | 20 | class GraphiteResource(ApiResource): 21 | endpoint = 'graphite' 22 | url_prefix = '/graphite' 23 | url_rules = { 24 | 'index': { 25 | 'rule': '/', 26 | } 27 | } 28 | 29 | def get(self): 30 | config = current_app.config['USER_CONFIG'].get('graphite', {}) 31 | results = [] 32 | 33 | for metric in config.get('metrics', []): 34 | url = config['url'] + "/render?format=json&from=" + metric['from'] 35 | for target in metric.get('targets', []): 36 | url += '&target=' + target 37 | 38 | if hasattr(ssl, '_create_unverified_context'): 39 | ssl._create_default_https_context = ssl._create_unverified_context 40 | resp = urlopen(url) 41 | 42 | collection = [] 43 | for index, dataset in enumerate(json.load(resp)): 44 | series = {} 45 | # map graphite timestamp to javascript timestamp 46 | data = [[ts * 1000, v] for v, ts in dataset.get('datapoints', []) if v is not None] 47 | 48 | series['data'] = data 49 | series['label'] = metric['labels'][index] if 'labels' in metric else None 50 | series['lines'] = dict(fill=True) 51 | series['mode'] = metric['mode'] if 'mode' in metric else None 52 | series['color'] = metric['colors'][index] if 'colors' in metric else None 53 | collection.append(series) 54 | 55 | results.append(collection) 56 | 57 | return jsonify(results=results) 58 | -------------------------------------------------------------------------------- /app/influx/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crapworks/ceph-dash/00d354beeb9ae92ac75d789c598621048a550a96/app/influx/__init__.py -------------------------------------------------------------------------------- /app/influx/views.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | 4 | from flask import jsonify 5 | from flask import current_app 6 | 7 | from influxdb import InfluxDBClient 8 | 9 | from app.base import ApiResource 10 | 11 | 12 | class InfluxResource(ApiResource): 13 | endpoint = 'influxdb' 14 | url_prefix = '/influxdb' 15 | url_rules = { 16 | 'index': { 17 | 'rule': '/', 18 | } 19 | } 20 | 21 | def get(self): 22 | config = current_app.config['USER_CONFIG'].get('influxdb', {}) 23 | client = InfluxDBClient.from_DSN(config['uri'], timeout=5) 24 | results = [] 25 | 26 | for metric in config.get('metrics', []): 27 | collection = [] 28 | for index, query in enumerate(metric.get('queries', [])): 29 | result = client.query(query, epoch='ms') 30 | 31 | if result: 32 | for dataset in result.raw['series']: 33 | series = {} 34 | series['data'] = dataset['values'] 35 | series['label'] = metric['labels'][index] if 'labels' in metric else None 36 | series['lines'] = dict(fill=True) 37 | series['mode'] = metric['mode'] if 'mode' in metric else None 38 | series['color'] = metric['colors'][index] if 'colors' in metric else None 39 | 40 | collection.append(series) 41 | 42 | results.append(collection) 43 | 44 | return jsonify(results=results) 45 | -------------------------------------------------------------------------------- /app/static/css/ceph.dash.css: -------------------------------------------------------------------------------- 1 | .icon-ok { 2 | color: #62C462; 3 | } 4 | 5 | .icon-warn { 6 | color: #F89406; 7 | } 8 | 9 | .icon-err { 10 | color: #EE5F5B; 11 | } 12 | 13 | .panel-body { 14 | font-weight: bold; 15 | } 16 | 17 | .panel-title { 18 | font-weight: bold; 19 | } 20 | 21 | .cd-collapsable h3:after { 22 | font-family: 'Glyphicons Halflings'; 23 | content: "\e113"; 24 | cursor: pointer; 25 | float: right; 26 | } 27 | 28 | .cd-collapsed h3:after { 29 | font-family: 'Glyphicons Halflings'; 30 | content: "\e114"; 31 | cursor: pointer; 32 | float: right; 33 | } 34 | 35 | .graphite { 36 | width: 500px; 37 | height: 150px; 38 | font-size: 14px; 39 | line-height: 1.2em; 40 | } 41 | 42 | .influxdb { 43 | width: 500px; 44 | height: 150px; 45 | font-size: 14px; 46 | line-height: 1.2em; 47 | } 48 | 49 | #tooltip { 50 | position: absolute; 51 | display: none; 52 | border: 1px solid #000; 53 | padding: 2px; 54 | background-color: #272b30; 55 | } 56 | -------------------------------------------------------------------------------- /app/static/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crapworks/ceph-dash/00d354beeb9ae92ac75d789c598621048a550a96/app/static/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /app/static/fonts/glyphicons-halflings-regular.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | -------------------------------------------------------------------------------- /app/static/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crapworks/ceph-dash/00d354beeb9ae92ac75d789c598621048a550a96/app/static/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /app/static/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crapworks/ceph-dash/00d354beeb9ae92ac75d789c598621048a550a96/app/static/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /app/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crapworks/ceph-dash/00d354beeb9ae92ac75d789c598621048a550a96/app/static/img/favicon.ico -------------------------------------------------------------------------------- /app/static/img/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crapworks/ceph-dash/00d354beeb9ae92ac75d789c598621048a550a96/app/static/img/icon.png -------------------------------------------------------------------------------- /app/static/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.0.2 by @fat and @mdo 3 | * Copyright 2013 Twitter, Inc. 4 | * Licensed under http://www.apache.org/licenses/LICENSE-2.0 5 | * 6 | * Designed and built with all the love in the world by @mdo and @fat. 7 | */ 8 | 9 | if("undefined"==typeof jQuery)throw new Error("Bootstrap requires jQuery");+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]}}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one(a.support.transition.end,function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b()})}(jQuery),+function(a){"use strict";var b='[data-dismiss="alert"]',c=function(c){a(c).on("click",b,this.close)};c.prototype.close=function(b){function c(){f.trigger("closed.bs.alert").remove()}var d=a(this),e=d.attr("data-target");e||(e=d.attr("href"),e=e&&e.replace(/.*(?=#[^\s]*$)/,""));var f=a(e);b&&b.preventDefault(),f.length||(f=d.hasClass("alert")?d:d.parent()),f.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(f.removeClass("in"),a.support.transition&&f.hasClass("fade")?f.one(a.support.transition.end,c).emulateTransitionEnd(150):c())};var d=a.fn.alert;a.fn.alert=function(b){return this.each(function(){var d=a(this),e=d.data("bs.alert");e||d.data("bs.alert",e=new c(this)),"string"==typeof b&&e[b].call(d)})},a.fn.alert.Constructor=c,a.fn.alert.noConflict=function(){return a.fn.alert=d,this},a(document).on("click.bs.alert.data-api",b,c.prototype.close)}(jQuery),+function(a){"use strict";var b=function(c,d){this.$element=a(c),this.options=a.extend({},b.DEFAULTS,d)};b.DEFAULTS={loadingText:"loading..."},b.prototype.setState=function(a){var b="disabled",c=this.$element,d=c.is("input")?"val":"html",e=c.data();a+="Text",e.resetText||c.data("resetText",c[d]()),c[d](e[a]||this.options[a]),setTimeout(function(){"loadingText"==a?c.addClass(b).attr(b,b):c.removeClass(b).removeAttr(b)},0)},b.prototype.toggle=function(){var a=this.$element.closest('[data-toggle="buttons"]');if(a.length){var b=this.$element.find("input").prop("checked",!this.$element.hasClass("active")).trigger("change");"radio"===b.prop("type")&&a.find(".active").removeClass("active")}this.$element.toggleClass("active")};var c=a.fn.button;a.fn.button=function(c){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof c&&c;e||d.data("bs.button",e=new b(this,f)),"toggle"==c?e.toggle():c&&e.setState(c)})},a.fn.button.Constructor=b,a.fn.button.noConflict=function(){return a.fn.button=c,this},a(document).on("click.bs.button.data-api","[data-toggle^=button]",function(b){var c=a(b.target);c.hasClass("btn")||(c=c.closest(".btn")),c.button("toggle"),b.preventDefault()})}(jQuery),+function(a){"use strict";var b=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=this.sliding=this.interval=this.$active=this.$items=null,"hover"==this.options.pause&&this.$element.on("mouseenter",a.proxy(this.pause,this)).on("mouseleave",a.proxy(this.cycle,this))};b.DEFAULTS={interval:5e3,pause:"hover",wrap:!0},b.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},b.prototype.getActiveIndex=function(){return this.$active=this.$element.find(".item.active"),this.$items=this.$active.parent().children(),this.$items.index(this.$active)},b.prototype.to=function(b){var c=this,d=this.getActiveIndex();return b>this.$items.length-1||0>b?void 0:this.sliding?this.$element.one("slid",function(){c.to(b)}):d==b?this.pause().cycle():this.slide(b>d?"next":"prev",a(this.$items[b]))},b.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition.end&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},b.prototype.next=function(){return this.sliding?void 0:this.slide("next")},b.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},b.prototype.slide=function(b,c){var d=this.$element.find(".item.active"),e=c||d[b](),f=this.interval,g="next"==b?"left":"right",h="next"==b?"first":"last",i=this;if(!e.length){if(!this.options.wrap)return;e=this.$element.find(".item")[h]()}this.sliding=!0,f&&this.pause();var j=a.Event("slide.bs.carousel",{relatedTarget:e[0],direction:g});if(!e.hasClass("active")){if(this.$indicators.length&&(this.$indicators.find(".active").removeClass("active"),this.$element.one("slid",function(){var b=a(i.$indicators.children()[i.getActiveIndex()]);b&&b.addClass("active")})),a.support.transition&&this.$element.hasClass("slide")){if(this.$element.trigger(j),j.isDefaultPrevented())return;e.addClass(b),e[0].offsetWidth,d.addClass(g),e.addClass(g),d.one(a.support.transition.end,function(){e.removeClass([b,g].join(" ")).addClass("active"),d.removeClass(["active",g].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger("slid")},0)}).emulateTransitionEnd(600)}else{if(this.$element.trigger(j),j.isDefaultPrevented())return;d.removeClass("active"),e.addClass("active"),this.sliding=!1,this.$element.trigger("slid")}return f&&this.cycle(),this}};var c=a.fn.carousel;a.fn.carousel=function(c){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},b.DEFAULTS,d.data(),"object"==typeof c&&c),g="string"==typeof c?c:f.slide;e||d.data("bs.carousel",e=new b(this,f)),"number"==typeof c?e.to(c):g?e[g]():f.interval&&e.pause().cycle()})},a.fn.carousel.Constructor=b,a.fn.carousel.noConflict=function(){return a.fn.carousel=c,this},a(document).on("click.bs.carousel.data-api","[data-slide], [data-slide-to]",function(b){var c,d=a(this),e=a(d.attr("data-target")||(c=d.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"")),f=a.extend({},e.data(),d.data()),g=d.attr("data-slide-to");g&&(f.interval=!1),e.carousel(f),(g=d.attr("data-slide-to"))&&e.data("bs.carousel").to(g),b.preventDefault()}),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var b=a(this);b.carousel(b.data())})})}(jQuery),+function(a){"use strict";var b=function(c,d){this.$element=a(c),this.options=a.extend({},b.DEFAULTS,d),this.transitioning=null,this.options.parent&&(this.$parent=a(this.options.parent)),this.options.toggle&&this.toggle()};b.DEFAULTS={toggle:!0},b.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},b.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b=a.Event("show.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.$parent&&this.$parent.find("> .panel > .in");if(c&&c.length){var d=c.data("bs.collapse");if(d&&d.transitioning)return;c.collapse("hide"),d||c.data("bs.collapse",null)}var e=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[e](0),this.transitioning=1;var f=function(){this.$element.removeClass("collapsing").addClass("in")[e]("auto"),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return f.call(this);var g=a.camelCase(["scroll",e].join("-"));this.$element.one(a.support.transition.end,a.proxy(f,this)).emulateTransitionEnd(350)[e](this.$element[0][g])}}},b.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse").removeClass("in"),this.transitioning=1;var d=function(){this.transitioning=0,this.$element.trigger("hidden.bs.collapse").removeClass("collapsing").addClass("collapse")};return a.support.transition?(this.$element[c](0).one(a.support.transition.end,a.proxy(d,this)).emulateTransitionEnd(350),void 0):d.call(this)}}},b.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()};var c=a.fn.collapse;a.fn.collapse=function(c){return this.each(function(){var d=a(this),e=d.data("bs.collapse"),f=a.extend({},b.DEFAULTS,d.data(),"object"==typeof c&&c);e||d.data("bs.collapse",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.collapse.Constructor=b,a.fn.collapse.noConflict=function(){return a.fn.collapse=c,this},a(document).on("click.bs.collapse.data-api","[data-toggle=collapse]",function(b){var c,d=a(this),e=d.attr("data-target")||b.preventDefault()||(c=d.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,""),f=a(e),g=f.data("bs.collapse"),h=g?"toggle":d.data(),i=d.attr("data-parent"),j=i&&a(i);g&&g.transitioning||(j&&j.find('[data-toggle=collapse][data-parent="'+i+'"]').not(d).addClass("collapsed"),d[f.hasClass("in")?"addClass":"removeClass"]("collapsed")),f.collapse(h)})}(jQuery),+function(a){"use strict";function b(){a(d).remove(),a(e).each(function(b){var d=c(a(this));d.hasClass("open")&&(d.trigger(b=a.Event("hide.bs.dropdown")),b.isDefaultPrevented()||d.removeClass("open").trigger("hidden.bs.dropdown"))})}function c(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}var d=".dropdown-backdrop",e="[data-toggle=dropdown]",f=function(b){a(b).on("click.bs.dropdown",this.toggle)};f.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=c(e),g=f.hasClass("open");if(b(),!g){if("ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a(''}),b.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),b.prototype.constructor=b,b.prototype.getDefaults=function(){return b.DEFAULTS},b.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content")[this.options.html?"html":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},b.prototype.hasContent=function(){return this.getTitle()||this.getContent()},b.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},b.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")},b.prototype.tip=function(){return this.$tip||(this.$tip=a(this.options.template)),this.$tip};var c=a.fn.popover;a.fn.popover=function(c){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof c&&c;e||d.data("bs.popover",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.popover.Constructor=b,a.fn.popover.noConflict=function(){return a.fn.popover=c,this}}(jQuery),+function(a){"use strict";function b(c,d){var e,f=a.proxy(this.process,this);this.$element=a(c).is("body")?a(window):a(c),this.$body=a("body"),this.$scrollElement=this.$element.on("scroll.bs.scroll-spy.data-api",f),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||(e=a(c).attr("href"))&&e.replace(/.*(?=#[^\s]+$)/,"")||"")+" .nav li > a",this.offsets=a([]),this.targets=a([]),this.activeTarget=null,this.refresh(),this.process()}b.DEFAULTS={offset:10},b.prototype.refresh=function(){var b=this.$element[0]==window?"offset":"position";this.offsets=a([]),this.targets=a([]);var c=this;this.$body.find(this.selector).map(function(){var d=a(this),e=d.data("target")||d.attr("href"),f=/^#\w/.test(e)&&a(e);return f&&f.length&&[[f[b]().top+(!a.isWindow(c.$scrollElement.get(0))&&c.$scrollElement.scrollTop()),e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){c.offsets.push(this[0]),c.targets.push(this[1])})},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.$scrollElement[0].scrollHeight||this.$body[0].scrollHeight,d=c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(b>=d)return g!=(a=f.last()[0])&&this.activate(a);for(a=e.length;a--;)g!=f[a]&&b>=e[a]&&(!e[a+1]||b<=e[a+1])&&this.activate(f[a])},b.prototype.activate=function(b){this.activeTarget=b,a(this.selector).parents(".active").removeClass("active");var c=this.selector+'[data-target="'+b+'"],'+this.selector+'[href="'+b+'"]',d=a(c).parents("li").addClass("active");d.parent(".dropdown-menu").length&&(d=d.closest("li.dropdown").addClass("active")),d.trigger("activate")};var c=a.fn.scrollspy;a.fn.scrollspy=function(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.scrollspy.Constructor=b,a.fn.scrollspy.noConflict=function(){return a.fn.scrollspy=c,this},a(window).on("load",function(){a('[data-spy="scroll"]').each(function(){var b=a(this);b.scrollspy(b.data())})})}(jQuery),+function(a){"use strict";var b=function(b){this.element=a(b)};b.prototype.show=function(){var b=this.element,c=b.closest("ul:not(.dropdown-menu)"),d=b.data("target");if(d||(d=b.attr("href"),d=d&&d.replace(/.*(?=#[^\s]*$)/,"")),!b.parent("li").hasClass("active")){var e=c.find(".active:last a")[0],f=a.Event("show.bs.tab",{relatedTarget:e});if(b.trigger(f),!f.isDefaultPrevented()){var g=a(d);this.activate(b.parent("li"),c),this.activate(g,g.parent(),function(){b.trigger({type:"shown.bs.tab",relatedTarget:e})})}}},b.prototype.activate=function(b,c,d){function e(){f.removeClass("active").find("> .dropdown-menu > .active").removeClass("active"),b.addClass("active"),g?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu")&&b.closest("li.dropdown").addClass("active"),d&&d()}var f=c.find("> .active"),g=d&&a.support.transition&&f.hasClass("fade");g?f.one(a.support.transition.end,e).emulateTransitionEnd(150):e(),f.removeClass("in")};var c=a.fn.tab;a.fn.tab=function(c){return this.each(function(){var d=a(this),e=d.data("bs.tab");e||d.data("bs.tab",e=new b(this)),"string"==typeof c&&e[c]()})},a.fn.tab.Constructor=b,a.fn.tab.noConflict=function(){return a.fn.tab=c,this},a(document).on("click.bs.tab.data-api",'[data-toggle="tab"], [data-toggle="pill"]',function(b){b.preventDefault(),a(this).tab("show")})}(jQuery),+function(a){"use strict";var b=function(c,d){this.options=a.extend({},b.DEFAULTS,d),this.$window=a(window).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(c),this.affixed=this.unpin=null,this.checkPosition()};b.RESET="affix affix-top affix-bottom",b.DEFAULTS={offset:0},b.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},b.prototype.checkPosition=function(){if(this.$element.is(":visible")){var c=a(document).height(),d=this.$window.scrollTop(),e=this.$element.offset(),f=this.options.offset,g=f.top,h=f.bottom;"object"!=typeof f&&(h=g=f),"function"==typeof g&&(g=f.top()),"function"==typeof h&&(h=f.bottom());var i=null!=this.unpin&&d+this.unpin<=e.top?!1:null!=h&&e.top+this.$element.height()>=c-h?"bottom":null!=g&&g>=d?"top":!1;this.affixed!==i&&(this.unpin&&this.$element.css("top",""),this.affixed=i,this.unpin="bottom"==i?e.top-d:null,this.$element.removeClass(b.RESET).addClass("affix"+(i?"-"+i:"")),"bottom"==i&&this.$element.offset({top:document.body.offsetHeight-h-this.$element.height()}))}};var c=a.fn.affix;a.fn.affix=function(c){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof c&&c;e||d.data("bs.affix",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.affix.Constructor=b,a.fn.affix.noConflict=function(){return a.fn.affix=c,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var b=a(this),c=b.data();c.offset=c.offset||{},c.offsetBottom&&(c.offset.bottom=c.offsetBottom),c.offsetTop&&(c.offset.top=c.offsetTop),b.affix(c)})})}(jQuery); -------------------------------------------------------------------------------- /app/static/js/ceph.dash.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | // global variable to configure refresh interval and timeout (in seconds!) 3 | var refreshInterval = 5; 4 | var refreshTimeout = 3; 5 | 6 | // calculate outdated warning thresholds 7 | var outDatedWarning = (refreshInterval * 3); 8 | var outDatedError = (refreshInterval * 10); 9 | 10 | // last updated timestamp global variable 11 | var lastUpdatedTimestamp = 0; 12 | 13 | // add a endsWith function to strings 14 | String.prototype.endsWith = function(suffix) { 15 | return this.indexOf(suffix, this.length - suffix.length) !== -1; 16 | }; 17 | 18 | // Gauge chart configuration options {{{ 19 | var gauge_options = { 20 | palette: 'Soft Pastel', 21 | animation: { 22 | enabled: false 23 | }, 24 | valueIndicator: { 25 | type: 'triangleNeedle', 26 | color: '#7a8288' 27 | }, 28 | title: { 29 | text: 'Cluster storage utilization', 30 | font: { size: 18, color: '#c8c8c8', family: 'Helvetica' }, 31 | position: 'bottom-center' 32 | }, 33 | geometry: { 34 | startAngle: 180, 35 | endAngle: 0 36 | }, 37 | margin: { 38 | top: 0, 39 | right: 10 40 | }, 41 | rangeContainer: { 42 | ranges: [ 43 | { startValue: 0, endValue: 60, color: '#62c462' }, 44 | { startValue: 60, endValue: 80, color: '#f89406' }, 45 | { startValue: 80, endValue: 100, color: '#ee5f5b' } 46 | ] 47 | }, 48 | scale: { 49 | startValue: 0, 50 | endValue: 100, 51 | majorTick: { 52 | tickInterval: 20 53 | }, 54 | label: { 55 | customizeText: function (arg) { 56 | return arg.valueText + ' %'; 57 | } 58 | } 59 | } 60 | }; 61 | // }}} 62 | 63 | // Graphite to flot configuration options {{{ 64 | var flot_options = { 65 | grid: { 66 | show: true 67 | }, 68 | xaxis: { 69 | mode: "time", 70 | timezone: "browser" 71 | }, 72 | legend: { 73 | show: true 74 | }, 75 | grid: { 76 | hoverable: true, 77 | clickable: true 78 | }, 79 | tooltip: true, 80 | tooltipOpts: { 81 | id: "tooltip", 82 | defaultTheme: false, 83 | content: "%s: %y" 84 | }, 85 | yaxis: { 86 | min: 0 87 | }, 88 | colors: [ "#62c462", "#f89406", "#ee5f5b", "#5bc0de" ] 89 | } 90 | 91 | function updatePlot(backend) { 92 | if (window.location.pathname.endsWith('/')) { 93 | var endpoint = window.location.pathname + backend +'/'; 94 | } else { 95 | var endpoint = window.location.pathname + '/' + backend +'/'; 96 | } 97 | 98 | $.getJSON(endpoint, function(data) { 99 | $.each(data.results, function(index, series) { 100 | //// set the yaxis mode 101 | flot_options.yaxis.mode = (typeof series[0].mode != "undefined") ? series[0].mode : null; 102 | 103 | //// update plot 104 | $.plot('#' + backend + (index+1), series, flot_options); 105 | }); 106 | }); 107 | } 108 | // }}} 109 | 110 | // Pie chart configuration options {{{ 111 | var chart_options = { 112 | animation: { 113 | enabled: false 114 | }, 115 | tooltip: { 116 | enabled: true, 117 | format:"decimal", 118 | percentPrecision: 2, 119 | font: { size: 14, color: '#1c1e22', family: 'Helvetica' }, 120 | arrowLength: 10, 121 | customizeText: function() { 122 | return this.valueText + " - " + this.argumentText + " (" + this.percentText + ")"; 123 | } 124 | }, 125 | customizePoint: function (point) { 126 | if (point.argument.indexOf('active+clean') >= 0) { 127 | return { 128 | color: '#62c462' 129 | } 130 | } else if (point.argument.indexOf('active') >= 0) { 131 | return { 132 | color: '#f89406' 133 | } 134 | } else { 135 | return { 136 | color: '#ee5f5b' 137 | } 138 | } 139 | }, 140 | size: { 141 | height: 350, 142 | width: 315 143 | }, 144 | legend: { 145 | visible: false 146 | }, 147 | series: [{ 148 | type: "doughnut", 149 | argumentField: "state_name", 150 | valueField: "count", 151 | label: { 152 | visible: false, 153 | } 154 | }] 155 | }; 156 | // }}} 157 | 158 | // Convert bytes to human readable form {{{ 159 | function fmtBytes(bytes) { 160 | if (bytes==0) { return "0 bytes"; } 161 | var s = ['bytes', 'kB', 'MB', 'GB', 'TB', 'PB']; 162 | var e = Math.floor(Math.log(bytes) / Math.log(1024)); 163 | return (bytes / Math.pow(1024, e)).toFixed(2) + " " + s[e]; 164 | } 165 | // }}} 166 | 167 | // Initialize unhealthy osd popover {{{ 168 | $("#unhealthy_osds").popover({ 169 | html: true, 170 | placement: 'bottom', 171 | trigger: 'hover' 172 | }); 173 | // }}} 174 | 175 | // MAKE SECTION COLLAPSABLE {{{ 176 | $('.cd-collapsable').on("click", function (e) { 177 | if ($(this).hasClass('cd-collapsed')) { 178 | // expand the panel 179 | $(this).parents('.panel').find('.panel-body').slideDown(); 180 | $(this).removeClass('cd-collapsed'); 181 | } 182 | else { 183 | // collapse the panel 184 | $(this).parents('.panel').find('.panel-body').slideUp(); 185 | $(this).addClass('cd-collapsed'); 186 | } 187 | }); 188 | // }}} 189 | 190 | // GENERIC AJAX WRAPER {{{ 191 | function ajaxCall(url, callback) { 192 | $.ajax({ 193 | url: url, 194 | dataType: 'json', 195 | type: 'GET', 196 | data: null, 197 | contentType: 'application/json', 198 | success: callback, 199 | error: function() { 200 | // refresh last updated timestamp 201 | timeStamp = Math.floor(Date.now() / 1000); 202 | timeDiff = timeStamp - lastUpdatedTimestamp; 203 | 204 | if (lastUpdatedTimestamp == 0) { 205 | lastUpdatedTimestamp = timeStamp - refreshInterval; 206 | timeDiff = refreshInterval; 207 | } 208 | 209 | 210 | if (timeDiff > outDatedWarning) { 211 | msg = 'Content has last been refreshed more than ' + timeDiff + ' seconds before'; 212 | $('#last_update').show(); 213 | $('#last_update').tooltip({ 214 | placement: 'bottom', 215 | }); 216 | $('#last_update').attr('data-original-title', msg); 217 | } 218 | }, 219 | timeout: (refreshTimeout * 1000) 220 | }); 221 | } 222 | // }}} 223 | 224 | // CREATE A ALERT MESSAGE {{{ 225 | function message(severity, msg) { 226 | if (severity == 'success') { icon = 'ok' } 227 | if (severity == 'warning') { icon = 'flash' } 228 | if (severity == 'danger') { icon = 'remove' } 229 | return '
 ' + msg + '
'; 230 | } 231 | // }}} 232 | 233 | // CREATE PANEL {{{ 234 | function panel(severity, titel, message) { 235 | tmp = '
'; 236 | tmp = tmp + '
' + titel + '
'; 237 | tmp = tmp + '
'; 238 | tmp = tmp + '

' + message + '

'; 239 | tmp = tmp + '
'; 240 | tmp = tmp + '
'; 241 | return tmp; 242 | } 243 | // }}} 244 | 245 | // MAPPING CEPH TO BOOTSTRAP {{{ 246 | var ceph2bootstrap = { 247 | HEALTH_OK: 'success', 248 | HEALTH_WARN: 'warning', 249 | HEALTH_ERR: 'danger', 250 | down: 'warning', 251 | out: 'danger' 252 | } 253 | // }}} 254 | 255 | // INITIALIZE EMPTY PIE CHART {{{ 256 | $("#pg_status").dxPieChart($.extend(true, {}, chart_options, { 257 | dataSource: [] 258 | })); 259 | // }}} 260 | 261 | // WORKER FUNCTION (UPDATED) {{{ 262 | function worker() { 263 | callback = function(data, status, xhr) { 264 | // refresh last updated timestamp 265 | timeStamp = Math.floor(Date.now() / 1000); 266 | lastUpdatedTimestamp = timeStamp; 267 | $('#last_update').hide(); 268 | 269 | // Update cluster fsid 270 | $("#cluster_fsid").html(data['fsid']); 271 | 272 | // load all relevant data from retrieved json {{{ 273 | // ---------------------------------------------------------------- 274 | // *storage capacity* 275 | bytesTotal = data['pgmap']['bytes_total']; 276 | bytesUsed = data['pgmap']['bytes_used']; 277 | percentUsed = Math.round((bytesUsed / bytesTotal) * 100); 278 | 279 | // *placement groups* 280 | pgsByState = data['pgmap']['pgs_by_state']; 281 | numPgs = data['pgmap']['num_pgs']; 282 | 283 | // *recovery status* 284 | recoverBytes = data['pgmap']['recovering_bytes_per_sec']; 285 | recoverKeys = data['pgmap']['recovering_keys_per_sec']; 286 | recoverObjects = data['pgmap']['recovering_objects_per_sec']; 287 | 288 | writesPerSec = fmtBytes(data['pgmap']['write_bytes_sec'] || 0); 289 | readsPerSec = fmtBytes(data['pgmap']['read_bytes_sec'] || 0); 290 | 291 | // *osd state* 292 | numOSDtotal = data['osdmap']['osdmap']['num_osds'] || 0; 293 | numOSDin = data['osdmap']['osdmap']['num_in_osds'] || 0; 294 | numOSDup = data['osdmap']['osdmap']['num_up_osds'] || 0; 295 | numOSDunhealthy = data['osdmap']['osdmap']['num_osds'] - data['osdmap']['osdmap']['num_up_osds'] || 0; 296 | unhealthyOSDDetails = data['osdmap']['details']; 297 | osdFull = data['osdmap']['osdmap']['full']; 298 | osdNearFull = data['osdmap']['osdmap']['nearfull']; 299 | 300 | var $parent = $("#unhealthy_osds").parent('.panel'); 301 | 302 | if (numOSDunhealthy == '0') { 303 | $parent.removeClass('panel-danger'); 304 | $parent.addClass('panel-success'); 305 | } else { 306 | $parent.removeClass('panel-success'); 307 | $parent.addClass('panel-danger'); 308 | } 309 | 310 | // *overall status* 311 | clusterStatusOverall = data['health']['overall_status']; 312 | if (data['health']['status'] || false) { 313 | clusterStatusOverall = data['health']['status']; 314 | } 315 | clusterHealthSummary = []; 316 | if (data['health']['checks'] || false) { 317 | $.each(data['health']['checks'], function(index, check) { 318 | clusterHealthSummary.push(check['summary']['message']); 319 | }); 320 | } 321 | else if (data['health']['summary'] || false) { 322 | $.each(data['health']['summary'], function(index, check) { 323 | clusterHealthSummary.push(check['summary']); 324 | }); 325 | } 326 | 327 | 328 | // *monitor state* 329 | monmapMons = data['monmap']['mons']; 330 | quorumMons = data['quorum_names']; 331 | // }}} 332 | 333 | // Update Content {{{ 334 | // ---------------------------------------------------------------- 335 | // update current throughput values 336 | if ('op_per_sec' in data['pgmap']) { 337 | $("#operations_per_second").html(data['pgmap']['op_per_sec'] || 0); 338 | $("#ops_container").show(); 339 | } else { 340 | if (!('write_op_per_sec' in data['pgmap'] || 'read_op_per_sec' in data['pgmap'])) { 341 | $("#operations_per_second").html(0); 342 | $("#ops_container").show(); 343 | } else { 344 | $("#ops_container").hide(); 345 | } 346 | } 347 | 348 | if ('write_op_per_sec' in data['pgmap'] || 'read_op_per_sec' in data['pgmap']) { 349 | $("#write_operations_per_second").html(data['pgmap']['write_op_per_sec'] || 0); 350 | $("#read_operations_per_second").html(data['pgmap']['read_op_per_sec'] || 0); 351 | $("#write_ops_container").show(); 352 | $("#read_ops_container").show(); 353 | } else { 354 | $("#read_ops_container").hide(); 355 | $("#write_ops_container").hide(); 356 | } 357 | 358 | // update storage capacity 359 | $("#utilization").dxCircularGauge($.extend(true, {}, gauge_options, { 360 | value: percentUsed 361 | })); 362 | $("#utilization_info").html(fmtBytes(bytesUsed) + " / " + fmtBytes(bytesTotal) + " (" + percentUsed + "%)"); 363 | 364 | // update placement group chart 365 | var chart = $("#pg_status").dxPieChart("instance"); 366 | chart.option('dataSource', pgsByState); 367 | 368 | $("#pg_status_info").html(numPgs + " placementgroups in cluster"); 369 | 370 | // update recovering status 371 | if (typeof(recoverBytes) != 'undefined') { 372 | $("#recovering_bytes").html(panel('warning', 'Recovering bytes / second', fmtBytes(recoverBytes))); 373 | } else { 374 | $("#recovering_bytes").empty(); 375 | } 376 | if (typeof(recoverKeys) != 'undefined') { 377 | $("#recovering_keys").html(panel('warning', 'Recovering keys / second', recoverKeys)); 378 | } else { 379 | $("#recovering_keys").empty(); 380 | } 381 | if (typeof(recoverObjects) != 'undefined') { 382 | $("#recovering_objects").html(panel('warning', 'Recovering objects / second', recoverObjects)); 383 | } else { 384 | $("#recovering_objects").empty(); 385 | } 386 | 387 | $("#write_bytes").html(writesPerSec); 388 | $("#read_bytes").html(readsPerSec); 389 | 390 | // update OSD states 391 | $("#num_osds").html(numOSDtotal); 392 | $("#num_in_osds").html(numOSDin); 393 | $("#num_up_osds").html(numOSDup); 394 | $("#unhealthy_osds").html(numOSDunhealthy); 395 | 396 | // update unhealthy osd popover if there are any unhealthy osds 397 | osdPopover = $('#unhealthy_osds').data('bs.popover'); 398 | osdPopover.options.content = ''; 399 | if (typeof(unhealthyOSDDetails) != 'undefined') { 400 | osdPopover.options.content += ''; 401 | $.each(unhealthyOSDDetails, function(index, osd_stats) { 402 | osdPopover.options.content += ''; 403 | osdPopover.options.content += ''; 404 | osdPopover.options.content += ''; 405 | osdPopover.options.content += ''; 406 | osdPopover.options.content += ''; 407 | }); 408 | osdPopover.options.content += '
' + osd_stats.status + '' + osd_stats.name + '' + osd_stats.host + '
'; 409 | } 410 | 411 | // update osd full / nearfull warnings 412 | $("#osd_warning").empty(); 413 | if (osdFull == "true") { 414 | $("#osd_warning").append(message('danger', 'OSD FULL ERROR')); 415 | } 416 | if (osdNearFull == "true") { 417 | $("#osd_warning").append(message('warning', 'OSD NEARFULL WARNING')); 418 | } 419 | 420 | // update overall cluster state 421 | $("#overall_status").empty(); 422 | $("#overall_status").append(message(ceph2bootstrap[clusterStatusOverall], 'Cluster Status:' + clusterStatusOverall)); 423 | 424 | // update overall cluster status details 425 | $("#overall_status").append('
    '); 426 | $.each(clusterHealthSummary, function(index, obj) { 427 | $("#overall_status").append('
  • ' + obj + '
  • '); 428 | }); 429 | $("#overall_status").append('
'); 430 | 431 | // update monitor status 432 | $("#monitor_status").empty(); 433 | $.each(monmapMons, function(index, mon) { 434 | health = 'HEALTH_ERR' 435 | if (quorumMons.includes(mon['name'])) { 436 | health = 'HEALTH_OK'; 437 | } 438 | msg = 'Monitor ' + mon['name'].toUpperCase() + ': ' + health; 439 | $("#monitor_status").append('
' + message(ceph2bootstrap[health], msg) + '
'); 440 | }); 441 | 442 | if ($('#graphite1').length > 0) { 443 | // update graphite graphs if available 444 | updatePlot('graphite'); 445 | } 446 | if ($('#influxdb1').length > 0) { 447 | // update influxdb graphs if available 448 | updatePlot('influxdb'); 449 | } 450 | // }}} 451 | } 452 | 453 | ajaxCall(window.location.pathname, callback); 454 | }; 455 | worker(); 456 | setInterval(worker, (refreshInterval * 1000)); 457 | // }}} 458 | }) 459 | 460 | // vim: set foldmethod=marker foldlevel=0 ts=4 sts=4 filetype=javascript fileencoding=utf-8 formatoptions+=ro expandtab: 461 | -------------------------------------------------------------------------------- /app/static/js/globalize.min.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crapworks/ceph-dash/00d354beeb9ae92ac75d789c598621048a550a96/app/static/js/globalize.min.js -------------------------------------------------------------------------------- /app/static/js/jquery.flot.byte.js: -------------------------------------------------------------------------------- 1 | (function ($) { 2 | "use strict"; 3 | 4 | var options = {}; 5 | 6 | //Round to nearby lower multiple of base 7 | function floorInBase(n, base) { 8 | return base * Math.floor(n / base); 9 | } 10 | 11 | function init(plot) { 12 | plot.hooks.processDatapoints.push(function (plot) { 13 | $.each(plot.getAxes(), function(axisName, axis) { 14 | var opts = axis.options; 15 | if (opts.mode === "byte" || opts.mode === "byteRate") { 16 | axis.tickGenerator = function (axis) { 17 | var returnTicks = [], 18 | tickSize = 2, 19 | delta = axis.delta, 20 | steps = 0, 21 | tickMin = 0, 22 | tickVal, 23 | tickCount = 0; 24 | 25 | //Set the reference for the formatter 26 | if (opts.mode === "byteRate") { 27 | axis.rate = true; 28 | } 29 | 30 | //Enforce maximum tick Decimals 31 | if (typeof opts.tickDecimals === "number") { 32 | axis.tickDecimals = opts.tickDecimals; 33 | } else { 34 | axis.tickDecimals = 2; 35 | } 36 | 37 | //Count the steps 38 | while (Math.abs(delta) >= 1024) { 39 | steps++; 40 | delta /= 1024; 41 | } 42 | 43 | //Set the tick size relative to the remaining delta 44 | while (tickSize <= 1024) { 45 | if (delta <= tickSize) { 46 | break; 47 | } 48 | tickSize *= 2; 49 | } 50 | 51 | //Tell flot the tickSize we've calculated 52 | if (typeof opts.minTickSize !== "undefined" && tickSize < opts.minTickSize) { 53 | axis.tickSize = opts.minTickSize; 54 | } else { 55 | axis.tickSize = tickSize * Math.pow(1024,steps); 56 | } 57 | 58 | //Calculate the new ticks 59 | tickMin = floorInBase(axis.min, axis.tickSize); 60 | do { 61 | tickVal = tickMin + (tickCount++) * axis.tickSize; 62 | returnTicks.push(tickVal); 63 | } while (tickVal < axis.max); 64 | 65 | return returnTicks; 66 | }; 67 | 68 | axis.tickFormatter = function(size, axis) { 69 | var ext, steps = 0; 70 | 71 | while (Math.abs(size) >= 1024) { 72 | steps++; 73 | size /= 1024; 74 | } 75 | 76 | 77 | switch (steps) { 78 | case 0: ext = " B"; break; 79 | case 1: ext = " KiB"; break; 80 | case 2: ext = " MiB"; break; 81 | case 3: ext = " GiB"; break; 82 | case 4: ext = " TiB"; break; 83 | case 5: ext = " PiB"; break; 84 | case 6: ext = " EiB"; break; 85 | case 7: ext = " ZiB"; break; 86 | case 8: ext = " YiB"; break; 87 | } 88 | 89 | 90 | if (typeof axis.rate !== "undefined") { 91 | ext += "/s"; 92 | } 93 | 94 | return (size.toFixed(axis.tickDecimals) + ext); 95 | }; 96 | } 97 | }); 98 | }); 99 | } 100 | 101 | $.plot.plugins.push({ 102 | init: init, 103 | options: options, 104 | name: "byte", 105 | version: "0.1" 106 | }); 107 | })(jQuery); -------------------------------------------------------------------------------- /app/static/js/jquery.flot.time.js: -------------------------------------------------------------------------------- 1 | /* Pretty handling of time axes. 2 | 3 | Copyright (c) 2007-2014 IOLA and Ole Laursen. 4 | Licensed under the MIT license. 5 | 6 | Set axis.mode to "time" to enable. See the section "Time series data" in 7 | API.txt for details. 8 | 9 | */ 10 | 11 | (function($) { 12 | 13 | var options = { 14 | xaxis: { 15 | timezone: null, // "browser" for local to the client or timezone for timezone-js 16 | timeformat: null, // format string to use 17 | twelveHourClock: false, // 12 or 24 time in time mode 18 | monthNames: null // list of names of months 19 | } 20 | }; 21 | 22 | // round to nearby lower multiple of base 23 | 24 | function floorInBase(n, base) { 25 | return base * Math.floor(n / base); 26 | } 27 | 28 | // Returns a string with the date d formatted according to fmt. 29 | // A subset of the Open Group's strftime format is supported. 30 | 31 | function formatDate(d, fmt, monthNames, dayNames) { 32 | 33 | if (typeof d.strftime == "function") { 34 | return d.strftime(fmt); 35 | } 36 | 37 | var leftPad = function(n, pad) { 38 | n = "" + n; 39 | pad = "" + (pad == null ? "0" : pad); 40 | return n.length == 1 ? pad + n : n; 41 | }; 42 | 43 | var r = []; 44 | var escape = false; 45 | var hours = d.getHours(); 46 | var isAM = hours < 12; 47 | 48 | if (monthNames == null) { 49 | monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; 50 | } 51 | 52 | if (dayNames == null) { 53 | dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; 54 | } 55 | 56 | var hours12; 57 | 58 | if (hours > 12) { 59 | hours12 = hours - 12; 60 | } else if (hours == 0) { 61 | hours12 = 12; 62 | } else { 63 | hours12 = hours; 64 | } 65 | 66 | for (var i = 0; i < fmt.length; ++i) { 67 | 68 | var c = fmt.charAt(i); 69 | 70 | if (escape) { 71 | switch (c) { 72 | case 'a': c = "" + dayNames[d.getDay()]; break; 73 | case 'b': c = "" + monthNames[d.getMonth()]; break; 74 | case 'd': c = leftPad(d.getDate()); break; 75 | case 'e': c = leftPad(d.getDate(), " "); break; 76 | case 'h': // For back-compat with 0.7; remove in 1.0 77 | case 'H': c = leftPad(hours); break; 78 | case 'I': c = leftPad(hours12); break; 79 | case 'l': c = leftPad(hours12, " "); break; 80 | case 'm': c = leftPad(d.getMonth() + 1); break; 81 | case 'M': c = leftPad(d.getMinutes()); break; 82 | // quarters not in Open Group's strftime specification 83 | case 'q': 84 | c = "" + (Math.floor(d.getMonth() / 3) + 1); break; 85 | case 'S': c = leftPad(d.getSeconds()); break; 86 | case 'y': c = leftPad(d.getFullYear() % 100); break; 87 | case 'Y': c = "" + d.getFullYear(); break; 88 | case 'p': c = (isAM) ? ("" + "am") : ("" + "pm"); break; 89 | case 'P': c = (isAM) ? ("" + "AM") : ("" + "PM"); break; 90 | case 'w': c = "" + d.getDay(); break; 91 | } 92 | r.push(c); 93 | escape = false; 94 | } else { 95 | if (c == "%") { 96 | escape = true; 97 | } else { 98 | r.push(c); 99 | } 100 | } 101 | } 102 | 103 | return r.join(""); 104 | } 105 | 106 | // To have a consistent view of time-based data independent of which time 107 | // zone the client happens to be in we need a date-like object independent 108 | // of time zones. This is done through a wrapper that only calls the UTC 109 | // versions of the accessor methods. 110 | 111 | function makeUtcWrapper(d) { 112 | 113 | function addProxyMethod(sourceObj, sourceMethod, targetObj, targetMethod) { 114 | sourceObj[sourceMethod] = function() { 115 | return targetObj[targetMethod].apply(targetObj, arguments); 116 | }; 117 | }; 118 | 119 | var utc = { 120 | date: d 121 | }; 122 | 123 | // support strftime, if found 124 | 125 | if (d.strftime != undefined) { 126 | addProxyMethod(utc, "strftime", d, "strftime"); 127 | } 128 | 129 | addProxyMethod(utc, "getTime", d, "getTime"); 130 | addProxyMethod(utc, "setTime", d, "setTime"); 131 | 132 | var props = ["Date", "Day", "FullYear", "Hours", "Milliseconds", "Minutes", "Month", "Seconds"]; 133 | 134 | for (var p = 0; p < props.length; p++) { 135 | addProxyMethod(utc, "get" + props[p], d, "getUTC" + props[p]); 136 | addProxyMethod(utc, "set" + props[p], d, "setUTC" + props[p]); 137 | } 138 | 139 | return utc; 140 | }; 141 | 142 | // select time zone strategy. This returns a date-like object tied to the 143 | // desired timezone 144 | 145 | function dateGenerator(ts, opts) { 146 | if (opts.timezone == "browser") { 147 | return new Date(ts); 148 | } else if (!opts.timezone || opts.timezone == "utc") { 149 | return makeUtcWrapper(new Date(ts)); 150 | } else if (typeof timezoneJS != "undefined" && typeof timezoneJS.Date != "undefined") { 151 | var d = new timezoneJS.Date(); 152 | // timezone-js is fickle, so be sure to set the time zone before 153 | // setting the time. 154 | d.setTimezone(opts.timezone); 155 | d.setTime(ts); 156 | return d; 157 | } else { 158 | return makeUtcWrapper(new Date(ts)); 159 | } 160 | } 161 | 162 | // map of app. size of time units in milliseconds 163 | 164 | var timeUnitSize = { 165 | "second": 1000, 166 | "minute": 60 * 1000, 167 | "hour": 60 * 60 * 1000, 168 | "day": 24 * 60 * 60 * 1000, 169 | "month": 30 * 24 * 60 * 60 * 1000, 170 | "quarter": 3 * 30 * 24 * 60 * 60 * 1000, 171 | "year": 365.2425 * 24 * 60 * 60 * 1000 172 | }; 173 | 174 | // the allowed tick sizes, after 1 year we use 175 | // an integer algorithm 176 | 177 | var baseSpec = [ 178 | [1, "second"], [2, "second"], [5, "second"], [10, "second"], 179 | [30, "second"], 180 | [1, "minute"], [2, "minute"], [5, "minute"], [10, "minute"], 181 | [30, "minute"], 182 | [1, "hour"], [2, "hour"], [4, "hour"], 183 | [8, "hour"], [12, "hour"], 184 | [1, "day"], [2, "day"], [3, "day"], 185 | [0.25, "month"], [0.5, "month"], [1, "month"], 186 | [2, "month"] 187 | ]; 188 | 189 | // we don't know which variant(s) we'll need yet, but generating both is 190 | // cheap 191 | 192 | var specMonths = baseSpec.concat([[3, "month"], [6, "month"], 193 | [1, "year"]]); 194 | var specQuarters = baseSpec.concat([[1, "quarter"], [2, "quarter"], 195 | [1, "year"]]); 196 | 197 | function init(plot) { 198 | plot.hooks.processOptions.push(function (plot, options) { 199 | $.each(plot.getAxes(), function(axisName, axis) { 200 | 201 | var opts = axis.options; 202 | 203 | if (opts.mode == "time") { 204 | axis.tickGenerator = function(axis) { 205 | 206 | var ticks = []; 207 | var d = dateGenerator(axis.min, opts); 208 | var minSize = 0; 209 | 210 | // make quarter use a possibility if quarters are 211 | // mentioned in either of these options 212 | 213 | var spec = (opts.tickSize && opts.tickSize[1] === 214 | "quarter") || 215 | (opts.minTickSize && opts.minTickSize[1] === 216 | "quarter") ? specQuarters : specMonths; 217 | 218 | if (opts.minTickSize != null) { 219 | if (typeof opts.tickSize == "number") { 220 | minSize = opts.tickSize; 221 | } else { 222 | minSize = opts.minTickSize[0] * timeUnitSize[opts.minTickSize[1]]; 223 | } 224 | } 225 | 226 | for (var i = 0; i < spec.length - 1; ++i) { 227 | if (axis.delta < (spec[i][0] * timeUnitSize[spec[i][1]] 228 | + spec[i + 1][0] * timeUnitSize[spec[i + 1][1]]) / 2 229 | && spec[i][0] * timeUnitSize[spec[i][1]] >= minSize) { 230 | break; 231 | } 232 | } 233 | 234 | var size = spec[i][0]; 235 | var unit = spec[i][1]; 236 | 237 | // special-case the possibility of several years 238 | 239 | if (unit == "year") { 240 | 241 | // if given a minTickSize in years, just use it, 242 | // ensuring that it's an integer 243 | 244 | if (opts.minTickSize != null && opts.minTickSize[1] == "year") { 245 | size = Math.floor(opts.minTickSize[0]); 246 | } else { 247 | 248 | var magn = Math.pow(10, Math.floor(Math.log(axis.delta / timeUnitSize.year) / Math.LN10)); 249 | var norm = (axis.delta / timeUnitSize.year) / magn; 250 | 251 | if (norm < 1.5) { 252 | size = 1; 253 | } else if (norm < 3) { 254 | size = 2; 255 | } else if (norm < 7.5) { 256 | size = 5; 257 | } else { 258 | size = 10; 259 | } 260 | 261 | size *= magn; 262 | } 263 | 264 | // minimum size for years is 1 265 | 266 | if (size < 1) { 267 | size = 1; 268 | } 269 | } 270 | 271 | axis.tickSize = opts.tickSize || [size, unit]; 272 | var tickSize = axis.tickSize[0]; 273 | unit = axis.tickSize[1]; 274 | 275 | var step = tickSize * timeUnitSize[unit]; 276 | 277 | if (unit == "second") { 278 | d.setSeconds(floorInBase(d.getSeconds(), tickSize)); 279 | } else if (unit == "minute") { 280 | d.setMinutes(floorInBase(d.getMinutes(), tickSize)); 281 | } else if (unit == "hour") { 282 | d.setHours(floorInBase(d.getHours(), tickSize)); 283 | } else if (unit == "month") { 284 | d.setMonth(floorInBase(d.getMonth(), tickSize)); 285 | } else if (unit == "quarter") { 286 | d.setMonth(3 * floorInBase(d.getMonth() / 3, 287 | tickSize)); 288 | } else if (unit == "year") { 289 | d.setFullYear(floorInBase(d.getFullYear(), tickSize)); 290 | } 291 | 292 | // reset smaller components 293 | 294 | d.setMilliseconds(0); 295 | 296 | if (step >= timeUnitSize.minute) { 297 | d.setSeconds(0); 298 | } 299 | if (step >= timeUnitSize.hour) { 300 | d.setMinutes(0); 301 | } 302 | if (step >= timeUnitSize.day) { 303 | d.setHours(0); 304 | } 305 | if (step >= timeUnitSize.day * 4) { 306 | d.setDate(1); 307 | } 308 | if (step >= timeUnitSize.month * 2) { 309 | d.setMonth(floorInBase(d.getMonth(), 3)); 310 | } 311 | if (step >= timeUnitSize.quarter * 2) { 312 | d.setMonth(floorInBase(d.getMonth(), 6)); 313 | } 314 | if (step >= timeUnitSize.year) { 315 | d.setMonth(0); 316 | } 317 | 318 | var carry = 0; 319 | var v = Number.NaN; 320 | var prev; 321 | 322 | do { 323 | 324 | prev = v; 325 | v = d.getTime(); 326 | ticks.push(v); 327 | 328 | if (unit == "month" || unit == "quarter") { 329 | if (tickSize < 1) { 330 | 331 | // a bit complicated - we'll divide the 332 | // month/quarter up but we need to take 333 | // care of fractions so we don't end up in 334 | // the middle of a day 335 | 336 | d.setDate(1); 337 | var start = d.getTime(); 338 | d.setMonth(d.getMonth() + 339 | (unit == "quarter" ? 3 : 1)); 340 | var end = d.getTime(); 341 | d.setTime(v + carry * timeUnitSize.hour + (end - start) * tickSize); 342 | carry = d.getHours(); 343 | d.setHours(0); 344 | } else { 345 | d.setMonth(d.getMonth() + 346 | tickSize * (unit == "quarter" ? 3 : 1)); 347 | } 348 | } else if (unit == "year") { 349 | d.setFullYear(d.getFullYear() + tickSize); 350 | } else { 351 | d.setTime(v + step); 352 | } 353 | } while (v < axis.max && v != prev); 354 | 355 | return ticks; 356 | }; 357 | 358 | axis.tickFormatter = function (v, axis) { 359 | 360 | var d = dateGenerator(v, axis.options); 361 | 362 | // first check global format 363 | 364 | if (opts.timeformat != null) { 365 | return formatDate(d, opts.timeformat, opts.monthNames, opts.dayNames); 366 | } 367 | 368 | // possibly use quarters if quarters are mentioned in 369 | // any of these places 370 | 371 | var useQuarters = (axis.options.tickSize && 372 | axis.options.tickSize[1] == "quarter") || 373 | (axis.options.minTickSize && 374 | axis.options.minTickSize[1] == "quarter"); 375 | 376 | var t = axis.tickSize[0] * timeUnitSize[axis.tickSize[1]]; 377 | var span = axis.max - axis.min; 378 | var suffix = (opts.twelveHourClock) ? " %p" : ""; 379 | var hourCode = (opts.twelveHourClock) ? "%I" : "%H"; 380 | var fmt; 381 | 382 | if (t < timeUnitSize.minute) { 383 | fmt = hourCode + ":%M:%S" + suffix; 384 | } else if (t < timeUnitSize.day) { 385 | if (span < 2 * timeUnitSize.day) { 386 | fmt = hourCode + ":%M" + suffix; 387 | } else { 388 | fmt = "%b %d " + hourCode + ":%M" + suffix; 389 | } 390 | } else if (t < timeUnitSize.month) { 391 | fmt = "%b %d"; 392 | } else if ((useQuarters && t < timeUnitSize.quarter) || 393 | (!useQuarters && t < timeUnitSize.year)) { 394 | if (span < timeUnitSize.year) { 395 | fmt = "%b"; 396 | } else { 397 | fmt = "%b %Y"; 398 | } 399 | } else if (useQuarters && t < timeUnitSize.year) { 400 | if (span < timeUnitSize.year) { 401 | fmt = "Q%q"; 402 | } else { 403 | fmt = "Q%q %Y"; 404 | } 405 | } else { 406 | fmt = "%Y"; 407 | } 408 | 409 | var rt = formatDate(d, fmt, opts.monthNames, opts.dayNames); 410 | 411 | return rt; 412 | }; 413 | } 414 | }); 415 | }); 416 | } 417 | 418 | $.plot.plugins.push({ 419 | init: init, 420 | options: options, 421 | name: 'time', 422 | version: '1.0' 423 | }); 424 | 425 | // Time-axis support used to be in Flot core, which exposed the 426 | // formatDate function on the plot object. Various plugins depend 427 | // on the function, so we need to re-expose it here. 428 | 429 | $.plot.formatDate = formatDate; 430 | $.plot.dateGenerator = dateGenerator; 431 | 432 | })(jQuery); 433 | -------------------------------------------------------------------------------- /app/static/js/jquery.flot.tooltip.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jquery.flot.tooltip 3 | * 4 | * description: easy-to-use tooltips for Flot charts 5 | * version: 0.8.4 6 | * authors: Krzysztof Urbas @krzysu [myviews.pl],Evan Steinkerchner @Roundaround 7 | * website: https://github.com/krzysu/flot.tooltip 8 | * 9 | * build on 2014-08-06 10 | * released under MIT License, 2012 11 | */ 12 | (function ($) { 13 | // plugin options, default values 14 | var defaultOptions = { 15 | tooltip: false, 16 | tooltipOpts: { 17 | id: "flotTip", 18 | content: "%s | X: %x | Y: %y", 19 | // allowed templates are: 20 | // %s -> series label, 21 | // %lx -> x axis label (requires flot-axislabels plugin https://github.com/markrcote/flot-axislabels), 22 | // %ly -> y axis label (requires flot-axislabels plugin https://github.com/markrcote/flot-axislabels), 23 | // %x -> X value, 24 | // %y -> Y value, 25 | // %x.2 -> precision of X value, 26 | // %p -> percent 27 | xDateFormat: null, 28 | yDateFormat: null, 29 | monthNames: null, 30 | dayNames: null, 31 | shifts: { 32 | x: 10, 33 | y: 20 34 | }, 35 | defaultTheme: true, 36 | lines: false, 37 | 38 | // callbacks 39 | onHover: function (flotItem, $tooltipEl) {}, 40 | 41 | $compat: false 42 | } 43 | }; 44 | 45 | // object 46 | var FlotTooltip = function (plot) { 47 | // variables 48 | this.tipPosition = {x: 0, y: 0}; 49 | 50 | this.init(plot); 51 | }; 52 | 53 | // main plugin function 54 | FlotTooltip.prototype.init = function (plot) { 55 | var that = this; 56 | 57 | // detect other flot plugins 58 | var plotPluginsLength = $.plot.plugins.length; 59 | this.plotPlugins = []; 60 | 61 | if (plotPluginsLength) { 62 | for (var p = 0; p < plotPluginsLength; p++) { 63 | this.plotPlugins.push($.plot.plugins[p].name); 64 | } 65 | } 66 | 67 | plot.hooks.bindEvents.push(function (plot, eventHolder) { 68 | 69 | // get plot options 70 | that.plotOptions = plot.getOptions(); 71 | 72 | // if not enabled return 73 | if (that.plotOptions.tooltip === false || typeof that.plotOptions.tooltip === 'undefined') return; 74 | 75 | // shortcut to access tooltip options 76 | that.tooltipOptions = that.plotOptions.tooltipOpts; 77 | 78 | if (that.tooltipOptions.$compat) { 79 | that.wfunc = 'width'; 80 | that.hfunc = 'height'; 81 | } else { 82 | that.wfunc = 'innerWidth'; 83 | that.hfunc = 'innerHeight'; 84 | } 85 | 86 | // create tooltip DOM element 87 | var $tip = that.getDomElement(); 88 | 89 | // bind event 90 | $( plot.getPlaceholder() ).bind("plothover", plothover); 91 | 92 | $(eventHolder).bind('mousemove', mouseMove); 93 | }); 94 | 95 | plot.hooks.shutdown.push(function (plot, eventHolder){ 96 | $(plot.getPlaceholder()).unbind("plothover", plothover); 97 | $(eventHolder).unbind("mousemove", mouseMove); 98 | }); 99 | 100 | function mouseMove(e){ 101 | var pos = {}; 102 | pos.x = e.pageX; 103 | pos.y = e.pageY; 104 | plot.setTooltipPosition(pos); 105 | } 106 | 107 | function plothover(event, pos, item) { 108 | // Simple distance formula. 109 | var lineDistance = function (p1x, p1y, p2x, p2y) { 110 | return Math.sqrt((p2x - p1x) * (p2x - p1x) + (p2y - p1y) * (p2y - p1y)); 111 | }; 112 | 113 | // Here is some voodoo magic for determining the distance to a line form a given point {x, y}. 114 | var dotLineLength = function (x, y, x0, y0, x1, y1, o) { 115 | if (o && !(o = 116 | function (x, y, x0, y0, x1, y1) { 117 | if (typeof x0 !== 'undefined') return { x: x0, y: y }; 118 | else if (typeof y0 !== 'undefined') return { x: x, y: y0 }; 119 | 120 | var left, 121 | tg = -1 / ((y1 - y0) / (x1 - x0)); 122 | 123 | return { 124 | x: left = (x1 * (x * tg - y + y0) + x0 * (x * -tg + y - y1)) / (tg * (x1 - x0) + y0 - y1), 125 | y: tg * left - tg * x + y 126 | }; 127 | } (x, y, x0, y0, x1, y1), 128 | o.x >= Math.min(x0, x1) && o.x <= Math.max(x0, x1) && o.y >= Math.min(y0, y1) && o.y <= Math.max(y0, y1)) 129 | ) { 130 | var l1 = lineDistance(x, y, x0, y0), l2 = lineDistance(x, y, x1, y1); 131 | return l1 > l2 ? l2 : l1; 132 | } else { 133 | var a = y0 - y1, b = x1 - x0, c = x0 * y1 - y0 * x1; 134 | return Math.abs(a * x + b * y + c) / Math.sqrt(a * a + b * b); 135 | } 136 | }; 137 | 138 | if (item) { 139 | plot.showTooltip(item, pos); 140 | } else if (that.plotOptions.series.lines.show && that.tooltipOptions.lines === true) { 141 | var maxDistance = that.plotOptions.grid.mouseActiveRadius; 142 | 143 | var closestTrace = { 144 | distance: maxDistance + 1 145 | }; 146 | 147 | $.each(plot.getData(), function (i, series) { 148 | var xBeforeIndex = 0, 149 | xAfterIndex = -1; 150 | 151 | // Our search here assumes our data is sorted via the x-axis. 152 | // TODO: Improve efficiency somehow - search smaller sets of data. 153 | for (var j = 1; j < series.data.length; j++) { 154 | if (series.data[j - 1][0] <= pos.x && series.data[j][0] >= pos.x) { 155 | xBeforeIndex = j - 1; 156 | xAfterIndex = j; 157 | } 158 | } 159 | 160 | if (xAfterIndex === -1) { 161 | plot.hideTooltip(); 162 | return; 163 | } 164 | 165 | var pointPrev = { x: series.data[xBeforeIndex][0], y: series.data[xBeforeIndex][1] }, 166 | pointNext = { x: series.data[xAfterIndex][0], y: series.data[xAfterIndex][1] }; 167 | 168 | var distToLine = dotLineLength(series.xaxis.p2c(pos.x), series.yaxis.p2c(pos.y), series.xaxis.p2c(pointPrev.x), 169 | series.yaxis.p2c(pointPrev.y), series.xaxis.p2c(pointNext.x), series.yaxis.p2c(pointNext.y), false); 170 | 171 | if (distToLine < closestTrace.distance) { 172 | 173 | var closestIndex = lineDistance(pointPrev.x, pointPrev.y, pos.x, pos.y) < 174 | lineDistance(pos.x, pos.y, pointNext.x, pointNext.y) ? xBeforeIndex : xAfterIndex; 175 | 176 | var pointSize = series.datapoints.pointsize; 177 | 178 | // Calculate the point on the line vertically closest to our cursor. 179 | var pointOnLine = [ 180 | pos.x, 181 | pointPrev.y + ((pointNext.y - pointPrev.y) * ((pos.x - pointPrev.x) / (pointNext.x - pointPrev.x))) 182 | ]; 183 | 184 | var item = { 185 | datapoint: pointOnLine, 186 | dataIndex: closestIndex, 187 | series: series, 188 | seriesIndex: i 189 | }; 190 | 191 | closestTrace = { 192 | distance: distToLine, 193 | item: item 194 | }; 195 | } 196 | }); 197 | 198 | if (closestTrace.distance < maxDistance + 1) 199 | plot.showTooltip(closestTrace.item, pos); 200 | else 201 | plot.hideTooltip(); 202 | } else { 203 | plot.hideTooltip(); 204 | } 205 | } 206 | 207 | // Quick little function for setting the tooltip position. 208 | plot.setTooltipPosition = function (pos) { 209 | var $tip = that.getDomElement(); 210 | 211 | var totalTipWidth = $tip.outerWidth() + that.tooltipOptions.shifts.x; 212 | var totalTipHeight = $tip.outerHeight() + that.tooltipOptions.shifts.y; 213 | if ((pos.x - $(window).scrollLeft()) > ($(window)[that.wfunc]() - totalTipWidth)) { 214 | pos.x -= totalTipWidth; 215 | } 216 | if ((pos.y - $(window).scrollTop()) > ($(window)[that.hfunc]() - totalTipHeight)) { 217 | pos.y -= totalTipHeight; 218 | } 219 | that.tipPosition.x = pos.x; 220 | that.tipPosition.y = pos.y; 221 | }; 222 | 223 | // Quick little function for showing the tooltip. 224 | plot.showTooltip = function (target, position) { 225 | var $tip = that.getDomElement(); 226 | 227 | // convert tooltip content template to real tipText 228 | var tipText = that.stringFormat(that.tooltipOptions.content, target); 229 | 230 | $tip.html(tipText); 231 | plot.setTooltipPosition({ x: position.pageX, y: position.pageY }); 232 | $tip.css({ 233 | left: that.tipPosition.x + that.tooltipOptions.shifts.x, 234 | top: that.tipPosition.y + that.tooltipOptions.shifts.y 235 | }).show(); 236 | 237 | // run callback 238 | if (typeof that.tooltipOptions.onHover === 'function') { 239 | that.tooltipOptions.onHover(target, $tip); 240 | } 241 | }; 242 | 243 | // Quick little function for hiding the tooltip. 244 | plot.hideTooltip = function () { 245 | that.getDomElement().hide().html(''); 246 | }; 247 | }; 248 | 249 | /** 250 | * get or create tooltip DOM element 251 | * @return jQuery object 252 | */ 253 | FlotTooltip.prototype.getDomElement = function () { 254 | var $tip = $('#' + this.tooltipOptions.id); 255 | 256 | if( $tip.length === 0 ){ 257 | $tip = $('
').attr('id', this.tooltipOptions.id); 258 | $tip.appendTo('body').hide().css({position: 'absolute'}); 259 | 260 | if(this.tooltipOptions.defaultTheme) { 261 | $tip.css({ 262 | 'background': '#fff', 263 | 'z-index': '1040', 264 | 'padding': '0.4em 0.6em', 265 | 'border-radius': '0.5em', 266 | 'font-size': '0.8em', 267 | 'border': '1px solid #111', 268 | 'display': 'none', 269 | 'white-space': 'nowrap' 270 | }); 271 | } 272 | } 273 | 274 | return $tip; 275 | }; 276 | 277 | /** 278 | * core function, create tooltip content 279 | * @param {string} content - template with tooltip content 280 | * @param {object} item - Flot item 281 | * @return {string} real tooltip content for current item 282 | */ 283 | FlotTooltip.prototype.stringFormat = function (content, item) { 284 | 285 | var percentPattern = /%p\.{0,1}(\d{0,})/; 286 | var seriesPattern = /%s/; 287 | var xLabelPattern = /%lx/; // requires flot-axislabels plugin https://github.com/markrcote/flot-axislabels, will be ignored if plugin isn't loaded 288 | var yLabelPattern = /%ly/; // requires flot-axislabels plugin https://github.com/markrcote/flot-axislabels, will be ignored if plugin isn't loaded 289 | var xPattern = /%x\.{0,1}(\d{0,})/; 290 | var yPattern = /%y\.{0,1}(\d{0,})/; 291 | var xPatternWithoutPrecision = "%x"; 292 | var yPatternWithoutPrecision = "%y"; 293 | var customTextPattern = "%ct"; 294 | 295 | var x, y, customText, p; 296 | 297 | // for threshold plugin we need to read data from different place 298 | if (typeof item.series.threshold !== "undefined") { 299 | x = item.datapoint[0]; 300 | y = item.datapoint[1]; 301 | customText = item.datapoint[2]; 302 | } else if (typeof item.series.lines !== "undefined" && item.series.lines.steps) { 303 | x = item.series.datapoints.points[item.dataIndex * 2]; 304 | y = item.series.datapoints.points[item.dataIndex * 2 + 1]; 305 | // TODO: where to find custom text in this variant? 306 | customText = ""; 307 | } else { 308 | x = item.series.data[item.dataIndex][0]; 309 | y = item.series.data[item.dataIndex][1]; 310 | customText = item.series.data[item.dataIndex][2]; 311 | } 312 | 313 | // I think this is only in case of threshold plugin 314 | if (item.series.label === null && item.series.originSeries) { 315 | item.series.label = item.series.originSeries.label; 316 | } 317 | 318 | // if it is a function callback get the content string 319 | if (typeof(content) === 'function') { 320 | content = content(item.series.label, x, y, item); 321 | } 322 | 323 | // percent match for pie charts and stacked percent 324 | if (typeof (item.series.percent) !== 'undefined') { 325 | p = item.series.percent; 326 | } else if (typeof (item.series.percents) !== 'undefined') { 327 | p = item.series.percents[item.dataIndex]; 328 | } 329 | if (typeof p === 'number') { 330 | content = this.adjustValPrecision(percentPattern, content, p); 331 | } 332 | 333 | // series match 334 | if (typeof(item.series.label) !== 'undefined') { 335 | content = content.replace(seriesPattern, item.series.label); 336 | } else { 337 | //remove %s if label is undefined 338 | content = content.replace(seriesPattern, ""); 339 | } 340 | 341 | // x axis label match 342 | if (this.hasAxisLabel('xaxis', item)) { 343 | content = content.replace(xLabelPattern, item.series.xaxis.options.axisLabel); 344 | } else { 345 | //remove %lx if axis label is undefined or axislabels plugin not present 346 | content = content.replace(xLabelPattern, ""); 347 | } 348 | 349 | // y axis label match 350 | if (this.hasAxisLabel('yaxis', item)) { 351 | content = content.replace(yLabelPattern, item.series.yaxis.options.axisLabel); 352 | } else { 353 | //remove %ly if axis label is undefined or axislabels plugin not present 354 | content = content.replace(yLabelPattern, ""); 355 | } 356 | 357 | // time mode axes with custom dateFormat 358 | if (this.isTimeMode('xaxis', item) && this.isXDateFormat(item)) { 359 | content = content.replace(xPattern, this.timestampToDate(x, this.tooltipOptions.xDateFormat, item.series.xaxis.options)); 360 | } 361 | if (this.isTimeMode('yaxis', item) && this.isYDateFormat(item)) { 362 | content = content.replace(yPattern, this.timestampToDate(y, this.tooltipOptions.yDateFormat, item.series.yaxis.options)); 363 | } 364 | 365 | // set precision if defined 366 | if (typeof x === 'number') { 367 | content = this.adjustValPrecision(xPattern, content, x); 368 | } 369 | if (typeof y === 'number') { 370 | content = this.adjustValPrecision(yPattern, content, y); 371 | } 372 | 373 | // change x from number to given label, if given 374 | if (typeof item.series.xaxis.ticks !== 'undefined') { 375 | 376 | var ticks; 377 | if (this.hasRotatedXAxisTicks(item)) { 378 | // xaxis.ticks will be an empty array if tickRotor is being used, but the values are available in rotatedTicks 379 | ticks = 'rotatedTicks'; 380 | } else { 381 | ticks = 'ticks'; 382 | } 383 | 384 | // see https://github.com/krzysu/flot.tooltip/issues/65 385 | var tickIndex = item.dataIndex + item.seriesIndex; 386 | 387 | if (item.series.xaxis[ticks].length > tickIndex && !this.isTimeMode('xaxis', item)) { 388 | var valueX = (this.isCategoriesMode('xaxis', item)) ? item.series.xaxis[ticks][tickIndex].label : item.series.xaxis[ticks][tickIndex].v; 389 | if (valueX === x) { 390 | content = content.replace(xPattern, item.series.xaxis[ticks][tickIndex].label); 391 | } 392 | } 393 | } 394 | 395 | // change y from number to given label, if given 396 | if (typeof item.series.yaxis.ticks !== 'undefined') { 397 | for (var index in item.series.yaxis.ticks) { 398 | if (item.series.yaxis.ticks.hasOwnProperty(index)) { 399 | var valueY = (this.isCategoriesMode('yaxis', item)) ? item.series.yaxis.ticks[index].label : item.series.yaxis.ticks[index].v; 400 | if (valueY === y) { 401 | content = content.replace(yPattern, item.series.yaxis.ticks[index].label); 402 | } 403 | } 404 | } 405 | } 406 | 407 | // if no value customization, use tickFormatter by default 408 | if (typeof item.series.xaxis.tickFormatter !== 'undefined') { 409 | //escape dollar 410 | content = content.replace(xPatternWithoutPrecision, item.series.xaxis.tickFormatter(x, item.series.xaxis).replace(/\$/g, '$$')); 411 | } 412 | if (typeof item.series.yaxis.tickFormatter !== 'undefined') { 413 | //escape dollar 414 | content = content.replace(yPatternWithoutPrecision, item.series.yaxis.tickFormatter(y, item.series.yaxis).replace(/\$/g, '$$')); 415 | } 416 | 417 | if (customText) 418 | content = content.replace(customTextPattern, customText); 419 | 420 | return content; 421 | }; 422 | 423 | // helpers just for readability 424 | FlotTooltip.prototype.isTimeMode = function (axisName, item) { 425 | return (typeof item.series[axisName].options.mode !== 'undefined' && item.series[axisName].options.mode === 'time'); 426 | }; 427 | 428 | FlotTooltip.prototype.isXDateFormat = function (item) { 429 | return (typeof this.tooltipOptions.xDateFormat !== 'undefined' && this.tooltipOptions.xDateFormat !== null); 430 | }; 431 | 432 | FlotTooltip.prototype.isYDateFormat = function (item) { 433 | return (typeof this.tooltipOptions.yDateFormat !== 'undefined' && this.tooltipOptions.yDateFormat !== null); 434 | }; 435 | 436 | FlotTooltip.prototype.isCategoriesMode = function (axisName, item) { 437 | return (typeof item.series[axisName].options.mode !== 'undefined' && item.series[axisName].options.mode === 'categories'); 438 | }; 439 | 440 | // 441 | FlotTooltip.prototype.timestampToDate = function (tmst, dateFormat, options) { 442 | var theDate = $.plot.dateGenerator(tmst, options); 443 | return $.plot.formatDate(theDate, dateFormat, this.tooltipOptions.monthNames, this.tooltipOptions.dayNames); 444 | }; 445 | 446 | // 447 | FlotTooltip.prototype.adjustValPrecision = function (pattern, content, value) { 448 | 449 | var precision; 450 | var matchResult = content.match(pattern); 451 | if( matchResult !== null ) { 452 | if(RegExp.$1 !== '') { 453 | precision = RegExp.$1; 454 | value = value.toFixed(precision); 455 | 456 | // only replace content if precision exists, in other case use thickformater 457 | content = content.replace(pattern, value); 458 | } 459 | } 460 | return content; 461 | }; 462 | 463 | // other plugins detection below 464 | 465 | // check if flot-axislabels plugin (https://github.com/markrcote/flot-axislabels) is used and that an axis label is given 466 | FlotTooltip.prototype.hasAxisLabel = function (axisName, item) { 467 | return ($.inArray(this.plotPlugins, 'axisLabels') !== -1 && typeof item.series[axisName].options.axisLabel !== 'undefined' && item.series[axisName].options.axisLabel.length > 0); 468 | }; 469 | 470 | // check whether flot-tickRotor, a plugin which allows rotation of X-axis ticks, is being used 471 | FlotTooltip.prototype.hasRotatedXAxisTicks = function (item) { 472 | return ($.inArray(this.plotPlugins, 'tickRotor') !== -1 && typeof item.series.xaxis.rotatedTicks !== 'undefined'); 473 | }; 474 | 475 | // 476 | var init = function (plot) { 477 | new FlotTooltip(plot); 478 | }; 479 | 480 | // define Flot plugin 481 | $.plot.plugins.push({ 482 | init: init, 483 | options: defaultOptions, 484 | name: 'tooltip', 485 | version: '0.8.4' 486 | }); 487 | 488 | })(jQuery); 489 | -------------------------------------------------------------------------------- /app/templates/status.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Ceph Dashboard 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |

Ceph Dashboard

15 |

16 | 17 | 18 | 19 | 20 |

21 |
22 | 23 | 24 |
25 |
26 |

27 | Ceph Cluster Overall Status 28 |

29 |
30 | 31 |
32 |
33 | 34 | 35 | 36 |
37 |
38 |

39 | Ceph Cluster Monitor Status 40 |

41 |
42 |
43 | 44 |
45 |
46 |
47 | 48 | 49 | 50 |
51 |
52 |

53 | Ceph Cluster OSD Status 54 |

55 |
56 |
57 | 58 | 59 |
60 | 61 |
62 |
63 |
64 |
Total
65 | 66 |
67 |
68 |
69 | 70 |
71 |
72 |
In
73 | 74 |
75 |
76 |
77 | 78 |
79 |
80 |
Up
81 | 82 |
83 |
84 |
85 | 86 |
87 |
88 |
Unhealthy
89 | 90 |
91 |
92 |
93 |
94 | 95 |
96 |
97 | 98 | 99 | 100 | {% if "graphite" in config or "influxdb" in config %} 101 |
102 |
103 |

104 | Ceph Cluster Performance Metrics 105 |

106 |
107 |
108 |
109 | {% for index in config.get('graphite', {}).get('metrics', []) %} 110 |
111 |
112 |
113 | {% endfor %} 114 | {% for index in config.get('influxdb', {}).get('metrics', []) %} 115 |
116 |
117 |
118 | {% endfor %} 119 |
120 |
121 |
122 | {% endif %} 123 | 124 | 125 | 126 | 127 |
128 |
129 |

130 | Ceph Cluster Placement Group Status 131 |

132 |
133 |
134 | 135 |
136 | 137 |
138 |
139 |
Storage
140 | 141 |
142 | 149 |
150 |
151 | 152 |
153 |
154 |
Write / second
155 |
156 | 157 |

158 |
159 |
160 | 161 |
162 |
Read / second
163 |
164 | 165 |

166 |
167 |
168 | 169 |
170 |
Operations / second
171 |
172 | 173 |

174 |
175 |
176 |
177 |
178 |
179 |
Write ops / second
180 |
181 | 182 |

183 |
184 |
185 |
186 |
187 |
188 |
Read ops / second
189 |
190 | 191 |

192 |
193 |
194 |
195 |
196 |
197 | 198 |
199 |
200 |
PG Status
201 | 202 |
203 | 210 |
211 |
212 | 213 |
214 | 215 | 216 |
217 |
218 |
219 |
220 |
221 | 222 | 223 |
224 |
225 | 226 | 227 |
228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | -------------------------------------------------------------------------------- /ceph-dash.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from app import app 5 | 6 | app.run(host='0.0.0.0', debug=True) 7 | -------------------------------------------------------------------------------- /config.graphite.json: -------------------------------------------------------------------------------- 1 | { 2 | "ceph_config": "/etc/ceph/ceph.conf", 3 | "graphite": { 4 | "url": "http://graphite.server.org", 5 | "metrics": [ 6 | { 7 | "targets": [ 8 | "avg(monitoring.ceph-mon*.ceph_overall_cluster_status.read_bytes_sec)", 9 | "avg(monitoring.ceph-mon*.ceph_overall_cluster_status.write_bytes_sec)" 10 | ], 11 | "labels": [ "Read", "Write" ], 12 | "from": "-2h", 13 | "mode": "byteRate" 14 | }, 15 | { 16 | "targets": [ 17 | "avg(monitoring.ceph-mon*.ceph_overall_cluster_status.op_per_sec)" 18 | ], 19 | "labels": [ "IOPS" ], 20 | "colors": [ "#5bc0de" ], 21 | "from": "-2h" 22 | } 23 | ] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /config.influxdb.json: -------------------------------------------------------------------------------- 1 | { 2 | "ceph_config": "/etc/ceph/ceph.conf", 3 | "influxdb": { 4 | "uri": "influxdb://myinfluxdb.server.com:8086/mydatabase", 5 | "metrics": [ 6 | { 7 | "queries" : [ 8 | "SELECT mean(value) FROM \"ceph.cluster\" WHERE hostname =~ /ceph-mon-.*/ AND type = 'read_bytes_sec' AND time > now() - 15m GROUP BY time(10s);", 9 | "SELECT mean(value) FROM \"ceph.cluster\" WHERE hostname =~ /ceph-mon-.*/ AND type = 'write_bytes_sec' AND time > now() - 15m GROUP BY time(10s);" 10 | ], 11 | "labels": [ "Read", "Write" ], 12 | "mode": "byteRate" 13 | }, 14 | { 15 | "queries" : [ 16 | "SELECT mean(value) FROM \"ceph.cluster\" WHERE hostname =~ /ceph-mon-.*/ AND type = 'op_per_sec' AND time > now() - 15m GROUP BY time(10s);" 17 | ], 18 | "labels": [ "IOPS" ], 19 | "colors": [ "#5bc0de" ] 20 | } 21 | ] 22 | } 23 | } 24 | 25 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "ceph_config": "/etc/ceph/ceph.conf" 3 | } 4 | -------------------------------------------------------------------------------- /contrib/apache/cephdash: -------------------------------------------------------------------------------- 1 | 2 | ServerName foo.bar.de 3 | 4 | RewriteEngine On 5 | RewriteCond %{REQUEST_URI} !^/server-status 6 | RewriteRule ^/?(.*) https://%{HTTP_HOST}/$1 [R,L] 7 | 8 | 9 | 10 | ServerName foo.bar.de 11 | 12 | WSGIDaemonProcess cephdash user=www-data group=www-data processes=1 threads=5 13 | WSGIScriptAlias / /opt/ceph-dash/contrib/wsgi/cephdash.wsgi 14 | WSGIPassAuthorization On 15 | 16 | SSLEngine on 17 | SSLCertificateFile /etc/apache2/ssl/ssl.crt 18 | SSLCertificateKeyFile /etc/apache2/ssl/ssl.key 19 | 20 | 21 | WSGIProcessGroup cephdash 22 | WSGIApplicationGroup %{GLOBAL} 23 | Order deny,allow 24 | Allow from all 25 | 26 | 27 | -------------------------------------------------------------------------------- /contrib/docker/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | CEPHCONFIG="/etc/ceph/ceph.conf" 4 | CEPHKEYRING="/etc/ceph/keyring" 5 | 6 | echo "# REQUIRED ENVIRONMENT VARIABLES" 7 | echo "* CEPHMONS (comma separated list of ceph monitor ip addresses)" 8 | echo "* KEYRING (full keyring to deploy in docker container)" 9 | echo "* Or KEYRING_FILE (path to file containing keyring to deploy in docker container)" 10 | echo "" 11 | echo "# OPTIONAL ENVIRONMENT VARIABLES" 12 | echo "* CONFIG (path to ceph-dash config file" 13 | echo "* NAME (keyring name to use)" 14 | echo "* ID (keyting id to use)" 15 | echo "" 16 | 17 | CEPHMONS=$(echo ${CEPHMONS} | sed 's/[a-z]\+=//g') 18 | CEPHMONS=$(echo ${CEPHMONS} | sed 's/rook-ceph-mon[0-9]\+=//g') 19 | 20 | echo -e "[global]\nmon host = ${CEPHMONS}" > ${CEPHCONFIG} 21 | 22 | echo "# CEPH STATUS" 23 | ceph -s 24 | 25 | export CEPHDASH_CEPHCONFIG="${CEPHCONFIG}" 26 | export CEPHDASH_KEYRING="${CEPHKEYRING}" 27 | 28 | if [ -n "${KEYRING_FILE}" ]; then 29 | cat ${KEYRING_FILE} > ${CEPHKEYRING} 30 | else 31 | echo "${KEYRING}" > ${CEPHKEYRING} 32 | fi 33 | 34 | if [ -n "${CONFIG}" ]; then 35 | export CEPHDASH_CONFIGFILE="${CONFIG}" 36 | fi 37 | 38 | if [ -n "${NAME}" ]; then 39 | export CEPHDASH_NAME="${NAME}" 40 | fi 41 | 42 | if [ -n "${ID}" ]; then 43 | export CEPHDASH_ID="${ID}" 44 | fi 45 | 46 | python $* 47 | -------------------------------------------------------------------------------- /contrib/nginx/cephdash.conf: -------------------------------------------------------------------------------- 1 | upstream uwsgi { 2 | server unix:///var/run/ceph-dash.sock; 3 | } 4 | 5 | server { 6 | listen 80; 7 | #server_name foo.bar.de # Put your machine's ip or FQDN here 8 | charset utf-8; 9 | # Send all non media requests to uwsgi 10 | location / { 11 | uwsgi_pass uwsgi; 12 | include /opt/ceph-dash/uwsgi_params; # the uwsgi_params file you installed 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /contrib/nginx/uwsgi.ini: -------------------------------------------------------------------------------- 1 | # uwsgi.ini file 2 | [uwsgi] 3 | 4 | # ceph-dash-related settings 5 | # the base directory (full path) 6 | chdir = /opt/ceph-dash 7 | # ceph-dash's wsgi file 8 | wsgi-file = cephdash.wsgi 9 | enable-threads = true 10 | # process-related settings 11 | # master 12 | master = true 13 | # maximum number of worker processes 14 | processes = 4 15 | # the socket (use the full path to be safe 16 | socket = /var/run/ceph-dash.sock 17 | # with appropriate permissions 18 | chmod-socket = 664 19 | # clear environment on exit 20 | vacuum = true 21 | uid = www-data 22 | gid = www-data 23 | # Run in the background as a daemon 24 | daemonize = /var/log/uwsgi/ceph-dash.log 25 | -------------------------------------------------------------------------------- /contrib/nginx/uwsgi_params: -------------------------------------------------------------------------------- 1 | uwsgi_param QUERY_STRING $query_string; 2 | uwsgi_param REQUEST_METHOD $request_method; 3 | uwsgi_param CONTENT_TYPE $content_type; 4 | uwsgi_param CONTENT_LENGTH $content_length; 5 | 6 | uwsgi_param REQUEST_URI $request_uri; 7 | uwsgi_param PATH_INFO $document_uri; 8 | uwsgi_param DOCUMENT_ROOT $document_root; 9 | uwsgi_param SERVER_PROTOCOL $server_protocol; 10 | uwsgi_param HTTPS $https if_not_empty; 11 | 12 | uwsgi_param REMOTE_ADDR $remote_addr; 13 | uwsgi_param REMOTE_PORT $remote_port; 14 | uwsgi_param SERVER_PORT $server_port; 15 | uwsgi_param SERVER_NAME $server_name; 16 | -------------------------------------------------------------------------------- /contrib/rook/ceph-dash-ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Ingress 3 | metadata: 4 | name: ceph-dash 5 | namespace: rook-ceph 6 | spec: 7 | backend: 8 | serviceName: ceph-dash 9 | servicePort: 5000 10 | -------------------------------------------------------------------------------- /contrib/rook/ceph-dash-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: ceph-dash 5 | namespace: rook-ceph 6 | spec: 7 | type: NodePort 8 | selector: 9 | app: ceph-dash 10 | ports: 11 | - protocol: TCP 12 | port: 5000 13 | targetPort: 5000 14 | -------------------------------------------------------------------------------- /contrib/rook/ceph-dash.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: ceph-dash 5 | namespace: rook-ceph 6 | labels: 7 | app: ceph-dash 8 | spec: 9 | selector: 10 | matchLabels: 11 | app: ceph-dash 12 | template: 13 | metadata: 14 | labels: 15 | app: ceph-dash 16 | spec: 17 | containers: 18 | - image: crapworks/ceph-dash:v1.5 19 | name: ceph-dash 20 | ports: 21 | - containerPort: 5000 22 | protocol: TCP 23 | resources: 24 | requests: 25 | memory: "300Mi" 26 | cpu: "100m" 27 | limits: 28 | memory: "500Mi" 29 | cpu: "200m" 30 | env: 31 | - name: CEPHMONS 32 | valueFrom: 33 | configMapKeyRef: 34 | name: rook-ceph-mon-endpoints 35 | key: data 36 | - name: KEYRING 37 | valueFrom: 38 | secretKeyRef: 39 | name: rook-ceph-mons-keyring 40 | key: keyring 41 | -------------------------------------------------------------------------------- /contrib/wsgi/cephdash.wsgi: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | 4 | # insert the path where the application resided to 5 | # pythons search path 6 | sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) 7 | 8 | # application holds the actual wsgi application 9 | from app import app as application 10 | -------------------------------------------------------------------------------- /debian/ceph-dash.install: -------------------------------------------------------------------------------- 1 | config.json /opt/ceph-dash 2 | ceph-dash.py /opt/ceph-dash 3 | app /opt/ceph-dash 4 | contrib/wsgi/cephdash.wsgi /opt/ceph-dash 5 | contrib/apache/cephdash /etc/apache2/sites-available 6 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | ceph-dash (0.1) UNRELEASED; urgency=low 2 | 3 | * Initial release: Does anyone read this? 4 | 5 | -- Christian Eichelmann Thu, 07 Feb 2013 10:33:30 +0100 6 | -------------------------------------------------------------------------------- /debian/compat: -------------------------------------------------------------------------------- 1 | 8 2 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: ceph-dash 2 | Section: misc 3 | Priority: extra 4 | Maintainer: Christian Eichelmann 5 | Build-Depends: debhelper (>= 8.0.0) 6 | Standards-Version: 3.9.2 7 | Vcs-Git: git://github.com/crapworks/ceph-dash.git 8 | Vcs-Browser: http://github.com/crapworks/ceph-dash 9 | 10 | Package: ceph-dash 11 | Architecture: all 12 | Depends: python, python-flask, python-ceph, debconf 13 | Pre-Depends: debconf 14 | Description: Dashboard to view ceph cluster status 15 | Dashboard to view ceph cluster status 16 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | Format: http://dep.debian.net/deps/dep5 2 | Upstream-Name: ceph-dash 3 | Source: https://github.com/crapworks/ceph-dash 4 | 5 | Files: * 6 | Copyright: 2014 Christian Eichelmann 7 | License: BSD License 8 | 9 | Redistribution and use in source and binary forms, with or without 10 | modification, are permitted provided that the following conditions are met: 11 | 12 | * Redistributions of source code must retain the above copyright notice, this 13 | list of conditions and the following disclaimer. 14 | 15 | * Redistributions in binary form must reproduce the above copyright notice, 16 | this list of conditions and the following disclaimer in the documentation 17 | and/or other materials provided with the distribution. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | # -*- makefile -*- 3 | # Sample debian/rules that uses debhelper. 4 | # This file was originally written by Joey Hess and Craig Small. 5 | # As a special exception, when this file is copied by dh-make into a 6 | # dh-make output file, you may use that output file without restriction. 7 | # This special exception was added by Craig Small in version 0.37 of dh-make. 8 | 9 | # Uncomment this to turn on verbose mode. 10 | #export DH_VERBOSE=1 11 | 12 | %: 13 | dh $@ 14 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (native) 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | flask>=0.10 2 | python-cephlibs 3 | -------------------------------------------------------------------------------- /screenshots/ceph-dash-content-warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crapworks/ceph-dash/00d354beeb9ae92ac75d789c598621048a550a96/screenshots/ceph-dash-content-warning.png -------------------------------------------------------------------------------- /screenshots/ceph-dash-graphite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crapworks/ceph-dash/00d354beeb9ae92ac75d789c598621048a550a96/screenshots/ceph-dash-graphite.png -------------------------------------------------------------------------------- /screenshots/ceph-dash-influxdb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crapworks/ceph-dash/00d354beeb9ae92ac75d789c598621048a550a96/screenshots/ceph-dash-influxdb.png -------------------------------------------------------------------------------- /screenshots/ceph-dash-popover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crapworks/ceph-dash/00d354beeb9ae92ac75d789c598621048a550a96/screenshots/ceph-dash-popover.png -------------------------------------------------------------------------------- /screenshots/ceph-dash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crapworks/ceph-dash/00d354beeb9ae92ac75d789c598621048a550a96/screenshots/ceph-dash.png -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | influxdb>=2.9.0 2 | nose 3 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crapworks/ceph-dash/00d354beeb9ae92ac75d789c598621048a550a96/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_config.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from app import app 3 | 4 | 5 | class TestConfig(unittest.TestCase): 6 | def setUp(self): 7 | self.app = app.test_client() 8 | self.app.testing = True 9 | 10 | def tearDown(self): 11 | pass 12 | 13 | def test_config(self): 14 | config = app.config['USER_CONFIG'] 15 | self.assertTrue(isinstance(config, dict)) 16 | self.assertTrue('ceph_config' in config) 17 | 18 | 19 | if __name__ == '__main__': 20 | unittest.main() 21 | -------------------------------------------------------------------------------- /tests/test_dashboard.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from app.dashboard.views import find_host_for_osd 3 | from app.dashboard.views import get_unhealthy_osd_details 4 | 5 | 6 | CEPH_OSD_TREE = { 7 | 'nodes': [ 8 | { 9 | 'type': 'host', 10 | 'name': 'testhost01', 11 | 'children': [ 12 | 1, 13 | 2, 14 | 3 15 | ] 16 | }, 17 | { 18 | 'type': 'osd', 19 | 'name': 'osd.1', 20 | 'id': 1, 21 | 'exists': 1, 22 | 'status': 'down', 23 | 'reweight': 1.0 24 | }, 25 | { 26 | 'type': 'osd', 27 | 'name': 'osd.2', 28 | 'id': 2, 29 | 'exists': 1, 30 | 'status': 'up', 31 | 'reweight': 0.0 32 | }, 33 | { 34 | 'type': 'osd', 35 | 'name': 'osd.3', 36 | 'id': 3, 37 | 'exists': 1, 38 | 'status': 'up', 39 | 'reweight': 1.0 40 | }, 41 | { 42 | 'type': 'osd', 43 | 'name': 'osd.4', 44 | 'id': 4, 45 | 'exists': 0, 46 | 'status': 'up', 47 | 'reweight': 1.0 48 | } 49 | ] 50 | } 51 | 52 | 53 | class TestDashboard(unittest.TestCase): 54 | def setUp(self): 55 | pass 56 | 57 | def tearDown(self): 58 | pass 59 | 60 | def test_find_host(self): 61 | result = find_host_for_osd(0, CEPH_OSD_TREE) 62 | self.assertEqual(result, 'unknown') 63 | result = find_host_for_osd(1, CEPH_OSD_TREE) 64 | self.assertEqual(result, 'testhost01') 65 | 66 | def test_unhealthy_osd(self): 67 | result = get_unhealthy_osd_details(CEPH_OSD_TREE) 68 | self.assertTrue(isinstance(result, list)) 69 | self.assertEqual(len(result), 2) 70 | 71 | def test_unhealthy_osd_detail(self): 72 | result = get_unhealthy_osd_details(CEPH_OSD_TREE) 73 | for item in result: 74 | if item['name'] == 'osd.1': 75 | self.assertEqual(item['status'], 'down') 76 | if item['name'] == 'osd.2': 77 | self.assertEqual(item['status'], 'out') 78 | 79 | 80 | if __name__ == '__main__': 81 | unittest.main() 82 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | minversion = 1.6 3 | skipsdist = True 4 | envlist = flake8, py27 5 | 6 | [testenv] 7 | setenv = VIRTUAL_ENV={envdir} 8 | deps = -r{toxinidir}/requirements.txt 9 | -r{toxinidir}/test-requirements.txt 10 | commands = 11 | nosetests 12 | 13 | [testenv:flake8] 14 | commands = flake8 15 | deps = flake8 16 | 17 | [flake8] 18 | max-line-length = 99 19 | builtins = unicode 20 | exclude = .venv,.tox,dist,doc,build,*.egg 21 | --------------------------------------------------------------------------------