]>
Commit | Line | Data |
---|---|---|
81eedcae TL |
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 | ||
494da23a TL |
36 | def __init__(self, mgr, fs_handle): |
37 | self.fs = fs_handle | |
81eedcae TL |
38 | self.rados = mgr.rados |
39 | ||
494da23a TL |
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"..")) | |
81eedcae | 46 | try: |
494da23a TL |
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]) | |
81eedcae | 56 | |
81eedcae TL |
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 | ||
494da23a | 76 | self.fs.mkdirs(subvolpath, mode) |
81eedcae | 77 | |
eafe8130 TL |
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 | |
81eedcae TL |
114 | |
115 | def remove_subvolume(self, spec, force): | |
116 | """ | |
117 | Make a subvolume inaccessible to guests. This function is idempotent. | |
494da23a TL |
118 | This is the fast part of tearing down a subvolume. The subvolume will |
119 | get purged in the background. | |
81eedcae TL |
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 | |
494da23a | 131 | self.fs.mkdirs(trashdir, 0o700) |
81eedcae | 132 | |
494da23a TL |
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 | |
81eedcae TL |
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: | |
494da23a | 144 | raise VolumeException(-e.args[0], e.args[1]) |
81eedcae | 145 | |
494da23a | 146 | def purge_subvolume(self, spec, should_cancel): |
81eedcae | 147 | """ |
494da23a | 148 | Finish clearing up a subvolume from the trash directory. |
81eedcae TL |
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: | |
494da23a | 158 | raise VolumeException(-e.args[0], e.args[1]) |
81eedcae | 159 | d = self.fs.readdir(dir_handle) |
494da23a TL |
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) | |
81eedcae TL |
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) | |
494da23a TL |
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) | |
81eedcae TL |
174 | |
175 | trashpath = spec.trash_path | |
494da23a TL |
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]) | |
81eedcae TL |
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: | |
494da23a | 189 | raise VolumeException(-e.args[0], e.args[1]) |
81eedcae TL |
190 | return path |
191 | ||
eafe8130 TL |
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 | ||
81eedcae TL |
213 | ### group operations |
214 | ||
215 | def create_group(self, spec, mode=0o755, pool=None): | |
216 | path = spec.group_path | |
494da23a | 217 | self.fs.mkdirs(path, mode) |
eafe8130 TL |
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 | |
81eedcae TL |
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: | |
494da23a | 245 | raise VolumeException(-e.args[0], e.args[1]) |
81eedcae TL |
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: | |
494da23a | 279 | raise VolumeException(-e.args[0], e.args[1]) |
81eedcae TL |
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: | |
494da23a | 294 | raise VolumeException(-e.args[0], e.args[1]) |
81eedcae TL |
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 | ||
494da23a TL |
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 | |
81eedcae | 321 | |
494da23a | 322 | ### context manager routines |
81eedcae TL |
323 | |
324 | def __enter__(self): | |
81eedcae TL |
325 | return self |
326 | ||
327 | def __exit__(self, exc_type, exc_val, exc_tb): | |
494da23a | 328 | pass |