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