]> git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/volumes/fs/operations/versions/subvolume_v1.py
a4c72ed26d4b3c182a78be72866863d8b031c6b2
[ceph.git] / ceph / src / pybind / mgr / volumes / fs / operations / versions / subvolume_v1.py
1 import os
2 import stat
3 import uuid
4 import errno
5 import logging
6 from datetime import datetime
7
8 import cephfs
9
10 from .metadata_manager import MetadataManager
11 from .subvolume_attrs import SubvolumeTypes, SubvolumeStates, SubvolumeFeatures
12 from .op_sm import SubvolumeOpSm
13 from .subvolume_base import SubvolumeBase
14 from ..template import SubvolumeTemplate
15 from ..snapshot_util import mksnap, rmsnap
16 from ...exception import IndexException, OpSmException, VolumeException, MetadataMgrException
17 from ...fs_util import listdir
18 from ..template import SubvolumeOpType
19
20 from ..clone_index import open_clone_index, create_clone_index
21
22 log = logging.getLogger(__name__)
23
24 class SubvolumeV1(SubvolumeBase, SubvolumeTemplate):
25 """
26 Version 1 subvolumes creates a subvolume with path as follows,
27 volumes/<group-name>/<subvolume-name>/<uuid>/
28
29 - The directory under which user data resides is <uuid>
30 - Snapshots of the subvolume are taken within the <uuid> directory
31 - A meta file is maintained under the <subvolume-name> directory as a metadata store, typically storing,
32 - global information about the subvolume (version, path, type, state)
33 - snapshots attached to an ongoing clone operation
34 - clone snapshot source if subvolume is a clone of a snapshot
35 - It retains backward compatability with legacy subvolumes by creating the meta file for legacy subvolumes under
36 /volumes/_legacy/ (see legacy_config_path), thus allowing cloning of older legacy volumes that lack the <uuid>
37 component in the path.
38 """
39 VERSION = 1
40
41 @staticmethod
42 def version():
43 return SubvolumeV1.VERSION
44
45 @property
46 def path(self):
47 try:
48 # no need to stat the path -- open() does that
49 return self.metadata_mgr.get_global_option(MetadataManager.GLOBAL_META_KEY_PATH).encode('utf-8')
50 except MetadataMgrException as me:
51 raise VolumeException(-errno.EINVAL, "error fetching subvolume metadata")
52
53 @property
54 def features(self):
55 return [SubvolumeFeatures.FEATURE_SNAPSHOT_CLONE.value, SubvolumeFeatures.FEATURE_SNAPSHOT_AUTOPROTECT.value]
56
57 def mark_subvolume(self):
58 # set subvolume attr, on subvolume root, marking it as a CephFS subvolume
59 # subvolume root is where snapshots would be taken, and hence is the <uuid> dir for v1 subvolumes
60 try:
61 # MDS treats this as a noop for already marked subvolume
62 self.fs.setxattr(self.path, 'ceph.dir.subvolume', b'1', 0)
63 except cephfs.InvalidValue as e:
64 raise VolumeException(-errno.EINVAL, "invalid value specified for ceph.dir.subvolume")
65 except cephfs.Error as e:
66 raise VolumeException(-e.args[0], e.args[1])
67
68 def snapshot_base_path(self):
69 """ Base path for all snapshots """
70 return os.path.join(self.path, self.vol_spec.snapshot_dir_prefix.encode('utf-8'))
71
72 def snapshot_path(self, snapname):
73 """ Path to a specific snapshot named 'snapname' """
74 return os.path.join(self.snapshot_base_path(), snapname.encode('utf-8'))
75
76 def snapshot_data_path(self, snapname):
77 """ Path to user data directory within a subvolume snapshot named 'snapname' """
78 return self.snapshot_path(snapname)
79
80 def create(self, size, isolate_nspace, pool, mode, uid, gid):
81 subvolume_type = SubvolumeTypes.TYPE_NORMAL
82 try:
83 initial_state = SubvolumeOpSm.get_init_state(subvolume_type)
84 except OpSmException as oe:
85 raise VolumeException(-errno.EINVAL, "subvolume creation failed: internal error")
86
87 subvol_path = os.path.join(self.base_path, str(uuid.uuid4()).encode('utf-8'))
88 try:
89 # create directory and set attributes
90 self.fs.mkdirs(subvol_path, mode)
91 self.mark_subvolume()
92 attrs = {
93 'uid': uid,
94 'gid': gid,
95 'data_pool': pool,
96 'pool_namespace': self.namespace if isolate_nspace else None,
97 'quota': size
98 }
99 self.set_attrs(subvol_path, attrs)
100
101 # persist subvolume metadata
102 qpath = subvol_path.decode('utf-8')
103 self.init_config(SubvolumeV1.VERSION, subvolume_type, qpath, initial_state)
104 except (VolumeException, MetadataMgrException, cephfs.Error) as e:
105 try:
106 log.info("cleaning up subvolume with path: {0}".format(self.subvolname))
107 self.remove()
108 except VolumeException as ve:
109 log.info("failed to cleanup subvolume '{0}' ({1})".format(self.subvolname, ve))
110
111 if isinstance(e, MetadataMgrException):
112 log.error("metadata manager exception: {0}".format(e))
113 e = VolumeException(-errno.EINVAL, "exception in subvolume metadata")
114 elif isinstance(e, cephfs.Error):
115 e = VolumeException(-e.args[0], e.args[1])
116 raise e
117
118 def add_clone_source(self, volname, subvolume, snapname, flush=False):
119 self.metadata_mgr.add_section("source")
120 self.metadata_mgr.update_section("source", "volume", volname)
121 if not subvolume.group.is_default_group():
122 self.metadata_mgr.update_section("source", "group", subvolume.group_name)
123 self.metadata_mgr.update_section("source", "subvolume", subvolume.subvol_name)
124 self.metadata_mgr.update_section("source", "snapshot", snapname)
125 if flush:
126 self.metadata_mgr.flush()
127
128 def remove_clone_source(self, flush=False):
129 self.metadata_mgr.remove_section("source")
130 if flush:
131 self.metadata_mgr.flush()
132
133 def create_clone(self, pool, source_volname, source_subvolume, snapname):
134 subvolume_type = SubvolumeTypes.TYPE_CLONE
135 try:
136 initial_state = SubvolumeOpSm.get_init_state(subvolume_type)
137 except OpSmException as oe:
138 raise VolumeException(-errno.EINVAL, "clone failed: internal error")
139
140 subvol_path = os.path.join(self.base_path, str(uuid.uuid4()).encode('utf-8'))
141 try:
142 # source snapshot attrs are used to create clone subvolume.
143 # attributes of subvolume's content though, are synced during the cloning process.
144 attrs = source_subvolume.get_attrs(source_subvolume.snapshot_data_path(snapname))
145
146 # override snapshot pool setting, if one is provided for the clone
147 if pool is not None:
148 attrs["data_pool"] = pool
149 attrs["pool_namespace"] = None
150
151 # create directory and set attributes
152 self.fs.mkdirs(subvol_path, attrs.get("mode"))
153 self.mark_subvolume()
154 self.set_attrs(subvol_path, attrs)
155
156 # persist subvolume metadata and clone source
157 qpath = subvol_path.decode('utf-8')
158 self.metadata_mgr.init(SubvolumeV1.VERSION, subvolume_type.value, qpath, initial_state.value)
159 self.add_clone_source(source_volname, source_subvolume, snapname)
160 self.metadata_mgr.flush()
161 except (VolumeException, MetadataMgrException, cephfs.Error) as e:
162 try:
163 log.info("cleaning up subvolume with path: {0}".format(self.subvolname))
164 self.remove()
165 except VolumeException as ve:
166 log.info("failed to cleanup subvolume '{0}' ({1})".format(self.subvolname, ve))
167
168 if isinstance(e, MetadataMgrException):
169 log.error("metadata manager exception: {0}".format(e))
170 e = VolumeException(-errno.EINVAL, "exception in subvolume metadata")
171 elif isinstance(e, cephfs.Error):
172 e = VolumeException(-e.args[0], e.args[1])
173 raise e
174
175 def allowed_ops_by_type(self, vol_type):
176 if vol_type == SubvolumeTypes.TYPE_CLONE:
177 return {op_type for op_type in SubvolumeOpType}
178
179 if vol_type == SubvolumeTypes.TYPE_NORMAL:
180 return {op_type for op_type in SubvolumeOpType} - {SubvolumeOpType.CLONE_STATUS,
181 SubvolumeOpType.CLONE_CANCEL,
182 SubvolumeOpType.CLONE_INTERNAL}
183
184 return {}
185
186 def allowed_ops_by_state(self, vol_state):
187 if vol_state == SubvolumeStates.STATE_COMPLETE:
188 return {op_type for op_type in SubvolumeOpType}
189
190 return {SubvolumeOpType.REMOVE_FORCE,
191 SubvolumeOpType.CLONE_CREATE,
192 SubvolumeOpType.CLONE_STATUS,
193 SubvolumeOpType.CLONE_CANCEL,
194 SubvolumeOpType.CLONE_INTERNAL}
195
196 def open(self, op_type):
197 if not isinstance(op_type, SubvolumeOpType):
198 raise VolumeException(-errno.ENOTSUP, "operation {0} not supported on subvolume '{1}'".format(
199 op_type.value, self.subvolname))
200 try:
201 self.metadata_mgr.refresh()
202
203 etype = self.subvol_type
204 if op_type not in self.allowed_ops_by_type(etype):
205 raise VolumeException(-errno.ENOTSUP, "operation '{0}' is not allowed on subvolume '{1}' of type {2}".format(
206 op_type.value, self.subvolname, etype.value))
207
208 estate = self.state
209 if op_type not in self.allowed_ops_by_state(estate):
210 raise VolumeException(-errno.EAGAIN, "subvolume '{0}' is not ready for operation {1}".format(
211 self.subvolname, op_type.value))
212
213 subvol_path = self.path
214 log.debug("refreshed metadata, checking subvolume path '{0}'".format(subvol_path))
215 st = self.fs.stat(subvol_path)
216 # unconditionally mark as subvolume, to handle pre-existing subvolumes without the mark
217 self.mark_subvolume()
218
219 self.uid = int(st.st_uid)
220 self.gid = int(st.st_gid)
221 self.mode = int(st.st_mode & ~stat.S_IFMT(st.st_mode))
222 except MetadataMgrException as me:
223 if me.errno == -errno.ENOENT:
224 raise VolumeException(-errno.ENOENT, "subvolume '{0}' does not exist".format(self.subvolname))
225 raise VolumeException(me.args[0], me.args[1])
226 except cephfs.ObjectNotFound:
227 log.debug("missing subvolume path '{0}' for subvolume '{1}'".format(subvol_path, self.subvolname))
228 raise VolumeException(-errno.ENOENT, "mount path missing for subvolume '{0}'".format(self.subvolname))
229 except cephfs.Error as e:
230 raise VolumeException(-e.args[0], e.args[1])
231
232 def _get_clone_source(self):
233 try:
234 clone_source = {
235 'volume' : self.metadata_mgr.get_option("source", "volume"),
236 'subvolume': self.metadata_mgr.get_option("source", "subvolume"),
237 'snapshot' : self.metadata_mgr.get_option("source", "snapshot"),
238 }
239
240 try:
241 clone_source["group"] = self.metadata_mgr.get_option("source", "group")
242 except MetadataMgrException as me:
243 if me.errno == -errno.ENOENT:
244 pass
245 else:
246 raise
247 except MetadataMgrException as me:
248 raise VolumeException(-errno.EINVAL, "error fetching subvolume metadata")
249 return clone_source
250
251 @property
252 def status(self):
253 state = SubvolumeStates.from_value(self.metadata_mgr.get_global_option(MetadataManager.GLOBAL_META_KEY_STATE))
254 subvolume_type = self.subvol_type
255 subvolume_status = {
256 'state' : state.value
257 }
258 if not SubvolumeOpSm.is_complete_state(state) and subvolume_type == SubvolumeTypes.TYPE_CLONE:
259 subvolume_status["source"] = self._get_clone_source()
260 return subvolume_status
261
262 @property
263 def state(self):
264 return SubvolumeStates.from_value(self.metadata_mgr.get_global_option(MetadataManager.GLOBAL_META_KEY_STATE))
265
266 @state.setter
267 def state(self, val):
268 state = val[0].value
269 flush = val[1]
270 self.metadata_mgr.update_global_section(MetadataManager.GLOBAL_META_KEY_STATE, state)
271 if flush:
272 self.metadata_mgr.flush()
273
274 def remove(self, retainsnaps=False):
275 if retainsnaps:
276 raise VolumeException(-errno.EINVAL, "subvolume '{0}' does not support snapshot retention on delete".format(self.subvolname))
277 if self.list_snapshots():
278 raise VolumeException(-errno.ENOTEMPTY, "subvolume '{0}' has snapshots".format(self.subvolname))
279 self.trash_base_dir()
280
281 def resize(self, newsize, noshrink):
282 subvol_path = self.path
283 return self._resize(subvol_path, newsize, noshrink)
284
285 def create_snapshot(self, snapname):
286 snappath = self.snapshot_path(snapname)
287 mksnap(self.fs, snappath)
288
289 def has_pending_clones(self, snapname):
290 try:
291 return self.metadata_mgr.section_has_item('clone snaps', snapname)
292 except MetadataMgrException as me:
293 if me.errno == -errno.ENOENT:
294 return False
295 raise
296
297 def remove_snapshot(self, snapname):
298 if self.has_pending_clones(snapname):
299 raise VolumeException(-errno.EAGAIN, "snapshot '{0}' has pending clones".format(snapname))
300 snappath = self.snapshot_path(snapname)
301 rmsnap(self.fs, snappath)
302
303 def snapshot_info(self, snapname):
304 snappath = self.snapshot_data_path(snapname)
305 snap_info = {}
306 try:
307 snap_attrs = {'created_at':'ceph.snap.btime', 'size':'ceph.dir.rbytes',
308 'data_pool':'ceph.dir.layout.pool'}
309 for key, val in snap_attrs.items():
310 snap_info[key] = self.fs.getxattr(snappath, val)
311 return {'size': int(snap_info['size']),
312 'created_at': str(datetime.fromtimestamp(float(snap_info['created_at']))),
313 'data_pool': snap_info['data_pool'].decode('utf-8'),
314 'has_pending_clones': "yes" if self.has_pending_clones(snapname) else "no"}
315 except cephfs.Error as e:
316 if e.errno == errno.ENOENT:
317 raise VolumeException(-errno.ENOENT,
318 "snapshot '{0}' does not exist".format(snapname))
319 raise VolumeException(-e.args[0], e.args[1])
320
321 def list_snapshots(self):
322 try:
323 dirpath = self.snapshot_base_path()
324 return listdir(self.fs, dirpath)
325 except VolumeException as ve:
326 if ve.errno == -errno.ENOENT:
327 return []
328 raise
329
330 def _add_snap_clone(self, track_id, snapname):
331 self.metadata_mgr.add_section("clone snaps")
332 self.metadata_mgr.update_section("clone snaps", track_id, snapname)
333 self.metadata_mgr.flush()
334
335 def _remove_snap_clone(self, track_id):
336 self.metadata_mgr.remove_option("clone snaps", track_id)
337 self.metadata_mgr.flush()
338
339 def attach_snapshot(self, snapname, tgt_subvolume):
340 if not snapname.encode('utf-8') in self.list_snapshots():
341 raise VolumeException(-errno.ENOENT, "snapshot '{0}' does not exist".format(snapname))
342 try:
343 create_clone_index(self.fs, self.vol_spec)
344 with open_clone_index(self.fs, self.vol_spec) as index:
345 track_idx = index.track(tgt_subvolume.base_path)
346 self._add_snap_clone(track_idx, snapname)
347 except (IndexException, MetadataMgrException) as e:
348 log.warning("error creating clone index: {0}".format(e))
349 raise VolumeException(-errno.EINVAL, "error cloning subvolume")
350
351 def detach_snapshot(self, snapname, track_id):
352 if not snapname.encode('utf-8') in self.list_snapshots():
353 raise VolumeException(-errno.ENOENT, "snapshot '{0}' does not exist".format(snapname))
354 try:
355 with open_clone_index(self.fs, self.vol_spec) as index:
356 index.untrack(track_id)
357 self._remove_snap_clone(track_id)
358 except (IndexException, MetadataMgrException) as e:
359 log.warning("error delining snapshot from clone: {0}".format(e))
360 raise VolumeException(-errno.EINVAL, "error delinking snapshot from clone")