__init__.py 23 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
# Copyright (C) 2012, 2013 GRNET S.A. All rights reserved.
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
#
#   1. Redistributions of source code must retain the above
#      copyright notice, this list of conditions and the following
#      disclaimer.
#
#   2. Redistributions in binary form must reproduce the above
#      copyright notice, this list of conditions and the following
#      disclaimer in the documentation and/or other materials
#      provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# The views and conclusions contained in the software and
# documentation are those of the authors and should not be
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.
33 34 35

import logging
import urlparse
36
import urllib
37
import hashlib
38
from copy import copy
39 40

import simplejson
41 42
from astakosclient.utils import \
    retry, scheme_to_class, parse_request, check_input
43
from astakosclient.errors import \
44
    AstakosClientException, Unauthorized, BadRequest, NotFound, Forbidden, \
45
    NoUserName, NoUUID, BadValue, QuotaLimit, InvalidResponse
46 47


48 49 50 51
def join_urls(a, b):
    """join_urls from synnefo.lib"""
    return a.rstrip("/") + "/" + b.lstrip("/")

52 53
# --------------------------------------------------------------------
# Astakos API urls
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
ACCOUNTS_PREFIX = 'accounts'
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(ACCOUNTS_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")

# --------------------------------------------------------------------
# Astakos Keystone API urls
KEYSTONE_PREFIX = 'keystone'
API_TOKENS = join_urls(KEYSTONE_PREFIX, "tokens")
TOKENS_ENDPOINTS = join_urls(API_TOKENS, "endpoints")
71 72


73
# --------------------------------------------------------------------
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
74
# Astakos Client Class
75

76
def get_token_from_cookie(request, cookie_name):
77 78 79 80 81 82 83 84 85 86 87 88 89 90
    """Extract token from the cookie name provided

    Cookie should be in the same form as astakos
    service sets its cookie contents:
        <user_uniq>|<user_token>

    """
    try:
        cookie_content = urllib.unquote(request.COOKIE.get(cookie_name, None))
        return cookie_content.split("|")[1]
    except:
        return None


Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
91 92 93 94
class AstakosClient():
    """AstakosClient Class Implementation"""

    # ----------------------------------
95 96
    def __init__(self, astakos_url, retry=0,
                 use_pool=False, pool_size=8, logger=None):
97
        """Initialize AstakosClient Class
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
98 99 100 101

        Keyword arguments:
        astakos_url -- i.e https://accounts.example.com (string)
        use_pool    -- use objpool for http requests (boolean)
102
        retry       -- how many time to retry (integer)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
103 104 105 106
        logger      -- pass a different logger

        """
        if logger is None:
107 108 109 110
            logging.basicConfig(
                format='%(asctime)s [%(levelname)s] %(name)s %(message)s',
                datefmt='%Y-%m-%d %H:%M:%S',
                level=logging.INFO)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
111
            logger = logging.getLogger("astakosclient")
112
        logger.debug("Intialize AstakosClient: astakos_url = %s, "
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
113 114
                     "use_pool = %s" % (astakos_url, use_pool))

115
        check_input("__init__", logger, astakos_url=astakos_url)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
116 117 118

        # Check for supported scheme
        p = urlparse.urlparse(astakos_url)
119
        conn_class = scheme_to_class(p.scheme, use_pool, pool_size)
120
        if conn_class is None:
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
121 122
            m = "Unsupported scheme: %s" % p.scheme
            logger.error(m)
123
            raise BadValue(m)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
124

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
125
        # Save astakos_url etc. in our class
126
        self.retry = retry
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
127 128 129
        self.logger = logger
        self.netloc = p.netloc
        self.scheme = p.scheme
130
        self.path = p.path.rstrip('/')
131
        self.conn_class = conn_class
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
132

133 134
    # ----------------------------------
    @retry
135 136
    def _call_astakos(self, token, request_path, headers=None,
                      body=None, method="GET", log_body=True):
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
137
        """Make the actual call to Astakos Service"""
138 139 140 141 142 143
        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"
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
144
        self.logger.debug(
145
            "Make a %s request to %s %s with headers %s and body %s"
146 147
            % (method, request_path, using_token, headers,
               body if log_body else "(not logged)"))
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
148

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
149
        # Check Input
150 151 152 153
        if headers is None:
            headers = {}
        if body is None:
            body = {}
154
        path = self.path + "/" + request_path.strip('/')
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
155

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
156 157
        # Build request's header and body
        kwargs = {}
158
        kwargs['headers'] = copy(headers)
159 160
        if token is not None:
            kwargs['headers']['X-Auth-Token'] = token
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
161
        if body:
162
            kwargs['body'] = copy(body)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
163 164 165 166 167 168
            kwargs['headers'].setdefault(
                'content-type', 'application/octet-stream')
        kwargs['headers'].setdefault('content-length',
                                     len(body) if body else 0)

        try:
169 170 171
            # Get the connection object
            with self.conn_class(self.netloc) as conn:
                # Send request
172
                (message, data, status) = \
173
                    _do_request(conn, method, path, **kwargs)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
174
        except Exception as err:
175
            self.logger.error("Failed to send request: %s" % repr(err))
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
176 177 178 179
            raise AstakosClientException(str(err))

        # Return
        self.logger.debug("Request returned with status %s" % status)
180
        if status == 400:
181
            raise BadRequest(message, data)
182
        elif status == 401:
183
            raise Unauthorized(message, data)
184
        elif status == 403:
185
            raise Forbidden(message, data)
186
        elif status == 404:
187
            raise NotFound(message, data)
188
        elif status < 200 or status >= 300:
189
            raise AstakosClientException(message, data, status)
190 191 192 193 194

        try:
            if data:
                return simplejson.loads(unicode(data))
            else:
195
                return None
196
        except Exception as err:
197 198
            self.logger.error("Cannot parse response \"%s\" with simplejson: %s"
                              % (data, str(err)))
199
            raise InvalidResponse(str(err), data)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
200 201

    # ------------------------
202
    # do a GET to ``API_AUTHENTICATE``
203
    def get_user_info(self, token, usage=False):
204
        """Authenticate user and get user's info as a dictionary
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
205 206

        Keyword arguments:
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
207
        token   -- user's token (string)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
208 209 210 211 212 213
        usage   -- return usage information for user (boolean)

        In case of success return user information (json parsed format).
        Otherwise raise an AstakosClientException.

        """
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
214
        # Send request
215
        auth_path = copy(API_AUTHENTICATE)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
216 217
        if usage:
            auth_path += "?usage=1"
218
        return self._call_astakos(token, auth_path)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
219 220

    # ----------------------------------
221
    # do a POST to ``API_USERCATALOGS`` (or ``API_SERVICE_USERCATALOGS``)
222
    #   with {'uuids': uuids}
223
    def _uuid_catalog(self, token, uuids, req_path):
224
        req_headers = {'content-type': 'application/json'}
225
        req_body = parse_request({'uuids': uuids}, self.logger)
226
        data = self._call_astakos(
227
            token, req_path, req_headers, req_body, "POST")
228 229 230
        if "uuid_catalog" in data:
            return data.get("uuid_catalog")
        else:
231
            m = "_uuid_catalog request returned %s. No uuid_catalog found" \
232 233 234
                % data
            self.logger.error(m)
            raise AstakosClientException(m)
235

236
    def get_usernames(self, token, uuids):
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
237 238 239
        """Return a uuid_catalog dictionary for the given uuids

        Keyword arguments:
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
240
        token   -- user's token (string)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
241 242 243 244 245 246
        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

        """
247
        req_path = copy(API_USERCATALOGS)
248
        return self._uuid_catalog(token, uuids, req_path)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
249

250 251
    def get_username(self, token, uuid):
        """Return the user name of a uuid (see get_usernames)"""
252
        check_input("get_username", self.logger, uuid=uuid)
253
        uuid_dict = self.get_usernames(token, [uuid])
254 255 256
        if uuid in uuid_dict:
            return uuid_dict.get(uuid)
        else:
257
            raise NoUserName(uuid)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
258

259
    def service_get_usernames(self, token, uuids):
260
        """Return a uuid_catalog dict using a service's token"""
261
        req_path = copy(API_SERVICE_USERCATALOGS)
262
        return self._uuid_catalog(token, uuids, req_path)
263

264
    def service_get_username(self, token, uuid):
265
        """Return the displayName of a uuid using a service's token"""
266
        check_input("service_get_username", self.logger, uuid=uuid)
267
        uuid_dict = self.service_get_usernames(token, [uuid])
268 269 270
        if uuid in uuid_dict:
            return uuid_dict.get(uuid)
        else:
271
            raise NoUserName(uuid)
272

273
    # ----------------------------------
274
    # do a POST to ``API_USERCATALOGS`` (or ``API_SERVICE_USERCATALOGS``)
275
    #   with {'displaynames': display_names}
276
    def _displayname_catalog(self, token, display_names, req_path):
277
        req_headers = {'content-type': 'application/json'}
278
        req_body = parse_request({'displaynames': display_names}, self.logger)
279
        data = self._call_astakos(
280
            token, req_path, req_headers, req_body, "POST")
281 282 283
        if "displayname_catalog" in data:
            return data.get("displayname_catalog")
        else:
284
            m = "_displayname_catalog request returned %s. " \
285 286 287
                "No displayname_catalog found" % data
            self.logger.error(m)
            raise AstakosClientException(m)
288

289
    def get_uuids(self, token, display_names):
290 291 292 293 294 295 296 297 298 299
        """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

        """
300
        req_path = copy(API_USERCATALOGS)
301
        return self._displayname_catalog(token, display_names, req_path)
302

303
    def get_uuid(self, token, display_name):
304
        """Return the uuid of a name (see getUUIDs)"""
305
        check_input("get_uuid", self.logger, display_name=display_name)
306
        name_dict = self.get_uuids(token, [display_name])
307 308 309 310
        if display_name in name_dict:
            return name_dict.get(display_name)
        else:
            raise NoUUID(display_name)
311

312
    def service_get_uuids(self, token, display_names):
313
        """Return a display_name catalog using a service's token"""
314
        req_path = copy(API_SERVICE_USERCATALOGS)
315
        return self._displayname_catalog(token, display_names, req_path)
316

317
    def service_get_uuid(self, token, display_name):
318
        """Return the uuid of a name using a service's token"""
319
        check_input("service_get_uuid", self.logger, display_name=display_name)
320
        name_dict = self.service_get_uuids(token, [display_name])
321 322 323 324
        if display_name in name_dict:
            return name_dict.get(display_name)
        else:
            raise NoUUID(display_name)
325

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
326
    # ----------------------------------
327
    # do a GET to ``API_GETSERVICES``
328
    def get_services(self):
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
329
        """Return a list of dicts with the registered services"""
330
        return self._call_astakos(None, copy(API_GETSERVICES))
331 332

    # ----------------------------------
333
    # do a GET to ``API_RESOURCES``
334 335
    def get_resources(self):
        """Return a dict of dicts with the available resources"""
336
        return self._call_astakos(None, copy(API_RESOURCES))
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
337

338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355
    # ----------------------------------
    # do a POST to ``API_FEEDBACK``
    def send_feedback(self, token, message, data):
        """Send feedback to astakos service

        keyword arguments:
        token       -- user's token (string)
        message     -- Feedback message
        data        -- Additional information about service client status

        In case of success return nothing.
        Otherwise raise an AstakosClientException

        """
        check_input("send_feedback", self.logger, message=message, data=data)
        path = copy(API_FEEDBACK)
        req_body = urllib.urlencode(
            {'feedback_msg': message, 'feedback_data': data})
356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374
        self._call_astakos(token, path, None, req_body, "POST")

    # ----------------------------------
    # do a GET to ``API_TOKENS``/<user_token>/``TOKENS_ENDPOINTS``
    def get_endpoints(self, token, belongs_to=None, marker=None, limit=None):
        """Request registered endpoints from astakos

        keyword arguments:
        token       -- user's token (string)
        belongs_to  -- user's uuid (string)
        marker      -- return endpoints whose ID is higher than marker's (int)
        limit       -- maximum number of endpoints to return (int)

        Return a json formatted dictionary containing information
        about registered endpoints.

        WARNING: This api call encodes the user's token inside the url.
        It's thoughs security unsafe to use it (both astakosclient and
        nginx tend to log requested urls).
375 376
        Avoid the use of get_endpoints method and use
        get_user_info_with_endpoints instead.
377 378 379 380 381 382 383 384 385 386 387 388

        """
        params = {}
        if belongs_to is not None:
            params['belongsTo'] = str(belongs_to)
        if marker is not None:
            params['marker'] = str(marker)
        if limit is not None:
            params['limit'] = str(limit)
        path = API_TOKENS + "/" + token + "/" + \
            TOKENS_ENDPOINTS + "?" + urllib.urlencode(params)
        return self._call_astakos(token, path)
389

390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412
    # ----------------------------------
    # do a POST to ``API_TOKENS``
    def get_user_info_with_endpoints(self, token, uuid=None):
        """ Fallback call for authenticate

        Keyword arguments:
        token   -- user's token (string)
        uuid    -- user's uniq id

        It returns back the token as well as information about the token
        holder and the services he/she can acess (in json format).
        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)

413
    # ----------------------------------
414
    # do a GET to ``API_QUOTAS``
415
    def get_quotas(self, token):
416 417 418 419 420 421 422 423 424
        """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

        """
425
        return self._call_astakos(token, copy(API_QUOTAS))
426

427
    # ----------------------------------
428
    # do a GET to ``API_SERVICE_QUOTAS``
429
    def service_get_quotas(self, token, user=None):
430 431 432 433
        """Get all quotas for resources associated with the service

        Keyword arguments:
        token   -- service's token (string)
434
        user    -- optionally, the uuid of a specific user
435 436

        In case of success return a dict of dicts of dicts with current quotas
437
        for all users, or of a specified user, if user argument is set.
438 439 440
        Otherwise raise an AstakosClientException

        """
441
        query = copy(API_SERVICE_QUOTAS)
442 443 444
        if user is not None:
            query += "?user=" + user
        return self._call_astakos(token, query)
445

446
    # ----------------------------------
447
    # do a POST to ``API_COMMISSIONS``
448 449 450 451
    def issue_commission(self, token, request):
        """Issue a commission

        Keyword arguments:
452
        token   -- service's token (string)
453 454 455 456 457 458 459
        request -- commision request (dict)

        In case of success return commission's id (int).
        Otherwise raise an AstakosClientException.

        """
        req_headers = {'content-type': 'application/json'}
460
        req_body = parse_request(request, self.logger)
461
        try:
462
            response = self._call_astakos(token, copy(API_COMMISSIONS),
463 464 465 466 467 468 469 470 471 472 473 474 475 476 477
                                          req_headers, req_body, "POST")
        except AstakosClientException as err:
            if err.status == 413:
                raise QuotaLimit(err.message, err.details)
            else:
                raise

        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)

478
    def issue_one_commission(self, token, holder, source, provisions,
479
                             name="", force=False, auto_accept=False):
480 481 482 483 484 485
        """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)
