self._session = requests.Session()
self._resp = None
- def _request(self, url, method, data=None):
+ def _request(self, url, method, data=None, params=None):
url = "{}{}".format(self.base_uri, url)
+
log.info("request %s to %s", method, url)
if method == 'GET':
- self._resp = self._session.get(url)
+ self._resp = self._session.get(url, params=params)
try:
return self._resp.json()
except ValueError as ex:
log.exception("Failed to decode response: %s", self._resp.text)
raise ex
elif method == 'POST':
- self._resp = self._session.post(url, json=data)
+ self._resp = self._session.post(url, json=data, params=params)
elif method == 'DELETE':
- self._resp = self._session.delete(url, json=data)
+ self._resp = self._session.delete(url, json=data, params=params)
elif method == 'PUT':
- self._resp = self._session.put(url, json=data)
+ self._resp = self._session.put(url, json=data, params=params)
else:
assert False
+ return None
- def _get(self, url):
- return self._request(url, 'GET')
+ def _get(self, url, params=None):
+ return self._request(url, 'GET', params=params)
- def _post(self, url, data=None):
- self._request(url, 'POST', data)
+ def _post(self, url, data=None, params=None):
+ self._request(url, 'POST', data, params=params)
- def _delete(self, url, data=None):
- self._request(url, 'DELETE', data)
+ def _delete(self, url, data=None, params=None):
+ self._request(url, 'DELETE', data, params=params)
- def _put(self, url, data=None):
- self._request(url, 'PUT', data)
+ def _put(self, url, data=None, params=None):
+ self._request(url, 'PUT', data, params=params)
def cookies(self):
return self._resp.cookies
args.extend(cmd)
cls.mgr_cluster.admin_remote.run(args=args)
+ @classmethod
+ def _radosgw_admin_cmd(cls, cmd):
+ args = [
+ 'radosgw-admin'
+ ]
+ args.extend(cmd)
+ cls.mgr_cluster.admin_remote.run(args=args)
+
@classmethod
def mons(cls):
out = cls.ceph_cluster.mon_manager.raw_cluster_cmd('mon_status')
# -*- coding: utf-8 -*-
from __future__ import absolute_import
+import urllib
+import logging
+logger = logging.getLogger(__name__)
from .helper import DashboardTestCase, authenticate
class RgwControllerTest(DashboardTestCase):
-
@authenticate
def test_rgw_daemon_list(self):
data = self._get('/api/rgw/daemon')
self.assertIn('rgw_id', data)
self.assertIn('rgw_status', data)
self.assertTrue(data['rgw_metadata'])
+
+class RgwProxyExceptionsTest(DashboardTestCase):
+
+ @classmethod
+ def setUpClass(cls):
+ super(RgwProxyExceptionsTest, cls).setUpClass()
+
+ cls._ceph_cmd(['dashboard', 'set-rgw-api-secret-key', ''])
+ cls._ceph_cmd(['dashboard', 'set-rgw-api-access-key', ''])
+
+ @authenticate
+ def test_no_credentials_exception(self):
+ resp = self._get('/api/rgw/proxy/status')
+ self.assertStatus(401)
+ self.assertIn('message', resp)
+
+
+class RgwProxyTest(DashboardTestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(RgwProxyTest, cls).setUpClass()
+ cls._radosgw_admin_cmd([
+ 'user', 'create', '--uid=admin', '--display-name=admin',
+ '--system', '--access-key=admin', '--secret=admin'
+ ])
+ cls._ceph_cmd(['dashboard', 'set-rgw-api-secret-key', 'admin'])
+ cls._ceph_cmd(['dashboard', 'set-rgw-api-access-key', 'admin'])
+
+ def _assert_user_data(self, data):
+ self.assertIn('caps', data)
+ self.assertIn('display_name', data)
+ self.assertIn('email', data)
+ self.assertIn('keys', data)
+ self.assertGreaterEqual(len(data['keys']), 1)
+ self.assertIn('max_buckets', data)
+ self.assertIn('subusers', data)
+ self.assertIn('suspended', data)
+ self.assertIn('swift_keys', data)
+ self.assertIn('tenant', data)
+ self.assertIn('user_id', data)
+
+ def _test_put(self):
+ self._put(
+ '/api/rgw/proxy/user',
+ params={
+ 'uid': 'teuth-test-user',
+ 'display-name': 'display name',
+ })
+ data = self._resp.json()
+
+ self._assert_user_data(data)
+ self.assertStatus(200)
+
+ data = self._get(
+ '/api/rgw/proxy/user', params={'uid': 'teuth-test-user'})
+
+ self.assertStatus(200)
+ self.assertEqual(data['user_id'], 'teuth-test-user')
+
+ def _test_get(self):
+ data = self._get(
+ '/api/rgw/proxy/user', params={'uid': 'teuth-test-user'})
+
+ self._assert_user_data(data)
+ self.assertStatus(200)
+ self.assertEquals(data['user_id'], 'teuth-test-user')
+
+ def _test_post(self):
+ """Updates the user"""
+ self._post(
+ '/api/rgw/proxy/user',
+ params={
+ 'uid': 'teuth-test-user',
+ 'display-name': 'new name'
+ })
+
+ self.assertStatus(200)
+ self._assert_user_data(self._resp.json())
+ self.assertEqual(self._resp.json()['display_name'], 'new name')
+
+ def _test_delete(self):
+ self._delete('/api/rgw/proxy/user', params={'uid': 'teuth-test-user'})
+ self.assertStatus(200)
+
+ self._delete('/api/rgw/proxy/user', params={'uid': 'teuth-test-user'})
+ self.assertStatus(404)
+ resp = self._resp.json()
+ self.assertIn('Code', resp)
+ self.assertIn('HostId', resp)
+ self.assertIn('RequestId', resp)
+ self.assertEqual(resp['Code'], 'NoSuchUser')
+
+ @authenticate
+ def test_rgw_proxy(self):
+ """Test basic request types"""
+ self.maxDiff = None
+
+ # PUT - Create a user
+ self._test_put()
+
+ # GET - Get the user details
+ self._test_get()
+
+ # POST - Update the user details
+ self._test_post()
+
+ # DELETE - Delete the user
+ self._test_delete()
The Dashboard's WebUI should then be reachable on TCP port 8080.
+Enabling the Object Gateway management frontend
+-----------------------------------------------
+
+If you want to use the Object Gateway management functionality of the
+dashboard, you will need to provide credentials. If you do not have a user
+which shall be used for providing those credentials, you will also need to
+create one::
+
+ $ radosgw-admin user create --uuid=<user> --display-name=<display-name> \
+ --system
+
+The credentials of a user can also be obtained by using `radosgw-admin`::
+
+ $ radosgw-admin user info --uid=<user>
+
+Finally, set the credentials to the dashboard module::
+
+ $ ceph dashboard set-rgw-api-secret-key <secret_key>
+ $ ceph dashboard set-rgw-api-access-key <access_key>
+
+This is all you have to do to get the Object Gateway management functionality
+working. The host and port of the Object Gateway are determined automatically.
+If multiple zones are used, it will automatically determine the host within the
+master zone group and master zone. This should be sufficient for most setups,
+but in some circumstances you might want to set the host and port manually::
+
+ $ ceph dashboard set-rgw-api-host <host>
+ $ ceph dashboard set-rgw-api-port <port>
+
+In addition to the settings mentioned so far, the following settings do also
+exist and you may find yourself in the situation that you have to use them::
+
+ $ ceph dashboard set-rgw-api-scheme <scheme> # http or https
+ $ ceph dashboard set-rgw-api-admin-resource <admin-resource>
+ $ ceph dashboard set-rgw-api-user-id <user-id>
+
Working on the Dashboard Code
-----------------------------
# -*- coding: utf-8 -*-
-<<<<<<< HEAD
# pylint: disable-all
-=======
->>>>>>> 60417c2dcc... dashboard/mgr: RGW proxy: Include `python-requests-aws`
#
# Copyright (c) 2012-2013 Paul Tax <paultax@gmail.com> All rights reserved.
#
import json
-from . import ApiController, RESTController, AuthRequired
+import cherrypy
+
from .. import logger
from ..services.ceph_service import CephService
+from ..tools import ApiController, RESTController, AuthRequired
+from ..services.rgw_client import RgwClient
+from ..rest_client import RequestException
+from ..exceptions import NoCredentialsException
@ApiController('rgw')
@ApiController('rgw/daemon')
@AuthRequired()
class RgwDaemon(RESTController):
-
def list(self):
daemons = []
for hostname, server in CephService.get_service_map('rgw').items():
for service in server['services']:
metadata = service['metadata']
+ status = service['status']
+ if 'json' in status:
+ try:
+ status = json.loads(status['json'])
+ except ValueError:
+ logger.warning("%s had invalid status json", service['id'])
+ status = {}
+ else:
+ logger.warning('%s has no key "json" in status', service['id'])
# extract per-daemon service data and health
daemon = {
daemon['rgw_metadata'] = metadata
daemon['rgw_status'] = status
+
return daemon
+
+
+@ApiController('rgw/proxy')
+@AuthRequired()
+class RgwProxy(RESTController):
+ @cherrypy.expose
+ def default(self, *vpath, **params):
+ try:
+ rgw_client = RgwClient.admin_instance()
+
+ except NoCredentialsException as e:
+ cherrypy.response.headers['Content-Type'] = 'application/json'
+ cherrypy.response.status = 401
+ return json.dumps({'message': e.message})
+
+ method = cherrypy.request.method
+ path = '/'.join(vpath)
+ data = None
+
+ if cherrypy.request.body.length:
+ data = cherrypy.request.body.read()
+
+ try:
+ return rgw_client.proxy(method, path, params, data)
+ except RequestException as e:
+ cherrypy.response.status = e.status_code
+ return e.content
--- /dev/null
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+
+class NoCredentialsException(Exception):
+ def __init__(self):
+ super(Exception, self).__init__(
+ 'No RGW credentials found, '
+ 'please consult the documentation on how to enable RGW for '
+ 'the dashboard.')
--- /dev/null
+# -*- coding: utf-8 -*-
+"""
+ * Copyright (c) 2017 SUSE LLC
+ *
+ * openATTIC is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; version 2.
+ *
+ * This package is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+"""
+from __future__ import absolute_import
+
+from .tools import build_url
+import inspect
+import itertools
+import re
+import requests
+from requests.exceptions import ConnectionError, InvalidURL
+from . import logger
+
+try:
+ from requests.packages.urllib3.exceptions import SSLError
+except ImportError:
+ from urllib3.exceptions import SSLError
+
+
+class RequestException(Exception):
+ def __init__(self,
+ message,
+ status_code=None,
+ content=None,
+ conn_errno=None,
+ conn_strerror=None):
+ super(RequestException, self).__init__(message)
+ self.status_code = status_code
+ self.content = content
+ self.conn_errno = conn_errno
+ self.conn_strerror = conn_strerror
+
+
+class BadResponseFormatException(RequestException):
+ def __init__(self, message):
+ super(BadResponseFormatException, self).__init__(
+ "Bad response format" if message is None else message, None)
+
+
+class _ResponseValidator(object):
+ """Simple JSON schema validator
+
+ This class implements a very simple validator for the JSON formatted
+ messages received by request responses from a RestClient instance.
+
+ The validator validates the JSON response against a "structure" string that
+ specifies the structure that the JSON response must comply. The validation
+ procedure raises a BadResponseFormatException in case of a validation
+ failure.
+
+ The structure syntax is given by the following grammar:
+
+ Structure ::= Level
+ Level ::= Path | Path '&' Level
+ Path ::= Step | Step '>'+ Path
+ Step ::= Key | '?' Key | '*' | '(' Level ')'
+ Key ::= <string> | Array+
+ Array ::= '[' <int> ']' | '[' '*' ']' | '[' '+' ']'
+
+ The symbols enclosed in ' ' are tokens of the language, and the + symbol
+ denotes repetition of of the preceding token at least once.
+
+ Examples of usage:
+
+ Example 1:
+ Validator args:
+ structure = "return > *"
+ response = { 'return': { ... } }
+
+ In the above example the structure will validate against any response
+ that contains a key named "return" in the root of the response
+ dictionary and its value is also a dictionary.
+
+ Example 2:
+ Validator args:
+ structure = "[*]"
+ response = [...]
+
+ In the above example the structure will validate against any response
+ that is an array of any size.
+
+ Example 3:
+ Validator args:
+ structure = "return[*]"
+ response = { 'return': [....] }
+
+ In the above example the structure will validate against any response
+ that contains a key named "return" in the root of the response
+ dictionary and its value is an array.
+
+ Example 4:
+ Validator args:
+ structure = "return[0] > token"
+ response = { 'return': [ { 'token': .... } ] }
+
+ In the above example the structure will validate against any response
+ that contains a key named "return" in the root of the response
+ dictionary and its value is an array, and the first element of the
+ array is a dictionary that contains the key 'token'.
+
+ Example 5:
+ Validator args:
+ structure = "return[0][*] > key1"
+ response = { 'return': [ [ { 'key1': ... } ], ...] }
+
+ In the above example the structure will validate against any response
+ that contains a key named "return" in the root of the response
+ dictionary where its value is an array, and the first value of this
+ array is also an array where all it's values must be a dictionary
+ containing a key named "key1".
+
+ Example 6:
+ Validator args:
+ structure = "return > (key1[*] & key2 & ?key3 > subkey)"
+ response = { 'return': { 'key1': [...], 'key2: .... } ] }
+
+ In the above example the structure will validate against any response
+ that contains a key named "return" in the root of the response
+ dictionary and its value is a dictionary that must contain a key named
+ "key1" that is an array, a key named "key2", and optionaly a key named
+ "key3" that is a dictionary that contains a key named "subkey".
+
+ Example 7:
+ Validator args:
+ structure = "return >> roles[*]"
+ response = { 'return': { 'key1': { 'roles': [...] }, 'key2': { 'roles': [...] } } }
+
+ In the above example the structure will validate against any response
+ that contains a key named "return" in the root of the response
+ dictionary, and its value is a dictionary that for any key present in
+ the dictionary their value is also a dictionary that must contain a key
+ named 'roles' that is an array. Please note that you can use any
+ number of successive '>' to denote the level in the JSON tree that you
+ want to match next step in the path.
+
+ """
+
+ @staticmethod
+ def validate(structure, response):
+ if structure is None:
+ return
+
+ _ResponseValidator._validate_level(structure, response)
+
+ @staticmethod
+ def _validate_level(level, resp):
+ if not isinstance(resp, dict) and not isinstance(resp, list):
+ raise BadResponseFormatException(
+ "{} is neither a dict nor a list".format(resp))
+
+ paths = _ResponseValidator._parse_level_paths(level)
+ for path in paths:
+ path_sep = path.find('>')
+ if path_sep != -1:
+ level_next = path[path_sep + 1:].strip()
+ else:
+ path_sep = len(path)
+ level_next = None
+ key = path[:path_sep].strip()
+
+ if key == '*':
+ continue
+ elif key == '': # check all keys
+ for k in resp.keys():
+ _ResponseValidator._validate_key(k, level_next, resp)
+ else:
+ _ResponseValidator._validate_key(key, level_next, resp)
+
+ @staticmethod
+ def _validate_array(array_seq, level_next, resp):
+ if array_seq:
+ if not isinstance(resp, list):
+ raise BadResponseFormatException(
+ "{} is not an array".format(resp))
+ if array_seq[0].isdigit():
+ idx = int(array_seq[0])
+ if len(resp) <= idx:
+ raise BadResponseFormatException(
+ "length of array {} is lower than the index {}".format(
+ resp, idx))
+ _ResponseValidator._validate_array(array_seq[1:], level_next,
+ resp[idx])
+ elif array_seq[0] == '*':
+ for r in resp:
+ _ResponseValidator._validate_array(array_seq[1:],
+ level_next, r)
+ elif array_seq[0] == '+':
+ if len(resp) < 1:
+ raise BadResponseFormatException(
+ "array should not be empty")
+ for r in resp:
+ _ResponseValidator._validate_array(array_seq[1:],
+ level_next, r)
+ else:
+ raise Exception(
+ "Response structure is invalid: only <int> | '*' are "
+ "allowed as array index arguments")
+ else:
+ if level_next:
+ _ResponseValidator._validate_level(level_next, resp)
+
+ @staticmethod
+ def _validate_key(key, level_next, resp):
+ array_access = [a.strip() for a in key.split("[")]
+ key = array_access[0]
+ if key:
+ optional = key[0] == '?'
+ if optional:
+ key = key[1:]
+ if key not in resp:
+ if optional:
+ return
+ raise BadResponseFormatException(
+ "key {} is not in dict {}".format(key, resp))
+ resp_next = resp[key]
+ else:
+ resp_next = resp
+ if len(array_access) > 1:
+ _ResponseValidator._validate_array(
+ [a[0:-1] for a in array_access[1:]], level_next, resp_next)
+ else:
+ if level_next:
+ _ResponseValidator._validate_level(level_next, resp_next)
+
+ @staticmethod
+ def _parse_level_paths(level):
+ level = level.strip()
+ if level[0] == '(':
+ level = level[1:]
+ if level[-1] == ')':
+ level = level[:-1]
+
+ paths = []
+ lp = 0
+ nested = 0
+ for i, c in enumerate(level):
+ if c == '&' and nested == 0:
+ paths.append(level[lp:i].strip())
+ lp = i + 1
+ elif c == '(':
+ nested += 1
+ elif c == ')':
+ nested -= 1
+ paths.append(level[lp:].strip())
+ return paths
+
+
+class _Request(object):
+ def __init__(self, method, path, path_params, rest_client, resp_structure):
+ self.method = method
+ self.path = path
+ self.path_params = path_params
+ self.rest_client = rest_client
+ self.resp_structure = resp_structure
+
+ def _gen_path(self):
+ new_path = self.path
+ matches = re.finditer(r'\{(\w+?)\}', self.path)
+ for match in matches:
+ if match:
+ param_key = match.group(1)
+ if param_key in self.path_params:
+ new_path = new_path.replace(
+ match.group(0), self.path_params[param_key])
+ else:
+ raise RequestException(
+ 'Invalid path. Param "{}" was not specified'
+ .format(param_key), None)
+ return new_path
+
+ def __call__(self,
+ req_data=None,
+ method=None,
+ params=None,
+ data=None,
+ raw_content=False):
+ method = method if method else self.method
+ if not method:
+ raise Exception('No HTTP request method specified')
+ if req_data:
+ if method == 'get':
+ if params:
+ raise Exception('Ambiguous source of GET params')
+ params = req_data
+ else:
+ if data:
+ raise Exception('Ambiguous source of {} data'.format(
+ method.upper()))
+ data = req_data
+ resp = self.rest_client.do_request(method, self._gen_path(), params,
+ data, raw_content)
+ if raw_content and self.resp_structure:
+ raise Exception("Cannot validate reponse in raw format")
+ _ResponseValidator.validate(self.resp_structure, resp)
+ return resp
+
+
+class RestClient(object):
+ def __init__(self, host, port, client_name=None, ssl=False, auth=None):
+ super(RestClient, self).__init__()
+ self.client_name = client_name if client_name else ''
+ self.host = host
+ self.port = port
+ self.base_url = build_url(
+ scheme='https' if ssl else 'http', host=host, port=port)
+ logger.debug("REST service base URL: %s", self.base_url)
+ self.headers = {'Accept': 'application/json'}
+ self.auth = auth
+ self.session = requests.Session()
+
+ def _login(self, request=None):
+ pass
+
+ def _is_logged_in(self):
+ pass
+
+ def _reset_login(self):
+ pass
+
+ def is_service_online(self, request=None):
+ pass
+
+ @staticmethod
+ def requires_login(func):
+ def func_wrapper(self, *args, **kwargs):
+ retries = 2
+ while True:
+ try:
+ if not self._is_logged_in():
+ self._login()
+ resp = func(self, *args, **kwargs)
+ return resp
+ except RequestException as e:
+ if isinstance(e, BadResponseFormatException):
+ raise e
+ retries -= 1
+ if e.status_code not in [401, 403] or retries == 0:
+ raise e
+ self._reset_login()
+
+ return func_wrapper
+
+ def do_request(self,
+ method,
+ path,
+ params=None,
+ data=None,
+ raw_content=False):
+ url = '{}{}'.format(self.base_url, path)
+ logger.debug('%s REST API %s req: %s data: %s', self.client_name,
+ method.upper(), path, data)
+ try:
+ if method.lower() == 'get':
+ resp = self.session.get(
+ url, headers=self.headers, params=params, auth=self.auth)
+ elif method.lower() == 'post':
+ resp = self.session.post(
+ url,
+ headers=self.headers,
+ params=params,
+ data=data,
+ auth=self.auth)
+ elif method.lower() == 'put':
+ resp = self.session.put(
+ url,
+ headers=self.headers,
+ params=params,
+ data=data,
+ auth=self.auth)
+ elif method.lower() == 'delete':
+ resp = self.session.delete(
+ url,
+ headers=self.headers,
+ params=params,
+ data=data,
+ auth=self.auth)
+ else:
+ raise RequestException('Method "{}" not supported'.format(
+ method.upper()), None)
+ if resp.ok:
+ logger.debug("%s REST API %s res status: %s content: %s",
+ self.client_name, method.upper(),
+ resp.status_code, resp.text)
+ if raw_content:
+ return resp.content
+ try:
+ return resp.json() if resp.text else None
+ except ValueError:
+ logger.error(
+ "%s REST API failed %s req while decoding JSON "
+ "response : %s",
+ self.client_name, method.upper(), resp.text)
+ raise RequestException(
+ "{} REST API failed request while decoding JSON "
+ "response: {}".format(self.client_name, resp.text),
+ resp.status_code, resp.text)
+ else:
+ logger.error(
+ "%s REST API failed %s req status: %s", self.client_name,
+ method.upper(), resp.status_code)
+ from pprint import pprint as pp
+ from pprint import pformat as pf
+
+ raise RequestException(
+ "{} REST API failed request with status code {}\n"
+ "{}" # TODO remove
+ .format(self.client_name, resp.status_code, pf(
+ resp.content)),
+ resp.status_code,
+ resp.content)
+ except ConnectionError as ex:
+ if ex.args:
+ if isinstance(ex.args[0], SSLError):
+ errno = "n/a"
+ strerror = "SSL error. Probably trying to access a non " \
+ "SSL connection."
+ logger.error("%s REST API failed %s, SSL error.",
+ self.client_name, method.upper())
+ else:
+ match = re.match(r'.*: \[Errno (-?\d+)\] (.+)',
+ ex.args[0].reason.args[0])
+ if match:
+ errno = match.group(1)
+ strerror = match.group(2)
+ logger.error(
+ "%s REST API failed %s, connection error: "
+ "[errno: %s] %s",
+ self.client_name, method.upper(), errno, strerror)
+ else:
+ errno = "n/a"
+ strerror = "n/a"
+ logger.error(
+ "%s REST API failed %s, connection error.",
+ self.client_name, method.upper())
+ else:
+ errno = "n/a"
+ strerror = "n/a"
+ logger.error("%s REST API failed %s, connection error.",
+ self.client_name, method.upper())
+
+ if errno != "n/a":
+ ex_msg = (
+ "{} REST API cannot be reached: {} [errno {}]. "
+ "Please check your configuration and that the API endpoint"
+ " is accessible"
+ .format(self.client_name, strerror, errno))
+ else:
+ ex_msg = (
+ "{} REST API cannot be reached. Please check "
+ "your configuration and that the API endpoint is"
+ " accessible"
+ .format(self.client_name))
+ raise RequestException(
+ ex_msg, conn_errno=errno, conn_strerror=strerror)
+ except InvalidURL as ex:
+ logger.exception("%s REST API failed %s: %s", self.client_name,
+ method.upper(), str(ex))
+ raise RequestException(str(ex))
+
+ @staticmethod
+ def api(path, **api_kwargs):
+ def call_decorator(func):
+ def func_wrapper(self, *args, **kwargs):
+ method = api_kwargs.get('method', None)
+ resp_structure = api_kwargs.get('resp_structure', None)
+ args_name = inspect.getargspec(func).args
+ args_dict = dict(itertools.izip(args_name[1:], args))
+ for key, val in kwargs:
+ args_dict[key] = val
+ return func(
+ self,
+ *args,
+ request=_Request(method, path, args_dict, self,
+ resp_structure),
+ **kwargs)
+
+ return func_wrapper
+
+ return call_decorator
+
+ @staticmethod
+ def api_get(path, resp_structure=None):
+ return RestClient.api(
+ path, method='get', resp_structure=resp_structure)
+
+ @staticmethod
+ def api_post(path, resp_structure=None):
+ return RestClient.api(
+ path, method='post', resp_structure=resp_structure)
+
+ @staticmethod
+ def api_put(path, resp_structure=None):
+ return RestClient.api(
+ path, method='put', resp_structure=resp_structure)
+
+ @staticmethod
+ def api_delete(path, resp_structure=None):
+ return RestClient.api(
+ path, method='delete', resp_structure=resp_structure)
--- /dev/null
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+import re
+from ..awsauth import S3Auth
+from ..settings import Settings, Options
+from ..rest_client import RestClient, RequestException
+from ..tools import build_url, dict_contains_path
+from ..exceptions import NoCredentialsException
+from .. import mgr, logger
+
+
+def _determine_rgw_addr():
+ service_map = mgr.get('service_map')
+
+ if not dict_contains_path(service_map, ['services', 'rgw', 'daemons', 'rgw']):
+ msg = 'No RGW found.'
+ raise LookupError(msg)
+
+ daemon = service_map['services']['rgw']['daemons']['rgw']
+ addr = daemon['addr'].split(':')[0]
+ match = re.search(r'port=(\d+)',
+ daemon['metadata']['frontend_config#0'])
+ if match:
+ port = int(match.group(1))
+ else:
+ msg = 'Failed to determine RGW port'
+ raise LookupError(msg)
+
+ return addr, port
+
+
+class RgwClient(RestClient):
+ _SYSTEM_USERID = None
+ _ADMIN_PATH = None
+ _host = None
+ _port = None
+ _ssl = None
+ _user_instances = {}
+
+ @staticmethod
+ def _load_settings():
+ if Settings.RGW_API_SCHEME and Settings.RGW_API_ACCESS_KEY and \
+ Settings.RGW_API_SECRET_KEY:
+ if Options.has_default_value('RGW_API_HOST') and \
+ Options.has_default_value('RGW_API_PORT'):
+ host, port = _determine_rgw_addr()
+ else:
+ host, port = Settings.RGW_API_HOST, Settings.RGW_API_PORT
+ else:
+ logger.warning('No credentials found, please consult the '
+ 'documentation about how to enable RGW for the '
+ 'dashboard.')
+ raise NoCredentialsException()
+
+ RgwClient._host = host
+ RgwClient._port = port
+ RgwClient._ssl = Settings.RGW_API_SCHEME == 'https'
+ RgwClient._ADMIN_PATH = Settings.RGW_API_ADMIN_RESOURCE
+ RgwClient._SYSTEM_USERID = Settings.RGW_API_USER_ID
+
+ logger.info("Creating new connection for user: %s",
+ Settings.RGW_API_USER_ID)
+ RgwClient._user_instances[RgwClient._SYSTEM_USERID] = \
+ RgwClient(Settings.RGW_API_USER_ID, Settings.RGW_API_ACCESS_KEY,
+ Settings.RGW_API_SECRET_KEY)
+
+ @staticmethod
+ def instance(userid):
+ if not RgwClient._user_instances:
+ RgwClient._load_settings()
+ if not userid:
+ userid = RgwClient._SYSTEM_USERID
+ if userid not in RgwClient._user_instances:
+ logger.info("Creating new connection for user: %s", userid)
+ keys = RgwClient.admin_instance().get_user_keys(userid)
+ if not keys:
+ raise Exception(
+ "User '{}' does not have any keys configured.".format(
+ userid))
+
+ RgwClient._user_instances[userid] = RgwClient(
+ userid, keys['access_key'], keys['secret_key'])
+ return RgwClient._user_instances[userid]
+
+ @staticmethod
+ def admin_instance():
+ return RgwClient.instance(RgwClient._SYSTEM_USERID)
+
+ def _reset_login(self):
+ if self.userid != RgwClient._SYSTEM_USERID:
+ logger.info("Fetching new keys for user: %s", self.userid)
+ keys = RgwClient.admin_instance().get_user_keys(self.userid)
+ self.auth = S3Auth(keys['access_key'], keys['secret_key'],
+ service_url=self.service_url)
+ else:
+ raise RequestException('Authentication failed for the "{}" user: wrong credentials'
+ .format(self.userid), status_code=401)
+
+ def __init__(self, # pylint: disable-msg=R0913
+ userid,
+ access_key,
+ secret_key,
+ host=None,
+ port=None,
+ admin_path='admin',
+ ssl=False):
+
+ if not host and not RgwClient._host:
+ RgwClient._load_settings()
+ host = host if host else RgwClient._host
+ port = port if port else RgwClient._port
+ admin_path = admin_path if admin_path else RgwClient._ADMIN_PATH
+ ssl = ssl if ssl else RgwClient._ssl
+
+ self.userid = userid
+ self.service_url = build_url(host=host, port=port)
+ self.admin_path = admin_path
+
+ s3auth = S3Auth(access_key, secret_key, service_url=self.service_url)
+ super(RgwClient, self).__init__(host, port, 'RGW', ssl, s3auth)
+
+ logger.info("Creating new connection")
+
+ @RestClient.api_get('/', resp_structure='[0] > ID')
+ def is_service_online(self, request=None):
+ response = request({'format': 'json'})
+ return response[0]['ID'] == 'online'
+
+ @RestClient.api_get('/{admin_path}/metadata/user', resp_structure='[+]')
+ def _is_system_user(self, admin_path, request=None):
+ # pylint: disable=unused-argument
+ response = request()
+ return self.userid in response
+
+ def is_system_user(self):
+ return self._is_system_user(self.admin_path)
+
+ @RestClient.api_get(
+ '/{admin_path}/user',
+ resp_structure='tenant & user_id & email & keys[*] > '
+ ' (user & access_key & secret_key)')
+ def _admin_get_user_keys(self, admin_path, userid, request=None):
+ # pylint: disable=unused-argument
+ colon_idx = userid.find(':')
+ user = userid if colon_idx == -1 else userid[:colon_idx]
+ response = request({'uid': user})
+ for keys in response['keys']:
+ if keys['user'] == userid:
+ return {
+ 'access_key': keys['access_key'],
+ 'secret_key': keys['secret_key']
+ }
+ return None
+
+ def get_user_keys(self, userid):
+ return self._admin_get_user_keys(self.admin_path, userid)
+
+ @RestClient.api('/{admin_path}/{path}')
+ def _proxy_request(self, # pylint: disable=too-many-arguments
+ admin_path,
+ path,
+ method,
+ params,
+ data,
+ request=None):
+ # pylint: disable=unused-argument
+ return request(
+ method=method, params=params, data=data, raw_content=True)
+
+ def proxy(self, method, path, params, data):
+ logger.debug("proxying method=%s path=%s params=%s data=%s", method,
+ path, params, data)
+ return self._proxy_request(self.admin_path, path, method, params, data)
+
+ @RestClient.api_get('/', resp_structure='[1][*] > Name')
+ def get_buckets(self, request=None):
+ """
+ Get a list of names from all existing buckets of this user.
+ :return: Returns a list of bucket names.
+ """
+ response = request({'format': 'json'})
+ return [bucket['Name'] for bucket in response[1]]
+
+ @RestClient.api_get('/{bucket_name}')
+ def bucket_exists(self, bucket_name, userid, request=None):
+ """
+ Check if the specified bucket exists for this user.
+ :param bucket_name: The name of the bucket.
+ :return: Returns True if the bucket exists, otherwise False.
+ """
+ # pylint: disable=unused-argument
+ try:
+ request()
+ my_buckets = self.get_buckets()
+ if bucket_name not in my_buckets:
+ raise RequestException(
+ 'Bucket "{}" belongs to other user'.format(bucket_name),
+ 403)
+ return True
+ except RequestException as e:
+ if e.status_code == 404:
+ return False
+ else:
+ raise e
+
+ @RestClient.api_put('/{bucket_name}')
+ def create_bucket(self, bucket_name, request=None):
+ logger.info("Creating bucket: %s", bucket_name)
+ return request()
"""
ENABLE_BROWSABLE_API = (True, bool)
+ # RGW settings
+ RGW_API_HOST = ('', str)
+ RGW_API_PORT = (80, int)
+ RGW_API_ACCESS_KEY = ('', str)
+ RGW_API_SECRET_KEY = ('', str)
+ RGW_API_ADMIN_RESOURCE = ('admin', str)
+ RGW_API_SCHEME = ('http', str)
+ RGW_API_USER_ID = ('', str)
+
+ @staticmethod
+ def has_default_value(name):
+ return getattr(Settings, name, None) is None or \
+ getattr(Settings, name) == getattr(Options, name)[0]
+
class SettingsMeta(type):
def __getattr__(cls, attr):
# -*- coding: utf-8 -*-
from __future__ import absolute_import
+import unittest
+
from cherrypy.lib.sessions import RamSession
from mock import patch
from .helper import ControllerTestCase
from ..controllers import RESTController, ApiController
+from ..tools import is_valid_ipv6_address, dict_contains_path
# pylint: disable=W0613
headers=[('Accept', 'text/html'), ('Content-Length', '0')],
method='put')
self.assertStatus(404)
+
+
+class TestFunctions(unittest.TestCase):
+
+ def test_is_valid_ipv6_address(self):
+ self.assertTrue(is_valid_ipv6_address('::'))
+ self.assertTrue(is_valid_ipv6_address('::1'))
+ self.assertFalse(is_valid_ipv6_address('127.0.0.1'))
+ self.assertFalse(is_valid_ipv6_address('localhost'))
+ self.assertTrue(is_valid_ipv6_address('1200:0000:AB00:1234:0000:2552:7777:1313'))
+ self.assertFalse(is_valid_ipv6_address('1200::AB00:1234::2552:7777:1313'))
+
+ def test_dict_contains_path(self):
+ x = {'a': {'b': {'c': 'foo'}}}
+ self.assertTrue(dict_contains_path(x, ['a', 'b', 'c']))
+ self.assertTrue(dict_contains_path(x, ['a', 'b', 'c']))
+ self.assertTrue(dict_contains_path(x, ['a']))
+ self.assertFalse(dict_contains_path(x, ['a', 'c']))
+
+ self.assertTrue(dict_contains_path(x, []))
import fnmatch
import time
import threading
-
+import socket
import cherrypy
from . import logger
+from six.moves import urllib
class RequestLoggingTool(cherrypy.Tool):
self.progress = percentage
if not in_lock:
self.lock.release()
+
+
+def is_valid_ipv6_address(addr):
+ try:
+ socket.inet_pton(socket.AF_INET6, addr)
+ return True
+ except socket.error:
+ return False
+
+
+def build_url(host, scheme=None, port=None):
+ """
+ Build a valid URL. IPv6 addresses specified in host will be enclosed in brackets
+ automatically.
+
+ >>> build_url('example.com', 'https', 443)
+ 'https://example.com:443'
+
+ >>> build_url(host='example.com', port=443)
+ '//example.com:443'
+
+ >>> build_url('fce:9af7:a667:7286:4917:b8d3:34df:8373', port=80, scheme='http')
+ 'http://[fce:9af7:a667:7286:4917:b8d3:34df:8373]:80'
+
+ :param scheme: The scheme, e.g. http, https or ftp.
+ :type scheme: str
+ :param host: Consisting of either a registered name (including but not limited to
+ a hostname) or an IP address.
+ :type host: str
+ :type port: int
+ :rtype: str
+ """
+ netloc = host if not is_valid_ipv6_address(host) else '[{}]'.format(host)
+ if port:
+ netloc += ':{}'.format(port)
+ pr = urllib.parse.ParseResult(
+ scheme=scheme if scheme else '',
+ netloc=netloc,
+ path='',
+ params='',
+ query='',
+ fragment='')
+ return pr.geturl()
+
+
+def dict_contains_path(dct, keys):
+ """
+ Tests wheter the keys exist recursively in `dictionary`.
+
+ :type dct: dict
+ :type keys: list
+ :rtype: bool
+ """
+ if keys:
+ key = keys.pop(0)
+ if key in dct:
+ dct = dct[key]
+ return dict_contains_path(dct, keys)
+ return False
+ return True
LD_LIBRARY_PATH = {toxinidir}/../../../../build/lib
PATH = {toxinidir}/../../../../build/bin:$PATH
commands=
- {envbindir}/py.test --cov=. --cov-report= --junitxml=junit.{envname}.xml --doctest-modules controllers/rbd.py services/ tests/
+ {envbindir}/py.test --cov=. --cov-report= --junitxml=junit.{envname}.xml --doctest-modules controllers/rbd.py services/ tools.py tests/
[testenv:cov-init]
setenv =