Skip to content

Commit bcbf91d

Browse files
committed
mgr/dashboard: CephFS statfs REST API
Introduce statfs for the CephFS REST API controller, we can easily get statfs of the specified path by it, it returns the used bytes, used files and used subdirs. Fixes: https://tracker.ceph.com/issues/61883 Signed-off-by: Yite Gu <[email protected]>
1 parent 72bd1c3 commit bcbf91d

File tree

4 files changed

+289
-1
lines changed

4 files changed

+289
-1
lines changed

qa/tasks/mgr/dashboard/test_cephfs.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,26 @@ def new_quota_dir(self):
7474
yield 1
7575
self.rm_dir(self.QUOTA_PATH)
7676

77+
def write_to_file(self, path, buf):
78+
params = {'path': path, 'buf': buf}
79+
self._post(f"/api/cephfs/{self.get_fs_id()}/write_to_file",
80+
params=params)
81+
self.assertStatus(200)
82+
83+
def unlink(self, path, expectedStatus=200):
84+
params = {'path': path}
85+
self._delete(f"/api/cephfs/{self.get_fs_id()}/unlink",
86+
params=params)
87+
self.assertStatus(expectedStatus)
88+
89+
def statfs(self, path):
90+
params = {'path': path}
91+
data = self._get(f"/api/cephfs/{self.get_fs_id()}/statfs",
92+
params=params)
93+
self.assertStatus(200)
94+
self.assertIsInstance(data, dict)
95+
return data
96+
7797
@DashboardTestCase.RunAs('test', 'test', ['block-manager'])
7898
def test_access_permissions(self):
7999
fs_id = self.get_fs_id()
@@ -290,3 +310,40 @@ def test_listing_of_ui_api_ls_on_deeper_levels(self):
290310
ui_api_ls = self.ui_ls_dir('/pictures', 0)
291311
self.assertEqual(api_ls, ui_api_ls)
292312
self.rm_dir('/pictures')
313+
314+
def test_statfs(self):
315+
self.statfs('/')
316+
317+
self.mk_dirs('/animal')
318+
stats = self.statfs('/animal')
319+
self.assertEqual(stats['bytes'], 0)
320+
self.assertEqual(stats['files'], 0)
321+
self.assertEqual(stats['subdirs'], 1)
322+
323+
buf = 'a' * 512
324+
self.write_to_file('/animal/lion', buf)
325+
stats = self.statfs('/animal')
326+
self.assertEqual(stats['bytes'], 512)
327+
self.assertEqual(stats['files'], 1)
328+
self.assertEqual(stats['subdirs'], 1)
329+
330+
buf = 'b' * 512
331+
self.write_to_file('/animal/tiger', buf)
332+
stats = self.statfs('/animal')
333+
self.assertEqual(stats['bytes'], 1024)
334+
self.assertEqual(stats['files'], 2)
335+
self.assertEqual(stats['subdirs'], 1)
336+
337+
self.unlink('/animal/tiger')
338+
stats = self.statfs('/animal')
339+
self.assertEqual(stats['bytes'], 512)
340+
self.assertEqual(stats['files'], 1)
341+
self.assertEqual(stats['subdirs'], 1)
342+
343+
self.unlink('/animal/lion')
344+
stats = self.statfs('/animal')
345+
self.assertEqual(stats['bytes'], 0)
346+
self.assertEqual(stats['files'], 0)
347+
self.assertEqual(stats['subdirs'], 1)
348+
349+
self.rm_dir('/animal')

src/pybind/mgr/dashboard/controllers/cephfs.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@
1919
'max_bytes': (int, ''),
2020
'max_files': (int, '')
2121
}
22+
GET_STATFS_SCHEMA = {
23+
'bytes': (int, ''),
24+
'files': (int, ''),
25+
'subdirs': (int, '')
26+
}
2227

2328
logger = logging.getLogger("controllers.rgw")
2429

@@ -461,6 +466,47 @@ def get_quota(self, fs_id, path):
461466
cfs = self._cephfs_instance(fs_id)
462467
return cfs.get_quotas(path)
463468

469+
@RESTController.Resource('POST', path='/write_to_file')
470+
@allow_empty_body
471+
def write_to_file(self, fs_id, path, buf) -> None:
472+
"""
473+
Write some data to the specified path.
474+
:param fs_id: The filesystem identifier.
475+
:param path: The path of the file to write.
476+
:param buf: The str to write to the buf.
477+
"""
478+
cfs = self._cephfs_instance(fs_id)
479+
cfs.write_to_file(path, buf)
480+
481+
@RESTController.Resource('DELETE', path='/unlink')
482+
def unlink(self, fs_id, path) -> None:
483+
"""
484+
Removes a file, link, or symbolic link.
485+
:param fs_id: The filesystem identifier.
486+
:param path: The path of the file or link to unlink.
487+
"""
488+
cfs = self._cephfs_instance(fs_id)
489+
cfs.unlink(path)
490+
491+
@RESTController.Resource('GET', path='/statfs')
492+
@EndpointDoc("Get Cephfs statfs of the specified path",
493+
parameters={
494+
'fs_id': (str, 'File System Identifier'),
495+
'path': (str, 'File System Path'),
496+
},
497+
responses={200: GET_STATFS_SCHEMA})
498+
def statfs(self, fs_id, path) -> dict:
499+
"""
500+
Get the statfs of the specified path.
501+
:param fs_id: The filesystem identifier.
502+
:param path: The path of the directory/file.
503+
:return: Returns a dictionary containing 'bytes',
504+
'files' and 'subdirs'.
505+
:rtype: dict
506+
"""
507+
cfs = self._cephfs_instance(fs_id)
508+
return cfs.statfs(path)
509+
464510
@RESTController.Resource('POST', path='/snapshot')
465511
@allow_empty_body
466512
def snapshot(self, fs_id, path, name=None):

src/pybind/mgr/dashboard/openapi.yaml

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2031,6 +2031,60 @@ paths:
20312031
- jwt: []
20322032
tags:
20332033
- Cephfs
2034+
/api/cephfs/{fs_id}/statfs:
2035+
get:
2036+
description: "\n Get the statfs of the specified path.\n :param\
2037+
\ fs_id: The filesystem identifier.\n :param path: The path of the\
2038+
\ directory/file.\n :return: Returns a dictionary containing 'bytes',\n\
2039+
\ 'files' and 'subdirs'.\n :rtype: dict\n "
2040+
parameters:
2041+
- description: File System Identifier
2042+
in: path
2043+
name: fs_id
2044+
required: true
2045+
schema:
2046+
type: string
2047+
- description: File System Path
2048+
in: query
2049+
name: path
2050+
required: true
2051+
schema:
2052+
type: string
2053+
responses:
2054+
'200':
2055+
content:
2056+
application/vnd.ceph.api.v1.0+json:
2057+
schema:
2058+
properties:
2059+
bytes:
2060+
description: ''
2061+
type: integer
2062+
files:
2063+
description: ''
2064+
type: integer
2065+
subdirs:
2066+
description: ''
2067+
type: integer
2068+
required:
2069+
- bytes
2070+
- files
2071+
- subdirs
2072+
type: object
2073+
description: OK
2074+
'400':
2075+
description: Operation exception. Please check the response body for details.
2076+
'401':
2077+
description: Unauthenticated access. Please login first.
2078+
'403':
2079+
description: Unauthorized access. Please check your permissions.
2080+
'500':
2081+
description: Unexpected error. Please check the response body for the stack
2082+
trace.
2083+
security:
2084+
- jwt: []
2085+
summary: Get Cephfs statfs of the specified path
2086+
tags:
2087+
- Cephfs
20342088
/api/cephfs/{fs_id}/tree:
20352089
delete:
20362090
description: "\n Remove a directory.\n :param fs_id: The filesystem\
@@ -2113,6 +2167,95 @@ paths:
21132167
- jwt: []
21142168
tags:
21152169
- Cephfs
2170+
/api/cephfs/{fs_id}/unlink:
2171+
delete:
2172+
description: "\n Removes a file, link, or symbolic link.\n :param\
2173+
\ fs_id: The filesystem identifier.\n :param path: The path of the\
2174+
\ file or link to unlink.\n "
2175+
parameters:
2176+
- in: path
2177+
name: fs_id
2178+
required: true
2179+
schema:
2180+
type: string
2181+
- in: query
2182+
name: path
2183+
required: true
2184+
schema:
2185+
type: string
2186+
responses:
2187+
'202':
2188+
content:
2189+
application/vnd.ceph.api.v1.0+json:
2190+
type: object
2191+
description: Operation is still executing. Please check the task queue.
2192+
'204':
2193+
content:
2194+
application/vnd.ceph.api.v1.0+json:
2195+
type: object
2196+
description: Resource deleted.
2197+
'400':
2198+
description: Operation exception. Please check the response body for details.
2199+
'401':
2200+
description: Unauthenticated access. Please login first.
2201+
'403':
2202+
description: Unauthorized access. Please check your permissions.
2203+
'500':
2204+
description: Unexpected error. Please check the response body for the stack
2205+
trace.
2206+
security:
2207+
- jwt: []
2208+
tags:
2209+
- Cephfs
2210+
/api/cephfs/{fs_id}/write_to_file:
2211+
post:
2212+
description: "\n Write some data to the specified path.\n :param\
2213+
\ fs_id: The filesystem identifier.\n :param path: The path of the\
2214+
\ file to write.\n :param buf: The str to write to the buf.\n \
2215+
\ "
2216+
parameters:
2217+
- in: path
2218+
name: fs_id
2219+
required: true
2220+
schema:
2221+
type: string
2222+
requestBody:
2223+
content:
2224+
application/json:
2225+
schema:
2226+
properties:
2227+
buf:
2228+
type: string
2229+
path:
2230+
type: string
2231+
required:
2232+
- path
2233+
- buf
2234+
type: object
2235+
responses:
2236+
'201':
2237+
content:
2238+
application/vnd.ceph.api.v1.0+json:
2239+
type: object
2240+
description: Resource created.
2241+
'202':
2242+
content:
2243+
application/vnd.ceph.api.v1.0+json:
2244+
type: object
2245+
description: Operation is still executing. Please check the task queue.
2246+
'400':
2247+
description: Operation exception. Please check the response body for details.
2248+
'401':
2249+
description: Unauthenticated access. Please login first.
2250+
'403':
2251+
description: Unauthorized access. Please check your permissions.
2252+
'500':
2253+
description: Unexpected error. Please check the response body for the stack
2254+
trace.
2255+
security:
2256+
- jwt: []
2257+
tags:
2258+
- Cephfs
21162259
/api/cluster:
21172260
get:
21182261
parameters: []

