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