]> git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/volumes/fs/operations/versions/subvolume_base.py
bump version to 18.2.4-pve3
[ceph.git] / ceph / src / pybind / mgr / volumes / fs / operations / versions / subvolume_base.py
1 import os
2 import stat
3
4 import errno
5 import logging
6 import hashlib
7 from typing import Dict, Union
8 from pathlib import Path
9
10 import cephfs
11
12 from ..pin_util import pin
13 from .subvolume_attrs import SubvolumeTypes
14 from .metadata_manager import MetadataManager
15 from ..trash import create_trashcan, open_trashcan
16 from ...fs_util import get_ancestor_xattr
17 from ...exception import MetadataMgrException, VolumeException
18 from .auth_metadata import AuthMetadataManager
19 from .subvolume_attrs import SubvolumeStates
20
21 log = logging.getLogger(__name__)
22
23
24 class SubvolumeBase(object):
25 LEGACY_CONF_DIR = "_legacy"
26
27 def __init__(self, mgr, fs, vol_spec, group, subvolname, legacy=False):
28 self.mgr = mgr
29 self.fs = fs
30 self.auth_mdata_mgr = AuthMetadataManager(fs)
31 self.cmode = None
32 self.user_id = None
33 self.group_id = None
34 self.vol_spec = vol_spec
35 self.group = group
36 self.subvolname = subvolname
37 self.legacy_mode = legacy
38 self.load_config()
39
40 @property
41 def uid(self):
42 return self.user_id
43
44 @uid.setter
45 def uid(self, val):
46 self.user_id = val
47
48 @property
49 def gid(self):
50 return self.group_id
51
52 @gid.setter
53 def gid(self, val):
54 self.group_id = val
55
56 @property
57 def mode(self):
58 return self.cmode
59
60 @mode.setter
61 def mode(self, val):
62 self.cmode = val
63
64 @property
65 def base_path(self):
66 return os.path.join(self.group.path, self.subvolname.encode('utf-8'))
67
68 @property
69 def config_path(self):
70 return os.path.join(self.base_path, b".meta")
71
72 @property
73 def legacy_dir(self):
74 return (os.path.join(self.vol_spec.base_dir.encode('utf-8'),
75 SubvolumeBase.LEGACY_CONF_DIR.encode('utf-8')))
76
77 @property
78 def legacy_config_path(self):
79 try:
80 m = hashlib.md5(self.base_path)
81 except ValueError:
82 try:
83 m = hashlib.md5(self.base_path, usedforsecurity=False) # type: ignore
84 except TypeError:
85 raise VolumeException(-errno.EINVAL,
86 "require python's hashlib library to support usedforsecurity flag in FIPS enabled systems")
87
88 meta_config = "{0}.meta".format(m.hexdigest())
89 return os.path.join(self.legacy_dir, meta_config.encode('utf-8'))
90
91 @property
92 def namespace(self):
93 return "{0}{1}".format(self.vol_spec.fs_namespace, self.subvolname)
94
95 @property
96 def group_name(self):
97 return self.group.group_name
98
99 @property
100 def subvol_name(self):
101 return self.subvolname
102
103 @property
104 def legacy_mode(self):
105 return self.legacy
106
107 @legacy_mode.setter
108 def legacy_mode(self, mode):
109 self.legacy = mode
110
111 @property
112 def path(self):
113 """ Path to subvolume data directory """
114 raise NotImplementedError
115
116 @property
117 def features(self):
118 """
119 List of features supported by the subvolume,
120 containing items from SubvolumeFeatures
121 """
122 raise NotImplementedError
123
124 @property
125 def state(self):
126 """ Subvolume state, one of SubvolumeStates """
127 return SubvolumeStates.from_value(self.metadata_mgr.get_global_option(MetadataManager.GLOBAL_META_KEY_STATE))
128
129 @property
130 def subvol_type(self):
131 return (SubvolumeTypes.from_value(self.metadata_mgr.get_global_option
132 (MetadataManager.GLOBAL_META_KEY_TYPE)))
133
134 @property
135 def purgeable(self):
136 """ Boolean declaring if subvolume can be purged """
137 raise NotImplementedError
138
139 def clean_stale_snapshot_metadata(self):
140 """ Clean up stale snapshot metadata """
141 raise NotImplementedError
142
143 def load_config(self):
144 try:
145 self.fs.stat(self.legacy_config_path)
146 self.legacy_mode = True
147 except cephfs.Error:
148 pass
149
150 log.debug("loading config "
151 "'{0}' [mode: {1}]".format(self.subvolname, "legacy"
152 if self.legacy_mode else "new"))
153 if self.legacy_mode:
154 self.metadata_mgr = MetadataManager(self.fs,
155 self.legacy_config_path,
156 0o640)
157 else:
158 self.metadata_mgr = MetadataManager(self.fs,
159 self.config_path, 0o640)
160
161 def get_attrs(self, pathname):
162 # get subvolume attributes
163 attrs: Dict[str, Union[int, str, None]] = {}
164 stx = self.fs.statx(pathname,
165 cephfs.CEPH_STATX_UID | cephfs.CEPH_STATX_GID
166 | cephfs.CEPH_STATX_MODE,
167 cephfs.AT_SYMLINK_NOFOLLOW)
168
169 attrs["uid"] = int(stx["uid"])
170 attrs["gid"] = int(stx["gid"])
171 attrs["mode"] = int(int(stx["mode"]) & ~stat.S_IFMT(stx["mode"]))
172
173 try:
174 attrs["data_pool"] = self.fs.getxattr(pathname,
175 'ceph.dir.layout.pool'
176 ).decode('utf-8')
177 except cephfs.NoData:
178 attrs["data_pool"] = None
179
180 try:
181 attrs["pool_namespace"] = self.fs.getxattr(pathname,
182 'ceph.dir.layout'
183 '.pool_namespace'
184 ).decode('utf-8')
185 except cephfs.NoData:
186 attrs["pool_namespace"] = None
187
188 try:
189 attrs["quota"] = int(self.fs.getxattr(pathname,
190 'ceph.quota.max_bytes'
191 ).decode('utf-8'))
192 except cephfs.NoData:
193 attrs["quota"] = None
194
195 return attrs
196
197 def set_attrs(self, path, attrs):
198 # set subvolume attributes
199 # set size
200 quota = attrs.get("quota")
201 if quota is not None:
202 try:
203 self.fs.setxattr(path, 'ceph.quota.max_bytes',
204 str(quota).encode('utf-8'), 0)
205 except cephfs.InvalidValue:
206 raise VolumeException(-errno.EINVAL,
207 "invalid size specified: '{0}'".format(quota))
208 except cephfs.Error as e:
209 raise VolumeException(-e.args[0], e.args[1])
210
211 # set pool layout
212 data_pool = attrs.get("data_pool")
213 if data_pool is not None:
214 try:
215 self.fs.setxattr(path, 'ceph.dir.layout.pool',
216 data_pool.encode('utf-8'), 0)
217 except cephfs.InvalidValue:
218 raise VolumeException(-errno.EINVAL,
219 "invalid pool layout '{0}'"
220 "--need a valid data pool"
221 .format(data_pool))
222 except cephfs.Error as e:
223 raise VolumeException(-e.args[0], e.args[1])
224
225 # isolate namespace
226 xattr_key = xattr_val = None
227 pool_namespace = attrs.get("pool_namespace")
228 if pool_namespace is not None:
229 # enforce security isolation, use separate namespace
230 # for this subvolume
231 xattr_key = 'ceph.dir.layout.pool_namespace'
232 xattr_val = pool_namespace
233 elif not data_pool:
234 # If subvolume's namespace layout is not set,
235 # then the subvolume's pool
236 # layout remains unset and will undesirably change with ancestor's
237 # pool layout changes.
238 xattr_key = 'ceph.dir.layout.pool'
239 xattr_val = None
240 try:
241 self.fs.getxattr(path, 'ceph.dir.layout.pool').decode('utf-8')
242 except cephfs.NoData:
243 xattr_val = get_ancestor_xattr(self.fs, os.path.split(path)[0],
244 "ceph.dir.layout.pool")
245 if xattr_key and xattr_val:
246 try:
247 self.fs.setxattr(path, xattr_key, xattr_val.encode('utf-8'), 0)
248 except cephfs.Error as e:
249 raise VolumeException(-e.args[0], e.args[1])
250
251 # set uid/gid
252 uid = attrs.get("uid")
253 if uid is None:
254 uid = self.group.uid
255 else:
256 try:
257 if uid < 0:
258 raise ValueError
259 except ValueError:
260 raise VolumeException(-errno.EINVAL, "invalid UID")
261
262 gid = attrs.get("gid")
263 if gid is None:
264 gid = self.group.gid
265 else:
266 try:
267 if gid < 0:
268 raise ValueError
269 except ValueError:
270 raise VolumeException(-errno.EINVAL, "invalid GID")
271
272 if uid is not None and gid is not None:
273 self.fs.chown(path, uid, gid)
274
275 # set mode
276 mode = attrs.get("mode", None)
277 if mode is not None:
278 self.fs.lchmod(path, mode)
279
280 def _resize(self, path, newsize, noshrink):
281 try:
282 newsize = int(newsize)
283 if newsize <= 0:
284 raise VolumeException(-errno.EINVAL,
285 "Invalid subvolume size")
286 except ValueError:
287 newsize = newsize.lower()
288 if not (newsize == "inf" or newsize == "infinite"):
289 raise (VolumeException(-errno.EINVAL,
290 "invalid size option '{0}'"
291 .format(newsize)))
292 newsize = 0
293 noshrink = False
294
295 try:
296 maxbytes = int(self.fs.getxattr(path,
297 'ceph.quota.max_bytes'
298 ).decode('utf-8'))
299 except cephfs.NoData:
300 maxbytes = 0
301 except cephfs.Error as e:
302 raise VolumeException(-e.args[0], e.args[1])
303
304 subvolstat = self.fs.stat(path)
305 if newsize > 0 and newsize < subvolstat.st_size:
306 if noshrink:
307 raise VolumeException(-errno.EINVAL,
308 "Can't resize the subvolume. "
309 "The new size '{0}' would be "
310 "lesser than the current "
311 "used size '{1}'"
312 .format(newsize,
313 subvolstat.st_size))
314
315 if not newsize == maxbytes:
316 try:
317 self.fs.setxattr(path, 'ceph.quota.max_bytes',
318 str(newsize).encode('utf-8'), 0)
319 except cephfs.Error as e:
320 raise (VolumeException(-e.args[0],
321 "Cannot set new size"
322 "for the subvolume. '{0}'"
323 .format(e.args[1])))
324 return newsize, subvolstat.st_size
325
326 def pin(self, pin_type, pin_setting):
327 return pin(self.fs, self.base_path, pin_type, pin_setting)
328
329 def init_config(self, version, subvolume_type,
330 subvolume_path, subvolume_state):
331 self.metadata_mgr.init(version, subvolume_type.value,
332 subvolume_path, subvolume_state.value)
333 self.metadata_mgr.flush()
334
335 def discover(self):
336 log.debug("discovering subvolume "
337 "'{0}' [mode: {1}]".format(self.subvolname, "legacy"
338 if self.legacy_mode else "new"))
339 try:
340 self.fs.stat(self.base_path)
341 self.metadata_mgr.refresh()
342 log.debug("loaded subvolume '{0}'".format(self.subvolname))
343 subvolpath = self.metadata_mgr.get_global_option(MetadataManager.GLOBAL_META_KEY_PATH)
344 # subvolume with retained snapshots has empty path, don't mistake it for
345 # fabricated metadata.
346 if (not self.legacy_mode and self.state != SubvolumeStates.STATE_RETAINED and
347 self.base_path.decode('utf-8') != str(Path(subvolpath).parent)):
348 raise MetadataMgrException(-errno.ENOENT, 'fabricated .meta')
349 except MetadataMgrException as me:
350 if me.errno in (-errno.ENOENT, -errno.EINVAL) and not self.legacy_mode:
351 log.warn("subvolume '{0}', {1}, "
352 "assuming legacy_mode".format(self.subvolname, me.error_str))
353 self.legacy_mode = True
354 self.load_config()
355 self.discover()
356 else:
357 raise
358 except cephfs.Error as e:
359 if e.args[0] == errno.ENOENT:
360 raise (VolumeException(-errno.ENOENT,
361 "subvolume '{0}' "
362 "does not exist"
363 .format(self.subvolname)))
364 raise VolumeException(-e.args[0],
365 "error accessing subvolume '{0}'"
366 .format(self.subvolname))
367
368 def _trash_dir(self, path):
369 create_trashcan(self.fs, self.vol_spec)
370 with open_trashcan(self.fs, self.vol_spec) as trashcan:
371 trashcan.dump(path)
372 log.info("subvolume path '{0}' moved to trashcan".format(path))
373
374 def _link_dir(self, path, bname):
375 create_trashcan(self.fs, self.vol_spec)
376 with open_trashcan(self.fs, self.vol_spec) as trashcan:
377 trashcan.link(path, bname)
378 log.info("subvolume path '{0}' "
379 "linked in trashcan bname {1}".format(path, bname))
380
381 def trash_base_dir(self):
382 if self.legacy_mode:
383 self.fs.unlink(self.legacy_config_path)
384 self._trash_dir(self.base_path)
385
386 def create_base_dir(self, mode):
387 try:
388 self.fs.mkdirs(self.base_path, mode)
389 except cephfs.Error as e:
390 raise VolumeException(-e.args[0], e.args[1])
391
392 def info(self):
393 subvolpath = (self.metadata_mgr.get_global_option(
394 MetadataManager.GLOBAL_META_KEY_PATH))
395 etype = self.subvol_type
396 st = self.fs.statx(subvolpath, cephfs.CEPH_STATX_BTIME
397 | cephfs.CEPH_STATX_SIZE
398 | cephfs.CEPH_STATX_UID | cephfs.CEPH_STATX_GID
399 | cephfs.CEPH_STATX_MODE | cephfs.CEPH_STATX_ATIME
400 | cephfs.CEPH_STATX_MTIME
401 | cephfs.CEPH_STATX_CTIME,
402 cephfs.AT_SYMLINK_NOFOLLOW)
403 usedbytes = st["size"]
404 try:
405 nsize = int(self.fs.getxattr(subvolpath,
406 'ceph.quota.max_bytes'
407 ).decode('utf-8'))
408 except cephfs.NoData:
409 nsize = 0
410
411 try:
412 data_pool = self.fs.getxattr(subvolpath,
413 'ceph.dir.layout.pool'
414 ).decode('utf-8')
415 pool_namespace = self.fs.getxattr(subvolpath,
416 'ceph.dir.layout.pool_namespace'
417 ).decode('utf-8')
418 except cephfs.Error as e:
419 raise VolumeException(-e.args[0], e.args[1])
420
421 return {'path': subvolpath,
422 'type': etype.value,
423 'uid': int(st["uid"]),
424 'gid': int(st["gid"]),
425 'atime': str(st["atime"]),
426 'mtime': str(st["mtime"]),
427 'ctime': str(st["ctime"]),
428 'mode': int(st["mode"]),
429 'data_pool': data_pool,
430 'created_at': str(st["btime"]),
431 'bytes_quota': "infinite" if nsize == 0 else nsize,
432 'bytes_used': int(usedbytes),
433 'bytes_pcent': "undefined"
434 if nsize == 0
435 else '{0:.2f}'.format((float(usedbytes) / nsize) * 100.0),
436 'pool_namespace': pool_namespace,
437 'features': self.features, 'state': self.state.value}
438
439 def set_user_metadata(self, keyname, value):
440 try:
441 self.metadata_mgr.add_section(MetadataManager.USER_METADATA_SECTION)
442 self.metadata_mgr.update_section(MetadataManager.USER_METADATA_SECTION, keyname, str(value))
443 self.metadata_mgr.flush()
444 except MetadataMgrException as me:
445 log.error(f"Failed to set user metadata key={keyname} value={value} on subvolume={self.subvol_name} "
446 f"group={self.group_name} reason={me.args[1]}, errno:{-me.args[0]}, {os.strerror(-me.args[0])}")
447 raise VolumeException(-me.args[0], me.args[1])
448
449 def get_user_metadata(self, keyname):
450 try:
451 value = self.metadata_mgr.get_option(MetadataManager.USER_METADATA_SECTION, keyname)
452 except MetadataMgrException as me:
453 if me.errno == -errno.ENOENT:
454 raise VolumeException(-errno.ENOENT, "key '{0}' does not exist.".format(keyname))
455 raise VolumeException(-me.args[0], me.args[1])
456 return value
457
458 def list_user_metadata(self):
459 return self.metadata_mgr.list_all_options_from_section(MetadataManager.USER_METADATA_SECTION)
460
461 def remove_user_metadata(self, keyname):
462 try:
463 ret = self.metadata_mgr.remove_option(MetadataManager.USER_METADATA_SECTION, keyname)
464 if not ret:
465 raise VolumeException(-errno.ENOENT, "key '{0}' does not exist.".format(keyname))
466 self.metadata_mgr.flush()
467 except MetadataMgrException as me:
468 if me.errno == -errno.ENOENT:
469 raise VolumeException(-errno.ENOENT, "subvolume metadata does not exist")
470 log.error(f"Failed to remove user metadata key={keyname} on subvolume={self.subvol_name} "
471 f"group={self.group_name} reason={me.args[1]}, errno:{-me.args[0]}, {os.strerror(-me.args[0])}")
472 raise VolumeException(-me.args[0], me.args[1])
473
474 def get_snap_section_name(self, snapname):
475 section = "SNAP_METADATA" + "_" + snapname;
476 return section;
477
478 def set_snapshot_metadata(self, snapname, keyname, value):
479 try:
480 section = self.get_snap_section_name(snapname)
481 self.metadata_mgr.add_section(section)
482 self.metadata_mgr.update_section(section, keyname, str(value))
483 self.metadata_mgr.flush()
484 except MetadataMgrException as me:
485 log.error(f"Failed to set snapshot metadata key={keyname} value={value} on snap={snapname} "
486 f"subvolume={self.subvol_name} group={self.group_name} "
487 f"reason={me.args[1]}, errno:{-me.args[0]}, {os.strerror(-me.args[0])}")
488 raise VolumeException(-me.args[0], me.args[1])
489
490 def get_snapshot_metadata(self, snapname, keyname):
491 try:
492 value = self.metadata_mgr.get_option(self.get_snap_section_name(snapname), keyname)
493 except MetadataMgrException as me:
494 if me.errno == -errno.ENOENT:
495 raise VolumeException(-errno.ENOENT, "key '{0}' does not exist.".format(keyname))
496 log.error(f"Failed to get snapshot metadata key={keyname} on snap={snapname} "
497 f"subvolume={self.subvol_name} group={self.group_name} "
498 f"reason={me.args[1]}, errno:{-me.args[0]}, {os.strerror(-me.args[0])}")
499 raise VolumeException(-me.args[0], me.args[1])
500 return value
501
502 def list_snapshot_metadata(self, snapname):
503 return self.metadata_mgr.list_all_options_from_section(self.get_snap_section_name(snapname))
504
505 def remove_snapshot_metadata(self, snapname, keyname):
506 try:
507 ret = self.metadata_mgr.remove_option(self.get_snap_section_name(snapname), keyname)
508 if not ret:
509 raise VolumeException(-errno.ENOENT, "key '{0}' does not exist.".format(keyname))
510 self.metadata_mgr.flush()
511 except MetadataMgrException as me:
512 if me.errno == -errno.ENOENT:
513 raise VolumeException(-errno.ENOENT, "snapshot metadata not does not exist")
514 log.error(f"Failed to remove snapshot metadata key={keyname} on snap={snapname} "
515 f"subvolume={self.subvol_name} group={self.group_name} "
516 f"reason={me.args[1]}, errno:{-me.args[0]}, {os.strerror(-me.args[0])}")
517 raise VolumeException(-me.args[0], me.args[1])