Commit 0e52c1b3 authored by Ilias Tsitsimpis's avatar Ilias Tsitsimpis Committed by Giorgos Korfiatis

astakosclient: Complete redesign astakosclient

Accept auth_url as the default entry point and
query astakos endpoints to find out the
account and ui urls.
parent 7e36eeef
......@@ -31,6 +31,10 @@
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.
"""
Simple and minimal client for the Astakos authentication service
"""
import logging
import urlparse
import urllib
......@@ -39,45 +43,10 @@ from copy import copy
import simplejson
from astakosclient.utils import \
retry, scheme_to_class, parse_request, check_input
retry_dec, scheme_to_class, parse_request, check_input, join_urls
from astakosclient.errors import \
AstakosClientException, Unauthorized, BadRequest, NotFound, Forbidden, \
NoUserName, NoUUID, BadValue, QuotaLimit, InvalidResponse
from .keypath import get_path
from .services import astakos_services
# Customize astakos_services here?
def join_urls(a, b):
"""join_urls from synnefo.lib"""
return a.rstrip("/") + "/" + b.lstrip("/")
# --------------------------------------------------------------------
# Astakos API urls
UI_PREFIX = get_path(astakos_services, 'astakos_ui.prefix')
ACCOUNTS_PREFIX = get_path(astakos_services, 'astakos_account.prefix')
ACCOUNTS_PREFIX = join_urls(ACCOUNTS_PREFIX, 'v1.0')
API_AUTHENTICATE = join_urls(ACCOUNTS_PREFIX, "authenticate")
API_USERCATALOGS = join_urls(ACCOUNTS_PREFIX, "user_catalogs")
API_SERVICE_USERCATALOGS = join_urls(ACCOUNTS_PREFIX, "service/user_catalogs")
API_GETSERVICES = join_urls(UI_PREFIX, "get_services")
API_RESOURCES = join_urls(ACCOUNTS_PREFIX, "resources")
API_QUOTAS = join_urls(ACCOUNTS_PREFIX, "quotas")
API_SERVICE_QUOTAS = join_urls(ACCOUNTS_PREFIX, "service_quotas")
API_COMMISSIONS = join_urls(ACCOUNTS_PREFIX, "commissions")
API_COMMISSIONS_ACTION = join_urls(API_COMMISSIONS, "action")
API_FEEDBACK = join_urls(ACCOUNTS_PREFIX, "feedback")
API_PROJECTS = join_urls(ACCOUNTS_PREFIX, "projects")
API_APPLICATIONS = join_urls(API_PROJECTS, "apps")
API_MEMBERSHIPS = join_urls(API_PROJECTS, "memberships")
# --------------------------------------------------------------------
# Astakos Keystone API urls
IDENTITY_PREFIX = get_path(astakos_services, 'astakos_identity.prefix')
IDENTITY_PREFIX = join_urls(IDENTITY_PREFIX, "v2.0")
API_TOKENS = join_urls(IDENTITY_PREFIX, "tokens")
NoUserName, NoUUID, BadValue, QuotaLimit, InvalidResponse, NoEndpoints
# --------------------------------------------------------------------
......@@ -94,80 +63,149 @@ def get_token_from_cookie(request, cookie_name):
try:
cookie_content = urllib.unquote(request.COOKIE.get(cookie_name, None))
return cookie_content.split("|")[1]
except:
except BaseException:
return None
class AstakosClient():
# Too many instance attributes. pylint: disable-msg=R0902
# Too many public methods. pylint: disable-msg=R0904
class AstakosClient(object):
"""AstakosClient Class Implementation"""
# ----------------------------------
def __init__(self, astakos_url, retry=0,
use_pool=False, pool_size=8, logger=None):
# Initialize AstakosClient Class
# Too many arguments. pylint: disable-msg=R0913
# Too many local variables. pylint: disable-msg=R0914
# Too many statements. pylint: disable-msg=R0915
def __init__(self, token, auth_url,
retry=0, use_pool=False, pool_size=8, logger=None):
"""Initialize AstakosClient Class
Keyword arguments:
astakos_url -- i.e https://accounts.example.com (string)
use_pool -- use objpool for http requests (boolean)
token -- user's/service's token (string)
auth_url -- i.e https://accounts.example.com/identity/v2.0
retry -- how many time to retry (integer)
use_pool -- use objpool for http requests (boolean)
pool_size -- if using pool, define the pool size
logger -- pass a different logger
"""
# Get logger
if logger is None:
logging.basicConfig(
format='%(asctime)s [%(levelname)s] %(name)s %(message)s',
datefmt='%Y-%m-%d %H:%M:%S',
level=logging.INFO)
logger = logging.getLogger("astakosclient")
logger.debug("Intialize AstakosClient: astakos_url = %s, "
"use_pool = %s" % (astakos_url, use_pool))
logger.debug("Intialize AstakosClient: auth_url = %s, "
"use_pool = %s, pool_size = %s",
auth_url, use_pool, pool_size)
check_input("__init__", logger, astakos_url=astakos_url)
# Check that token and auth_url (mandatory options) are given
check_input("__init__", logger, token=token, auth_url=auth_url)
# Check for supported scheme
p = urlparse.urlparse(astakos_url)
conn_class = scheme_to_class(p.scheme, use_pool, pool_size)
# Initialize connection class
parsed_auth_url = urlparse.urlparse(auth_url)
conn_class = \
scheme_to_class(parsed_auth_url.scheme, use_pool, pool_size)
if conn_class is None:
m = "Unsupported scheme: %s" % p.scheme
logger.error(m)
raise BadValue(m)
msg = "Unsupported scheme: %s" % parsed_auth_url.scheme
logger.error(msg)
raise BadValue(msg)
# Save astakos_url etc. in our class
# Save astakos base url, logger, connection class etc in our class
self.retry = retry
self.logger = logger
self.netloc = p.netloc
self.scheme = p.scheme
self.path = p.path.rstrip('/')
self.token = token
self.astakos_base_url = parsed_auth_url.netloc
self.scheme = parsed_auth_url.scheme
self.conn_class = conn_class
# Initialize astakos api prefixes
# API urls under auth_url
self.auth_prefix = parsed_auth_url.path
self.api_tokens = join_urls(self.auth_prefix, "tokens")
# ------------------------------
# API urls under account_url
# Get account_url from get_endpoints
# get_endpoints needs self.api_tokens
endpoints = self.get_endpoints(non_authentication=True)
account_service_catalog = parse_endpoints(
endpoints, ep_name="astakos_account", ep_version_id="v1.0")
self.account_url = \
account_service_catalog[0]['endpoints'][0]['publicURL']
parsed_account_url = urlparse.urlparse(self.account_url)
self.account_prefix = parsed_account_url.path
self.logger.debug("Got account_prefix \"%s\"" % self.account_prefix)
self.api_authenticate = join_urls(
self.account_prefix, "authenticate")
self.api_usercatalogs = join_urls(
self.account_prefix, "user_catalogs")
self.api_service_usercatalogs = join_urls(
self.account_prefix, "service/user_catalogs")
self.api_resources = join_urls(
self.account_prefix, "resources")
self.api_quotas = join_urls(
self.account_prefix, "quotas")
self.api_service_quotas = join_urls(
self.account_prefix, "service_quotas")
self.api_commissions = join_urls(
self.account_prefix, "commissions")
self.api_commissions_action = join_urls(
self.api_commissions, "action")
self.api_feedback = join_urls(
self.account_prefix, "feedback")
self.api_projects = join_urls(
self.account_prefix, "projects")
self.api_applications = join_urls(
self.api_projects, "apps")
self.api_memberships = join_urls(
self.api_projects, "memberships")
# ------------------------------
# API urls under ui_url
# Get ui url from get_endpoints
# get_endpoints needs self.api_tokens
ui_service_catalog = parse_endpoints(
endpoints, ep_name="astakos_account", ep_version_id="v1.0")
parsed_ui_url = urlparse.urlparse(
ui_service_catalog[0]['endpoints'][0]['SNF:uiURL'])
self.ui_url = \
ui_service_catalog[0]['endpoints'][0]['SNF:uiURL']
parsed_ui_url = urlparse.urlparse(self.ui_url)
self.ui_prefix = parsed_ui_url.path
self.logger.debug("Got ui_prefix \"%s\"" % self.ui_prefix)
self.api_getservices = join_urls(self.ui_prefix, "get_services")
# ----------------------------------
@retry
def _call_astakos(self, token, request_path, headers=None,
@retry_dec
def _call_astakos(self, request_path, headers=None,
body=None, method="GET", log_body=True):
"""Make the actual call to Astakos Service"""
if token is not None:
hashed_token = hashlib.sha1()
hashed_token.update(token)
using_token = "using token %s" % (hashed_token.hexdigest())
else:
using_token = "without using token"
hashed_token = hashlib.sha1()
hashed_token.update(self.token)
self.logger.debug(
"Make a %s request to %s %s with headers %s and body %s"
% (method, request_path, using_token, headers,
body if log_body else "(not logged)"))
"Make a %s request to %s, using token with hash %s, "
"with headers %s and body %s",
method, request_path, hashed_token.hexdigest(), headers,
body if log_body else "(not logged)")
# Check Input
if headers is None:
headers = {}
if body is None:
body = {}
path = self.path + "/" + request_path.strip('/')
# Build request's header and body
kwargs = {}
kwargs['headers'] = copy(headers)
if token is not None:
kwargs['headers']['X-Auth-Token'] = token
kwargs['headers']['X-Auth-Token'] = self.token
if body:
kwargs['body'] = copy(body)
kwargs['headers'].setdefault(
......@@ -177,10 +215,11 @@ class AstakosClient():
try:
# Get the connection object
with self.conn_class(self.netloc) as conn:
with self.conn_class(self.astakos_base_url) as conn:
# Send request
# Used * or ** magic. pylint: disable-msg=W0142
(message, data, status) = \
_do_request(conn, method, path, **kwargs)
_do_request(conn, method, request_path, **kwargs)
except Exception as err:
self.logger.error("Failed to send request: %s" % repr(err))
raise AstakosClientException(str(err))
......@@ -210,11 +249,10 @@ class AstakosClient():
# ------------------------
# do a GET to ``API_AUTHENTICATE``
def get_user_info(self, token, usage=False):
def get_user_info(self, usage=False):
"""Authenticate user and get user's info as a dictionary
Keyword arguments:
token -- user's token (string)
usage -- return usage information for user (boolean)
In case of success return user information (json parsed format).
......@@ -222,59 +260,57 @@ class AstakosClient():
"""
# Send request
auth_path = copy(API_AUTHENTICATE)
auth_path = self.api_authenticate
if usage:
auth_path += "?usage=1"
return self._call_astakos(token, auth_path)
return self._call_astakos(auth_path)
# ----------------------------------
# do a POST to ``API_USERCATALOGS`` (or ``API_SERVICE_USERCATALOGS``)
# with {'uuids': uuids}
def _uuid_catalog(self, token, uuids, req_path):
def _uuid_catalog(self, uuids, req_path):
"""Helper function to retrieve uuid catalog"""
req_headers = {'content-type': 'application/json'}
req_body = parse_request({'uuids': uuids}, self.logger)
data = self._call_astakos(
token, req_path, req_headers, req_body, "POST")
data = self._call_astakos(req_path, headers=req_headers,
body=req_body, method="POST")
if "uuid_catalog" in data:
return data.get("uuid_catalog")
else:
m = "_uuid_catalog request returned %s. No uuid_catalog found" \
% data
self.logger.error(m)
raise AstakosClientException(m)
msg = "_uuid_catalog request returned %s. No uuid_catalog found" \
% data
self.logger.error(msg)
raise AstakosClientException(msg)
def get_usernames(self, token, uuids):
def get_usernames(self, uuids):
"""Return a uuid_catalog dictionary for the given uuids
Keyword arguments:
token -- user's token (string)
uuids -- list of user ids (list of strings)
The returned uuid_catalog is a dictionary with uuids as
keys and the corresponding user names as values
"""
req_path = copy(API_USERCATALOGS)
return self._uuid_catalog(token, uuids, req_path)
return self._uuid_catalog(uuids, self.api_usercatalogs)
def get_username(self, token, uuid):
def get_username(self, uuid):
"""Return the user name of a uuid (see get_usernames)"""
check_input("get_username", self.logger, uuid=uuid)
uuid_dict = self.get_usernames(token, [uuid])
uuid_dict = self.get_usernames([uuid])
if uuid in uuid_dict:
return uuid_dict.get(uuid)
else:
raise NoUserName(uuid)
def service_get_usernames(self, token, uuids):
def service_get_usernames(self, uuids):
"""Return a uuid_catalog dict using a service's token"""
req_path = copy(API_SERVICE_USERCATALOGS)
return self._uuid_catalog(token, uuids, req_path)
return self._uuid_catalog(uuids, self.api_service_usercatalogs)
def service_get_username(self, token, uuid):
def service_get_username(self, uuid):
"""Return the displayName of a uuid using a service's token"""
check_input("service_get_username", self.logger, uuid=uuid)
uuid_dict = self.service_get_usernames(token, [uuid])
uuid_dict = self.service_get_usernames([uuid])
if uuid in uuid_dict:
return uuid_dict.get(uuid)
else:
......@@ -283,51 +319,51 @@ class AstakosClient():
# ----------------------------------
# do a POST to ``API_USERCATALOGS`` (or ``API_SERVICE_USERCATALOGS``)
# with {'displaynames': display_names}
def _displayname_catalog(self, token, display_names, req_path):
def _displayname_catalog(self, display_names, req_path):
"""Helper function to retrieve display names catalog"""
req_headers = {'content-type': 'application/json'}
req_body = parse_request({'displaynames': display_names}, self.logger)
data = self._call_astakos(
token, req_path, req_headers, req_body, "POST")
data = self._call_astakos(req_path, headers=req_headers,
body=req_body, method="POST")
if "displayname_catalog" in data:
return data.get("displayname_catalog")
else:
m = "_displayname_catalog request returned %s. " \
"No displayname_catalog found" % data
self.logger.error(m)
raise AstakosClientException(m)
msg = "_displayname_catalog request returned %s. " \
"No displayname_catalog found" % data
self.logger.error(msg)
raise AstakosClientException(msg)
def get_uuids(self, token, display_names):
def get_uuids(self, display_names):
"""Return a displayname_catalog for the given names
Keyword arguments:
token -- user's token (string)
display_names -- list of user names (list of strings)
The returned displayname_catalog is a dictionary with
the names as keys and the corresponding uuids as values
"""
req_path = copy(API_USERCATALOGS)
return self._displayname_catalog(token, display_names, req_path)
return self._displayname_catalog(
display_names, self.api_usercatalogs)
def get_uuid(self, token, display_name):
def get_uuid(self, display_name):
"""Return the uuid of a name (see getUUIDs)"""
check_input("get_uuid", self.logger, display_name=display_name)
name_dict = self.get_uuids(token, [display_name])
name_dict = self.get_uuids([display_name])
if display_name in name_dict:
return name_dict.get(display_name)
else:
raise NoUUID(display_name)
def service_get_uuids(self, token, display_names):
def service_get_uuids(self, display_names):
"""Return a display_name catalog using a service's token"""
req_path = copy(API_SERVICE_USERCATALOGS)
return self._displayname_catalog(token, display_names, req_path)
return self._displayname_catalog(
display_names, self.api_service_usercatalogs)
def service_get_uuid(self, token, display_name):
def service_get_uuid(self, display_name):
"""Return the uuid of a name using a service's token"""
check_input("service_get_uuid", self.logger, display_name=display_name)
name_dict = self.service_get_uuids(token, [display_name])
name_dict = self.service_get_uuids([display_name])
if display_name in name_dict:
return name_dict.get(display_name)
else:
......@@ -337,21 +373,20 @@ class AstakosClient():
# do a GET to ``API_GETSERVICES``
def get_services(self):
"""Return a list of dicts with the registered services"""
return self._call_astakos(None, copy(API_GETSERVICES))
return self._call_astakos(self.api_getservices)
# ----------------------------------
# do a GET to ``API_RESOURCES``
def get_resources(self):
"""Return a dict of dicts with the available resources"""
return self._call_astakos(None, copy(API_RESOURCES))
return self._call_astakos(self.api_resources)
# ----------------------------------
# do a POST to ``API_FEEDBACK``
def send_feedback(self, token, message, data):
def send_feedback(self, message, data):
"""Send feedback to astakos service
keyword arguments:
token -- user's token (string)
message -- Feedback message
data -- Additional information about service client status
......@@ -360,55 +395,63 @@ class AstakosClient():
"""
check_input("send_feedback", self.logger, message=message, data=data)
path = copy(API_FEEDBACK)
req_body = urllib.urlencode(
{'feedback_msg': message, 'feedback_data': data})
self._call_astakos(token, path, None, req_body, "POST")
self._call_astakos(self.api_feedback, headers=None,
body=req_body, method="POST")
# ----------------------------------
# do a POST to ``API_TOKENS``
def get_endpoints(self, token, uuid=None):
""" Fallback call for authenticate
def get_endpoints(self, tenant_name=None, non_authentication=False):
""" Authenticate and get services' endpoints
Keyword arguments:
token -- user's token (string)
uuid -- user's uniq id
tenant_name -- user's uniq id (optional)
non_authentication -- get only non authentication protected info
It returns back the token as well as information about the token
holder and the services he/she can acess (in json format).
The tenant_name is optional and if it is given it must match the
user's uuid.
In case on of the `name', `type', `region', `version_id' parameters
is given, return only the endpoints that match all of these criteria.
If no match is found then raise NoEndpoints exception.
In case of error raise an AstakosClientException.
"""
req_path = copy(API_TOKENS)
req_headers = {'content-type': 'application/json'}
body = {'auth': {'token': {'id': token}}}
if uuid is not None:
body['auth']['tenantName'] = uuid
req_body = parse_request(body, self.logger)
return self._call_astakos(token, req_path, req_headers,
req_body, "POST", False)
if non_authentication:
req_body = None
else:
body = {'auth': {'token': {'id': self.token}}}
if tenant_name is not None:
body['auth']['tenantName'] = tenant_name
req_body = parse_request(body, self.logger)
return self._call_astakos(self.api_tokens, headers=req_headers,
body=req_body, method="POST",
log_body=False)
# ----------------------------------
# do a GET to ``API_QUOTAS``
def get_quotas(self, token):
def get_quotas(self):
"""Get user's quotas
Keyword arguments:
token -- user's token (string)
In case of success return a dict of dicts with user's current quotas.
Otherwise raise an AstakosClientException
"""
return self._call_astakos(token, copy(API_QUOTAS))
return self._call_astakos(self.api_quotas)
# ----------------------------------
# do a GET to ``API_SERVICE_QUOTAS``
def service_get_quotas(self, token, user=None):
def service_get_quotas(self, user=None):
"""Get all quotas for resources associated with the service
Keyword arguments:
token -- service's token (string)
user -- optionally, the uuid of a specific user
In case of success return a dict of dicts of dicts with current quotas
......@@ -416,18 +459,17 @@ class AstakosClient():
Otherwise raise an AstakosClientException
"""
query = copy(API_SERVICE_QUOTAS)
query = self.api_service_quotas
if user is not None:
query += "?user=" + user
return self._call_astakos(token, query)
return self._call_astakos(query)
# ----------------------------------
# do a POST to ``API_COMMISSIONS``
def issue_commission(self, token, request):
def issue_commission(self, request):
"""Issue a commission
Keyword arguments:
token -- service's token (string)
request -- commision request (dict)
In case of success return commission's id (int).
......@@ -437,8 +479,10 @@ class AstakosClient():
req_headers = {'content-type': 'application/json'}
req_body = parse_request(request, self.logger)
try:
response = self._call_astakos(token, copy(API_COMMISSIONS),
req_headers, req_body, "POST")
response = self._call_astakos(self.api_commissions,
headers=req_headers,
body=req_body,
method="POST")
except AstakosClientException as err:
if err.status == 413:
raise QuotaLimit(err.message, err.details)
......@@ -448,17 +492,16 @@ class AstakosClient():
if "serial" in response:
return response['serial']
else:
m = "issue_commission_core request returned %s. No serial found" \
% response
self.logger.error(m)
raise AstakosClientException(m)
msg = "issue_commission_core request returned %s. " + \
"No serial found" % response
self.logger.error(msg)
raise AstakosClientException(msg)
def issue_one_commission(self, token, holder, source, provisions,
def issue_one_commission(self, holder, source, provisions,
name="", force=False, auto_accept=False):
"""Issue one commission (with specific holder and source)
keyword arguments:
token -- service's token (string)
holder -- user's id (string)
source -- commission's source (ex system) (string)
provisions -- resources with their quantity (dict from string to int)
......@@ -482,36 +525,32 @@ class AstakosClient():
try:
request["provisions"] = []
for resource, quantity in provisions.iteritems():
t = {"holder": holder, "source": source,
"resource": resource, "quantity": quantity}
request["provisions"].append(t)
prov = {"holder": holder, "source": source,
"resource": resource, "quantity": quantity}
request["provisions"].append(prov)
except Exception as err:
self.logger.error(str(err))
raise BadValue(str(err))
return self.issue_commission(token, request)
return self.issue_commission(request)
# ----------------------------------
# do a GET to ``API_COMMISSIONS``
def get_pending_commissions(self, token):
def get_pending_commissions(self):
"""Get Pending Commissions
Keyword arguments:
token -- service's token (string)
In case of success return a list of pending commissions' ids
(list of integers)
"""
return self._call_astakos(token, copy(API_COMMISSIONS))
return self._call_astakos(self.api_commissions)
# ----------------------------------
# do a GET to ``API_COMMISSIONS``/<serial>
def get_commission_info(self, token, serial):
def get_commission_info(self, serial):
"""Get Description of a Commission
Keyword arguments:
token -- service's token (string)
serial -- commission's id (int)
In case of success return a dict of dicts containing
......@@ -520,16 +559,15 @@ class AstakosClient():
"""
check_input("get_commission_info", self.logger, serial=serial)
path = API_COMMISSIONS + "/" + str(serial)
return self._call_astakos(token, path)
path = self.api_commissions.rstrip('/') + "/" + str(serial)
return self._call_astakos(path)
# ----------------------------------
# do a POST to ``API_COMMISSIONS``/<serial>/action"
def commission_action(self, token, serial, action):
def commission_action(self, serial, action):
"""Perform a commission action
Keyword arguments:
token -- service's token (string)
serial -- commission's id (int)
action -- action to perform, currently accept/reject (string)
......@@ -539,26 +577,26 @@ class AstakosClient():
check_input("commission_action", self.logger,
serial=serial, action=action)
path = API_COMMISSIONS + "/" + str(serial) + "/action"
path = self.api_commissions.rstrip('/') + "/" + str(serial) + "/action"
req_headers = {'content-type': 'application/json'}
req_body = parse_request({str(action): ""}, self.logger)
self._call_astakos(token, path, req_headers, req_body, "POST")
self._call_astakos(path, headers=req_headers,
body=req_body, method="POST")
def accept_commission(self, token, serial):
def accept_commission(self, serial):
"""Accept a commission (see commission_action)"""
self.commission_action(token, serial, "accept")
self.commission_action(serial, "accept")
def reject_commission(self, token, serial):
def reject_commission(self, serial):
"""Reject a commission (see commission_action)"""
self.commission_action(token, serial, "reject")
self.commission_action(serial, "reject")
# ----------------------------------
# do a POST to ``API_COMMISSIONS_ACTION``
def resolve_commissions(self, token, accept_serials, reject_serials):
def resolve_commissions(self, accept_serials, reject_serials):
"""Resolve multiple commissions at once
Keyword arguments:
token -- service's token (string)
accept_serials -- commissions to accept (list of ints)
reject_serials -- commissions to reject (list of ints)
......@@ -571,27 +609,26 @@ class AstakosClient():
accept_serials=accept_serials,
reject_serials=reject_serials)
path = copy(API_COMMISSIONS_ACTION)
req_headers = {'content-type': 'application/json'}
req_body = parse_request({"accept": accept_serials,
"reject": reject_serials},
self.logger)
return self._call_astakos(token, path, req_headers, req_body, "POST")
return self._call_astakos(self.api_commissions_action,
headers=req_headers, body=req_body,
method="POST")
# ----------------------------
# do a GET to ``API_PROJECTS``
def get_projects(self, token, name=None, state=None, owner=None):
def get_projects(self, name=None, state=None, owner=None):
"""Retrieve all accessible projects
Arguments:
token -- user's token (string)
name -- filter by name (optional)
state -- filter by state (optional)
owner -- filter by owner (optional)
In case of success, return a list of project descriptions.
"""
path = API_PROJECTS
filters = {}
if name is not None: