__init__.py 23.2 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
from .keypath import get_path
from .services import astakos_services


# Customize astakos_services here?
51 52


53 54 55 56
def join_urls(a, b):
    """join_urls from synnefo.lib"""
    return a.rstrip("/") + "/" + b.lstrip("/")

57 58
# --------------------------------------------------------------------
# Astakos API urls
59
ACCOUNTS_PREFIX = get_path(astakos_services, 'astakos_account.prefix')
60
ACCOUNTS_PREFIX = join_urls(ACCOUNTS_PREFIX, 'v1.0')
61 62 63 64 65 66 67 68 69 70 71 72 73
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
74 75
IDENTITY_PREFIX = get_path(astakos_services, 'astakos_identity.prefix')
API_TOKENS = join_urls(IDENTITY_PREFIX, "tokens")
76
TOKENS_ENDPOINTS = join_urls(API_TOKENS, "endpoints")
77 78


79
# --------------------------------------------------------------------
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
80
# Astakos Client Class
81

82
def get_token_from_cookie(request, cookie_name):
83 84 85 86 87 88 89 90 91 92 93 94 95 96
    """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
97 98 99 100
class AstakosClient():
    """AstakosClient Class Implementation"""

    # ----------------------------------
101 102
    def __init__(self, astakos_url, retry=0,
                 use_pool=False, pool_size=8, logger=None):
103
        """Initialize AstakosClient Class
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
104 105 106 107

        Keyword arguments:
        astakos_url -- i.e https://accounts.example.com (string)
        use_pool    -- use objpool for http requests (boolean)
108
        retry       -- how many time to retry (integer)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
109 110 111 112
        logger      -- pass a different logger

        """
        if logger is None:
113 114 115 116
            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
117
            logger = logging.getLogger("astakosclient")
118
        logger.debug("Intialize AstakosClient: astakos_url = %s, "
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
119 120
                     "use_pool = %s" % (astakos_url, use_pool))

121
        check_input("__init__", logger, astakos_url=astakos_url)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
122 123 124

        # Check for supported scheme
        p = urlparse.urlparse(astakos_url)
125
        conn_class = scheme_to_class(p.scheme, use_pool, pool_size)
126
        if conn_class is None:
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
127 128
            m = "Unsupported scheme: %s" % p.scheme
            logger.error(m)
129
            raise BadValue(m)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
130

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
131
        # Save astakos_url etc. in our class
132
        self.retry = retry
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
133 134 135
        self.logger = logger
        self.netloc = p.netloc
        self.scheme = p.scheme
136
        self.path = p.path.rstrip('/')
137
        self.conn_class = conn_class
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
138

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

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
155
        # Check Input
156 157 158 159
        if headers is None:
            headers = {}
        if body is None:
            body = {}
160
        path = self.path + "/" + request_path.strip('/')
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
161

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

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

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

        try:
            if data:
                return simplejson.loads(unicode(data))
            else:
201
                return None
202
        except Exception as err:
203 204
            self.logger.error("Cannot parse response \"%s\" with simplejson: %s"
                              % (data, str(err)))
205
            raise InvalidResponse(str(err), data)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
206 207

    # ------------------------
208
    # do a GET to ``API_AUTHENTICATE``
209
    def get_user_info(self, token, usage=False):
210
        """Authenticate user and get user's info as a dictionary
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
211 212

        Keyword arguments:
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
213
        token   -- user's token (string)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
214 215 216 217 218 219
        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
220
        # Send request
221
        auth_path = copy(API_AUTHENTICATE)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
222 223
        if usage:
            auth_path += "?usage=1"
224
        return self._call_astakos(token, auth_path)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
225 226

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

242
    def get_usernames(self, token, uuids):
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
243 244 245
        """Return a uuid_catalog dictionary for the given uuids

        Keyword arguments:
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
246
        token   -- user's token (string)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
247 248 249 250 251 252
        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

        """
253
        req_path = copy(API_USERCATALOGS)
254
        return self._uuid_catalog(token, uuids, req_path)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
255

