+++ /dev/null
-
-"""
-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.")