486
        provisions  -- resources with their quantity (dict from string to int)
487
        name        -- description of the commission (string)
488 489 490 491 492 493 494 495
        force       -- force this commission (boolean)
        auto_accept -- auto accept this commission (boolean)

        In case of success return commission's id (int).
        Otherwise raise an AstakosClientException.
        (See also issue_commission)

        """
496 497 498
        check_input("issue_one_commission", self.logger,
                    holder=holder, source=source,
                    provisions=provisions)
499 500 501 502

        request = {}
        request["force"] = force
        request["auto_accept"] = auto_accept
503
        request["name"] = name
504 505
        try:
            request["provisions"] = []
506
            for resource, quantity in provisions.iteritems():
507 508 509 510 511 512 513 514 515
                t = {"holder": holder, "source": source,
                     "resource": resource, "quantity": quantity}
                request["provisions"].append(t)
        except Exception as err:
            self.logger.error(str(err))
            raise BadValue(str(err))

        return self.issue_commission(token, request)

516
    # ----------------------------------
517
    # do a GET to ``API_COMMISSIONS``
518 519 520 521
    def get_pending_commissions(self, token):
        """Get Pending Commissions

        Keyword arguments:
522
        token   -- service's token (string)
523 524 525 526 527

        In case of success return a list of pending commissions' ids
        (list of integers)

        """
528
        return self._call_astakos(token, copy(API_COMMISSIONS))
529

530
    # ----------------------------------
531
    # do a GET to ``API_COMMISSIONS``/<serial>
532 533 534 535
    def get_commission_info(self, token, serial):
        """Get Description of a Commission

        Keyword arguments:
