return format_number(n, 1024, ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB']);
};
+ rivets.formatters.block_health_color = function(rbd_mirroring) {
+ if (rbd_mirroring.errors > 0) {
+ return "color: #ff0000";
+ } else if (rbd_mirroring.warnings > 0) {
+ return "color: #ffc200";
+ }
+ return "";
+ };
+
rivets.formatters.short_version = function(version) {
// Expect "ceph version 1.2.3-g9asdasd (as98d7a0s8d7)"
var result = /ceph version\s+([^ ]+)\s+\(.+\)/.exec(version);
return val.length;
}
+ rivets.formatters.hide_count_box = function(value) {
+ value = +value
+ return (isNaN(value) || value == 0)
+ };
+
rivets.bind($("#health"), toplevel_data);
rivets.bind($("section.sidebar"), toplevel_data);
setTimeout(refresh, refresh_interval);
</li>
<li class="treeview{%if path_info.startswith('/rbd')%} active{%endif%}">
<a href="#">
- <i class="fa fa-hdd-o"></i> <span>Block</span>
+ <i class="fa fa-hdd-o" rv-style="rbd_mirroring | block_health_color"></i> <span>Block</span>
<span class="pull-right-container">
<i class="fa fa-angle-left pull-right"></i>
</span>
</a>
<ul class="treeview-menu menu-open">
+ <li>
+ <a href="/rbd_mirroring">
+ <i class="fa fa-exchange"></i> Mirroring
+ <span class="pull-right-container">
+ <small rv-hide="rbd_mirroring.warnings | hide_count_box" class="label pull-right bg-yellow">{rbd_mirroring.warnings}</small>
+ <small rv-hide="rbd_mirroring.errors | hide_count_box" class="label pull-right bg-red">{rbd_mirroring.errors}</small>
+ </span>
+ </a>
+ </li>
<li class="treeview{%if path_info.startswith('/rbd_pool')%} active menu-open{%endif%}">
<a href="#">
<i class="fa fa-dot-circle-o"></i> <span>Pools</span>
PgSummary, Health, MonStatus
import rados
+import rbd_mirroring
from rbd_ls import RbdLs, RbdPoolLs
from cephfs_clients import CephFSClients
-
log = logging.getLogger("dashboard")
# pools
self.rbd_pool_ls = RbdPoolLs(self)
+ # Stateful instance of RbdMirroring, hold cached results.
+ self.rbd_mirroring = rbd_mirroring.Controller(self)
+
# Stateful instances of CephFSClients, hold cached results. Key to
# dict is FSCID
self.cephfs_clients = {}
for name in data
], key=lambda k: k['name'])
+ status, rbd_mirroring = global_instance().rbd_mirroring.toplevel.get()
+ if rbd_mirroring is None:
+ log.warning("Failed to get RBD mirroring summary")
+ rbd_mirroring = {}
+
fsmap = global_instance().get_sync_object(FsMap)
filesystems = [
{
return {
'rbd_pools': rbd_pools,
+ 'rbd_mirroring': rbd_mirroring,
'health_status': self._health_data()['status'],
'filesystems': filesystems
}
def rbd_pool_data(self, pool_name):
return self._rbd_pool(pool_name)
+ def _rbd_mirroring(self):
+ status, data = global_instance().rbd_mirroring.content_data.get()
+ if data is None:
+ log.warning("Failed to get RBD mirroring status")
+ return {}
+ return data
+
+ @cherrypy.expose
+ def rbd_mirroring(self):
+ template = env.get_template("rbd_mirroring.html")
+
+ toplevel_data = self._toplevel_data()
+ content_data = self._rbd_mirroring()
+
+ return template.render(
+ ceph_version=global_instance().version,
+ path_info=cherrypy.request.path_info,
+ toplevel_data=json.dumps(toplevel_data, indent=2),
+ content_data=json.dumps(content_data, indent=2)
+ )
+
+ @cherrypy.expose
+ @cherrypy.tools.json_out()
+ def rbd_mirroring_data(self):
+ return self._rbd_mirroring()
+
@cherrypy.expose
def health(self):
template = env.get_template("health.html")
--- /dev/null
+{% extends "base.html" %}
+
+{% block content %}
+
+<script>
+ $(document).ready(function(){
+ // Pre-populated initial data at page load
+ var content_data = {{ content_data }};
+
+ var refresh = function() {
+ $.get("/rbd_mirroring_data", function(data) {
+ _.extend(content_data, data);
+ setTimeout(refresh, 30000);
+ });
+ };
+
+ console.log(content_data);
+
+ rivets.formatters.mirror_health_color = function(status_str) {
+ if (status_str == "warning") {
+ return "label label-warning";
+ } else if (status_str == "error") {
+ return "label label-danger";
+ } else if (status_str == "success") {
+ return "label label-success";
+ }
+ return "label label-info";
+ }
+ rivets.formatters.sync_progress_bar = function(progress){
+ var ratio = progress / 100.0;
+ return "width: " + Math.round(ratio * 100).toString() + "%";
+ };
+
+ rivets.bind($("div#content"), content_data);
+ setTimeout(refresh, 30000);
+
+ var table_ids = ["daemons", "pools"];
+ for (var i = 0; i < table_ids.length; ++i) {
+ $('#' + table_ids[i]).DataTable({
+ 'paging' : true,
+ 'pageLength' : 5,
+ 'lengthChange': false,
+ 'info' : false,
+ 'autoWidth' : false,
+ 'searching' : false
+ });
+ }
+
+ var table_ids = ["image_errors", "image_syncing", "image_ready"];
+ for (var i = 0; i < table_ids.length; ++i) {
+ $('#' + table_ids[i]).DataTable({
+ 'paging' : true,
+ 'pageLength' : 10,
+ 'lengthChange': false,
+ 'searching' : true,
+ 'ordering' : true,
+ 'info' : false
+ });
+ };
+ });
+</script>
+
+
+<section class="content-header">
+ <h1>
+ Block Mirroring
+ </h1>
+</section>
+
+<section class="content">
+ <div class="row">
+ <div class="col-sm-6">
+ <div class="box">
+ <div class="box-header">
+ <h3 class="box-title">Daemons</h3>
+ </div>
+ <div class="box-body">
+ <table id="daemons" class="table table-condensed">
+ <thead>
+ <tr>
+ <th>ID</th>
+ <th>Instance</th>
+ <th>Hostname</th>
+ <th>Version</th>
+ <th>Health</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr rv-each-daemon="daemons">
+ <td>{daemon.id}</td>
+ <td>{daemon.instance_id}</td>
+ <td>{daemon.server_hostname}</td>
+ <td>{daemon.version | short_version}</td>
+ <td><span rv-class="daemon.health_color | mirror_health_color">{daemon.health}</span></td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
+ </div>
+
+ <div class="col-sm-6">
+ <div class="box">
+ <div class="box-header">
+ <h3 class="box-title">Pools</h3>
+ </div>
+ <div class="box-body">
+ <table id="pools" class="table table-condensed">
+ <thead>
+ <tr>
+ <th>Name</th>
+ <th>Mode</th>
+ <th>Leader</th>
+ <th># Local</th>
+ <th># Remote</th>
+ <th>Health</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr rv-each-pool="pools">
+ <td>{pool.name}</td>
+ <td>{pool.mirror_mode}</td>
+ <td>{pool.leader_id}</td>
+ <td>{pool.image_local_count}</td>
+ <td>{pool.image_remote_count}</td>
+ <td><span rv-class="pool.health_color | mirror_health_color">{pool.health}</span></td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="box">
+ <div class="box-header">
+ <h3 class="box-title">Images</h3>
+ </div>
+ <div class="box-body">
+ <div class="nav-tabs-custom">
+ <ul class="nav nav-tabs">
+ <li class="active"><a href="#tab_1" data-toggle="tab">Issues</a></li>
+ <li><a href="#tab_2" data-toggle="tab">Syncing</a></li>
+ <li><a href="#tab_3" data-toggle="tab">Ready</a></li>
+ </ul>
+ <div class="tab-content">
+ <div class="tab-pane active" id="tab_1">
+ <table id="image_errors" class="table table-condensed">
+ <thead>
+ <tr>
+ <th>Pool</th>
+ <th>Image</th>
+ <th>Issue</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr rv-each-image="image_error">
+ <td>{image.pool}</td>
+ <td>{image.name}</td>
+ <td>{image.description}</td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <div class="tab-pane" id="tab_2">
+ <table id="image_syncing" class="table table-condensed">
+ <thead>
+ <tr>
+ <th>Pool</th>
+ <th>Image</th>
+ <th>Progress</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr rv-each-image="image_syncing">
+ <td>{image.pool}</td>
+ <td>{image.name}</td>
+ <td>
+ <div class="progress" style="width: 100px; height: 18px;">
+ <div class="progress-bar progress-bar-aqua" rv-style="image.progress | sync_progress_bar">
+ {image.progress}
+ </div>
+ </div>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <div class="tab-pane" id="tab_3">
+ <table id="image_ready" class="table table-condensed">
+ <thead>
+ <tr>
+ <th>Pool</th>
+ <th>Image</th>
+ <th>State</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr rv-each-image="image_ready">
+ <td>{image.pool}</td>
+ <td>{image.name}</td>
+ <td><span rv-class="image.state_color | mirror_health_color">{image.state}</span></td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+</section>
+<!-- /.content -->
+
+{% endblock %}
--- /dev/null
+
+import json
+import rados
+import rbd
+from remote_view_cache import RemoteViewCache
+
+class Daemons(RemoteViewCache):
+ def _get(self):
+ daemons = []
+ for server in self._module.list_servers():
+ for service in server['services']:
+ if service['type'] == 'rbd-mirror':
+ metadata = self._module.get_metadata('rbd-mirror',
+ service['id'])
+ try:
+ status = JSON.parse(metadata['json'])
+ except:
+ status = {}
+ daemons.append({
+ 'service': service,
+ 'server': server,
+ 'metadata': metadata,
+ 'status': status
+ })
+ return daemons
+
+class PoolDatum(RemoteViewCache):
+ def __init__(self, module_inst, pool_name):
+ super(PoolDatum, self).__init__(module_inst)
+ self.pool_name = pool_name
+
+ def _get(self):
+ data = {}
+ self.log.debug("Constructing IOCtx " + self.pool_name)
+ try:
+ ioctx = self._module.rados.open_ioctx(self.pool_name)
+ except:
+ self.log.exception("Failed to open pool " + pool_name)
+ return None
+
+ try:
+ rbdctx = rbd.RBD()
+ mirror_mode = rbdctx.mirror_mode_get(ioctx)
+ except:
+ self.log.exception("Failed to query mirror mode " + pool_name)
+ if mirror_mode == rbd.RBD_MIRROR_MODE_IMAGE:
+ mirror_mode = "image"
+ elif mirror_mode == rbd.RBD_MIRROR_MODE_POOL:
+ mirror_mode = "pool"
+ else:
+ mirror_mode = None
+ data['mirror_mode'] = mirror_mode
+
+ return data
+
+class Toplevel(RemoteViewCache):
+ def __init__(self, module_inst, daemons):
+ super(Toplevel, self).__init__(module_inst)
+ self.daemons = daemons
+
+ def _get(self):
+ return {'warnings': 2, 'errors': 1}
+
+class ContentData(RemoteViewCache):
+ def __init__(self, module_inst, daemons, pool_data):
+ super(ContentData, self).__init__(module_inst)
+
+ self.daemons = daemons
+ self.pool_data = pool_data
+
+ def _get(self):
+ status, pool_names = self._module.rbd_pool_ls.get()
+ if pool_names is None:
+ log.warning("Failed to get RBD pool list")
+ return None
+
+ status, daemons = self.daemons.get()
+ if daemons is None:
+ log.warning("Failed to get rbd-mirror daemons list")
+ daemons = []
+
+ daemons = sorted([
+ {
+ 'id': daemon['service']['id'],
+ 'instance_id': daemon['metadata']['instance_id'],
+ 'version': daemon['metadata']['ceph_version'],
+ 'server_hostname': daemon['server']['hostname'],
+ 'health_color': 'warning',
+ 'health': 'Warning'
+ }
+ for daemon in daemons
+ ], key=lambda k: k['id'])
+
+
+ pools = sorted([
+ {
+ 'name': pool_name,
+ 'mirror_mode': self.get_pool_mirror_mode(pool_name),
+ 'health_color': 'error',
+ 'health': 'Error'
+ }
+ for pool_name in pool_names
+ ], key=lambda k: k['name'])
+
+ return {
+ 'daemons': daemons,
+ 'pools' : pools,
+ 'image_error': [
+ {'pool': 'images', 'name': 'foo', 'description': "couldn't blah"},
+ {'pool': 'images', 'name': 'goo', 'description': "failed XYZ"}
+ ],
+ 'image_syncing': [
+ {'pool': 'images', 'name': 'foo', 'progress': 93},
+ {'pool': 'images', 'name': 'goo', 'progress': 0}
+ ],
+ 'image_ready': [
+ {'pool': 'images', 'name': 'foo', 'state_color': 'info', 'state': 'Primary'},
+ {'pool': 'images', 'name': 'goo', 'state_color': 'success', 'state': 'Mirroring'}
+ ]
+ }
+
+ def get_pool_mirror_mode(self, pool_name):
+ pool_datum = self.pool_data.get(pool_name, None)
+ if pool_datum is None:
+ pool_datum = PoolDatum(self._module, pool_name)
+ self.pool_data[pool_name] = pool_datum
+
+ status, value = pool_datum.get()
+ if value is None:
+ return None
+ return value.get('mirror_mode', None)
+
+class Controller:
+ def __init__(self, module_inst):
+ self.daemons = Daemons(module_inst)
+ self.pool_data = {}
+ self.toplevel = Toplevel(module_inst, self.daemons)
+ self.content_data = ContentData(module_inst, self.daemons,
+ self.pool_data)
+