__init__.py 22.5 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 52 53 54 55 56 57 58
# --------------------------------------------------------------------
# Astakos API urls
API_AUTHENTICATE = "/astakos/api/authenticate"
API_USERCATALOGS = "/astakos/api/user_catalogs"
API_SERVICE_USERCATALOGS = "/astakos/api/service/user_catalogs"
API_GETSERVICES = "/astakos/api/get_services"
API_RESOURCES = "/astakos/api/resources"
API_QUOTAS = "/astakos/api/quotas"
API_SERVICE_QUOTAS = "/astakos/api/service_quotas"
API_COMMISSIONS = "/astakos/api/commissions"
API_COMMISSIONS_ACTION = API_COMMISSIONS + "/action"
59
API_FEEDBACK = "/astakos/api/feedback"
60 61
API_TOKENS = "/astakos/api/tokens"
TOKENS_ENDPOINTS = "endpoints"
62 63


64
# --------------------------------------------------------------------
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
65
# Astakos Client Class
66

67
def get_token_from_cookie(request, cookie_name):
68 69 70 71 72 73 74 75 76 77 78 79 80 81
    """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
82 83 84 85
class AstakosClient():
    """AstakosClient Class Implementation"""

    # ----------------------------------
86 87
    def __init__(self, astakos_url, retry=0,
                 use_pool=False, pool_size=8, logger=None):
88
        """Initialize AstakosClient Class
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
89 90 91 92

        Keyword arguments:
        astakos_url -- i.e https://accounts.example.com (string)
        use_pool    -- use objpool for http requests (boolean)
93
        retry       -- how many time to retry (integer)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
94 95 96 97
        logger      -- pass a different logger

        """
        if logger is None:
98 99 100 101
            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
102
            logger = logging.getLogger("astakosclient")
103
        logger.debug("Intialize AstakosClient: astakos_url = %s, "
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
104 105
                     "use_pool = %s" % (astakos_url, use_pool))

106
        check_input("__init__", logger, astakos_url=astakos_url)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
107 108 109

        # Check for supported scheme
        p = urlparse.urlparse(astakos_url)
110
        conn_class = scheme_to_class(p.scheme, use_pool, pool_size)
111
        if conn_class is None:
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
112 113
            m = "Unsupported scheme: %s" % p.scheme
            logger.error(m)
114
            raise BadValue(m)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
115

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
116
        # Save astakos_url etc. in our class
117
        self.retry = retry
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
118 119 120
        self.logger = logger
        self.netloc = p.netloc
        self.scheme = p.scheme
121
        self.path = p.path.rstrip('/')
122
        self.conn_class = conn_class
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
123

124 125
    # ----------------------------------
    @retry
126 127
    def _call_astakos(self, token, request_path, headers=None,
                      body=None, method="GET", log_body=True):
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
128
        """Make the actual call to Astakos Service"""
129 130 131 132 133 134
        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
135
        self.logger.debug(
136
            "Make a %s request to %s %s with headers %s and body %s"
137 138
            % (method, request_path, using_token, headers,
               body if log_body else "(not logged)"))
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
139

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
140
        # Check Input
141 142 143 144
        if headers is None:
            headers = {}
        if body is None:
            body = {}
145
        path = self.path + "/" + request_path.strip('/')
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
146

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
147 148
        # Build request's header and body
        kwargs = {}
149
        kwargs['headers'] = copy(headers)
150 151
        if token is not None:
            kwargs['headers']['X-Auth-Token'] = token
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
152
        if body:
153
            kwargs['body'] = copy(body)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
154 155 156 157 158 159
            kwargs['headers'].setdefault(
                'content-type', 'application/octet-stream')
        kwargs['headers'].setdefault('content-length',
                                     len(body) if body else 0)

        try:
160 161 162
            # Get the connection object
            with self.conn_class(self.netloc) as conn:
                # Send request
163
                (message, data, status) = \
164
                    _do_request(conn, method, path, **kwargs)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
165
        except Exception as err:
166
            self.logger.error("Failed to send request: %s" % repr(err))
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
167 168 169 170
            raise AstakosClientException(str(err))

        # Return
        self.logger.debug("Request returned with status %s" % status)
171
        if status == 400:
172
            raise BadRequest(message, data)
173
        elif status == 401:
174
            raise Unauthorized(message, data)
175
        elif status == 403:
176
            raise Forbidden(message, data)
177
        elif status == 404:
178
            raise NotFound(message, data)
179
        elif status < 200 or status >= 300:
180
            raise AstakosClientException(message, data, status)
181 182 183 184 185

        try:
            if data:
                return simplejson.loads(unicode(data))
            else:
186
                return None
187
        except Exception as err:
188 189
            self.logger.error("Cannot parse response \"%s\" with simplejson: %s"
                              % (data, str(err)))
190
            raise InvalidResponse(str(err), data)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
191 192

    # ------------------------
193
    # do a GET to ``API_AUTHENTICATE``
194
    def get_user_info(self, token, usage=False):
195
        """Authenticate user and get user's info as a dictionary
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
196 197

        Keyword arguments:
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
198
        token   -- user's token (string)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
