]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: update health display 16043/head
authorJohn Spray <john.spray@redhat.com>
Thu, 22 Jun 2017 00:34:27 +0000 (20:34 -0400)
committerJohn Spray <john.spray@redhat.com>
Fri, 14 Jul 2017 14:05:54 +0000 (10:05 -0400)
This takes account of the new health format, also
expands and visually cleans up the frontpage
where we put the health information.

Dark backgrounds make it much easier to use
red/amber/green colours to grab attention.

Signed-off-by: John Spray <john.spray@redhat.com>
src/pybind/mgr/dashboard/base.html
src/pybind/mgr/dashboard/clients.html
src/pybind/mgr/dashboard/filesystem.html
src/pybind/mgr/dashboard/health.html
src/pybind/mgr/dashboard/module.py
src/pybind/mgr/dashboard/static/logo-mini.png [new file with mode: 0644]
src/pybind/mgr/dashboard/types.py

index 18874fb565f060788fd3bf38ee2ea2787ed3f13d..efe09260e30d06e4bb0753a926432364d73b8104 100644 (file)
                 return format_number(n, 1024, ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB']);
             };
 
-            <!--rivet.formatters.mon_summary = function(mon_map) {-->
-            <!--}-->
+            /* This is useful if you need to display some alternative text
+             * when a collection is empty using rv-hide */
+            rivets.formatters.length = function(val) {
+              return val.length;
+            }
 
             rivets.bind($("#health"), toplevel_data);
             rivets.bind($("section.sidebar"), toplevel_data);
 
       <link rel="shortcut icon" href="http://ceph.com/wp-content/themes/ceph/favicon.ico">
       <link rel="shortcut icon" href="/static/favicon.ico">
+
+    <style>
+        div.box {
+            background-color: #222d32;
+            color: #fff;
+        }
+
+        div.info-box {
+            background-color: #222d32;
+            color: #fff;
+        }
+
+        .box {
+            border-top-color: #b8c7ce;
+        }
+
+        div.box-header {
+            color: #b8c7ce;
+        }
+
+        a.logo {
+            background-color: #222d32;
+        }
+
+        body {
+            background-color: #222d32;
+        }
+
+        .navbar {
+            background-color: #222d32;
+            color: #222d32;
+        }
+
+        div#content {
+            background-color: #424d52;
+            color: #ddd;
+        }
+
+        div.progress-bar {
+            border-width: 1px;
+            border-color: #ddd;
+        }
+
+        .ceph-log {
+            font-family: monospace;
+            background-color: #333;
+            color: #ddd;
+        }
+
+        .nav-tabs>li.active>a {
+            background-color: #424d52;
+            color: #ddd;
+        }
+
+        .navbar a {
+            color: #b8c7ce;
+        }
+
+        .ceph-none-found {
+            color: #8aa4af;
+            font-style: italic;
+            padding-left: 15px;
+            padding-right: 5px;
+            padding-top: 5px;
+            padding-bottom: 5px;
+        }
+
+        table.ceph-chartbox {
+            margin-left: 40px;
+        }
+
+        .ceph-chartbox td {
+            padding-left: 35px;
+            text-align: center;
+            font-weight: bold;
+        }
+
+    </style>
+
 </head>
 
-<body class="hold-transition skin-blue sidebar-mini">
+<body class="hold-transition sidebar-mini sidebar-collapse">
 <div class="wrapper">
 
-  <!-- Main Header -->
+    <!-- Main Header -->
     <header class="main-header">
         <!-- Logo -->
         <a href="/" class="logo">
-            <!--
-          <span class="logo-mini"><b>A</b>LT</span>
-          <span class="logo-lg"><b>Admin</b>LTE</span> -->
       <span class="logo-lg">
         <img src="/static/Ceph_Logo_Standard_RGB_White_120411_fa.png"
              width="123px" height="34px"/>
           </span>
+      <span class="logo-mini">
+        <img src="/static/logo-mini.png"
+             width="34px" height="34px"/>
+          </span>
         </a>
 
         <!-- Header Navbar -->
         </nav>
     </header>
   <!-- Left side column. contains the logo and sidebar -->
-  <aside class="main-sidebar">
+  <aside class="main-sidebar skin-blue">
 
     <!-- sidebar: style can be found in sidebar.less -->
     <section class="sidebar">
       <!-- Sidebar Menu -->
       <ul class="sidebar-menu">
         <!-- Optionally, you can add icons to the links -->
-        <li><a href="/health"><i class="fa fa-heart"></i>
+        <li><a href="/health">
+            <i class="fa fa-heartbeat" rv-style="health_status | health_color"></i>
             <span>Cluster health</span></a>
         </li>
-        <li><a href="/servers"><i class="fa fa-server"></i>
+        <li><a href="/servers">
+            <i class="fa fa-server"></i>
             <span>Servers</span></a>
         </li>
         <li class="treeview active">
             <li rv-each-pool="rbd_pools">
                 <a rv-href="pool.url">{pool.name}</a>
             </li>
+            <li class="ceph-none-found" rv-hide="rbd_pools | length">None found</li>
           </ul>
         </li>
         <li class="treeview active">
             <li rv-each-filesystem="filesystems">
                 <a rv-href="filesystem.url">{filesystem.name}</a>
             </li>
+            <li class="ceph-none-found" rv-hide="filesystems | length">None found</li>
           </ul>
         </li>
       </ul>
index 8d775cc5a10ca0e85181a3f74f1fa75cf6c4efac..bd5a557210bca21fc92c69806a03bec57445aaa0 100644 (file)
 
 <section class="content-header">
     <h1>
-        Clients
+        Clients of <a rv-href="fs_url">{fs_name}</a>
     </h1>
 </section>
 
 <section class="content">
     <div class="box">
-        <div class="box-header">
-            <h3 class="box-title">Clients</h3>
-        </div>
         <div class="box-body">
             <table class="table table-bordered">
                 <thead>
index a8bd193c9ac9c088eabb63d198a98e64275088ea..c58f1e18fd57c967444e5b84e6a65188d9d49b67 100644 (file)
                                         },
                                     ]
                                 },
-                                options:{
-                                    legend: {
-                                        position: 'right',
-                                        display: top_chart
-                                    }
-                                },
                                 options: {
+                                    legend: {
+                                        position: 'top',
+                                        display: top_chart,
+                                        labels:{fontColor: "#ddd"}
+                                    },
                                     scales: {
                                         xAxes: [{
                                             position: 'top',
                                             type: 'time',
                                             display: top_chart,
+                                            ticks: {fontColor:"#ddd"},
                                             time: {
                                                 displayFormats: {
                                                     quarter: 'MMM YYYY'
                                             id: 'LHS',
                                             type: 'linear',
                                             position: 'left',
+                                            ticks: {fontColor:"#ddd"},
                                             min: 0
                                         }, {
                                             id: 'RHS',
                                             type: 'linear',
                                             position: 'right',
+                                            ticks: {fontColor:"#ddd"},
                                             min: 0
                                         }]
                                     }
         </div>
     </div>
 
-    <div id="mds_charts">
+    <div id="mds_charts" style="color: #ddd;">
     </div>
 
 </section>
index de5a794f27dc5919f28ae94bf0faf43041cc9eaa..70cc7f8efe727eb19bbf2128e1abdfeb75fec29c 100644 (file)
@@ -8,14 +8,6 @@
             // Pre-populated initial data at page load
             var content_data = {{ content_data }};
 
-            var refresh = function() {
-                $.get("/health_data", function(data) {
-                    _.extend(content_data, data);
-                    setTimeout(refresh, 5000);
-                });
-            };
-            setTimeout(refresh, 5000);
-
             rivets.formatters.mon_summary = function(mon_status) {
                 var result = mon_status.monmap.mons.length.toString() + " (quorum ";
                 result += mon_status.quorum.join(", ");
                 return result;
             };
 
+            rivets.formatters.mds_summary = function(fs_map) {
+                var standbys = 0;
+                var active = 0;
+                var standby_replay = 0;
+                $.each(fs_map.standbys, function(i, s) {
+                    standbys += 1;
+                });
+
+                if (fs_map.standbys && !fs_map.filesystems) {
+                    return standbys + ", no filesystems"
+                } else if (fs_map.filesystems.length == 0) {
+                    return "no filesystems";
+                } else {
+                    $.each(fs_map.filesystems, function(i, fs) {
+                        $.each(fs.mdsmap.info, function(j, mds) {
+                            if (mds.state == "up:standby-replay") {
+                                standby_replay += 1;
+                            } else {
+                                active += 1;
+                            }
+                        });
+                    });
+
+                    return active + " active, " + (standbys + standby_replay) + " standby";
+                }
+            };
+
+            rivets.formatters.mgr_summary = function(mgr_map) {
+                var result = "";
+                result += "active: " + mgr_map.active_name;
+                if (mgr_map.standbys.length) {
+                    result += ", " + mgr_map.standbys.length + " standbys";
+                }
+
+                return result;
+            };
+
             rivets.formatters.log_color = function(log_line) {
                 if (log_line.priority == "[INF]") {
-                    return "color: #000000";
+                    return "";  // Inherit
                 } else if (log_line.priority == "[WRN]") {
                     return "color: #FFC200";
                 } else if (log_line.priority == "[ERR]") {
                 return strings.join(", ");
             };
 
-            rivets.bind($("#content"), content_data);
-        });
-    </script>
+            // An extension to Chart.js to enable rendering some
+            // text in the middle of a doughnut
+            Chart.pluginService.register({
+              beforeDraw: function(chart) {
+                if (!chart.options.center_text) {
+                    return;
+                }
+                var width = chart.chart.width,
+                    height = chart.chart.height,
+                    ctx = chart.chart.ctx;
+
+                ctx.restore();
+                var fontSize = (height / 114).toFixed(2);
+                ctx.font = fontSize + "em sans-serif";
+                ctx.fillStyle = "#ddd";
+                ctx.textBaseline = "middle";
+
+
+                var text = chart.options.center_text,
+                    textX = Math.round((width - ctx.measureText(text).width) / 2),
+                    textY = height / 2;
+
+                ctx.fillText(text, textX, textY);
+                ctx.save();
+              }
+            });
+
+            var draw_usage_charts = function() {
+                var raw_usage_text = Math.round(100*(
+                    content_data.df.stats.total_used_bytes
+                    / content_data.df.stats.total_bytes)) + "%";
+                var raw_usage_canvas = $("#raw_usage_chart").get(0).getContext("2d");
+                var raw_usage_chart = new Chart(raw_usage_canvas, {
+                    type: 'doughnut',
+                    data: {
+                        labels:[
+                            "Raw Used",
+                            "Raw Available"
+                        ],
+                        datasets: [
+                            {
+                            'label': null,
+                            borderWidth: 0,
+                            data:[
+                                content_data.df.stats.total_used_bytes,
+                                content_data.df.stats.total_avail_bytes
+                            ],
+                            backgroundColor: ["#424d52", "#222d32"]
+                            }
+                        ]
+                    },
+                    options: {
+                        center_text: raw_usage_text,
+                        responsive: false,
+                        legend: {display: false},
+                        animation: {duration: 0}
+                    }
+                });
 
+                var colors = ['#3366CC','#DC3912','#FF9900','#109618','#990099',
+                    '#3B3EAC','#0099C6','#DD4477','#66AA00','#B82E2E','#316395',
+                    '#994499','#22AA99','#AAAA11','#6633CC','#E67300','#8B0707',
+                    '#329262','#5574A6','#3B3EAC'];
 
-    <!-- Content Header (Page header) -->
-    <section class="content-header">
-      <h1>
-        Health
-      </h1>
+                var pool_usage_canvas = $("#pool_usage_chart").get(0).getContext("2d");
+                var pool_labels = [];
+                var pool_data = [];
 
-    </section>
+                $.each(content_data.df.pools, function(i, pool) {
+                    pool_labels.push(pool['name']);
+                    pool_data.push(pool['stats']['bytes_used']);
+                });
 
-    <!-- Main content -->
-    <section class="content">
+                var pool_usage_chart = new Chart(pool_usage_canvas, {
+                    type: 'doughnut',
+                    data: {
+                        labels:pool_labels,
+                        datasets: [
+                            {
+                            'label': null,
+                            borderWidth: 0,
+                            data:pool_data,
+                            backgroundColor: colors
+                            }
+                        ]
+                    },
+                    options: {
+                        responsive: false,
+                        legend: {display: false},
+                        animation: {duration: 0}
+                    }
+                });
+            }
 
-    <div class="box-body">
-    Overall status: <span rv-style="health.status | health_color">{health.status}</span>
+            draw_usage_charts();
+            rivets.bind($("#content"), content_data);
 
-    <ul>
-        <ul>
-            <li rv-each-check="health.checks">
-                <span rv-style="check.severity | health_color">{check.type}</span>:
-                {check.message}
-            </li>
-        </ul>
-    </ul>
+            var refresh = function() {
+                $.get("/health_data", function(data) {
+                    _.extend(content_data, data);
+                    draw_usage_charts();
+                    setTimeout(refresh, 5000);
+                });
+            };
+            setTimeout(refresh, 5000);
+        });
+    </script>
 
+    <!-- Main content -->
+    <section class="content">
         <div class="row">
+            <div class="col-sm-6">
+                <div class="box">
+                    <div class="box-header">
+                        Health
+                    </div>
+                    <div class="box-body">
+                        Overall status: <span
+                            rv-style="health.status | health_color">{health.status}</span>
+
+                        <ul>
+                            <li rv-each-check="health.checks">
+                                <span rv-style="check.severity | health_color">{check.type}</span>:
+                                {check.message}
+                            </li>
+                        </ul>
+                    </div>
+                </div>
+            </div>
             <div class="col-sm-3">
                 <div class="info-box">
-                    <span class="info-box-icon bg-aqua"><i
+                    <span class="info-box-icon bg-grey"><i
                             class="fa fa-database"></i></span>
 
                     <div class="info-box-content">
                         <span class="info-box-text">Monitors</span>
                         <span class="info-box-number">{mon_status | mon_summary}</span>
                     </div>
-                    <!-- /.info-box-content -->
                 </div>
-            </div>
-
-            <div class="col-sm-3">
                 <div class="info-box">
-                    <span class="info-box-icon bg-aqua"><i
+                    <span class="info-box-icon bg-grey"><i
                             class="fa fa-hdd-o"></i></span>
 
                     <div class="info-box-content">
                         <span class="info-box-text">OSDs</span>
                         <span class="info-box-number">{osd_map | osd_summary}</span>
                     </div>
-                    <!-- /.info-box-content -->
                 </div>
             </div>
-        </div>
 
-        <div class="box">
-            <div class="box-header">
-                Pools
+            <div class="col-sm-3">
+                <div class="info-box">
+                    <span class="info-box-icon bg-grey"><i
+                            class="fa fa-folder"></i></span>
+
+                    <div class="info-box-content">
+                        <span class="info-box-text">Metadata servers</span>
+                        <span class="info-box-number">{fs_map | mds_summary}</span>
+                    </div>
+                </div>
+                <div class="info-box">
+                    <span class="info-box-icon bg-grey"><i
+                            class="fa fa-cog"></i></span>
+
+                    <div class="info-box-content">
+                        <span class="info-box-text">Manager daemons</span>
+                        <span class="info-box-number">{mgr_map | mgr_summary}</span>
+                    </div>
+                </div>
             </div>
-            <div class="box-body">
 
-                <table class="table table-condensed">
-                    <thead>
-                    <th>Name</th>
-                    <th>PG status</th>
-                    <th>Usage</th>
-                    <th>Activity</th>
-                    </thead>
-                    <tbody>
-                    <tr rv-each-pool="pools">
-                        <td style="text-align: right;">
-                            {pool.pool_name}
-                        </td>
-                        <td rv-style="pool.pg_status | pg_status_style">
-                            {pool.pg_status | pg_status}
-                        </td>
-                        <td>
-                            {pool.stats.bytes_used.latest | dimless} /
-                            {pool.stats.max_avail.latest | dimless }
-                        </td>
-                        <td>
-                            {pool.stats.rd_bytes.rate | dimless } rd, {
-                            pool.stats.wr_bytes.rate | dimless } wr
-                        </td>
-                    </tr>
-                    </tbody>
-                </table>
+        </div>
+
+        <div class="row">
+            <div class="col-sm-6">
+                <div class="box">
+                    <div class="box-header">
+                        Usage
+                    </div>
+                    <div class="box-body" style="text-align:center;">
+                        <table class="ceph-chartbox">
+                            <tr>
+                                <td>
+                                    <span style="font-size: 45px;">{df.stats.total_objects | dimless}</span>
+                                </td>
+                                <td>
+                                    <canvas id="raw_usage_chart"
+                                            style="height:120px; width:120px;"></canvas>
+                                </td>
+                                <td>
+                                    <canvas id="pool_usage_chart"
+                                            style="height:120px; width: 120px;"></canvas>
+                                </td>
+                            </tr>
+                            <tr>
+                                <td>Objects</td>
+                                <td>Raw capacity<br>({df.stats.total_used_bytes | dimless_binary} used)</td>
+                                <td>Usage by pool</td>
+                            </tr>
+                        </table>
+
+                    </div>
+                </div>
             </div>
 
+            <div class="col-sm-6">
+                <div class="box">
+                    <div class="box-header">
+                        Pools
+                    </div>
+                    <div class="box-body">
+                        <table class="table table-condensed">
+                            <thead>
+                            <th>Name</th>
+                            <th>PG status</th>
+                            <th>Usage</th>
+                            <th>Activity</th>
+                            </thead>
+                            <tbody>
+                            <tr rv-each-pool="pools">
+                                <td style="text-align: right;">
+                                    {pool.pool_name}
+                                </td>
+                                <td rv-style="pool.pg_status | pg_status_style">
+                                    {pool.pg_status | pg_status}
+                                </td>
+                                <td>
+                                    {pool.stats.bytes_used.latest | dimless} /
+                                    {pool.stats.max_avail.latest | dimless }
+                                </td>
+                                <td>
+                                    {pool.stats.rd_bytes.rate | dimless } rd, {
+                                    pool.stats.wr_bytes.rate | dimless } wr
+                                </td>
+                            </tr>
+                            </tbody>
+                        </table>
+                    </div>
+                </div>
+            </div>
         </div>
 
         <div class="box">
-            <div class="box-header">
-                Cluster log
-            </div>
             <div class="box-body">
                 <ul class="nav nav-tabs">
                   <li class="active"><a data-toggle="tab" href="#clog">Cluster log</a></li>
                   <li><a data-toggle="tab" href="#audit_log">Audit log</a></li>
                 </ul>
-                <div class="tab-content" style="font-family:monospace; background-color: #ddd; color: #333">
+                <div class="tab-content ceph-log">
                     <div id="clog" class="tab-pane fade in active">
                         <span>
                             <span rv-each-line="clog">
                                 { line.stamp }&nbsp;{line.priority}&nbsp;
                                 <span  rv-style="line | log_color">
-                                    <span style="font-weight: bold;">
                                         { line.message }
-                                    </span><br>
+                                    <br>
                                 </span>
                             </span>
                         </span>
             </div>
 
         </div>
-
-        <!--
-
-    cluster ac15351a-571b-4393-9c71-815fc98dacd6
-     health HEALTH_WARN
-            noin,sortbitwise,require_jewel_osds,require_kraken_osds flag(s) set
-     monmap e2: 3 mons at {a=192.168.1.7:6789/0,b=192.168.1.7:6790/0,c=192.168.1.7:6791/0}
-            election epoch 6, quorum 0,1,2 a,b,c
-      fsmap e9: cephfs_a-1/1/1 up cephfs_b-1/1/1 up {[cephfs_a:0]=a=up:active,[cephfs_b:0]=d=up:active}, 2 up:standby
-        mgr active: x
-     osdmap e17: 3 osds: 3 up, 3 in
-            flags noin,sortbitwise,require_jewel_osds,require_kraken_osds
-      pgmap v115: 40 pgs, 5 pools, 4296 bytes data, 40 objects
-            279 GB used, 205 GB / 485 GB avail
-                  40 active+clean
-                  -->
-
-
     </section>
 
 {% endblock %}
