]>
Commit | Line | Data |
---|---|---|
92f5a8d4 | 1 | import os |
adb31ebb | 2 | import stat |
92f5a8d4 TL |
3 | import uuid |
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 |
adb31ebb | 12 | from .subvolume_attrs import SubvolumeTypes, SubvolumeStates |
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 | |
adb31ebb | 17 | from .op_sm import SubvolumeOpSm |
cd265ab1 | 18 | from .auth_metadata import AuthMetadataManager |
92f5a8d4 TL |
19 | |
20 | log = logging.getLogger(__name__) | |
21 | ||
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): | |
72 | return os.path.join(self.vol_spec.base_dir.encode('utf-8'), SubvolumeBase.LEGACY_CONF_DIR.encode('utf-8')) | |
73 | ||
74 | @property | |
75 | def legacy_config_path(self): | |
76 | m = md5() | |
77 | m.update(self.base_path) | |
9f95a23c | 78 | meta_config = "{0}.meta".format(m.digest().hex()) |
92f5a8d4 TL |
79 | return os.path.join(self.legacy_dir, meta_config.encode('utf-8')) |
80 | ||
81 | @property | |
82 | def namespace(self): | |
83 | return "{0}{1}".format(self.vol_spec.fs_namespace, self.subvolname) | |
84 | ||
85 | @property | |
86 | def group_name(self): | |
87 | return self.group.group_name | |
88 | ||
89 | @property | |
90 | def subvol_name(self): | |
91 | return self.subvolname | |
92 | ||
93 | @property | |
94 | def legacy_mode(self): | |
95 | return self.legacy | |
96 | ||
97 | @legacy_mode.setter | |
98 | def legacy_mode(self, mode): | |
99 | self.legacy = mode | |
100 | ||
adb31ebb TL |
101 | @property |
102 | def path(self): | |
103 | """ Path to subvolume data directory """ | |
104 | raise NotImplementedError | |
105 | ||
f6b5b4d7 TL |
106 | @property |
107 | def features(self): | |
adb31ebb TL |
108 | """ List of features supported by the subvolume, containing items from SubvolumeFeatures """ |
109 | raise NotImplementedError | |
110 | ||
111 | @property | |
112 | def state(self): | |
113 | """ Subvolume state, one of SubvolumeStates """ | |
114 | raise NotImplementedError | |
115 | ||
116 | @property | |
117 | def subvol_type(self): | |
118 | return SubvolumeTypes.from_value(self.metadata_mgr.get_global_option(MetadataManager.GLOBAL_META_KEY_TYPE)) | |
119 | ||
120 | @property | |
121 | def purgeable(self): | |
122 | """ Boolean declaring if subvolume can be purged """ | |
f6b5b4d7 TL |
123 | raise NotImplementedError |
124 | ||
92f5a8d4 TL |
125 | def load_config(self): |
126 | if self.legacy_mode: | |
127 | self.metadata_mgr = MetadataManager(self.fs, self.legacy_config_path, 0o640) | |
128 | else: | |
129 | self.metadata_mgr = MetadataManager(self.fs, self.config_path, 0o640) | |
130 | ||
adb31ebb TL |
131 | def get_attrs(self, pathname): |
132 | # get subvolume attributes | |
133 | attrs = {} # type: Dict[str, Union[int, str, None]] | |
134 | stx = self.fs.statx(pathname, | |
135 | cephfs.CEPH_STATX_UID | cephfs.CEPH_STATX_GID | cephfs.CEPH_STATX_MODE, | |
136 | cephfs.AT_SYMLINK_NOFOLLOW) | |
137 | ||
138 | attrs["uid"] = int(stx["uid"]) | |
139 | attrs["gid"] = int(stx["gid"]) | |
140 | attrs["mode"] = int(int(stx["mode"]) & ~stat.S_IFMT(stx["mode"])) | |
141 | ||
142 | try: | |
143 | attrs["data_pool"] = self.fs.getxattr(pathname, 'ceph.dir.layout.pool').decode('utf-8') | |
144 | except cephfs.NoData: | |
145 | attrs["data_pool"] = None | |
146 | ||
147 | try: | |
148 | attrs["pool_namespace"] = self.fs.getxattr(pathname, 'ceph.dir.layout.pool_namespace').decode('utf-8') | |
149 | except cephfs.NoData: | |
150 | attrs["pool_namespace"] = None | |
151 | ||
152 | try: | |
153 | attrs["quota"] = int(self.fs.getxattr(pathname, 'ceph.quota.max_bytes').decode('utf-8')) | |
154 | except cephfs.NoData: | |
155 | attrs["quota"] = None | |
156 | ||
157 | return attrs | |
158 | ||
159 | def set_attrs(self, path, attrs): | |
160 | # set subvolume attributes | |
92f5a8d4 | 161 | # set size |
adb31ebb TL |
162 | quota = attrs.get("quota") |
163 | if quota is not None: | |
92f5a8d4 | 164 | try: |
adb31ebb | 165 | self.fs.setxattr(path, 'ceph.quota.max_bytes', str(quota).encode('utf-8'), 0) |
92f5a8d4 | 166 | except cephfs.InvalidValue as e: |
adb31ebb | 167 | raise VolumeException(-errno.EINVAL, "invalid size specified: '{0}'".format(quota)) |
92f5a8d4 TL |
168 | except cephfs.Error as e: |
169 | raise VolumeException(-e.args[0], e.args[1]) | |
170 | ||
171 | # set pool layout | |
adb31ebb TL |
172 | data_pool = attrs.get("data_pool") |
173 | if data_pool is not None: | |
92f5a8d4 | 174 | try: |
adb31ebb | 175 | self.fs.setxattr(path, 'ceph.dir.layout.pool', data_pool.encode('utf-8'), 0) |
92f5a8d4 TL |
176 | except cephfs.InvalidValue: |
177 | raise VolumeException(-errno.EINVAL, | |
adb31ebb | 178 | "invalid pool layout '{0}' -- need a valid data pool".format(data_pool)) |
92f5a8d4 TL |
179 | except cephfs.Error as e: |
180 | raise VolumeException(-e.args[0], e.args[1]) | |
181 | ||
182 | # isolate namespace | |
183 | xattr_key = xattr_val = None | |
adb31ebb TL |
184 | pool_namespace = attrs.get("pool_namespace") |
185 | if pool_namespace is not None: | |
92f5a8d4 TL |
186 | # enforce security isolation, use separate namespace for this subvolume |
187 | xattr_key = 'ceph.dir.layout.pool_namespace' | |
adb31ebb TL |
188 | xattr_val = pool_namespace |
189 | elif not data_pool: | |
92f5a8d4 TL |
190 | # If subvolume's namespace layout is not set, then the subvolume's pool |
191 | # layout remains unset and will undesirably change with ancestor's | |
192 | # pool layout changes. | |
193 | xattr_key = 'ceph.dir.layout.pool' | |
e306af50 TL |
194 | xattr_val = None |
195 | try: | |
196 | self.fs.getxattr(path, 'ceph.dir.layout.pool').decode('utf-8') | |
197 | except cephfs.NoData as e: | |
198 | xattr_val = get_ancestor_xattr(self.fs, os.path.split(path)[0], "ceph.dir.layout.pool") | |
92f5a8d4 TL |
199 | if xattr_key and xattr_val: |
200 | try: | |
201 | self.fs.setxattr(path, xattr_key, xattr_val.encode('utf-8'), 0) | |
202 | except cephfs.Error as e: | |
203 | raise VolumeException(-e.args[0], e.args[1]) | |
204 | ||
205 | # set uid/gid | |
adb31ebb | 206 | uid = attrs.get("uid") |
92f5a8d4 TL |
207 | if uid is None: |
208 | uid = self.group.uid | |
209 | else: | |
210 | try: | |
92f5a8d4 TL |
211 | if uid < 0: |
212 | raise ValueError | |
213 | except ValueError: | |
214 | raise VolumeException(-errno.EINVAL, "invalid UID") | |
adb31ebb TL |
215 | |
216 | gid = attrs.get("gid") | |
92f5a8d4 TL |
217 | if gid is None: |
218 | gid = self.group.gid | |
219 | else: | |
220 | try: | |
92f5a8d4 TL |
221 | if gid < 0: |
222 | raise ValueError | |
223 | except ValueError: | |
224 | raise VolumeException(-errno.EINVAL, "invalid GID") | |
adb31ebb | 225 | |
92f5a8d4 TL |
226 | if uid is not None and gid is not None: |
227 | self.fs.chown(path, uid, gid) | |
228 | ||
229 | def _resize(self, path, newsize, noshrink): | |
230 | try: | |
231 | newsize = int(newsize) | |
232 | if newsize <= 0: | |
233 | raise VolumeException(-errno.EINVAL, "Invalid subvolume size") | |
234 | except ValueError: | |
235 | newsize = newsize.lower() | |
236 | if not (newsize == "inf" or newsize == "infinite"): | |
237 | raise VolumeException(-errno.EINVAL, "invalid size option '{0}'".format(newsize)) | |
238 | newsize = 0 | |
239 | noshrink = False | |
240 | ||
241 | try: | |
242 | maxbytes = int(self.fs.getxattr(path, 'ceph.quota.max_bytes').decode('utf-8')) | |
243 | except cephfs.NoData: | |
244 | maxbytes = 0 | |
245 | except cephfs.Error as e: | |
246 | raise VolumeException(-e.args[0], e.args[1]) | |
247 | ||
248 | subvolstat = self.fs.stat(path) | |
249 | if newsize > 0 and newsize < subvolstat.st_size: | |
250 | if noshrink: | |
251 | raise VolumeException(-errno.EINVAL, "Can't resize the subvolume. The new size '{0}' would be lesser than the current " | |
252 | "used size '{1}'".format(newsize, subvolstat.st_size)) | |
253 | ||
254 | if not newsize == maxbytes: | |
255 | try: | |
256 | self.fs.setxattr(path, 'ceph.quota.max_bytes', str(newsize).encode('utf-8'), 0) | |
257 | except cephfs.Error as e: | |
258 | raise VolumeException(-e.args[0], "Cannot set new size for the subvolume. '{0}'".format(e.args[1])) | |
259 | return newsize, subvolstat.st_size | |
260 | ||
f6b5b4d7 TL |
261 | def pin(self, pin_type, pin_setting): |
262 | return pin(self.fs, self.base_path, pin_type, pin_setting) | |
263 | ||
92f5a8d4 | 264 | def init_config(self, version, subvolume_type, subvolume_path, subvolume_state): |
adb31ebb | 265 | self.metadata_mgr.init(version, subvolume_type.value, subvolume_path, subvolume_state.value) |
92f5a8d4 TL |
266 | self.metadata_mgr.flush() |
267 | ||
268 | def discover(self): | |
269 | log.debug("discovering subvolume '{0}' [mode: {1}]".format(self.subvolname, "legacy" if self.legacy_mode else "new")) | |
270 | try: | |
271 | self.fs.stat(self.base_path) | |
272 | self.metadata_mgr.refresh() | |
273 | log.debug("loaded subvolume '{0}'".format(self.subvolname)) | |
274 | except MetadataMgrException as me: | |
275 | if me.errno == -errno.ENOENT and not self.legacy_mode: | |
276 | self.legacy_mode = True | |
277 | self.load_config() | |
278 | self.discover() | |
279 | else: | |
280 | raise | |
281 | except cephfs.Error as e: | |
282 | if e.args[0] == errno.ENOENT: | |
283 | raise VolumeException(-errno.ENOENT, "subvolume '{0}' does not exist".format(self.subvolname)) | |
284 | raise VolumeException(-e.args[0], "error accessing subvolume '{0}'".format(self.subvolname)) | |
285 | ||
adb31ebb TL |
286 | def _trash_dir(self, path): |
287 | create_trashcan(self.fs, self.vol_spec) | |
288 | with open_trashcan(self.fs, self.vol_spec) as trashcan: | |
289 | trashcan.dump(path) | |
290 | log.info("subvolume path '{0}' moved to trashcan".format(path)) | |
291 | ||
292 | def _link_dir(self, path, bname): | |
293 | create_trashcan(self.fs, self.vol_spec) | |
294 | with open_trashcan(self.fs, self.vol_spec) as trashcan: | |
295 | trashcan.link(path, bname) | |
296 | log.info("subvolume path '{0}' linked in trashcan bname {1}".format(path, bname)) | |
297 | ||
92f5a8d4 TL |
298 | def trash_base_dir(self): |
299 | if self.legacy_mode: | |
300 | self.fs.unlink(self.legacy_config_path) | |
adb31ebb | 301 | self._trash_dir(self.base_path) |
92f5a8d4 TL |
302 | |
303 | def create_base_dir(self, mode): | |
304 | try: | |
305 | self.fs.mkdirs(self.base_path, mode) | |
306 | except cephfs.Error as e: | |
307 | raise VolumeException(-e.args[0], e.args[1]) | |
1911f103 TL |
308 | |
309 | def info (self): | |
adb31ebb TL |
310 | subvolpath = self.metadata_mgr.get_global_option(MetadataManager.GLOBAL_META_KEY_PATH) |
311 | etype = self.subvol_type | |
1911f103 TL |
312 | st = self.fs.statx(subvolpath, cephfs.CEPH_STATX_BTIME | cephfs.CEPH_STATX_SIZE | |
313 | cephfs.CEPH_STATX_UID | cephfs.CEPH_STATX_GID | | |
314 | cephfs.CEPH_STATX_MODE | cephfs.CEPH_STATX_ATIME | | |
315 | cephfs.CEPH_STATX_MTIME | cephfs.CEPH_STATX_CTIME, | |
316 | cephfs.AT_SYMLINK_NOFOLLOW) | |
317 | usedbytes = st["size"] | |
318 | try: | |
319 | nsize = int(self.fs.getxattr(subvolpath, 'ceph.quota.max_bytes').decode('utf-8')) | |
320 | except cephfs.NoData: | |
321 | nsize = 0 | |
322 | ||
323 | try: | |
324 | data_pool = self.fs.getxattr(subvolpath, 'ceph.dir.layout.pool').decode('utf-8') | |
e306af50 | 325 | pool_namespace = self.fs.getxattr(subvolpath, 'ceph.dir.layout.pool_namespace').decode('utf-8') |
1911f103 TL |
326 | except cephfs.Error as e: |
327 | raise VolumeException(-e.args[0], e.args[1]) | |
328 | ||
adb31ebb | 329 | return {'path': subvolpath, 'type': etype.value, 'uid': int(st["uid"]), 'gid': int(st["gid"]), |
1911f103 TL |
330 | 'atime': str(st["atime"]), 'mtime': str(st["mtime"]), 'ctime': str(st["ctime"]), |
331 | 'mode': int(st["mode"]), 'data_pool': data_pool, 'created_at': str(st["btime"]), | |
332 | 'bytes_quota': "infinite" if nsize == 0 else nsize, 'bytes_used': int(usedbytes), | |
e306af50 | 333 | 'bytes_pcent': "undefined" if nsize == 0 else '{0:.2f}'.format((float(usedbytes) / nsize) * 100.0), |
adb31ebb | 334 | 'pool_namespace': pool_namespace, 'features': self.features, 'state': self.state.value} |