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