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