src/pybind/mgr/dashboard/services/cephfs.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import datetime
44
import logging
55
import os
6-
from contextlib import contextmanager
6+
from contextlib import contextmanager, suppress
77

88
import cephfs
99

@@ -260,3 +260,45 @@ def set_quotas(self, path, max_bytes=None, max_files=None):
260260
if max_files is not None:
261261
self.cfs.setxattr(path, 'ceph.quota.max_files',
262262
str(max_files).encode(), 0)
263+
264+
def write_to_file(self, path, buf) -> None:
265+
"""
266+
Write some data to the specified path.
267+
:param path: The path of the file to write.
268+
:type path: str.
269+
:param buf: The str to write to the buf.
270+
:type buf: str.
271+
"""
272+
try:
273+
fd = self.cfs.open(path, 'w', 644)
274+
self.cfs.write(fd, buf.encode('utf-8'), 0)
275+
except cephfs.Error as e:
276+
logger.debug("EIO: %s", str(e))
277+
finally:
278+
if fd is not None:
279+
self.cfs.close(fd)
280+
281+
def unlink(self, path) -> None:
282+
"""
283+
Removes a file, link, or symbolic link.
284+
:param path: The path of the file or link to unlink.
285+
"""
286+
self.cfs.unlink(path)
287+
288+
def statfs(self, path) -> dict:
289+
"""
290+
Get the statfs of the specified path by xattr.
291+
:param path: The path of the directory/file.
292+
:type path: str
293+
:return: Returns a dictionary containing 'bytes',
294+
'files' and 'subdirs'.
295+
:rtype: dict
296+
"""
297+
rbytes = 0
298+
rfiles = 0
299+
rsubdirs = 0
300+
with suppress(cephfs.NoData):
301+
rbytes = int(self.cfs.getxattr(path, 'ceph.dir.rbytes'))
302+
rfiles = int(self.cfs.getxattr(path, 'ceph.dir.rfiles'))
303+
rsubdirs = int(self.cfs.getxattr(path, 'ceph.dir.rsubdirs'))
304+
return {'bytes': rbytes, 'files': rfiles, 'subdirs': rsubdirs}

0 commit comments

Comments
 (0)