]>
Commit | Line | Data |
---|---|---|
cd265ab1 TL |
1 | from contextlib import contextmanager |
2 | import os | |
3 | import fcntl | |
4 | import json | |
5 | import logging | |
6 | import struct | |
7 | import uuid | |
8 | ||
9 | import cephfs | |
10 | ||
11 | from ..group import Group | |
12 | ||
13 | log = logging.getLogger(__name__) | |
14 | ||
20effc67 | 15 | |
cd265ab1 TL |
16 | class AuthMetadataError(Exception): |
17 | pass | |
18 | ||
20effc67 | 19 | |
cd265ab1 TL |
20 | class AuthMetadataManager(object): |
21 | ||
22 | # Current version | |
23 | version = 6 | |
24 | ||
25 | # Filename extensions for meta files. | |
26 | META_FILE_EXT = ".meta" | |
27 | DEFAULT_VOL_PREFIX = "/volumes" | |
28 | ||
29 | def __init__(self, fs): | |
30 | self.fs = fs | |
31 | self._id = struct.unpack(">Q", uuid.uuid1().bytes[0:8])[0] | |
32 | self.volume_prefix = self.DEFAULT_VOL_PREFIX | |
33 | ||
34 | def _to_bytes(self, param): | |
35 | ''' | |
36 | Helper method that returns byte representation of the given parameter. | |
37 | ''' | |
38 | if isinstance(param, str): | |
39 | return param.encode('utf-8') | |
40 | elif param is None: | |
41 | return param | |
42 | else: | |
43 | return str(param).encode('utf-8') | |
44 | ||
45 | def _subvolume_metadata_path(self, group_name, subvol_name): | |
46 | return os.path.join(self.volume_prefix, "_{0}:{1}{2}".format( | |
47 | group_name if group_name != Group.NO_GROUP_NAME else "", | |
48 | subvol_name, | |
49 | self.META_FILE_EXT)) | |
50 | ||
51 | def _check_compat_version(self, compat_version): | |
52 | if self.version < compat_version: | |
53 | msg = ("The current version of AuthMetadataManager, version {0} " | |
54 | "does not support the required feature. Need version {1} " | |
55 | "or greater".format(self.version, compat_version) | |
20effc67 | 56 | ) |
cd265ab1 TL |
57 | log.error(msg) |
58 | raise AuthMetadataError(msg) | |
59 | ||
60 | def _metadata_get(self, path): | |
61 | """ | |
62 | Return a deserialized JSON object, or None | |
63 | """ | |
64 | fd = self.fs.open(path, "r") | |
65 | # TODO iterate instead of assuming file < 4MB | |
66 | read_bytes = self.fs.read(fd, 0, 4096 * 1024) | |
67 | self.fs.close(fd) | |
68 | if read_bytes: | |
69 | return json.loads(read_bytes.decode()) | |
70 | else: | |
71 | return None | |
72 | ||
73 | def _metadata_set(self, path, data): | |
74 | serialized = json.dumps(data) | |
75 | fd = self.fs.open(path, "w") | |
76 | try: | |
77 | self.fs.write(fd, self._to_bytes(serialized), 0) | |
78 | self.fs.fsync(fd, 0) | |
79 | finally: | |
80 | self.fs.close(fd) | |
81 | ||
82 | def _lock(self, path): | |
83 | @contextmanager | |
84 | def fn(): | |
85 | while(1): | |
86 | fd = self.fs.open(path, os.O_CREAT, 0o755) | |
87 | self.fs.flock(fd, fcntl.LOCK_EX, self._id) | |
88 | ||
89 | # The locked file will be cleaned up sometime. It could be | |
90 | # unlinked by consumer e.g., an another manila-share service | |
91 | # instance, before lock was applied on it. Perform checks to | |
92 | # ensure that this does not happen. | |
93 | try: | |
94 | statbuf = self.fs.stat(path) | |
95 | except cephfs.ObjectNotFound: | |
96 | self.fs.close(fd) | |
97 | continue | |
98 | ||
99 | fstatbuf = self.fs.fstat(fd) | |
100 | if statbuf.st_ino == fstatbuf.st_ino: | |
101 | break | |
102 | ||
103 | try: | |
104 | yield | |
105 | finally: | |
106 | self.fs.flock(fd, fcntl.LOCK_UN, self._id) | |
107 | self.fs.close(fd) | |
108 | ||
109 | return fn() | |
110 | ||
111 | def _auth_metadata_path(self, auth_id): | |
112 | return os.path.join(self.volume_prefix, "${0}{1}".format( | |
113 | auth_id, self.META_FILE_EXT)) | |
114 | ||
115 | def auth_lock(self, auth_id): | |
116 | return self._lock(self._auth_metadata_path(auth_id)) | |
117 | ||
118 | def auth_metadata_get(self, auth_id): | |
119 | """ | |
120 | Call me with the metadata locked! | |
121 | ||
122 | Check whether a auth metadata structure can be decoded by the current | |
123 | version of AuthMetadataManager. | |
124 | ||
125 | Return auth metadata that the current version of AuthMetadataManager | |
126 | can decode. | |
127 | """ | |
128 | auth_metadata = self._metadata_get(self._auth_metadata_path(auth_id)) | |
129 | ||
130 | if auth_metadata: | |
131 | self._check_compat_version(auth_metadata['compat_version']) | |
132 | ||
133 | return auth_metadata | |
134 | ||
135 | def auth_metadata_set(self, auth_id, data): | |
136 | """ | |
137 | Call me with the metadata locked! | |
138 | ||
139 | Fsync the auth metadata. | |
140 | ||
141 | Add two version attributes to the auth metadata, | |
142 | 'compat_version', the minimum AuthMetadataManager version that can | |
143 | decode the metadata, and 'version', the AuthMetadataManager version | |
144 | that encoded the metadata. | |
145 | """ | |
146 | data['compat_version'] = 6 | |
147 | data['version'] = self.version | |
148 | return self._metadata_set(self._auth_metadata_path(auth_id), data) | |
149 | ||
150 | def create_subvolume_metadata_file(self, group_name, subvol_name): | |
151 | """ | |
152 | Create a subvolume metadata file, if it does not already exist, to store | |
153 | data about auth ids having access to the subvolume | |
154 | """ | |
155 | fd = self.fs.open(self._subvolume_metadata_path(group_name, subvol_name), | |
156 | os.O_CREAT, 0o755) | |
157 | self.fs.close(fd) | |
158 | ||
159 | def delete_subvolume_metadata_file(self, group_name, subvol_name): | |
160 | vol_meta_path = self._subvolume_metadata_path(group_name, subvol_name) | |
161 | try: | |
162 | self.fs.unlink(vol_meta_path) | |
163 | except cephfs.ObjectNotFound: | |
164 | pass | |
165 | ||
166 | def subvol_metadata_lock(self, group_name, subvol_name): | |
167 | """ | |
168 | Return a ContextManager which locks the authorization metadata for | |
169 | a particular subvolume, and persists a flag to the metadata indicating | |
170 | that it is currently locked, so that we can detect dirty situations | |
171 | during recovery. | |
172 | ||
173 | This lock isn't just to make access to the metadata safe: it's also | |
174 | designed to be used over the two-step process of checking the | |
175 | metadata and then responding to an authorization request, to | |
176 | ensure that at the point we respond the metadata hasn't changed | |
177 | in the background. It's key to how we avoid security holes | |
178 | resulting from races during that problem , | |
179 | """ | |
180 | return self._lock(self._subvolume_metadata_path(group_name, subvol_name)) | |
181 | ||
182 | def subvol_metadata_get(self, group_name, subvol_name): | |
183 | """ | |
184 | Call me with the metadata locked! | |
185 | ||
186 | Check whether a subvolume metadata structure can be decoded by the current | |
187 | version of AuthMetadataManager. | |
188 | ||
189 | Return a subvolume_metadata structure that the current version of | |
190 | AuthMetadataManager can decode. | |
191 | """ | |
192 | subvolume_metadata = self._metadata_get(self._subvolume_metadata_path(group_name, subvol_name)) | |
193 | ||
194 | if subvolume_metadata: | |
195 | self._check_compat_version(subvolume_metadata['compat_version']) | |
196 | ||
197 | return subvolume_metadata | |
198 | ||
199 | def subvol_metadata_set(self, group_name, subvol_name, data): | |
200 | """ | |
201 | Call me with the metadata locked! | |
202 | ||
203 | Add two version attributes to the subvolume metadata, | |
204 | 'compat_version', the minimum AuthMetadataManager version that can | |
205 | decode the metadata and 'version', the AuthMetadataManager version | |
206 | that encoded the metadata. | |
207 | """ | |
208 | data['compat_version'] = 1 | |
209 | data['version'] = self.version | |
210 | return self._metadata_set(self._subvolume_metadata_path(group_name, subvol_name), data) |