256 257
    def get_username(self, token, uuid):
        """Return the user name of a uuid (see get_usernames)"""
258
        check_input("get_username", self.logger, uuid=uuid)
259
        uuid_dict = self.get_usernames(token, [uuid])
260 261 262
        if uuid in uuid_dict:
            return uuid_dict.get(uuid)
        else:
263
            raise NoUserName(uuid)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
264

265
    def service_get_usernames(self, token, uuids):
266
        """Return a uuid_catalog dict using a service's token"""
267
        req_path = copy(API_SERVICE_USERCATALOGS)
268
        return self._uuid_catalog(token, uuids, req_path)
269

270
    def service_get_username(self, token, uuid):
271
        """Return the displayName of a uuid using a service's token"""
272
        check_input("service_get_username", self.logger, uuid=uuid)
273
        uuid_dict = self.service_get_usernames(token, [uuid])
274 275 276
        if uuid in uuid_dict:
            return uuid_dict.get(uuid)
        else:
277
            raise NoUserName(uuid)
278

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

295
    def get_uuids(self, token, display_names):
296 297 298 299 300 301 302 303 304 305
        """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

        """
306
        req_path = copy(API_USERCATALOGS)
307
        return self._displayname_catalog(token, display_names, req_path)
308

309
    def get_uuid(self, token, display_name):
310
        """Return the uuid of a name (see getUUIDs)"""
311
        check_input("get_uuid", self.logger, display_name=display_name)
312
        name_dict = self.get_uuids(token, [display_name])
313 314 315 316
        if display_name in name_dict:
            return name_dict.get(display_name)
        else:
            raise NoUUID(display_name)
317

318
    def service_get_uuids(self, token, display_names):
319
        """Return a display_name catalog using a service's token"""
320
        req_path = copy(API_SERVICE_USERCATALOGS)
321
        return self._displayname_catalog(token, display_names, req_path)
322

323
    def service_get_uuid(self, token, display_name):
324
        """Return the uuid of a name using a service's token"""
325
        check_input("service_get_uuid", self.logger, display_name=display_name)
326
        name_dict = self.service_get_uuids(token, [display_name])
327 328 329 330
        if display_name in name_dict:
            return name_dict.get(display_name)
        else:
            raise NoUUID(display_name)
331

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
332
    # ----------------------------------
333
    # do a GET to ``API_GETSERVICES``
334
    def get_services(self):
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
335
        """Return a list of dicts with the registered services"""
336
        return self._call_astakos(None, copy(API_GETSERVICES))
337 338

    # ----------------------------------
339
    # do a GET to ``API_RESOURCES``
340 341
    def get_resources(self):
        """Return a dict of dicts with the available resources"""
342
        return self._call_astakos(None, copy(API_RESOURCES))
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
343

344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361
    # ----------------------------------
    # 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})
362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380
        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).
381 382
        Avoid the use of get_endpoints method and use
        get_user_info_with_endpoints instead.
383 384 385 386 387 388 389 390 391 392 393 394

        """
        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)
395

396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418
    # ----------------------------------
    # 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)

419
    # ----------------------------------
420
    # do a GET to ``API_QUOTAS``
421
    def get_quotas(self, token):
422 423 424 425 426 427 428 429 430
        """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

        """
431
        return self._call_astakos(token, copy(API_QUOTAS))
432

433
    # ----------------------------------
434
    # do a GET to ``API_SERVICE_QUOTAS``
435
    def service_get_quotas(self, token, user=None):
436 437 438 439
        """Get all quotas for resources associated with the service

        Keyword arguments:
        token   -- service's token (string)
440
        user    -- optionally, the uuid of a specific user
441 442

        In case of success return a dict of dicts of dicts with current quotas
443
        for all users, or of a specified user, if user argument is set.
444 445 446
        Otherwise raise an AstakosClientException

        """
447
        query = copy(API_SERVICE_QUOTAS)
448 449 450
        if user is not None:
            query += "?user=" + user
        return self._call_astakos(token, query)
451

452
    # ----------------------------------
453
    # do a POST to ``API_COMMISSIONS``
454 455 456 457
    def issue_commission(self, token, request):
        """Issue a commission

        Keyword arguments:
