]>
Commit | Line | Data |
---|---|---|
11fdf7f2 TL |
1 | # -*- coding: utf-8 -*- |
2 | from __future__ import absolute_import | |
3 | ||
4 | import re | |
9f95a23c TL |
5 | import logging |
6 | import ipaddress | |
11fdf7f2 | 7 | from distutils.util import strtobool |
9f95a23c TL |
8 | import xml.etree.ElementTree as ET # noqa: N814 |
9 | import six | |
11fdf7f2 | 10 | from ..awsauth import S3Auth |
9f95a23c | 11 | from ..exceptions import DashboardException |
11fdf7f2 TL |
12 | from ..settings import Settings, Options |
13 | from ..rest_client import RestClient, RequestException | |
9f95a23c TL |
14 | from ..tools import build_url, dict_contains_path, json_str_to_object,\ |
15 | partial_dict, dict_get | |
16 | from .. import mgr | |
17 | ||
18 | try: | |
19 | from typing import Dict, List, Optional # pylint: disable=unused-import | |
20 | except ImportError: | |
21 | pass # For typing only | |
22 | ||
23 | logger = logging.getLogger('rgw_client') | |
11fdf7f2 TL |
24 | |
25 | ||
26 | class 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 | ||
34 | def _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 | ||
95 | def _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 | ||
149 | def _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 | ||
200 | class 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') |