summaryrefslogtreecommitdiffstats
path: root/src/pybind/mgr/dashboard/controllers/osd.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/pybind/mgr/dashboard/controllers/osd.py')
-rw-r--r--src/pybind/mgr/dashboard/controllers/osd.py242
1 files changed, 242 insertions, 0 deletions
diff --git a/src/pybind/mgr/dashboard/controllers/osd.py b/src/pybind/mgr/dashboard/controllers/osd.py
new file mode 100644
index 00000000..a971a512
--- /dev/null
+++ b/src/pybind/mgr/dashboard/controllers/osd.py
@@ -0,0 +1,242 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+from mgr_util import get_most_recent_rate
+
+from . import ApiController, RESTController, UpdatePermission, \
+ allow_empty_body
+from .. import mgr, logger
+from ..security import Scope
+from ..services.ceph_service import CephService, SendCommandError
+from ..services.exception import handle_send_command_error
+from ..tools import str_to_bool
+try:
+ from typing import Dict, List, Any, Union # pylint: disable=unused-import
+except ImportError:
+ pass # For typing only
+
+
+@ApiController('/osd', Scope.OSD)
+class Osd(RESTController):
+ def list(self):
+ osds = self.get_osd_map()
+
+ # Extending by osd stats information
+ for stat in mgr.get('osd_stats')['osd_stats']:
+ if stat['osd'] in osds:
+ osds[stat['osd']]['osd_stats'] = stat
+
+ # Extending by osd node information
+ nodes = mgr.get('osd_map_tree')['nodes']
+ for node in nodes:
+ if node['type'] == 'osd' and node['id'] in osds:
+ osds[node['id']]['tree'] = node
+
+ # Extending by osd parent node information
+ for host in [n for n in nodes if n['type'] == 'host']:
+ for osd_id in host['children']:
+ if osd_id >= 0 and osd_id in osds:
+ osds[osd_id]['host'] = host
+
+ # Extending by osd histogram data
+ for osd_id, osd in osds.items():
+ osd['stats'] = {}
+ osd['stats_history'] = {}
+ osd_spec = str(osd_id)
+ if 'osd' not in osd:
+ continue
+ for stat in ['osd.op_w', 'osd.op_in_bytes', 'osd.op_r', 'osd.op_out_bytes']:
+ prop = stat.split('.')[1]
+ rates = CephService.get_rates('osd', osd_spec, stat)
+ osd['stats'][prop] = get_most_recent_rate(rates)
+ osd['stats_history'][prop] = rates
+ # Gauge stats
+ for stat in ['osd.numpg', 'osd.stat_bytes', 'osd.stat_bytes_used']:
+ osd['stats'][stat.split('.')[1]] = mgr.get_latest('osd', osd_spec, stat)
+
+ return list(osds.values())
+
+ @staticmethod
+ def get_osd_map(svc_id=None):
+ # type: (Union[int, None]) -> Dict[int, Union[Dict[str, Any], Any]]
+ def add_id(osd):
+ osd['id'] = osd['osd']
+ return osd
+ resp = {
+ osd['osd']: add_id(osd)
+ for osd in mgr.get('osd_map')['osds'] if svc_id is None or osd['osd'] == int(svc_id)
+ }
+ return resp if svc_id is None else resp[int(svc_id)]
+
+ @handle_send_command_error('osd')
+ def get(self, svc_id):
+ """
+ Returns collected data about an OSD.
+
+ :return: Returns the requested data. The `histogram` key man contain a
+ string with an error that occurred when the OSD is down.
+ """
+ try:
+ histogram = CephService.send_command('osd', srv_spec=svc_id,
+ prefix='perf histogram dump')
+ except SendCommandError as e:
+ if 'osd down' in str(e):
+ histogram = str(e)
+ else:
+ raise
+
+ return {
+ 'osd_map': self.get_osd_map(svc_id),
+ 'osd_metadata': mgr.get_metadata('osd', svc_id),
+ 'histogram': histogram,
+ }
+
+ @RESTController.Resource('POST', query_params=['deep'])
+ @UpdatePermission
+ @allow_empty_body
+ def scrub(self, svc_id, deep=False):
+ api_scrub = "osd deep-scrub" if str_to_bool(deep) else "osd scrub"
+ CephService.send_command("mon", api_scrub, who=svc_id)
+
+ @RESTController.Resource('POST')
+ @allow_empty_body
+ def mark_out(self, svc_id):
+ CephService.send_command('mon', 'osd out', ids=[svc_id])
+
+ @RESTController.Resource('POST')
+ @allow_empty_body
+ def mark_in(self, svc_id):
+ CephService.send_command('mon', 'osd in', ids=[svc_id])
+
+ @RESTController.Resource('POST')
+ @allow_empty_body
+ def mark_down(self, svc_id):
+ CephService.send_command('mon', 'osd down', ids=[svc_id])
+
+ @RESTController.Resource('POST')
+ @allow_empty_body
+ def reweight(self, svc_id, weight):
+ """
+ Reweights the OSD temporarily.
+
+ Note that ‘ceph osd reweight’ is not a persistent setting. When an OSD
+ gets marked out, the osd weight will be set to 0. When it gets marked
+ in again, the weight will be changed to 1.
+
+ Because of this ‘ceph osd reweight’ is a temporary solution. You should
+ only use it to keep your cluster running while you’re ordering more
+ hardware.
+
+ - Craig Lewis (http://lists.ceph.com/pipermail/ceph-users-ceph.com/2014-June/040967.html)
+ """
+ CephService.send_command(
+ 'mon',
+ 'osd reweight',
+ id=int(svc_id),
+ weight=float(weight))
+
+ @RESTController.Resource('POST')
+ @allow_empty_body
+ def mark_lost(self, svc_id):
+ """
+ Note: osd must be marked `down` before marking lost.
+ """
+ CephService.send_command(
+ 'mon',
+ 'osd lost',
+ id=int(svc_id),
+ yes_i_really_mean_it=True)
+
+ def create(self, uuid=None, svc_id=None):
+ """
+ :param uuid: Will be set automatically if the OSD starts up.
+ :param id: The ID is only used if a valid uuid is given.
+ :return:
+ """
+ result = CephService.send_command(
+ 'mon', 'osd create', id=int(svc_id), uuid=uuid)
+ return {
+ 'result': result,
+ 'svc_id': int(svc_id),
+ 'uuid': uuid,
+ }
+
+ @RESTController.Resource('POST')
+ @allow_empty_body
+ def purge(self, svc_id):
+ """
+ Note: osd must be marked `down` before removal.
+ """
+ CephService.send_command('mon', 'osd purge-actual', id=int(svc_id),
+ yes_i_really_mean_it=True)
+
+ @RESTController.Resource('POST')
+ @allow_empty_body
+ def destroy(self, svc_id):
+ """
+ Mark osd as being destroyed. Keeps the ID intact (allowing reuse), but
+ removes cephx keys, config-key data and lockbox keys, rendering data
+ permanently unreadable.
+
+ The osd must be marked down before being destroyed.
+ """
+ CephService.send_command(
+ 'mon', 'osd destroy-actual', id=int(svc_id), yes_i_really_mean_it=True)
+
+ @RESTController.Resource('GET')
+ def safe_to_destroy(self, svc_id):
+ """
+ :type svc_id: int|[int]
+ """
+ if not isinstance(svc_id, list):
+ svc_id = [svc_id]
+ svc_id = list(map(str, svc_id))
+ try:
+ result = CephService.send_command(
+ 'mon', 'osd safe-to-destroy', ids=svc_id, target=('mgr', ''))
+ result['is_safe_to_destroy'] = set(result['safe_to_destroy']) == set(map(int, svc_id))
+ return result
+
+ except SendCommandError as e:
+ return {
+ 'message': str(e),
+ 'is_safe_to_destroy': False,
+ }
+
+
+@ApiController('/osd/flags', Scope.OSD)
+class OsdFlagsController(RESTController):
+ @staticmethod
+ def _osd_flags():
+ enabled_flags = mgr.get('osd_map')['flags_set']
+ if 'pauserd' in enabled_flags and 'pausewr' in enabled_flags:
+ # 'pause' is set by calling `ceph osd set pause` and unset by
+ # calling `set osd unset pause`, but `ceph osd dump | jq '.flags'`
+ # will contain 'pauserd,pausewr' if pause is set.
+ # Let's pretend to the API that 'pause' is in fact a proper flag.
+ enabled_flags = list(
+ set(enabled_flags) - {'pauserd', 'pausewr'} | {'pause'})
+ return sorted(enabled_flags)
+
+ def list(self):
+ return self._osd_flags()
+
+ def bulk_set(self, flags):
+ """
+ The `recovery_deletes`, `sortbitwise` and `pglog_hardlimit` flags cannot be unset.
+ `purged_snapshots` cannot even be set. It is therefore required to at
+ least include those four flags for a successful operation.
+ """
+ assert isinstance(flags, list)
+
+ enabled_flags = set(self._osd_flags())
+ data = set(flags)
+ added = data - enabled_flags
+ removed = enabled_flags - data
+ for flag in added:
+ CephService.send_command('mon', 'osd set', '', key=flag)
+ for flag in removed:
+ CephService.send_command('mon', 'osd unset', '', key=flag)
+ logger.info('Changed OSD flags: added=%s removed=%s', added, removed)
+
+ return sorted(enabled_flags - removed | added)