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