]> git.proxmox.com Git - ceph.git/blame - ceph/src/pybind/mgr/dashboard/services/rgw_client.py
import 14.2.4 nautilus point release
[ceph.git] / ceph / src / pybind / mgr / dashboard / services / rgw_client.py
CommitLineData
11fdf7f2
TL
1# -*- coding: utf-8 -*-
2from __future__ import absolute_import
3
4import re
5from distutils.util import strtobool
6from ..awsauth import S3Auth
7from ..settings import Settings, Options
8from ..rest_client import RestClient, RequestException
9from ..tools import build_url, dict_contains_path, is_valid_ip_address
10from .. import mgr, logger
11
12
13class NoCredentialsException(RequestException):
14 def __init__(self):
15 super(NoCredentialsException, self).__init__(
16 'No RGW credentials found, '
17 'please consult the documentation on how to enable RGW for '
18 'the dashboard.')
19
20
21def _determine_rgw_addr():
22 """
23 Get a RGW daemon to determine the configured host (IP address) and port.
24 Note, the service id of the RGW daemons may differ depending on the setup.
25 Example 1:
26 {
27 ...
28 'services': {
29 'rgw': {
30 'daemons': {
31 'summary': '',
32 '0': {
33 ...
34 'addr': '[2001:db8:85a3::8a2e:370:7334]:49774/1534999298',
35 'metadata': {
36 'frontend_config#0': 'civetweb port=7280',
37 }
38 ...
39 }
40 }
41 }
42 }
43 }
44 Example 2:
45 {
46 ...
47 'services': {
48 'rgw': {
49 'daemons': {
50 'summary': '',
51 'rgw': {
52 ...
53 'addr': '192.168.178.3:49774/1534999298',
54 'metadata': {
55 'frontend_config#0': 'civetweb port=8000',
56 }
57 ...
58 }
59 }
60 }
61 }
62 }
63 """
64 service_map = mgr.get('service_map')
65 if not dict_contains_path(service_map, ['services', 'rgw', 'daemons']):
66 raise LookupError('No RGW found')
67 daemon = None
68 daemons = service_map['services']['rgw']['daemons']
69 for key in daemons.keys():
70 if dict_contains_path(daemons[key], ['metadata', 'frontend_config#0']):
71 daemon = daemons[key]
72 break
73 if daemon is None:
74 raise LookupError('No RGW daemon found')
75
76 addr = _parse_addr(daemon['addr'])
77 port, ssl = _parse_frontend_config(daemon['metadata']['frontend_config#0'])
78
79 return addr, port, ssl
80
81
82def _parse_addr(value):
83 """
84 Get the IP address the RGW is running on.
85
86 >>> _parse_addr('192.168.178.3:49774/1534999298')
87 '192.168.178.3'
88
89 >>> _parse_addr('[2001:db8:85a3::8a2e:370:7334]:49774/1534999298')
90 '2001:db8:85a3::8a2e:370:7334'
91
92 >>> _parse_addr('xyz')
93 Traceback (most recent call last):
94 ...
95 LookupError: Failed to determine RGW address
96
97 >>> _parse_addr('192.168.178.a:8080/123456789')
98 Traceback (most recent call last):
99 ...
100 LookupError: Invalid RGW address '192.168.178.a' found
101
102 >>> _parse_addr('[2001:0db8:1234]:443/123456789')
103 Traceback (most recent call last):
104 ...
105 LookupError: Invalid RGW address '2001:0db8:1234' found
106
107 >>> _parse_addr('2001:0db8::1234:49774/1534999298')
108 Traceback (most recent call last):
109 ...
110 LookupError: Failed to determine RGW address
111
112 :param value: The string to process. The syntax is '<HOST>:<PORT>/<NONCE>'.
113 :type: str
114 :raises LookupError if parsing fails to determine the IP address.
115 :return: The IP address.
116 :rtype: str
117 """
118 match = re.search(r'^(\[)?(?(1)([^\]]+)\]|([^:]+)):\d+/\d+?', value)
119 if match:
120 # IPv4:
121 # Group 0: 192.168.178.3:49774/1534999298
122 # Group 3: 192.168.178.3
123 # IPv6:
124 # Group 0: [2001:db8:85a3::8a2e:370:7334]:49774/1534999298
125 # Group 1: [
126 # Group 2: 2001:db8:85a3::8a2e:370:7334
127 addr = match.group(3) if match.group(3) else match.group(2)
128 if not is_valid_ip_address(addr):
129 raise LookupError('Invalid RGW address \'{}\' found'.format(addr))
130 return addr
131 raise LookupError('Failed to determine RGW address')
132
133
134def _parse_frontend_config(config):
135 """
136 Get the port the RGW is running on. Due the complexity of the
137 syntax not all variations are supported.
138
139 Get more details about the configuration syntax here:
140 http://docs.ceph.com/docs/master/radosgw/frontends/
141 https://civetweb.github.io/civetweb/UserManual.html
142
143 >>> _parse_frontend_config('beast port=8000')
144 (8000, False)
145
146 >>> _parse_frontend_config('civetweb port=8000s')
147 (8000, True)
148
149 >>> _parse_frontend_config('beast port=192.0.2.3:80')
150 (80, False)
151
152 >>> _parse_frontend_config('civetweb port=172.5.2.51:8080s')
153 (8080, True)
154
155 >>> _parse_frontend_config('civetweb port=[::]:8080')
156 (8080, False)
157
158 >>> _parse_frontend_config('civetweb port=ip6-localhost:80s')
159 (80, True)
160
161 >>> _parse_frontend_config('civetweb port=[2001:0db8::1234]:80')
162 (80, False)
163
164 >>> _parse_frontend_config('civetweb port=[::1]:8443s')
165 (8443, True)
166
167 >>> _parse_frontend_config('civetweb port=xyz')
168 Traceback (most recent call last):
169 ...
170 LookupError: Failed to determine RGW port
171
172 >>> _parse_frontend_config('civetweb')
173 Traceback (most recent call last):
174 ...
175 LookupError: Failed to determine RGW port
176
177 :param config: The configuration string to parse.
178 :type config: str
179 :raises LookupError if parsing fails to determine the port.
180 :return: A tuple containing the port number and the information
181 whether SSL is used.
182 :rtype: (int, boolean)
183 """
184 match = re.search(r'port=(.*:)?(\d+)(s)?', config)
185 if match:
186 port = int(match.group(2))
187 ssl = match.group(3) == 's'
188 return port, ssl
189 raise LookupError('Failed to determine RGW port')
190
191
192class RgwClient(RestClient):
193 _SYSTEM_USERID = None
194 _ADMIN_PATH = None
195 _host = None
196 _port = None
197 _ssl = None
198 _user_instances = {}
494da23a 199 _rgw_settings_snapshot = None
11fdf7f2
TL
200
201 @staticmethod
202 def _load_settings():
203 # The API access key and secret key are mandatory for a minimal configuration.
204 if not (Settings.RGW_API_ACCESS_KEY and Settings.RGW_API_SECRET_KEY):
205 logger.warning('No credentials found, please consult the '
206 'documentation about how to enable RGW for the '
207 'dashboard.')
208 raise NoCredentialsException()
209
210 if Options.has_default_value('RGW_API_HOST') and \
211 Options.has_default_value('RGW_API_PORT') and \
212 Options.has_default_value('RGW_API_SCHEME'):
213 host, port, ssl = _determine_rgw_addr()
214 else:
215 host = Settings.RGW_API_HOST
216 port = Settings.RGW_API_PORT
217 ssl = Settings.RGW_API_SCHEME == 'https'
218
219 RgwClient._host = host
220 RgwClient._port = port
221 RgwClient._ssl = ssl
222 RgwClient._ADMIN_PATH = Settings.RGW_API_ADMIN_RESOURCE
223
224 # Create an instance using the configured settings.
225 instance = RgwClient(Settings.RGW_API_USER_ID,
226 Settings.RGW_API_ACCESS_KEY,
227 Settings.RGW_API_SECRET_KEY)
228
229 RgwClient._SYSTEM_USERID = instance.userid
230
231 # Append the instance to the internal map.
232 RgwClient._user_instances[RgwClient._SYSTEM_USERID] = instance
233
494da23a
TL
234 @staticmethod
235 def _rgw_settings():
236 return (Settings.RGW_API_HOST,
237 Settings.RGW_API_PORT,
238 Settings.RGW_API_ACCESS_KEY,
239 Settings.RGW_API_SECRET_KEY,
240 Settings.RGW_API_ADMIN_RESOURCE,
241 Settings.RGW_API_SCHEME,
242 Settings.RGW_API_USER_ID,
243 Settings.RGW_API_SSL_VERIFY)
244
11fdf7f2
TL
245 @staticmethod
246 def instance(userid):
494da23a
TL
247 # Discard all cached instances if any rgw setting has changed
248 if RgwClient._rgw_settings_snapshot != RgwClient._rgw_settings():
249 RgwClient._rgw_settings_snapshot = RgwClient._rgw_settings()
250 RgwClient._user_instances.clear()
251
11fdf7f2
TL
252 if not RgwClient._user_instances:
253 RgwClient._load_settings()
254
255 if not userid:
256 userid = RgwClient._SYSTEM_USERID
257
258 if userid not in RgwClient._user_instances:
259 # Get the access and secret keys for the specified user.
260 keys = RgwClient.admin_instance().get_user_keys(userid)
261 if not keys:
262 raise RequestException(
263 "User '{}' does not have any keys configured.".format(
264 userid))
265
266 # Create an instance and append it to the internal map.
267 RgwClient._user_instances[userid] = RgwClient(userid,
268 keys['access_key'],
269 keys['secret_key'])
270
271 return RgwClient._user_instances[userid]
272
273 @staticmethod
274 def admin_instance():
275 return RgwClient.instance(RgwClient._SYSTEM_USERID)
276
277 def _reset_login(self):
278 if self.userid != RgwClient._SYSTEM_USERID:
279 logger.info("Fetching new keys for user: %s", self.userid)
280 keys = RgwClient.admin_instance().get_user_keys(self.userid)
281 self.auth = S3Auth(keys['access_key'], keys['secret_key'],
282 service_url=self.service_url)
283 else:
284 raise RequestException('Authentication failed for the "{}" user: wrong credentials'
285 .format(self.userid), status_code=401)
286
287 def __init__(self, # pylint: disable-msg=R0913
288 userid,
289 access_key,
290 secret_key,
291 host=None,
292 port=None,
81eedcae 293 admin_path=None,
11fdf7f2
TL
294 ssl=False):
295
296 if not host and not RgwClient._host:
297 RgwClient._load_settings()
298 host = host if host else RgwClient._host
299 port = port if port else RgwClient._port
300 admin_path = admin_path if admin_path else RgwClient._ADMIN_PATH
301 ssl = ssl if ssl else RgwClient._ssl
302 ssl_verify = Settings.RGW_API_SSL_VERIFY
303
304 self.service_url = build_url(host=host, port=port)
305 self.admin_path = admin_path
306
307 s3auth = S3Auth(access_key, secret_key, service_url=self.service_url)
308 super(RgwClient, self).__init__(host, port, 'RGW', ssl, s3auth, ssl_verify=ssl_verify)
309
310 # If user ID is not set, then try to get it via the RGW Admin Ops API.
311 self.userid = userid if userid else self._get_user_id(self.admin_path)
312
313 logger.info("Created new connection for user: %s", self.userid)
314
315 @RestClient.api_get('/', resp_structure='[0] > (ID & DisplayName)')
316 def is_service_online(self, request=None):
317 """
318 Consider the service as online if the response contains the
319 specified keys. Nothing more is checked here.
320 """
321 _ = request({'format': 'json'})
322 return True
323
324 @RestClient.api_get('/{admin_path}/metadata/user?myself',
325 resp_structure='data > user_id')
326 def _get_user_id(self, admin_path, request=None):
327 # pylint: disable=unused-argument
328 """
329 Get the user ID of the user that is used to communicate with the
330 RGW Admin Ops API.
331 :rtype: str
332 :return: The user ID of the user that is used to sign the
333 RGW Admin Ops API calls.
334 """
335 response = request()
336 return response['data']['user_id']
337
338 @RestClient.api_get('/{admin_path}/metadata/user', resp_structure='[+]')
339 def _user_exists(self, admin_path, user_id, request=None):
340 # pylint: disable=unused-argument
341 response = request()
342 if user_id:
343 return user_id in response
344 return self.userid in response
345
346 def user_exists(self, user_id=None):
347 return self._user_exists(self.admin_path, user_id)
348
349 @RestClient.api_get('/{admin_path}/metadata/user?key={userid}',
350 resp_structure='data > system')
351 def _is_system_user(self, admin_path, userid, request=None):
352 # pylint: disable=unused-argument
353 response = request()
354 return strtobool(response['data']['system'])
355
356 def is_system_user(self):
357 return self._is_system_user(self.admin_path, self.userid)
358
359 @RestClient.api_get(
360 '/{admin_path}/user',
361 resp_structure='tenant & user_id & email & keys[*] > '
362 ' (user & access_key & secret_key)')
363 def _admin_get_user_keys(self, admin_path, userid, request=None):
364 # pylint: disable=unused-argument
365 colon_idx = userid.find(':')
366 user = userid if colon_idx == -1 else userid[:colon_idx]
367 response = request({'uid': user})
368 for key in response['keys']:
369 if key['user'] == userid:
370 return {
371 'access_key': key['access_key'],
372 'secret_key': key['secret_key']
373 }
374 return None
375
376 def get_user_keys(self, userid):
377 return self._admin_get_user_keys(self.admin_path, userid)
378
379 @RestClient.api('/{admin_path}/{path}')
380 def _proxy_request(self, # pylint: disable=too-many-arguments
381 admin_path,
382 path,
383 method,
384 params,
385 data,
386 request=None):
387 # pylint: disable=unused-argument
388 return request(
389 method=method, params=params, data=data, raw_content=True)
390
391 def proxy(self, method, path, params, data):
392 logger.debug("proxying method=%s path=%s params=%s data=%s", method,
393 path, params, data)
394 return self._proxy_request(self.admin_path, path, method, params, data)
395
396 @RestClient.api_get('/', resp_structure='[1][*] > Name')
397 def get_buckets(self, request=None):
398 """
399 Get a list of names from all existing buckets of this user.
400 :return: Returns a list of bucket names.
401 """
402 response = request({'format': 'json'})
403 return [bucket['Name'] for bucket in response[1]]
404
405 @RestClient.api_get('/{bucket_name}')
406 def bucket_exists(self, bucket_name, userid, request=None):
407 """
408 Check if the specified bucket exists for this user.
409 :param bucket_name: The name of the bucket.
410 :return: Returns True if the bucket exists, otherwise False.
411 """
412 # pylint: disable=unused-argument
413 try:
414 request()
415 my_buckets = self.get_buckets()
416 if bucket_name not in my_buckets:
417 raise RequestException(
418 'Bucket "{}" belongs to other user'.format(bucket_name),
419 403)
420 return True
421 except RequestException as e:
422 if e.status_code == 404:
423 return False
424
425 raise e
426
427 @RestClient.api_put('/{bucket_name}')
428 def create_bucket(self, bucket_name, request=None):
429 logger.info("Creating bucket: %s", bucket_name)
430 return request()