index 93300135f49c21c15d96edec499e398d0afa4d34..d3ce972811a567ef3629ef4273e9b67f70363808 100644 (file)
@@ -540,21 +540,36 @@ class Module(MgrModule):
                 return clients
 
             @cherrypy.expose
-            def clients(self, fs_id):
-                template = env.get_template("clients.html")
-
-                toplevel_data = self._toplevel_data()
-
-                clients = self._clients(int(fs_id))
+            def clients(self, fscid_str):
+                try:
+                    fscid = int(fscid_str)
+                except ValueError:
+                    raise cherrypy.HTTPError(400,
+                        "Invalid filesystem id {0}".format(fscid_str))
+
+                try:
+                    fs_name = FsMap(global_instance().get(
+                        "fs_map")).get_filesystem(fscid)['mdsmap']['fs_name']
+                except NotFound:
+                    log.warning("Missing FSCID, dumping fsmap:\n{0}".format(
+                        json.dumps(global_instance().get("fs_map"), indent=2)
+                    ))
+                    raise cherrypy.HTTPError(404,
+                                             "No filesystem with id {0}".format(fscid))
+
+                clients = self._clients(fscid)
                 global_instance().log.debug(json.dumps(clients, indent=2))
                 content_data = {
                     "clients": clients,
-                    "fscid": fs_id
+                    "fs_name": fs_name,
+                    "fscid": fscid,
+                    "fs_url": "/filesystem/" + fscid_str + "/"
                 }
 
+                template = env.get_template("clients.html")
                 return template.render(
                     ceph_version=global_instance().version,
-                    toplevel_data=json.dumps(toplevel_data, indent=2),
+                    toplevel_data=json.dumps(self._toplevel_data(), indent=2),
                     content_data=json.dumps(content_data, indent=2)
                 )
 
@@ -625,7 +640,6 @@ class Module(MgrModule):
                 )
 
             def _servers(self):
-                servers = global_instance().list_servers()
                 return {
                     'servers': global_instance().list_servers()
                 }
@@ -684,14 +698,21 @@ class Module(MgrModule):
                 # to UI
                 del osd_map['pg_temp']
 
+                df = global_instance().get("df")
+                df['stats']['total_objects'] = sum(
+                    [p['stats']['objects'] for p in df['pools']])
+
                 return {
                     "health": self._health_data(),
                     "mon_status": global_instance().get_sync_object(
                         MonStatus).data,
+                    "fs_map": global_instance().get_sync_object(FsMap).data,
                     "osd_map": osd_map,
                     "clog": list(global_instance().log_buffer),
                     "audit_log": list(global_instance().audit_buffer),
-                    "pools": pools
+                    "pools": pools,
+                    "mgr_map": global_instance().get("mgr_map"),
+                    "df": df
                 }
 
             @cherrypy.expose
diff --git a/src/pybind/mgr/dashboard/static/logo-mini.png b/src/pybind/mgr/dashboard/static/logo-mini.png
new file mode 100644 (file)
index 0000000..b3446a8
Binary files /dev/null and b/src/pybind/mgr/dashboard/static/logo-mini.png differ
index 79122266dc7260a1ecde50d5b98f6f8c60ea3069..13754ea02115e3a41b43dfd972f181b77a482441 100644 (file)
@@ -179,6 +179,13 @@ class OsdMap(DataWrapper):
 class FsMap(DataWrapper):
     str = 'fs_map'
 
+    def get_filesystem(self, fscid):
+        for fs in self.data['filesystems']:
+            if fs['id'] == fscid:
+                return fs
+
+        raise NotFound("filesystem", fscid)
+
 
 class MonMap(DataWrapper):
     str = 'mon_map'