536
        token   -- service's token (string)
537 538 539
        serial  -- commission's id (int)

        In case of success return a dict of dicts containing
540
        informations (details) about the requested commission
541 542

        """
543
        check_input("get_commission_info", self.logger, serial=serial)
544

545
        path = API_COMMISSIONS + "/" + str(serial)
546 547
        return self._call_astakos(token, path)

548
    # ----------------------------------
549
    # do a POST to ``API_COMMISSIONS``/<serial>/action"
550 551
    def commission_action(self, token, serial, action):
        """Perform a commission action
552 553

        Keyword arguments:
554
        token   -- service's token (string)
555 556 557 558 559 560
        serial  -- commission's id (int)
        action  -- action to perform, currently accept/reject (string)

        In case of success return nothing.

        """
561 562
        check_input("commission_action", self.logger,
                    serial=serial, action=action)
563

564
        path = API_COMMISSIONS + "/" + str(serial) + "/action"
565
        req_headers = {'content-type': 'application/json'}
566
        req_body = parse_request({str(action): ""}, self.logger)
567 568
        self._call_astakos(token, path, req_headers, req_body, "POST")

569 570 571
    def accept_commission(self, token, serial):
        """Accept a commission (see commission_action)"""
        self.commission_action(token, serial, "accept")
572

573 574 575
    def reject_commission(self, token, serial):
        """Reject a commission (see commission_action)"""
        self.commission_action(token, serial, "reject")
576

577
    # ----------------------------------
578
    # do a POST to ``API_COMMISSIONS_ACTION``
579 580 581 582 583 584 585 586 587 588 589 590 591
    def resolve_commissions(self, token, 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)

        In case of success return a dict of dicts describing which
        commissions accepted, which rejected and which failed to
        resolved.

        """
592 593 594
        check_input("resolve_commissions", self.logger,
                    accept_serials=accept_serials,
                    reject_serials=reject_serials)
595

596
        path = copy(API_COMMISSIONS_ACTION)
597 598 599 600 601 602
        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")

603

604 605
# --------------------------------------------------------------------
# Private functions
606 607
# We want _doRequest to be a distinct function
# so that we can replace it during unit tests.
608
def _do_request(conn, method, url, **kwargs):
609 610 611 612 613 614
    """The actual request. This function can easily be mocked"""
    conn.request(method, url, **kwargs)
    response = conn.getresponse()
    length = response.getheader('content-length', None)
    data = response.read(length)
    status = int(response.status)
615 616
    message = response.reason
    return (message, data, status)