]>
Commit | Line | Data |
---|---|---|
11fdf7f2 | 1 | # -*- coding: utf-8 -*- |
11fdf7f2 | 2 | |
9f95a23c | 3 | import ipaddress |
522d829b | 4 | import json |
f67539c2 TL |
5 | import logging |
6 | import re | |
9f95a23c | 7 | import xml.etree.ElementTree as ET # noqa: N814 |
f67539c2 | 8 | from distutils.util import strtobool |
522d829b TL |
9 | from subprocess import SubprocessError |
10 | ||
11 | from mgr_util import build_url | |
f67539c2 TL |
12 | |
13 | from .. import mgr | |
11fdf7f2 | 14 | from ..awsauth import S3Auth |
9f95a23c | 15 | from ..exceptions import DashboardException |
f67539c2 TL |
16 | from ..rest_client import RequestException, RestClient |
17 | from ..settings import Settings | |
522d829b | 18 | from ..tools import dict_contains_path, dict_get, json_str_to_object |
9f95a23c TL |
19 | |
20 | try: | |
b3b6e05e | 21 | from typing import Any, Dict, List, Optional, Tuple, Union |
9f95a23c TL |
22 | except ImportError: |
23 | pass # For typing only | |
24 | ||
25 | logger = logging.getLogger('rgw_client') | |
11fdf7f2 TL |
26 | |
27 | ||
f67539c2 TL |
28 | class NoRgwDaemonsException(Exception): |
29 | def __init__(self): | |
30 | super().__init__('No RGW service is running.') | |
31 | ||
32 | ||
522d829b | 33 | class NoCredentialsException(Exception): |
11fdf7f2 TL |
34 | def __init__(self): |
35 | super(NoCredentialsException, self).__init__( | |
36 | 'No RGW credentials found, ' | |
37 | 'please consult the documentation on how to enable RGW for ' | |
38 | 'the dashboard.') | |
39 | ||
40 | ||
522d829b TL |
41 | class RgwAdminException(Exception): |
42 | pass | |
43 | ||
44 | ||
f67539c2 TL |
45 | class RgwDaemon: |
46 | """Simple representation of a daemon.""" | |
47 | host: str | |
48 | name: str | |
49 | port: int | |
50 | ssl: bool | |
522d829b | 51 | realm_name: str |
f67539c2 | 52 | zonegroup_name: str |
522d829b | 53 | zone_name: str |
f67539c2 TL |
54 | |
55 | ||
56 | def _get_daemons() -> Dict[str, RgwDaemon]: | |
11fdf7f2 | 57 | """ |
f91f0fd5 | 58 | Retrieve RGW daemon info from MGR. |
11fdf7f2 TL |
59 | """ |
60 | service_map = mgr.get('service_map') | |
61 | if not dict_contains_path(service_map, ['services', 'rgw', 'daemons']): | |
f67539c2 | 62 | raise NoRgwDaemonsException |
11fdf7f2 | 63 | |
f67539c2 TL |
64 | daemons = {} |
65 | daemon_map = service_map['services']['rgw']['daemons'] | |
66 | for key in daemon_map.keys(): | |
67 | if dict_contains_path(daemon_map[key], ['metadata', 'frontend_config#0']): | |
68 | daemon = _determine_rgw_addr(daemon_map[key]) | |
69 | daemon.name = daemon_map[key]['metadata']['id'] | |
522d829b | 70 | daemon.realm_name = daemon_map[key]['metadata']['realm_name'] |
f67539c2 | 71 | daemon.zonegroup_name = daemon_map[key]['metadata']['zonegroup_name'] |
522d829b | 72 | daemon.zone_name = daemon_map[key]['metadata']['zone_name'] |
f67539c2 TL |
73 | daemons[daemon.name] = daemon |
74 | logger.info('Found RGW daemon with configuration: host=%s, port=%d, ssl=%s', | |
75 | daemon.host, daemon.port, str(daemon.ssl)) | |
76 | if not daemons: | |
77 | raise NoRgwDaemonsException | |
78 | ||
79 | return daemons | |
f91f0fd5 TL |
80 | |
81 | ||
f67539c2 | 82 | def _determine_rgw_addr(daemon_info: Dict[str, Any]) -> RgwDaemon: |
f91f0fd5 TL |
83 | """ |
84 | Parse RGW daemon info to determine the configured host (IP address) and port. | |
85 | """ | |
f67539c2 TL |
86 | daemon = RgwDaemon() |
87 | daemon.host = _parse_addr(daemon_info['addr']) | |
88 | daemon.port, daemon.ssl = _parse_frontend_config(daemon_info['metadata']['frontend_config#0']) | |
11fdf7f2 | 89 | |
f67539c2 | 90 | return daemon |
11fdf7f2 TL |
91 | |
92 | ||
adb31ebb | 93 | def _parse_addr(value) -> str: |
11fdf7f2 TL |
94 | """ |
95 | Get the IP address the RGW is running on. | |
96 | ||
97 | >>> _parse_addr('192.168.178.3:49774/1534999298') | |
98 | '192.168.178.3' | |
99 | ||
100 | >>> _parse_addr('[2001:db8:85a3::8a2e:370:7334]:49774/1534999298') | |
101 | '2001:db8:85a3::8a2e:370:7334' | |
102 | ||
103 | >>> _parse_addr('xyz') | |
104 | Traceback (most recent call last): | |
105 | ... | |
106 | LookupError: Failed to determine RGW address | |
107 | ||
108 | >>> _parse_addr('192.168.178.a:8080/123456789') | |
109 | Traceback (most recent call last): | |
110 | ... | |
111 | LookupError: Invalid RGW address '192.168.178.a' found | |
112 | ||
113 | >>> _parse_addr('[2001:0db8:1234]:443/123456789') | |
114 | Traceback (most recent call last): | |
115 | ... | |
116 | LookupError: Invalid RGW address '2001:0db8:1234' found | |
117 | ||
118 | >>> _parse_addr('2001:0db8::1234:49774/1534999298') | |
119 | Traceback (most recent call last): | |
120 | ... | |
121 | LookupError: Failed to determine RGW address | |
122 | ||
123 | :param value: The string to process. The syntax is '<HOST>:<PORT>/<NONCE>'. | |
124 | :type: str | |
125 | :raises LookupError if parsing fails to determine the IP address. | |
126 | :return: The IP address. | |
127 | :rtype: str | |
128 | """ | |
129 | match = re.search(r'^(\[)?(?(1)([^\]]+)\]|([^:]+)):\d+/\d+?', value) | |
130 | if match: | |
131 | # IPv4: | |
132 | # Group 0: 192.168.178.3:49774/1534999298 | |
133 | # Group 3: 192.168.178.3 | |
134 | # IPv6: | |
135 | # Group 0: [2001:db8:85a3::8a2e:370:7334]:49774/1534999298 | |
136 | # Group 1: [ | |
137 | # Group 2: 2001:db8:85a3::8a2e:370:7334 | |
138 | addr = match.group(3) if match.group(3) else match.group(2) | |
9f95a23c | 139 | try: |
f67539c2 | 140 | ipaddress.ip_address(addr) |
9f95a23c TL |
141 | return addr |
142 | except ValueError: | |
11fdf7f2 | 143 | raise LookupError('Invalid RGW address \'{}\' found'.format(addr)) |
11fdf7f2 TL |
144 | raise LookupError('Failed to determine RGW address') |
145 | ||
146 | ||
adb31ebb | 147 | def _parse_frontend_config(config) -> Tuple[int, bool]: |
11fdf7f2 TL |
148 | """ |
149 | Get the port the RGW is running on. Due the complexity of the | |
150 | syntax not all variations are supported. | |
151 | ||
9f95a23c TL |
152 | If there are multiple (ssl_)ports/(ssl_)endpoints options, then |
153 | the first found option will be returned. | |
154 | ||
11fdf7f2 | 155 | Get more details about the configuration syntax here: |
f67539c2 | 156 | http://docs.ceph.com/en/latest/radosgw/frontends/ |
11fdf7f2 TL |
157 | https://civetweb.github.io/civetweb/UserManual.html |
158 | ||
11fdf7f2 TL |
159 | :param config: The configuration string to parse. |
160 | :type config: str | |
161 | :raises LookupError if parsing fails to determine the port. | |
162 | :return: A tuple containing the port number and the information | |
163 | whether SSL is used. | |
164 | :rtype: (int, boolean) | |
165 | """ | |
9f95a23c | 166 | match = re.search(r'^(beast|civetweb)\s+.+$', config) |
11fdf7f2 | 167 | if match: |
9f95a23c TL |
168 | if match.group(1) == 'beast': |
169 | match = re.search(r'(port|ssl_port|endpoint|ssl_endpoint)=(.+)', | |
170 | config) | |
171 | if match: | |
172 | option_name = match.group(1) | |
173 | if option_name in ['port', 'ssl_port']: | |
174 | match = re.search(r'(\d+)', match.group(2)) | |
175 | if match: | |
176 | port = int(match.group(1)) | |
177 | ssl = option_name == 'ssl_port' | |
178 | return port, ssl | |
179 | if option_name in ['endpoint', 'ssl_endpoint']: | |
180 | match = re.search(r'([\d.]+|\[.+\])(:(\d+))?', | |
181 | match.group(2)) # type: ignore | |
182 | if match: | |
183 | port = int(match.group(3)) if \ | |
184 | match.group(2) is not None else 443 if \ | |
185 | option_name == 'ssl_endpoint' else \ | |
186 | 80 | |
187 | ssl = option_name == 'ssl_endpoint' | |
188 | return port, ssl | |
189 | if match.group(1) == 'civetweb': # type: ignore | |
190 | match = re.search(r'port=(.*:)?(\d+)(s)?', config) | |
191 | if match: | |
192 | port = int(match.group(2)) | |
193 | ssl = match.group(3) == 's' | |
194 | return port, ssl | |
195 | raise LookupError('Failed to determine RGW port from "{}"'.format(config)) | |
11fdf7f2 TL |
196 | |
197 | ||
522d829b TL |
198 | def _parse_secrets(user: str, data: dict) -> Tuple[str, str]: |
199 | for key in data.get('keys', []): | |
200 | if key.get('user') == user and data.get('system') in ['true', True]: | |
201 | access_key = key.get('access_key') | |
202 | secret_key = key.get('secret_key') | |
203 | return access_key, secret_key | |
204 | return '', '' | |
205 | ||
206 | ||
207 | def _get_user_keys(user: str, realm: Optional[str] = None) -> Tuple[str, str]: | |
208 | access_key = '' | |
209 | secret_key = '' | |
210 | rgw_user_info_cmd = ['user', 'info', '--uid', user] | |
211 | cmd_realm_option = ['--rgw-realm', realm] if realm else [] | |
212 | if realm: | |
213 | rgw_user_info_cmd += cmd_realm_option | |
214 | try: | |
215 | _, out, err = mgr.send_rgwadmin_command(rgw_user_info_cmd) | |
216 | if out: | |
217 | access_key, secret_key = _parse_secrets(user, out) | |
218 | if not access_key: | |
219 | rgw_create_user_cmd = [ | |
220 | 'user', 'create', | |
221 | '--uid', user, | |
222 | '--display-name', 'Ceph Dashboard', | |
223 | '--system', | |
224 | ] + cmd_realm_option | |
225 | _, out, err = mgr.send_rgwadmin_command(rgw_create_user_cmd) | |
226 | if out: | |
227 | access_key, secret_key = _parse_secrets(user, out) | |
228 | if not access_key: | |
229 | logger.error('Unable to create rgw user "%s": %s', user, err) | |
230 | except SubprocessError as error: | |
231 | logger.exception(error) | |
232 | ||
233 | return access_key, secret_key | |
234 | ||
235 | ||
236 | def configure_rgw_credentials(): | |
237 | logger.info('Configuring dashboard RGW credentials') | |
238 | user = 'dashboard' | |
239 | realms = [] | |
240 | access_key = '' | |
241 | secret_key = '' | |
242 | try: | |
243 | _, out, err = mgr.send_rgwadmin_command(['realm', 'list']) | |
244 | if out: | |
245 | realms = out.get('realms', []) | |
246 | if err: | |
247 | logger.error('Unable to list RGW realms: %s', err) | |
248 | if realms: | |
249 | realm_access_keys = {} | |
250 | realm_secret_keys = {} | |
251 | for realm in realms: | |
252 | realm_access_key, realm_secret_key = _get_user_keys(user, realm) | |
253 | if realm_access_key: | |
254 | realm_access_keys[realm] = realm_access_key | |
255 | realm_secret_keys[realm] = realm_secret_key | |
256 | if realm_access_keys: | |
257 | access_key = json.dumps(realm_access_keys) | |
258 | secret_key = json.dumps(realm_secret_keys) | |
259 | else: | |
260 | access_key, secret_key = _get_user_keys(user) | |
261 | ||
262 | assert access_key and secret_key | |
263 | Settings.RGW_API_ACCESS_KEY = access_key | |
264 | Settings.RGW_API_SECRET_KEY = secret_key | |
265 | except (AssertionError, SubprocessError) as error: | |
266 | logger.exception(error) | |
267 | raise NoCredentialsException | |
268 | ||
269 | ||
11fdf7f2 | 270 | class RgwClient(RestClient): |
11fdf7f2 TL |
271 | _host = None |
272 | _port = None | |
273 | _ssl = None | |
f67539c2 TL |
274 | _user_instances = {} # type: Dict[str, Dict[str, RgwClient]] |
275 | _config_instances = {} # type: Dict[str, RgwClient] | |
494da23a | 276 | _rgw_settings_snapshot = None |
f67539c2 TL |
277 | _daemons: Dict[str, RgwDaemon] = {} |
278 | daemon: RgwDaemon | |
279 | got_keys_from_config: bool | |
280 | userid: str | |
11fdf7f2 TL |
281 | |
282 | @staticmethod | |
f67539c2 TL |
283 | def _handle_response_status_code(status_code: int) -> int: |
284 | # Do not return auth error codes (so they are not handled as ceph API user auth errors). | |
285 | return 404 if status_code in [401, 403] else status_code | |
11fdf7f2 | 286 | |
f67539c2 TL |
287 | @staticmethod |
288 | def _get_daemon_connection_info(daemon_name: str) -> dict: | |
289 | try: | |
522d829b TL |
290 | realm_name = RgwClient._daemons[daemon_name].realm_name |
291 | access_key = Settings.RGW_API_ACCESS_KEY[realm_name] | |
292 | secret_key = Settings.RGW_API_SECRET_KEY[realm_name] | |
f67539c2 TL |
293 | except TypeError: |
294 | # Legacy string values. | |
295 | access_key = Settings.RGW_API_ACCESS_KEY | |
296 | secret_key = Settings.RGW_API_SECRET_KEY | |
297 | except KeyError as error: | |
298 | raise DashboardException(msg='Credentials not found for RGW Daemon: {}'.format(error), | |
299 | http_status_code=404, | |
300 | component='rgw') | |
11fdf7f2 | 301 | |
f67539c2 | 302 | return {'access_key': access_key, 'secret_key': secret_key} |
11fdf7f2 | 303 | |
9f95a23c TL |
304 | def _get_daemon_zone_info(self): # type: () -> dict |
305 | return json_str_to_object(self.proxy('GET', 'config?type=zone', None, None)) | |
306 | ||
e306af50 TL |
307 | def _get_realms_info(self): # type: () -> dict |
308 | return json_str_to_object(self.proxy('GET', 'realm?list', None, None)) | |
309 | ||
a4b75251 TL |
310 | def _get_realm_info(self, realm_id: str) -> Dict[str, Any]: |
311 | return json_str_to_object(self.proxy('GET', f'realm?id={realm_id}', None, None)) | |
312 | ||
494da23a TL |
313 | @staticmethod |
314 | def _rgw_settings(): | |
522d829b | 315 | return (Settings.RGW_API_ACCESS_KEY, |
494da23a TL |
316 | Settings.RGW_API_SECRET_KEY, |
317 | Settings.RGW_API_ADMIN_RESOURCE, | |
494da23a TL |
318 | Settings.RGW_API_SSL_VERIFY) |
319 | ||
11fdf7f2 | 320 | @staticmethod |
f67539c2 TL |
321 | def instance(userid: Optional[str] = None, |
322 | daemon_name: Optional[str] = None) -> 'RgwClient': | |
323 | # pylint: disable=too-many-branches | |
324 | ||
325 | RgwClient._daemons = _get_daemons() | |
326 | ||
327 | # The API access key and secret key are mandatory for a minimal configuration. | |
328 | if not (Settings.RGW_API_ACCESS_KEY and Settings.RGW_API_SECRET_KEY): | |
522d829b | 329 | configure_rgw_credentials() |
f67539c2 TL |
330 | |
331 | if not daemon_name: | |
f67539c2 | 332 | # Select 1st daemon: |
522d829b | 333 | daemon_name = next(iter(RgwClient._daemons.keys())) |
f67539c2 | 334 | |
494da23a TL |
335 | # Discard all cached instances if any rgw setting has changed |
336 | if RgwClient._rgw_settings_snapshot != RgwClient._rgw_settings(): | |
337 | RgwClient._rgw_settings_snapshot = RgwClient._rgw_settings() | |
adb31ebb | 338 | RgwClient.drop_instance() |
494da23a | 339 | |
f67539c2 TL |
340 | if daemon_name not in RgwClient._config_instances: |
341 | connection_info = RgwClient._get_daemon_connection_info(daemon_name) | |
342 | RgwClient._config_instances[daemon_name] = RgwClient(connection_info['access_key'], | |
343 | connection_info['secret_key'], | |
344 | daemon_name) | |
11fdf7f2 | 345 | |
f67539c2 TL |
346 | if not userid or userid == RgwClient._config_instances[daemon_name].userid: |
347 | return RgwClient._config_instances[daemon_name] | |
11fdf7f2 | 348 | |
f67539c2 TL |
349 | if daemon_name not in RgwClient._user_instances \ |
350 | or userid not in RgwClient._user_instances[daemon_name]: | |
11fdf7f2 | 351 | # Get the access and secret keys for the specified user. |
f67539c2 | 352 | keys = RgwClient._config_instances[daemon_name].get_user_keys(userid) |
11fdf7f2 TL |
353 | if not keys: |
354 | raise RequestException( | |
355 | "User '{}' does not have any keys configured.".format( | |
356 | userid)) | |
f67539c2 TL |
357 | instance = RgwClient(keys['access_key'], |
358 | keys['secret_key'], | |
359 | daemon_name, | |
360 | userid) | |
361 | RgwClient._user_instances.update({daemon_name: {userid: instance}}) | |
11fdf7f2 | 362 | |
f67539c2 | 363 | return RgwClient._user_instances[daemon_name][userid] |
11fdf7f2 TL |
364 | |
365 | @staticmethod | |
f67539c2 TL |
366 | def admin_instance(daemon_name: Optional[str] = None) -> 'RgwClient': |
367 | return RgwClient.instance(daemon_name=daemon_name) | |
11fdf7f2 | 368 | |
adb31ebb | 369 | @staticmethod |
f67539c2 | 370 | def drop_instance(instance: Optional['RgwClient'] = None): |
adb31ebb | 371 | """ |
f67539c2 | 372 | Drop a cached instance or all. |
adb31ebb | 373 | """ |
f67539c2 TL |
374 | if instance: |
375 | if instance.got_keys_from_config: | |
376 | del RgwClient._config_instances[instance.daemon.name] | |
377 | else: | |
378 | del RgwClient._user_instances[instance.daemon.name][instance.userid] | |
adb31ebb | 379 | else: |
f67539c2 | 380 | RgwClient._config_instances.clear() |
adb31ebb TL |
381 | RgwClient._user_instances.clear() |
382 | ||
11fdf7f2 | 383 | def _reset_login(self): |
f67539c2 | 384 | if self.got_keys_from_config: |
11fdf7f2 TL |
385 | raise RequestException('Authentication failed for the "{}" user: wrong credentials' |
386 | .format(self.userid), status_code=401) | |
f67539c2 TL |
387 | logger.info("Fetching new keys for user: %s", self.userid) |
388 | keys = RgwClient.admin_instance(daemon_name=self.daemon.name).get_user_keys(self.userid) | |
389 | self.auth = S3Auth(keys['access_key'], keys['secret_key'], | |
390 | service_url=self.service_url) | |
11fdf7f2 | 391 | |
f67539c2 | 392 | def __init__(self, |
522d829b TL |
393 | access_key: str, |
394 | secret_key: str, | |
395 | daemon_name: str, | |
396 | user_id: Optional[str] = None) -> None: | |
f67539c2 TL |
397 | try: |
398 | daemon = RgwClient._daemons[daemon_name] | |
399 | except KeyError as error: | |
400 | raise DashboardException(msg='RGW Daemon not found: {}'.format(error), | |
401 | http_status_code=404, | |
402 | component='rgw') | |
11fdf7f2 | 403 | ssl_verify = Settings.RGW_API_SSL_VERIFY |
f67539c2 TL |
404 | self.admin_path = Settings.RGW_API_ADMIN_RESOURCE |
405 | self.service_url = build_url(host=daemon.host, port=daemon.port) | |
406 | ||
407 | self.auth = S3Auth(access_key, secret_key, service_url=self.service_url) | |
408 | super(RgwClient, self).__init__(daemon.host, | |
409 | daemon.port, | |
410 | 'RGW', | |
411 | daemon.ssl, | |
412 | self.auth, | |
413 | ssl_verify=ssl_verify) | |
414 | self.got_keys_from_config = not user_id | |
415 | try: | |
416 | self.userid = self._get_user_id(self.admin_path) if self.got_keys_from_config \ | |
417 | else user_id | |
418 | except RequestException as error: | |
522d829b | 419 | logger.exception(error) |
f67539c2 TL |
420 | msg = 'Error connecting to Object Gateway' |
421 | if error.status_code == 404: | |
422 | msg = '{}: {}'.format(msg, str(error)) | |
423 | raise DashboardException(msg=msg, | |
424 | http_status_code=error.status_code, | |
425 | component='rgw') | |
426 | self.daemon = daemon | |
11fdf7f2 | 427 | |
f67539c2 TL |
428 | logger.info("Created new connection: daemon=%s, host=%s, port=%s, ssl=%d, sslverify=%d", |
429 | daemon.name, daemon.host, daemon.port, daemon.ssl, ssl_verify) | |
11fdf7f2 TL |
430 | |
431 | @RestClient.api_get('/', resp_structure='[0] > (ID & DisplayName)') | |
f67539c2 | 432 | def is_service_online(self, request=None) -> bool: |
11fdf7f2 TL |
433 | """ |
434 | Consider the service as online if the response contains the | |
435 | specified keys. Nothing more is checked here. | |
436 | """ | |
437 | _ = request({'format': 'json'}) | |
438 | return True | |
439 | ||
440 | @RestClient.api_get('/{admin_path}/metadata/user?myself', | |
441 | resp_structure='data > user_id') | |
442 | def _get_user_id(self, admin_path, request=None): | |
443 | # pylint: disable=unused-argument | |
444 | """ | |
445 | Get the user ID of the user that is used to communicate with the | |
446 | RGW Admin Ops API. | |
447 | :rtype: str | |
448 | :return: The user ID of the user that is used to sign the | |
449 | RGW Admin Ops API calls. | |
450 | """ | |
451 | response = request() | |
452 | return response['data']['user_id'] | |
453 | ||
454 | @RestClient.api_get('/{admin_path}/metadata/user', resp_structure='[+]') | |
455 | def _user_exists(self, admin_path, user_id, request=None): | |
456 | # pylint: disable=unused-argument | |
457 | response = request() | |
458 | if user_id: | |
459 | return user_id in response | |
460 | return self.userid in response | |
461 | ||
462 | def user_exists(self, user_id=None): | |
463 | return self._user_exists(self.admin_path, user_id) | |
464 | ||
465 | @RestClient.api_get('/{admin_path}/metadata/user?key={userid}', | |
466 | resp_structure='data > system') | |
f67539c2 | 467 | def _is_system_user(self, admin_path, userid, request=None) -> bool: |
11fdf7f2 TL |
468 | # pylint: disable=unused-argument |
469 | response = request() | |
470 | return strtobool(response['data']['system']) | |
471 | ||
f67539c2 | 472 | def is_system_user(self) -> bool: |
11fdf7f2 TL |
473 | return self._is_system_user(self.admin_path, self.userid) |
474 | ||
475 | @RestClient.api_get( | |
476 | '/{admin_path}/user', | |
477 | resp_structure='tenant & user_id & email & keys[*] > ' | |
478 | ' (user & access_key & secret_key)') | |
479 | def _admin_get_user_keys(self, admin_path, userid, request=None): | |
480 | # pylint: disable=unused-argument | |
481 | colon_idx = userid.find(':') | |
482 | user = userid if colon_idx == -1 else userid[:colon_idx] | |
483 | response = request({'uid': user}) | |
484 | for key in response['keys']: | |
485 | if key['user'] == userid: | |
486 | return { | |
487 | 'access_key': key['access_key'], | |
488 | 'secret_key': key['secret_key'] | |
489 | } | |
490 | return None | |
491 | ||
492 | def get_user_keys(self, userid): | |
493 | return self._admin_get_user_keys(self.admin_path, userid) | |
494 | ||
495 | @RestClient.api('/{admin_path}/{path}') | |
9f95a23c TL |
496 | def _proxy_request( |
497 | self, # pylint: disable=too-many-arguments | |
498 | admin_path, | |
499 | path, | |
500 | method, | |
501 | params, | |
502 | data, | |
503 | request=None): | |
11fdf7f2 | 504 | # pylint: disable=unused-argument |
9f95a23c TL |
505 | return request(method=method, params=params, data=data, |
506 | raw_content=True) | |
11fdf7f2 TL |
507 | |
508 | def proxy(self, method, path, params, data): | |
9f95a23c TL |
509 | logger.debug("proxying method=%s path=%s params=%s data=%s", |
510 | method, path, params, data) | |
511 | return self._proxy_request(self.admin_path, path, method, | |
512 | params, data) | |
11fdf7f2 TL |
513 | |
514 | @RestClient.api_get('/', resp_structure='[1][*] > Name') | |
515 | def get_buckets(self, request=None): | |
516 | """ | |
517 | Get a list of names from all existing buckets of this user. | |
518 | :return: Returns a list of bucket names. | |
519 | """ | |
520 | response = request({'format': 'json'}) | |
521 | return [bucket['Name'] for bucket in response[1]] | |
522 | ||
523 | @RestClient.api_get('/{bucket_name}') | |
524 | def bucket_exists(self, bucket_name, userid, request=None): | |
525 | """ | |
526 | Check if the specified bucket exists for this user. | |
527 | :param bucket_name: The name of the bucket. | |
528 | :return: Returns True if the bucket exists, otherwise False. | |
529 | """ | |
530 | # pylint: disable=unused-argument | |
531 | try: | |
532 | request() | |
533 | my_buckets = self.get_buckets() | |
534 | if bucket_name not in my_buckets: | |
535 | raise RequestException( | |
536 | 'Bucket "{}" belongs to other user'.format(bucket_name), | |
537 | 403) | |
538 | return True | |
539 | except RequestException as e: | |
540 | if e.status_code == 404: | |
541 | return False | |
542 | ||
543 | raise e | |
544 | ||
545 | @RestClient.api_put('/{bucket_name}') | |
9f95a23c TL |
546 | def create_bucket(self, bucket_name, zonegroup=None, |
547 | placement_target=None, lock_enabled=False, | |
548 | request=None): | |
549 | logger.info("Creating bucket: %s, zonegroup: %s, placement_target: %s", | |
550 | bucket_name, zonegroup, placement_target) | |
551 | data = None | |
552 | if zonegroup and placement_target: | |
553 | create_bucket_configuration = ET.Element('CreateBucketConfiguration') | |
554 | location_constraint = ET.SubElement(create_bucket_configuration, 'LocationConstraint') | |
555 | location_constraint.text = '{}:{}'.format(zonegroup, placement_target) | |
556 | data = ET.tostring(create_bucket_configuration, encoding='unicode') | |
557 | ||
558 | headers = None # type: Optional[dict] | |
559 | if lock_enabled: | |
560 | headers = {'x-amz-bucket-object-lock-enabled': 'true'} | |
561 | ||
562 | return request(data=data, headers=headers) | |
563 | ||
564 | def get_placement_targets(self): # type: () -> dict | |
565 | zone = self._get_daemon_zone_info() | |
9f95a23c TL |
566 | placement_targets = [] # type: List[Dict] |
567 | for placement_pool in zone['placement_pools']: | |
568 | placement_targets.append( | |
569 | { | |
570 | 'name': placement_pool['key'], | |
571 | 'data_pool': placement_pool['val']['storage_classes']['STANDARD']['data_pool'] | |
572 | } | |
573 | ) | |
574 | ||
f67539c2 TL |
575 | return {'zonegroup': self.daemon.zonegroup_name, |
576 | 'placement_targets': placement_targets} | |
9f95a23c | 577 | |
e306af50 TL |
578 | def get_realms(self): # type: () -> List |
579 | realms_info = self._get_realms_info() | |
580 | if 'realms' in realms_info and realms_info['realms']: | |
581 | return realms_info['realms'] | |
582 | ||
583 | return [] | |
584 | ||
a4b75251 TL |
585 | def get_default_realm(self) -> str: |
586 | realms_info = self._get_realms_info() | |
587 | if 'default_info' in realms_info and realms_info['default_info']: | |
588 | realm_info = self._get_realm_info(realms_info['default_info']) | |
589 | if 'name' in realm_info and realm_info['name']: | |
590 | return realm_info['name'] | |
591 | raise DashboardException(msg='Default realm not found.', | |
592 | http_status_code=404, | |
593 | component='rgw') | |
594 | ||
9f95a23c TL |
595 | @RestClient.api_get('/{bucket_name}?versioning') |
596 | def get_bucket_versioning(self, bucket_name, request=None): | |
597 | """ | |
598 | Get bucket versioning. | |
599 | :param str bucket_name: the name of the bucket. | |
600 | :return: versioning info | |
601 | :rtype: Dict | |
602 | """ | |
603 | # pylint: disable=unused-argument | |
604 | result = request() | |
605 | if 'Status' not in result: | |
606 | result['Status'] = 'Suspended' | |
607 | if 'MfaDelete' not in result: | |
608 | result['MfaDelete'] = 'Disabled' | |
609 | return result | |
610 | ||
611 | @RestClient.api_put('/{bucket_name}?versioning') | |
612 | def set_bucket_versioning(self, bucket_name, versioning_state, mfa_delete, | |
613 | mfa_token_serial, mfa_token_pin, request=None): | |
614 | """ | |
615 | Set bucket versioning. | |
616 | :param str bucket_name: the name of the bucket. | |
617 | :param str versioning_state: | |
618 | https://docs.aws.amazon.com/AmazonS3/latest/API/RESTBucketPUTVersioningStatus.html | |
619 | :param str mfa_delete: MFA Delete state. | |
620 | :param str mfa_token_serial: | |
621 | https://docs.ceph.com/docs/master/radosgw/mfa/ | |
622 | :param str mfa_token_pin: value of a TOTP token at a certain time (auth code) | |
623 | :return: None | |
624 | """ | |
625 | # pylint: disable=unused-argument | |
626 | versioning_configuration = ET.Element('VersioningConfiguration') | |
627 | status_element = ET.SubElement(versioning_configuration, 'Status') | |
628 | status_element.text = versioning_state | |
629 | ||
630 | headers = {} | |
631 | if mfa_delete and mfa_token_serial and mfa_token_pin: | |
632 | headers['x-amz-mfa'] = '{} {}'.format(mfa_token_serial, mfa_token_pin) | |
633 | mfa_delete_element = ET.SubElement(versioning_configuration, 'MfaDelete') | |
634 | mfa_delete_element.text = mfa_delete | |
635 | ||
636 | data = ET.tostring(versioning_configuration, encoding='unicode') | |
637 | ||
638 | try: | |
639 | request(data=data, headers=headers) | |
640 | except RequestException as error: | |
641 | msg = str(error) | |
f67539c2 TL |
642 | if mfa_delete and mfa_token_serial and mfa_token_pin \ |
643 | and 'AccessDenied' in error.content.decode(): | |
9f95a23c | 644 | msg = 'Bad MFA credentials: {}'.format(msg) |
9f95a23c | 645 | raise DashboardException(msg=msg, |
f67539c2 | 646 | http_status_code=error.status_code, |
9f95a23c TL |
647 | component='rgw') |
648 | ||
649 | @RestClient.api_get('/{bucket_name}?object-lock') | |
650 | def get_bucket_locking(self, bucket_name, request=None): | |
651 | # type: (str, Optional[object]) -> dict | |
652 | """ | |
653 | Gets the locking configuration for a bucket. The locking | |
654 | configuration will be applied by default to every new object | |
655 | placed in the specified bucket. | |
656 | :param bucket_name: The name of the bucket. | |
657 | :type bucket_name: str | |
658 | :return: The locking configuration. | |
659 | :rtype: Dict | |
660 | """ | |
661 | # pylint: disable=unused-argument | |
662 | ||
663 | # Try to get the Object Lock configuration. If there is none, | |
664 | # then return default values. | |
665 | try: | |
666 | result = request() # type: ignore | |
667 | return { | |
668 | 'lock_enabled': dict_get(result, 'ObjectLockEnabled') == 'Enabled', | |
669 | 'lock_mode': dict_get(result, 'Rule.DefaultRetention.Mode'), | |
670 | 'lock_retention_period_days': dict_get(result, 'Rule.DefaultRetention.Days', 0), | |
671 | 'lock_retention_period_years': dict_get(result, 'Rule.DefaultRetention.Years', 0) | |
672 | } | |
673 | except RequestException as e: | |
674 | if e.content: | |
675 | content = json_str_to_object(e.content) | |
676 | if content.get( | |
677 | 'Code') == 'ObjectLockConfigurationNotFoundError': | |
678 | return { | |
679 | 'lock_enabled': False, | |
680 | 'lock_mode': 'compliance', | |
681 | 'lock_retention_period_days': None, | |
682 | 'lock_retention_period_years': None | |
683 | } | |
684 | raise e | |
685 | ||
686 | @RestClient.api_put('/{bucket_name}?object-lock') | |
687 | def set_bucket_locking(self, | |
b3b6e05e TL |
688 | bucket_name: str, |
689 | mode: str, | |
690 | retention_period_days: Optional[Union[int, str]] = None, | |
691 | retention_period_years: Optional[Union[int, str]] = None, | |
692 | request: Optional[object] = None) -> None: | |
9f95a23c TL |
693 | """ |
694 | Places the locking configuration on the specified bucket. The | |
695 | locking configuration will be applied by default to every new | |
696 | object placed in the specified bucket. | |
697 | :param bucket_name: The name of the bucket. | |
698 | :type bucket_name: str | |
699 | :param mode: The lock mode, e.g. `COMPLIANCE` or `GOVERNANCE`. | |
700 | :type mode: str | |
701 | :param retention_period_days: | |
702 | :type retention_period_days: int | |
703 | :param retention_period_years: | |
704 | :type retention_period_years: int | |
705 | :rtype: None | |
706 | """ | |
707 | # pylint: disable=unused-argument | |
708 | ||
709 | # Do some validations. | |
b3b6e05e TL |
710 | try: |
711 | retention_period_days = int(retention_period_days) if retention_period_days else 0 | |
712 | retention_period_years = int(retention_period_years) if retention_period_years else 0 | |
713 | if retention_period_days < 0 or retention_period_years < 0: | |
714 | raise ValueError | |
715 | except (TypeError, ValueError): | |
716 | msg = "Retention period must be a positive integer." | |
717 | raise DashboardException(msg=msg, component='rgw') | |
9f95a23c TL |
718 | if retention_period_days and retention_period_years: |
719 | # https://docs.aws.amazon.com/AmazonS3/latest/API/archive-RESTBucketPUTObjectLockConfiguration.html | |
720 | msg = "Retention period requires either Days or Years. "\ | |
721 | "You can't specify both at the same time." | |
722 | raise DashboardException(msg=msg, component='rgw') | |
723 | if not retention_period_days and not retention_period_years: | |
724 | msg = "Retention period requires either Days or Years. "\ | |
725 | "You must specify at least one." | |
726 | raise DashboardException(msg=msg, component='rgw') | |
b3b6e05e TL |
727 | if not isinstance(mode, str) or mode.upper() not in ['COMPLIANCE', 'GOVERNANCE']: |
728 | msg = "Retention mode must be either COMPLIANCE or GOVERNANCE." | |
729 | raise DashboardException(msg=msg, component='rgw') | |
9f95a23c TL |
730 | |
731 | # Generate the XML data like this: | |
732 | # <ObjectLockConfiguration> | |
733 | # <ObjectLockEnabled>string</ObjectLockEnabled> | |
734 | # <Rule> | |
735 | # <DefaultRetention> | |
736 | # <Days>integer</Days> | |
737 | # <Mode>string</Mode> | |
738 | # <Years>integer</Years> | |
739 | # </DefaultRetention> | |
740 | # </Rule> | |
741 | # </ObjectLockConfiguration> | |
742 | locking_configuration = ET.Element('ObjectLockConfiguration') | |
743 | enabled_element = ET.SubElement(locking_configuration, | |
744 | 'ObjectLockEnabled') | |
745 | enabled_element.text = 'Enabled' # Locking can't be disabled. | |
746 | rule_element = ET.SubElement(locking_configuration, 'Rule') | |
747 | default_retention_element = ET.SubElement(rule_element, | |
748 | 'DefaultRetention') | |
749 | mode_element = ET.SubElement(default_retention_element, 'Mode') | |
750 | mode_element.text = mode.upper() | |
751 | if retention_period_days: | |
752 | days_element = ET.SubElement(default_retention_element, 'Days') | |
753 | days_element.text = str(retention_period_days) | |
754 | if retention_period_years: | |
755 | years_element = ET.SubElement(default_retention_element, 'Years') | |
756 | years_element.text = str(retention_period_years) | |
757 | ||
758 | data = ET.tostring(locking_configuration, encoding='unicode') | |
759 | ||
760 | try: | |
761 | _ = request(data=data) # type: ignore | |
762 | except RequestException as e: | |
763 | raise DashboardException(msg=str(e), component='rgw') |