199 200 201 202 203 204
        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
205
        # Send request
206
        auth_path = copy(API_AUTHENTICATE)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
207 208
        if usage:
            auth_path += "?usage=1"
209
        return self._call_astakos(token, auth_path)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
210 211

    # ----------------------------------
212
    # do a POST to ``API_USERCATALOGS`` (or ``API_SERVICE_USERCATALOGS``)
213
    #   with {'uuids': uuids}
214
    def _uuid_catalog(self, token, uuids, req_path):
215
        req_headers = {'content-type': 'application/json'}
216
        req_body = parse_request({'uuids': uuids}, self.logger)
217
        data = self._call_astakos(
218
            token, req_path, req_headers, req_body, "POST")
219 220 221
        if "uuid_catalog" in data:
            return data.get("uuid_catalog")
        else:
222
            m = "_uuid_catalog request returned %s. No uuid_catalog found" \
223 224 225
                % data
            self.logger.error(m)
            raise AstakosClientException(m)
226

227
    def get_usernames(self, token, uuids):
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
228 229 230
        """Return a uuid_catalog dictionary for the given uuids

        Keyword arguments:
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
231
        token   -- user's token (string)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
232 233 234 235 236 237
        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

        """
238
        req_path = copy(API_USERCATALOGS)
239
        return self._uuid_catalog(token, uuids, req_path)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
240

241 242
    def get_username(self, token, uuid):
        """Return the user name of a uuid (see get_usernames)"""
243
        check_input("get_username", self.logger, uuid=uuid)
244
        uuid_dict = self.get_usernames(token, [uuid])
245 246 247
        if uuid in uuid_dict:
            return uuid_dict.get(uuid)
        else:
248
            raise NoUserName(uuid)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
249

250
    def service_get_usernames(self, token, uuids):
251
        """Return a uuid_catalog dict using a service's token"""
252
        req_path = copy(API_SERVICE_USERCATALOGS)
253
        return self._uuid_catalog(token, uuids, req_path)
254

255
    def service_get_username(self, token, uuid):
256
        """Return the displayName of a uuid using a service's token"""
257
        check_input("service_get_username", self.logger, uuid=uuid)
258
        uuid_dict = self.service_get_usernames(token, [uuid])
259 260 261
        if uuid in uuid_dict:
            return uuid_dict.get(uuid)
        else:
262
            raise NoUserName(uuid)
263

264
    # ----------------------------------
265
    # do a POST to ``API_USERCATALOGS`` (or ``API_SERVICE_USERCATALOGS``)
266
    #   with {'displaynames': display_names}
267
    def _displayname_catalog(self, token, display_names, req_path):
268
        req_headers = {'content-type': 'application/json'}
269
        req_body = parse_request({'displaynames': display_names}, self.logger)
270
        data = self._call_astakos(
271
            token, req_path, req_headers, req_body, "POST")
272 273 274
        if "displayname_catalog" in data:
            return data.get("displayname_catalog")
        else:
275
            m = "_displayname_catalog request returned %s. " \
276 277 278
                "No displayname_catalog found" % data
            self.logger.error(m)
            raise AstakosClientException(m)
279

280
    def get_uuids(self, token, display_names):
281 282 283 284 285 286 287 288 289 290
        """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

        """
291
        req_path = copy(API_USERCATALOGS)
292
        return self._displayname_catalog(token, display_names, req_path)
293

294
    def get_uuid(self, token, display_name):
295
        """Return the uuid of a name (see getUUIDs)"""
296
        check_input("get_uuid", self.logger, display_name=display_name)
297
        name_dict = self.get_uuids(token, [display_name])
298 299 300 301
        if display_name in name_dict:
            return name_dict.get(display_name)
        else:
            raise NoUUID(display_name)
302

303
    def service_get_uuids(self, token, display_names):
304
        """Return a display_name catalog using a service's token"""
305
        req_path = copy(API_SERVICE_USERCATALOGS)
306
        return self._displayname_catalog(token, display_names, req_path)
307

308
    def service_get_uuid(self, token, display_name):
309
        """Return the uuid of a name using a service's token"""
310
        check_input("service_get_uuid", self.logger, display_name=display_name)
311
        name_dict = self.service_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

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
317
    # ----------------------------------
318
    # do a GET to ``API_GETSERVICES``
319
    def get_services(self):
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
320
        """Return a list of dicts with the registered services"""
321
        return self._call_astakos(None, copy(API_GETSERVICES))
322 323

    # ----------------------------------
324
    # do a GET to ``API_RESOURCES``
325 326
    def get_resources(self):
        """Return a dict of dicts with the available resources"""
327
        return self._call_astakos(None, copy(API_RESOURCES))
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
328

329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346
    # ----------------------------------
    # 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})
347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365
        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).
366 367
        Avoid the use of get_endpoints method and use
        get_user_info_with_endpoints instead.
368 369 370 371 372 373 374 375 376 377 378 379

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