458
        token   -- service's token (string)
459 460 461 462 463 464 465
        request -- commision request (dict)

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

        """
        req_headers = {'content-type': 'application/json'}
466
        req_body = parse_request(request, self.logger)
467
        try:
468
            response = self._call_astakos(token, copy(API_COMMISSIONS),
469 470 471 472 473 474 475 476 477 478 479 480 481 482 483
                                          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)

484
    def issue_one_commission(self, token, holder, source, provisions,
485
                             name="", force=False, auto_accept=False):
486 487 488 489 490 491
        """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)
492
        provisions  -- resources with their quantity (dict from string to int)
493
        name        -- description of the commission (string)
494 495 496 497 498 499 500 501
        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)

        """
502 503 504
        check_input("issue_one_commission", self.logger,
                    holder=holder, source=source,
                    provisions=provisions)
505 506 507 508

        request = {}
        request["force"] = force
        request["auto_accept"] = auto_accept
509
        request["name"] = name
510 511
        try:
            request["provisions"] = []
512
            for resource, quantity in provisions.iteritems():
513 514 515 516 517 518 519 520 521
                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)

522
    # ----------------------------------
523
    # do a GET to ``API_COMMISSIONS``
524 525 526 527
    def get_pending_commissions(self, token):
        """Get Pending Commissions

        Keyword arguments:
528
        token   -- service's token (string)
529 530 531 532 533

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

        """
534
        return self._call_astakos(token, copy(API_COMMISSIONS))
535

536
    # ----------------------------------
537
    # do a GET to ``API_COMMISSIONS``/<serial>
538 539 540 541
    def get_commission_info(self, token, serial):
        """Get Description of a Commission

        Keyword arguments:
542
        token   -- service's token (string)
543 544 545
        serial  -- commission's id (int)

        In case of success return a dict of dicts containing
546
        informations (details) about the requested commission
547 548

        """
549
        check_input("get_commission_info", self.logger, serial=serial)
550

551
        path = API_COMMISSIONS + "/" + str(serial)
552 553
        return self._call_astakos(token, path)

554
    # ----------------------------------
555
    # do a POST to ``API_COMMISSIONS``/<serial>/action"
556 557
    def commission_action(self, token, serial, action):
        """Perform a commission action
558 559

        Keyword arguments:
560
        token   -- service's token (string)
561 562 563 564 565 566
        serial  -- commission's id (int)
        action  -- action to perform, currently accept/reject (string)

        In case of success return nothing.

        """
567 568
        check_input("commission_action", self.logger,
                    serial=serial, action=action)
569

570
        path = API_COMMISSIONS + "/" + str(serial) + "/action"
571
        req_headers = {'content-type': 'application/json'}
572
        req_body = parse_request({str(action): ""}, self.logger)
573 574
        self._call_astakos(token, path, req_headers, req_body, "POST")

575 576 577
    def accept_commission(self, token, serial):
        """Accept a commission (see commission_action)"""
        self.commission_action(token, serial, "accept")
578

579 580 581
    def reject_commission(self, token, serial):
        """Reject a commission (see commission_action)"""
        self.commission_action(token, serial, "reject")
582

583
    # ----------------------------------
584
    # do a POST to ``API_COMMISSIONS_ACTION``
585 586 587 588 589 590 591 592 593 594 595 596 597
    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.

        """
598 599 600
        check_input("resolve_commissions", self.logger,
                    accept_serials=accept_serials,
                    reject_serials=reject_serials)
601

602
        path = copy(API_COMMISSIONS_ACTION)
603 604 605 606 607 608
        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")

609

610 611
# --------------------------------------------------------------------
# Private functions
612 613
# We want _doRequest to be a distinct function
# so that we can replace it during unit tests.
614
def _do_request(conn, method, url, **kwargs):
615 616 617 618 619 620
    """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)
621 622
    message = response.reason
    return (message, data, status)