]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: initial block mirroring status page
authorJason Dillaman <dillaman@redhat.com>
Mon, 10 Jul 2017 21:55:46 +0000 (17:55 -0400)
committerJason Dillaman <dillaman@redhat.com>
Tue, 18 Jul 2017 14:47:50 +0000 (10:47 -0400)
Signed-off-by: Jason Dillaman <dillaman@redhat.com>
src/pybind/mgr/dashboard/base.html
src/pybind/mgr/dashboard/module.py
src/pybind/mgr/dashboard/rbd_mirroring.html [new file with mode: 0644]
src/pybind/mgr/dashboard/rbd_mirroring.py [new file with mode: 0644]

index da01000cc6fba0a520771b59e57f2186c34c5513..420cf8405de8e8f4abae29556ed7342f748dbde9 100644 (file)
                 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>
index 0bc2e1f7e610df4c532fd09766f71a613a9f55af..9fc42aaf1ea23086d2ddf31fe1a183ae826a481b 100644 (file)
@@ -32,10 +32,10 @@ from types import OsdMap, NotFound, Config, FsMap, MonMap, \
     PgSummary, Health, MonStatus
 
 import rados
+import rbd_mirroring
 from rbd_ls import RbdLs, RbdPoolLs
 from cephfs_clients import CephFSClients
 
-
 log = logging.getLogger("dashboard")
 
 
@@ -83,6 +83,9 @@ class Module(MgrModule):
         # 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 = {}
@@ -438,6 +441,11 @@ class Module(MgrModule):
                     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 = [
                     {
@@ -450,6 +458,7 @@ class Module(MgrModule):
 
                 return {
                     'rbd_pools': rbd_pools,
+                    'rbd_mirroring': rbd_mirroring,
                     'health_status': self._health_data()['status'],
                     'filesystems': filesystems
                 }
@@ -589,6 +598,32 @@ class Module(MgrModule):
             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")
diff --git a/src/pybind/mgr/dashboard/rbd_mirroring.html b/src/pybind/mgr/dashboard/rbd_mirroring.html
new file mode 100644 (file)
index 0000000..3c53c33
--- /dev/null
@@ -0,0 +1,214 @@
+{% 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 %}
diff --git a/src/pybind/mgr/dashboard/rbd_mirroring.py b/src/pybind/mgr/dashboard/rbd_mirroring.py
new file mode 100644 (file)
index 0000000..a5019db
--- /dev/null
@@ -0,0 +1,140 @@
+
+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)
+