import sys
import time
import threading
+import socket
import cherrypy
import jinja2
+import urlparse
-from mgr_module import MgrModule, CommandResult
+from mgr_module import MgrModule, MgrStandbyModule, CommandResult
from types import OsdMap, NotFound, Config, FsMap, MonMap, \
PgSummary, Health, MonStatus
import rados
-from rbd_ls import RbdLs
+import rbd_iscsi
+import rbd_mirroring
+from rbd_ls import RbdLs, RbdPoolLs
from cephfs_clients import CephFSClients
-
log = logging.getLogger("dashboard")
LOG_BUFFER_SIZE = 30
# cherrypy likes to sys.exit on error. don't let it take us down too!
-def os_exit_noop():
+def os_exit_noop(*args, **kwargs):
pass
os._exit = os_exit_noop
log.info("%s %d (%s)" % (path, sys.getrefcount(root), root.__class__))
+def get_prefixed_url(url):
+ return global_instance().url_prefix.rstrip('/') + url
+
+
+
+def prepare_url_prefix(url_prefix):
+ """
+ return '' if no prefix, or '/prefix' without slash in the end.
+ """
+ url_prefix = urlparse.urljoin('/', url_prefix)
+ return url_prefix.rstrip('/')
+
+class StandbyModule(MgrStandbyModule):
+ def serve(self):
+ server_addr = self.get_localized_config('server_addr', '::')
+ server_port = self.get_localized_config('server_port', '7000')
+ url_prefix = prepare_url_prefix(self.get_config('url_prefix', default=''))
+
+ 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 default(self, *args, **kwargs):
+ active_uri = module.get_active_uri()
+ if active_uri:
+ log.info("Redirecting to active '{0}'".format(active_uri + "/".join(args)))
+ raise cherrypy.HTTPRedirect(active_uri + "/".join(args))
+ else:
+ template = env.get_template("standby.html")
+ return template.render(delay=5)
+
+ cherrypy.tree.mount(Root(), url_prefix, {})
+ 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):
# 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 = {}
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):
"""
if self._rados:
return self._rados
- from mgr_module import ceph_state
- ctx_capsule = ceph_state.get_context()
+ ctx_capsule = self.get_context()
self._rados = rados.Rados(context=ctx_capsule)
self._rados.connect()
return self._rados
- def get_localized_config(self, key):
- r = self.get_config(self.get_mgr_id() + '/' + key)
- if r is None:
- r = self.get_config(key)
- return r
-
def update_pool_stats(self):
df = global_instance().get("df")
pool_stats = dict([(p['id'], p['stats']) for p in df['pools']])
filesystem = fs
break
+ if filesystem is None:
+ raise cherrypy.HTTPError(404,
+ "Filesystem id {0} not found".format(fs_id))
+
rank_table = []
mdsmap = filesystem['mdsmap']
) + "/s"
metadata = self.get_metadata('mds', info['name'])
- mds_versions[metadata['ceph_version']].append(info['name'])
+ mds_versions[metadata.get('ceph_version', 'unknown')].append(info['name'])
rank_table.append(
{
"rank": rank,
standby_table = []
for standby in fsmap['standbys']:
metadata = self.get_metadata('mds', standby['name'])
- mds_versions[metadata['ceph_version']].append(standby['name'])
+ mds_versions[metadata.get('ceph_version', 'unknown')].append(standby['name'])
standby_table.append({
'name': standby['name']
"id": fs_id,
"name": mdsmap['fs_name'],
"client_count": client_count,
- "clients_url": "/clients/{0}/".format(fs_id),
+ "clients_url": get_prefixed_url("/clients/{0}/".format(fs_id)),
"ranks": rank_table,
"pools": pools_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)
- result = CommandResult("")
- self.send_command(result, "mon", "", json.dumps({
- "prefix":"log last",
- "format": "json"
- }), "")
- 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:
- if l['channel'] == 'audit':
- self.audit_buffer.appendleft(l)
- else:
- self.log_buffer.appendleft(l)
+ self._prime_log()
- self.log_primed = True
+ 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
- class Root(object):
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": "/filesystem/{0}/".format(f['id'])
+ "url": get_prefixed_url("/filesystem/{0}/".format(f['id']))
}
for f in fsmap.data['filesystems']
]
return {
- 'health': global_instance().get_sync_object(Health).data,
+ '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")
}
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)
)
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:
client['hostname'] = client['client_metadata']['hostname']
elif "kernel_version" in client['client_metadata']:
client['type'] = "kernel"
- client['version'] = client['kernel_version']
+ client['version'] = client['client_metadata']['kernel_version']
client['hostname'] = client['client_metadata']['hostname']
else:
client['type'] = "unknown"
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": 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,
- toplevel_data=json.dumps(toplevel_data, indent=2),
+ path_info=cherrypy.request.path_info,
+ toplevel_data=json.dumps(self._toplevel_data(), indent=2),
content_data=json.dumps(content_data, indent=2)
)
def clients_data(self, fs_id):
return self._clients(int(fs_id))
- def _rbd(self, pool_name):
+ 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)
return value
@cherrypy.expose
- def rbd(self, pool_name):
- template = env.get_template("rbd.html")
+ def rbd_pool(self, pool_name):
+ template = env.get_template("rbd_pool.html")
toplevel_data = self._toplevel_data()
- images = self._rbd(pool_name)
+ 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_data(self, pool_name):
- return self._rbd(pool_name)
+ 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)
)
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):
- servers = global_instance().list_servers()
return {
'servers': global_instance().list_servers()
}
# 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": global_instance().get_sync_object(Health).data,
+ "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
return dict(result)
- server_addr = self.get_localized_config('server_addr')
- server_port = self.get_localized_config('server_port') or '7000'
+ @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 = prepare_url_prefix(self.get_config('url_prefix', default=''))
+ 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 put mgr/dashboard/server_addr <ip>"')
+ 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,
'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}{2}/".format(
+ socket.getfqdn() if server_addr == "::" else server_addr,
+ server_port,
+ url_prefix
+ ))
+
static_dir = os.path.join(current_dir, 'static')
conf = {
"/static": {
}
}
log.info("Serving static from {0}".format(static_dir))
- cherrypy.tree.mount(Root(), "/", conf)
- log.info("Starting engine...")
+ 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()