]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: add OSD list view 16373/head
authorJohn Spray <john.spray@redhat.com>
Sat, 15 Jul 2017 23:10:37 +0000 (19:10 -0400)
committerJohn Spray <john.spray@redhat.com>
Mon, 17 Jul 2017 16:44:57 +0000 (12:44 -0400)
Signed-off-by: John Spray <john.spray@redhat.com>
src/pybind/mgr/dashboard/base.html
src/pybind/mgr/dashboard/module.py
src/pybind/mgr/dashboard/osd_perf.html
src/pybind/mgr/dashboard/osds.html [new file with mode: 0644]

index efe09260e30d06e4bb0753a926432364d73b8104..6a67410e0deff9d7c3276e92244d4684e9ef15b8 100644 (file)
@@ -20,6 +20,7 @@
           href="/static/AdminLTE-2.3.7/dist/css/skins/skin-blue.min.css">
 
     <script src="/static/AdminLTE-2.3.7/plugins/jQuery/jquery-2.2.3.min.js"></script>
+    <script src="/static/AdminLTE-2.3.7/plugins/sparkline/jquery.sparkline.min.js"></script>
 
     <script src="/static/rivets.bundled.min.js"></script>
     <script src="/static/underscore-min.js"></script>
             <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>
-            <span>Servers</span></a>
+        <li class="treeview active">
+          <a href="#"><i class="fa fa-server"></i> <span>Cluster</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="/servers">Servers</a>
+            </li>
+            <li>
+                <a href="/osd">OSDs</a>
+            </li>
+          </ul>
         </li>
         <li class="treeview active">
           <a href="#"><i class="fa fa-hdd-o"></i> <span>Block</span>
index d3ce972811a567ef3629ef4273e9b67f70363808..42f62a8c8af8d5d4291a9715b73a5cc911f54cb6 100644 (file)
@@ -405,7 +405,22 @@ class Module(MgrModule):
 
         self.log_primed = True
 