381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403
    # ----------------------------------
    # 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)

404
    # ----------------------------------
405
    # do a GET to ``API_QUOTAS``
406
    def get_quotas(self, token):
407 408 409 410 411 412 413 414 415
        """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

        """
416
        return self._call_astakos(token, copy(API_QUOTAS))
417

418
    # ----------------------------------
419
    # do a GET to ``API_SERVICE_QUOTAS``
420
    def service_get_quotas(self, token, user=None):
421 422 423 424
        """Get all quotas for resources associated with the service

        Keyword arguments:
        token   -- service's token (string)
425
        user    -- optionally, the uuid of a specific user
426 427

        In case of success return a dict of dicts of dicts with current quotas
428
        for all users, or of a specified user, if user argument is set.
429 430 431
        Otherwise raise an AstakosClientException

        """
432
        query = copy(API_SERVICE_QUOTAS)
433 434 435
        if user is not None:
            query += "?user=" + user
        return self._call_astakos(token, query)
436

437
    # ----------------------------------
438
    # do a POST to ``API_COMMISSIONS``
439 440 441 442
    def issue_commission(self, token, request):
        """Issue a commission

        Keyword arguments:
443
        token   -- service's token (string)
444 445 446 447 448 449 450
        request -- commision request (dict)

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

        """
        req_headers = {'content-type': 'application/json'}
451
        req_body = parse_request(request, self.logger)
452
        try:
453
            response = self._call_astakos(token, copy(API_COMMISSIONS),
454 455 456 457 458 459 460 461 462 463 464 465 466 467 468
                                          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)

469
    def issue_one_commission(self, token, holder, source, provisions,
470
                             name="", force=False, auto_accept=False):
471 472 473 474 475 476
        """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)
477
        provisions  -- resources with their quantity (dict from string to int)
478
        name        -- description of the commission (string)
479 480 481 482 483 484 485 486
        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)

        """
487 488 489
        check_input("issue_one_commission", self.logger,
                    holder=holder, source=source,
                    provisions=provisions)
490 491 492 493

        request = {}
        request["force"] = force
        request["auto_accept"] = auto_accept
494
        request["name"] = name
495 496
        try:
            request["provisions"] = []
497
            for resource, quantity in provisions.iteritems():
498 499 500 501 502 503 504 505 506
                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)

507
    # ----------------------------------
508
    # do a GET to ``API_COMMISSIONS``
509 510 511 512
    def get_pending_commissions(self, token):
        """Get Pending Commissions

        Keyword arguments:
513
        token   -- service's token (string)
514 515 516 517 518

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

        """
519
        return self._call_astakos(token, copy(API_COMMISSIONS))
520

521
    # ----------------------------------
522
    # do a GET to ``API_COMMISSIONS``/<serial>
523 524 525 526
    def get_commission_info(self, token, serial):
        """Get Description of a Commission

        Keyword arguments:
527
        token   -- service's token (string)
528 529 530
        serial  -- commission's id (int)

        In case of success return a dict of dicts containing
531
        informations (details) about the requested commission
532 533

        """
534
        check_input("get_commission_info", self.logger, serial=serial)
535

536
        path = API_COMMISSIONS + "/" + str(serial)
537 538
        return self._call_astakos(token, path)

539
    # ----------------------------------
540
    # do a POST to ``API_COMMISSIONS``/<serial>/action"
541 542
    def commission_action(self, token, serial, action):
        """Perform a commission action
543 544

        Keyword arguments:
545
        token   -- service's token (string)
546 547 548 549 550 551
        serial  -- commission's id (int)
        action  -- action to perform, currently accept/reject (string)

        In case of success return nothing.

        """
552 553
        check_input("commission_action", self.logger,
                    serial=serial, action=action)
554

555
        path = API_COMMISSIONS + "/" + str(serial) + "/action"
556
        req_headers = {'content-type': 'application/json'}
557
        req_body = parse_request({str(action): ""}, self.logger)
558 559
        self._call_astakos(token, path, req_headers, req_body, "POST")

560 561 562
    def accept_commission(self, token, serial):
        """Accept a commission (see commission_action)"""
        self.commission_action(token, serial, "accept")
563

564 565 566
    def reject_commission(self, token, serial):
        """Reject a commission (see commission_action)"""
        self.commission_action(token, serial, "reject")
567

568
    # ----------------------------------
569
    # do a POST to ``API_COMMISSIONS_ACTION``
570 571 572 573 574 575 576 577 578 579 580 581 582
    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.

        """
583 584 585
        check_input("resolve_commissions", self.logger,
                    accept_serials=accept_serials,
                    reject_serials=reject_serials)
586

587
        path = copy(API_COMMISSIONS_ACTION)
588 589 590 591 592 593
        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")

594

595 596
# --------------------------------------------------------------------
# Private functions
597 598
# We want _doRequest to be a distinct function
# so that we can replace it during unit tests.
599
def _do_request(conn, method, url, **kwargs):
600 601 602 603 604 605
    """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)
606 607
    message = response.reason
    return (message, data, status)