]>
Commit | Line | Data |
---|---|---|
92f5a8d4 | 1 | import os |
adb31ebb | 2 | import stat |
20effc67 | 3 | |
92f5a8d4 TL |
4 | import errno |
5 | import logging | |
6 | from hashlib import md5 | |
adb31ebb | 7 | from typing import Dict, Union |
92f5a8d4 TL |
8 | |
9 | import cephfs | |
10 | ||
f6b5b4d7 | 11 | from ..pin_util import pin |
20effc67 | 12 | from .subvolume_attrs import SubvolumeTypes |
92f5a8d4 TL |
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 | |
cd265ab1 | 17 | from .auth_metadata import AuthMetadataManager |
92f5a8d4 TL |
18 | |
19 | log = logging.getLogger(__name__) | |
20 | ||
20effc67 | 21 | |
92f5a8d4 TL |
22 | class SubvolumeBase(object): |
23 | LEGACY_CONF_DIR = "_legacy" | |
24 | ||
cd265ab1 TL |
25 | def __init__(self, mgr, fs, vol_spec, group, subvolname, legacy=False): |
26 | self.mgr = mgr | |
92f5a8d4 | 27 | self.fs = fs |
cd265ab1 | 28 | self.auth_mdata_mgr = AuthMetadataManager(fs) |
92f5a8d4 TL |
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 | ||
92f5a8d4 TL |
42 | @uid.setter |
43 | def uid(self, val): | |
44 | self.user_id = val | |
45 | ||
9f95a23c TL |
46 | @property |
47 | def gid(self): | |
48 | return self.group_id | |
49 | ||
92f5a8d4 TL |
50 | @gid.setter |
51 | def gid(self, val): | |
52 | self.group_id = val | |
53 | ||
9f95a23c TL |
54 | @property |
55 | def mode(self): | |
56 | return self.cmode | |
57 | ||
92f5a8d4 TL |
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): | |
20effc67 TL |
72 | return (os.path.join(self.vol_spec.base_dir.encode('utf-8'), |
73 | SubvolumeBase.LEGACY_CONF_DIR.encode('utf-8'))) | |
92f5a8d4 TL |
74 | |
75 | @property | |
76 | def legacy_config_path(self): | |
77 | m = md5() | |
78 | m.update(self.base_path) | |
9f95a23c | 79 | meta_config = "{0}.meta".format(m.digest().hex()) |
92f5a8d4 TL |
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 | ||
adb31ebb TL |
102 | @property |
103 | def path(self): | |
104 | """ Path to subvolume data directory """ | |
105 | raise NotImplementedError | |
106 | ||
f6b5b4d7 TL |
107 | @property |
108 | def features(self): | |
20effc67 TL |
109 | """ |
110 | List of features supported by the subvolume, | |
111 | containing items from SubvolumeFeatures | |
112 | """ | |
adb31ebb TL |
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): | |
20effc67 TL |
122 | return (SubvolumeTypes.from_value(self.metadata_mgr.get_global_option |
123 | (MetadataManager.GLOBAL_META_KEY_TYPE))) | |
adb31ebb TL |
124 | |
125 | @property | |
126 | def purgeable(self): | |
127 | """ Boolean declaring if subvolume can be purged """ | |
f6b5b4d7 TL |
128 | raise NotImplementedError |
129 | ||
92f5a8d4 TL |
130 | def load_config(self): |
131 | if self.legacy_mode: | |
20effc67 TL |
132 | self.metadata_mgr = MetadataManager(self.fs, |
133 | self.legacy_config_path, | |
134 | 0o640) | |
92f5a8d4 | 135 | else: |
20effc67 TL |
136 | self.metadata_mgr = MetadataManager(self.fs, |
137 | self.config_path, 0o640) | |
92f5a8d4 | 138 | |
adb31ebb TL |
139 | def get_attrs(self, pathname): |
140 | # get subvolume attributes | |
20effc67 | 141 | attrs = {} # type: Dict[str, Union[int, str, None]] |
adb31ebb | 142 | stx = self.fs.statx(pathname, |
20effc67 TL |
143 | cephfs.CEPH_STATX_UID | cephfs.CEPH_STATX_GID |
144 | | cephfs.CEPH_STATX_MODE, | |
adb31ebb TL |
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: | |
20effc67 TL |
152 | attrs["data_pool"] = self.fs.getxattr(pathname, |
153 | 'ceph.dir.layout.pool' | |
154 | ).decode('utf-8') | |
adb31ebb TL |
155 | except cephfs.NoData: |
156 | attrs["data_pool"] = None | |
157 | ||
158 | try: | |
20effc67 TL |
159 | attrs["pool_namespace"] = self.fs.getxattr(pathname, |
160 | 'ceph.dir.layout' | |
161 | '.pool_namespace' | |
162 | ).decode('utf-8') | |
adb31ebb TL |
163 | except cephfs.NoData: |
164 | attrs["pool_namespace"] = None | |
165 | ||
166 | try: | |
20effc67 TL |
167 | attrs["quota"] = int(self.fs.getxattr(pathname, |
168 | 'ceph.quota.max_bytes' | |
169 | ).decode('utf-8')) | |
adb31ebb TL |
170 | except cephfs.NoData: |
171 | attrs["quota"] = None | |
172 | ||
173 | return attrs | |
174 | ||
175 | def set_attrs(self, path, attrs): | |
176 | # set subvolume attributes | |
92f5a8d4 | 177 | # set size |
adb31ebb TL |
178 | quota = attrs.get("quota") |
179 | if quota is not None: | |
92f5a8d4 | 180 | try: |
20effc67 TL |
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)) | |
92f5a8d4 TL |
186 | except cephfs.Error as e: |
187 | raise VolumeException(-e.args[0], e.args[1]) | |
188 | ||
189 | # set pool layout | |
adb31ebb TL |
190 | data_pool = attrs.get("data_pool") |
191 | if data_pool is not None: | |
92f5a8d4 | 192 | try: |
20effc67 TL |
193 | self.fs.setxattr(path, 'ceph.dir.layout.pool', |
194 | data_pool.encode('utf-8'), 0) | |
92f5a8d4 TL |
195 | except cephfs.InvalidValue: |
196 | raise VolumeException(-errno.EINVAL, | |
20effc67 TL |
197 | "invalid pool layout '{0}'" |
198 | "--need a valid data pool" | |
199 | .format(data_pool)) | |
92f5a8d4 TL |
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 | |
adb31ebb TL |
205 | pool_namespace = attrs.get("pool_namespace") |
206 | if pool_namespace is not None: | |
20effc67 TL |
207 | # enforce security isolation, use separate namespace |
208 | # for this subvolume | |
92f5a8d4 | 209 | xattr_key = 'ceph.dir.layout.pool_namespace' |
adb31ebb TL |
210 | xattr_val = pool_namespace |
211 | elif not data_pool: | |
20effc67 TL |
212 | # If subvolume's namespace layout is not set, |
213 | # then the subvolume's pool | |
92f5a8d4 TL |
214 | # layout remains unset and will undesirably change with ancestor's |
215 | # pool layout changes. | |
216 | xattr_key = 'ceph.dir.layout.pool' | |
e306af50 TL |
217 | xattr_val = None |
218 | try: | |
219 | self.fs.getxattr(path, 'ceph.dir.layout.pool').decode('utf-8') | |
20effc67 TL |
220 | except cephfs.NoData: |
221 | xattr_val = get_ancestor_xattr(self.fs, os.path.split(path)[0], | |
222 | "ceph.dir.layout.pool") | |
92f5a8d4 TL |
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 | |
adb31ebb | 230 | uid = attrs.get("uid") |
92f5a8d4 TL |
231 | if uid is None: |
232 | uid = self.group.uid | |
233 | else: | |
234 | try: | |
92f5a8d4 TL |
235 | if uid < 0: |
236 | raise ValueError | |
237 | except ValueError: | |
238 | raise VolumeException(-errno.EINVAL, "invalid UID") | |
adb31ebb TL |
239 | |
240 | gid = attrs.get("gid") | |
92f5a8d4 TL |
241 | if gid is None: |
242 | gid = self.group.gid | |
243 | else: | |
244 | try: | |
92f5a8d4 TL |
245 | if gid < 0: |
246 | raise ValueError | |
247 | except ValueError: | |
248 | raise VolumeException(-errno.EINVAL, "invalid GID") | |
adb31ebb | 249 | |
92f5a8d4 TL |
250 | if uid is not None and gid is not None: |
251 | self.fs.chown(path, uid, gid) | |
252 | ||
1d09f67e TL |
253 | # set mode |
254 | mode = attrs.get("mode", None) | |
255 | if mode is not None: | |
256 | self.fs.lchmod(path, mode) | |
257 | ||
92f5a8d4 TL |
258 | def _resize(self, path, newsize, noshrink): |
259 | try: | |
260 | newsize = int(newsize) | |
261 | if newsize <= 0: | |
20effc67 TL |
262 | raise VolumeException(-errno.EINVAL, |
263 | "Invalid subvolume size") | |
92f5a8d4 TL |
264 | except ValueError: |
265 | newsize = newsize.lower() | |
266 | if not (newsize == "inf" or newsize == "infinite"): | |
20effc67 TL |
267 | raise (VolumeException(-errno.EINVAL, |
268 | "invalid size option '{0}'" | |
269 | .format(newsize))) | |
92f5a8d4 TL |
270 | newsize = 0 |
271 | noshrink = False | |
272 | ||
273 | try: | |
20effc67 TL |
274 | maxbytes = int(self.fs.getxattr(path, |
275 | 'ceph.quota.max_bytes' | |
276 | ).decode('utf-8')) | |
92f5a8d4 TL |
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: | |
20effc67 TL |
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)) | |
92f5a8d4 TL |
292 | |
293 | if not newsize == maxbytes: | |
294 | try: | |
20effc67 TL |
295 | self.fs.setxattr(path, 'ceph.quota.max_bytes', |
296 | str(newsize).encode('utf-8'), 0) | |
92f5a8d4 | 297 | except cephfs.Error as e: |
20effc67 TL |
298 | raise (VolumeException(-e.args[0], |
299 | "Cannot set new size" | |
300 | "for the subvolume. '{0}'" | |
301 | .format(e.args[1]))) | |
92f5a8d4 TL |
302 | return newsize, subvolstat.st_size |
303 | ||
f6b5b4d7 TL |
304 | def pin(self, pin_type, pin_setting): |
305 | return pin(self.fs, self.base_path, pin_type, pin_setting) | |
306 | ||
20effc67 TL |
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) | |
92f5a8d4 TL |
311 | self.metadata_mgr.flush() |
312 | ||
313 | def discover(self): | |
20effc67 TL |
314 | log.debug("discovering subvolume " |
315 | "'{0}' [mode: {1}]".format(self.subvolname, "legacy" | |
316 | if self.legacy_mode else "new")) | |
92f5a8d4 TL |
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: | |
20effc67 TL |
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)) | |
92f5a8d4 | 337 | |
adb31ebb TL |
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) | |
20effc67 TL |
348 | log.info("subvolume path '{0}' " |
349 | "linked in trashcan bname {1}".format(path, bname)) | |
adb31ebb | 350 | |
92f5a8d4 TL |
351 | def trash_base_dir(self): |
352 | if self.legacy_mode: | |
353 | self.fs.unlink(self.legacy_config_path) | |
adb31ebb | 354 | self._trash_dir(self.base_path) |
92f5a8d4 TL |
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]) | |
1911f103 | 361 | |
20effc67 TL |
362 | def info(self): |
363 | subvolpath = (self.metadata_mgr.get_global_option( | |
364 | MetadataManager.GLOBAL_META_KEY_PATH)) | |
adb31ebb | 365 | etype = self.subvol_type |
20effc67 TL |
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) | |
1911f103 TL |
373 | usedbytes = st["size"] |
374 | try: | |
20effc67 TL |
375 | nsize = int(self.fs.getxattr(subvolpath, |
376 | 'ceph.quota.max_bytes' | |
377 | ).decode('utf-8')) | |
1911f103 TL |
378 | except cephfs.NoData: |
379 | nsize = 0 | |
380 | ||
381 | try: | |
20effc67 TL |
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') | |
1911f103 TL |
388 | except cephfs.Error as e: |
389 | raise VolumeException(-e.args[0], e.args[1]) | |
390 | ||
20effc67 TL |
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} | |
33c7a0ef TL |
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]) |