]> git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/dashboard/controllers/cephfs.py
import ceph 15.2.10
[ceph.git] / ceph / src / pybind / mgr / dashboard / controllers / cephfs.py
1 # -*- coding: utf-8 -*-
2 from __future__ import absolute_import
3
4 from collections import defaultdict
5
6 import os
7
8 import cherrypy
9 import cephfs
10
11 from . import ApiController, ControllerDoc, RESTController, UiApiController, \
12 allow_empty_body
13 from .. import mgr
14 from ..exceptions import DashboardException
15 from ..security import Scope
16 from ..services.cephfs import CephFS as CephFS_
17 from ..services.ceph_service import CephService
18 from ..tools import ViewCache
19
20
21 @ApiController('/cephfs', Scope.CEPHFS)
22 class CephFS(RESTController):
23 def __init__(self): # pragma: no cover
24 super(CephFS, self).__init__()
25
26 # Stateful instances of CephFSClients, hold cached results. Key to
27 # dict is FSCID
28 self.cephfs_clients = {}
29
30 def list(self):
31 fsmap = mgr.get("fs_map")
32 return fsmap['filesystems']
33
34 def get(self, fs_id):
35 fs_id = self.fs_id_to_int(fs_id)
36 return self.fs_status(fs_id)
37
38 @RESTController.Resource('GET')
39 def clients(self, fs_id):
40 fs_id = self.fs_id_to_int(fs_id)
41
42 return self._clients(fs_id)
43
44 @RESTController.Resource('DELETE', path='/client/{client_id}')
45 def evict(self, fs_id, client_id):
46 fs_id = self.fs_id_to_int(fs_id)
47 client_id = self.client_id_to_int(client_id)
48
49 return self._evict(fs_id, client_id)
50
51 @RESTController.Resource('GET')
52 def mds_counters(self, fs_id, counters=None):
53 fs_id = self.fs_id_to_int(fs_id)
54 return self._mds_counters(fs_id, counters)
55
56 def _mds_counters(self, fs_id, counters=None):
57 """
58 Result format: map of daemon name to map of counter to list of datapoints
59 rtype: dict[str, dict[str, list]]
60 """
61
62 if counters is None:
63 # Opinionated list of interesting performance counters for the GUI
64 counters = [
65 "mds_server.handle_client_request",
66 "mds_log.ev",
67 "mds_cache.num_strays",
68 "mds.exported",
69 "mds.exported_inodes",
70 "mds.imported",
71 "mds.imported_inodes",
72 "mds.inodes",
73 "mds.caps",
74 "mds.subtrees",
75 "mds_mem.ino"
76 ]
77
78 result = {} # type: dict
79 mds_names = self._get_mds_names(fs_id)
80
81 for mds_name in mds_names:
82 result[mds_name] = {}
83 for counter in counters:
84 data = mgr.get_counter("mds", mds_name, counter)
85 if data is not None:
86 result[mds_name][counter] = data[counter]
87 else:
88 result[mds_name][counter] = []
89
90 return dict(result)
91
92 @staticmethod
93 def fs_id_to_int(fs_id):
94 try:
95 return int(fs_id)
96 except ValueError:
97 raise DashboardException(code='invalid_cephfs_id',
98 msg="Invalid cephfs ID {}".format(fs_id),
99 component='cephfs')
100
101 @staticmethod
102 def client_id_to_int(client_id):
103 try:
104 return int(client_id)
105 except ValueError:
106 raise DashboardException(code='invalid_cephfs_client_id',
107 msg="Invalid cephfs client ID {}".format(client_id),
108 component='cephfs')
109
110 def _get_mds_names(self, filesystem_id=None):
111 names = []
112
113 fsmap = mgr.get("fs_map")
114 for fs in fsmap['filesystems']:
115 if filesystem_id is not None and fs['id'] != filesystem_id:
116 continue
117 names.extend([info['name']
118 for _, info in fs['mdsmap']['info'].items()])
119
120 if filesystem_id is None:
121 names.extend(info['name'] for info in fsmap['standbys'])
122
123 return names
124
125 def _append_mds_metadata(self, mds_versions, metadata_key):
126 metadata = mgr.get_metadata('mds', metadata_key)
127 if metadata is None:
128 return
129 mds_versions[metadata.get('ceph_version', 'unknown')].append(metadata_key)
130
131 # pylint: disable=too-many-statements,too-many-branches
132 def fs_status(self, fs_id):
133 mds_versions = defaultdict(list) # type: dict
134
135 fsmap = mgr.get("fs_map")
136 filesystem = None
137 for fs in fsmap['filesystems']:
138 if fs['id'] == fs_id:
139 filesystem = fs
140 break
141
142 if filesystem is None:
143 raise cherrypy.HTTPError(404,
144 "CephFS id {0} not found".format(fs_id))
145
146 rank_table = []
147
148 mdsmap = filesystem['mdsmap']
149
150 client_count = 0
151
152 for rank in mdsmap["in"]:
153 up = "mds_{0}".format(rank) in mdsmap["up"]
154 if up:
155 gid = mdsmap['up']["mds_{0}".format(rank)]
156 info = mdsmap['info']['gid_{0}'.format(gid)]
157 dns = mgr.get_latest("mds", info['name'], "mds_mem.dn")
158 inos = mgr.get_latest("mds", info['name'], "mds_mem.ino")
159
160 if rank == 0:
161 client_count = mgr.get_latest("mds", info['name'],
162 "mds_sessions.session_count")
163 elif client_count == 0:
164 # In case rank 0 was down, look at another rank's
165 # sessionmap to get an indication of clients.
166 client_count = mgr.get_latest("mds", info['name'],
167 "mds_sessions.session_count")
168
169 laggy = "laggy_since" in info
170
171 state = info['state'].split(":")[1]
172 if laggy:
173 state += "(laggy)"
174
175 # Populate based on context of state, e.g. client
176 # ops for an active daemon, replay progress, reconnect
177 # progress
178 if state == "active":
179 activity = CephService.get_rate("mds",
180 info['name'],
181 "mds_server.handle_client_request")
182 else:
183 activity = 0.0 # pragma: no cover
184
185 self._append_mds_metadata(mds_versions, info['name'])
186 rank_table.append(
187 {
188 "rank": rank,
189 "state": state,
190 "mds": info['name'],
191 "activity": activity,
192 "dns": dns,
193 "inos": inos
194 }
195 )
196
197 else:
198 rank_table.append(
199 {
200 "rank": rank,
201 "state": "failed",
202 "mds": "",
203 "activity": 0.0,
204 "dns": 0,
205 "inos": 0
206 }
207 )
208
209 # Find the standby replays
210 # pylint: disable=unused-variable
211 for gid_str, daemon_info in mdsmap['info'].items():
212 if daemon_info['state'] != "up:standby-replay":
213 continue
214
215 inos = mgr.get_latest("mds", daemon_info['name'], "mds_mem.ino")
216 dns = mgr.get_latest("mds", daemon_info['name'], "mds_mem.dn")
217
218 activity = CephService.get_rate(
219 "mds", daemon_info['name'], "mds_log.replay")
220
221 rank_table.append(
222 {
223 "rank": "{0}-s".format(daemon_info['rank']),
224 "state": "standby-replay",
225 "mds": daemon_info['name'],
226 "activity": activity,
227 "dns": dns,
228 "inos": inos
229 }
230 )
231
232 df = mgr.get("df")
233 pool_stats = {p['id']: p['stats'] for p in df['pools']}
234 osdmap = mgr.get("osd_map")
235 pools = {p['pool']: p for p in osdmap['pools']}
236 metadata_pool_id = mdsmap['metadata_pool']
237 data_pool_ids = mdsmap['data_pools']
238
239 pools_table = []
240 for pool_id in [metadata_pool_id] + data_pool_ids:
241 pool_type = "metadata" if pool_id == metadata_pool_id else "data"
242 stats = pool_stats[pool_id]
243 pools_table.append({
244 "pool": pools[pool_id]['pool_name'],
245 "type": pool_type,
246 "used": stats['bytes_used'],
247 "avail": stats['max_avail']
248 })
249
250 standby_table = []
251 for standby in fsmap['standbys']:
252 self._append_mds_metadata(mds_versions, standby['name'])
253 standby_table.append({
254 'name': standby['name']
255 })
256
257 return {
258 "cephfs": {
259 "id": fs_id,
260 "name": mdsmap['fs_name'],
261 "client_count": client_count,
262 "ranks": rank_table,
263 "pools": pools_table
264 },
265 "standbys": standby_table,
266 "versions": mds_versions
267 }
268
269 def _clients(self, fs_id):
270 cephfs_clients = self.cephfs_clients.get(fs_id, None)
271 if cephfs_clients is None:
272 cephfs_clients = CephFSClients(mgr, fs_id)
273 self.cephfs_clients[fs_id] = cephfs_clients
274
275 try:
276 status, clients = cephfs_clients.get()
277 except AttributeError:
278 raise cherrypy.HTTPError(404,
279 "No cephfs with id {0}".format(fs_id))
280
281 if clients is None:
282 raise cherrypy.HTTPError(404,
283 "No cephfs with id {0}".format(fs_id))
284
285 # Decorate the metadata with some fields that will be
286 # indepdendent of whether it's a kernel or userspace
287 # client, so that the javascript doesn't have to grok that.
288 for client in clients:
289 if "ceph_version" in client['client_metadata']: # pragma: no cover - no complexity
290 client['type'] = "userspace"
291 client['version'] = client['client_metadata']['ceph_version']
292 client['hostname'] = client['client_metadata']['hostname']
293 client['root'] = client['client_metadata']['root']
294 elif "kernel_version" in client['client_metadata']: # pragma: no cover - no complexity
295 client['type'] = "kernel"
296 client['version'] = client['client_metadata']['kernel_version']
297 client['hostname'] = client['client_metadata']['hostname']
298 client['root'] = client['client_metadata']['root']
299 else: # pragma: no cover - no complexity there
300 client['type'] = "unknown"
301 client['version'] = ""
302 client['hostname'] = ""
303
304 return {
305 'status': status,
306 'data': clients
307 }
308
309 def _evict(self, fs_id, client_id):
310 clients = self._clients(fs_id)
311 if not [c for c in clients['data'] if c['id'] == client_id]:
312 raise cherrypy.HTTPError(404,
313 "Client {0} does not exist in cephfs {1}".format(client_id,
314 fs_id))
315 CephService.send_command('mds', 'client evict',
316 srv_spec='{0}:0'.format(fs_id), id=client_id)
317
318 @staticmethod
319 def _cephfs_instance(fs_id):
320 """
321 :param fs_id: The filesystem identifier.
322 :type fs_id: int | str
323 :return: A instance of the CephFS class.
324 """
325 fs_name = CephFS_.fs_name_from_id(fs_id)
326 if fs_name is None:
327 raise cherrypy.HTTPError(404, "CephFS id {} not found".format(fs_id))
328 return CephFS_(fs_name)
329
330 @RESTController.Resource('GET')
331 def get_root_directory(self, fs_id):
332 """
333 The root directory that can't be fetched using ls_dir (api).
334 :param fs_id: The filesystem identifier.
335 :return: The root directory
336 :rtype: dict
337 """
338 try:
339 return self._get_root_directory(self._cephfs_instance(fs_id))
340 except (cephfs.PermissionError, cephfs.ObjectNotFound): # pragma: no cover
341 return None
342
343 def _get_root_directory(self, cfs):
344 """
345 The root directory that can't be fetched using ls_dir (api).
346 It's used in ls_dir (ui-api) and in get_root_directory (api).
347 :param cfs: CephFS service instance
348 :type cfs: CephFS
349 :return: The root directory
350 :rtype: dict
351 """
352 return cfs.get_directory(os.sep.encode())
353
354 @RESTController.Resource('GET')
355 def ls_dir(self, fs_id, path=None, depth=1):
356 """
357 List directories of specified path.
358 :param fs_id: The filesystem identifier.
359 :param path: The path where to start listing the directory content.
360 Defaults to '/' if not set.
361 :type path: str | bytes
362 :param depth: The number of steps to go down the directory tree.
363 :type depth: int | str
364 :return: The names of the directories below the specified path.
365 :rtype: list
366 """
367 path = self._set_ls_dir_path(path)
368 try:
369 cfs = self._cephfs_instance(fs_id)
370 paths = cfs.ls_dir(path, depth)
371 except (cephfs.PermissionError, cephfs.ObjectNotFound): # pragma: no cover
372 paths = []
373 return paths
374
375 def _set_ls_dir_path(self, path):
376 """
377 Transforms input path parameter of ls_dir methods (api and ui-api).
378 :param path: The path where to start listing the directory content.
379 Defaults to '/' if not set.
380 :type path: str | bytes
381 :return: Normalized path or root path
382 :return: str
383 """
384 if path is None:
385 path = os.sep
386 else:
387 path = os.path.normpath(path)
388 return path
389
390 @RESTController.Resource('POST')
391 @allow_empty_body
392 def mk_dirs(self, fs_id, path):
393 """
394 Create a directory.
395 :param fs_id: The filesystem identifier.
396 :param path: The path of the directory.
397 """
398 cfs = self._cephfs_instance(fs_id)
399 cfs.mk_dirs(path)
400
401 @RESTController.Resource('POST')
402 @allow_empty_body
403 def rm_dir(self, fs_id, path):
404 """
405 Remove a directory.
406 :param fs_id: The filesystem identifier.
407 :param path: The path of the directory.
408 """
409 cfs = self._cephfs_instance(fs_id)
410 cfs.rm_dir(path)
411
412 @RESTController.Resource('POST')
413 @allow_empty_body
414 def mk_snapshot(self, fs_id, path, name=None):
415 """
416 Create a snapshot.
417 :param fs_id: The filesystem identifier.
418 :param path: The path of the directory.
419 :param name: The name of the snapshot. If not specified,
420 a name using the current time in RFC3339 UTC format
421 will be generated.
422 :return: The name of the snapshot.
423 :rtype: str
424 """
425 cfs = self._cephfs_instance(fs_id)
426 return cfs.mk_snapshot(path, name)
427
428 @RESTController.Resource('POST')
429 @allow_empty_body
430 def rm_snapshot(self, fs_id, path, name):
431 """
432 Remove a snapshot.
433 :param fs_id: The filesystem identifier.
434 :param path: The path of the directory.
435 :param name: The name of the snapshot.
436 """
437 cfs = self._cephfs_instance(fs_id)
438 cfs.rm_snapshot(path, name)
439
440 @RESTController.Resource('GET')
441 def get_quotas(self, fs_id, path):
442 """
443 Get the quotas of the specified path.
444 :param fs_id: The filesystem identifier.
445 :param path: The path of the directory/file.
446 :return: Returns a dictionary containing 'max_bytes'
447 and 'max_files'.
448 :rtype: dict
449 """
450 cfs = self._cephfs_instance(fs_id)
451 return cfs.get_quotas(path)
452
453 @RESTController.Resource('POST')
454 @allow_empty_body
455 def set_quotas(self, fs_id, path, max_bytes=None, max_files=None):
456 """
457 Set the quotas of the specified path.
458 :param fs_id: The filesystem identifier.
459 :param path: The path of the directory/file.
460 :param max_bytes: The byte limit.
461 :param max_files: The file limit.
462 """
463 cfs = self._cephfs_instance(fs_id)
464 return cfs.set_quotas(path, max_bytes, max_files)
465
466
467 class CephFSClients(object):
468 def __init__(self, module_inst, fscid):
469 self._module = module_inst
470 self.fscid = fscid
471
472 @ViewCache()
473 def get(self):
474 return CephService.send_command('mds', 'session ls', srv_spec='{0}:0'.format(self.fscid))
475
476
477 @UiApiController('/cephfs', Scope.CEPHFS)
478 @ControllerDoc("Dashboard UI helper function; not part of the public API", "CephFSUi")
479 class CephFsUi(CephFS):
480 RESOURCE_ID = 'fs_id'
481
482 @RESTController.Resource('GET')
483 def tabs(self, fs_id):
484 data = {}
485 fs_id = self.fs_id_to_int(fs_id)
486
487 # Needed for detail tab
488 fs_status = self.fs_status(fs_id)
489 for pool in fs_status['cephfs']['pools']:
490 pool['size'] = pool['used'] + pool['avail']
491 data['pools'] = fs_status['cephfs']['pools']
492 data['ranks'] = fs_status['cephfs']['ranks']
493 data['name'] = fs_status['cephfs']['name']
494 data['standbys'] = ', '.join([x['name'] for x in fs_status['standbys']])
495 counters = self._mds_counters(fs_id)
496 for k, v in counters.items():
497 v['name'] = k
498 data['mds_counters'] = counters
499
500 # Needed for client tab
501 data['clients'] = self._clients(fs_id)
502
503 return data
504
505 @RESTController.Resource('GET')
506 def ls_dir(self, fs_id, path=None, depth=1):
507 """
508 The difference to the API version is that the root directory will be send when listing
509 the root directory.
510 To only do one request this endpoint was created.
511 :param fs_id: The filesystem identifier.
512 :type fs_id: int | str
513 :param path: The path where to start listing the directory content.
514 Defaults to '/' if not set.
515 :type path: str | bytes
516 :param depth: The number of steps to go down the directory tree.
517 :type depth: int | str
518 :return: The names of the directories below the specified path.
519 :rtype: list
520 """
521 path = self._set_ls_dir_path(path)
522 try:
523 cfs = self._cephfs_instance(fs_id)
524 paths = cfs.ls_dir(path, depth)
525 if path == os.sep:
526 paths = [self._get_root_directory(cfs)] + paths
527 except (cephfs.PermissionError, cephfs.ObjectNotFound): # pragma: no cover
528 paths = []
529 return paths