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