]> git.proxmox.com Git - ceph.git/blame - ceph/src/pybind/mgr/dashboard/services/rgw_client.py
dowstream patches: fix-up series file
[ceph.git] / ceph / src / pybind / mgr / dashboard / services / rgw_client.py
CommitLineData
11fdf7f2 1# -*- coding: utf-8 -*-
aee94f69
TL
2# pylint: disable=C0302
3# pylint: disable=too-many-branches
4# pylint: disable=too-many-lines
11fdf7f2 5
9f95a23c 6import ipaddress
522d829b 7import json
f67539c2 8import logging
1e59de90 9import os
f67539c2 10import re
9f95a23c 11import xml.etree.ElementTree as ET # noqa: N814
f78120f9 12from enum import Enum
522d829b
TL
13from subprocess import SubprocessError
14
aee94f69 15from mgr_util import build_url, name_to_config_section
f67539c2
TL
16
17from .. import mgr
11fdf7f2 18from ..awsauth import S3Auth
9f95a23c 19from ..exceptions import DashboardException
f67539c2
TL
20from ..rest_client import RequestException, RestClient
21from ..settings import Settings
aee94f69
TL
22from ..tools import dict_contains_path, dict_get, json_str_to_object, str_to_bool
23from .ceph_service import CephService
9f95a23c
TL
24
25try:
b3b6e05e 26 from typing import Any, Dict, List, Optional, Tuple, Union
9f95a23c
TL
27except ImportError:
28 pass # For typing only
29
30logger = logging.getLogger('rgw_client')
11fdf7f2 31
f78120f9
TL
32_SYNC_GROUP_ID = 'dashboard_admin_group'
33_SYNC_FLOW_ID = 'dashboard_admin_flow'
34_SYNC_PIPE_ID = 'dashboard_admin_pipe'
35
11fdf7f2 36
f67539c2
TL
37class NoRgwDaemonsException(Exception):
38 def __init__(self):
39 super().__init__('No RGW service is running.')
40
41
522d829b 42class NoCredentialsException(Exception):
11fdf7f2
TL
43 def __init__(self):
44 super(NoCredentialsException, self).__init__(
45 'No RGW credentials found, '
46 'please consult the documentation on how to enable RGW for '
47 'the dashboard.')
48
49
522d829b
TL
50class RgwAdminException(Exception):
51 pass
52
53
f67539c2
TL
54class RgwDaemon:
55 """Simple representation of a daemon."""
56 host: str
57 name: str
58 port: int
59 ssl: bool
522d829b 60 realm_name: str
f67539c2 61 zonegroup_name: str
f78120f9 62 zonegroup_id: str
522d829b 63 zone_name: str
f67539c2
TL
64
65
66def _get_daemons() -> Dict[str, RgwDaemon]:
11fdf7f2 67 """
f91f0fd5 68 Retrieve RGW daemon info from MGR.
11fdf7f2
TL
69 """
70 service_map = mgr.get('service_map')
71 if not dict_contains_path(service_map, ['services', 'rgw', 'daemons']):
f67539c2 72 raise NoRgwDaemonsException
11fdf7f2 73
f67539c2
TL
74 daemons = {}
75 daemon_map = service_map['services']['rgw']['daemons']
76 for key in daemon_map.keys():
77 if dict_contains_path(daemon_map[key], ['metadata', 'frontend_config#0']):
78 daemon = _determine_rgw_addr(daemon_map[key])
79 daemon.name = daemon_map[key]['metadata']['id']
522d829b 80 daemon.realm_name = daemon_map[key]['metadata']['realm_name']
f67539c2 81 daemon.zonegroup_name = daemon_map[key]['metadata']['zonegroup_name']
f78120f9 82 daemon.zonegroup_id = daemon_map[key]['metadata']['zonegroup_id']
522d829b 83 daemon.zone_name = daemon_map[key]['metadata']['zone_name']
f67539c2
TL
84 daemons[daemon.name] = daemon
85 logger.info('Found RGW daemon with configuration: host=%s, port=%d, ssl=%s',
86 daemon.host, daemon.port, str(daemon.ssl))
87 if not daemons:
88 raise NoRgwDaemonsException
89
90 return daemons
f91f0fd5
TL
91
92
f67539c2 93def _determine_rgw_addr(daemon_info: Dict[str, Any]) -> RgwDaemon:
f91f0fd5
TL
94 """
95 Parse RGW daemon info to determine the configured host (IP address) and port.
96 """
f67539c2 97 daemon = RgwDaemon()
aee94f69
TL
98 rgw_dns_name = CephService.send_command('mon', 'config get',
99 who=name_to_config_section('rgw.' + daemon_info['metadata']['id']), # noqa E501 #pylint: disable=line-too-long
100 key='rgw_dns_name').rstrip()
101
f67539c2 102 daemon.port, daemon.ssl = _parse_frontend_config(daemon_info['metadata']['frontend_config#0'])
11fdf7f2 103
aee94f69
TL
104 if rgw_dns_name:
105 daemon.host = rgw_dns_name
106 elif daemon.ssl:
107 daemon.host = daemon_info['metadata']['hostname']
108 else:
109 daemon.host = _parse_addr(daemon_info['addr'])
110
f67539c2 111 return daemon
11fdf7f2
TL
112
113
adb31ebb 114def _parse_addr(value) -> str:
11fdf7f2
TL
115 """
116 Get the IP address the RGW is running on.
117
118 >>> _parse_addr('192.168.178.3:49774/1534999298')
119 '192.168.178.3'
120
121 >>> _parse_addr('[2001:db8:85a3::8a2e:370:7334]:49774/1534999298')
122 '2001:db8:85a3::8a2e:370:7334'
123
124 >>> _parse_addr('xyz')
125 Traceback (most recent call last):
126 ...
127 LookupError: Failed to determine RGW address
128
129 >>> _parse_addr('192.168.178.a:8080/123456789')
130 Traceback (most recent call last):
131 ...
132 LookupError: Invalid RGW address '192.168.178.a' found
133
134 >>> _parse_addr('[2001:0db8:1234]:443/123456789')
135 Traceback (most recent call last):
136 ...
137 LookupError: Invalid RGW address '2001:0db8:1234' found
138
139 >>> _parse_addr('2001:0db8::1234:49774/1534999298')
140 Traceback (most recent call last):
141 ...
142 LookupError: Failed to determine RGW address
143
144 :param value: The string to process. The syntax is '<HOST>:<PORT>/<NONCE>'.
145 :type: str
146 :raises LookupError if parsing fails to determine the IP address.
147 :return: The IP address.
148 :rtype: str
149 """
150 match = re.search(r'^(\[)?(?(1)([^\]]+)\]|([^:]+)):\d+/\d+?', value)
151 if match:
152 # IPv4:
153 # Group 0: 192.168.178.3:49774/1534999298
154 # Group 3: 192.168.178.3
155 # IPv6:
156 # Group 0: [2001:db8:85a3::8a2e:370:7334]:49774/1534999298
157 # Group 1: [
158 # Group 2: 2001:db8:85a3::8a2e:370:7334
159 addr = match.group(3) if match.group(3) else match.group(2)
9f95a23c 160 try:
f67539c2 161 ipaddress.ip_address(addr)
9f95a23c
TL
162 return addr
163 except ValueError:
11fdf7f2 164 raise LookupError('Invalid RGW address \'{}\' found'.format(addr))
11fdf7f2
TL
165 raise LookupError('Failed to determine RGW address')
166
167
adb31ebb 168def _parse_frontend_config(config) -> Tuple[int, bool]:
11fdf7f2
TL
169 """
170 Get the port the RGW is running on. Due the complexity of the
171 syntax not all variations are supported.
172
9f95a23c
TL
173 If there are multiple (ssl_)ports/(ssl_)endpoints options, then
174 the first found option will be returned.
175
11fdf7f2 176 Get more details about the configuration syntax here:
f67539c2 177 http://docs.ceph.com/en/latest/radosgw/frontends/
11fdf7f2
TL
178 https://civetweb.github.io/civetweb/UserManual.html
179
11fdf7f2
TL
180 :param config: The configuration string to parse.
181 :type config: str
182 :raises LookupError if parsing fails to determine the port.
183 :return: A tuple containing the port number and the information
184 whether SSL is used.
185 :rtype: (int, boolean)
186 """
9f95a23c 187 match = re.search(r'^(beast|civetweb)\s+.+$', config)
11fdf7f2 188 if match:
9f95a23c
TL
189 if match.group(1) == 'beast':
190 match = re.search(r'(port|ssl_port|endpoint|ssl_endpoint)=(.+)',
191 config)
192 if match:
193 option_name = match.group(1)
194 if option_name in ['port', 'ssl_port']:
195 match = re.search(r'(\d+)', match.group(2))
196 if match:
197 port = int(match.group(1))
198 ssl = option_name == 'ssl_port'
199 return port, ssl
200 if option_name in ['endpoint', 'ssl_endpoint']:
201 match = re.search(r'([\d.]+|\[.+\])(:(\d+))?',
202 match.group(2)) # type: ignore
203 if match:
204 port = int(match.group(3)) if \
205 match.group(2) is not None else 443 if \
206 option_name == 'ssl_endpoint' else \
207 80
208 ssl = option_name == 'ssl_endpoint'
209 return port, ssl
210 if match.group(1) == 'civetweb': # type: ignore
211 match = re.search(r'port=(.*:)?(\d+)(s)?', config)
212 if match:
213 port = int(match.group(2))
214 ssl = match.group(3) == 's'
215 return port, ssl
216 raise LookupError('Failed to determine RGW port from "{}"'.format(config))
11fdf7f2
TL
217
218
522d829b
TL
219def _parse_secrets(user: str, data: dict) -> Tuple[str, str]:
220 for key in data.get('keys', []):
221 if key.get('user') == user and data.get('system') in ['true', True]:
222 access_key = key.get('access_key')
223 secret_key = key.get('secret_key')
224 return access_key, secret_key
225 return '', ''
226
227
228def _get_user_keys(user: str, realm: Optional[str] = None) -> Tuple[str, str]:
229 access_key = ''
230 secret_key = ''
231 rgw_user_info_cmd = ['user', 'info', '--uid', user]
232 cmd_realm_option = ['--rgw-realm', realm] if realm else []
233 if realm:
234 rgw_user_info_cmd += cmd_realm_option
235 try:
236 _, out, err = mgr.send_rgwadmin_command(rgw_user_info_cmd)
237 if out:
238 access_key, secret_key = _parse_secrets(user, out)
239 if not access_key:
240 rgw_create_user_cmd = [
241 'user', 'create',
242 '--uid', user,
243 '--display-name', 'Ceph Dashboard',
244 '--system',
245 ] + cmd_realm_option
246 _, out, err = mgr.send_rgwadmin_command(rgw_create_user_cmd)
247 if out:
248 access_key, secret_key = _parse_secrets(user, out)
249 if not access_key:
250 logger.error('Unable to create rgw user "%s": %s', user, err)
251 except SubprocessError as error:
252 logger.exception(error)
253
254 return access_key, secret_key
255
256
257def configure_rgw_credentials():
258 logger.info('Configuring dashboard RGW credentials')
259 user = 'dashboard'
260 realms = []
261 access_key = ''
262 secret_key = ''
263 try:
264 _, out, err = mgr.send_rgwadmin_command(['realm', 'list'])
265 if out:
266 realms = out.get('realms', [])
267 if err:
268 logger.error('Unable to list RGW realms: %s', err)
269 if realms:
270 realm_access_keys = {}
271 realm_secret_keys = {}
272 for realm in realms:
273 realm_access_key, realm_secret_key = _get_user_keys(user, realm)
274 if realm_access_key:
275 realm_access_keys[realm] = realm_access_key
276 realm_secret_keys[realm] = realm_secret_key
277 if realm_access_keys:
278 access_key = json.dumps(realm_access_keys)
279 secret_key = json.dumps(realm_secret_keys)
280 else:
281 access_key, secret_key = _get_user_keys(user)
282
283 assert access_key and secret_key
284 Settings.RGW_API_ACCESS_KEY = access_key
285 Settings.RGW_API_SECRET_KEY = secret_key
286 except (AssertionError, SubprocessError) as error:
287 logger.exception(error)
288 raise NoCredentialsException
289
290
39ae355f 291# pylint: disable=R0904
11fdf7f2 292class RgwClient(RestClient):
11fdf7f2
TL
293 _host = None
294 _port = None
295 _ssl = None
f67539c2
TL
296 _user_instances = {} # type: Dict[str, Dict[str, RgwClient]]
297 _config_instances = {} # type: Dict[str, RgwClient]
494da23a 298 _rgw_settings_snapshot = None
f67539c2
TL
299 _daemons: Dict[str, RgwDaemon] = {}
300 daemon: RgwDaemon
301 got_keys_from_config: bool
302 userid: str
11fdf7f2
TL
303
304 @staticmethod
f67539c2
TL
305 def _handle_response_status_code(status_code: int) -> int:
306 # Do not return auth error codes (so they are not handled as ceph API user auth errors).
307 return 404 if status_code in [401, 403] else status_code
11fdf7f2 308
f67539c2
TL
309 @staticmethod
310 def _get_daemon_connection_info(daemon_name: str) -> dict:
311 try:
522d829b
TL
312 realm_name = RgwClient._daemons[daemon_name].realm_name
313 access_key = Settings.RGW_API_ACCESS_KEY[realm_name]
314 secret_key = Settings.RGW_API_SECRET_KEY[realm_name]
f67539c2
TL
315 except TypeError:
316 # Legacy string values.
317 access_key = Settings.RGW_API_ACCESS_KEY
318 secret_key = Settings.RGW_API_SECRET_KEY
319 except KeyError as error:
320 raise DashboardException(msg='Credentials not found for RGW Daemon: {}'.format(error),
321 http_status_code=404,
322 component='rgw')
11fdf7f2 323
f67539c2 324 return {'access_key': access_key, 'secret_key': secret_key}
11fdf7f2 325
9f95a23c
TL
326 def _get_daemon_zone_info(self): # type: () -> dict
327 return json_str_to_object(self.proxy('GET', 'config?type=zone', None, None))
328
e306af50
TL
329 def _get_realms_info(self): # type: () -> dict
330 return json_str_to_object(self.proxy('GET', 'realm?list', None, None))
331
a4b75251
TL
332 def _get_realm_info(self, realm_id: str) -> Dict[str, Any]:
333 return json_str_to_object(self.proxy('GET', f'realm?id={realm_id}', None, None))
334
494da23a
TL
335 @staticmethod
336 def _rgw_settings():
522d829b 337 return (Settings.RGW_API_ACCESS_KEY,
494da23a
TL
338 Settings.RGW_API_SECRET_KEY,
339 Settings.RGW_API_ADMIN_RESOURCE,
494da23a
TL
340 Settings.RGW_API_SSL_VERIFY)
341
11fdf7f2 342 @staticmethod
f67539c2
TL
343 def instance(userid: Optional[str] = None,
344 daemon_name: Optional[str] = None) -> 'RgwClient':
345 # pylint: disable=too-many-branches
346
347 RgwClient._daemons = _get_daemons()
348
349 # The API access key and secret key are mandatory for a minimal configuration.
350 if not (Settings.RGW_API_ACCESS_KEY and Settings.RGW_API_SECRET_KEY):
522d829b 351 configure_rgw_credentials()
f67539c2 352
f78120f9 353 daemon_keys = RgwClient._daemons.keys()
f67539c2 354 if not daemon_name:
f78120f9
TL
355 if len(daemon_keys) > 1:
356 try:
357 multiiste = RgwMultisite()
358 default_zonegroup = multiiste.get_all_zonegroups_info()['default_zonegroup']
359
360 # Iterate through _daemons.values() to find the daemon with the
361 # matching zonegroup_id
362 for daemon in RgwClient._daemons.values():
363 if daemon.zonegroup_id == default_zonegroup:
364 daemon_name = daemon.name
365 break
366 except Exception: # pylint: disable=broad-except
367 daemon_name = next(iter(daemon_keys))
368 else:
369 # Handle the case where there is only one or no key in _daemons
370 daemon_name = next(iter(daemon_keys))
f67539c2 371
494da23a
TL
372 # Discard all cached instances if any rgw setting has changed
373 if RgwClient._rgw_settings_snapshot != RgwClient._rgw_settings():
374 RgwClient._rgw_settings_snapshot = RgwClient._rgw_settings()
adb31ebb 375 RgwClient.drop_instance()
494da23a 376
f67539c2 377 if daemon_name not in RgwClient._config_instances:
f78120f9
TL
378 connection_info = RgwClient._get_daemon_connection_info(daemon_name) # type: ignore
379 RgwClient._config_instances[daemon_name] = RgwClient(connection_info['access_key'], # type: ignore # noqa E501 #pylint: disable=line-too-long
f67539c2 380 connection_info['secret_key'],
f78120f9 381 daemon_name) # type: ignore
11fdf7f2 382
f78120f9
TL
383 if not userid or userid == RgwClient._config_instances[daemon_name].userid: # type: ignore
384 return RgwClient._config_instances[daemon_name] # type: ignore
11fdf7f2 385
f67539c2
TL
386 if daemon_name not in RgwClient._user_instances \
387 or userid not in RgwClient._user_instances[daemon_name]:
11fdf7f2 388 # Get the access and secret keys for the specified user.
f78120f9 389 keys = RgwClient._config_instances[daemon_name].get_user_keys(userid) # type: ignore
11fdf7f2
TL
390 if not keys:
391 raise RequestException(
392 "User '{}' does not have any keys configured.".format(
393 userid))
f67539c2
TL
394 instance = RgwClient(keys['access_key'],
395 keys['secret_key'],
f78120f9 396 daemon_name, # type: ignore
f67539c2 397 userid)
f78120f9 398 RgwClient._user_instances.update({daemon_name: {userid: instance}}) # type: ignore
11fdf7f2 399
f78120f9 400 return RgwClient._user_instances[daemon_name][userid] # type: ignore
11fdf7f2
TL
401
402 @staticmethod
f67539c2
TL
403 def admin_instance(daemon_name: Optional[str] = None) -> 'RgwClient':
404 return RgwClient.instance(daemon_name=daemon_name)
11fdf7f2 405
adb31ebb 406 @staticmethod
f67539c2 407 def drop_instance(instance: Optional['RgwClient'] = None):
adb31ebb 408 """
f67539c2 409 Drop a cached instance or all.
adb31ebb 410 """
f67539c2
TL
411 if instance:
412 if instance.got_keys_from_config:
413 del RgwClient._config_instances[instance.daemon.name]
414 else:
415 del RgwClient._user_instances[instance.daemon.name][instance.userid]
adb31ebb 416 else:
f67539c2 417 RgwClient._config_instances.clear()
adb31ebb
TL
418 RgwClient._user_instances.clear()
419
11fdf7f2 420 def _reset_login(self):
f67539c2 421 if self.got_keys_from_config:
11fdf7f2
TL
422 raise RequestException('Authentication failed for the "{}" user: wrong credentials'
423 .format(self.userid), status_code=401)
f67539c2
TL
424 logger.info("Fetching new keys for user: %s", self.userid)
425 keys = RgwClient.admin_instance(daemon_name=self.daemon.name).get_user_keys(self.userid)
426 self.auth = S3Auth(keys['access_key'], keys['secret_key'],
427 service_url=self.service_url)
11fdf7f2 428
f67539c2 429 def __init__(self,
522d829b
TL
430 access_key: str,
431 secret_key: str,
432 daemon_name: str,
433 user_id: Optional[str] = None) -> None:
f67539c2
TL
434 try:
435 daemon = RgwClient._daemons[daemon_name]
436 except KeyError as error:
437 raise DashboardException(msg='RGW Daemon not found: {}'.format(error),
438 http_status_code=404,
439 component='rgw')
11fdf7f2 440 ssl_verify = Settings.RGW_API_SSL_VERIFY
f67539c2
TL
441 self.admin_path = Settings.RGW_API_ADMIN_RESOURCE
442 self.service_url = build_url(host=daemon.host, port=daemon.port)
443
444 self.auth = S3Auth(access_key, secret_key, service_url=self.service_url)
445 super(RgwClient, self).__init__(daemon.host,
446 daemon.port,
447 'RGW',
448 daemon.ssl,
449 self.auth,
450 ssl_verify=ssl_verify)
451 self.got_keys_from_config = not user_id
452 try:
453 self.userid = self._get_user_id(self.admin_path) if self.got_keys_from_config \
454 else user_id
455 except RequestException as error:
522d829b 456 logger.exception(error)
f67539c2
TL
457 msg = 'Error connecting to Object Gateway'
458 if error.status_code == 404:
459 msg = '{}: {}'.format(msg, str(error))
460 raise DashboardException(msg=msg,
461 http_status_code=error.status_code,
462 component='rgw')
463 self.daemon = daemon
11fdf7f2 464
f67539c2
TL
465 logger.info("Created new connection: daemon=%s, host=%s, port=%s, ssl=%d, sslverify=%d",
466 daemon.name, daemon.host, daemon.port, daemon.ssl, ssl_verify)
11fdf7f2
TL
467
468 @RestClient.api_get('/', resp_structure='[0] > (ID & DisplayName)')
f67539c2 469 def is_service_online(self, request=None) -> bool:
11fdf7f2
TL
470 """
471 Consider the service as online if the response contains the
472 specified keys. Nothing more is checked here.
473 """
474 _ = request({'format': 'json'})
475 return True
476
477 @RestClient.api_get('/{admin_path}/metadata/user?myself',
478 resp_structure='data > user_id')
479 def _get_user_id(self, admin_path, request=None):
480 # pylint: disable=unused-argument
481 """
482 Get the user ID of the user that is used to communicate with the
483 RGW Admin Ops API.
484 :rtype: str
485 :return: The user ID of the user that is used to sign the
486 RGW Admin Ops API calls.
487 """
488 response = request()
489 return response['data']['user_id']
490
491 @RestClient.api_get('/{admin_path}/metadata/user', resp_structure='[+]')
492 def _user_exists(self, admin_path, user_id, request=None):
493 # pylint: disable=unused-argument
494 response = request()
495 if user_id:
496 return user_id in response
497 return self.userid in response
498
499 def user_exists(self, user_id=None):
500 return self._user_exists(self.admin_path, user_id)
501
502 @RestClient.api_get('/{admin_path}/metadata/user?key={userid}',
503 resp_structure='data > system')
f67539c2 504 def _is_system_user(self, admin_path, userid, request=None) -> bool:
11fdf7f2
TL
505 # pylint: disable=unused-argument
506 response = request()
1e59de90 507 return response['data']['system']
11fdf7f2 508
f67539c2 509 def is_system_user(self) -> bool:
11fdf7f2
TL
510 return self._is_system_user(self.admin_path, self.userid)
511
512 @RestClient.api_get(
513 '/{admin_path}/user',
514 resp_structure='tenant & user_id & email & keys[*] > '
515 ' (user & access_key & secret_key)')
516 def _admin_get_user_keys(self, admin_path, userid, request=None):
517 # pylint: disable=unused-argument
518 colon_idx = userid.find(':')
519 user = userid if colon_idx == -1 else userid[:colon_idx]
520 response = request({'uid': user})
521 for key in response['keys']:
522 if key['user'] == userid:
523 return {
524 'access_key': key['access_key'],
525 'secret_key': key['secret_key']
526 }
527 return None
528
529 def get_user_keys(self, userid):
530 return self._admin_get_user_keys(self.admin_path, userid)
531
532 @RestClient.api('/{admin_path}/{path}')
9f95a23c
TL
533 def _proxy_request(
534 self, # pylint: disable=too-many-arguments
535 admin_path,
536 path,
537 method,
538 params,
539 data,
540 request=None):
11fdf7f2 541 # pylint: disable=unused-argument
9f95a23c
TL
542 return request(method=method, params=params, data=data,
543 raw_content=True)
11fdf7f2
TL
544
545 def proxy(self, method, path, params, data):
9f95a23c
TL
546 logger.debug("proxying method=%s path=%s params=%s data=%s",
547 method, path, params, data)
548 return self._proxy_request(self.admin_path, path, method,
549 params, data)
11fdf7f2
TL
550
551 @RestClient.api_get('/', resp_structure='[1][*] > Name')
552 def get_buckets(self, request=None):
553 """
554 Get a list of names from all existing buckets of this user.
555 :return: Returns a list of bucket names.
556 """
557 response = request({'format': 'json'})
558 return [bucket['Name'] for bucket in response[1]]
559
560 @RestClient.api_get('/{bucket_name}')
561 def bucket_exists(self, bucket_name, userid, request=None):
562 """
563 Check if the specified bucket exists for this user.
564 :param bucket_name: The name of the bucket.
565 :return: Returns True if the bucket exists, otherwise False.
566 """
567 # pylint: disable=unused-argument
568 try:
569 request()
570 my_buckets = self.get_buckets()
571 if bucket_name not in my_buckets:
572 raise RequestException(
573 'Bucket "{}" belongs to other user'.format(bucket_name),
574 403)
575 return True
576 except RequestException as e:
577 if e.status_code == 404:
578 return False
579
580 raise e
581
582 @RestClient.api_put('/{bucket_name}')
9f95a23c
TL
583 def create_bucket(self, bucket_name, zonegroup=None,
584 placement_target=None, lock_enabled=False,
585 request=None):
586 logger.info("Creating bucket: %s, zonegroup: %s, placement_target: %s",
587 bucket_name, zonegroup, placement_target)
588 data = None
589 if zonegroup and placement_target:
590 create_bucket_configuration = ET.Element('CreateBucketConfiguration')
591 location_constraint = ET.SubElement(create_bucket_configuration, 'LocationConstraint')
592 location_constraint.text = '{}:{}'.format(zonegroup, placement_target)
593 data = ET.tostring(create_bucket_configuration, encoding='unicode')
594
595 headers = None # type: Optional[dict]
596 if lock_enabled:
597 headers = {'x-amz-bucket-object-lock-enabled': 'true'}
598
599 return request(data=data, headers=headers)
600
601 def get_placement_targets(self): # type: () -> dict
602 zone = self._get_daemon_zone_info()
9f95a23c
TL
603 placement_targets = [] # type: List[Dict]
604 for placement_pool in zone['placement_pools']:
605 placement_targets.append(
606 {
607 'name': placement_pool['key'],
608 'data_pool': placement_pool['val']['storage_classes']['STANDARD']['data_pool']
609 }
610 )
611
f67539c2
TL
612 return {'zonegroup': self.daemon.zonegroup_name,
613 'placement_targets': placement_targets}
9f95a23c 614
e306af50
TL
615 def get_realms(self): # type: () -> List
616 realms_info = self._get_realms_info()
617 if 'realms' in realms_info and realms_info['realms']:
618 return realms_info['realms']
e306af50
TL
619 return []
620
aee94f69 621 def get_default_realm(self):
a4b75251
TL
622 realms_info = self._get_realms_info()
623 if 'default_info' in realms_info and realms_info['default_info']:
624 realm_info = self._get_realm_info(realms_info['default_info'])
625 if 'name' in realm_info and realm_info['name']:
626 return realm_info['name']
aee94f69 627 return None
a4b75251 628
f78120f9
TL
629 def get_default_zonegroup(self):
630 return self.daemon.zonegroup_name
631
9f95a23c
TL
632 @RestClient.api_get('/{bucket_name}?versioning')
633 def get_bucket_versioning(self, bucket_name, request=None):
634 """
635 Get bucket versioning.
636 :param str bucket_name: the name of the bucket.
637 :return: versioning info
638 :rtype: Dict
639 """
640 # pylint: disable=unused-argument
641 result = request()
642 if 'Status' not in result:
643 result['Status'] = 'Suspended'
644 if 'MfaDelete' not in result:
645 result['MfaDelete'] = 'Disabled'
646 return result
647
648 @RestClient.api_put('/{bucket_name}?versioning')
649 def set_bucket_versioning(self, bucket_name, versioning_state, mfa_delete,
650 mfa_token_serial, mfa_token_pin, request=None):
651 """
652 Set bucket versioning.
653 :param str bucket_name: the name of the bucket.
654 :param str versioning_state:
655 https://docs.aws.amazon.com/AmazonS3/latest/API/RESTBucketPUTVersioningStatus.html
656 :param str mfa_delete: MFA Delete state.
657 :param str mfa_token_serial:
658 https://docs.ceph.com/docs/master/radosgw/mfa/
659 :param str mfa_token_pin: value of a TOTP token at a certain time (auth code)
660 :return: None
661 """
662 # pylint: disable=unused-argument
663 versioning_configuration = ET.Element('VersioningConfiguration')
664 status_element = ET.SubElement(versioning_configuration, 'Status')
665 status_element.text = versioning_state
666
667 headers = {}
668 if mfa_delete and mfa_token_serial and mfa_token_pin:
669 headers['x-amz-mfa'] = '{} {}'.format(mfa_token_serial, mfa_token_pin)
670 mfa_delete_element = ET.SubElement(versioning_configuration, 'MfaDelete')
671 mfa_delete_element.text = mfa_delete
672
673 data = ET.tostring(versioning_configuration, encoding='unicode')
674
675 try:
676 request(data=data, headers=headers)
677 except RequestException as error:
678 msg = str(error)
f67539c2
TL
679 if mfa_delete and mfa_token_serial and mfa_token_pin \
680 and 'AccessDenied' in error.content.decode():
9f95a23c 681 msg = 'Bad MFA credentials: {}'.format(msg)
9f95a23c 682 raise DashboardException(msg=msg,
f67539c2 683 http_status_code=error.status_code,
9f95a23c
TL
684 component='rgw')
685
f38dd50b
TL
686 @RestClient.api_get('/{bucket_name}?acl')
687 def get_acl(self, bucket_name, request=None):
688 # pylint: disable=unused-argument
689 try:
690 result = request(raw_content=True) # type: ignore
691 return result.decode("utf-8")
692 except RequestException as error:
693 msg = 'Error getting ACLs'
694 if error.status_code == 404:
695 msg = '{}: {}'.format(msg, str(error))
696 raise DashboardException(msg=msg,
697 http_status_code=error.status_code,
698 component='rgw')
699
700 @RestClient.api_put('/{bucket_name}?acl')
701 def set_acl(self, bucket_name, acl, request=None):
702 # pylint: disable=unused-argument
703 headers = {'x-amz-acl': acl}
704 try:
705 result = request(headers=headers) # type: ignore
706 except RequestException as e:
707 raise DashboardException(msg=str(e), component='rgw')
708 return result
709
39ae355f
TL
710 @RestClient.api_get('/{bucket_name}?encryption')
711 def get_bucket_encryption(self, bucket_name, request=None):
712 # pylint: disable=unused-argument
713 try:
714 result = request() # type: ignore
715 result['Status'] = 'Enabled'
716 return result
717 except RequestException as e:
718 if e.content:
719 content = json_str_to_object(e.content)
720 if content.get(
721 'Code') == 'ServerSideEncryptionConfigurationNotFoundError':
722 return {
723 'Status': 'Disabled',
724 }
725 raise e
726
727 @RestClient.api_delete('/{bucket_name}?encryption')
728 def delete_bucket_encryption(self, bucket_name, request=None):
729 # pylint: disable=unused-argument
730 result = request() # type: ignore
731 return result
732
733 @RestClient.api_put('/{bucket_name}?encryption')
734 def set_bucket_encryption(self, bucket_name, key_id,
735 sse_algorithm, request: Optional[object] = None):
736 # pylint: disable=unused-argument
737 encryption_configuration = ET.Element('ServerSideEncryptionConfiguration')
738 rule_element = ET.SubElement(encryption_configuration, 'Rule')
739 default_encryption_element = ET.SubElement(rule_element,
740 'ApplyServerSideEncryptionByDefault')
741 sse_algo_element = ET.SubElement(default_encryption_element,
742 'SSEAlgorithm')
743 sse_algo_element.text = sse_algorithm
744 if sse_algorithm == 'aws:kms':
745 kms_master_key_element = ET.SubElement(default_encryption_element,
746 'KMSMasterKeyID')
747 kms_master_key_element.text = key_id
748 data = ET.tostring(encryption_configuration, encoding='unicode')
749 try:
750 _ = request(data=data) # type: ignore
751 except RequestException as e:
752 raise DashboardException(msg=str(e), component='rgw')
753
f38dd50b
TL
754 @RestClient.api_put('/{bucket_name}?tagging')
755 def set_tags(self, bucket_name, tags, request=None):
756 # pylint: disable=unused-argument
757 try:
758 ET.fromstring(tags)
759 except ET.ParseError:
760 return "Data must be properly formatted"
761 try:
762 result = request(data=tags) # type: ignore
763 except RequestException as e:
764 raise DashboardException(msg=str(e), component='rgw')
765 return result
766
f78120f9
TL
767 @RestClient.api_get('/{bucket_name}?lifecycle')
768 def get_lifecycle(self, bucket_name, request=None):
769 # pylint: disable=unused-argument
770 try:
771 result = request() # type: ignore
772 result = {'LifecycleConfiguration': result}
773 except RequestException as e:
774 if e.content:
775 content = json_str_to_object(e.content)
776 if content.get(
777 'Code') == 'NoSuchLifecycleConfiguration':
778 return None
779 raise DashboardException(msg=str(e), component='rgw')
780 return result
781
782 @staticmethod
783 def dict_to_xml(data):
784 if not data or data == '{}':
785 return ''
786 if isinstance(data, str):
787 try:
788 data = json.loads(data)
789 except json.JSONDecodeError:
790 raise DashboardException('Could not load json string')
791
792 def transform(data):
793 xml: str = ''
794 if isinstance(data, dict):
795 for key, value in data.items():
796 if isinstance(value, list):
797 for item in value:
798 if key == 'Rules':
799 key = 'Rule'
800 xml += f'<{key}>\n{transform(item)}</{key}>\n'
801 elif isinstance(value, dict):
802 xml += f'<{key}>\n{transform(value)}</{key}>\n'
803 else:
804 xml += f'<{key}>{str(value)}</{key}>\n'
805
806 elif isinstance(data, list):
807 for item in data:
808 xml += transform(item)
809 else:
810 xml += f'{data}'
811
812 return xml
813
814 return transform(data)
815
816 @RestClient.api_put('/{bucket_name}?lifecycle')
817 def set_lifecycle(self, bucket_name, lifecycle, request=None):
818 # pylint: disable=unused-argument
819 lifecycle = lifecycle.strip()
820 if lifecycle.startswith('{'):
821 lifecycle = RgwClient.dict_to_xml(lifecycle)
822 try:
823 if lifecycle and '<LifecycleConfiguration>' not in str(lifecycle):
824 lifecycle = f'<LifecycleConfiguration>{lifecycle}</LifecycleConfiguration>'
825 result = request(data=lifecycle) # type: ignore
826 except RequestException as e:
827 if e.content:
828 content = json_str_to_object(e.content)
829 if content.get("Code") == "MalformedXML":
830 msg = "Invalid Lifecycle document"
831 raise DashboardException(msg=msg, component='rgw')
832 raise DashboardException(msg=str(e), component='rgw')
833 return result
834
835 @RestClient.api_delete('/{bucket_name}?lifecycle')
836 def delete_lifecycle(self, bucket_name, request=None):
837 # pylint: disable=unused-argument
838 try:
839 result = request()
840 except RequestException as e:
841 raise DashboardException(msg=str(e), component='rgw')
842 return result
843
9f95a23c
TL
844 @RestClient.api_get('/{bucket_name}?object-lock')
845 def get_bucket_locking(self, bucket_name, request=None):
846 # type: (str, Optional[object]) -> dict
847 """
848 Gets the locking configuration for a bucket. The locking
849 configuration will be applied by default to every new object
850 placed in the specified bucket.
851 :param bucket_name: The name of the bucket.
852 :type bucket_name: str
853 :return: The locking configuration.
854 :rtype: Dict
855 """
856 # pylint: disable=unused-argument
857
858 # Try to get the Object Lock configuration. If there is none,
859 # then return default values.
860 try:
861 result = request() # type: ignore
862 return {
863 'lock_enabled': dict_get(result, 'ObjectLockEnabled') == 'Enabled',
864 'lock_mode': dict_get(result, 'Rule.DefaultRetention.Mode'),
865 'lock_retention_period_days': dict_get(result, 'Rule.DefaultRetention.Days', 0),
866 'lock_retention_period_years': dict_get(result, 'Rule.DefaultRetention.Years', 0)
867 }
868 except RequestException as e:
869 if e.content:
870 content = json_str_to_object(e.content)
871 if content.get(
872 'Code') == 'ObjectLockConfigurationNotFoundError':
873 return {
874 'lock_enabled': False,
875 'lock_mode': 'compliance',
876 'lock_retention_period_days': None,
877 'lock_retention_period_years': None
878 }
879 raise e
880
881 @RestClient.api_put('/{bucket_name}?object-lock')
882 def set_bucket_locking(self,
b3b6e05e
TL
883 bucket_name: str,
884 mode: str,
885 retention_period_days: Optional[Union[int, str]] = None,
886 retention_period_years: Optional[Union[int, str]] = None,
887 request: Optional[object] = None) -> None:
9f95a23c
TL
888 """
889 Places the locking configuration on the specified bucket. The
890 locking configuration will be applied by default to every new
891 object placed in the specified bucket.
892 :param bucket_name: The name of the bucket.
893 :type bucket_name: str
894 :param mode: The lock mode, e.g. `COMPLIANCE` or `GOVERNANCE`.
895 :type mode: str
896 :param retention_period_days:
897 :type retention_period_days: int
898 :param retention_period_years:
899 :type retention_period_years: int
900 :rtype: None
901 """
902 # pylint: disable=unused-argument
903
1e59de90
TL
904 retention_period_days, retention_period_years = self.perform_validations(
905 retention_period_days, retention_period_years, mode)
9f95a23c
TL
906
907 # Generate the XML data like this:
908 # <ObjectLockConfiguration>
909 # <ObjectLockEnabled>string</ObjectLockEnabled>
910 # <Rule>
911 # <DefaultRetention>
912 # <Days>integer</Days>
913 # <Mode>string</Mode>
914 # <Years>integer</Years>
915 # </DefaultRetention>
916 # </Rule>
917 # </ObjectLockConfiguration>
918 locking_configuration = ET.Element('ObjectLockConfiguration')
919 enabled_element = ET.SubElement(locking_configuration,
920 'ObjectLockEnabled')
921 enabled_element.text = 'Enabled' # Locking can't be disabled.
922 rule_element = ET.SubElement(locking_configuration, 'Rule')
923 default_retention_element = ET.SubElement(rule_element,
924 'DefaultRetention')
925 mode_element = ET.SubElement(default_retention_element, 'Mode')
926 mode_element.text = mode.upper()
927 if retention_period_days:
928 days_element = ET.SubElement(default_retention_element, 'Days')
929 days_element.text = str(retention_period_days)
930 if retention_period_years:
931 years_element = ET.SubElement(default_retention_element, 'Years')
932 years_element.text = str(retention_period_years)
933
934 data = ET.tostring(locking_configuration, encoding='unicode')
935
936 try:
937 _ = request(data=data) # type: ignore
938 except RequestException as e:
939 raise DashboardException(msg=str(e), component='rgw')
1e59de90
TL
940
941 def list_roles(self) -> List[Dict[str, Any]]:
942 rgw_list_roles_command = ['role', 'list']
943 code, roles, err = mgr.send_rgwadmin_command(rgw_list_roles_command)
944 if code < 0:
945 logger.warning('Error listing roles with code %d: %s', code, err)
946 return []
947
f38dd50b
TL
948 for role in roles:
949 if 'PermissionPolicies' not in role:
950 role['PermissionPolicies'] = []
1e59de90
TL
951 return roles
952
953 def create_role(self, role_name: str, role_path: str, role_assume_policy_doc: str) -> None:
954 try:
955 json.loads(role_assume_policy_doc)
956 except: # noqa: E722
957 raise DashboardException('Assume role policy document is not a valid json')
958
959 # valid values:
960 # pylint: disable=C0301
961 # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-role.html#cfn-iam-role-path # noqa: E501
962 if len(role_name) > 64:
963 raise DashboardException(
964 f'Role name "{role_name}" is invalid. Should be 64 characters or less')
965
966 role_name_regex = '[0-9a-zA-Z_+=,.@-]+'
967 if not re.fullmatch(role_name_regex, role_name):
968 raise DashboardException(
969 f'Role name "{role_name}" is invalid. Valid characters are "{role_name_regex}"')
970
971 if not os.path.isabs(role_path):
972 raise DashboardException(
973 f'Role path "{role_path}" is invalid. It should be an absolute path')
974 if role_path[-1] != '/':
975 raise DashboardException(
976 f'Role path "{role_path}" is invalid. It should start and end with a slash')
977 path_regex = '(\u002F)|(\u002F[\u0021-\u007E]+\u002F)'
978 if not re.fullmatch(path_regex, role_path):
979 raise DashboardException(
980 (f'Role path "{role_path}" is invalid.'
981 f'Role path should follow the pattern "{path_regex}"'))
982
983 rgw_create_role_command = ['role', 'create', '--role-name', role_name, '--path', role_path]
984 if role_assume_policy_doc:
985 rgw_create_role_command += ['--assume-role-policy-doc', f"{role_assume_policy_doc}"]
986
987 code, _roles, _err = mgr.send_rgwadmin_command(rgw_create_role_command,
988 stdout_as_json=False)
989 if code != 0:
990 # pylint: disable=C0301
991 link = 'https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-role.html#cfn-iam-role-path' # noqa: E501
992 msg = (f'Error creating role with code {code}: '
993 'Looks like the document has a wrong format.'
994 f' For more information about the format look at {link}')
995 raise DashboardException(msg=msg, component='rgw')
996
f38dd50b
TL
997 def get_role(self, role_name: str):
998 rgw_get_role_command = ['role', 'get', '--role-name', role_name]
999 code, role, _err = mgr.send_rgwadmin_command(rgw_get_role_command)
1000 if code != 0:
1001 raise DashboardException(msg=f'Error getting role with code {code}: {_err}',
1002 component='rgw')
1003 return role
1004
1005 def update_role(self, role_name: str, max_session_duration: str):
1006 rgw_update_role_command = ['role', 'update', '--role-name',
1007 role_name, '--max_session_duration', max_session_duration]
1008 code, _, _err = mgr.send_rgwadmin_command(rgw_update_role_command,
1009 stdout_as_json=False)
1010 if code != 0:
1011 raise DashboardException(msg=f'Error updating role with code {code}: {_err}',
1012 component='rgw')
1013
1014 def delete_role(self, role_name: str) -> None:
1015 rgw_delete_role_command = ['role', 'delete', '--role-name', role_name]
1016 code, _, _err = mgr.send_rgwadmin_command(rgw_delete_role_command,
1017 stdout_as_json=False)
1018 if code != 0:
1019 raise DashboardException(msg=f'Error deleting role with code {code}: {_err}',
1020 component='rgw')
1021
1022 @RestClient.api_get('/{bucket_name}?policy')
1023 def get_bucket_policy(self, bucket_name: str, request=None):
1024 """
1025 Gets the bucket policy for a bucket.
1026 :param bucket_name: The name of the bucket.
1027 :type bucket_name: str
1028 :rtype: None
1029 """
1030 # pylint: disable=unused-argument
1031
1032 try:
1033 request = request()
1034 return request
1035 except RequestException as e:
1036 if e.content:
1037 content = json_str_to_object(e.content)
1038 if content.get(
1039 'Code') == 'NoSuchBucketPolicy':
1040 return None
1041 raise e
1042
1043 @RestClient.api_put('/{bucket_name}?policy')
1044 def set_bucket_policy(self, bucket_name: str, policy: str, request=None):
1045 """
1046 Sets the bucket policy for a bucket.
1047 :param bucket_name: The name of the bucket.
1048 :type bucket_name: str
1049 :param policy: The bucket policy.
1050 :type policy: JSON Structured Document
1051 :return: The bucket policy.
1052 :rtype: Dict
1053 """
1054 # pylint: disable=unused-argument
1055 try:
1056 request = request(data=policy)
1057 except RequestException as e:
1058 if e.content:
1059 content = json_str_to_object(e.content)
1060 if content.get("Code") == "InvalidArgument":
1061 msg = "Invalid JSON document"
1062 raise DashboardException(msg=msg, component='rgw')
1063 raise DashboardException(e)
1064
1e59de90
TL
1065 def perform_validations(self, retention_period_days, retention_period_years, mode):
1066 try:
1067 retention_period_days = int(retention_period_days) if retention_period_days else 0
1068 retention_period_years = int(retention_period_years) if retention_period_years else 0
1069 if retention_period_days < 0 or retention_period_years < 0:
1070 raise ValueError
1071 except (TypeError, ValueError):
1072 msg = "Retention period must be a positive integer."
1073 raise DashboardException(msg=msg, component='rgw')
1074 if retention_period_days and retention_period_years:
1075 # https://docs.aws.amazon.com/AmazonS3/latest/API/archive-RESTBucketPUTObjectLockConfiguration.html
1076 msg = "Retention period requires either Days or Years. "\
1077 "You can't specify both at the same time."
1078 raise DashboardException(msg=msg, component='rgw')
1079 if not retention_period_days and not retention_period_years:
1080 msg = "Retention period requires either Days or Years. "\
1081 "You must specify at least one."
1082 raise DashboardException(msg=msg, component='rgw')
1083 if not isinstance(mode, str) or mode.upper() not in ['COMPLIANCE', 'GOVERNANCE']:
1084 msg = "Retention mode must be either COMPLIANCE or GOVERNANCE."
1085 raise DashboardException(msg=msg, component='rgw')
1086 return retention_period_days, retention_period_years
aee94f69 1087
f78120f9
TL
1088 @RestClient.api_put('/{bucket_name}?replication')
1089 def set_bucket_replication(self, bucket_name, replication: bool, request=None):
1090 # pGenerate the minimum replication configuration
1091 # required for enabling the replication
1092 root = ET.Element('ReplicationConfiguration',
1093 xmlns='http://s3.amazonaws.com/doc/2006-03-01/')
1094 role = ET.SubElement(root, 'Role')
1095 role.text = f'{bucket_name}_replication_role'
1096
1097 rule = ET.SubElement(root, 'Rule')
1098 rule_id = ET.SubElement(rule, 'ID')
1099 rule_id.text = _SYNC_PIPE_ID
1100
1101 status = ET.SubElement(rule, 'Status')
1102 status.text = 'Enabled' if replication else 'Disabled'
1103
1104 filter_elem = ET.SubElement(rule, 'Filter')
1105 prefix = ET.SubElement(filter_elem, 'Prefix')
1106 prefix.text = ''
1107
1108 destination = ET.SubElement(rule, 'Destination')
1109
1110 bucket = ET.SubElement(destination, 'Bucket')
1111 bucket.text = bucket_name
1112
1113 replication_config = ET.tostring(root, encoding='utf-8', method='xml').decode()
1114
1115 try:
1116 request = request(data=replication_config)
1117 except RequestException as e:
1118 raise DashboardException(msg=str(e), component='rgw')
1119
1120 @RestClient.api_get('/{bucket_name}?replication')
1121 def get_bucket_replication(self, bucket_name, request=None):
1122 # pylint: disable=unused-argument
1123 try:
1124 result = request()
1125 return result
1126 except RequestException as e:
1127 if e.content:
1128 content = json_str_to_object(e.content)
1129 if content.get('Code') == 'ReplicationConfigurationNotFoundError':
1130 return None
1131 raise e
1132
1133
1134class SyncStatus(Enum):
1135 enabled = 'enabled'
1136 allowed = 'allowed'
1137 forbidden = 'forbidden'
1138
1139
1140class SyncFlowTypes(Enum):
1141 directional = 'directional'
1142 symmetrical = 'symmetrical'
1143
aee94f69
TL
1144
1145class RgwMultisite:
1146 def migrate_to_multisite(self, realm_name: str, zonegroup_name: str, zone_name: str,
1147 zonegroup_endpoints: str, zone_endpoints: str, access_key: str,
1148 secret_key: str):
1149 rgw_realm_create_cmd = ['realm', 'create', '--rgw-realm', realm_name, '--default']
1150 try:
1151 exit_code, _, err = mgr.send_rgwadmin_command(rgw_realm_create_cmd, False)
1152 if exit_code > 0:
1153 raise DashboardException(e=err, msg='Unable to create realm',
1154 http_status_code=500, component='rgw')
1155 except SubprocessError as error:
1156 raise DashboardException(error, http_status_code=500, component='rgw')
1157
1158 rgw_zonegroup_edit_cmd = ['zonegroup', 'rename', '--rgw-zonegroup', 'default',
1159 '--zonegroup-new-name', zonegroup_name]
1160 try:
1161 exit_code, _, err = mgr.send_rgwadmin_command(rgw_zonegroup_edit_cmd, False)
1162 if exit_code > 0:
1163 raise DashboardException(e=err, msg='Unable to rename zonegroup to {}'.format(zonegroup_name), # noqa E501 #pylint: disable=line-too-long
1164 http_status_code=500, component='rgw')
1165 except SubprocessError as error:
1166 raise DashboardException(error, http_status_code=500, component='rgw')
1167
1168 rgw_zone_edit_cmd = ['zone', 'rename', '--rgw-zone',
1169 'default', '--zone-new-name', zone_name,
1170 '--rgw-zonegroup', zonegroup_name]
1171 try:
1172 exit_code, _, err = mgr.send_rgwadmin_command(rgw_zone_edit_cmd, False)
1173 if exit_code > 0:
1174 raise DashboardException(e=err, msg='Unable to rename zone to {}'.format(zone_name), # noqa E501 #pylint: disable=line-too-long
1175 http_status_code=500, component='rgw')
1176 except SubprocessError as error:
1177 raise DashboardException(error, http_status_code=500, component='rgw')
1178
1179 rgw_zonegroup_modify_cmd = ['zonegroup', 'modify',
1180 '--rgw-realm', realm_name,
1181 '--rgw-zonegroup', zonegroup_name]
1182 if zonegroup_endpoints:
1183 rgw_zonegroup_modify_cmd.append('--endpoints')
1184 rgw_zonegroup_modify_cmd.append(zonegroup_endpoints)
1185 rgw_zonegroup_modify_cmd.append('--master')
1186 rgw_zonegroup_modify_cmd.append('--default')
1187 try:
1188 exit_code, _, err = mgr.send_rgwadmin_command(rgw_zonegroup_modify_cmd)
1189 if exit_code > 0:
1190 raise DashboardException(e=err, msg='Unable to modify zonegroup {}'.format(zonegroup_name), # noqa E501 #pylint: disable=line-too-long
1191 http_status_code=500, component='rgw')
1192 except SubprocessError as error:
1193 raise DashboardException(error, http_status_code=500, component='rgw')
1194
1195 rgw_zone_modify_cmd = ['zone', 'modify', '--rgw-realm', realm_name,
1196 '--rgw-zonegroup', zonegroup_name,
1197 '--rgw-zone', zone_name]
1198 if zone_endpoints:
1199 rgw_zone_modify_cmd.append('--endpoints')
1200 rgw_zone_modify_cmd.append(zone_endpoints)
1201 rgw_zone_modify_cmd.append('--master')
1202 rgw_zone_modify_cmd.append('--default')
1203 try:
1204 exit_code, _, err = mgr.send_rgwadmin_command(rgw_zone_modify_cmd)
1205 if exit_code > 0:
1206 raise DashboardException(e=err, msg='Unable to modify zone',
1207 http_status_code=500, component='rgw')
1208 except SubprocessError as error:
1209 raise DashboardException(error, http_status_code=500, component='rgw')
1210
1211 if access_key and secret_key:
1212 rgw_zone_modify_cmd = ['zone', 'modify', '--rgw-zone', zone_name,
1213 '--access-key', access_key, '--secret', secret_key]
1214 try:
1215 exit_code, _, err = mgr.send_rgwadmin_command(rgw_zone_modify_cmd)
1216 if exit_code > 0:
1217 raise DashboardException(e=err, msg='Unable to modify zone',
1218 http_status_code=500, component='rgw')
1219 except SubprocessError as error:
1220 raise DashboardException(error, http_status_code=500, component='rgw')
1221
1222 def create_realm(self, realm_name: str, default: bool):
1223 rgw_realm_create_cmd = ['realm', 'create']
1224 cmd_create_realm_options = ['--rgw-realm', realm_name]
f38dd50b 1225 if default:
aee94f69
TL
1226 cmd_create_realm_options.append('--default')
1227 rgw_realm_create_cmd += cmd_create_realm_options
1228 try:
1229 exit_code, _, _ = mgr.send_rgwadmin_command(rgw_realm_create_cmd)
1230 if exit_code > 0:
1231 raise DashboardException(msg='Unable to create realm',
1232 http_status_code=500, component='rgw')
1233 except SubprocessError as error:
1234 raise DashboardException(error, http_status_code=500, component='rgw')
1235
1236 def list_realms(self):
1237 rgw_realm_list = {}
1238 rgw_realm_list_cmd = ['realm', 'list']
1239 try:
1240 exit_code, out, _ = mgr.send_rgwadmin_command(rgw_realm_list_cmd)
1241 if exit_code > 0:
1242 raise DashboardException(msg='Unable to fetch realm list',
1243 http_status_code=500, component='rgw')
1244 rgw_realm_list = out
1245 except SubprocessError as error:
1246 raise DashboardException(error, http_status_code=500, component='rgw')
1247 return rgw_realm_list
1248
1249 def get_realm(self, realm_name: str):
1250 realm_info = {}
1251 rgw_realm_info_cmd = ['realm', 'get', '--rgw-realm', realm_name]
1252 try:
1253 exit_code, out, _ = mgr.send_rgwadmin_command(rgw_realm_info_cmd)
1254 if exit_code > 0:
1255 raise DashboardException('Unable to get realm info',
1256 http_status_code=500, component='rgw')
1257 realm_info = out
1258 except SubprocessError as error:
1259 raise DashboardException(error, http_status_code=500, component='rgw')
1260 return realm_info
1261
1262 def get_all_realms_info(self):
1263 all_realms_info = {}
1264 realms_info = []
1265 rgw_realm_list = self.list_realms()
1266 if 'realms' in rgw_realm_list:
1267 if rgw_realm_list['realms'] != []:
1268 for rgw_realm in rgw_realm_list['realms']:
1269 realm_info = self.get_realm(rgw_realm)
1270 realms_info.append(realm_info)
1271 all_realms_info['realms'] = realms_info # type: ignore
1272 else:
1273 all_realms_info['realms'] = [] # type: ignore
1274 if 'default_info' in rgw_realm_list and rgw_realm_list['default_info'] != '':
1275 all_realms_info['default_realm'] = rgw_realm_list['default_info'] # type: ignore
1276 else:
1277 all_realms_info['default_realm'] = '' # type: ignore
1278 return all_realms_info
1279
1280 def edit_realm(self, realm_name: str, new_realm_name: str, default: str = ''):
1281 rgw_realm_edit_cmd = []
1282 if new_realm_name != realm_name:
1283 rgw_realm_edit_cmd = ['realm', 'rename', '--rgw-realm',
1284 realm_name, '--realm-new-name', new_realm_name]
1285 try:
1286 exit_code, _, err = mgr.send_rgwadmin_command(rgw_realm_edit_cmd, False)
1287 if exit_code > 0:
1288 raise DashboardException(e=err, msg='Unable to edit realm',
1289 http_status_code=500, component='rgw')
1290 except SubprocessError as error:
1291 raise DashboardException(error, http_status_code=500, component='rgw')
1292 if default and str_to_bool(default):
1293 rgw_realm_edit_cmd = ['realm', 'default', '--rgw-realm', new_realm_name]
1294 try:
1295 exit_code, _, _ = mgr.send_rgwadmin_command(rgw_realm_edit_cmd, False)
1296 if exit_code > 0:
1297 raise DashboardException(msg='Unable to set {} as default realm'.format(new_realm_name), # noqa E501 #pylint: disable=line-too-long
1298 http_status_code=500, component='rgw')
1299 except SubprocessError as error:
1300 raise DashboardException(error, http_status_code=500, component='rgw')
1301
1302 def delete_realm(self, realm_name: str):
1303 rgw_delete_realm_cmd = ['realm', 'rm', '--rgw-realm', realm_name]
1304 try:
1305 exit_code, _, _ = mgr.send_rgwadmin_command(rgw_delete_realm_cmd)
1306 if exit_code > 0:
1307 raise DashboardException(msg='Unable to delete realm',
1308 http_status_code=500, component='rgw')
1309 except SubprocessError as error:
1310 raise DashboardException(error, http_status_code=500, component='rgw')
1311
1312 def create_zonegroup(self, realm_name: str, zonegroup_name: str,
1313 default: bool, master: bool, endpoints: str):
1314 rgw_zonegroup_create_cmd = ['zonegroup', 'create']
1315 cmd_create_zonegroup_options = ['--rgw-zonegroup', zonegroup_name]
1316 if realm_name != 'null':
1317 cmd_create_zonegroup_options.append('--rgw-realm')
1318 cmd_create_zonegroup_options.append(realm_name)
1319 if default != 'false':
1320 cmd_create_zonegroup_options.append('--default')
1321 if master != 'false':
1322 cmd_create_zonegroup_options.append('--master')
1323 if endpoints:
1324 cmd_create_zonegroup_options.append('--endpoints')
1325 cmd_create_zonegroup_options.append(endpoints)
1326 rgw_zonegroup_create_cmd += cmd_create_zonegroup_options
1327 try:
1328 exit_code, out, err = mgr.send_rgwadmin_command(rgw_zonegroup_create_cmd)
1329 if exit_code > 0:
1330 raise DashboardException(e=err, msg='Unable to get realm info',
1331 http_status_code=500, component='rgw')
1332 except SubprocessError as error:
1333 raise DashboardException(error, http_status_code=500, component='rgw')
1334 return out
1335
1336 def list_zonegroups(self):
1337 rgw_zonegroup_list = {}
1338 rgw_zonegroup_list_cmd = ['zonegroup', 'list']
1339 try:
1340 exit_code, out, _ = mgr.send_rgwadmin_command(rgw_zonegroup_list_cmd)
1341 if exit_code > 0:
1342 raise DashboardException(msg='Unable to fetch zonegroup list',
1343 http_status_code=500, component='rgw')
1344 rgw_zonegroup_list = out
1345 except SubprocessError as error:
1346 raise DashboardException(error, http_status_code=500, component='rgw')
1347 return rgw_zonegroup_list
1348
1349 def get_zonegroup(self, zonegroup_name: str):
1350 zonegroup_info = {}
1351 if zonegroup_name != 'default':
1352 rgw_zonegroup_info_cmd = ['zonegroup', 'get', '--rgw-zonegroup', zonegroup_name]
1353 else:
1354 rgw_zonegroup_info_cmd = ['zonegroup', 'get', '--rgw-zonegroup',
1355 zonegroup_name, '--rgw-realm', 'default']
1356 try:
1357 exit_code, out, _ = mgr.send_rgwadmin_command(rgw_zonegroup_info_cmd)
1358 if exit_code > 0:
1359 raise DashboardException('Unable to get zonegroup info',
1360 http_status_code=500, component='rgw')
1361 zonegroup_info = out
1362 except SubprocessError as error:
1363 raise DashboardException(error, http_status_code=500, component='rgw')
1364 return zonegroup_info
1365
1366 def get_all_zonegroups_info(self):
1367 all_zonegroups_info = {}
1368 zonegroups_info = []
1369 rgw_zonegroup_list = self.list_zonegroups()
1370 if 'zonegroups' in rgw_zonegroup_list:
1371 if rgw_zonegroup_list['zonegroups'] != []:
1372 for rgw_zonegroup in rgw_zonegroup_list['zonegroups']:
1373 zonegroup_info = self.get_zonegroup(rgw_zonegroup)
1374 zonegroups_info.append(zonegroup_info)
1375 all_zonegroups_info['zonegroups'] = zonegroups_info # type: ignore
1376 else:
1377 all_zonegroups_info['zonegroups'] = [] # type: ignore
1378 if 'default_info' in rgw_zonegroup_list and rgw_zonegroup_list['default_info'] != '':
1379 all_zonegroups_info['default_zonegroup'] = rgw_zonegroup_list['default_info']
1380 else:
1381 all_zonegroups_info['default_zonegroup'] = '' # type: ignore
1382 return all_zonegroups_info
1383
1384 def delete_zonegroup(self, zonegroup_name: str, delete_pools: str, pools: List[str]):
1385 if delete_pools == 'true':
1386 zonegroup_info = self.get_zonegroup(zonegroup_name)
1387 rgw_delete_zonegroup_cmd = ['zonegroup', 'delete', '--rgw-zonegroup', zonegroup_name]
1388 try:
1389 exit_code, _, _ = mgr.send_rgwadmin_command(rgw_delete_zonegroup_cmd)
1390 if exit_code > 0:
1391 raise DashboardException(msg='Unable to delete zonegroup',
1392 http_status_code=500, component='rgw')
1393 except SubprocessError as error:
1394 raise DashboardException(error, http_status_code=500, component='rgw')
1395 self.update_period()
1396 if delete_pools == 'true':
1397 for zone in zonegroup_info['zones']:
1398 self.delete_zone(zone['name'], 'true', pools)
1399
1400 def modify_zonegroup(self, realm_name: str, zonegroup_name: str, default: str, master: str,
1401 endpoints: str):
1402
1403 rgw_zonegroup_modify_cmd = ['zonegroup', 'modify',
1404 '--rgw-realm', realm_name,
1405 '--rgw-zonegroup', zonegroup_name]
1406 if endpoints:
1407 rgw_zonegroup_modify_cmd.append('--endpoints')
1408 rgw_zonegroup_modify_cmd.append(endpoints)
1409 if master and str_to_bool(master):
1410 rgw_zonegroup_modify_cmd.append('--master')
1411 if default and str_to_bool(default):
1412 rgw_zonegroup_modify_cmd.append('--default')
1413 try:
1414 exit_code, _, err = mgr.send_rgwadmin_command(rgw_zonegroup_modify_cmd)
1415 if exit_code > 0:
1416 raise DashboardException(e=err, msg='Unable to modify zonegroup {}'.format(zonegroup_name), # noqa E501 #pylint: disable=line-too-long
1417 http_status_code=500, component='rgw')
1418 except SubprocessError as error:
1419 raise DashboardException(error, http_status_code=500, component='rgw')
1420 self.update_period()
1421
1422 def add_or_remove_zone(self, zonegroup_name: str, zone_name: str, action: str):
1423 if action == 'add':
1424 rgw_zonegroup_add_zone_cmd = ['zonegroup', 'add', '--rgw-zonegroup',
1425 zonegroup_name, '--rgw-zone', zone_name]
1426 try:
1427 exit_code, _, err = mgr.send_rgwadmin_command(rgw_zonegroup_add_zone_cmd)
1428 if exit_code > 0:
1429 raise DashboardException(e=err, msg='Unable to add zone {} to zonegroup {}'.format(zone_name, zonegroup_name), # noqa E501 #pylint: disable=line-too-long
1430 http_status_code=500, component='rgw')
1431 except SubprocessError as error:
1432 raise DashboardException(error, http_status_code=500, component='rgw')
1433 self.update_period()
1434 if action == 'remove':
1435 rgw_zonegroup_rm_zone_cmd = ['zonegroup', 'remove',
1436 '--rgw-zonegroup', zonegroup_name, '--rgw-zone', zone_name]
1437 try:
1438 exit_code, _, err = mgr.send_rgwadmin_command(rgw_zonegroup_rm_zone_cmd)
1439 if exit_code > 0:
1440 raise DashboardException(e=err, msg='Unable to remove zone {} from zonegroup {}'.format(zone_name, zonegroup_name), # noqa E501 #pylint: disable=line-too-long
1441 http_status_code=500, component='rgw')
1442 except SubprocessError as error:
1443 raise DashboardException(error, http_status_code=500, component='rgw')
1444 self.update_period()
1445
1446 def get_placement_targets_by_zonegroup(self, zonegroup_name: str):
1447 rgw_get_placement_cmd = ['zonegroup', 'placement',
1448 'list', '--rgw-zonegroup', zonegroup_name]
1449 try:
1450 exit_code, out, err = mgr.send_rgwadmin_command(rgw_get_placement_cmd)
1451 if exit_code > 0:
1452 raise DashboardException(e=err, msg='Unable to get placement targets',
1453 http_status_code=500, component='rgw')
1454 except SubprocessError as error:
1455 raise DashboardException(error, http_status_code=500, component='rgw')
1456 return out
1457
1458 def add_placement_targets(self, zonegroup_name: str, placement_targets: List[Dict]):
1459 rgw_add_placement_cmd = ['zonegroup', 'placement', 'add']
1460 for placement_target in placement_targets:
1461 cmd_add_placement_options = ['--rgw-zonegroup', zonegroup_name,
1462 '--placement-id', placement_target['placement_id']]
1463 if placement_target['tags']:
1464 cmd_add_placement_options += ['--tags', placement_target['tags']]
1465 rgw_add_placement_cmd += cmd_add_placement_options
1466 try:
1467 exit_code, _, err = mgr.send_rgwadmin_command(rgw_add_placement_cmd)
1468 if exit_code > 0:
1469 raise DashboardException(e=err,
1470 msg='Unable to add placement target {} to zonegroup {}'.format(placement_target['placement_id'], zonegroup_name), # noqa E501 #pylint: disable=line-too-long
1471 http_status_code=500, component='rgw')
1472 except SubprocessError as error:
1473 raise DashboardException(error, http_status_code=500, component='rgw')
1474 self.update_period()
1475 storage_classes = placement_target['storage_class'].split(",") if placement_target['storage_class'] else [] # noqa E501 #pylint: disable=line-too-long
1476 if storage_classes:
1477 for sc in storage_classes:
1478 cmd_add_placement_options = ['--storage-class', sc]
1479 try:
1480 exit_code, _, err = mgr.send_rgwadmin_command(
1481 rgw_add_placement_cmd + cmd_add_placement_options)
1482 if exit_code > 0:
1483 raise DashboardException(e=err,
1484 msg='Unable to add placement target {} to zonegroup {}'.format(placement_target['placement_id'], zonegroup_name), # noqa E501 #pylint: disable=line-too-long
1485 http_status_code=500, component='rgw')
1486 except SubprocessError as error:
1487 raise DashboardException(error, http_status_code=500, component='rgw')
1488 self.update_period()
1489
1490 def modify_placement_targets(self, zonegroup_name: str, placement_targets: List[Dict]):
1491 rgw_add_placement_cmd = ['zonegroup', 'placement', 'modify']
1492 for placement_target in placement_targets:
1493 cmd_add_placement_options = ['--rgw-zonegroup', zonegroup_name,
1494 '--placement-id', placement_target['placement_id']]
1495 if placement_target['tags']:
1496 cmd_add_placement_options += ['--tags', placement_target['tags']]
1497 rgw_add_placement_cmd += cmd_add_placement_options
1498 storage_classes = placement_target['storage_class'].split(",") if placement_target['storage_class'] else [] # noqa E501 #pylint: disable=line-too-long
1499 if storage_classes:
1500 for sc in storage_classes:
1501 cmd_add_placement_options = []
1502 cmd_add_placement_options = ['--storage-class', sc]
1503 try:
1504 exit_code, _, err = mgr.send_rgwadmin_command(
1505 rgw_add_placement_cmd + cmd_add_placement_options)
1506 if exit_code > 0:
1507 raise DashboardException(e=err,
1508 msg='Unable to add placement target {} to zonegroup {}'.format(placement_target['placement_id'], zonegroup_name), # noqa E501 #pylint: disable=line-too-long
1509 http_status_code=500, component='rgw')
1510 except SubprocessError as error:
1511 raise DashboardException(error, http_status_code=500, component='rgw')
1512 self.update_period()
1513 else:
1514 try:
1515 exit_code, _, err = mgr.send_rgwadmin_command(rgw_add_placement_cmd)
1516 if exit_code > 0:
1517 raise DashboardException(e=err,
1518 msg='Unable to add placement target {} to zonegroup {}'.format(placement_target['placement_id'], zonegroup_name), # noqa E501 #pylint: disable=line-too-long
1519 http_status_code=500, component='rgw')
1520 except SubprocessError as error:
1521 raise DashboardException(error, http_status_code=500, component='rgw')
1522 self.update_period()
1523
1524 # pylint: disable=W0102
1525 def edit_zonegroup(self, realm_name: str, zonegroup_name: str, new_zonegroup_name: str,
1526 default: str = '', master: str = '', endpoints: str = '',
1527 add_zones: List[str] = [], remove_zones: List[str] = [],
1528 placement_targets: List[Dict[str, str]] = []):
1529 rgw_zonegroup_edit_cmd = []
1530 if new_zonegroup_name != zonegroup_name:
1531 rgw_zonegroup_edit_cmd = ['zonegroup', 'rename', '--rgw-zonegroup', zonegroup_name,
1532 '--zonegroup-new-name', new_zonegroup_name]
1533 try:
1534 exit_code, _, err = mgr.send_rgwadmin_command(rgw_zonegroup_edit_cmd, False)
1535 if exit_code > 0:
1536 raise DashboardException(e=err, msg='Unable to rename zonegroup to {}'.format(new_zonegroup_name), # noqa E501 #pylint: disable=line-too-long
1537 http_status_code=500, component='rgw')
1538 except SubprocessError as error:
1539 raise DashboardException(error, http_status_code=500, component='rgw')
1540 self.update_period()
1541 self.modify_zonegroup(realm_name, new_zonegroup_name, default, master, endpoints)
1542 if add_zones:
1543 for zone_name in add_zones:
1544 self.add_or_remove_zone(new_zonegroup_name, zone_name, 'add')
1545 if remove_zones:
1546 for zone_name in remove_zones:
1547 self.add_or_remove_zone(new_zonegroup_name, zone_name, 'remove')
1548 existing_placement_targets = self.get_placement_targets_by_zonegroup(new_zonegroup_name)
1549 existing_placement_targets_ids = [pt['key'] for pt in existing_placement_targets]
1550 if placement_targets:
1551 for pt in placement_targets:
1552 if pt['placement_id'] in existing_placement_targets_ids:
1553 self.modify_placement_targets(new_zonegroup_name, placement_targets)
1554 else:
1555 self.add_placement_targets(new_zonegroup_name, placement_targets)
1556
1557 def update_period(self):
1558 rgw_update_period_cmd = ['period', 'update', '--commit']
1559 try:
1560 exit_code, _, err = mgr.send_rgwadmin_command(rgw_update_period_cmd)
1561 if exit_code > 0:
1562 raise DashboardException(e=err, msg='Unable to update period',
1563 http_status_code=500, component='rgw')
1564 except SubprocessError as error:
1565 raise DashboardException(error, http_status_code=500, component='rgw')
1566
1567 def create_zone(self, zone_name, zonegroup_name, default, master, endpoints, access_key,
1568 secret_key):
1569 rgw_zone_create_cmd = ['zone', 'create']
1570 cmd_create_zone_options = ['--rgw-zone', zone_name]
1571 if zonegroup_name != 'null':
1572 cmd_create_zone_options.append('--rgw-zonegroup')
1573 cmd_create_zone_options.append(zonegroup_name)
1574 if default != 'false':
1575 cmd_create_zone_options.append('--default')
1576 if master != 'false':
1577 cmd_create_zone_options.append('--master')
1578 if endpoints != 'null':
1579 cmd_create_zone_options.append('--endpoints')
1580 cmd_create_zone_options.append(endpoints)
1581 if access_key is not None:
1582 cmd_create_zone_options.append('--access-key')
1583 cmd_create_zone_options.append(access_key)
1584 if secret_key is not None:
1585 cmd_create_zone_options.append('--secret')
1586 cmd_create_zone_options.append(secret_key)
1587 rgw_zone_create_cmd += cmd_create_zone_options
1588 try:
1589 exit_code, out, err = mgr.send_rgwadmin_command(rgw_zone_create_cmd)
1590 if exit_code > 0:
1591 raise DashboardException(e=err, msg='Unable to create zone',
1592 http_status_code=500, component='rgw')
1593 except SubprocessError as error:
1594 raise DashboardException(error, http_status_code=500, component='rgw')
1595
1596 self.update_period()
1597 return out
1598
1599 def parse_secrets(self, user, data):
1600 for key in data.get('keys', []):
1601 if key.get('user') == user:
1602 access_key = key.get('access_key')
1603 secret_key = key.get('secret_key')
1604 return access_key, secret_key
1605 return '', ''
1606
1607 def modify_zone(self, zone_name: str, zonegroup_name: str, default: str, master: str,
1608 endpoints: str, access_key: str, secret_key: str):
1609 rgw_zone_modify_cmd = ['zone', 'modify', '--rgw-zonegroup',
1610 zonegroup_name, '--rgw-zone', zone_name]
1611 if endpoints:
1612 rgw_zone_modify_cmd.append('--endpoints')
1613 rgw_zone_modify_cmd.append(endpoints)
1614 if default and str_to_bool(default):
1615 rgw_zone_modify_cmd.append('--default')
1616 if master and str_to_bool(master):
1617 rgw_zone_modify_cmd.append('--master')
1618 if access_key is not None:
1619 rgw_zone_modify_cmd.append('--access-key')
1620 rgw_zone_modify_cmd.append(access_key)
1621 if secret_key is not None:
1622 rgw_zone_modify_cmd.append('--secret')
1623 rgw_zone_modify_cmd.append(secret_key)
1624 try:
1625 exit_code, _, err = mgr.send_rgwadmin_command(rgw_zone_modify_cmd)
1626 if exit_code > 0:
1627 raise DashboardException(e=err, msg='Unable to modify zone',
1628 http_status_code=500, component='rgw')
1629 except SubprocessError as error:
1630 raise DashboardException(error, http_status_code=500, component='rgw')
1631 self.update_period()
1632
1633 def add_placement_targets_zone(self, zone_name: str, placement_target: str, data_pool: str,
1634 index_pool: str, data_extra_pool: str):
1635 rgw_zone_add_placement_cmd = ['zone', 'placement', 'add', '--rgw-zone', zone_name,
1636 '--placement-id', placement_target, '--data-pool', data_pool,
1637 '--index-pool', index_pool,
1638 '--data-extra-pool', data_extra_pool]
1639 try:
1640 exit_code, _, err = mgr.send_rgwadmin_command(rgw_zone_add_placement_cmd)
1641 if exit_code > 0:
1642 raise DashboardException(e=err, msg='Unable to add placement target {} to zone {}'.format(placement_target, zone_name), # noqa E501 #pylint: disable=line-too-long
1643 http_status_code=500, component='rgw')
1644 except SubprocessError as error:
1645 raise DashboardException(error, http_status_code=500, component='rgw')
1646 self.update_period()
1647
1648 def add_storage_class_zone(self, zone_name: str, placement_target: str, storage_class: str,
1649 data_pool: str, compression: str):
1650 rgw_zone_add_storage_class_cmd = ['zone', 'placement', 'add', '--rgw-zone', zone_name,
1651 '--placement-id', placement_target,
1652 '--storage-class', storage_class,
1653 '--data-pool', data_pool,
1654 '--compression', compression]
1655 try:
1656 exit_code, _, err = mgr.send_rgwadmin_command(rgw_zone_add_storage_class_cmd)
1657 if exit_code > 0:
1658 raise DashboardException(e=err, msg='Unable to add storage class {} to zone {}'.format(storage_class, zone_name), # noqa E501 #pylint: disable=line-too-long
1659 http_status_code=500, component='rgw')
1660 except SubprocessError as error:
1661 raise DashboardException(error, http_status_code=500, component='rgw')
1662 self.update_period()
1663
1664 def edit_zone(self, zone_name: str, new_zone_name: str, zonegroup_name: str, default: str = '',
1665 master: str = '', endpoints: str = '', access_key: str = '', secret_key: str = '',
1666 placement_target: str = '', data_pool: str = '', index_pool: str = '',
1667 data_extra_pool: str = '', storage_class: str = '', data_pool_class: str = '',
1668 compression: str = ''):
1669 if new_zone_name != zone_name:
1670 rgw_zone_rename_cmd = ['zone', 'rename', '--rgw-zone',
1671 zone_name, '--zone-new-name', new_zone_name]
1672 try:
1673 exit_code, _, err = mgr.send_rgwadmin_command(rgw_zone_rename_cmd, False)
1674 if exit_code > 0:
1675 raise DashboardException(e=err, msg='Unable to rename zone to {}'.format(new_zone_name), # noqa E501 #pylint: disable=line-too-long
1676 http_status_code=500, component='rgw')
1677 except SubprocessError as error:
1678 raise DashboardException(error, http_status_code=500, component='rgw')
1679 self.update_period()
1680 self.modify_zone(new_zone_name, zonegroup_name, default, master, endpoints, access_key,
1681 secret_key)
1682 self.add_placement_targets_zone(new_zone_name, placement_target,
1683 data_pool, index_pool, data_extra_pool)
1684 self.add_storage_class_zone(new_zone_name, placement_target, storage_class,
1685 data_pool_class, compression)
1686
1687 def list_zones(self):
1688 rgw_zone_list = {}
1689 rgw_zone_list_cmd = ['zone', 'list']
1690 try:
1691 exit_code, out, _ = mgr.send_rgwadmin_command(rgw_zone_list_cmd)
1692 if exit_code > 0:
1693 raise DashboardException(msg='Unable to fetch zone list',
1694 http_status_code=500, component='rgw')
1695 rgw_zone_list = out
1696 except SubprocessError as error:
1697 raise DashboardException(error, http_status_code=500, component='rgw')
1698 return rgw_zone_list
1699
1700 def get_zone(self, zone_name: str):
1701 zone_info = {}
1702 rgw_zone_info_cmd = ['zone', 'get', '--rgw-zone', zone_name]
1703 try:
1704 exit_code, out, _ = mgr.send_rgwadmin_command(rgw_zone_info_cmd)
1705 if exit_code > 0:
1706 raise DashboardException('Unable to get zone info',
1707 http_status_code=500, component='rgw')
1708 zone_info = out
1709 except SubprocessError as error:
1710 raise DashboardException(error, http_status_code=500, component='rgw')
1711 return zone_info
1712
1713 def get_all_zones_info(self):
1714 all_zones_info = {}
1715 zones_info = []
1716 rgw_zone_list = self.list_zones()
1717 if 'zones' in rgw_zone_list:
1718 if rgw_zone_list['zones'] != []:
1719 for rgw_zone in rgw_zone_list['zones']:
1720 zone_info = self.get_zone(rgw_zone)
1721 zones_info.append(zone_info)
1722 all_zones_info['zones'] = zones_info # type: ignore
1723 else:
1724 all_zones_info['zones'] = []
1725 if 'default_info' in rgw_zone_list and rgw_zone_list['default_info'] != '':
1726 all_zones_info['default_zone'] = rgw_zone_list['default_info'] # type: ignore
1727 else:
1728 all_zones_info['default_zone'] = '' # type: ignore
1729 return all_zones_info
1730
1731 def delete_zone(self, zone_name: str, delete_pools: str, pools: List[str],
1732 zonegroup_name: str = '',):
1733 rgw_remove_zone_from_zonegroup_cmd = ['zonegroup', 'remove', '--rgw-zonegroup',
1734 zonegroup_name, '--rgw-zone', zone_name]
1735 rgw_delete_zone_cmd = ['zone', 'delete', '--rgw-zone', zone_name]
1736 if zonegroup_name:
1737 try:
1738 exit_code, _, _ = mgr.send_rgwadmin_command(rgw_remove_zone_from_zonegroup_cmd)
1739 if exit_code > 0:
1740 raise DashboardException(msg='Unable to remove zone from zonegroup',
1741 http_status_code=500, component='rgw')
1742 except SubprocessError as error:
1743 raise DashboardException(error, http_status_code=500, component='rgw')
1744 self.update_period()
1745 try:
1746 exit_code, _, _ = mgr.send_rgwadmin_command(rgw_delete_zone_cmd)
1747 if exit_code > 0:
1748 raise DashboardException(msg='Unable to delete zone',
1749 http_status_code=500, component='rgw')
1750 except SubprocessError as error:
1751 raise DashboardException(error, http_status_code=500, component='rgw')
1752 self.update_period()
1753 if delete_pools == 'true':
1754 self.delete_pools(pools)
1755
1756 def delete_pools(self, pools):
1757 for pool in pools:
1758 if mgr.rados.pool_exists(pool):
1759 mgr.rados.delete_pool(pool)
1760
1761 def create_system_user(self, userName: str, zoneName: str):
1762 rgw_user_create_cmd = ['user', 'create', '--uid', userName,
1763 '--display-name', userName, '--rgw-zone', zoneName, '--system']
1764 try:
1765 exit_code, out, _ = mgr.send_rgwadmin_command(rgw_user_create_cmd)
1766 if exit_code > 0:
1767 raise DashboardException(msg='Unable to create system user',
1768 http_status_code=500, component='rgw')
1769 return out
1770 except SubprocessError as error:
1771 raise DashboardException(error, http_status_code=500, component='rgw')
1772
1773 def get_user_list(self, zoneName: str):
1774 all_users_info = []
1775 user_list = []
1776 rgw_user_list_cmd = ['user', 'list', '--rgw-zone', zoneName]
1777 try:
1778 exit_code, out, _ = mgr.send_rgwadmin_command(rgw_user_list_cmd)
1779 if exit_code > 0:
1780 raise DashboardException('Unable to get user list',
1781 http_status_code=500, component='rgw')
1782 user_list = out
1783 except SubprocessError as error:
1784 raise DashboardException(error, http_status_code=500, component='rgw')
1785
1786 if len(user_list) > 0:
1787 for user_name in user_list:
1788 rgw_user_info_cmd = ['user', 'info', '--uid', user_name, '--rgw-zone', zoneName]
1789 try:
1790 exit_code, out, _ = mgr.send_rgwadmin_command(rgw_user_info_cmd)
1791 if exit_code > 0:
1792 raise DashboardException('Unable to get user info',
1793 http_status_code=500, component='rgw')
1794 all_users_info.append(out)
1795 except SubprocessError as error:
1796 raise DashboardException(error, http_status_code=500, component='rgw')
1797 return all_users_info
1798
1799 def get_multisite_status(self):
1800 is_multisite_configured = True
1801 rgw_realm_list = self.list_realms()
1802 rgw_zonegroup_list = self.list_zonegroups()
1803 rgw_zone_list = self.list_zones()
f78120f9
TL
1804 if len(rgw_realm_list['realms']) < 1 and len(rgw_zonegroup_list['zonegroups']) <= 1 \
1805 and len(rgw_zone_list['zones']) <= 1:
aee94f69
TL
1806 is_multisite_configured = False
1807 return is_multisite_configured
1808
1809 def get_multisite_sync_status(self):
1810 rgw_multisite_sync_status_cmd = ['sync', 'status']
1811 try:
1812 exit_code, out, _ = mgr.send_rgwadmin_command(rgw_multisite_sync_status_cmd, False)
1813 if exit_code > 0:
1814 raise DashboardException('Unable to get sync status',
1815 http_status_code=500, component='rgw')
1816 if out:
1817 return self.process_data(out)
1818 except SubprocessError as error:
1819 raise DashboardException(error, http_status_code=500, component='rgw')
1820 return {}
1821
1822 def process_data(self, data):
1823 primary_zone_data, metadata_sync_data = self.extract_metadata_and_primary_zone_data(data)
1824 replica_zones_info = []
1825 if metadata_sync_data != {}:
1826 datasync_info = self.extract_datasync_info(data)
1827 replica_zones_info = [self.extract_replica_zone_data(item) for item in datasync_info]
1828
1829 replica_zones_info_object = {
1830 'metadataSyncInfo': metadata_sync_data,
1831 'dataSyncInfo': replica_zones_info,
1832 'primaryZoneData': primary_zone_data
1833 }
1834
1835 return replica_zones_info_object
1836
1837 def extract_metadata_and_primary_zone_data(self, data):
1838 primary_zone_info, metadata_sync_infoormation = self.extract_zones_data(data)
1839
1840 primary_zone_tree = primary_zone_info.split('\n') if primary_zone_info else []
1841 realm = self.get_primary_zonedata(primary_zone_tree[0])
1842 zonegroup = self.get_primary_zonedata(primary_zone_tree[1])
1843 zone = self.get_primary_zonedata(primary_zone_tree[2])
1844
1845 primary_zone_data = [realm, zonegroup, zone]
1846 zonegroup_info = self.get_zonegroup(zonegroup)
1847 metadata_sync_data = {}
1848 if len(zonegroup_info['zones']) > 1:
1849 metadata_sync_data = self.extract_metadata_sync_data(metadata_sync_infoormation)
1850
1851 return primary_zone_data, metadata_sync_data
1852
1853 def extract_zones_data(self, data):
1854 result = data
1855 primary_zone_info = result.split('metadata sync')[0] if 'metadata sync' in result else None
1856 metadata_sync_infoormation = result.split('metadata sync')[1] if 'metadata sync' in result else None # noqa E501 #pylint: disable=line-too-long
1857 return primary_zone_info, metadata_sync_infoormation
1858
1859 def extract_metadata_sync_data(self, metadata_sync_infoormation):
1860 metadata_sync_info = metadata_sync_infoormation.split('data sync source')[0].strip() if 'data sync source' in metadata_sync_infoormation else None # noqa E501 #pylint: disable=line-too-long
1861
1862 if metadata_sync_info == 'no sync (zone is master)':
1863 return metadata_sync_info
1864
1865 metadata_sync_data = {}
1866 metadata_sync_info_array = metadata_sync_info.split('\n') if metadata_sync_info else []
1867 metadata_sync_data['syncstatus'] = metadata_sync_info_array[0].strip() if len(metadata_sync_info_array) > 0 else None # noqa E501 #pylint: disable=line-too-long
1868
1869 for item in metadata_sync_info_array:
1870 self.extract_metadata_sync_info(metadata_sync_data, item)
1871
1872 metadata_sync_data['fullSyncStatus'] = metadata_sync_info_array
1873 return metadata_sync_data
1874
1875 def extract_metadata_sync_info(self, metadata_sync_data, item):
1876 if 'oldest incremental change not applied:' in item:
1877 metadata_sync_data['timestamp'] = item.split('applied:')[1].split()[0].strip()
1878
1879 def extract_datasync_info(self, data):
1880 metadata_sync_infoormation = data.split('metadata sync')[1] if 'metadata sync' in data else None # noqa E501 #pylint: disable=line-too-long
1881 if 'data sync source' in metadata_sync_infoormation:
1882 datasync_info = metadata_sync_infoormation.split('data sync source')[1].split('source:')
1883 return datasync_info
1884 return []
1885
1886 def extract_replica_zone_data(self, datasync_item):
1887 replica_zone_data = {}
1888 datasync_info_array = datasync_item.split('\n')
1889 replica_zone_name = self.get_primary_zonedata(datasync_info_array[0])
1890 replica_zone_data['name'] = replica_zone_name.strip()
1891 replica_zone_data['syncstatus'] = datasync_info_array[1].strip()
1892 replica_zone_data['fullSyncStatus'] = datasync_info_array
1893 for item in datasync_info_array:
1894 self.extract_metadata_sync_info(replica_zone_data, item)
1895 return replica_zone_data
1896
1897 def get_primary_zonedata(self, data):
1898 regex = r'\(([^)]+)\)'
1899 match = re.search(regex, data)
1900
1901 if match and match.group(1):
1902 return match.group(1)
1903
1904 return ''
f78120f9
TL
1905
1906 def get_sync_policy(self, bucket_name: str = '', zonegroup_name: str = ''):
1907 rgw_sync_policy_cmd = ['sync', 'policy', 'get']
1908 if bucket_name:
1909 rgw_sync_policy_cmd += ['--bucket', bucket_name]
1910 if zonegroup_name:
1911 rgw_sync_policy_cmd += ['--rgw-zonegroup', zonegroup_name]
1912 try:
1913 exit_code, out, err = mgr.send_rgwadmin_command(rgw_sync_policy_cmd)
1914 if exit_code > 0:
1915 raise DashboardException(f'Unable to get sync policy: {err}',
1916 http_status_code=500, component='rgw')
1917 return out
1918 except SubprocessError as error:
1919 raise DashboardException(error, http_status_code=500, component='rgw')
1920
1921 def get_sync_policy_group(self, group_id: str, bucket_name: str = '',
1922 zonegroup_name: str = ''):
1923 rgw_sync_policy_cmd = ['sync', 'group', 'get', '--group-id', group_id]
1924 if bucket_name:
1925 rgw_sync_policy_cmd += ['--bucket', bucket_name]
1926 if zonegroup_name:
1927 rgw_sync_policy_cmd += ['--rgw-zonegroup', zonegroup_name]
1928 try:
1929 exit_code, out, err = mgr.send_rgwadmin_command(rgw_sync_policy_cmd)
1930 if exit_code > 0:
1931 raise DashboardException(f'Unable to get sync policy group: {err}',
1932 http_status_code=500, component='rgw')
1933 return out
1934 except SubprocessError as error:
1935 raise DashboardException(error, http_status_code=500, component='rgw')
1936
1937 def create_sync_policy_group(self, group_id: str, status: str, bucket_name: str = ''):
1938 rgw_sync_policy_cmd = ['sync', 'group', 'create', '--group-id', group_id,
1939 '--status', SyncStatus[status].value]
1940 if bucket_name:
1941 rgw_sync_policy_cmd += ['--bucket', bucket_name]
1942 try:
1943 exit_code, _, err = mgr.send_rgwadmin_command(rgw_sync_policy_cmd)
1944 if exit_code > 0:
1945 raise DashboardException(f'Unable to create sync policy group: {err}',
1946 http_status_code=500, component='rgw')
1947 except SubprocessError as error:
1948 raise DashboardException(error, http_status_code=500, component='rgw')
1949
1950 def update_sync_policy_group(self, group_id: str, status: str, bucket_name: str = ''):
1951 rgw_sync_policy_cmd = ['sync', 'group', 'modify', '--group-id', group_id,
1952 '--status', SyncStatus[status].value]
1953 if bucket_name:
1954 rgw_sync_policy_cmd += ['--bucket', bucket_name]
1955 try:
1956 exit_code, _, err = mgr.send_rgwadmin_command(rgw_sync_policy_cmd)
1957 if exit_code > 0:
1958 raise DashboardException(f'Unable to update sync policy group: {err}',
1959 http_status_code=500, component='rgw')
1960 except SubprocessError as error:
1961 raise DashboardException(error, http_status_code=500, component='rgw')
1962
1963 def remove_sync_policy_group(self, group_id: str, bucket_name=''):
1964 rgw_sync_policy_cmd = ['sync', 'group', 'remove', '--group-id', group_id]
1965 if bucket_name:
1966 rgw_sync_policy_cmd += ['--bucket', bucket_name]
1967 try:
1968 exit_code, _, err = mgr.send_rgwadmin_command(rgw_sync_policy_cmd)
1969 if exit_code > 0:
1970 raise DashboardException(f'Unable to remove sync policy group: {err}',
1971 http_status_code=500, component='rgw')
1972 except SubprocessError as error:
1973 raise DashboardException(error, http_status_code=500, component='rgw')
1974
1975 def create_sync_flow(self, group_id: str, flow_id: str, flow_type: str,
1976 zones: Optional[List[str]] = None, bucket_name: str = '',
1977 source_zone: Optional[List[str]] = None,
1978 destination_zone: Optional[List[str]] = None):
1979 rgw_sync_policy_cmd = ['sync', 'group', 'flow', 'create', '--group-id', group_id,
1980 '--flow-id', flow_id, '--flow-type', SyncFlowTypes[flow_type].value]
1981
1982 if SyncFlowTypes[flow_type].value == 'directional':
1983 if source_zone is not None:
1984 rgw_sync_policy_cmd += ['--source-zone', ','.join(source_zone)]
1985 if destination_zone is not None:
1986 rgw_sync_policy_cmd += ['--dest-zone', ','.join(destination_zone)]
1987 else:
1988 if zones:
1989 rgw_sync_policy_cmd += ['--zones', ','.join(zones)]
1990
1991 if bucket_name:
1992 rgw_sync_policy_cmd += ['--bucket', bucket_name]
1993
1994 try:
1995 exit_code, _, err = mgr.send_rgwadmin_command(rgw_sync_policy_cmd)
1996 if exit_code > 0:
1997 raise DashboardException(f'Unable to create sync flow: {err}',
1998 http_status_code=500, component='rgw')
1999 except SubprocessError as error:
2000 raise DashboardException(error, http_status_code=500, component='rgw')
2001
2002 def remove_sync_flow(self, group_id: str, flow_id: str, flow_type: str,
2003 source_zone='', destination_zone='',
2004 zones: Optional[List[str]] = None, bucket_name: str = ''):
2005 rgw_sync_policy_cmd = ['sync', 'group', 'flow', 'remove', '--group-id', group_id,
2006 '--flow-id', flow_id, '--flow-type', SyncFlowTypes[flow_type].value]
2007
2008 if SyncFlowTypes[flow_type].value == 'directional':
2009 rgw_sync_policy_cmd += ['--source-zone', source_zone, '--dest-zone', destination_zone]
2010 else:
2011 if zones:
2012 rgw_sync_policy_cmd += ['--zones', ','.join(zones)]
2013
2014 if bucket_name:
2015 rgw_sync_policy_cmd += ['--bucket', bucket_name]
2016
2017 try:
2018 exit_code, _, err = mgr.send_rgwadmin_command(rgw_sync_policy_cmd)
2019 if exit_code > 0:
2020 raise DashboardException(f'Unable to remove sync flow: {err}',
2021 http_status_code=500, component='rgw')
2022 except SubprocessError as error:
2023 raise DashboardException(error, http_status_code=500, component='rgw')
2024
2025 def create_sync_pipe(self, group_id: str, pipe_id: str,
2026 source_zones: Optional[List[str]] = None,
2027 destination_zones: Optional[List[str]] = None,
2028 source_bucket: str = '',
2029 destination_bucket: str = '',
2030 bucket_name: str = ''):
2031 rgw_sync_policy_cmd = ['sync', 'group', 'pipe', 'create',
2032 '--group-id', group_id, '--pipe-id', pipe_id]
2033
2034 if bucket_name:
2035 rgw_sync_policy_cmd += ['--bucket', bucket_name]
2036
2037 if source_zones:
2038 rgw_sync_policy_cmd += ['--source-zones', ','.join(source_zones)]
2039
2040 if destination_zones:
2041 rgw_sync_policy_cmd += ['--dest-zones', ','.join(destination_zones)]
2042
2043 if source_bucket:
2044 rgw_sync_policy_cmd += ['--source-bucket', source_bucket]
2045
2046 if destination_bucket:
2047 rgw_sync_policy_cmd += ['--dest-bucket', destination_bucket]
2048
2049 try:
2050 exit_code, _, err = mgr.send_rgwadmin_command(rgw_sync_policy_cmd)
2051 if exit_code > 0:
2052 raise DashboardException(f'Unable to create sync pipe: {err}',
2053 http_status_code=500, component='rgw')
2054 except SubprocessError as error:
2055 raise DashboardException(error, http_status_code=500, component='rgw')
2056
2057 def remove_sync_pipe(self, group_id: str, pipe_id: str,
2058 source_zones: Optional[List[str]] = None,
2059 destination_zones: Optional[List[str]] = None,
2060 destination_bucket: str = '', bucket_name: str = ''):
2061 rgw_sync_policy_cmd = ['sync', 'group', 'pipe', 'remove',
2062 '--group-id', group_id, '--pipe-id', pipe_id]
2063
2064 if bucket_name:
2065 rgw_sync_policy_cmd += ['--bucket', bucket_name]
2066
2067 if source_zones:
2068 rgw_sync_policy_cmd += ['--source-zones', ','.join(source_zones)]
2069
2070 if destination_zones:
2071 rgw_sync_policy_cmd += ['--dest-zones', ','.join(destination_zones)]
2072
2073 if destination_bucket:
2074 rgw_sync_policy_cmd += ['--dest-bucket', destination_bucket]
2075
2076 try:
2077 exit_code, _, err = mgr.send_rgwadmin_command(rgw_sync_policy_cmd)
2078 if exit_code > 0:
2079 raise DashboardException(f'Unable to remove sync pipe: {err}',
2080 http_status_code=500, component='rgw')
2081 except SubprocessError as error:
2082 raise DashboardException(error, http_status_code=500, component='rgw')
2083
2084 def create_dashboard_admin_sync_group(self, zonegroup_name: str = ''):
2085
2086 zonegroup_info = self.get_zonegroup(zonegroup_name)
2087 zone_names = []
2088 for zones in zonegroup_info['zones']:
2089 zone_names.append(zones['name'])
2090
2091 # create a sync policy group with status allowed
2092 self.create_sync_policy_group(_SYNC_GROUP_ID, SyncStatus.allowed.value)
2093 # create a sync flow with source and destination zones
2094 self.create_sync_flow(_SYNC_GROUP_ID, _SYNC_FLOW_ID,
2095 SyncFlowTypes.symmetrical.value,
2096 zones=zone_names)
2097 # create a sync pipe with source and destination zones
2098 self.create_sync_pipe(_SYNC_GROUP_ID, _SYNC_PIPE_ID, source_zones=['*'],
2099 destination_zones=['*'], source_bucket='*', destination_bucket='*')
2100 # period update --commit
2101 self.update_period()
2102
2103 def policy_group_exists(self, group_name: str, zonegroup_name: str):
2104 try:
2105 _ = self.get_sync_policy_group(
2106 group_id=group_name, zonegroup_name=zonegroup_name)
2107 return True
2108 except DashboardException:
2109 return False