-        class Root(object):
+        class EndPoint(object):
+            def _health_data(self):
+                health = global_instance().get_sync_object(Health).data
+                # Transform the `checks` dict into a list for the convenience
+                # of rendering from javascript.
+                checks = []
+                for k, v in health['checks'].iteritems():
+                    v['type'] = k
+                    checks.append(v)
+
+                checks = sorted(checks, cmp=lambda a, b: a['severity'] > b['severity'])
+
+                health['checks'] = checks
+
+                return health
+
             def _toplevel_data(self):
                 """
                 Data consumed by the base.html template
@@ -439,6 +454,7 @@ class Module(MgrModule):
                     'filesystems': filesystems
                 }
 
+        class Root(EndPoint):
             @cherrypy.expose
             def filesystem(self, fs_id):
                 template = env.get_template("filesystem.html")
@@ -460,57 +476,6 @@ class Module(MgrModule):
             def filesystem_data(self, fs_id):
                 return global_instance().fs_status(int(fs_id))
 
-            def _osd(self, osd_id):
-                #global_instance().fs_status(int(fs_id))
-                osd_id = int(osd_id)
-
-                osd_map = global_instance().get("osd_map")
-
-                osd = None
-                for o in osd_map['osds']:
-                    if o['osd'] == osd_id:
-                        osd = o
-                        break
-
-                assert osd is not None  # TODO 400
-
-                osd_spec = "{0}".format(osd_id) 
-
-                osd_metadata = global_instance().get_metadata(
-                        "osd", osd_spec)
-
-                result = CommandResult("")
-                global_instance().send_command(result, "osd", osd_spec,
-                       json.dumps({
-                           "prefix": "perf histogram dump",
-                           }),
-                       "")
-                r, outb, outs = result.wait()
-                assert r == 0
-                histogram = json.loads(outb)
-
-                return {
-                    "osd": osd,
-                    "osd_metadata": osd_metadata,
-                    "osd_histogram": histogram
-                }
-
-            @cherrypy.expose
-            def osd_perf(self, osd_id):
-                template = env.get_template("osd_perf.html")
-                toplevel_data = self._toplevel_data()
-
-                return template.render(
-                    ceph_version=global_instance().version,
-                    toplevel_data=json.dumps(toplevel_data, indent=2),
-                    content_data=json.dumps(self._osd(osd_id), indent=2)
-                )
-
-            @cherrypy.expose
-            @cherrypy.tools.json_out()
-            def osd_perf_data(self, osd_id):
-                return self._osd(osd_id)
-
             def _clients(self, fs_id):
                 cephfs_clients = global_instance().cephfs_clients.get(fs_id, None)
                 if cephfs_clients is None:
@@ -649,21 +614,6 @@ class Module(MgrModule):
             def servers_data(self):
                 return self._servers()
 
-            def _health_data(self):
-                health = global_instance().get_sync_object(Health).data
-                # Transform the `checks` dict into a list for the convenience
-                # of rendering from javascript.
-                checks = []
-                for k, v in health['checks'].iteritems():
-                    v['type'] = k
-                    checks.append(v)
-
-                checks = sorted(checks, cmp=lambda a, b: a['severity'] > b['severity'])
-
-                health['checks'] = checks
-
-                return health
-
             def _health(self):
                 # Fuse osdmap with pg_summary to get description of pools
                 # including their PG states
@@ -799,7 +749,148 @@ class Module(MgrModule):
             }
         }
         log.info("Serving static from {0}".format(static_dir))
+
+        class OSDEndpoint(EndPoint):
+            def _osd(self, osd_id):
+                osd_id = int(osd_id)
+
+                osd_map = global_instance().get("osd_map")
+
+                osd = None
+                for o in osd_map['osds']:
+                    if o['osd'] == osd_id:
+                        osd = o
+                        break
+
+                assert osd is not None  # TODO 400
+
+                osd_spec = "{0}".format(osd_id)
+
+                osd_metadata = global_instance().get_metadata(
+                        "osd", osd_spec)
+
+                result = CommandResult("")
+                global_instance().send_command(result, "osd", osd_spec,
+                       json.dumps({
+                           "prefix": "perf histogram dump",
+                           }),
+                       "")
+                r, outb, outs = result.wait()
+                assert r == 0
+                histogram = json.loads(outb)
+
+                return {
+                    "osd": osd,
+                    "osd_metadata": osd_metadata,
+                    "osd_histogram": histogram
+                }
+
+            @cherrypy.expose
+            def perf(self, osd_id):
+                template = env.get_template("osd_perf.html")
+                toplevel_data = self._toplevel_data()
+
+                return template.render(
+                    ceph_version=global_instance().version,
+                    toplevel_data=json.dumps(toplevel_data, indent=2),
+                    content_data=json.dumps(self._osd(osd_id), indent=2)
+                )
+
+            @cherrypy.expose
+            @cherrypy.tools.json_out()
+            def perf_data(self, osd_id):
+                return self._osd(osd_id)
+
+            @cherrypy.expose
+            @cherrypy.tools.json_out()
+            def list_data(self):
+                return self._osds_by_server()
+
+            def _osd_summary(self, osd_id, osd_info):
+                """
+                The info used for displaying an OSD in a table
+                """
+
+                osd_spec = "{0}".format(osd_id)
+
+                result = {}
+                result['id'] = osd_id
+                result['stats'] = {}
+                result['stats_history'] = {}
+
+                # Counter stats
+                for s in ['osd.op_w', 'osd.op_in_bytes', 'osd.op_r', 'osd.op_out_bytes']:
+                    result['stats'][s.split(".")[1]] = global_instance().get_rate('osd', osd_spec, s)
+                    result['stats_history'][s.split(".")[1]] = \
+                        global_instance().get_counter('osd', osd_spec, s)[s]
+
+                # Gauge stats
+                for s in ["osd.numpg", "osd.stat_bytes", "osd.stat_bytes_used"]:
+                    result['stats'][s.split(".")[1]] = global_instance().get_latest('osd', osd_spec, s)
+
+                result['up'] = osd_info['up']
+                result['in'] = osd_info['in']
+
+                result['url'] = "/osd/perf/{0}".format(osd_id)
+
+                return result
+
+            def _osds_by_server(self):
+                result = defaultdict(list)
+                servers = global_instance().list_servers()
+
+                osd_map = global_instance().get_sync_object(OsdMap)
+
+                for server in servers:
+                    hostname = server['hostname']
+                    services = server['services']
+                    first = True
+                    for s in services:
+                        if s["type"] == "osd":
+                            osd_id = int(s["id"])
+                            # If metadata doesn't tally with osdmap, drop it.
+                            if osd_id not in osd_map.osds_by_id:
+                                global_instance().log.warn(
+                                    "OSD service {0} missing in OSDMap, stale metadata?".format(osd_id))
+                                continue
+                            summary = self._osd_summary(osd_id,
+                                                        osd_map.osds_by_id[osd_id])
+
+                            if first:
+                                # A little helper for rendering
+                                summary['first'] = True
+                                first = False
+                            result[hostname].append(summary)
+
+                global_instance().log.warn("result.size {0} servers.size {1}".format(
+                    len(result), len(servers)
+                ))
+
+                # Return list form for convenience of rendering
+                return result.items()
+
+            @cherrypy.expose
+            def index(self):
+                """
+                List of all OSDS grouped by host
+                :return:
+                """
+
+                template = env.get_template("osds.html")
+                toplevel_data = self._toplevel_data()
+
+                content_data = {
+                    "osds_by_server": self._osds_by_server()
+                }
+
+                return template.render(
+                    ceph_version=global_instance().version,
+                    toplevel_data=json.dumps(toplevel_data, indent=2),
+                    content_data=json.dumps(content_data, indent=2)
+                )
+
         cherrypy.tree.mount(Root(), "/", conf)
+        cherrypy.tree.mount(OSDEndpoint(), "/osd", conf)
 
         log.info("Starting engine...")
         cherrypy.engine.start()
index 8c92414cf027735b1e8a65a939f40d652fe17a9d..7ab958effb56d3bb6b0fd06dcfbc4e9324af2786 100644 (file)
@@ -7,8 +7,6 @@
             // Pre-populated initial data at page load
             var content_data = {{ content_data }};
 
-
-
             var hexdigits = function(v) {
                 var i = Math.floor(v * 255);
                 if (Math.floor(i) < 0x10) {
@@ -98,7 +96,7 @@
             post_load();
 
             var refresh = function() {
-                $.get("/osd_perf_data/" + content_data.osd.osd  + "/", function(data) {
+                $.get("/osd/perf_data/" + content_data.osd.osd  + "/", function(data) {
                     _.extend(content_data.osd_histogram, data.osd_histogram);
                     _.extend(content_data.osd, data.osd);
                     _.extend(content_data.osd_metadata, data.osd_metadata);
diff --git a/src/pybind/mgr/dashboard/osds.html b/src/pybind/mgr/dashboard/osds.html
new file mode 100644 (file)
index 0000000..ddcb857
--- /dev/null
@@ -0,0 +1,105 @@
+{% 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("/osd/list_data/", function(data) {
+                    content_data.osds_by_server = data;
+                    $('.inlinesparkline').sparkline();
+                    setTimeout(refresh, 5000);
+                });
+            };
+
+            rivets.formatters.colored_up_in = function(osd){
+                var result = "";
+                if (osd.up) {
+                    result += "<span style='color:#00bb00;'>up</span>";
+                } else {
+                    result += "<span style='color:#bb0000;'>down</span>";
+                }
+
+                result += ", ";
+
+                if (osd.in) {
+                    result += "<span style='color:#00bb00;'>in</span>";
+                } else {
+                    result += "<span style='color:#bb0000;'>out</span>";
+                }
+
+                return result;
+            };
+
+            rivets.formatters.sparkline_data = function(time_series) {
+                result = "";
+                for (var i = 1; i < time_series.length; ++i) {
+                    var delta_v = time_series[i][1] - time_series[i - 1][1];
+                    var delta_t = time_series[i][0] - time_series[i - 1][0];
+                    result += (delta_v / delta_t + ",");
+                }
+                return result;
+            };
+
+            rivets.bind($("div#content"), content_data);
+            $('.inlinesparkline').sparkline();
+            setTimeout(refresh, 5000);
+        });
+</script>
+
+<section class="content-header">
+    <h1>
+        OSD daemons
+    </h1>
+</section>
+
+<section class="content">
+    <div class="box">
+        <div class="box-body">
+
+            <table class="table table-condensed table-bordered">
+            <thead>
+            <tr>
+                <th>Host</th>
+                <th>ID</th>
+                <th>Status</th>
+                <th>PGs</th>
+                <th>Usage</th>
+                <th>Read bytes</th>
+                <th>Write bytes</th>
+                <th>Read ops</th>
+                <th>Write ops</th>
+            </tr>
+            </thead>
+
+            <tbody rv-each-server="osds_by_server">
+                <tr rv-each-osd="server.1">
+                    <td rv-if="osd.first" rv-rowspan="server.1 | length">{server.0}</td>
+                    <td><a rv-href="osd.url">{osd.id}</a></td>
+                    <td rv-html="osd | colored_up_in"></td>
+                    <td>{osd.stats.numpg}</td>
+                    <td>{osd.stats.stat_bytes_used | dimless_binary} / {osd.stats.stat_bytes | dimless_binary}</td>
+                    <td>{osd.stats.op_out_bytes | dimless_binary}/s <span class="inlinesparkline" rv-html="osd.stats_history.op_out_bytes | sparkline_data"></span></td>
+                    <td>{osd.stats.op_in_bytes | dimless_binary}/s <span class="inlinesparkline" rv-html="osd.stats_history.op_in_bytes | sparkline_data"></span></td>
+                    <td>{osd.stats.op_r | dimless}/s</td>
+                    <td>{osd.stats.op_w | dimless}/s</td>
+                </tr>
+            </tbody>
+
+
+            </table>
+
+
+
+
+        </div>
+    </div>
+
+
+</section>
+<!-- /.content -->
+
+{% endblock %}