__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 61 62 63 64 65 66 67 68 69 70 71 72
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
73
KEYSTONE_PREFIX = get_path(astakos_services, 'astakos_keystone.prefix')
74 75
API_TOKENS = join_urls(KEYSTONE_PREFIX, "tokens")
TOKENS_ENDPOINTS = join_urls(API_TOKENS, "endpoints")
76 77


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        In case of success return nothing.

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

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

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

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

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

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

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

608

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