]> git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/volumes/fs/subvolume.py
Add patch for failing prerm scripts
[ceph.git] / ceph / src / pybind / mgr / volumes / fs / subvolume.py
1 """
2 Copyright (C) 2019 Red Hat, Inc.
3
4 LGPL2.1. See file COPYING.
5 """
6
7 import logging
8 import os
9 import errno
10
11 import cephfs
12
13 from .subvolspec import SubvolumeSpec
14 from .exception import VolumeException
15
16 log = logging.getLogger(__name__)
17
18 class SubVolume(object):
19 """
20 Combine libcephfs and librados interfaces to implement a
21 'Subvolume' concept implemented as a cephfs directory.
22
23 Additionally, subvolumes may be in a 'Group'. Conveniently,
24 subvolumes are a lot like manila shares, and groups are a lot
25 like manila consistency groups.
26
27 Refer to subvolumes with SubvolumePath, which specifies the
28 subvolume and group IDs (both strings). The group ID may
29 be None.
30
31 In general, functions in this class are allowed raise rados.Error
32 or cephfs.Error exceptions in unexpected situations.
33 """
34
35
36 def __init__(self, mgr, fs_handle):
37 self.fs = fs_handle
38 self.rados = mgr.rados
39
40 def _get_single_dir_entry(self, dir_path, exclude=[]):
41 """
42 Return a directory entry in a given directory excluding passed
43 in entries.
44 """
45 exclude.extend((b".", b".."))
46 try:
47 with self.fs.opendir(dir_path) as d:
48 entry = self.fs.readdir(d)
49 while entry:
50 if entry.d_name not in exclude and entry.is_dir():
51 return entry.d_name
52 entry = self.fs.readdir(d)
53 return None
54 except cephfs.Error as e:
55 raise VolumeException(-e.args[0], e.args[1])
56
57
58 ### basic subvolume operations
59
60 def create_subvolume(self, spec, size=None, namespace_isolated=True, mode=0o755, pool=None):
61 """
62 Set up metadata, pools and auth for a subvolume.
63
64 This function is idempotent. It is safe to call this again
65 for an already-created subvolume, even if it is in use.
66
67 :param spec: subvolume path specification
68 :param size: In bytes, or None for no size limit
69 :param namespace_isolated: If true, use separate RADOS namespace for this subvolume
70 :param pool: the RADOS pool where the data objects of the subvolumes will be stored
71 :return: None
72 """
73 subvolpath = spec.subvolume_path
74 log.info("creating subvolume with path: {0}".format(subvolpath))
75
76 self.fs.mkdirs(subvolpath, mode)
77
78 try:
79 if size is not None:
80 try:
81 self.fs.setxattr(subvolpath, 'ceph.quota.max_bytes', str(size).encode('utf-8'), 0)
82 except cephfs.InvalidValue as e:
83 raise VolumeException(-errno.EINVAL, "Invalid size: '{0}'".format(size))
84 if pool:
85 try:
86 self.fs.setxattr(subvolpath, 'ceph.dir.layout.pool', pool.encode('utf-8'), 0)
87 except cephfs.InvalidValue:
88 raise VolumeException(-errno.EINVAL,
89 "Invalid pool layout '{0}'. It must be a valid data pool".format(pool))
90
91 xattr_key = xattr_val = None
92 if namespace_isolated:
93 # enforce security isolation, use separate namespace for this subvolume
94 xattr_key = 'ceph.dir.layout.pool_namespace'
95 xattr_val = spec.fs_namespace
96 elif not pool:
97 # If subvolume's namespace layout is not set, then the subvolume's pool
98 # layout remains unset and will undesirably change with ancestor's
99 # pool layout changes.
100 xattr_key = 'ceph.dir.layout.pool'
101 xattr_val = self._get_ancestor_xattr(subvolpath, "ceph.dir.layout.pool")
102 # TODO: handle error...
103 self.fs.setxattr(subvolpath, xattr_key, xattr_val.encode('utf-8'), 0)
104 except Exception as e:
105 try:
106 # cleanup subvol path on best effort basis
107 log.debug("cleaning up subvolume with path: {0}".format(subvolpath))
108 self.fs.rmdir(subvolpath)
109 except Exception:
110 log.debug("failed to clean up subvolume with path: {0}".format(subvolpath))
111 pass
112 finally:
113 raise e
114
115 def remove_subvolume(self, spec, force):
116 """
117 Make a subvolume inaccessible to guests. This function is idempotent.
118 This is the fast part of tearing down a subvolume. The subvolume will
119 get purged in the background.
120
121 :param spec: subvolume path specification
122 :param force: flag to ignore non-existent path (never raise exception)
123 :return: None
124 """
125
126 subvolpath = spec.subvolume_path
127 log.info("deleting subvolume with path: {0}".format(subvolpath))
128
129 # Create the trash directory if it doesn't already exist
130 trashdir = spec.trash_dir
131 self.fs.mkdirs(trashdir, 0o700)
132
133 # mangle the trash directroy entry to a random string so that subsequent
134 # subvolume create and delete with same name moves the subvolume directory
135 # to a unique trash dir (else, rename() could fail if the trash dir exist).
136 trashpath = spec.unique_trash_path
137 try:
138 self.fs.rename(subvolpath, trashpath)
139 except cephfs.ObjectNotFound:
140 if not force:
141 raise VolumeException(
142 -errno.ENOENT, "Subvolume '{0}' not found, cannot remove it".format(spec.subvolume_id))
143 except cephfs.Error as e:
144 raise VolumeException(-e.args[0], e.args[1])
145
146 def purge_subvolume(self, spec, should_cancel):
147 """
148 Finish clearing up a subvolume from the trash directory.
149 """
150
151 def rmtree(root_path):
152 log.debug("rmtree {0}".format(root_path))
153 try:
154 dir_handle = self.fs.opendir(root_path)
155 except cephfs.ObjectNotFound:
156 return
157 except cephfs.Error as e:
158 raise VolumeException(-e.args[0], e.args[1])
159 d = self.fs.readdir(dir_handle)
160 while d and not should_cancel():
161 if d.d_name not in (b".", b".."):
162 d_full = os.path.join(root_path, d.d_name)
163 if d.is_dir():
164 rmtree(d_full)
165 else:
166 self.fs.unlink(d_full)
167
168 d = self.fs.readdir(dir_handle)
169 self.fs.closedir(dir_handle)
170 # remove the directory only if we were not asked to cancel
171 # (else we would fail to remove this anyway)
172 if not should_cancel():
173 self.fs.rmdir(root_path)
174
175 trashpath = spec.trash_path
176 # catch any unlink errors
177 try:
178 rmtree(trashpath)
179 except cephfs.Error as e:
180 raise VolumeException(-e.args[0], e.args[1])
181
182 def get_subvolume_path(self, spec):
183 path = spec.subvolume_path
184 try:
185 self.fs.stat(path)
186 except cephfs.ObjectNotFound:
187 return None
188 except cephfs.Error as e:
189 raise VolumeException(-e.args[0], e.args[1])
190 return path
191
192 def get_dir_entries(self, path):
193 """
194 Get the directory names in a given path
195 :param path: the given path
196 :return: the list of directory names
197 """
198 dirs = []
199 try:
200 with self.fs.opendir(path) as dir_handle:
201 d = self.fs.readdir(dir_handle)
202 while d:
203 if (d.d_name not in (b".", b"..")) and d.is_dir():
204 dirs.append(d.d_name)
205 d = self.fs.readdir(dir_handle)
206 except cephfs.ObjectNotFound:
207 # When the given path is not found, we just return an empty list
208 return []
209 except cephfs.Error as e:
210 raise VolumeException(-e.args[0], e.args[1])
211 return dirs
212
213 ### group operations
214
215 def create_group(self, spec, mode=0o755, pool=None):
216 path = spec.group_path
217 self.fs.mkdirs(path, mode)
218 try:
219 if not pool:
220 pool = self._get_ancestor_xattr(path, "ceph.dir.layout.pool")
221 try:
222 self.fs.setxattr(path, 'ceph.dir.layout.pool', pool.encode('utf-8'), 0)
223 except cephfs.InvalidValue:
224 raise VolumeException(-errno.EINVAL,
225 "Invalid pool layout '{0}'. It must be a valid data pool".format(pool))
226 except Exception as e:
227 try:
228 # cleanup group path on best effort basis
229 log.debug("cleaning up subvolumegroup with path: {0}".format(path))
230 self.fs.rmdir(path)
231 except Exception:
232 log.debug("failed to clean up subvolumegroup with path: {0}".format(path))
233 pass
234 finally:
235 raise e
236
237 def remove_group(self, spec, force):
238 path = spec.group_path
239 try:
240 self.fs.rmdir(path)
241 except cephfs.ObjectNotFound:
242 if not force:
243 raise VolumeException(-errno.ENOENT, "Subvolume group '{0}' not found".format(spec.group_id))
244 except cephfs.Error as e:
245 raise VolumeException(-e.args[0], e.args[1])
246
247 def get_group_path(self, spec):
248 path = spec.group_path
249 try:
250 self.fs.stat(path)
251 except cephfs.ObjectNotFound:
252 return None
253 return path
254
255 def _get_ancestor_xattr(self, path, attr):
256 """
257 Helper for reading layout information: if this xattr is missing
258 on the requested path, keep checking parents until we find it.
259 """
260 try:
261 return self.fs.getxattr(path, attr).decode('utf-8')
262 except cephfs.NoData:
263 if path == "/":
264 raise
265 else:
266 return self._get_ancestor_xattr(os.path.split(path)[0], attr)
267
268 ### snapshot operations
269
270 def _snapshot_create(self, snappath, mode=0o755):
271 """
272 Create a snapshot, or do nothing if it already exists.
273 """
274 try:
275 self.fs.stat(snappath)
276 except cephfs.ObjectNotFound:
277 self.fs.mkdir(snappath, mode)
278 except cephfs.Error as e:
279 raise VolumeException(-e.args[0], e.args[1])
280 else:
281 log.warn("Snapshot '{0}' already exists".format(snappath))
282
283 def _snapshot_delete(self, snappath, force):
284 """
285 Remove a snapshot, or do nothing if it doesn't exist.
286 """
287 try:
288 self.fs.stat(snappath)
289 self.fs.rmdir(snappath)
290 except cephfs.ObjectNotFound:
291 if not force:
292 raise VolumeException(-errno.ENOENT, "Snapshot '{0}' not found, cannot remove it".format(snappath))
293 except cephfs.Error as e:
294 raise VolumeException(-e.args[0], e.args[1])
295
296 def create_subvolume_snapshot(self, spec, snapname, mode=0o755):
297 snappath = spec.make_subvol_snap_path(self.rados.conf_get('client_snapdir'), snapname)
298 self._snapshot_create(snappath, mode)
299
300 def remove_subvolume_snapshot(self, spec, snapname, force):
301 snappath = spec.make_subvol_snap_path(self.rados.conf_get('client_snapdir'), snapname)
302 self._snapshot_delete(snappath, force)
303
304 def create_group_snapshot(self, spec, snapname, mode=0o755):
305 snappath = spec.make_group_snap_path(self.rados.conf_get('client_snapdir'), snapname)
306 self._snapshot_create(snappath, mode)
307
308 def remove_group_snapshot(self, spec, snapname, force):
309 snappath = spec.make_group_snap_path(self.rados.conf_get('client_snapdir'), snapname)
310 return self._snapshot_delete(snappath, force)
311
312 def get_trash_entry(self, spec, exclude):
313 try:
314 trashdir = spec.trash_dir
315 return self._get_single_dir_entry(trashdir, exclude)
316 except VolumeException as ve:
317 if ve.errno == -errno.ENOENT:
318 # trash dir does not exist yet, signal success
319 return None
320 raise
321
322 ### context manager routines
323
324 def __enter__(self):
325 return self
326
327 def __exit__(self, exc_type, exc_val, exc_tb):
328 pass