6 from datetime
import datetime
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
20 from ..clone_index
import open_clone_index
, create_clone_index
22 log
= logging
.getLogger(__name__
)
24 class SubvolumeV1(SubvolumeBase
, SubvolumeTemplate
):
26 Version 1 subvolumes creates a subvolume with path as follows,
27 volumes/<group-name>/<subvolume-name>/<uuid>/
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.
43 return SubvolumeV1
.VERSION
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")
55 return [SubvolumeFeatures
.FEATURE_SNAPSHOT_CLONE
.value
, SubvolumeFeatures
.FEATURE_SNAPSHOT_AUTOPROTECT
.value
]
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
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])
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'))
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'))
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
)
80 def create(self
, size
, isolate_nspace
, pool
, mode
, uid
, gid
):
81 subvolume_type
= SubvolumeTypes
.TYPE_NORMAL
83 initial_state
= SubvolumeOpSm
.get_init_state(subvolume_type
)
84 except OpSmException
as oe
:
85 raise VolumeException(-errno
.EINVAL
, "subvolume creation failed: internal error")
87 subvol_path
= os
.path
.join(self
.base_path
, str(uuid
.uuid4()).encode('utf-8'))
89 # create directory and set attributes
90 self
.fs
.mkdirs(subvol_path
, mode
)
96 'pool_namespace': self
.namespace
if isolate_nspace
else None,
99 self
.set_attrs(subvol_path
, attrs
)
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
:
106 log
.info("cleaning up subvolume with path: {0}".format(self
.subvolname
))
108 except VolumeException
as ve
:
109 log
.info("failed to cleanup subvolume '{0}' ({1})".format(self
.subvolname
, ve
))
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])
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
)
126 self
.metadata_mgr
.flush()
128 def remove_clone_source(self
, flush
=False):
129 self
.metadata_mgr
.remove_section("source")
131 self
.metadata_mgr
.flush()
133 def create_clone(self
, pool
, source_volname
, source_subvolume
, snapname
):
134 subvolume_type
= SubvolumeTypes
.TYPE_CLONE
136 initial_state
= SubvolumeOpSm
.get_init_state(subvolume_type
)
137 except OpSmException
as oe
:
138 raise VolumeException(-errno
.EINVAL
, "clone failed: internal error")
140 subvol_path
= os
.path
.join(self
.base_path
, str(uuid
.uuid4()).encode('utf-8'))
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
))
146 # override snapshot pool setting, if one is provided for the clone
148 attrs
["data_pool"] = pool
149 attrs
["pool_namespace"] = None
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
)
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
:
163 log
.info("cleaning up subvolume with path: {0}".format(self
.subvolname
))
165 except VolumeException
as ve
:
166 log
.info("failed to cleanup subvolume '{0}' ({1})".format(self
.subvolname
, ve
))
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])
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
}
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
}
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
}
190 return {SubvolumeOpType
.REMOVE_FORCE
,
191 SubvolumeOpType
.CLONE_CREATE
,
192 SubvolumeOpType
.CLONE_STATUS
,
193 SubvolumeOpType
.CLONE_CANCEL
,
194 SubvolumeOpType
.CLONE_INTERNAL
}
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
))
201 self
.metadata_mgr
.refresh()
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
))
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
))
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()
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])
232 def _get_clone_source(self
):
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"),
241 clone_source
["group"] = self
.metadata_mgr
.get_option("source", "group")
242 except MetadataMgrException
as me
:
243 if me
.errno
== -errno
.ENOENT
:
247 except MetadataMgrException
as me
:
248 raise VolumeException(-errno
.EINVAL
, "error fetching subvolume metadata")
253 state
= SubvolumeStates
.from_value(self
.metadata_mgr
.get_global_option(MetadataManager
.GLOBAL_META_KEY_STATE
))
254 subvolume_type
= self
.subvol_type
256 'state' : state
.value
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
264 return SubvolumeStates
.from_value(self
.metadata_mgr
.get_global_option(MetadataManager
.GLOBAL_META_KEY_STATE
))
267 def state(self
, val
):
270 self
.metadata_mgr
.update_global_section(MetadataManager
.GLOBAL_META_KEY_STATE
, state
)
272 self
.metadata_mgr
.flush()
274 def remove(self
, retainsnaps
=False):
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()
281 def resize(self
, newsize
, noshrink
):
282 subvol_path
= self
.path
283 return self
._resize
(subvol_path
, newsize
, noshrink
)
285 def create_snapshot(self
, snapname
):
286 snappath
= self
.snapshot_path(snapname
)
287 mksnap(self
.fs
, snappath
)
289 def has_pending_clones(self
, snapname
):
291 return self
.metadata_mgr
.section_has_item('clone snaps', snapname
)
292 except MetadataMgrException
as me
:
293 if me
.errno
== -errno
.ENOENT
:
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
)
303 def snapshot_info(self
, snapname
):
304 snappath
= self
.snapshot_data_path(snapname
)
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])
321 def list_snapshots(self
):
323 dirpath
= self
.snapshot_base_path()
324 return listdir(self
.fs
, dirpath
)
325 except VolumeException
as ve
:
326 if ve
.errno
== -errno
.ENOENT
:
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()
335 def _remove_snap_clone(self
, track_id
):
336 self
.metadata_mgr
.remove_option("clone snaps", track_id
)
337 self
.metadata_mgr
.flush()
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
))
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")
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
))
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")