]>
Commit | Line | Data |
---|---|---|
11fdf7f2 TL |
1 | # -*- coding: utf-8 -*- |
2 | from __future__ import absolute_import | |
3 | ||
4 | import re | |
5 | from distutils.util import strtobool | |
6 | from ..awsauth import S3Auth | |
7 | from ..settings import Settings, Options | |
8 | from ..rest_client import RestClient, RequestException | |
9 | from ..tools import build_url, dict_contains_path, is_valid_ip_address | |
10 | from .. import mgr, logger | |
11 | ||
12 | ||
13 | class NoCredentialsException(RequestException): | |
14 | def __init__(self): | |
15 | super(NoCredentialsException, self).__init__( | |
16 | 'No RGW credentials found, ' | |
17 | 'please consult the documentation on how to enable RGW for ' | |
18 | 'the dashboard.') | |
19 | ||
20 | ||
21 | def _determine_rgw_addr(): | |
22 | """ | |
23 | Get a RGW daemon to determine the configured host (IP address) and port. | |
24 | Note, the service id of the RGW daemons may differ depending on the setup. | |
25 | Example 1: | |
26 | { | |
27 | ... | |
28 | 'services': { | |
29 | 'rgw': { | |
30 | 'daemons': { | |
31 | 'summary': '', | |
32 | '0': { | |
33 | ... | |
34 | 'addr': '[2001:db8:85a3::8a2e:370:7334]:49774/1534999298', | |
35 | 'metadata': { | |
36 | 'frontend_config#0': 'civetweb port=7280', | |
37 | } | |
38 | ... | |
39 | } | |
40 | } | |
41 | } | |
42 | } | |
43 | } | |
44 | Example 2: | |
45 | { | |
46 | ... | |
47 | 'services': { | |
48 | 'rgw': { | |
49 | 'daemons': { | |
50 | 'summary': '', | |
51 | 'rgw': { | |
52 | ... | |
53 | 'addr': '192.168.178.3:49774/1534999298', | |
54 | 'metadata': { | |
55 | 'frontend_config#0': 'civetweb port=8000', | |
56 | } | |
57 | ... | |
58 | } | |
59 | } | |
60 | } | |
61 | } | |
62 | } | |
63 | """ | |
64 | service_map = mgr.get('service_map') | |
65 | if not dict_contains_path(service_map, ['services', 'rgw', 'daemons']): | |
66 | raise LookupError('No RGW found') | |
67 | daemon = None | |
68 | daemons = service_map['services']['rgw']['daemons'] | |
69 | for key in daemons.keys(): | |
70 | if dict_contains_path(daemons[key], ['metadata', 'frontend_config#0']): | |
71 | daemon = daemons[key] | |
72 | break | |
73 | if daemon is None: | |
74 | raise LookupError('No RGW daemon found') | |
75 | ||
76 | addr = _parse_addr(daemon['addr']) | |
77 | port, ssl = _parse_frontend_config(daemon['metadata']['frontend_config#0']) | |
78 | ||
79 | return addr, port, ssl | |
80 | ||
81 | ||
82 | def _parse_addr(value): | |
83 | """ | |
84 | Get the IP address the RGW is running on. | |
85 | ||
86 | >>> _parse_addr('192.168.178.3:49774/1534999298') | |
87 | '192.168.178.3' | |
88 | ||
89 | >>> _parse_addr('[2001:db8:85a3::8a2e:370:7334]:49774/1534999298') | |
90 | '2001:db8:85a3::8a2e:370:7334' | |
91 | ||
92 | >>> _parse_addr('xyz') | |
93 | Traceback (most recent call last): | |
94 | ... | |
95 | LookupError: Failed to determine RGW address | |
96 | ||
97 | >>> _parse_addr('192.168.178.a:8080/123456789') | |
98 | Traceback (most recent call last): | |
99 | ... | |
100 | LookupError: Invalid RGW address '192.168.178.a' found | |
101 | ||
102 | >>> _parse_addr('[2001:0db8:1234]:443/123456789') | |
103 | Traceback (most recent call last): | |
104 | ... | |
105 | LookupError: Invalid RGW address '2001:0db8:1234' found | |
106 | ||
107 | >>> _parse_addr('2001:0db8::1234:49774/1534999298') | |
108 | Traceback (most recent call last): | |
109 | ... | |
110 | LookupError: Failed to determine RGW address | |
111 | ||
112 | :param value: The string to process. The syntax is '<HOST>:<PORT>/<NONCE>'. | |
113 | :type: str | |
114 | :raises LookupError if parsing fails to determine the IP address. | |
115 | :return: The IP address. | |
116 | :rtype: str | |
117 | """ | |
118 | match = re.search(r'^(\[)?(?(1)([^\]]+)\]|([^:]+)):\d+/\d+?', value) | |
119 | if match: | |
120 | # IPv4: | |
121 | # Group 0: 192.168.178.3:49774/1534999298 | |
122 | # Group 3: 192.168.178.3 | |
123 | # IPv6: | |
124 | # Group 0: [2001:db8:85a3::8a2e:370:7334]:49774/1534999298 | |
125 | # Group 1: [ | |
126 | # Group 2: 2001:db8:85a3::8a2e:370:7334 | |
127 | addr = match.group(3) if match.group(3) else match.group(2) | |
128 | if not is_valid_ip_address(addr): | |
129 | raise LookupError('Invalid RGW address \'{}\' found'.format(addr)) | |
130 | return addr | |
131 | raise LookupError('Failed to determine RGW address') | |
132 | ||
133 | ||
134 | def _parse_frontend_config(config): | |
135 | """ | |
136 | Get the port the RGW is running on. Due the complexity of the | |
137 | syntax not all variations are supported. | |
138 | ||
139 | Get more details about the configuration syntax here: | |
140 | http://docs.ceph.com/docs/master/radosgw/frontends/ | |
141 | https://civetweb.github.io/civetweb/UserManual.html | |
142 | ||
143 | >>> _parse_frontend_config('beast port=8000') | |
144 | (8000, False) | |
145 | ||
146 | >>> _parse_frontend_config('civetweb port=8000s') | |
147 | (8000, True) | |
148 | ||
149 | >>> _parse_frontend_config('beast port=192.0.2.3:80') | |
150 | (80, False) | |
151 | ||
152 | >>> _parse_frontend_config('civetweb port=172.5.2.51:8080s') | |
153 | (8080, True) | |
154 | ||
155 | >>> _parse_frontend_config('civetweb port=[::]:8080') | |
156 | (8080, False) | |
157 | ||
158 | >>> _parse_frontend_config('civetweb port=ip6-localhost:80s') | |
159 | (80, True) | |
160 | ||
161 | >>> _parse_frontend_config('civetweb port=[2001:0db8::1234]:80') | |
162 | (80, False) | |
163 | ||
164 | >>> _parse_frontend_config('civetweb port=[::1]:8443s') | |
165 | (8443, True) | |
166 | ||
167 | >>> _parse_frontend_config('civetweb port=xyz') | |
168 | Traceback (most recent call last): | |
169 | ... | |
170 | LookupError: Failed to determine RGW port | |
171 | ||
172 | >>> _parse_frontend_config('civetweb') | |
173 | Traceback (most recent call last): | |
174 | ... | |
175 | LookupError: Failed to determine RGW port | |
176 | ||
177 | :param config: The configuration string to parse. | |
178 | :type config: str | |
179 | :raises LookupError if parsing fails to determine the port. | |
180 | :return: A tuple containing the port number and the information | |
181 | whether SSL is used. | |
182 | :rtype: (int, boolean) | |
183 | """ | |
184 | match = re.search(r'port=(.*:)?(\d+)(s)?', config) | |
185 | if match: | |
186 | port = int(match.group(2)) | |
187 | ssl = match.group(3) == 's' | |
188 | return port, ssl | |
189 | raise LookupError('Failed to determine RGW port') | |
190 | ||
191 | ||
192 | class RgwClient(RestClient): | |
193 | _SYSTEM_USERID = None | |
194 | _ADMIN_PATH = None | |
195 | _host = None | |
196 | _port = None | |
197 | _ssl = None | |
198 | _user_instances = {} | |
494da23a | 199 | _rgw_settings_snapshot = None |
11fdf7f2 TL |
200 | |
201 | @staticmethod | |
202 | def _load_settings(): | |
203 | # The API access key and secret key are mandatory for a minimal configuration. | |
204 | if not (Settings.RGW_API_ACCESS_KEY and Settings.RGW_API_SECRET_KEY): | |
205 | logger.warning('No credentials found, please consult the ' | |
206 | 'documentation about how to enable RGW for the ' | |
207 | 'dashboard.') | |
208 | raise NoCredentialsException() | |
209 | ||
210 | if Options.has_default_value('RGW_API_HOST') and \ | |
211 | Options.has_default_value('RGW_API_PORT') and \ | |
212 | Options.has_default_value('RGW_API_SCHEME'): | |
213 | host, port, ssl = _determine_rgw_addr() | |
214 | else: | |
215 | host = Settings.RGW_API_HOST | |
216 | port = Settings.RGW_API_PORT | |
217 | ssl = Settings.RGW_API_SCHEME == 'https' | |
218 | ||
219 | RgwClient._host = host | |
220 | RgwClient._port = port | |
221 | RgwClient._ssl = ssl | |
222 | RgwClient._ADMIN_PATH = Settings.RGW_API_ADMIN_RESOURCE | |
223 | ||
224 | # Create an instance using the configured settings. | |
225 | instance = RgwClient(Settings.RGW_API_USER_ID, | |
226 | Settings.RGW_API_ACCESS_KEY, | |
227 | Settings.RGW_API_SECRET_KEY) | |
228 | ||
229 | RgwClient._SYSTEM_USERID = instance.userid | |
230 | ||
231 | # Append the instance to the internal map. | |
232 | RgwClient._user_instances[RgwClient._SYSTEM_USERID] = instance | |
233 | ||
494da23a TL |
234 | @staticmethod |
235 | def _rgw_settings(): | |
236 | return (Settings.RGW_API_HOST, | |
237 | Settings.RGW_API_PORT, | |
238 | Settings.RGW_API_ACCESS_KEY, | |
239 | Settings.RGW_API_SECRET_KEY, | |
240 | Settings.RGW_API_ADMIN_RESOURCE, | |
241 | Settings.RGW_API_SCHEME, | |
242 | Settings.RGW_API_USER_ID, | |
243 | Settings.RGW_API_SSL_VERIFY) | |
244 | ||
11fdf7f2 TL |
245 | @staticmethod |
246 | def instance(userid): | |
494da23a TL |
247 | # Discard all cached instances if any rgw setting has changed |
248 | if RgwClient._rgw_settings_snapshot != RgwClient._rgw_settings(): | |
249 | RgwClient._rgw_settings_snapshot = RgwClient._rgw_settings() | |
250 | RgwClient._user_instances.clear() | |
251 | ||
11fdf7f2 TL |
252 | if not RgwClient._user_instances: |
253 | RgwClient._load_settings() | |
254 | ||
255 | if not userid: | |
256 | userid = RgwClient._SYSTEM_USERID | |
257 | ||
258 | if userid not in RgwClient._user_instances: | |
259 | # Get the access and secret keys for the specified user. | |
260 | keys = RgwClient.admin_instance().get_user_keys(userid) | |
261 | if not keys: | |
262 | raise RequestException( | |
263 | "User '{}' does not have any keys configured.".format( | |
264 | userid)) | |
265 | ||
266 | # Create an instance and append it to the internal map. | |
267 | RgwClient._user_instances[userid] = RgwClient(userid, | |
268 | keys['access_key'], | |
269 | keys['secret_key']) | |
270 | ||
271 | return RgwClient._user_instances[userid] | |
272 | ||
273 | @staticmethod | |
274 | def admin_instance(): | |
275 | return RgwClient.instance(RgwClient._SYSTEM_USERID) | |
276 | ||
277 | def _reset_login(self): | |
278 | if self.userid != RgwClient._SYSTEM_USERID: | |
279 | logger.info("Fetching new keys for user: %s", self.userid) | |
280 | keys = RgwClient.admin_instance().get_user_keys(self.userid) | |
281 | self.auth = S3Auth(keys['access_key'], keys['secret_key'], | |
282 | service_url=self.service_url) | |
283 | else: | |
284 | raise RequestException('Authentication failed for the "{}" user: wrong credentials' | |
285 | .format(self.userid), status_code=401) | |
286 | ||
287 | def __init__(self, # pylint: disable-msg=R0913 | |
288 | userid, | |
289 | access_key, | |
290 | secret_key, | |
291 | host=None, | |
292 | port=None, | |
81eedcae | 293 | admin_path=None, |
11fdf7f2 TL |
294 | ssl=False): |
295 | ||
296 | if not host and not RgwClient._host: | |
297 | RgwClient._load_settings() | |
298 | host = host if host else RgwClient._host | |
299 | port = port if port else RgwClient._port | |
300 | admin_path = admin_path if admin_path else RgwClient._ADMIN_PATH | |
301 | ssl = ssl if ssl else RgwClient._ssl | |
302 | ssl_verify = Settings.RGW_API_SSL_VERIFY | |
303 | ||
304 | self.service_url = build_url(host=host, port=port) | |
305 | self.admin_path = admin_path | |
306 | ||
307 | s3auth = S3Auth(access_key, secret_key, service_url=self.service_url) | |
308 | super(RgwClient, self).__init__(host, port, 'RGW', ssl, s3auth, ssl_verify=ssl_verify) | |
309 | ||
310 | # If user ID is not set, then try to get it via the RGW Admin Ops API. | |
311 | self.userid = userid if userid else self._get_user_id(self.admin_path) | |
312 | ||
313 | logger.info("Created new connection for user: %s", self.userid) | |
314 | ||
315 | @RestClient.api_get('/', resp_structure='[0] > (ID & DisplayName)') | |
316 | def is_service_online(self, request=None): | |
317 | """ | |
318 | Consider the service as online if the response contains the | |
319 | specified keys. Nothing more is checked here. | |
320 | """ | |
321 | _ = request({'format': 'json'}) | |
322 | return True | |
323 | ||
324 | @RestClient.api_get('/{admin_path}/metadata/user?myself', | |
325 | resp_structure='data > user_id') | |
326 | def _get_user_id(self, admin_path, request=None): | |
327 | # pylint: disable=unused-argument | |
328 | """ | |
329 | Get the user ID of the user that is used to communicate with the | |
330 | RGW Admin Ops API. | |
331 | :rtype: str | |
332 | :return: The user ID of the user that is used to sign the | |
333 | RGW Admin Ops API calls. | |
334 | """ | |
335 | response = request() | |
336 | return response['data']['user_id'] | |
337 | ||
338 | @RestClient.api_get('/{admin_path}/metadata/user', resp_structure='[+]') | |
339 | def _user_exists(self, admin_path, user_id, request=None): | |
340 | # pylint: disable=unused-argument | |
341 | response = request() | |
342 | if user_id: | |
343 | return user_id in response | |
344 | return self.userid in response | |
345 | ||
346 | def user_exists(self, user_id=None): | |
347 | return self._user_exists(self.admin_path, user_id) | |
348 | ||
349 | @RestClient.api_get('/{admin_path}/metadata/user?key={userid}', | |
350 | resp_structure='data > system') | |
351 | def _is_system_user(self, admin_path, userid, request=None): | |
352 | # pylint: disable=unused-argument | |
353 | response = request() | |
354 | return strtobool(response['data']['system']) | |
355 | ||
356 | def is_system_user(self): | |
357 | return self._is_system_user(self.admin_path, self.userid) | |
358 | ||
359 | @RestClient.api_get( | |
360 | '/{admin_path}/user', | |
361 | resp_structure='tenant & user_id & email & keys[*] > ' | |
362 | ' (user & access_key & secret_key)') | |
363 | def _admin_get_user_keys(self, admin_path, userid, request=None): | |
364 | # pylint: disable=unused-argument | |
365 | colon_idx = userid.find(':') | |
366 | user = userid if colon_idx == -1 else userid[:colon_idx] | |
367 | response = request({'uid': user}) | |
368 | for key in response['keys']: | |
369 | if key['user'] == userid: | |
370 | return { | |
371 | 'access_key': key['access_key'], | |
372 | 'secret_key': key['secret_key'] | |
373 | } | |
374 | return None | |
375 | ||
376 | def get_user_keys(self, userid): | |
377 | return self._admin_get_user_keys(self.admin_path, userid) | |
378 | ||
379 | @RestClient.api('/{admin_path}/{path}') | |
380 | def _proxy_request(self, # pylint: disable=too-many-arguments | |
381 | admin_path, | |
382 | path, | |
383 | method, | |
384 | params, | |
385 | data, | |
386 | request=None): | |
387 | # pylint: disable=unused-argument | |
388 | return request( | |
389 | method=method, params=params, data=data, raw_content=True) | |
390 | ||
391 | def proxy(self, method, path, params, data): | |
392 | logger.debug("proxying method=%s path=%s params=%s data=%s", method, | |
393 | path, params, data) | |
394 | return self._proxy_request(self.admin_path, path, method, params, data) | |
395 | ||
396 | @RestClient.api_get('/', resp_structure='[1][*] > Name') | |
397 | def get_buckets(self, request=None): | |
398 | """ | |
399 | Get a list of names from all existing buckets of this user. | |
400 | :return: Returns a list of bucket names. | |
401 | """ | |
402 | response = request({'format': 'json'}) | |
403 | return [bucket['Name'] for bucket in response[1]] | |
404 | ||
405 | @RestClient.api_get('/{bucket_name}') | |
406 | def bucket_exists(self, bucket_name, userid, request=None): | |
407 | """ | |
408 | Check if the specified bucket exists for this user. | |
409 | :param bucket_name: The name of the bucket. | |
410 | :return: Returns True if the bucket exists, otherwise False. | |
411 | """ | |
412 | # pylint: disable=unused-argument | |
413 | try: | |
414 | request() | |
415 | my_buckets = self.get_buckets() | |
416 | if bucket_name not in my_buckets: | |
417 | raise RequestException( | |
418 | 'Bucket "{}" belongs to other user'.format(bucket_name), | |
419 | 403) | |
420 | return True | |
421 | except RequestException as e: | |
422 | if e.status_code == 404: | |
423 | return False | |
424 | ||
425 | raise e | |
426 | ||
427 | @RestClient.api_put('/{bucket_name}') | |
428 | def create_bucket(self, bucket_name, request=None): | |
429 | logger.info("Creating bucket: %s", bucket_name) | |
430 | return request() |