+
+"""
+Demonstrate writing a Ceph web interface inside a mgr module.
+"""
+
+# We must share a global reference to this instance, because it is the
+# gatekeeper to all accesses to data from the C++ side (e.g. the REST API
+# request handlers need to see it)
+from collections import defaultdict
+import collections
+
+_global_instance = {'plugin': None}
+def global_instance():
+ assert _global_instance['plugin'] is not None
+ return _global_instance['plugin']
+
+
+import os
+import logging
+import logging.config
+import json
+import sys
+import time
+import threading
+import socket
+
+import cherrypy
+import jinja2
+
+from mgr_module import MgrModule, MgrStandbyModule, CommandResult
+
+from types import OsdMap, NotFound, Config, FsMap, MonMap, \
+ PgSummary, Health, MonStatus
+
+import rados
+import rbd_iscsi
+import rbd_mirroring
+from rbd_ls import RbdLs, RbdPoolLs
+from cephfs_clients import CephFSClients
+
+log = logging.getLogger("dashboard")
+
+
+# How many cluster log lines shall we hold onto in our
+# python module for the convenience of the GUI?
+LOG_BUFFER_SIZE = 30
+
+# cherrypy likes to sys.exit on error. don't let it take us down too!
+def os_exit_noop(*args, **kwargs):
+ pass
+
+os._exit = os_exit_noop
+
+
+def recurse_refs(root, path):
+ if isinstance(root, dict):
+ for k, v in root.items():
+ recurse_refs(v, path + "->%s" % k)
+ elif isinstance(root, list):
+ for n, i in enumerate(root):
+ recurse_refs(i, path + "[%d]" % n)
+
+ log.info("%s %d (%s)" % (path, sys.getrefcount(root), root.__class__))
+
+def get_prefixed_url(url):
+ return global_instance().url_prefix + url
+
+
+
+class StandbyModule(MgrStandbyModule):
+ def serve(self):
+ server_addr = self.get_localized_config('server_addr', '::')
+ server_port = self.get_localized_config('server_port', '7000')
+ if server_addr is None:
+ raise RuntimeError('no server_addr configured; try "ceph config-key set mgr/dashboard/server_addr <ip>"')
+ log.info("server_addr: %s server_port: %s" % (server_addr, server_port))
+ cherrypy.config.update({
+ 'server.socket_host': server_addr,
+ 'server.socket_port': int(server_port),
+ 'engine.autoreload.on': False
+ })
+
+ current_dir = os.path.dirname(os.path.abspath(__file__))
+ jinja_loader = jinja2.FileSystemLoader(current_dir)
+ env = jinja2.Environment(loader=jinja_loader)
+
+ module = self
+
+ class Root(object):
+ @cherrypy.expose
+ def index(self):
+ active_uri = module.get_active_uri()
+ if active_uri:
+ log.info("Redirecting to active '{0}'".format(active_uri))
+ raise cherrypy.HTTPRedirect(active_uri)
+ else:
+ template = env.get_template("standby.html")
+ return template.render(delay=5)
+
+ cherrypy.tree.mount(Root(), "/", {})
+ log.info("Starting engine...")
+ cherrypy.engine.start()
+ log.info("Waiting for engine...")
+ cherrypy.engine.wait(state=cherrypy.engine.states.STOPPED)
+ log.info("Engine done.")
+
+ def shutdown(self):
+ log.info("Stopping server...")
+ cherrypy.engine.wait(state=cherrypy.engine.states.STARTED)
+ cherrypy.engine.stop()
+ log.info("Stopped server")
+
+
+class Module(MgrModule):
+ def __init__(self, *args, **kwargs):
+ super(Module, self).__init__(*args, **kwargs)
+ _global_instance['plugin'] = self
+ self.log.info("Constructing module {0}: instance {1}".format(
+ __name__, _global_instance))
+
+ self.log_primed = False
+ self.log_buffer = collections.deque(maxlen=LOG_BUFFER_SIZE)
+ self.audit_buffer = collections.deque(maxlen=LOG_BUFFER_SIZE)
+
+ # Keep a librados instance for those that need it.
+ self._rados = None
+
+ # Stateful instances of RbdLs, hold cached results. Key to dict
+ # is pool name.
+ self.rbd_ls = {}
+
+ # Stateful instance of RbdPoolLs, hold cached list of RBD
+ # pools
+ self.rbd_pool_ls = RbdPoolLs(self)
+
+ # Stateful instance of RbdISCSI
+ self.rbd_iscsi = rbd_iscsi.Controller(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 = {}
+
+ # A short history of pool df stats
+ self.pool_stats = defaultdict(lambda: defaultdict(
+ lambda: collections.deque(maxlen=10)))
+
+ # A prefix for all URLs to use the dashboard with a reverse http proxy
+ self.url_prefix = ''
+
+ @property
+ def rados(self):
+ """
+ A librados instance to be shared by any classes within
+ this mgr module that want one.
+ """
+ if self._rados:
+ return self._rados
+
+ ctx_capsule = self.get_context()
+ self._rados = rados.Rados(context=ctx_capsule)
+ self._rados.connect()
+
+ return self._rados
+
+ def update_pool_stats(self):
+ df = global_instance().get("df")
+ pool_stats = dict([(p['id'], p['stats']) for p in df['pools']])
+ now = time.time()
+ for pool_id, stats in pool_stats.items():
+ for stat_name, stat_val in stats.items():
+ self.pool_stats[pool_id][stat_name].appendleft((now, stat_val))
+
+ def notify(self, notify_type, notify_val):
+ if notify_type == "clog":
+ # Only store log messages once we've done our initial load,
+ # so that we don't end up duplicating.
+ if self.log_primed:
+ if notify_val['channel'] == "audit":
+ self.audit_buffer.appendleft(notify_val)
+ else:
+ self.log_buffer.appendleft(notify_val)
+ elif notify_type == "pg_summary":
+ self.update_pool_stats()
+ else:
+ pass
+
+ def get_sync_object(self, object_type, path=None):
+ if object_type == OsdMap:
+ data = self.get("osd_map")
+
+ assert data is not None
+
+ data['tree'] = self.get("osd_map_tree")
+ data['crush'] = self.get("osd_map_crush")
+ data['crush_map_text'] = self.get("osd_map_crush_map_text")
+ data['osd_metadata'] = self.get("osd_metadata")
+ obj = OsdMap(data)
+ elif object_type == Config:
+ data = self.get("config")
+ obj = Config( data)
+ elif object_type == MonMap:
+ data = self.get("mon_map")
+ obj = MonMap(data)
+ elif object_type == FsMap:
+ data = self.get("fs_map")
+ obj = FsMap(data)
+ elif object_type == PgSummary:
+ data = self.get("pg_summary")
+ self.log.debug("JSON: {0}".format(data))
+ obj = PgSummary(data)
+ elif object_type == Health:
+ data = self.get("health")
+ obj = Health(json.loads(data['json']))
+ elif object_type == MonStatus:
+ data = self.get("mon_status")
+ obj = MonStatus(json.loads(data['json']))
+ else:
+ raise NotImplementedError(object_type)
+
+ # TODO: move 'path' handling up into C++ land so that we only
+ # Pythonize the part we're interested in
+ if path:
+ try:
+ for part in path:
+ if isinstance(obj, dict):
+ obj = obj[part]
+ else:
+ obj = getattr(obj, part)
+ except (AttributeError, KeyError):
+ raise NotFound(object_type, path)
+
+ return obj
+
+ def shutdown(self):
+ log.info("Stopping server...")
+ cherrypy.engine.exit()
+ log.info("Stopped server")
+
+ log.info("Stopping librados...")
+ if self._rados:
+ self._rados.shutdown()
+ log.info("Stopped librados.")
+
+ def get_latest(self, daemon_type, daemon_name, stat):
+ data = self.get_counter(daemon_type, daemon_name, stat)[stat]
+ if data:
+ return data[-1][1]
+ else:
+ return 0
+
+ def get_rate(self, daemon_type, daemon_name, stat):
+ data = self.get_counter(daemon_type, daemon_name, stat)[stat]
+
+ if data and len(data) > 1:
+ return (data[-1][1] - data[-2][1]) / float(data[-1][0] - data[-2][0])
+ else:
+ return 0
+
+ def format_dimless(self, n, width, colored=True):
+ """
+ Format a number without units, so as to fit into `width` characters, substituting
+ an appropriate unit suffix.
+ """
+ units = [' ', 'k', 'M', 'G', 'T', 'P']
+ unit = 0
+ while len("%s" % (int(n) // (1000**unit))) > width - 1:
+ unit += 1
+
+ if unit > 0:
+ truncated_float = ("%f" % (n / (1000.0 ** unit)))[0:width - 1]
+ if truncated_float[-1] == '.':
+ truncated_float = " " + truncated_float[0:-1]
+ else:
+ truncated_float = "%{wid}d".format(wid=width-1) % n
+ formatted = "%s%s" % (truncated_float, units[unit])
+
+ if colored:
+ # TODO: html equivalent
+ # if n == 0:
+ # color = self.BLACK, False
+ # else:
+ # color = self.YELLOW, False
+ # return self.bold(self.colorize(formatted[0:-1], color[0], color[1])) \
+ # + self.bold(self.colorize(formatted[-1], self.BLACK, False))
+ return formatted
+ else:
+ return formatted
+
+ def fs_status(self, fs_id):
+ mds_versions = defaultdict(list)
+
+ fsmap = self.get("fs_map")
+ filesystem = None
+ for fs in fsmap['filesystems']:
+ if fs['id'] == fs_id:
+ filesystem = fs
+ break
+
+ rank_table = []
+
+ mdsmap = filesystem['mdsmap']
+
+ client_count = 0
+
+ for rank in mdsmap["in"]:
+ up = "mds_{0}".format(rank) in mdsmap["up"]
+ if up:
+ gid = mdsmap['up']["mds_{0}".format(rank)]
+ info = mdsmap['info']['gid_{0}'.format(gid)]
+ dns = self.get_latest("mds", info['name'], "mds.inodes")
+ inos = self.get_latest("mds", info['name'], "mds_mem.ino")
+
+ if rank == 0:
+ client_count = self.get_latest("mds", info['name'],
+ "mds_sessions.session_count")
+ elif client_count == 0:
+ # In case rank 0 was down, look at another rank's
+ # sessionmap to get an indication of clients.
+ client_count = self.get_latest("mds", info['name'],
+ "mds_sessions.session_count")
+
+ laggy = "laggy_since" in info
+
+ state = info['state'].split(":")[1]
+ if laggy:
+ state += "(laggy)"
+
+ # if state == "active" and not laggy:
+ # c_state = self.colorize(state, self.GREEN)
+ # else:
+ # c_state = self.colorize(state, self.YELLOW)
+
+ # Populate based on context of state, e.g. client
+ # ops for an active daemon, replay progress, reconnect
+ # progress
+ activity = ""
+
+ if state == "active":
+ activity = "Reqs: " + self.format_dimless(
+ self.get_rate("mds", info['name'], "mds_server.handle_client_request"),
+ 5
+ ) + "/s"
+
+ metadata = self.get_metadata('mds', info['name'])
+ mds_versions[metadata.get('ceph_version', 'unknown')].append(info['name'])
+ rank_table.append(
+ {
+ "rank": rank,
+ "state": state,
+ "mds": info['name'],
+ "activity": activity,
+ "dns": dns,
+ "inos": inos
+ }
+ )
+
+ else:
+ rank_table.append(
+ {
+ "rank": rank,
+ "state": "failed",
+ "mds": "",
+ "activity": "",
+ "dns": 0,
+ "inos": 0
+ }
+ )
+
+ # Find the standby replays
+ for gid_str, daemon_info in mdsmap['info'].iteritems():
+ if daemon_info['state'] != "up:standby-replay":
+ continue
+
+ inos = self.get_latest("mds", daemon_info['name'], "mds_mem.ino")
+ dns = self.get_latest("mds", daemon_info['name'], "mds.inodes")
+
+ activity = "Evts: " + self.format_dimless(
+ self.get_rate("mds", daemon_info['name'], "mds_log.replay"),
+ 5
+ ) + "/s"
+
+ rank_table.append(
+ {
+ "rank": "{0}-s".format(daemon_info['rank']),
+ "state": "standby-replay",
+ "mds": daemon_info['name'],
+ "activity": activity,
+ "dns": dns,
+ "inos": inos
+ }
+ )
+
+ df = self.get("df")
+ pool_stats = dict([(p['id'], p['stats']) for p in df['pools']])
+ osdmap = self.get("osd_map")
+ pools = dict([(p['pool'], p) for p in osdmap['pools']])
+ metadata_pool_id = mdsmap['metadata_pool']
+ data_pool_ids = mdsmap['data_pools']
+
+ pools_table = []
+ for pool_id in [metadata_pool_id] + data_pool_ids:
+ pool_type = "metadata" if pool_id == metadata_pool_id else "data"
+ stats = pool_stats[pool_id]
+ pools_table.append({
+ "pool": pools[pool_id]['pool_name'],
+ "type": pool_type,
+ "used": stats['bytes_used'],
+ "avail": stats['max_avail']
+ })
+
+ standby_table = []
+ for standby in fsmap['standbys']:
+ metadata = self.get_metadata('mds', standby['name'])
+ mds_versions[metadata.get('ceph_version', 'unknown')].append(standby['name'])
+
+ standby_table.append({
+ 'name': standby['name']
+ })
+
+ return {
+ "filesystem": {
+ "id": fs_id,
+ "name": mdsmap['fs_name'],
+ "client_count": client_count,
+ "clients_url": get_prefixed_url("/clients/{0}/".format(fs_id)),
+ "ranks": rank_table,
+ "pools": pools_table
+ },
+ "standbys": standby_table,
+ "versions": mds_versions
+ }
+
+ def _prime_log(self):
+ def load_buffer(buf, channel_name):
+ result = CommandResult("")
+ self.send_command(result, "mon", "", json.dumps({
+ "prefix": "log last",
+ "format": "json",
+ "channel": channel_name,
+ "num": LOG_BUFFER_SIZE
+ }), "")
+ r, outb, outs = result.wait()
+ if r != 0:
+ # Oh well. We won't let this stop us though.
+ self.log.error("Error fetching log history (r={0}, \"{1}\")".format(
+ r, outs))
+ else:
+ try:
+ lines = json.loads(outb)
+ except ValueError:
+ self.log.error("Error decoding log history")
+ else:
+ for l in lines:
+ buf.appendleft(l)
+
+ load_buffer(self.log_buffer, "cluster")
+ load_buffer(self.audit_buffer, "audit")
+ self.log_primed = True
+
+ def serve(self):
+ current_dir = os.path.dirname(os.path.abspath(__file__))
+
+ jinja_loader = jinja2.FileSystemLoader(current_dir)
+ env = jinja2.Environment(loader=jinja_loader)
+
+ self._prime_log()
+
+ 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
+ """
+ status, data = global_instance().rbd_pool_ls.get()
+ if data is None:
+ log.warning("Failed to get RBD pool list")
+ data = []
+
+ rbd_pools = sorted([
+ {
+ "name": name,
+ "url": get_prefixed_url("/rbd_pool/{0}/".format(name))
+ }
+ 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 = [
+ {
+ "id": f['id'],
+ "name": f['mdsmap']['fs_name'],
+ "url": get_prefixed_url("/filesystem/{0}/".format(f['id']))
+ }
+ for f in fsmap.data['filesystems']
+ ]
+
+ return {
+ 'rbd_pools': rbd_pools,
+ 'rbd_mirroring': rbd_mirroring,
+ 'health_status': self._health_data()['status'],
+ 'filesystems': filesystems
+ }
+
+ class Root(EndPoint):
+ @cherrypy.expose
+ def filesystem(self, fs_id):
+ template = env.get_template("filesystem.html")
+
+ toplevel_data = self._toplevel_data()
+
+ content_data = {
+ "fs_status": global_instance().fs_status(int(fs_id))
+ }
+
+ return template.render(
+ url_prefix = global_instance().url_prefix,
+ 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 filesystem_data(self, fs_id):
+ return global_instance().fs_status(int(fs_id))
+
+ def _clients(self, fs_id):
+ cephfs_clients = global_instance().cephfs_clients.get(fs_id, None)
+ if cephfs_clients is None:
+ cephfs_clients = CephFSClients(global_instance(), fs_id)
+ global_instance().cephfs_clients[fs_id] = cephfs_clients
+
+ status, clients = cephfs_clients.get()
+ #TODO do something sensible with status
+
+ # Decorate the metadata with some fields that will be
+ # indepdendent of whether it's a kernel or userspace
+ # client, so that the javascript doesn't have to grok that.
+ for client in clients:
+ if "ceph_version" in client['client_metadata']:
+ client['type'] = "userspace"
+ client['version'] = client['client_metadata']['ceph_version']
+ client['hostname'] = client['client_metadata']['hostname']
+ elif "kernel_version" in client['client_metadata']:
+ client['type'] = "kernel"
+ client['version'] = client['client_metadata']['kernel_version']
+ client['hostname'] = client['client_metadata']['hostname']
+ else:
+ client['type'] = "unknown"
+ client['version'] = ""
+ client['hostname'] = ""
+
+ return clients
+
+ @cherrypy.expose
+ 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,
+ "fs_name": fs_name,
+ "fscid": fscid,
+ "fs_url": get_prefixed_url("/filesystem/" + fscid_str + "/")
+ }
+
+ template = env.get_template("clients.html")
+ return template.render(
+ url_prefix = global_instance().url_prefix,
+ ceph_version=global_instance().version,
+ path_info=cherrypy.request.path_info,
+ toplevel_data=json.dumps(self._toplevel_data(), indent=2),
+ content_data=json.dumps(content_data, indent=2)
+ )
+
+ @cherrypy.expose
+ @cherrypy.tools.json_out()
+ def clients_data(self, fs_id):
+ return self._clients(int(fs_id))
+
+ def _rbd_pool(self, pool_name):
+ rbd_ls = global_instance().rbd_ls.get(pool_name, None)
+ if rbd_ls is None:
+ rbd_ls = RbdLs(global_instance(), pool_name)
+ global_instance().rbd_ls[pool_name] = rbd_ls
+
+ status, value = rbd_ls.get()
+
+ interval = 5
+
+ wait = interval - rbd_ls.latency
+ def wait_and_load():
+ time.sleep(wait)
+ rbd_ls.get()
+
+ threading.Thread(target=wait_and_load).start()
+
+ assert status != RbdLs.VALUE_NONE # FIXME bubble status up to UI
+ return value
+
+ @cherrypy.expose
+ def rbd_pool(self, pool_name):
+ template = env.get_template("rbd_pool.html")
+
+ toplevel_data = self._toplevel_data()
+
+ images = self._rbd_pool(pool_name)
+ content_data = {
+ "images": images,
+ "pool_name": pool_name
+ }
+
+ return template.render(
+ url_prefix = global_instance().url_prefix,
+ 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_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(
+ url_prefix = global_instance().url_prefix,
+ 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()
+
+ def _rbd_iscsi(self):
+ status, data = global_instance().rbd_iscsi.content_data.get()
+ if data is None:
+ log.warning("Failed to get RBD iSCSI status")
+ return {}
+ return data
+
+ @cherrypy.expose
+ def rbd_iscsi(self):
+ template = env.get_template("rbd_iscsi.html")
+
+ toplevel_data = self._toplevel_data()
+ content_data = self._rbd_iscsi()
+
+ return template.render(
+ url_prefix = global_instance().url_prefix,
+ 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_iscsi_data(self):
+ return self._rbd_iscsi()
+
+ @cherrypy.expose
+ def health(self):
+ template = env.get_template("health.html")
+ return template.render(
+ url_prefix = global_instance().url_prefix,
+ ceph_version=global_instance().version,
+ path_info=cherrypy.request.path_info,
+ toplevel_data=json.dumps(self._toplevel_data(), indent=2),
+ content_data=json.dumps(self._health(), indent=2)
+ )
+
+ @cherrypy.expose
+ def servers(self):
+ template = env.get_template("servers.html")
+ return template.render(
+ url_prefix = global_instance().url_prefix,
+ ceph_version=global_instance().version,
+ path_info=cherrypy.request.path_info,
+ toplevel_data=json.dumps(self._toplevel_data(), indent=2),
+ content_data=json.dumps(self._servers(), indent=2)
+ )
+
+ def _servers(self):
+ return {
+ 'servers': global_instance().list_servers()
+ }
+
+ @cherrypy.expose
+ @cherrypy.tools.json_out()
+ def servers_data(self):
+ return self._servers()
+
+ def _health(self):
+ # Fuse osdmap with pg_summary to get description of pools
+ # including their PG states
+ osd_map = global_instance().get_sync_object(OsdMap).data
+ pg_summary = global_instance().get_sync_object(PgSummary).data
+ pools = []
+
+ if len(global_instance().pool_stats) == 0:
+ global_instance().update_pool_stats()
+
+ for pool in osd_map['pools']:
+ pool['pg_status'] = pg_summary['by_pool'][pool['pool'].__str__()]
+ stats = global_instance().pool_stats[pool['pool']]
+ s = {}
+
+ def get_rate(series):
+ if len(series) >= 2:
+ return (float(series[0][1]) - float(series[1][1])) / (float(series[0][0]) - float(series[1][0]))
+ else:
+ return 0
+
+ for stat_name, stat_series in stats.items():
+ s[stat_name] = {
+ 'latest': stat_series[0][1],
+ 'rate': get_rate(stat_series),
+ 'series': [i for i in stat_series]
+ }
+ pool['stats'] = s
+ pools.append(pool)
+
+ # Not needed, skip the effort of transmitting this
+ # 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,
+ "mgr_map": global_instance().get("mgr_map"),
+ "df": df
+ }
+
+ @cherrypy.expose
+ @cherrypy.tools.json_out()
+ def health_data(self):
+ return self._health()
+
+ @cherrypy.expose
+ def index(self):
+ return self.health()
+
+ @cherrypy.expose
+ @cherrypy.tools.json_out()
+ def toplevel_data(self):
+ return self._toplevel_data()
+
+ def _get_mds_names(self, filesystem_id=None):
+ names = []
+
+ fsmap = global_instance().get("fs_map")
+ for fs in fsmap['filesystems']:
+ if filesystem_id is not None and fs['id'] != filesystem_id:
+ continue
+ names.extend([info['name'] for _, info in fs['mdsmap']['info'].items()])
+
+ if filesystem_id is None:
+ names.extend(info['name'] for info in fsmap['standbys'])
+
+ return names
+
+ @cherrypy.expose
+ @cherrypy.tools.json_out()
+ def mds_counters(self, fs_id):
+ """
+ Result format: map of daemon name to map of counter to list of datapoints
+ """
+
+ # Opinionated list of interesting performance counters for the GUI --
+ # if you need something else just add it. See how simple life is
+ # when you don't have to write general purpose APIs?
+ counters = [
+ "mds_server.handle_client_request",
+ "mds_log.ev",
+ "mds_cache.num_strays",
+ "mds.exported",
+ "mds.exported_inodes",
+ "mds.imported",
+ "mds.imported_inodes",
+ "mds.inodes",
+ "mds.caps",
+ "mds.subtrees"
+ ]
+
+ result = {}
+ mds_names = self._get_mds_names(int(fs_id))
+
+ for mds_name in mds_names:
+ result[mds_name] = {}
+ for counter in counters:
+ data = global_instance().get_counter("mds", mds_name, counter)
+ if data is not None:
+ result[mds_name][counter] = data[counter]
+ else:
+ result[mds_name][counter] = []
+
+ return dict(result)
+
+ @cherrypy.expose
+ @cherrypy.tools.json_out()
+ def get_counter(self, type, id, path):
+ return global_instance().get_counter(type, id, path)
+
+ @cherrypy.expose
+ @cherrypy.tools.json_out()
+ def get_perf_schema(self, **args):
+ type = args.get('type', '')
+ id = args.get('id', '')
+ schema = global_instance().get_perf_schema(type, id)
+ ret = dict()
+ for k1 in schema.keys(): # 'perf_schema'
+ ret[k1] = collections.OrderedDict()
+ for k2 in sorted(schema[k1].keys()):
+ sorted_dict = collections.OrderedDict(
+ sorted(schema[k1][k2].items(), key=lambda i: i[0])
+ )
+ ret[k1][k2] = sorted_dict
+ return ret
+
+ url_prefix = self.get_config('url_prefix')
+ if url_prefix == None:
+ url_prefix = ''
+ else:
+ if len(url_prefix) != 0:
+ if url_prefix[0] != '/':
+ url_prefix = '/'+url_prefix
+ if url_prefix[-1] == '/':
+ url_prefix = url_prefix[:-1]
+ self.url_prefix = url_prefix
+
+ server_addr = self.get_localized_config('server_addr', '::')
+ server_port = self.get_localized_config('server_port', '7000')
+ if server_addr is None:
+ raise RuntimeError('no server_addr configured; try "ceph config-key set mgr/dashboard/server_addr <ip>"')
+ log.info("server_addr: %s server_port: %s" % (server_addr, server_port))
+ cherrypy.config.update({
+ 'server.socket_host': server_addr,
+ 'server.socket_port': int(server_port),
+ 'engine.autoreload.on': False
+ })
+
+ osdmap = self.get_osdmap()
+ log.info("latest osdmap is %d" % osdmap.get_epoch())
+
+ # Publish the URI that others may use to access the service we're
+ # about to start serving
+ self.set_uri("http://{0}:{1}/".format(
+ socket.getfqdn() if server_addr == "::" else server_addr,
+ server_port
+ ))
+
+ static_dir = os.path.join(current_dir, 'static')
+ conf = {
+ "/static": {
+ "tools.staticdir.on": True,
+ 'tools.staticdir.dir': static_dir
+ }
+ }
+ 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(
+ url_prefix = global_instance().url_prefix,
+ ceph_version=global_instance().version,
+ path_info='/osd' + cherrypy.request.path_info,
+ 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'] = get_prefixed_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']
+ 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])
+
+ result[hostname].append(summary)
+
+ result[hostname].sort(key=lambda a: a['id'])
+ if len(result[hostname]):
+ result[hostname][0]['first'] = True
+
+ global_instance().log.warn("result.size {0} servers.size {1}".format(
+ len(result), len(servers)
+ ))
+
+ # Return list form for convenience of rendering
+ return sorted(result.items(), key=lambda a: a[0])
+
+ @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(
+ url_prefix = global_instance().url_prefix,
+ ceph_version=global_instance().version,
+ path_info='/osd' + cherrypy.request.path_info,
+ toplevel_data=json.dumps(toplevel_data, indent=2),
+ content_data=json.dumps(content_data, indent=2)
+ )
+
+ cherrypy.tree.mount(Root(), get_prefixed_url("/"), conf)
+ cherrypy.tree.mount(OSDEndpoint(), get_prefixed_url("/osd"), conf)
+
+ log.info("Starting engine on {0}:{1}...".format(
+ server_addr, server_port))
+ cherrypy.engine.start()
+ log.info("Waiting for engine...")
+ cherrypy.engine.block()
+ log.info("Engine done.")