]>
Commit | Line | Data |
---|---|---|
11fdf7f2 TL |
1 | # -*- coding: utf-8 -*- |
2 | from __future__ import absolute_import | |
3 | ||
4 | from contextlib import contextmanager | |
9f95a23c | 5 | import logging |
11fdf7f2 | 6 | |
9f95a23c TL |
7 | import datetime |
8 | import os | |
9 | import six | |
11fdf7f2 TL |
10 | import cephfs |
11 | ||
9f95a23c TL |
12 | from .. import mgr |
13 | ||
14 | ||
15 | logger = logging.getLogger('cephfs') | |
11fdf7f2 TL |
16 | |
17 | ||
18 | class CephFS(object): | |
19 | @classmethod | |
20 | def list_filesystems(cls): | |
21 | fsmap = mgr.get("fs_map") | |
22 | return [{'id': fs['id'], 'name': fs['mdsmap']['fs_name']} | |
23 | for fs in fsmap['filesystems']] | |
24 | ||
9f95a23c TL |
25 | @classmethod |
26 | def fs_name_from_id(cls, fs_id): | |
27 | """ | |
28 | Get the filesystem name from ID. | |
29 | :param fs_id: The filesystem ID. | |
30 | :type fs_id: int | str | |
31 | :return: The filesystem name or None. | |
32 | :rtype: str | None | |
33 | """ | |
34 | fs_map = mgr.get("fs_map") | |
35 | fs_info = list(filter(lambda x: str(x['id']) == str(fs_id), | |
36 | fs_map['filesystems'])) | |
37 | if not fs_info: | |
38 | return None | |
39 | return fs_info[0]['mdsmap']['fs_name'] | |
40 | ||
11fdf7f2 | 41 | def __init__(self, fs_name=None): |
9f95a23c | 42 | logger.debug("initializing cephfs connection") |
11fdf7f2 | 43 | self.cfs = cephfs.LibCephFS(rados_inst=mgr.rados) |
9f95a23c | 44 | logger.debug("mounting cephfs filesystem: %s", fs_name) |
11fdf7f2 TL |
45 | if fs_name: |
46 | self.cfs.mount(filesystem_name=fs_name) | |
47 | else: | |
48 | self.cfs.mount() | |
9f95a23c | 49 | logger.debug("mounted cephfs filesystem") |
11fdf7f2 TL |
50 | |
51 | def __del__(self): | |
9f95a23c | 52 | logger.debug("shutting down cephfs filesystem") |
11fdf7f2 TL |
53 | self.cfs.shutdown() |
54 | ||
55 | @contextmanager | |
56 | def opendir(self, dirpath): | |
57 | d = None | |
58 | try: | |
59 | d = self.cfs.opendir(dirpath) | |
60 | yield d | |
61 | finally: | |
62 | if d: | |
63 | self.cfs.closedir(d) | |
64 | ||
9f95a23c TL |
65 | def ls_dir(self, path, depth): |
66 | """ | |
67 | List directories of specified path with additional information. | |
68 | :param path: The root directory path. | |
69 | :type path: str | bytes | |
70 | :param depth: The number of steps to go down the directory tree. | |
71 | :type depth: int | str | |
72 | :return: A list of directory dicts which consist of name, path, | |
73 | parent, snapshots and quotas. | |
74 | :rtype: list | |
75 | """ | |
76 | paths = self._ls_dir(path, int(depth)) | |
77 | # Convert (bytes => string), prettify paths (strip slashes) | |
78 | # and append additional information. | |
79 | return [self.get_directory(p) for p in paths if p != path.encode()] | |
80 | ||
81 | def _ls_dir(self, path, depth): | |
82 | """ | |
83 | List directories of specified path. | |
84 | :param path: The root directory path. | |
85 | :type path: str | bytes | |
86 | :param depth: The number of steps to go down the directory tree. | |
87 | :type depth: int | |
88 | :return: A list of directory paths (bytes encoded). | |
89 | Example: | |
90 | ls_dir('/photos', 1) => [ | |
91 | b'/photos/flowers', b'/photos/cars' | |
92 | ] | |
93 | :rtype: list | |
94 | """ | |
95 | if isinstance(path, six.string_types): | |
96 | path = path.encode() | |
97 | logger.debug("get_dir_list dirpath=%s depth=%s", path, | |
98 | depth) | |
99 | if depth == 0: | |
100 | return [path] | |
101 | logger.debug("opening dirpath=%s", path) | |
102 | with self.opendir(path) as d: | |
11fdf7f2 | 103 | dent = self.cfs.readdir(d) |
9f95a23c | 104 | paths = [path] |
11fdf7f2 | 105 | while dent: |
9f95a23c TL |
106 | logger.debug("found entry=%s", dent.d_name) |
107 | if dent.d_name in [b'.', b'..']: | |
11fdf7f2 TL |
108 | dent = self.cfs.readdir(d) |
109 | continue | |
110 | if dent.is_dir(): | |
9f95a23c TL |
111 | logger.debug("found dir=%s", dent.d_name) |
112 | subdir_path = os.path.join(path, dent.d_name) | |
113 | paths.extend(self._ls_dir(subdir_path, depth - 1)) | |
11fdf7f2 TL |
114 | dent = self.cfs.readdir(d) |
115 | return paths | |
116 | ||
9f95a23c TL |
117 | def get_directory(self, path): |
118 | """ | |
119 | Transforms path of directory into a meaningful dictionary. | |
120 | :param path: The root directory path. | |
121 | :type path: str | bytes | |
122 | :return: Dict consists of name, path, parent, snapshots and quotas. | |
123 | :rtype: dict | |
124 | """ | |
125 | path = path.decode() | |
126 | not_root = path != os.sep | |
127 | return { | |
128 | 'name': os.path.basename(path) if not_root else path, | |
129 | 'path': path, | |
130 | 'parent': os.path.dirname(path) if not_root else None, | |
131 | 'snapshots': self.ls_snapshots(path), | |
132 | 'quotas': self.get_quotas(path) if not_root else None | |
133 | } | |
134 | ||
135 | def dir_exists(self, path): | |
11fdf7f2 | 136 | try: |
9f95a23c | 137 | with self.opendir(path): |
11fdf7f2 TL |
138 | return True |
139 | except cephfs.ObjectNotFound: | |
140 | return False | |
141 | ||
9f95a23c TL |
142 | def mk_dirs(self, path): |
143 | """ | |
144 | Create a directory. | |
145 | :param path: The path of the directory. | |
146 | """ | |
147 | if path == os.sep: | |
11fdf7f2 | 148 | raise Exception('Cannot create root directory "/"') |
9f95a23c TL |
149 | if self.dir_exists(path): |
150 | return | |
151 | logger.info("Creating directory: %s", path) | |
152 | self.cfs.mkdirs(path, 0o755) | |
153 | ||
154 | def rm_dir(self, path): | |
155 | """ | |
156 | Remove a directory. | |
157 | :param path: The path of the directory. | |
158 | """ | |
159 | if path == os.sep: | |
160 | raise Exception('Cannot remove root directory "/"') | |
161 | if not self.dir_exists(path): | |
11fdf7f2 | 162 | return |
9f95a23c TL |
163 | logger.info("Removing directory: %s", path) |
164 | self.cfs.rmdir(path) | |
165 | ||
166 | def mk_snapshot(self, path, name=None, mode=0o755): | |
167 | """ | |
168 | Create a snapshot. | |
169 | :param path: The path of the directory. | |
170 | :type path: str | |
171 | :param name: The name of the snapshot. If not specified, | |
172 | a name using the current time in RFC3339 UTC format | |
173 | will be generated. | |
174 | :type name: str | None | |
175 | :param mode: The permissions the directory should have | |
176 | once created. | |
177 | :type mode: int | |
178 | :return: Returns the name of the snapshot. | |
179 | :rtype: str | |
180 | """ | |
181 | if name is None: | |
182 | now = datetime.datetime.now() | |
183 | tz = now.astimezone().tzinfo | |
184 | name = now.replace(tzinfo=tz).isoformat('T') | |
185 | client_snapdir = self.cfs.conf_get('client_snapdir') | |
186 | snapshot_path = os.path.join(path, client_snapdir, name) | |
187 | logger.info("Creating snapshot: %s", snapshot_path) | |
188 | self.cfs.mkdir(snapshot_path, mode) | |
189 | return name | |
190 | ||
191 | def ls_snapshots(self, path): | |
192 | """ | |
193 | List snapshots for the specified path. | |
194 | :param path: The path of the directory. | |
195 | :type path: str | |
196 | :return: A list of dictionaries containing the name and the | |
197 | creation time of the snapshot. | |
198 | :rtype: list | |
199 | """ | |
200 | result = [] | |
201 | client_snapdir = self.cfs.conf_get('client_snapdir') | |
202 | path = os.path.join(path, client_snapdir).encode() | |
203 | with self.opendir(path) as d: | |
204 | dent = self.cfs.readdir(d) | |
205 | while dent: | |
206 | if dent.is_dir(): | |
207 | if dent.d_name not in [b'.', b'..'] and not dent.d_name.startswith(b'_'): | |
208 | snapshot_path = os.path.join(path, dent.d_name) | |
209 | stat = self.cfs.stat(snapshot_path) | |
210 | result.append({ | |
211 | 'name': dent.d_name.decode(), | |
212 | 'path': snapshot_path.decode(), | |
213 | 'created': '{}Z'.format(stat.st_ctime.isoformat('T')) | |
214 | }) | |
215 | dent = self.cfs.readdir(d) | |
216 | return result | |
217 | ||
218 | def rm_snapshot(self, path, name): | |
219 | """ | |
220 | Remove a snapshot. | |
221 | :param path: The path of the directory. | |
222 | :type path: str | |
223 | :param name: The name of the snapshot. | |
224 | :type name: str | |
225 | """ | |
226 | client_snapdir = self.cfs.conf_get('client_snapdir') | |
227 | snapshot_path = os.path.join(path, client_snapdir, name) | |
228 | logger.info("Removing snapshot: %s", snapshot_path) | |
229 | self.cfs.rmdir(snapshot_path) | |
230 | ||
231 | def get_quotas(self, path): | |
232 | """ | |
233 | Get the quotas of the specified path. | |
234 | :param path: The path of the directory/file. | |
235 | :type path: str | |
236 | :return: Returns a dictionary containing 'max_bytes' | |
237 | and 'max_files'. | |
238 | :rtype: dict | |
239 | """ | |
240 | try: | |
241 | max_bytes = int(self.cfs.getxattr(path, 'ceph.quota.max_bytes')) | |
242 | except cephfs.NoData: | |
243 | max_bytes = 0 | |
244 | try: | |
245 | max_files = int(self.cfs.getxattr(path, 'ceph.quota.max_files')) | |
246 | except cephfs.NoData: | |
247 | max_files = 0 | |
248 | return {'max_bytes': max_bytes, 'max_files': max_files} | |
11fdf7f2 | 249 | |
9f95a23c TL |
250 | def set_quotas(self, path, max_bytes=None, max_files=None): |
251 | """ | |
252 | Set the quotas of the specified path. | |
253 | :param path: The path of the directory/file. | |
254 | :type path: str | |
255 | :param max_bytes: The byte limit. | |
256 | :type max_bytes: int | None | |
257 | :param max_files: The file limit. | |
258 | :type max_files: int | None | |
259 | """ | |
260 | if max_bytes is not None: | |
261 | self.cfs.setxattr(path, 'ceph.quota.max_bytes', | |
262 | str(max_bytes).encode(), 0) | |
263 | if max_files is not None: | |
264 | self.cfs.setxattr(path, 'ceph.quota.max_files', | |
265 | str(max_files).encode(), 0) |