]> git.proxmox.com Git - ceph.git/blame - ceph/src/pybind/mgr/dashboard/services/rgw_client.py
import quincy beta 17.1.0
[ceph.git] / ceph / src / pybind / mgr / dashboard / services / rgw_client.py
CommitLineData
11fdf7f2 1# -*- coding: utf-8 -*-
11fdf7f2 2
9f95a23c 3import ipaddress
522d829b 4import json
f67539c2
TL
5import logging
6import re
9f95a23c 7import xml.etree.ElementTree as ET # noqa: N814
f67539c2 8from distutils.util import strtobool
522d829b
TL
9from subprocess import SubprocessError
10
11from mgr_util import build_url
f67539c2
TL
12
13from .. import mgr
11fdf7f2 14from ..awsauth import S3Auth
9f95a23c 15from ..exceptions import DashboardException
f67539c2
TL
16from ..rest_client import RequestException, RestClient
17from ..settings import Settings
522d829b 18from ..tools import dict_contains_path, dict_get, json_str_to_object
9f95a23c
TL
19
20try:
b3b6e05e 21 from typing import Any, Dict, List, Optional, Tuple, Union
9f95a23c
TL
22except ImportError:
23 pass # For typing only
24
25logger = logging.getLogger('rgw_client')
11fdf7f2
TL
26
27
f67539c2
TL
28class NoRgwDaemonsException(Exception):
29 def __init__(self):
30 super().__init__('No RGW service is running.')
31
32
522d829b 33class 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
41class RgwAdminException(Exception):
42 pass
43
44
f67539c2
TL
45class 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
56def _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 82def _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 93def _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 147def _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
198def _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
207def _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
236def 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 270class 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')